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.
This commit is contained in:
Thomas Eizinger
2025-11-26 14:30:18 +11:00
committed by GitHub
parent ae5d80902c
commit cbe07caaea
3 changed files with 90 additions and 51 deletions

View File

@@ -147,15 +147,16 @@ pub struct TcpFlags {
pub rst: bool,
}
pub fn udp_packet<IP>(
saddr: IP,
daddr: IP,
pub fn udp_packet<SIP, DIP>(
saddr: SIP,
daddr: DIP,
sport: u16,
dport: u16,
payload: Vec<u8>,
) -> Result<IpPacket>
where
IP: Into<IpAddr>,
SIP: Into<IpAddr>,
DIP: Into<IpAddr>,
{
match (saddr.into(), daddr.into()) {
(IpAddr::V4(src), IpAddr::V4(dst)) => {

View File

@@ -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::<Ipv4Addr>().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,

View File

@@ -22,7 +22,11 @@ export default function Gateway() {
return (
<Entries downloadLinks={downloadLinks} title="Gateway">
<Unreleased></Unreleased>
<Unreleased>
<ChangeItem pull="10972">
Fixes an issue where IPv6-only DNS resources could not be reached.
</ChangeItem>
</Unreleased>
<Entry version="1.4.18" date={new Date("2025-11-10")}>
<ChangeItem pull="10620">
Adds a `--log-format` CLI option to output logs as JSON.