Limit wildcard TLS migration to an observed tailnet bind conflict
This commit is contained in:
@@ -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";
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user