diff --git a/rust/connlib/tunnel/src/peer.rs b/rust/connlib/tunnel/src/peer.rs
index e22b84890..eedae2d9b 100644
--- a/rust/connlib/tunnel/src/peer.rs
+++ b/rust/connlib/tunnel/src/peer.rs
@@ -147,6 +147,11 @@ impl ClientOnGateway {
for (proxy_ip, real_ip) in ip_maps {
tracing::debug!(%name, %proxy_ip, %real_ip);
+ if self.nat_table.has_entry_for_inside(*proxy_ip) {
+ tracing::debug!(%name, %proxy_ip, %real_ip, "Skipping DNS resource NAT entry because we have open NAT sessions for it");
+ continue;
+ }
+
self.permanent_translations
.insert(*proxy_ip, TranslationState::new(resource_id, real_ip));
}
@@ -852,12 +857,12 @@ mod tests {
peer.setup_nat(
foo_name().parse().unwrap(),
resource_id(),
- BTreeSet::from([foo_real_ip().into()]),
- BTreeSet::from([foo_proxy_ip().into()]),
+ BTreeSet::from([foo_real_ip1().into()]),
+ BTreeSet::from([foo_proxy_ip1().into()]),
)
.unwrap();
- assert_eq!(bar_contained_ip(), foo_real_ip());
+ assert_eq!(bar_contained_ip(), foo_real_ip1());
let pkt = ip_packet::make::udp_packet(
client_tun_ipv4(),
@@ -886,7 +891,7 @@ mod tests {
let pkt = ip_packet::make::udp_packet(
client_tun_ipv4(),
- foo_proxy_ip(),
+ foo_proxy_ip1(),
1,
bar_allowed_port(),
vec![0, 0, 0, 0, 0, 0, 0, 0],
@@ -900,7 +905,7 @@ mod tests {
let pkt = ip_packet::make::udp_packet(
client_tun_ipv4(),
- foo_proxy_ip(),
+ foo_proxy_ip1(),
1,
foo_allowed_port(),
vec![0, 0, 0, 0, 0, 0, 0, 0],
@@ -918,14 +923,14 @@ mod tests {
peer.setup_nat(
foo_name().parse().unwrap(),
resource_id(),
- BTreeSet::from([foo_real_ip().into()]),
- BTreeSet::from([foo_proxy_ip().into()]),
+ BTreeSet::from([foo_real_ip1().into()]),
+ BTreeSet::from([foo_proxy_ip1().into()]),
)
.unwrap();
let pkt = ip_packet::make::udp_packet(
client_tun_ipv4(),
- foo_proxy_ip(),
+ foo_proxy_ip1(),
1,
foo_allowed_port(),
vec![0, 0, 0, 0, 0, 0, 0, 0],
@@ -936,7 +941,7 @@ mod tests {
let pkt = ip_packet::make::udp_packet(
client_tun_ipv4(),
- foo_proxy_ip(),
+ foo_proxy_ip1(),
1,
600,
vec![0, 0, 0, 0, 0, 0, 0, 0],
@@ -969,14 +974,14 @@ mod tests {
peer.setup_nat(
foo_name().parse().unwrap(),
resource_id(),
- BTreeSet::from([foo_real_ip().into()]),
- BTreeSet::from([foo_proxy_ip().into()]),
+ BTreeSet::from([foo_real_ip1().into()]),
+ BTreeSet::from([foo_proxy_ip1().into()]),
)
.unwrap();
let request = ip_packet::make::udp_packet(
client_tun_ipv4(),
- foo_proxy_ip(),
+ foo_proxy_ip1(),
1,
foo_allowed_port(),
vec![0, 0, 0, 0, 0, 0, 0, 0],
@@ -991,7 +996,7 @@ mod tests {
));
let response = ip_packet::make::udp_packet(
- foo_real_ip(),
+ foo_real_ip1(),
client_tun_ipv4(),
foo_allowed_port(),
1,
@@ -1008,7 +1013,7 @@ mod tests {
);
let response = ip_packet::make::udp_packet(
- foo_real_ip(),
+ foo_real_ip1(),
client_tun_ipv4(),
foo_allowed_port(),
1,
@@ -1025,6 +1030,61 @@ mod tests {
);
}
+ #[test]
+ fn setting_up_dns_resource_nat_does_not_clear_existing_nat_session() {
+ let _guard = firezone_logging::test("trace");
+
+ let now = Instant::now();
+
+ let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun());
+ peer.add_resource(foo_dns_resource(), None);
+ peer.setup_nat(
+ foo_name().parse().unwrap(),
+ resource_id(),
+ BTreeSet::from([foo_real_ip1().into()]),
+ BTreeSet::from([foo_proxy_ip1().into(), foo_proxy_ip2().into()]),
+ )
+ .unwrap();
+
+ let request = ip_packet::make::udp_packet(
+ client_tun_ipv4(),
+ foo_proxy_ip1(),
+ 1,
+ foo_allowed_port(),
+ vec![0, 0, 0, 0, 0, 0, 0, 0],
+ )
+ .unwrap();
+
+ let result = peer.translate_outbound(request.clone(), now).unwrap();
+
+ assert!(matches!(result, TranslateOutboundResult::Send(_)));
+
+ peer.setup_nat(
+ foo_name().parse().unwrap(),
+ resource_id(),
+ BTreeSet::from([foo_real_ip2().into()]), // Setting up with a new IP!
+ BTreeSet::from([foo_proxy_ip1().into(), foo_proxy_ip2().into()]),
+ )
+ .unwrap();
+
+ let result = peer.translate_outbound(request, now).unwrap();
+
+ assert!(matches!(result, TranslateOutboundResult::Send(_)));
+
+ let response = ip_packet::make::udp_packet(
+ foo_real_ip1(),
+ client_tun_ipv4(),
+ foo_allowed_port(),
+ 1,
+ vec![0, 0, 0, 0, 0, 0, 0, 0],
+ )
+ .unwrap();
+
+ let response = peer.translate_inbound(response, now).unwrap();
+
+ assert!(response.is_some());
+ }
+
fn foo_dns_resource() -> crate::messages::gateway::ResourceDescription {
crate::messages::gateway::ResourceDescription::Dns(
crate::messages::gateway::ResourceDescriptionDns {
@@ -1069,18 +1129,26 @@ mod tests {
443
}
- fn foo_real_ip() -> Ipv4Addr {
+ fn foo_real_ip1() -> Ipv4Addr {
"10.0.0.1".parse().unwrap()
}
+ fn foo_real_ip2() -> Ipv4Addr {
+ "10.0.0.2".parse().unwrap()
+ }
+
fn bar_contained_ip() -> Ipv4Addr {
"10.0.0.1".parse().unwrap()
}
- fn foo_proxy_ip() -> Ipv4Addr {
+ fn foo_proxy_ip1() -> Ipv4Addr {
"100.96.0.1".parse().unwrap()
}
+ fn foo_proxy_ip2() -> Ipv4Addr {
+ "100.96.0.2".parse().unwrap()
+ }
+
fn foo_name() -> String {
"foo.com".to_string()
}
diff --git a/rust/connlib/tunnel/src/peer/nat_table.rs b/rust/connlib/tunnel/src/peer/nat_table.rs
index ea8a1ce6c..c735a2f8f 100644
--- a/rust/connlib/tunnel/src/peer/nat_table.rs
+++ b/rust/connlib/tunnel/src/peer/nat_table.rs
@@ -39,6 +39,11 @@ impl NatTable {
}
}
+ /// Returns true if the NAT table has any entries with the given "inside" IP address.
+ pub(crate) fn has_entry_for_inside(&self, ip: IpAddr) -> bool {
+ self.table.left_values().any(|(_, c)| c == &ip)
+ }
+
pub(crate) fn translate_outgoing(
&mut self,
packet: &IpPacket,
diff --git a/website/src/components/Changelog/Gateway.tsx b/website/src/components/Changelog/Gateway.tsx
index d6b20e830..9bfcf3442 100644
--- a/website/src/components/Changelog/Gateway.tsx
+++ b/website/src/components/Changelog/Gateway.tsx
@@ -36,6 +36,10 @@ export default function Gateway() {
Improves connection reliability by maintaining the order of IP packets
across GSO batches.
+
+ Fixes an issue where connections to DNS resources which utilise round-robin
+ DNS may be interrupted whenever the Client re-queried the DNS name.
+