Limit wildcard TLS migration to an observed tailnet bind conflict

This commit is contained in:
archipelago
2026-10-07 00:01:53 -04:00
parent fdee8658c3
commit 40fd91b9e1
2 changed files with 46 additions and 1 deletions
+40 -1
View File
@@ -1070,6 +1070,15 @@ fn is_cgnat(addr: &str) -> bool {
(64..=127).contains(&second) (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 <ip>:443 ssl;` set for one config's text. Returns the /// Rewrite the `listen <ip>:443 ssl;` set for one config's text. Returns the
/// new text when it differs. Lines for absent addresses are dropped and one /// new text when it differs. Lines for absent addresses are dropped and one
/// line per present address is kept, preserving the file's indentation. /// line per present address is kept, preserving the file's indentation.
@@ -1219,6 +1228,16 @@ async fn run_nginx_listener_repair() -> Result<bool> {
if present.is_empty() { if present.is_empty() {
return Ok(false); // no network yet; a later boot pass will do it 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 changed = false;
let mut seen = std::collections::HashSet::new(); let mut seen = std::collections::HashSet::new();
let repair_id = std::time::SystemTime::now() let repair_id = std::time::SystemTime::now()
@@ -1234,7 +1253,12 @@ async fn run_nginx_listener_repair() -> Result<bool> {
let Ok(text) = tokio::fs::read_to_string(&target).await else { let Ok(text) = tokio::fs::read_to_string(&target).await else {
continue; 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; continue;
}; };
let staged = "/var/lib/archipelago/nginx-listeners.staged"; let staged = "/var/lib/archipelago/nginx-listeners.staged";
@@ -2367,6 +2391,21 @@ mod tests {
assert!(retarget_https_listeners(&healed, &present).is_none()); 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] #[test]
fn managed_wildcard_tls_migration_preserves_other_vhosts() { 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"; 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";
+6
View File
@@ -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 reload a real node. Added Rust profile-migration tests and the integrated backend
compile remain pending the shared qualification slot. The live repaired nodes compile remain pending the shared qualification slot. The live repaired nodes
still run the previously qualified 49703d7e binary. 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.