From cbe07caaeaa6ee073270fcf2230ee06666d7b839 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Wed, 26 Nov 2025 14:30:18 +1100 Subject: [PATCH] fix(gateway): always map all proxy IPs (#10972) In a recent refactor, we appear to have broken DNS resources that are IPv6 only. This was caused by a change in how we iterate over the proxy IP mappings. By bailing out as soon as we "run out" of IP mappings, we actually never get to the IPv6 mappings. This PR fixes this behaviour and adds a regression test to ensure we always insert an entry for all proxy IPs. --- rust/libs/connlib/ip-packet/src/make.rs | 9 +- .../tunnel/src/gateway/client_on_gateway.rs | 126 +++++++++++------- website/src/components/Changelog/Gateway.tsx | 6 +- 3 files changed, 90 insertions(+), 51 deletions(-) diff --git a/rust/libs/connlib/ip-packet/src/make.rs b/rust/libs/connlib/ip-packet/src/make.rs index f92a22bec..df0cf9c31 100644 --- a/rust/libs/connlib/ip-packet/src/make.rs +++ b/rust/libs/connlib/ip-packet/src/make.rs @@ -147,15 +147,16 @@ pub struct TcpFlags { pub rst: bool, } -pub fn udp_packet( - saddr: IP, - daddr: IP, +pub fn udp_packet( + saddr: SIP, + daddr: DIP, sport: u16, dport: u16, payload: Vec, ) -> Result where - IP: Into, + SIP: Into, + DIP: Into, { match (saddr.into(), daddr.into()) { (IpAddr::V4(src), IpAddr::V4(dst)) => { diff --git a/rust/libs/connlib/tunnel/src/gateway/client_on_gateway.rs b/rust/libs/connlib/tunnel/src/gateway/client_on_gateway.rs index 0bc99217f..c178ea9bb 100644 --- a/rust/libs/connlib/tunnel/src/gateway/client_on_gateway.rs +++ b/rust/libs/connlib/tunnel/src/gateway/client_on_gateway.rs @@ -10,7 +10,6 @@ use dns_types::DomainName; use ip_network::{IpNetwork, Ipv4Network, Ipv6Network}; use ip_network_table::IpNetworkTable; use ip_packet::{IpPacket, Protocol, UnsupportedProtocol}; -use itertools::{EitherOrBoth, Itertools}; use crate::client::{IPV4_RESOURCES, IPV6_RESOURCES}; use crate::gateway::filter_engine::FilterEngine; @@ -112,43 +111,25 @@ impl ClientOnGateway { tracing::debug!(domain = %name, %resource_id, "No A / AAAA records for domain") } - let ipv4_maps = proxy_ips.iter().filter(|ip| ip.is_ipv4()).zip_longest( - resolved_ips - .iter() - .filter(|ip| ip.is_ipv4()) - .copied() - .cycle(), - ); - let ipv6_maps = proxy_ips.iter().filter(|ip| ip.is_ipv6()).zip_longest( - resolved_ips - .iter() - .filter(|ip| ip.is_ipv6()) - .copied() - .cycle(), - ); + let mut resolved_ipv4 = resolved_ips.iter().filter(|ip| ip.is_ipv4()).cycle(); + let mut resolved_ipv6 = resolved_ips.iter().filter(|ip| ip.is_ipv6()).cycle(); - // Clear out all current translations for these proxy IPs. - for proxy_ip in proxy_ips.iter() { - self.permanent_translations.remove(proxy_ip); - } + tracing::debug!(domain = %name, ?resolved_ips, ?proxy_ips, "Setting up DNS resource NAT"); - for either_or_both in ipv4_maps.chain(ipv6_maps) { - let (proxy_ip, maybe_real_ip) = match either_or_both { - EitherOrBoth::Both(proxy_ip, real_ip) => (proxy_ip, Some(real_ip)), - EitherOrBoth::Left(proxy_ip) => (proxy_ip, None), - EitherOrBoth::Right(_) => break, + for proxy_ip in proxy_ips { + let maybe_real_ip = match proxy_ip { + IpAddr::V4(_) => resolved_ipv4.next(), + IpAddr::V6(_) => resolved_ipv6.next(), }; tracing::debug!(%name, %proxy_ip, real_ip = ?maybe_real_ip); self.permanent_translations.insert( - *proxy_ip, - TranslationState::new(resource_id, maybe_real_ip, name.clone()), + proxy_ip, + TranslationState::new(resource_id, maybe_real_ip.copied(), name.clone()), ); } - tracing::debug!(domain = %name, ?resolved_ips, ?proxy_ips, "Set up DNS resource NAT"); - domains.insert(name, resolved_ips); self.recalculate_filters(); @@ -851,7 +832,7 @@ mod tests { foo_name().parse().unwrap(), foo_resource_id(), BTreeSet::from([foo_real_ip1().into()]), - BTreeSet::from([proxy_ip1().into()]), + BTreeSet::from([proxy_ip4_1()]), ) .unwrap(); @@ -884,7 +865,7 @@ mod tests { let pkt = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, bar_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -898,7 +879,7 @@ mod tests { let pkt = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, foo_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -922,13 +903,13 @@ mod tests { foo_name().parse().unwrap(), foo_resource_id(), BTreeSet::from([foo_real_ip1().into()]), - BTreeSet::from([proxy_ip1().into()]), + BTreeSet::from([proxy_ip4_1()]), ) .unwrap(); let pkt = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, foo_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -939,7 +920,7 @@ mod tests { let pkt = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, 600, vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -953,7 +934,7 @@ mod tests { let pkt = ip_packet::make::udp_packet( client_tun_ipv4(), - "1.1.1.1".parse().unwrap(), + "1.1.1.1".parse::().unwrap(), 1, 600, vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -978,13 +959,13 @@ mod tests { foo_name().parse().unwrap(), foo_resource_id(), BTreeSet::from([foo_real_ip1().into()]), - BTreeSet::from([proxy_ip1().into()]), + BTreeSet::from([proxy_ip4_1()]), ) .unwrap(); let request = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, foo_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -1051,14 +1032,14 @@ mod tests { foo_name().parse().unwrap(), foo_resource_id(), BTreeSet::from([foo_real_ip1().into()]), - BTreeSet::from([proxy_ip1().into(), proxy_ip2().into()]), + BTreeSet::from([proxy_ip4_1(), proxy_ip4_2()]), ) .unwrap(); { let request = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, foo_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -1073,7 +1054,7 @@ mod tests { foo_name().parse().unwrap(), foo_resource_id(), BTreeSet::from([foo_real_ip2().into()]), // Setting up with a new IP! - BTreeSet::from([proxy_ip1().into(), proxy_ip2().into()]), + BTreeSet::from([proxy_ip4_1(), proxy_ip4_2()]), ) .unwrap(); @@ -1096,7 +1077,7 @@ mod tests { { let request = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 2, // Using a new source port foo_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -1146,13 +1127,13 @@ mod tests { foo_name().parse().unwrap(), foo_resource_id(), BTreeSet::from([foo_real_ip1().into()]), - BTreeSet::from([proxy_ip1().into(), proxy_ip2().into()]), + BTreeSet::from([proxy_ip4_1(), proxy_ip4_2()]), ) .unwrap(); let request = ip_packet::make::udp_packet( client_tun_ipv4(), - proxy_ip1(), + proxy_ip4_1(), 1, foo_allowed_port(), vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -1173,7 +1154,7 @@ mod tests { baz_name().parse().unwrap(), baz_resource_id(), BTreeSet::from([baz_real_ip1().into()]), - BTreeSet::from([proxy_ip1().into(), proxy_ip2().into()]), + BTreeSet::from([proxy_ip4_1(), proxy_ip4_2()]), ) .unwrap(); @@ -1185,6 +1166,50 @@ mod tests { assert_eq!(packet.destination(), baz_real_ip1()); } + #[test] + fn setup_dns_resource_nat_ipv4_only_adds_ipv6_translation_state_entries() { + let _guard = logging::test("trace"); + + let now = Instant::now(); + + let mut peer = ClientOnGateway::new( + client_id(), + client_tun(), + gateway_tun(), + flow_tracker::ClientProperties::default(), + ); + peer.add_resource(foo_dns_resource(), None); + peer.setup_nat( + foo_name().parse().unwrap(), + foo_resource_id(), + BTreeSet::from([foo_real_ip1().into()]), + BTreeSet::from([proxy_ip4_1(), proxy_ip4_2(), proxy_ip6_1(), proxy_ip6_2()]), + ) + .unwrap(); + + let request = ip_packet::make::udp_packet( + client_tun_ipv6(), + proxy_ip6_1(), + 1, + foo_allowed_port(), + vec![0, 0, 0, 0, 0, 0, 0, 0], + ) + .unwrap(); + + let TranslateOutboundResult::DestinationUnreachable(packet) = + peer.translate_outbound(request, now).unwrap() + else { + panic!("Bad translation result") + }; + + assert_eq!(packet.destination(), client_tun_ipv6()); + + assert!(peer.permanent_translations.contains_key(&proxy_ip4_1())); + assert!(peer.permanent_translations.contains_key(&proxy_ip4_2())); + assert!(peer.permanent_translations.contains_key(&proxy_ip6_1())); + assert!(peer.permanent_translations.contains_key(&proxy_ip6_2())); + } + fn foo_dns_resource() -> crate::messages::gateway::ResourceDescription { crate::messages::gateway::ResourceDescription::Dns( crate::messages::gateway::ResourceDescriptionDns { @@ -1256,14 +1281,22 @@ mod tests { "10.0.0.1".parse().unwrap() } - fn proxy_ip1() -> Ipv4Addr { + fn proxy_ip4_1() -> IpAddr { "100.96.0.1".parse().unwrap() } - fn proxy_ip2() -> Ipv4Addr { + fn proxy_ip4_2() -> IpAddr { "100.96.0.2".parse().unwrap() } + fn proxy_ip6_1() -> IpAddr { + "fd00:2021:1111:8000::".parse().unwrap() + } + + fn proxy_ip6_2() -> IpAddr { + "fd00:2021:1111:8000::1".parse().unwrap() + } + fn foo_name() -> String { "foo.com".to_string() } @@ -1336,6 +1369,7 @@ mod proptests { }; use crate::proptest::*; use ip_packet::make::{TcpFlags, icmp_request_packet, tcp_packet, udp_packet}; + use itertools::Itertools as _; use proptest::{ arbitrary::any, collection, prop_oneof, diff --git a/website/src/components/Changelog/Gateway.tsx b/website/src/components/Changelog/Gateway.tsx index 30e0990b6..9c10d8880 100644 --- a/website/src/components/Changelog/Gateway.tsx +++ b/website/src/components/Changelog/Gateway.tsx @@ -22,7 +22,11 @@ export default function Gateway() { return ( - + + + Fixes an issue where IPv6-only DNS resources could not be reached. + + Adds a `--log-format` CLI option to output logs as JSON.