From 3f4e004a48097115af4dc7f5e8b68baaa2c46c7c Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Sat, 10 May 2025 00:34:21 +0930 Subject: [PATCH] fix(connlib): don't recreate DNS resource NAT for failed domains (#9064) Before a Client can send packets to a DNS resource, the Gateway must first setup a NAT table between the IPs assigned by the Client and the IPs the domain actually resolves to. This is what we call the DNS resource NAT. The communication for this process happens over IP through the tunnel which is an unreliable transport. To ensure that this works reliably even in the presence of packet loss on the wire, the Client uses an idempotent algorithm where it tracks the state of the NAT for each domain that is has ever assigned IPs for (i.e. received an A or AAAA query from an application). This algorithm ensures that if we don't hear anything back from the Gateway within 2s, another packet for setting up the NAT is sent as soon as we receive _any_ DNS query. This design balances efficiency (we don't try forever) with reliability (we always check all of them). In case a domain does not resolve at all or there are resolution errors, the Gateway replies with `NatStatus::Inactive`. At present, the Client doesn't handle this in any particular way other than logging that it was not able to successfully setup the NAT. The combination of the above results in an undesirable behaviour: If an application queries a domain without A and AAAA records once, we will keep retrying forever to resolve it upon every other DNS query issued to the system. To fix this, we introduce `dns_resource_nat::State::Failed`. Entries in this state are ignored as part of the above algorithm and only recreated when explicitly told to do so which we only do when we receive another DNS query for this domain. To handle the increased complexity around this system, we extract it into its own component and add a fleet of unit tests for its behaviour. --- rust/connlib/model/src/lib.rs | 10 +- rust/connlib/tunnel/src/client.rs | 207 ++------ .../tunnel/src/client/dns_resource_nat.rs | 442 ++++++++++++++++++ 3 files changed, 479 insertions(+), 180 deletions(-) create mode 100644 rust/connlib/tunnel/src/client/dns_resource_nat.rs diff --git a/rust/connlib/model/src/lib.rs b/rust/connlib/model/src/lib.rs index 49615651c..8ae784735 100644 --- a/rust/connlib/model/src/lib.rs +++ b/rust/connlib/model/src/lib.rs @@ -28,7 +28,7 @@ pub struct ResourceId(Uuid); pub struct RelayId(Uuid); impl RelayId { - pub fn from_u128(v: u128) -> Self { + pub const fn from_u128(v: u128) -> Self { Self(Uuid::from_u128(v)) } } @@ -46,13 +46,13 @@ impl ResourceId { ResourceId(Uuid::new_v4()) } - pub fn from_u128(v: u128) -> Self { + pub const fn from_u128(v: u128) -> Self { Self(Uuid::from_u128(v)) } } impl GatewayId { - pub fn from_u128(v: u128) -> Self { + pub const fn from_u128(v: u128) -> Self { Self(Uuid::from_u128(v)) } } @@ -69,7 +69,7 @@ impl FromStr for ClientId { } impl ClientId { - pub fn from_u128(v: u128) -> Self { + pub const fn from_u128(v: u128) -> Self { Self(Uuid::from_u128(v)) } } @@ -168,7 +168,7 @@ impl FromStr for SiteId { } impl SiteId { - pub fn from_u128(v: u128) -> Self { + pub const fn from_u128(v: u128) -> Self { Self(Uuid::from_u128(v)) } } diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index 8ddc8f668..34f1c903f 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -1,6 +1,8 @@ +mod dns_resource_nat; mod resource; -use dns_types::{DomainName, ResponseCode}; +use dns_resource_nat::DnsResourceNat; +use dns_types::ResponseCode; pub(crate) use resource::{CidrResource, Resource}; #[cfg(all(feature = "proptest", test))] pub(crate) use resource::{DnsResource, InternetResource}; @@ -28,7 +30,6 @@ use crate::peer::GatewayOnClient; use lru::LruCache; use secrecy::{ExposeSecret as _, Secret}; use snownet::{ClientNode, NoTurnServers, RelaySocket, Transmit}; -use std::collections::hash_map::Entry; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::num::NonZeroUsize; @@ -93,12 +94,7 @@ pub struct ClientState { peers: PeerStore, /// Tracks the flows to resources that we are currently trying to establish. pending_flows: HashMap, - /// Tracks the domains for which we have set up a NAT per gateway. - /// - /// The IPs for DNS resources get assigned on the client. - /// In order to route them to the actual resource, the gateway needs to set up a NAT table. - /// Until the NAT is set up, packets sent to these resources are effectively black-holed. - dns_resource_nat_by_gateway: BTreeMap<(GatewayId, DomainName), DnsResourceNatState>, + dns_resource_nat: DnsResourceNat, /// Tracks which gateway to use for a particular Resource. resources_gateways: HashMap, /// The site a gateway belongs to. @@ -149,41 +145,6 @@ pub struct ClientState { buffered_dns_queries: VecDeque, } -enum DnsResourceNatState { - Pending { - sent_at: Instant, - buffered_packets: UniquePacketBuffer, - - is_recreating: bool, - }, - Recreating, - Confirmed, -} - -impl DnsResourceNatState { - fn num_buffered_packets(&self) -> usize { - match self { - DnsResourceNatState::Pending { - buffered_packets, .. - } => buffered_packets.len(), - DnsResourceNatState::Confirmed => 0, - DnsResourceNatState::Recreating => 0, - } - } - - fn confirm(&mut self) -> impl Iterator + use<> { - let buffered_packets = match std::mem::replace(self, DnsResourceNatState::Confirmed) { - DnsResourceNatState::Pending { - buffered_packets, .. - } => Some(buffered_packets.into_iter()), - DnsResourceNatState::Recreating => None, - DnsResourceNatState::Confirmed => None, - }; - - buffered_packets.into_iter().flatten() - } -} - struct PendingFlow { last_intent_sent_at: Instant, resource_packets: UniquePacketBuffer, @@ -251,7 +212,7 @@ impl ClientState { tcp_dns_server: dns_over_tcp::Server::new(now), tcp_dns_streams_by_upstream_and_query_id: Default::default(), pending_flows: Default::default(), - dns_resource_nat_by_gateway: BTreeMap::new(), + dns_resource_nat: Default::default(), } } @@ -362,8 +323,6 @@ impl ClientState { }, ); - use std::collections::btree_map::Entry; - for (domain, rid, proxy_ips, gid) in self.stub_resolver .resolved_resources() @@ -385,76 +344,24 @@ impl ClientState { .remove(&(*gid, domain)) .unwrap_or_default(); - match self - .dns_resource_nat_by_gateway - .entry((*gid, domain.clone())) - { - Entry::Vacant(v) => { - self.peers - .add_ips_with_resource(gid, proxy_ips.iter().copied(), rid); - let mut buffered_packets = - UniquePacketBuffer::with_capacity_power_of_2(5, "dns-resource-nat-initial"); // 2^5 = 32 - buffered_packets.extend(packets_for_domain); - - v.insert(DnsResourceNatState::Pending { - sent_at: now, - buffered_packets, - - is_recreating: false, - }); - } - Entry::Occupied(mut o) => { - let state = o.get_mut(); - - match state { - DnsResourceNatState::Confirmed => continue, - DnsResourceNatState::Recreating => { - let mut buffered_packets = UniquePacketBuffer::with_capacity_power_of_2( - 5, // 2^5 = 32 - "dns-resource-nat-recreating", - ); - buffered_packets.extend(packets_for_domain); - - *state = DnsResourceNatState::Pending { - sent_at: now, - buffered_packets, - is_recreating: true, - }; - } - DnsResourceNatState::Pending { - sent_at, - buffered_packets, - .. - } => { - let time_since_last_attempt = now.duration_since(*sent_at); - buffered_packets.extend(packets_for_domain); - - if time_since_last_attempt < Duration::from_secs(2) { - continue; - } - - *sent_at = now; - } - } - } - } - - let packet = match p2p_control::dns_resource_nat::assigned_ips( - *rid, + let Some(intent) = self.dns_resource_nat.update( domain.clone(), - proxy_ips.clone(), - ) { - Ok(packet) => packet, - Err(e) => { - tracing::warn!("Failed to create IP packet for `AssignedIp`s event: {e:#}"); - continue; - } + *gid, + *rid, + proxy_ips, + packets_for_domain, + now, + ) else { + continue; }; + self.peers + .add_ips_with_resource(gid, proxy_ips.iter().copied(), rid); + tracing::debug!(%gid, %domain, "Setting up DNS resource NAT"); encapsulate_and_buffer( - packet, + intent, *gid, now, &mut self.node, @@ -463,35 +370,6 @@ impl ClientState { } } - /// Recreate the DNS resource NAT state for a given domain. - /// - /// This will trigger the client to submit another `AssignedIp`s event to the Gateway. - /// On the Gateway, such an event causes a new DNS resolution. - /// - /// We call this function every time a client issues a DNS query for a certain domain. - /// Coupling this behaviour together allows a client to refresh the DNS resolution of a DNS resource on the Gateway - /// through local DNS resolutions. - /// - /// We model the [`DnsResourceNatState::Recreating`] state differently from just removing the entry to allow packets - /// to continue flowing to the Gateway while the DNS resource NAT is being recreated. - /// In most cases, the DNS records will not change and as such, performing this will not interrupt the flow of packets. - fn recreate_dns_resource_nat_for_domain(&mut self, message: &dns_types::Response) { - for state in self - .dns_resource_nat_by_gateway - .iter_mut() - .filter_map(|((_, candidate), b)| (candidate == &message.domain()).then_some(b)) - { - match state { - DnsResourceNatState::Pending { .. } => continue, - DnsResourceNatState::Recreating => continue, - DnsResourceNatState::Confirmed => { - tracing::debug!(domain = %message.domain(), "Re-creating DNS resource NAT"); - *state = DnsResourceNatState::Recreating; - } - } - } - } - fn is_cidr_resource_connected(&self, resource: &ResourceId) -> bool { let Some(gateway_id) = self.resources_gateways.get(resource) else { return false; @@ -549,7 +427,7 @@ impl ClientState { handle_p2p_control_packet( gid, fz_p2p_control, - &mut self.dns_resource_nat_by_gateway, + &mut self.dns_resource_nat, &mut self.node, &mut self.buffered_transmits, now, @@ -625,7 +503,7 @@ impl ClientState { } } - fn encapsulate(&mut self, packet: IpPacket, now: Instant) -> Option { + fn encapsulate(&mut self, mut packet: IpPacket, now: Instant) -> Option { let dst = packet.destination(); if is_definitely_not_a_resource(dst) { @@ -659,17 +537,9 @@ impl ClientState { // Re-send if older than X. if let Some((domain, _)) = self.stub_resolver.resolve_resource_by_ip(&dst) { - if let Some(DnsResourceNatState::Pending { - buffered_packets, - is_recreating: false, // Important: Only buffer if this is the first time we are setting up the DNS resource NAT (i.e. if we are not recreating it). - .. - }) = self - .dns_resource_nat_by_gateway - .get_mut(&(peer.id(), domain.clone())) - { - buffered_packets.push(packet); - return None; - } + packet = self + .dns_resource_nat + .handle_outgoing(peer.id(), domain, packet)?; } let gid = peer.id(); @@ -911,6 +781,8 @@ impl ClientState { trigger: impl Into, now: Instant, ) { + use std::collections::hash_map::Entry; + let trigger = trigger.into(); debug_assert!(self.resources_by_id.contains_key(&resource)); @@ -1014,8 +886,7 @@ impl ClientState { self.peers.remove(disconnected_gateway); self.resources_gateways .retain(|_, g| g != disconnected_gateway); - self.dns_resource_nat_by_gateway - .retain(|(gateway, _), _| gateway != disconnected_gateway); + self.dns_resource_nat.clear_by_gateway(disconnected_gateway); } fn routes(&self) -> impl Iterator + '_ { @@ -1232,7 +1103,7 @@ impl ClientState { match self.stub_resolver.handle(&message) { dns::ResolveStrategy::LocalResponse(response) => { - self.recreate_dns_resource_nat_for_domain(&response); + self.dns_resource_nat.recreate(message.domain()); self.update_dns_resource_nat(now, iter::empty()); unwrap_or_debug!( @@ -1309,7 +1180,7 @@ impl ClientState { return; } - self.recreate_dns_resource_nat_for_domain(&response); + self.dns_resource_nat.recreate(message.domain()); self.update_dns_resource_nat(now, iter::empty()); let maybe_packet = ip_packet::make::udp_packet( @@ -1383,7 +1254,7 @@ impl ClientState { match self.stub_resolver.handle(&query.message) { dns::ResolveStrategy::LocalResponse(response) => { - self.recreate_dns_resource_nat_for_domain(&response); + self.dns_resource_nat.recreate(query.message.domain()); self.update_dns_resource_nat(now, iter::empty()); unwrap_or_debug!( @@ -1624,7 +1495,7 @@ impl ClientState { self.resources_gateways.clear(); // Clear Resource <> Gateway mapping (we will re-create this as new flows are authorized). self.recently_connected_gateways.clear(); // Ensure we don't have sticky gateways when we roam. - self.dns_resource_nat_by_gateway.clear(); // Clear all state related to DNS resource NATs. + self.dns_resource_nat.clear(); // Clear all state related to DNS resource NATs. self.drain_node_events(); // Resetting the client will trigger a failed `QueryResult` for each one that is in-progress. @@ -1818,8 +1689,7 @@ impl ClientState { .resolved_resources() .filter_map(|(domain, candidate, _)| (candidate == &id).then_some(domain)) { - self.dns_resource_nat_by_gateway - .retain(|(_, candidate), _| candidate != domain); + self.dns_resource_nat.clear_by_domain(domain); } } @@ -1911,7 +1781,7 @@ fn encapsulate_and_buffer( fn handle_p2p_control_packet( gid: GatewayId, fz_p2p_control: ip_packet::FzP2pControlSlice, - dns_resource_nat_by_gateway: &mut BTreeMap<(GatewayId, DomainName), DnsResourceNatState>, + dns_resource_nat: &mut DnsResourceNat, node: &mut ClientNode, buffered_transmits: &mut VecDeque, now: Instant, @@ -1926,20 +1796,7 @@ fn handle_p2p_control_packet( return; }; - if res.status != dns_resource_nat::NatStatus::Active { - tracing::debug!(%gid, domain = %res.domain, "DNS resource NAT is not active"); - return; - } - - let Some(nat_state) = dns_resource_nat_by_gateway.get_mut(&(gid, res.domain.clone())) - else { - tracing::debug!(%gid, domain = %res.domain, "No DNS resource NAT state, ignoring response"); - return; - }; - - tracing::debug!(%gid, domain = %res.domain, num_buffered_packets = %nat_state.num_buffered_packets(), "DNS resource NAT is active"); - - let buffered_packets = nat_state.confirm(); + let buffered_packets = dns_resource_nat.on_domain_status(gid, res); for packet in buffered_packets { encapsulate_and_buffer(packet, gid, now, node, buffered_transmits); diff --git a/rust/connlib/tunnel/src/client/dns_resource_nat.rs b/rust/connlib/tunnel/src/client/dns_resource_nat.rs new file mode 100644 index 000000000..eb5195602 --- /dev/null +++ b/rust/connlib/tunnel/src/client/dns_resource_nat.rs @@ -0,0 +1,442 @@ +use std::{ + collections::{BTreeMap, VecDeque, btree_map::Entry}, + net::IpAddr, + time::{Duration, Instant}, +}; + +use connlib_model::{GatewayId, ResourceId}; +use dns_types::DomainName; +use ip_packet::IpPacket; + +use crate::{p2p_control, unique_packet_buffer::UniquePacketBuffer}; + +/// Tracks the domains for which we have set up a NAT per gateway. +/// +/// The IPs for DNS resources get assigned on the client. +/// In order to route them to the actual resource, the gateway needs to set up a NAT table. +/// Until the NAT is set up, packets sent to these resources are effectively black-holed. +#[derive(Default)] +pub struct DnsResourceNat { + inner: BTreeMap<(GatewayId, DomainName), State>, +} + +impl DnsResourceNat { + pub fn update( + &mut self, + domain: DomainName, + gid: GatewayId, + rid: ResourceId, + proxy_ips: &[IpAddr], + packets_for_domain: VecDeque, + now: Instant, + ) -> Option { + match self.inner.entry((gid, domain.clone())) { + Entry::Vacant(v) => { + let mut buffered_packets = + UniquePacketBuffer::with_capacity_power_of_2(5, "dns-resource-nat-initial"); // 2^5 = 32 + buffered_packets.extend(packets_for_domain); + + v.insert(State::Pending { + sent_at: now, + buffered_packets, + + should_buffer: true, + }); + } + Entry::Occupied(mut o) => { + let state = o.get_mut(); + + match state { + State::Confirmed => return None, + State::Failed => return None, + State::Recreating { should_buffer } => { + let mut buffered_packets = UniquePacketBuffer::with_capacity_power_of_2( + 5, // 2^5 = 32 + "dns-resource-nat-recreating", + ); + buffered_packets.extend(packets_for_domain); + + *state = State::Pending { + sent_at: now, + buffered_packets, + should_buffer: *should_buffer, + }; + } + State::Pending { + sent_at, + buffered_packets, + .. + } => { + let time_since_last_attempt = now.duration_since(*sent_at); + buffered_packets.extend(packets_for_domain); + + if time_since_last_attempt < Duration::from_secs(2) { + return None; + } + + *sent_at = now; + } + } + } + } + + let packet = + match p2p_control::dns_resource_nat::assigned_ips(rid, domain, proxy_ips.to_vec()) { + Ok(packet) => packet, + Err(e) => { + tracing::warn!("Failed to create IP packet for `AssignedIp`s event: {e:#}"); + return None; + } + }; + + Some(packet) + } + + /// Recreate the DNS resource NAT state for a given domain. + /// + /// This will trigger the client to submit another `AssignedIp`s event to the Gateway. + /// On the Gateway, such an event causes a new DNS resolution. + /// + /// We call this function every time a client issues a DNS query for a certain domain. + /// Coupling this behaviour together allows a client to refresh the DNS resolution of a DNS resource on the Gateway + /// through local DNS resolutions. + /// + /// We model the [`State::Recreating`] state differently from just removing the entry to allow packets + /// to continue flowing to the Gateway while the DNS resource NAT is being recreated. + /// In most cases, the DNS records will not change and as such, performing this will not interrupt the flow of packets. + pub fn recreate(&mut self, domain: DomainName) { + for state in self + .inner + .iter_mut() + .filter_map(|((_, candidate), b)| (candidate == &domain).then_some(b)) + { + let should_buffer = match state { + State::Recreating { .. } | State::Pending { .. } => continue, + State::Confirmed => false, // Don't buffer packets if already confirmed. + State::Failed => true, // No NAT yet, buffer packets until confirmed. + }; + + tracing::debug!(%domain, "Re-creating DNS resource NAT"); + *state = State::Recreating { should_buffer }; + } + } + + /// Handles an outgoing packet for a DNS resource. + /// + /// If the DNS resource NAT is still being created, the packet gets buffered. + /// Otherwise, it is returned again. + pub fn handle_outgoing( + &mut self, + gid: GatewayId, + domain: &DomainName, + packet: IpPacket, + ) -> Option { + let Some(state) = self.inner.get_mut(&(gid, domain.clone())) else { + tracing::debug!(%gid, %domain, "No DNS resource NAT entry"); + + return Some(packet); // Pass-through packet. + }; + + match state { + State::Pending { + should_buffer: true, + buffered_packets, + .. + } => { + buffered_packets.push(packet); + None + } + State::Pending { .. } | State::Recreating { .. } | State::Confirmed | State::Failed => { + // Some of these might be black-holed on the Gateway (i.e. in `Failed`). + // But there isn't much we can do ... + Some(packet) + } + } + } + + pub fn clear_by_gateway(&mut self, gid: &GatewayId) { + self.inner.retain(|(gateway, _), _| gateway != gid); + } + + pub fn clear_by_domain(&mut self, domain: &DomainName) { + self.inner.retain(|(_, candidate), _| candidate != domain); + } + + pub fn clear(&mut self) { + self.inner.clear(); + } + + pub(crate) fn on_domain_status( + &mut self, + gid: GatewayId, + res: p2p_control::dns_resource_nat::DomainStatus, + ) -> impl IntoIterator { + let Entry::Occupied(mut nat_entry) = self.inner.entry((gid, res.domain.clone())) else { + tracing::debug!(%gid, domain = %res.domain, "No DNS resource NAT state, ignoring response"); + return into_iter(None); + }; + + let nat_state = nat_entry.get_mut(); + + if res.status != p2p_control::dns_resource_nat::NatStatus::Active { + tracing::debug!(%gid, domain = %res.domain, "DNS resource NAT is not active"); + nat_state.failed(); + return into_iter(None); + } + + tracing::debug!(%gid, domain = %res.domain, num_buffered_packets = %nat_state.num_buffered_packets(), "DNS resource NAT is active"); + + into_iter(Some(nat_state.confirm())) + } +} + +fn into_iter(option: Option) -> impl IntoIterator +where + T: IntoIterator, +{ + option.into_iter().flatten() +} + +enum State { + Pending { + sent_at: Instant, + buffered_packets: UniquePacketBuffer, + + should_buffer: bool, + }, + Recreating { + should_buffer: bool, + }, + Confirmed, + Failed, +} + +impl State { + fn num_buffered_packets(&self) -> usize { + match self { + State::Pending { + buffered_packets, .. + } => buffered_packets.len(), + State::Confirmed => 0, + State::Recreating { .. } => 0, + State::Failed => 0, + } + } + + fn confirm(&mut self) -> impl Iterator + use<> { + let buffered_packets = match std::mem::replace(self, State::Confirmed) { + State::Pending { + buffered_packets, .. + } => Some(buffered_packets.into_iter()), + State::Recreating { .. } => None, + State::Confirmed => None, + State::Failed => None, + }; + + buffered_packets.into_iter().flatten() + } + + fn failed(&mut self) { + *self = State::Failed; + } +} + +#[cfg(test)] +mod tests { + use std::net::Ipv4Addr; + + use dns_types::DomainNameRef; + use dns_types::prelude::*; + + use super::*; + + #[test] + fn no_recreate_nat_for_failed_response() { + let mut dns_resource_nat = DnsResourceNat::default(); + + let intent = dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + assert!(intent.is_some()); + + dns_resource_nat.on_domain_status( + GID, + p2p_control::dns_resource_nat::DomainStatus { + status: p2p_control::dns_resource_nat::NatStatus::Inactive, + resource: RID, + domain: EXAMPLE_COM.to_vec(), + }, + ); + + let intent = dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + assert!(intent.is_none()); + } + + #[test] + fn recreate_failed_nat() { + let mut dns_resource_nat = DnsResourceNat::default(); + + dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + dns_resource_nat.on_domain_status( + GID, + p2p_control::dns_resource_nat::DomainStatus { + status: p2p_control::dns_resource_nat::NatStatus::Inactive, + resource: RID, + domain: EXAMPLE_COM.to_vec(), + }, + ); + + dns_resource_nat.recreate(EXAMPLE_COM.to_vec()); + + let intent = dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + assert!(intent.is_some()); + + // Should buffer packets if we are coming from `Failed`. + let packet = + ip_packet::make::udp_packet(Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST, 0, 0, vec![]) + .unwrap(); + + let maybe_packet = dns_resource_nat.handle_outgoing(GID, &EXAMPLE_COM.to_vec(), packet); + + assert!(maybe_packet.is_none()); + } + + #[test] + fn buffer_packets_until_nat_is_active() { + let mut dns_resource_nat = DnsResourceNat::default(); + + dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + + let packet = + ip_packet::make::udp_packet(Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST, 0, 0, vec![]) + .unwrap(); + + let maybe_packet = + dns_resource_nat.handle_outgoing(GID, &EXAMPLE_COM.to_vec(), packet.clone()); + + assert!(maybe_packet.is_none()); + + let packets = dns_resource_nat.on_domain_status( + GID, + p2p_control::dns_resource_nat::DomainStatus { + status: p2p_control::dns_resource_nat::NatStatus::Active, + resource: RID, + domain: EXAMPLE_COM.to_vec(), + }, + ); + + assert_eq!(packets.into_iter().collect::>(), vec![packet]); + } + + #[test] + fn dont_buffer_packets_upon_recreate() { + let mut dns_resource_nat = DnsResourceNat::default(); + + dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + dns_resource_nat.on_domain_status( + GID, + p2p_control::dns_resource_nat::DomainStatus { + status: p2p_control::dns_resource_nat::NatStatus::Active, + resource: RID, + domain: EXAMPLE_COM.to_vec(), + }, + ); + + dns_resource_nat.recreate(EXAMPLE_COM.to_vec()); + dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + Instant::now(), + ); + + let packet = + ip_packet::make::udp_packet(Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST, 0, 0, vec![]) + .unwrap(); + + let maybe_packet = dns_resource_nat.handle_outgoing(GID, &EXAMPLE_COM.to_vec(), packet); + + assert!(maybe_packet.is_some()); + } + + #[test] + fn resend_intent_after_2_seconds() { + let mut dns_resource_nat = DnsResourceNat::default(); + let mut now = Instant::now(); + + let mut update_fn = |now| { + dns_resource_nat.update( + EXAMPLE_COM.to_vec(), + GID, + RID, + PROXY_IPS, + VecDeque::default(), + now, + ) + }; + + assert!(update_fn(now).is_some()); + assert!(update_fn(now).is_none()); + + now += Duration::from_secs(2); + + assert!(update_fn(now).is_some()); + } + + const EXAMPLE_COM: DomainNameRef = + unsafe { DomainNameRef::from_octets_unchecked(b"\x08example\x03com\x00") }; + const GID: GatewayId = GatewayId::from_u128(1); + const RID: ResourceId = ResourceId::from_u128(2); + const PROXY_IPS: &[IpAddr] = &[ + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 3)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 4)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 5)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 6)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 7)), + IpAddr::V4(Ipv4Addr::new(127, 0, 0, 8)), + ]; +}