diff --git a/CHANGELOG.md b/CHANGELOG.md index dca34408d3..7aff8a72b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### Changes +- Client + - Get client IP from the daemon in the disconnect command, matching the connect command's behavior, to avoid IP mismatches behind NAT + ## [v0.13.0](https://github.com/malbeclabs/doublezero/compare/client/v0.12.0...client/v0.13.0) - 2026-03-20 ### Breaking diff --git a/client/doublezero/src/command/connect.rs b/client/doublezero/src/command/connect.rs index 2f91b0708e..b284b1ff31 100644 --- a/client/doublezero/src/command/connect.rs +++ b/client/doublezero/src/command/connect.rs @@ -126,19 +126,7 @@ impl ProvisioningCliCommand { } // Get public IP from daemon - let v2_status = controller.v2_status().await?; - if v2_status.client_ip.is_empty() { - return Err(eyre::eyre!( - "Daemon has not discovered its client IP. Ensure the daemon is running \ - and has started up successfully, or set --client-ip on the daemon." - )); - } - let client_ip: Ipv4Addr = v2_status.client_ip.parse().map_err(|e| { - eyre::eyre!( - "Daemon returned invalid client IP '{}': {e}", - v2_status.client_ip - ) - })?; + let client_ip = super::helpers::resolve_client_ip(controller).await?; let client_ip_str = client_ip.to_string(); if !check_accesspass(client, client_ip)? { diff --git a/client/doublezero/src/command/disconnect.rs b/client/doublezero/src/command/disconnect.rs index f8a2b905ed..4c6e7d7459 100644 --- a/client/doublezero/src/command/disconnect.rs +++ b/client/doublezero/src/command/disconnect.rs @@ -20,8 +20,6 @@ use doublezero_sdk::{ UserType, }; -use super::helpers::look_for_ip; - #[allow(clippy::upper_case_acronyms)] #[derive(Clone, Debug, ValueEnum)] pub enum DzMode { @@ -55,8 +53,9 @@ impl DecommissioningCliCommand { // READY spinner.println("🔍 Decommissioning User"); - // Get public IP - let (client_ip, _) = look_for_ip(&self.client_ip, &spinner).await?; + // Get client IP from daemon (same source as connect) + let client_ip = super::helpers::resolve_client_ip(&controller).await?; + spinner.println(format!(" Client IP: {client_ip}")); spinner.inc(1); spinner.set_message("deleting user account..."); @@ -214,7 +213,11 @@ impl DecommissioningCliCommand { #[cfg(test)] mod tests { use super::*; - use crate::servicecontroller::{DoubleZeroStatus, MockServiceController, StatusResponse}; + use std::net::Ipv4Addr; + + use crate::servicecontroller::{ + DoubleZeroStatus, MockServiceController, StatusResponse, V2StatusResponse, + }; fn test_cmd() -> DecommissioningCliCommand { DecommissioningCliCommand { @@ -361,4 +364,70 @@ mod tests { "should have polled at least twice" ); } + + fn v2_status_with_ip(client_ip: &str) -> V2StatusResponse { + V2StatusResponse { + reconciler_enabled: true, + client_ip: client_ip.to_string(), + network: "mainnet".to_string(), + services: vec![], + } + } + + #[tokio::test] + async fn test_resolve_client_ip_success() { + let mut mock = MockServiceController::new(); + mock.expect_v2_status() + .returning(|| Ok(v2_status_with_ip("1.2.3.4"))); + + let ip = crate::command::helpers::resolve_client_ip(&mock) + .await + .unwrap(); + assert_eq!(ip, Ipv4Addr::new(1, 2, 3, 4)); + } + + #[tokio::test] + async fn test_resolve_client_ip_empty() { + let mut mock = MockServiceController::new(); + mock.expect_v2_status() + .returning(|| Ok(v2_status_with_ip(""))); + + let err = crate::command::helpers::resolve_client_ip(&mock) + .await + .unwrap_err(); + assert!( + err.to_string().contains("has not discovered its client IP"), + "unexpected error: {err}" + ); + } + + #[tokio::test] + async fn test_resolve_client_ip_invalid() { + let mut mock = MockServiceController::new(); + mock.expect_v2_status() + .returning(|| Ok(v2_status_with_ip("not-an-ip"))); + + let err = crate::command::helpers::resolve_client_ip(&mock) + .await + .unwrap_err(); + assert!( + err.to_string().contains("invalid client IP 'not-an-ip'"), + "unexpected error: {err}" + ); + } + + #[tokio::test] + async fn test_resolve_client_ip_daemon_unreachable() { + let mut mock = MockServiceController::new(); + mock.expect_v2_status() + .returning(|| Err(eyre::eyre!("connection refused"))); + + let err = crate::command::helpers::resolve_client_ip(&mock) + .await + .unwrap_err(); + assert!( + err.to_string().contains("connection refused"), + "unexpected error: {err}" + ); + } } diff --git a/client/doublezero/src/command/helpers.rs b/client/doublezero/src/command/helpers.rs index 4a74bdd09b..5545433829 100644 --- a/client/doublezero/src/command/helpers.rs +++ b/client/doublezero/src/command/helpers.rs @@ -1,313 +1,19 @@ -use backon::{BlockingRetryable, ExponentialBuilder}; -use doublezero_cli::helpers::get_public_ipv4; -use indicatif::ProgressBar; -use std::{ - net::{Ipv4Addr, UdpSocket}, - time::Duration, -}; - -pub async fn look_for_ip( - client_ip: &Option, - spinner: &ProgressBar, -) -> eyre::Result<(Ipv4Addr, String)> { - look_for_ip_with(client_ip, spinner, discover_public_ip).await -} - -/// Discovers the client's public IP address. -/// -/// Resolution order: -/// 1. Ask the kernel for the default route's source address (via a UDP -/// connect to 8.8.8.8 — no packets are sent). If the source is a -/// publicly routable IPv4 address, use it. -/// 2. Fall back to querying ifconfig.me/ip. -/// -/// This matches the daemon's discovery logic so both always agree on the IP. -fn discover_public_ip() -> Result> { - // Try default route source hint first. - if let Ok(ip) = discover_from_default_route() { - return Ok(ip.to_string()); - } - - // Fall back to external discovery. - get_public_ipv4() -} - -/// Performs a kernel route lookup by binding a UDP socket to a well-known -/// public IP. The local address chosen by the kernel reflects the default -/// route's source hint. Returns the IP only if it's publicly routable. -fn discover_from_default_route() -> Result> { - let socket = UdpSocket::bind("0.0.0.0:0")?; - socket.connect("8.8.8.8:80")?; - let local_addr = socket.local_addr()?; - let ip = match local_addr.ip() { - std::net::IpAddr::V4(ip) => ip, - _ => return Err("default route source is not IPv4".into()), - }; - if ip.is_loopback() - || ip.is_private() - || ip.is_link_local() - || ip.is_multicast() - || ip.is_broadcast() - || ip.is_unspecified() - { - return Err(format!("default route source {ip} is not publicly routable").into()); - } - Ok(ip) -} - -async fn look_for_ip_with( - client_ip: &Option, - spinner: &ProgressBar, - ip_fetch_func: impl FnMut() -> Result>, -) -> eyre::Result<(Ipv4Addr, String)> { - let client_ip = match client_ip { - Some(ip) => { - spinner.println(format!(" Using Public IP: {ip}")); - ip - } - None => &{ - spinner.set_message("Discovering your public IP..."); - - let builder = ExponentialBuilder::new() - .with_max_times(3) - .with_min_delay(Duration::from_secs(1)); - - let ipv4 = ip_fetch_func - .retry(builder) - .notify(|_, dur| { - spinner.set_message(format!("Fetching IP Address (checking in {dur:?})...")) - }) - .call() - .map_err(|_| eyre::eyre!("Timeout waiting for IP address")); - - match ipv4 { - Ok(ip) => { - spinner.println(format!("Public IP detected: {ip} - If you want to use a different IP, set --client-ip on the daemon (doublezerod)")); - ip - } - Err(e) => { - eyre::bail!("Could not detect your public IP. Set --client-ip on the daemon (doublezerod). ({})", e.to_string()); - } - } - }, - }; - - let ip: Ipv4Addr = client_ip - .parse() - .map_err(|_| eyre::eyre!("Invalid IPv4 address format: {}", client_ip))?; - - if let Some(reason) = is_bgp_martian(ip) { - eyre::bail!( - "Client IP {} is a BGP martian address ({}). A publicly routable IP address is required.", - ip, - reason - ); - } - - Ok((ip, client_ip.to_string())) -} - -/// Returns `Some(reason)` if the given IPv4 address is a BGP martian (should -/// never appear as a source in the global routing table), or `None` if the -/// address is publicly routable. -fn is_bgp_martian(ip: Ipv4Addr) -> Option<&'static str> { - let octets = ip.octets(); - - // 0.0.0.0/8 — "this" network (RFC 791) - if octets[0] == 0 { - return Some("\"this\" network (0.0.0.0/8)"); - } - // 10.0.0.0/8 — private (RFC 1918) - if octets[0] == 10 { - return Some("private (10.0.0.0/8)"); - } - // 100.64.0.0/10 — shared / CGNAT (RFC 6598) - if octets[0] == 100 && (octets[1] & 0xC0) == 64 { - return Some("shared/CGNAT (100.64.0.0/10)"); - } - // 127.0.0.0/8 — loopback (RFC 1122) - if octets[0] == 127 { - return Some("loopback (127.0.0.0/8)"); - } - // 169.254.0.0/16 — link-local (RFC 3927) - if octets[0] == 169 && octets[1] == 254 { - return Some("link-local (169.254.0.0/16)"); - } - // 172.16.0.0/12 — private (RFC 1918) - if octets[0] == 172 && (octets[1] & 0xF0) == 16 { - return Some("private (172.16.0.0/12)"); - } - // 192.0.0.0/24 — IETF protocol assignments (RFC 6890) - if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 { - return Some("IETF protocol assignments (192.0.0.0/24)"); - } - // 192.0.2.0/24 — documentation TEST-NET-1 (RFC 5737) - if octets[0] == 192 && octets[1] == 0 && octets[2] == 2 { - return Some("documentation TEST-NET-1 (192.0.2.0/24)"); - } - // 192.168.0.0/16 — private (RFC 1918) - if octets[0] == 192 && octets[1] == 168 { - return Some("private (192.168.0.0/16)"); - } - // 198.51.100.0/24 — documentation TEST-NET-2 (RFC 5737) - if octets[0] == 198 && octets[1] == 51 && octets[2] == 100 { - return Some("documentation TEST-NET-2 (198.51.100.0/24)"); - } - // 203.0.113.0/24 — documentation TEST-NET-3 (RFC 5737) - if octets[0] == 203 && octets[1] == 0 && octets[2] == 113 { - return Some("documentation TEST-NET-3 (203.0.113.0/24)"); - } - // 224.0.0.0/4 — multicast (RFC 5771) - if (octets[0] & 0xF0) == 224 { - return Some("multicast (224.0.0.0/4)"); - } - // 240.0.0.0/4 — reserved for future use (RFC 1112) + broadcast - if octets[0] >= 240 { - return Some("reserved (240.0.0.0/4)"); - } - - None -} - -#[cfg(test)] -mod tests { - use super::*; - use indicatif::ProgressBar; - use std::{error::Error, net::Ipv4Addr}; - - // Dummy ProgressBar for testing - fn dummy_spinner() -> ProgressBar { - ProgressBar::hidden() - } - - #[tokio::test] - async fn test_look_for_ip_with_client_ip() { - let client_ip = Some("203.0.114.1".to_string()); - let spinner = dummy_spinner(); - - let mock_ip_fetch = || Ok("8.8.8.8".to_string()); - - let result = look_for_ip_with(&client_ip, &spinner, mock_ip_fetch) - .await - .unwrap(); - assert_eq!(result.0, Ipv4Addr::new(203, 0, 114, 1)); - assert_eq!(result.1, "203.0.114.1"); - } - - #[tokio::test] - async fn test_look_for_ip_with_fetch_success() { - let client_ip = None; - let spinner = dummy_spinner(); - - let mock_ip_fetch = || Ok("8.8.8.8".to_string()); - - let result = look_for_ip_with(&client_ip, &spinner, mock_ip_fetch) - .await - .unwrap(); - assert_eq!(result.0, Ipv4Addr::new(8, 8, 8, 8)); - assert_eq!(result.1, "8.8.8.8"); - } - - #[tokio::test] - async fn test_look_for_ip_with_fetch_failure() { - let client_ip = None; - let spinner = dummy_spinner(); - - let mock_ip_fetch = - || Err::>(Box::::from("fetch failed")); - - let result = look_for_ip_with(&client_ip, &spinner, mock_ip_fetch).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_look_for_ip_with_invalid_ip() { - let client_ip = Some("not_an_ip".to_string()); - let spinner = dummy_spinner(); - - let mock_ip_fetch = || Ok("8.8.8.8".to_string()); - - let result = look_for_ip_with(&client_ip, &spinner, mock_ip_fetch).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_look_for_ip_rejects_martian_address() { - let spinner = dummy_spinner(); - let mock_ip_fetch = || Ok("8.8.8.8".to_string()); - - // Private (RFC 1918) - let result = - look_for_ip_with(&Some("192.168.1.1".to_string()), &spinner, mock_ip_fetch).await; - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("BGP martian")); - } - - #[tokio::test] - async fn test_look_for_ip_rejects_auto_detected_martian() { - let spinner = dummy_spinner(); - // Simulate auto-detection returning a private IP - let mock_ip_fetch = || Ok("10.0.0.1".to_string()); - - let result = look_for_ip_with(&None, &spinner, mock_ip_fetch).await; - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("BGP martian")); - } - - #[test] - fn test_is_bgp_martian() { - // Martian addresses - assert!(is_bgp_martian(Ipv4Addr::new(0, 0, 0, 0)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(0, 1, 2, 3)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(10, 0, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(10, 255, 255, 255)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(100, 64, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(100, 127, 255, 255)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(127, 0, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(169, 254, 1, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(172, 16, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(172, 31, 255, 255)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(192, 0, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(192, 0, 2, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(192, 168, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(198, 51, 100, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(203, 0, 113, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(224, 0, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(239, 255, 255, 255)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(240, 0, 0, 1)).is_some()); - assert!(is_bgp_martian(Ipv4Addr::new(255, 255, 255, 255)).is_some()); - - // Non-martian (publicly routable) - assert!(is_bgp_martian(Ipv4Addr::new(1, 1, 1, 1)).is_none()); - assert!(is_bgp_martian(Ipv4Addr::new(8, 8, 8, 8)).is_none()); - assert!(is_bgp_martian(Ipv4Addr::new(100, 63, 255, 255)).is_none()); // just below CGNAT - assert!(is_bgp_martian(Ipv4Addr::new(100, 128, 0, 0)).is_none()); // just above CGNAT - assert!(is_bgp_martian(Ipv4Addr::new(172, 15, 255, 255)).is_none()); // just below 172.16/12 - assert!(is_bgp_martian(Ipv4Addr::new(172, 32, 0, 0)).is_none()); // just above 172.16/12 - assert!(is_bgp_martian(Ipv4Addr::new(198, 18, 0, 1)).is_none()); // benchmarking — allowed - assert!(is_bgp_martian(Ipv4Addr::new(198, 19, 0, 1)).is_none()); // benchmarking — allowed - assert!(is_bgp_martian(Ipv4Addr::new(203, 0, 114, 1)).is_none()); // just above TEST-NET-3 - } - - #[tokio::test] - async fn test_look_for_ip_with_retry_success() { - let client_ip = None; - let spinner = dummy_spinner(); - - let mut first = true; - let mock_ip_fetch = move || { - if first { - first = false; - Err(Box::::from("fetch failed")) - } else { - Ok("8.8.4.4".to_string()) - } - }; - - let result = look_for_ip_with(&client_ip, &spinner, mock_ip_fetch) - .await - .unwrap(); - assert_eq!(result.0, Ipv4Addr::new(8, 8, 4, 4)); - assert_eq!(result.1, "8.8.4.4"); - } +use std::net::Ipv4Addr; + +use crate::servicecontroller::ServiceController; + +pub async fn resolve_client_ip(controller: &T) -> eyre::Result { + let v2_status = controller.v2_status().await?; + if v2_status.client_ip.is_empty() { + return Err(eyre::eyre!( + "Daemon has not discovered its client IP. Ensure the daemon is running \ + and has started up successfully, or set --client-ip on the daemon." + )); + } + v2_status.client_ip.parse().map_err(|e| { + eyre::eyre!( + "Daemon returned invalid client IP '{}': {e}", + v2_status.client_ip + ) + }) } diff --git a/e2e/allocation_lifecycle_test.go b/e2e/allocation_lifecycle_test.go index fa454941e6..84f1cbc03b 100644 --- a/e2e/allocation_lifecycle_test.go +++ b/e2e/allocation_lifecycle_test.go @@ -736,7 +736,7 @@ func TestE2E_Multicast_ReactivationPreservesAllocations(t *testing.T) { // Phase 1: Initial activation as Multicast publisher // ========================================================================= log.Debug("==> Phase 1: Connecting as multicast publisher to first group") - _, err = client.Exec(ctx, []string{"bash", "-c", "doublezero connect multicast publisher test-mc01 --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(ctx, []string{"bash", "-c", "doublezero connect multicast publisher test-mc01"}) require.NoError(t, err, "failed to connect as multicast publisher") // Wait for user to be activated @@ -792,7 +792,7 @@ func TestE2E_Multicast_ReactivationPreservesAllocations(t *testing.T) { log.Debug("==> Phase 2: Disconnecting and reconnecting with both multicast groups to trigger re-activation") // Disconnect existing multicast service first (required — daemon doesn't support updating in-place) - _, err = client.Exec(ctx, []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(ctx, []string{"bash", "-c", "doublezero disconnect multicast"}) require.NoError(t, err, "failed to disconnect multicast") // Wait for daemon to fully tear down the multicast service before reconnecting @@ -806,7 +806,7 @@ func TestE2E_Multicast_ReactivationPreservesAllocations(t *testing.T) { }, 30*time.Second, 2*time.Second, "daemon did not tear down multicast service within timeout") // Reconnect with both pub groups in a single command - _, err = client.Exec(ctx, []string{"bash", "-c", "doublezero connect multicast --publish test-mc01 test-mc02 --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(ctx, []string{"bash", "-c", "doublezero connect multicast --publish test-mc01 test-mc02"}) require.NoError(t, err, "failed to reconnect with both pub groups") // ========================================================================= diff --git a/e2e/ibrl_multicast_coexistence_test.go b/e2e/ibrl_multicast_coexistence_test.go index 2713bb078e..2b6082cdad 100644 --- a/e2e/ibrl_multicast_coexistence_test.go +++ b/e2e/ibrl_multicast_coexistence_test.go @@ -115,7 +115,7 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * // Connect IBRL client log.Info("==> Connecting client with IBRL") - ibrlCmd := "doublezero connect ibrl --client-ip " + client.CYOANetworkIP + ibrlCmd := "doublezero connect ibrl" _, err = client.Exec(t.Context(), []string{"bash", "-c", ibrlCmd}) require.NoError(t, err) @@ -152,7 +152,7 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * log.Info("==> PHASE 2: Adding multicast subscriber") - mcastCmd := "doublezero connect multicast subscriber mg01 --client-ip " + client.CYOANetworkIP + " 2>&1" + mcastCmd := "doublezero connect multicast subscriber mg01 2>&1" mcastOutput, err := client.Exec(t.Context(), []string{"bash", "-c", mcastCmd}) log.Info("==> Multicast subscriber connect output", "output", string(mcastOutput)) require.NoError(t, err) @@ -175,7 +175,7 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * log.Info("==> PHASE 3: Removing multicast subscriber") - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) require.NoError(t, err) log.Info("--> Multicast subscriber disconnected") @@ -193,7 +193,7 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * log.Info("==> PHASE 4: Adding multicast publisher") - mcastCmd = "doublezero connect multicast publisher mg01 --client-ip " + client.CYOANetworkIP + " 2>&1" + mcastCmd = "doublezero connect multicast publisher mg01 2>&1" mcastOutput, err = client.Exec(t.Context(), []string{"bash", "-c", mcastCmd}) log.Info("==> Multicast publisher connect output", "output", string(mcastOutput)) require.NoError(t, err) @@ -216,7 +216,7 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * log.Info("==> PHASE 5: Removing multicast publisher") - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) require.NoError(t, err) log.Info("--> Multicast publisher disconnected") @@ -234,7 +234,7 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * log.Info("==> PHASE 6: Re-adding multicast subscriber (swap from publisher back to subscriber)") - mcastCmd = "doublezero connect multicast subscriber mg01 --client-ip " + client.CYOANetworkIP + " 2>&1" + mcastCmd = "doublezero connect multicast subscriber mg01 2>&1" mcastOutput, err = client.Exec(t.Context(), []string{"bash", "-c", mcastCmd}) log.Info("==> Multicast subscriber connect output", "output", string(mcastOutput)) require.NoError(t, err) @@ -258,13 +258,13 @@ func runSingleClientMulticastPubSubSwapTest(t *testing.T, log *slog.Logger, dn * log.Info("==> DISCONNECT PHASE") log.Info("==> Disconnecting multicast") - _, disconnectMcastErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, disconnectMcastErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) if disconnectMcastErr != nil { log.Info("--> Warning: Multicast disconnect failed", "error", disconnectMcastErr) } log.Info("==> Disconnecting IBRL") - _, disconnectErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect --client-ip " + client.CYOANetworkIP}) + _, disconnectErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect"}) if disconnectErr != nil { log.Info("--> Warning: IBRL disconnect failed", "error", disconnectErr) } else { @@ -316,7 +316,7 @@ func TestE2E_Multicast_PublisherAndSubscriber(t *testing.T) { // Connect as both publisher and subscriber using new flags log.Debug("==> Connecting as both publisher and subscriber") - cmd := "doublezero connect multicast --publish mg01 --subscribe mg01 --client-ip " + client.CYOANetworkIP + " 2>&1" + cmd := "doublezero connect multicast --publish mg01 --subscribe mg01 2>&1" output, err := client.Exec(t.Context(), []string{"bash", "-c", cmd}) log.Debug("==> Connect output", "output", string(output)) require.NoError(t, err, "should be able to connect as both publisher and subscriber") @@ -348,7 +348,7 @@ func TestE2E_Multicast_PublisherAndSubscriber(t *testing.T) { // Disconnect log.Debug("==> Disconnecting") - disconnectOutput, disconnectErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP + " 2>&1"}) + disconnectOutput, disconnectErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast 2>&1"}) log.Debug("==> Disconnect output", "output", string(disconnectOutput)) require.NoError(t, disconnectErr, "disconnect should succeed") verifyTunnelRemoved(t, client, "doublezero1") @@ -496,7 +496,7 @@ func runSingleClientIBRLThenMulticastTest(t *testing.T, log *slog.Logger, dn *de // Connect IBRL client (latency-based device selection) log.Debug("==> Connecting client with IBRL", "useAllocatedAddr", useAllocatedAddr) - ibrlCmd := "doublezero connect ibrl --client-ip " + client.CYOANetworkIP + ibrlCmd := "doublezero connect ibrl" if useAllocatedAddr { ibrlCmd += " --allocate-addr" } @@ -542,10 +542,10 @@ func runSingleClientIBRLThenMulticastTest(t *testing.T, log *slog.Logger, dn *de var mcastCmd string if asPublisher { log.Debug("==> Adding multicast publisher subscription") - mcastCmd = "doublezero connect multicast publisher mg01 --client-ip " + client.CYOANetworkIP + " 2>&1" + mcastCmd = "doublezero connect multicast publisher mg01 2>&1" } else { log.Debug("==> Adding multicast subscriber subscription") - mcastCmd = "doublezero connect multicast subscriber mg01 --client-ip " + client.CYOANetworkIP + " 2>&1" + mcastCmd = "doublezero connect multicast subscriber mg01 2>&1" } mcastOutput, err := client.Exec(t.Context(), []string{"bash", "-c", mcastCmd}) log.Debug("==> Multicast connect command output", "output", string(mcastOutput)) @@ -635,7 +635,7 @@ func runSingleClientIBRLThenMulticastTest(t *testing.T, log *slog.Logger, dn *de // Disconnect multicast first log.Debug("==> Disconnecting multicast") - _, disconnectMcastErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, disconnectMcastErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) if disconnectMcastErr != nil { log.Debug("--> Warning: Multicast disconnect failed (ledger may be unavailable)", "error", disconnectMcastErr) } @@ -647,7 +647,7 @@ func runSingleClientIBRLThenMulticastTest(t *testing.T, log *slog.Logger, dn *de // Disconnect IBRL log.Debug("==> Disconnecting IBRL") - _, disconnectErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect --client-ip " + client.CYOANetworkIP}) + _, disconnectErr := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect"}) if disconnectErr != nil { log.Debug("--> Warning: IBRL disconnect failed (ledger may be unavailable)", "error", disconnectErr) } else { @@ -795,7 +795,7 @@ func runIBRLWithMulticastSubscriberTest(t *testing.T, log *slog.Logger, dn *devn // Connect IBRL client log.Debug("==> Connecting IBRL client", "useAllocatedAddr", useAllocatedAddr) - ibrlCmd := "doublezero connect ibrl --client-ip " + ibrlClient.CYOANetworkIP + ibrlCmd := "doublezero connect ibrl" if useAllocatedAddr { ibrlCmd += " --allocate-addr" } @@ -804,7 +804,7 @@ func runIBRLWithMulticastSubscriberTest(t *testing.T, log *slog.Logger, dn *devn // Connect multicast subscriber log.Debug("==> Connecting multicast subscriber") - _, err = mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast subscriber mg01 --client-ip " + mcastClient.CYOANetworkIP}) + _, err = mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast subscriber mg01"}) require.NoError(t, err) // Wait for tunnels to come up @@ -857,7 +857,7 @@ func runIBRLWithMulticastSubscriberTest(t *testing.T, log *slog.Logger, dn *devn // Disconnect multicast subscriber - don't fail if ledger is unavailable log.Debug("==> Disconnecting multicast subscriber to test independence") - _, disconnectMcastErr := mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + mcastClient.CYOANetworkIP}) + _, disconnectMcastErr := mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) if disconnectMcastErr != nil { log.Debug("--> Warning: Multicast disconnect failed (ledger may be unavailable)", "error", disconnectMcastErr) return @@ -872,7 +872,7 @@ func runIBRLWithMulticastSubscriberTest(t *testing.T, log *slog.Logger, dn *devn log.Debug("==> FULL DISCONNECT PHASE") // Disconnect IBRL client - don't fail test if ledger is unavailable (infrastructure flakiness) - _, disconnectErr := ibrlClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect --client-ip " + ibrlClient.CYOANetworkIP}) + _, disconnectErr := ibrlClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect"}) if disconnectErr != nil { log.Debug("--> Warning: IBRL disconnect failed (ledger may be unavailable)", "error", disconnectErr) } else { @@ -907,7 +907,7 @@ func runIBRLWithMulticastPublisherTest(t *testing.T, log *slog.Logger, dn *devne // Connect IBRL client log.Debug("==> Connecting IBRL client", "useAllocatedAddr", useAllocatedAddr) - ibrlCmd := "doublezero connect ibrl --client-ip " + ibrlClient.CYOANetworkIP + ibrlCmd := "doublezero connect ibrl" if useAllocatedAddr { ibrlCmd += " --allocate-addr" } @@ -916,7 +916,7 @@ func runIBRLWithMulticastPublisherTest(t *testing.T, log *slog.Logger, dn *devne // Connect multicast publisher log.Debug("==> Connecting multicast publisher") - _, err = mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast publisher mg01 --client-ip " + mcastClient.CYOANetworkIP}) + _, err = mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast publisher mg01"}) require.NoError(t, err) // Wait for tunnels to come up @@ -961,7 +961,7 @@ func runIBRLWithMulticastPublisherTest(t *testing.T, log *slog.Logger, dn *devne // Disconnect multicast publisher - don't fail if ledger is unavailable log.Debug("==> Disconnecting multicast publisher to test independence") - _, disconnectMcastErr := mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + mcastClient.CYOANetworkIP}) + _, disconnectMcastErr := mcastClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) if disconnectMcastErr != nil { log.Debug("--> Warning: Multicast disconnect failed (ledger may be unavailable)", "error", disconnectMcastErr) return @@ -976,7 +976,7 @@ func runIBRLWithMulticastPublisherTest(t *testing.T, log *slog.Logger, dn *devne log.Debug("==> FULL DISCONNECT PHASE") // Disconnect IBRL client - don't fail test if ledger is unavailable (infrastructure flakiness) - _, disconnectErr := ibrlClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect --client-ip " + ibrlClient.CYOANetworkIP}) + _, disconnectErr := ibrlClient.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect"}) if disconnectErr != nil { log.Debug("--> Warning: IBRL disconnect failed (ledger may be unavailable)", "error", disconnectErr) log.Debug("--> Skipping tunnel removal verification due to disconnect failure") diff --git a/e2e/main_test.go b/e2e/main_test.go index 6e1d4be433..accb906926 100644 --- a/e2e/main_test.go +++ b/e2e/main_test.go @@ -367,7 +367,7 @@ func (dn *TestDevnet) Start(t *testing.T) (*devnet.Device, *devnet.Client) { func (dn *TestDevnet) DisconnectUserTunnel(t *testing.T, client *devnet.Client) { dn.log.Debug("==> Disconnecting user tunnel") - _, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect --client-ip " + client.CYOANetworkIP}) + _, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect"}) require.NoError(t, err) dn.log.Debug("--> User tunnel disconnected") @@ -452,7 +452,7 @@ func (dn *TestDevnet) ConnectIBRLUserTunnel(t *testing.T, client *devnet.Client) _, err := dn.Manager.Exec(t.Context(), []string{"bash", "-c", "doublezero access-pass set --accesspass-type prepaid --client-ip " + client.CYOANetworkIP + " --user-payer " + client.Pubkey}) require.NoError(t, err) - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect ibrl --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect ibrl"}) require.NoError(t, err) dn.log.Debug("--> IBRL user tunnel connected") @@ -466,7 +466,7 @@ func (dn *TestDevnet) ConnectUserTunnelWithAllocatedIP(t *testing.T, client *dev _, err := dn.Manager.Exec(t.Context(), []string{"bash", "-c", "doublezero access-pass set --accesspass-type prepaid --client-ip " + client.CYOANetworkIP + " --user-payer " + client.Pubkey}) require.NoError(t, err) - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect ibrl --client-ip " + client.CYOANetworkIP + " --allocate-addr"}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect ibrl --allocate-addr"}) require.NoError(t, err) dn.log.Debug("--> User tunnel with allocated IP connected") @@ -480,7 +480,7 @@ func (dn *TestDevnet) ConnectMulticastPublisher(t *testing.T, client *devnet.Cli require.NoError(t, err) groupArgs := strings.Join(multicastGroupCodes, " ") - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast publisher " + groupArgs + " --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast publisher " + groupArgs}) require.NoError(t, err, "failed to connect multicast publisher") dn.log.Debug("--> Multicast publisher connected") @@ -490,7 +490,7 @@ func (dn *TestDevnet) ConnectMulticastPublisherSkipAccessPass(t *testing.T, clie dn.log.Debug("==> Connecting multicast publisher", "clientIP", client.CYOANetworkIP, "groups", multicastGroupCodes) groupArgs := strings.Join(multicastGroupCodes, " ") - _, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast publisher " + groupArgs + " --client-ip " + client.CYOANetworkIP}) + _, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast publisher " + groupArgs}) require.NoError(t, err, "failed to connect multicast publisher") dn.log.Debug("--> Multicast publisher connected") @@ -504,7 +504,7 @@ func (dn *TestDevnet) DisconnectMulticastPublisher(t *testing.T, client *devnet. _, err := dn.Manager.Exec(t.Context(), []string{"bash", "-c", "doublezero access-pass set --accesspass-type prepaid --client-ip " + client.CYOANetworkIP + " --user-payer " + client.Pubkey}) require.NoError(t, err) - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) require.NoError(t, err, "failed to disconnect multicast publisher") dn.log.Debug("--> Multicast publisher disconnected") @@ -518,7 +518,7 @@ func (dn *TestDevnet) ConnectMulticastSubscriber(t *testing.T, client *devnet.Cl require.NoError(t, err) groupArgs := strings.Join(multicastGroupCodes, " ") - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast subscriber " + groupArgs + " --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast subscriber " + groupArgs}) require.NoError(t, err) dn.log.Debug("--> Multicast subscriber connected") @@ -528,7 +528,7 @@ func (dn *TestDevnet) ConnectMulticastSubscriberSkipAccessPass(t *testing.T, cli dn.log.Debug("==> Connecting multicast subscriber", "clientIP", client.CYOANetworkIP, "groups", multicastGroupCodes) groupArgs := strings.Join(multicastGroupCodes, " ") - _, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast subscriber " + groupArgs + " --client-ip " + client.CYOANetworkIP}) + _, err := client.Exec(t.Context(), []string{"bash", "-c", "doublezero connect multicast subscriber " + groupArgs}) require.NoError(t, err) dn.log.Debug("--> Multicast subscriber connected") @@ -541,7 +541,7 @@ func (dn *TestDevnet) DisconnectMulticastSubscriber(t *testing.T, client *devnet _, err := dn.Manager.Exec(t.Context(), []string{"bash", "-c", "doublezero access-pass set --accesspass-type prepaid --client-ip " + client.CYOANetworkIP + " --user-payer " + client.Pubkey}) require.NoError(t, err) - _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP}) + _, err = client.Exec(t.Context(), []string{"bash", "-c", "doublezero disconnect multicast"}) require.NoError(t, err) dn.log.Debug("--> Multicast subscriber disconnected") diff --git a/e2e/multi_tunnel_endpoint_test.go b/e2e/multi_tunnel_endpoint_test.go index 30b0865272..602de33c72 100644 --- a/e2e/multi_tunnel_endpoint_test.go +++ b/e2e/multi_tunnel_endpoint_test.go @@ -187,8 +187,8 @@ func runMultiTunnelFallbackTest(t *testing.T, log *slog.Logger, dn *devnet.Devne // Connect IBRL client to device1 using --device flag log.Info("==> Connecting client with IBRL to device1", "device", device1.Spec.Code) - ibrlCmd := fmt.Sprintf("doublezero connect ibrl --client-ip %s --device %s", - client.CYOANetworkIP, device1.Spec.Code) + ibrlCmd := fmt.Sprintf("doublezero connect ibrl --device %s", + device1.Spec.Code) if useAllocatedAddr { ibrlCmd += " --allocate-addr" } @@ -220,8 +220,7 @@ func runMultiTunnelFallbackTest(t *testing.T, log *slog.Logger, dn *devnet.Devne // Connect multicast without specifying device - it should automatically pick device2 // because device1's tunnel endpoint is already in use by the IBRL tunnel - mcastCmd := fmt.Sprintf("doublezero connect multicast subscriber mg01 --client-ip %s 2>&1", - client.CYOANetworkIP) + mcastCmd := "doublezero connect multicast subscriber mg01 2>&1" mcastOutput, err := client.Exec(t.Context(), []string{"bash", "-c", mcastCmd}) log.Info("==> Multicast connect output", "output", string(mcastOutput)) require.NoError(t, err) @@ -287,7 +286,7 @@ func runMultiTunnelFallbackTest(t *testing.T, log *slog.Logger, dn *devnet.Devne log.Info("==> Disconnecting multicast") _, err = client.Exec(t.Context(), []string{ "bash", "-c", - "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP, + "doublezero disconnect multicast", }) if err != nil { log.Info("--> Warning: multicast disconnect returned error", "error", err) @@ -305,7 +304,7 @@ func runMultiTunnelFallbackTest(t *testing.T, log *slog.Logger, dn *devnet.Devne log.Info("==> Disconnecting IBRL") _, err = client.Exec(t.Context(), []string{ "bash", "-c", - "doublezero disconnect --client-ip " + client.CYOANetworkIP, + "doublezero disconnect", }) if err != nil { log.Info("--> Warning: IBRL disconnect returned error", "error", err) @@ -511,7 +510,7 @@ func runSimultaneousTunnelTest(t *testing.T, log *slog.Logger, dn *devnet.Devnet // Connect IBRL. With only one device, the CLI auto-selects it. // The client picks the best tunnel endpoint based on latency probing. log.Info("==> Connecting client with IBRL") - ibrlCmd := fmt.Sprintf("doublezero connect ibrl --client-ip %s", client.CYOANetworkIP) + ibrlCmd := "doublezero connect ibrl" _, err = client.Exec(t.Context(), []string{"bash", "-c", ibrlCmd}) require.NoError(t, err) @@ -543,8 +542,7 @@ func runSimultaneousTunnelTest(t *testing.T, log *slog.Logger, dn *devnet.Devnet // Connect multicast subscriber. The client's exclude_ips list contains the // first tunnel's endpoint, so it selects the remaining endpoint on this device. - mcastCmd := fmt.Sprintf("doublezero connect multicast subscriber mg01 --client-ip %s 2>&1", - client.CYOANetworkIP) + mcastCmd := "doublezero connect multicast subscriber mg01 2>&1" mcastOutput, err := client.Exec(t.Context(), []string{"bash", "-c", mcastCmd}) log.Info("==> Multicast connect output", "output", string(mcastOutput)) require.NoError(t, err) @@ -616,7 +614,7 @@ func runSimultaneousTunnelTest(t *testing.T, log *slog.Logger, dn *devnet.Devnet log.Info("==> Disconnecting multicast") _, err = client.Exec(t.Context(), []string{ "bash", "-c", - "doublezero disconnect multicast --client-ip " + client.CYOANetworkIP, + "doublezero disconnect multicast", }) if err != nil { log.Info("--> Warning: multicast disconnect returned error", "error", err) @@ -634,7 +632,7 @@ func runSimultaneousTunnelTest(t *testing.T, log *slog.Logger, dn *devnet.Devnet log.Info("==> Disconnecting IBRL") _, err = client.Exec(t.Context(), []string{ "bash", "-c", - "doublezero disconnect --client-ip " + client.CYOANetworkIP, + "doublezero disconnect", }) if err != nil { log.Info("--> Warning: IBRL disconnect returned error", "error", err) diff --git a/e2e/user_limits_test.go b/e2e/user_limits_test.go index 7a37b44e2c..26063a22c2 100644 --- a/e2e/user_limits_test.go +++ b/e2e/user_limits_test.go @@ -184,7 +184,7 @@ func TestE2E_UserLimits(t *testing.T) { // Use --device to specify device explicitly (no latency probing needed) _, err = client1.Exec(ctx, []string{"bash", "-c", - "doublezero connect ibrl --device " + deviceCode + " --client-ip " + client1.CYOANetworkIP + " --allocate-addr"}) + "doublezero connect ibrl --device " + deviceCode + " --allocate-addr"}) require.NoError(t, err) // Wait for first user to be activated @@ -234,7 +234,7 @@ func TestE2E_UserLimits(t *testing.T) { // The exit code check is done via require.Error — do not append "; echo EXIT_CODE=$?" // as that would mask the non-zero exit and err would always be nil. output, err := client2.Exec(ctx, []string{"bash", "-c", - "doublezero connect ibrl --device " + deviceCode + " --client-ip " + client2.CYOANetworkIP + " --allocate-addr 2>&1"}) + "doublezero connect ibrl --device " + deviceCode + " --allocate-addr 2>&1"}) outputStr := string(output) log.Info("Second unicast user creation result", "output", outputStr, "err", err) @@ -278,7 +278,7 @@ func TestE2E_UserLimits(t *testing.T) { // Use --device to specify device explicitly (no latency probing needed) _, err = client1.Exec(ctx, []string{"bash", "-c", - "doublezero connect multicast subscriber limit-mc01 --device " + deviceCode + " --client-ip " + client1.CYOANetworkIP}) + "doublezero connect multicast subscriber limit-mc01 --device " + deviceCode + ""}) require.NoError(t, err) // Wait for first multicast subscriber to be activated @@ -333,7 +333,7 @@ func TestE2E_UserLimits(t *testing.T) { // The exit code check is done via require.Error — do not append "; echo EXIT_CODE=$?" // as that would mask the non-zero exit and err would always be nil. output, err := client2.Exec(ctx, []string{"bash", "-c", - "doublezero connect multicast subscriber limit-mc01 --device " + deviceCode + " --client-ip " + client2.CYOANetworkIP + " 2>&1"}) + "doublezero connect multicast subscriber limit-mc01 --device " + deviceCode + " 2>&1"}) outputStr := string(output) log.Info("Second multicast subscriber creation result", "output", outputStr, "err", err) @@ -376,7 +376,7 @@ func TestE2E_UserLimits(t *testing.T) { // Use --device to specify device explicitly (no latency probing needed) _, err = client1.Exec(ctx, []string{"bash", "-c", - "doublezero connect multicast publisher limit-mc01 --device " + deviceCode + " --client-ip " + client1.CYOANetworkIP}) + "doublezero connect multicast publisher limit-mc01 --device " + deviceCode + ""}) require.NoError(t, err) // Wait for first multicast publisher to be activated @@ -431,7 +431,7 @@ func TestE2E_UserLimits(t *testing.T) { // The exit code check is done via require.Error — do not append "; echo EXIT_CODE=$?" // as that would mask the non-zero exit and err would always be nil. output, err := client2.Exec(ctx, []string{"bash", "-c", - "doublezero connect multicast publisher limit-mc01 --device " + deviceCode + " --client-ip " + client2.CYOANetworkIP + " 2>&1"}) + "doublezero connect multicast publisher limit-mc01 --device " + deviceCode + " 2>&1"}) outputStr := string(output) log.Info("Second multicast publisher creation result", "output", outputStr, "err", err)