fix(security): FIPS v6 relay hands gated ports to the app gate
The mesh relay is a raw unauthenticated forward to the app's loopback, and whether it or the gate owned a fips0 ULA port was decided by a bind race — the dev box happened to be safe because the gate bound first. The relay now skips ports declared auth: gated and tears down any existing bridge for a port that became gated since it was bridged (catalog refresh), releasing the bind for the gate's next sweep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3760a00ea3
commit
f08ed79b8a
@@ -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<bool>) {
|
||||
use std::collections::HashSet;
|
||||
let mut bridged: HashSet<u16> = HashSet::new();
|
||||
use std::collections::HashMap;
|
||||
let mut bridged: HashMap<u16, tokio::task::JoinHandle<()>> = 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<u16> = 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<bo
|
||||
// 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 [{fips_ip}]:{port} -> 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<bo
|
||||
}
|
||||
}
|
||||
});
|
||||
bridged.insert(port, handle);
|
||||
}
|
||||
}
|
||||
_ = shutdown_rx.changed() => return,
|
||||
|
||||
Reference in New Issue
Block a user