feat(enrollment): add route-based IP selection and fix package versioning for v1.1.5
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1916,7 +1916,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "0.3.12"
|
version = "1.1.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "0.3.12"
|
version = "1.1.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Echo <echo@moon-dragon.us>"]
|
authors = ["Echo <echo@moon-dragon.us>"]
|
||||||
description = "Secure remote package management API for Linux systems"
|
description = "Secure remote package management API for Linux systems"
|
||||||
|
|||||||
@ -64,7 +64,7 @@ WORKSPACE_DIR=/home/builduser
|
|||||||
echo "Creating APKBUILD..."
|
echo "Creating APKBUILD..."
|
||||||
cat > APKBUILD << EOF
|
cat > APKBUILD << EOF
|
||||||
pkgname=linux-patch-api
|
pkgname=linux-patch-api
|
||||||
pkgver=1.0.0
|
pkgver=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Secure remote package management API for Linux systems"
|
pkgdesc="Secure remote package management API for Linux systems"
|
||||||
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
||||||
|
|||||||
@ -40,7 +40,7 @@ cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml
|
|||||||
echo "Creating PKGBUILD..."
|
echo "Creating PKGBUILD..."
|
||||||
cat > PKGBUILD << 'EOF'
|
cat > PKGBUILD << 'EOF'
|
||||||
pkgname=linux-patch-api
|
pkgname=linux-patch-api
|
||||||
pkgver=1.0.0
|
pkgver=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Secure remote package management API for Linux systems"
|
pkgdesc="Secure remote package management API for Linux systems"
|
||||||
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
url="https://gitea.moon-dragon.us/echo/linux_patch_api"
|
||||||
|
|||||||
2
build-rpm.sh
Executable file → Normal file
2
build-rpm.sh
Executable file → Normal file
@ -26,7 +26,7 @@ mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
|||||||
|
|
||||||
# Create source tarball (required by %autosetup in spec file)
|
# Create source tarball (required by %autosetup in spec file)
|
||||||
echo "Creating source tarball..."
|
echo "Creating source tarball..."
|
||||||
VERSION="1.0.0"
|
VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*=.*"\([^"]*\)".*/\1/')
|
||||||
TMPDIR=$(mktemp -d)
|
TMPDIR=$(mktemp -d)
|
||||||
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}"
|
mkdir -p "$TMPDIR/linux-patch-api-${VERSION}"
|
||||||
# Copy files excluding unwanted directories using find
|
# Copy files excluding unwanted directories using find
|
||||||
|
|||||||
@ -62,6 +62,12 @@ package_manager:
|
|||||||
# # Example: "eth0", "ens192", "enp0s3"
|
# # Example: "eth0", "ens192", "enp0s3"
|
||||||
# report_interface: "eth0"
|
# report_interface: "eth0"
|
||||||
# # Explicit IPv4 address reported to the manager.
|
# # Explicit IPv4 address reported to the manager.
|
||||||
# # Highest priority — overrides both report_interface and auto-detect.
|
# # Highest priority — overrides both report_interface and route-based selection.
|
||||||
# # Useful when the host has multiple IPs or runs inside a container.
|
# # Useful when the host has multiple IPs or runs inside a container.
|
||||||
# report_ip: "192.168.3.36"
|
# report_ip: "192.168.3.36"
|
||||||
|
# # Route-based IP selection is enabled by default when manager_url is set.
|
||||||
|
# The agent resolves the manager hostname to an IP, then uses `ip route get <manager_ip>`
|
||||||
|
# to determine which local source IP the kernel would use to reach the manager.
|
||||||
|
# This is the most accurate method for multi-homed hosts because it queries
|
||||||
|
# the kernel routing table directly.
|
||||||
|
# Priority order: report_ip > report_interface > route-based > auto-detect
|
||||||
|
|||||||
10
debian/changelog
vendored
10
debian/changelog
vendored
@ -1,3 +1,13 @@
|
|||||||
|
linux-patch-api (1.1.5-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* Fix enrollment IP detection: filter Docker bridge subnets (172.16.0.0/12)
|
||||||
|
* Fix enrollment IP detection: filter link-local addresses (169.254.0.0/16)
|
||||||
|
* Add report_interface and report_ip config options for explicit IP override
|
||||||
|
* Add route-based IP selection using kernel routing table
|
||||||
|
* Fix package versioning to derive from Cargo.toml
|
||||||
|
|
||||||
|
-- Echo <echo@moon-dragon.us> Sun, 18 May 2026 02:00:00 -0500
|
||||||
|
|
||||||
linux-patch-api (0.3.12-1) unstable; urgency=low
|
linux-patch-api (0.3.12-1) unstable; urgency=low
|
||||||
|
|
||||||
* Fix socket activation detection to use resolved service name
|
* Fix socket activation detection to use resolved service name
|
||||||
|
|||||||
@ -103,7 +103,8 @@ impl EnrollmentClient {
|
|||||||
/// See [`identity::get_primary_ip`] for resolution priority:
|
/// See [`identity::get_primary_ip`] for resolution priority:
|
||||||
/// 1. `report_ip` — explicit IP (highest priority)
|
/// 1. `report_ip` — explicit IP (highest priority)
|
||||||
/// 2. `report_interface` — IP from named interface
|
/// 2. `report_interface` — IP from named interface
|
||||||
/// 3. Auto-detect — first routable IP (container bridge subnets filtered)
|
/// 3. Route-based — IP from kernel routing table for reaching the manager
|
||||||
|
/// 4. Auto-detect — first routable IP (container bridge subnets filtered)
|
||||||
pub fn with_ip_overrides(
|
pub fn with_ip_overrides(
|
||||||
manager_url: &str,
|
manager_url: &str,
|
||||||
report_interface: Option<String>,
|
report_interface: Option<String>,
|
||||||
@ -202,7 +203,10 @@ impl EnrollmentClient {
|
|||||||
/// - `Ok(EnrollmentResponse)` with the polling token on HTTP 202
|
/// - `Ok(EnrollmentResponse)` with the polling token on HTTP 202
|
||||||
/// - Error on 429 (rate limited), 5xx (server error), or network failure
|
/// - Error on 429 (rate limited), 5xx (server error), or network failure
|
||||||
pub async fn register(&self) -> Result<EnrollmentResponse> {
|
pub async fn register(&self) -> Result<EnrollmentResponse> {
|
||||||
// 1. Collect identity data
|
// 1. Resolve manager IP for route-based IP selection
|
||||||
|
let route_target = self.manager_ip().await.ok();
|
||||||
|
|
||||||
|
// 2. Collect identity data
|
||||||
let machine_id = identity::get_machine_id()
|
let machine_id = identity::get_machine_id()
|
||||||
.context("Failed to read machine-id — host cannot enroll without identity")?;
|
.context("Failed to read machine-id — host cannot enroll without identity")?;
|
||||||
let fqdn = identity::get_fqdn()
|
let fqdn = identity::get_fqdn()
|
||||||
@ -210,6 +214,7 @@ impl EnrollmentClient {
|
|||||||
let ip_address = identity::get_primary_ip(
|
let ip_address = identity::get_primary_ip(
|
||||||
self.report_interface.as_deref(),
|
self.report_interface.as_deref(),
|
||||||
self.report_ip.as_deref(),
|
self.report_ip.as_deref(),
|
||||||
|
route_target.as_deref(),
|
||||||
)
|
)
|
||||||
.context("Failed to determine reportable IP address — check network configuration or set report_interface/report_ip in config")?;
|
.context("Failed to determine reportable IP address — check network configuration or set report_interface/report_ip in config")?;
|
||||||
let os_details = identity::get_os_details()
|
let os_details = identity::get_os_details()
|
||||||
|
|||||||
@ -123,6 +123,60 @@ pub fn is_link_local(addr: &Ipv4Addr) -> bool {
|
|||||||
octets[0] == 169 && octets[1] == 254
|
octets[0] == 169 && octets[1] == 254
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine the local source IP that would be used to reach a target IP.
|
||||||
|
/// Uses the kernel routing table via `ip route get <target>`.
|
||||||
|
///
|
||||||
|
/// This is the most accurate way to select the correct local IP because it
|
||||||
|
/// queries the kernel routing table directly, which accounts for all routing
|
||||||
|
/// rules, interface priorities, and source address selection.
|
||||||
|
pub fn get_route_source_ip(target_ip: &str) -> Result<String> {
|
||||||
|
let output = Command::new("ip")
|
||||||
|
.args(["route", "get", target_ip])
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute 'ip route get' — is iproute2 installed?")?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
return Err(anyhow!(
|
||||||
|
"'ip route get {}' failed: {}",
|
||||||
|
target_ip,
|
||||||
|
stderr.trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
|
||||||
|
// Parse output like: "192.168.3.36 via 192.168.1.1 dev eth0 src 192.168.3.36 uid ..."
|
||||||
|
// We want the 'src' field value
|
||||||
|
let mut found_src = false;
|
||||||
|
for part in stdout.split_whitespace() {
|
||||||
|
if found_src {
|
||||||
|
// Validate it's a valid IPv4 address
|
||||||
|
if part.parse::<Ipv4Addr>().is_ok() {
|
||||||
|
let addr = part.parse::<Ipv4Addr>().unwrap();
|
||||||
|
if !addr.is_loopback() && !is_container_bridge(&addr) && !is_link_local(&addr) {
|
||||||
|
tracing::info!(
|
||||||
|
target_ip = target_ip,
|
||||||
|
source_ip = part,
|
||||||
|
"Route-based IP selection: local source IP for reaching target"
|
||||||
|
);
|
||||||
|
return Ok(part.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if part == "src" {
|
||||||
|
found_src = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(anyhow!(
|
||||||
|
"Could not determine source IP for route to '{}' — 'ip route get' output: {}",
|
||||||
|
target_ip,
|
||||||
|
stdout.trim()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the IPv4 address of a specific network interface by name.
|
/// Get the IPv4 address of a specific network interface by name.
|
||||||
///
|
///
|
||||||
/// Returns the first non-loopback IPv4 address on the named interface.
|
/// Returns the first non-loopback IPv4 address on the named interface.
|
||||||
@ -158,8 +212,13 @@ pub fn get_ip_for_interface(interface_name: &str) -> Result<String> {
|
|||||||
/// Resolution priority:
|
/// Resolution priority:
|
||||||
/// 1. `report_ip` — explicit IP from config (highest priority)
|
/// 1. `report_ip` — explicit IP from config (highest priority)
|
||||||
/// 2. `report_interface` — IP from a named interface
|
/// 2. `report_interface` — IP from a named interface
|
||||||
/// 3. Auto-detect — first IP from `get_ip_addresses()` (bridge subnets already filtered)
|
/// 3. `route_target` — route-based selection using kernel routing table
|
||||||
pub fn get_primary_ip(report_interface: Option<&str>, report_ip: Option<&str>) -> Result<String> {
|
/// 4. Auto-detect — first IP from `get_ip_addresses()` (bridge subnets already filtered)
|
||||||
|
pub fn get_primary_ip(
|
||||||
|
report_interface: Option<&str>,
|
||||||
|
report_ip: Option<&str>,
|
||||||
|
route_target: Option<&str>,
|
||||||
|
) -> Result<String> {
|
||||||
// Priority 1: Explicit IP override
|
// Priority 1: Explicit IP override
|
||||||
if let Some(ip) = report_ip {
|
if let Some(ip) = report_ip {
|
||||||
// Validate it parses as IPv4
|
// Validate it parses as IPv4
|
||||||
@ -188,13 +247,34 @@ pub fn get_primary_ip(report_interface: Option<&str>, report_ip: Option<&str>) -
|
|||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
interface = iface,
|
interface = iface,
|
||||||
error = %e,
|
error = %e,
|
||||||
"Configured report_interface lookup failed — falling back to auto-detect"
|
"Configured report_interface lookup failed — falling back to route-based or auto-detect"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority 3: Auto-detect (bridge subnets already filtered by get_ip_addresses)
|
// Priority 3: Route-based selection using kernel routing table
|
||||||
|
if let Some(target) = route_target {
|
||||||
|
match get_route_source_ip(target) {
|
||||||
|
Ok(ip) => {
|
||||||
|
tracing::info!(
|
||||||
|
target = target,
|
||||||
|
ip = %ip,
|
||||||
|
"Using route-based IP selection for target"
|
||||||
|
);
|
||||||
|
return Ok(ip);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target = target,
|
||||||
|
error = %e,
|
||||||
|
"Route-based IP selection failed — falling back to auto-detect"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 4: Auto-detect (bridge subnets already filtered by get_ip_addresses)
|
||||||
let addrs = get_ip_addresses()?;
|
let addrs = get_ip_addresses()?;
|
||||||
addrs
|
addrs
|
||||||
.first()
|
.first()
|
||||||
@ -368,7 +448,7 @@ mod tests {
|
|||||||
fn test_get_primary_ip_auto_detect() {
|
fn test_get_primary_ip_auto_detect() {
|
||||||
// Without overrides, should return a valid non-bridge IP
|
// Without overrides, should return a valid non-bridge IP
|
||||||
// In Docker containers, auto-detect may find no routable IPs — that's valid
|
// In Docker containers, auto-detect may find no routable IPs — that's valid
|
||||||
match get_primary_ip(None, None) {
|
match get_primary_ip(None, None, None) {
|
||||||
Ok(ip) => {
|
Ok(ip) => {
|
||||||
assert!(!ip.is_empty(), "Primary IP should not be empty");
|
assert!(!ip.is_empty(), "Primary IP should not be empty");
|
||||||
let parsed: Ipv4Addr = ip.parse().expect("Primary IP should be valid IPv4");
|
let parsed: Ipv4Addr = ip.parse().expect("Primary IP should be valid IPv4");
|
||||||
@ -386,7 +466,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_get_primary_ip_explicit_override() {
|
fn test_get_primary_ip_explicit_override() {
|
||||||
// Explicit IP should be returned as-is
|
// Explicit IP should be returned as-is
|
||||||
let ip = get_primary_ip(None, Some("10.99.99.1")).expect("Failed with explicit IP");
|
let ip = get_primary_ip(None, Some("10.99.99.1"), None).expect("Failed with explicit IP");
|
||||||
assert_eq!(ip, "10.99.99.1");
|
assert_eq!(ip, "10.99.99.1");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,7 +474,7 @@ mod tests {
|
|||||||
fn test_get_primary_ip_rejects_loopback_override() {
|
fn test_get_primary_ip_rejects_loopback_override() {
|
||||||
// Loopback in report_ip should fall back to auto-detect
|
// Loopback in report_ip should fall back to auto-detect
|
||||||
// In Docker containers, auto-detect may also fail — that's valid
|
// In Docker containers, auto-detect may also fail — that's valid
|
||||||
match get_primary_ip(None, Some("127.0.0.1")) {
|
match get_primary_ip(None, Some("127.0.0.1"), None) {
|
||||||
Ok(ip) => assert_ne!(ip, "127.0.0.1"),
|
Ok(ip) => assert_ne!(ip, "127.0.0.1"),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -408,7 +488,7 @@ mod tests {
|
|||||||
fn test_get_primary_ip_invalid_override_falls_back() {
|
fn test_get_primary_ip_invalid_override_falls_back() {
|
||||||
// Invalid IP in report_ip should fall back to auto-detect
|
// Invalid IP in report_ip should fall back to auto-detect
|
||||||
// In Docker containers, auto-detect may also fail — that's valid
|
// In Docker containers, auto-detect may also fail — that's valid
|
||||||
match get_primary_ip(None, Some("not-an-ip")) {
|
match get_primary_ip(None, Some("not-an-ip"), None) {
|
||||||
Ok(ip) => assert!(!ip.is_empty()),
|
Ok(ip) => assert!(!ip.is_empty()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -417,4 +497,68 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_primary_ip_route_target_priority() {
|
||||||
|
// Route-based selection should be tried before auto-detect
|
||||||
|
// We test with a well-known IP; if iproute2 is available this may succeed,
|
||||||
|
// otherwise it falls back gracefully
|
||||||
|
match get_primary_ip(None, None, Some("8.8.8.8")) {
|
||||||
|
Ok(ip) => {
|
||||||
|
assert!(!ip.is_empty(), "Route-based IP should not be empty");
|
||||||
|
let parsed: Ipv4Addr = ip.parse().expect("Route-based IP should be valid IPv4");
|
||||||
|
assert!(
|
||||||
|
!is_container_bridge(&parsed),
|
||||||
|
"Route-based IP should not be Docker bridge"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!parsed.is_loopback(),
|
||||||
|
"Route-based IP should not be loopback"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!(
|
||||||
|
"NOTE: Route-based selection failed — iproute2 may not be available in this environment"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_primary_ip_explicit_overrides_route_target() {
|
||||||
|
// Explicit report_ip should take priority over route_target
|
||||||
|
let ip = get_primary_ip(None, Some("10.99.99.1"), Some("8.8.8.8"))
|
||||||
|
.expect("Explicit IP should override route_target");
|
||||||
|
assert_eq!(ip, "10.99.99.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_route_source_ip_known_target() {
|
||||||
|
// Test route-based IP detection with a well-known target
|
||||||
|
// This test requires iproute2 to be installed
|
||||||
|
match get_route_source_ip("8.8.8.8") {
|
||||||
|
Ok(ip) => {
|
||||||
|
let parsed: Ipv4Addr = ip.parse().expect("Route source IP should be valid IPv4");
|
||||||
|
assert!(
|
||||||
|
!parsed.is_loopback(),
|
||||||
|
"Route source IP should not be loopback"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!is_container_bridge(&parsed),
|
||||||
|
"Route source IP should not be Docker bridge"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!is_link_local(&parsed),
|
||||||
|
"Route source IP should not be link-local"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Acceptable in containers without iproute2 or routing
|
||||||
|
eprintln!(
|
||||||
|
"NOTE: Route-based IP detection failed: {} — may be unavailable in this environment",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ pub use client::{
|
|||||||
/// Re-export identity extraction functions.
|
/// Re-export identity extraction functions.
|
||||||
pub use identity::{
|
pub use identity::{
|
||||||
get_fqdn, get_ip_addresses, get_ip_for_interface, get_machine_id, get_os_details,
|
get_fqdn, get_ip_addresses, get_ip_for_interface, get_machine_id, get_os_details,
|
||||||
get_primary_ip, is_container_bridge, is_link_local,
|
get_primary_ip, get_route_source_ip, is_container_bridge, is_link_local,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Run the full enrollment flow against the manager at the given URL.
|
/// Run the full enrollment flow against the manager at the given URL.
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use linux_patch_api::enroll::identity::{
|
use linux_patch_api::enroll::identity::{
|
||||||
get_fqdn, get_ip_addresses, get_machine_id, get_os_details, get_primary_ip,
|
get_fqdn, get_ip_addresses, get_machine_id, get_os_details, get_primary_ip,
|
||||||
is_container_bridge, is_link_local,
|
get_route_source_ip, is_container_bridge, is_link_local,
|
||||||
};
|
};
|
||||||
use linux_patch_api::enroll::EnrollmentRequest;
|
use linux_patch_api::enroll::EnrollmentRequest;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -586,7 +586,7 @@ fn test_get_ip_addresses_excludes_link_local() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_get_primary_ip_auto_detect_no_bridge() {
|
fn test_get_primary_ip_auto_detect_no_bridge() {
|
||||||
// In Docker containers, auto-detect may find no routable IPs — that's valid
|
// In Docker containers, auto-detect may find no routable IPs — that's valid
|
||||||
match get_primary_ip(None, None) {
|
match get_primary_ip(None, None, None) {
|
||||||
Ok(ip) => {
|
Ok(ip) => {
|
||||||
assert!(!ip.is_empty(), "Primary IP should not be empty");
|
assert!(!ip.is_empty(), "Primary IP should not be empty");
|
||||||
let parsed: std::net::Ipv4Addr = ip.parse().expect("Primary IP should be valid IPv4");
|
let parsed: std::net::Ipv4Addr = ip.parse().expect("Primary IP should be valid IPv4");
|
||||||
@ -603,14 +603,14 @@ fn test_get_primary_ip_auto_detect_no_bridge() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_primary_ip_explicit_override() {
|
fn test_get_primary_ip_explicit_override() {
|
||||||
let ip = get_primary_ip(None, Some("10.99.99.1")).expect("Failed with explicit IP");
|
let ip = get_primary_ip(None, Some("10.99.99.1"), None).expect("Failed with explicit IP");
|
||||||
assert_eq!(ip, "10.99.99.1");
|
assert_eq!(ip, "10.99.99.1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_primary_ip_rejects_loopback_override() {
|
fn test_get_primary_ip_rejects_loopback_override() {
|
||||||
// Loopback override should fall back to auto-detect; if auto-detect also fails, that's valid
|
// Loopback override should fall back to auto-detect; if auto-detect also fails, that's valid
|
||||||
match get_primary_ip(None, Some("127.0.0.1")) {
|
match get_primary_ip(None, Some("127.0.0.1"), None) {
|
||||||
Ok(ip) => assert_ne!(ip, "127.0.0.1"),
|
Ok(ip) => assert_ne!(ip, "127.0.0.1"),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -623,7 +623,7 @@ fn test_get_primary_ip_rejects_loopback_override() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_get_primary_ip_invalid_override_falls_back() {
|
fn test_get_primary_ip_invalid_override_falls_back() {
|
||||||
// Invalid IP override should fall back to auto-detect; if auto-detect also fails, that's valid
|
// Invalid IP override should fall back to auto-detect; if auto-detect also fails, that's valid
|
||||||
match get_primary_ip(None, Some("not-an-ip")) {
|
match get_primary_ip(None, Some("not-an-ip"), None) {
|
||||||
Ok(ip) => assert!(!ip.is_empty()),
|
Ok(ip) => assert!(!ip.is_empty()),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
@ -632,3 +632,61 @@ fn test_get_primary_ip_invalid_override_falls_back() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_primary_ip_route_target_priority() {
|
||||||
|
// Route-based selection should be tried before auto-detect
|
||||||
|
// If iproute2 is available this may succeed, otherwise falls back gracefully
|
||||||
|
match get_primary_ip(None, None, Some("8.8.8.8")) {
|
||||||
|
Ok(ip) => {
|
||||||
|
assert!(!ip.is_empty(), "Route-based IP should not be empty");
|
||||||
|
let parsed: std::net::Ipv4Addr =
|
||||||
|
ip.parse().expect("Route-based IP should be valid IPv4");
|
||||||
|
assert!(
|
||||||
|
!is_container_bridge(&parsed),
|
||||||
|
"Route-based IP should not be Docker bridge"
|
||||||
|
);
|
||||||
|
assert!(!parsed.is_loopback(), "Route-based IP should not be loopback");
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
eprintln!(
|
||||||
|
"NOTE: Route-based selection failed — iproute2 may not be available in this environment"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_primary_ip_explicit_overrides_route_target() {
|
||||||
|
// Explicit report_ip should take priority over route_target
|
||||||
|
let ip = get_primary_ip(None, Some("10.99.99.1"), Some("8.8.8.8"))
|
||||||
|
.expect("Explicit IP should override route_target");
|
||||||
|
assert_eq!(ip, "10.99.99.1");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_route_source_ip_known_target() {
|
||||||
|
// Test route-based IP detection with a well-known target
|
||||||
|
// Requires iproute2 to be installed
|
||||||
|
match get_route_source_ip("8.8.8.8") {
|
||||||
|
Ok(ip) => {
|
||||||
|
let parsed: std::net::Ipv4Addr =
|
||||||
|
ip.parse().expect("Route source IP should be valid IPv4");
|
||||||
|
assert!(!parsed.is_loopback(), "Route source IP should not be loopback");
|
||||||
|
assert!(
|
||||||
|
!is_container_bridge(&parsed),
|
||||||
|
"Route source IP should not be Docker bridge"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!is_link_local(&parsed),
|
||||||
|
"Route source IP should not be link-local"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"NOTE: Route-based IP detection failed: {} — may be unavailable in this environment",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user