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)), + ]; +}