diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index a3c21ea0..8969b212 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -1145,16 +1145,37 @@ fn fips_app_relay_addr(ip: std::net::Ipv6Addr, port: u16) -> SocketAddr { /// 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) { - use std::collections::HashSet; - let mut bridged: HashSet = HashSet::new(); + use std::collections::HashMap; + let mut bridged: HashMap> = HashMap::new(); let mut interval = tokio::time::interval(std::time::Duration::from_secs(60)); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { tokio::select! { _ = interval.tick() => { let Some(fips_ip) = crate::fips::iface::fips0_ula() else { continue }; + // Ports declared `auth: gated` belong to the app gate on the + // fips0 ULA. This relay is a raw unauthenticated forward to + // the app's loopback, so bridging a gated port would bypass + // the gate — and which of the two wins the bind used to be a + // race. Skip them here, and tear down any bridge for a port + // that became gated since it was bridged (catalog refresh), + // releasing the bind so the gate's next sweep claims it. + let gated: std::collections::HashSet = crate::appgate::identity::build_port_map() + .gated_ports() + .map(|g| g.port) + .collect(); for &port in crate::fips::app_ports::APP_LAUNCH_PORTS { - if bridged.contains(&port) { + if gated.contains(&port) { + if let Some(handle) = bridged.remove(&port) { + handle.abort(); + info!( + port, + "v6 relay released a bridge: port is now gate-owned" + ); + } + continue; + } + if bridged.contains_key(&port) { continue; } // ONLY bridge a port that a running app already answers on @@ -1181,10 +1202,9 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver 127.0.0.1:{port}"); let mut rx = shutdown_rx.clone(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { loop { tokio::select! { accepted = listener.accept() => { @@ -1205,6 +1225,7 @@ async fn app_port_v6_relay_loop(mut shutdown_rx: tokio::sync::watch::Receiver return,