From 40fd91b9e15bc5735a4cddd15ccd8a943cf47326 Mon Sep 17 00:00:00 2001 From: archipelago Date: Wed, 7 Oct 2026 00:01:53 -0400 Subject: [PATCH] Limit wildcard TLS migration to an observed tailnet bind conflict --- core/archipelago/src/bootstrap.rs | 41 +++++++++++++++++++++++- docs/https-app-gate-followup-20261006.md | 6 ++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/core/archipelago/src/bootstrap.rs b/core/archipelago/src/bootstrap.rs index 083b7ab4..34d068cb 100644 --- a/core/archipelago/src/bootstrap.rs +++ b/core/archipelago/src/bootstrap.rs @@ -1070,6 +1070,15 @@ fn is_cgnat(addr: &str) -> bool { (64..=127).contains(&second) } +fn has_tailnet_https_listener(sockets: &str) -> bool { + sockets.lines().any(|line| { + line.split_whitespace() + .nth(3) + .and_then(|local| local.rsplit_once(':')) + .is_some_and(|(address, port)| port == "443" && is_cgnat(address)) + }) +} + /// Rewrite the `listen :443 ssl;` set for one config's text. Returns the /// new text when it differs. Lines for absent addresses are dropped and one /// line per present address is kept, preserving the file's indentation. @@ -1219,6 +1228,16 @@ async fn run_nginx_listener_repair() -> Result { if present.is_empty() { return Ok(false); // no network yet; a later boot pass will do it } + // Preserve wildcard/IPv6 service on nodes without a competing tailnet + // listener. Only the observed address-specific bind conflict warrants + // migrating the managed wildcard profile to LAN-only IPv4 listeners. + let repair_wildcards = tokio::process::Command::new("ss") + .args(["-H", "-4", "-ltn"]) + .output() + .await + .ok() + .filter(|output| output.status.success()) + .is_some_and(|output| has_tailnet_https_listener(&String::from_utf8_lossy(&output.stdout))); let mut changed = false; let mut seen = std::collections::HashSet::new(); let repair_id = std::time::SystemTime::now() @@ -1234,7 +1253,12 @@ async fn run_nginx_listener_repair() -> Result { let Ok(text) = tokio::fs::read_to_string(&target).await else { continue; }; - let Some(healed) = retarget_https_listeners(&text, &present) else { + let healed = if repair_wildcards { + retarget_https_listeners(&text, &present) + } else { + retarget_pinned_https_listeners(&text, &present) + }; + let Some(healed) = healed else { continue; }; let staged = "/var/lib/archipelago/nginx-listeners.staged"; @@ -2367,6 +2391,21 @@ mod tests { assert!(retarget_https_listeners(&healed, &present).is_none()); } + #[test] + fn tailnet_https_conflict_requires_an_actual_specific_socket() { + assert!(has_tailnet_https_listener( + "LISTEN 0 4096 100.72.136.7:443 0.0.0.0:*\n" + )); + for sockets in [ + "LISTEN 0 511 0.0.0.0:443 0.0.0.0:*\n", + "LISTEN 0 4096 100.72.136.7:8443 0.0.0.0:*\n", + "LISTEN 0 511 192.168.1.50:443 0.0.0.0:*\n", + "", + ] { + assert!(!has_tailnet_https_listener(sockets)); + } + } + #[test] fn managed_wildcard_tls_migration_preserves_other_vhosts() { let profile = "server {\n listen 443 ssl default_server;\n listen [::]:443 ssl default_server;\n server_name _;\n ssl_certificate /etc/archipelago/ssl/archipelago.crt;\n ssl_certificate_key /etc/archipelago/ssl/archipelago.key;\n root /opt/archipelago/web-ui;\n}\n"; diff --git a/docs/https-app-gate-followup-20261006.md b/docs/https-app-gate-followup-20261006.md index 16ab1c93..6489ca56 100644 --- a/docs/https-app-gate-followup-20261006.md +++ b/docs/https-app-gate-followup-20261006.md @@ -150,3 +150,9 @@ These run the exact embedded shell against fake service commands; they do not reload a real node. Added Rust profile-migration tests and the integrated backend compile remain pending the shared qualification slot. The live repaired nodes still run the previously qualified 49703d7e binary. + +Review refinement: wildcard conversion additionally requires an actual IPv4 +CGNAT-address port-443 listener, observed through read-only socket inspection. +Nodes without that competing tailnet bind retain their existing wildcard and +IPv6 HTTPS service. A configured Tailscale interface alone is not sufficient. +The added socket-profile cases are pending the same backend qualification run.