From 7222167b1396ca464ef8a03e783071f69432f0a7 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 17 Sep 2025 19:52:29 +0000 Subject: [PATCH] fix(connlib): limit the number of optimistic candidates (#10367) To facilitate direct connections, `connlib` generates "optimistic" candidates that combine the port of the host candidate with the IP of the server-reflexive candidate. This allows sysadmins to port-forward the Firezone port 52625 on the Gateway, allowing for direct connections to happen behind symmetric NAT. This feature is only really useful for IPv4 as IPv6 doesn't need symmetric NAT due to the larger address space. It is also quite common that users have multiple IPv6 addresses on a single interface. The combination of the two can result in CPU spikes on the Gateway if a client connects and sends over e.g. 10 IPv6 host candidates and various IPv6 server-reflexive candidates. The Gateway then ends up in a loop where it creates an NxM matrix of all these candidates. To mitigate this, we disable optimistic candidates for IPv6 altogether and limit the number of IPv4 optimistic candidates to 2. --- rust/connlib/snownet/src/node.rs | 66 +++++++++++++++++++- website/src/components/Changelog/Gateway.tsx | 7 ++- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/rust/connlib/snownet/src/node.rs b/rust/connlib/snownet/src/node.rs index b2258dfa9..fa46cea40 100644 --- a/rust/connlib/snownet/src/node.rs +++ b/rust/connlib/snownet/src/node.rs @@ -1292,7 +1292,7 @@ fn generate_optimistic_candidates(agent: &mut IceAgent) { let optimistic_candidates = public_ips .cartesian_product(host_candidates) - .filter(|(ip, base)| ip.is_ipv4() == base.is_ipv4()) + .filter(|(ip, base)| ip.is_ipv4() && base.is_ipv4()) .filter_map(|(ip, base)| { let addr = SocketAddr::new(ip, base.port()); @@ -1303,6 +1303,7 @@ fn generate_optimistic_candidates(agent: &mut IceAgent) { .ok() }) .filter(|c| !remote_candidates.contains(c)) + .take(2) .collect::>(); for c in optimistic_candidates { @@ -2649,7 +2650,7 @@ impl fmt::Display for SessionId { #[cfg(test)] mod tests { - use std::net::{IpAddr, Ipv4Addr, SocketAddrV4}; + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6}; use super::*; @@ -2691,4 +2692,65 @@ mod tests { assert!(agent.remote_candidates().contains(&expected_candidate)) } + + #[test] + fn skips_optimistic_candidates_for_ipv6() { + let base = SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::new(10, 0, 0, 0, 0, 0, 0, 1), + 52625, + 0, + 0, + )); + let addr = IpAddr::V6(Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1)); + + let host = Candidate::host(base, "udp").unwrap(); + let srvflx = + Candidate::server_reflexive(SocketAddr::new(addr, 40000), base, "udp").unwrap(); + + let mut agent = IceAgent::new(); + agent.add_remote_candidate(host); + agent.add_remote_candidate(srvflx); + + generate_optimistic_candidates(&mut agent); + + let unexpected_candidate = + Candidate::server_reflexive(SocketAddr::new(addr, 52625), base, "udp").unwrap(); + + assert!(!agent.remote_candidates().contains(&unexpected_candidate)) + } + + #[test] + fn limits_optimistic_ipv4_candidates_to_2() { + let base = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 52625)); + let addr1 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)); + let addr2 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 2)); + let addr3 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 3)); + + let host = Candidate::host(base, "udp").unwrap(); + let srflx1 = + Candidate::server_reflexive(SocketAddr::new(addr1, 40000), base, "udp").unwrap(); + let srflx2 = + Candidate::server_reflexive(SocketAddr::new(addr2, 40000), base, "udp").unwrap(); + let srflx3 = + Candidate::server_reflexive(SocketAddr::new(addr3, 40000), base, "udp").unwrap(); + + let mut agent = IceAgent::new(); + agent.add_remote_candidate(host); + agent.add_remote_candidate(srflx1); + agent.add_remote_candidate(srflx2); + agent.add_remote_candidate(srflx3); + + generate_optimistic_candidates(&mut agent); + + let expected_candidate1 = + Candidate::server_reflexive(SocketAddr::new(addr1, 52625), base, "udp").unwrap(); + let expected_candidate2 = + Candidate::server_reflexive(SocketAddr::new(addr2, 52625), base, "udp").unwrap(); + let unexpected_candidate3 = + Candidate::server_reflexive(SocketAddr::new(addr3, 52625), base, "udp").unwrap(); + + assert!(agent.remote_candidates().contains(&expected_candidate1)); + assert!(agent.remote_candidates().contains(&expected_candidate2)); + assert!(!agent.remote_candidates().contains(&unexpected_candidate3)); + } } diff --git a/website/src/components/Changelog/Gateway.tsx b/website/src/components/Changelog/Gateway.tsx index 99589af52..2b669b817 100644 --- a/website/src/components/Changelog/Gateway.tsx +++ b/website/src/components/Changelog/Gateway.tsx @@ -22,7 +22,12 @@ export default function Gateway() { return ( - + + + Fixes a rare CPU-spike issue in case a Client connected with many + possible IPv6 addresses. + + Remove the FIREZONE_NUM_TUN_THREADS env variable. The Gateway will now