fix: harden fips startup and app port relays
This commit is contained in:
@@ -940,14 +940,16 @@ impl Server {
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Err(e) = crate::fips::service::activate(crate::fips::SERVICE_UNIT).await {
|
||||
let unit = crate::fips::service::activation_unit().await;
|
||||
if let Err(e) = crate::fips::service::activate(unit).await {
|
||||
tracing::warn!(
|
||||
"archipelago-fips activate failed on startup: {} — user can retry via fips.install RPC",
|
||||
"FIPS activate failed on startup via {}: {} — user can retry via fips.install RPC",
|
||||
unit,
|
||||
e
|
||||
);
|
||||
return;
|
||||
}
|
||||
tracing::info!("archipelago-fips auto-activated on startup");
|
||||
tracing::info!("FIPS auto-activated on startup via {}", unit);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1000,10 +1002,8 @@ impl Server {
|
||||
_ => None,
|
||||
};
|
||||
let v6_task = if let Some(port) = v4_any_port {
|
||||
let v6_addr = SocketAddr::new(
|
||||
std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
|
||||
port,
|
||||
);
|
||||
let v6_addr =
|
||||
SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), port);
|
||||
match bind_v6_only(v6_addr) {
|
||||
Ok(listener) => {
|
||||
info!("IPv6 web listener bound {} (mesh ULA reachable)", v6_addr);
|
||||
@@ -1017,7 +1017,10 @@ impl Server {
|
||||
)))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("IPv6 web listener bind {} failed: {} — UI stays v4-only", v6_addr, e);
|
||||
warn!(
|
||||
"IPv6 web listener bind {} failed: {} — UI stays v4-only",
|
||||
v6_addr, e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1031,9 +1034,9 @@ impl Server {
|
||||
// IPv4 only for most apps, so a phone dialing [ULA]:8123 got
|
||||
// nothing even with the firewall open (HA/FileBrowser/Gitea/
|
||||
// Portainer/Pine all v4-only on 2026-07-26; a few bind [::]
|
||||
// themselves). Bridge each catalog launch port to its v4 loopback
|
||||
// listener with a V6ONLY relay — self-selecting: where the app
|
||||
// already answers on v6 the bind fails and the app wins.
|
||||
// themselves). Bridge each catalog launch port on the fips0 ULA
|
||||
// only. Binding wildcard [::]:port reserves the same host ports
|
||||
// Podman needs and can restart-loop apps that publish those ports.
|
||||
let relay_task = tokio::spawn(app_port_v6_relay_loop(tx.subscribe()));
|
||||
|
||||
let peer_task = tokio::spawn(peer_late_bind_loop(
|
||||
@@ -1087,10 +1090,14 @@ fn bind_v6_only(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
|
||||
tokio::net::TcpListener::from_std(socket.into())
|
||||
}
|
||||
|
||||
fn fips_app_relay_addr(ip: std::net::Ipv6Addr, port: u16) -> SocketAddr {
|
||||
SocketAddr::new(std::net::IpAddr::V6(ip), port)
|
||||
}
|
||||
|
||||
/// IPv6→IPv4 relay for catalog app launch ports (see the spawn site for
|
||||
/// why). Rescans every 60s so ports of freshly installed apps get bridged
|
||||
/// without a daemon restart. Each relay is a V6ONLY listener forwarding
|
||||
/// raw TCP to the same port on IPv4 loopback.
|
||||
/// without a daemon restart. Each relay binds to the fips0 ULA only and
|
||||
/// forwards raw TCP to the same port on IPv4 loopback.
|
||||
async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver<bool>) {
|
||||
use std::collections::HashSet;
|
||||
let mut bridged: HashSet<u16> = HashSet::new();
|
||||
@@ -1099,6 +1106,7 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver<bo
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
let Some(fips_ip) = crate::fips::iface::fips0_ula() else { continue };
|
||||
for &port in crate::fips::app_ports::APP_LAUNCH_PORTS {
|
||||
if bridged.contains(&port) {
|
||||
continue;
|
||||
@@ -1123,15 +1131,12 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver<bo
|
||||
if !v4_up {
|
||||
continue;
|
||||
}
|
||||
let addr = SocketAddr::new(
|
||||
std::net::IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
|
||||
port,
|
||||
);
|
||||
// EADDRINUSE = the app itself already answers on v6 —
|
||||
// exactly when we must stay out of the way.
|
||||
let addr = fips_app_relay_addr(fips_ip, port);
|
||||
// EADDRINUSE = fipsd or another process already answers
|
||||
// on this mesh address/port, so stay out of the way.
|
||||
let Ok(listener) = bind_v6_only(addr) else { continue };
|
||||
bridged.insert(port);
|
||||
debug!("v6 relay bridging [::]:{port} -> 127.0.0.1:{port}");
|
||||
debug!("v6 relay bridging [{fips_ip}]:{port} -> 127.0.0.1:{port}");
|
||||
let mut rx = shutdown_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -1945,6 +1950,14 @@ mod merge_tests {
|
||||
assert!(!is_peer_allowed_path("/rpc/v2"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_relay_binds_to_fips_ula_not_wildcard() {
|
||||
let ula = "fd12:3456:789a::1".parse().unwrap();
|
||||
let addr = fips_app_relay_addr(ula, 8083);
|
||||
assert_eq!(addr.ip(), std::net::IpAddr::V6(ula));
|
||||
assert_eq!(addr.port(), 8083);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_transitional_state_on_merge() {
|
||||
// existing: user initiated a stop, spawn_transitional set Stopping.
|
||||
|
||||
Reference in New Issue
Block a user