From 315d99f7238d881c01cd79a57cc1ed745fa59535 Mon Sep 17 00:00:00 2001 From: Thomas Eizinger Date: Fri, 28 Feb 2025 10:49:05 +1100 Subject: [PATCH] feat(gateway): allow tunneling packets to and from TUN device (#8283) At present, Clients are only allowed to send packets to resources accessible via the Gateway but not to the Gateway itself. Thus, any application (including Firezone itself) that opens a listening socket on the TUN device will never receive any traffic. This has opens up interesting features like hosting additional services on the machine that the Gateway is running on. Concretely, in order to implement #8221, we will run a DNS server on port 53 of the TUN device as part of the Gateway. The diff for this ended up being a bit larger because we are introducing an `IpConfig` abstraction so we don't have to track 4 IP addresses as separate fields within `ClientOnGateway`; the connection-specific state on a Gateway. This is where we allow / deny traffic from a Client. To allow traffic for this particular Gateway, we need to know our own TUN IP configuration within the component. --- rust/connlib/clients/shared/src/eventloop.rs | 4 +- rust/connlib/tunnel/src/client.rs | 30 ++- rust/connlib/tunnel/src/gateway.rs | 25 ++- rust/connlib/tunnel/src/lib.rs | 9 +- rust/connlib/tunnel/src/peer.rs | 211 ++++++++++++++----- rust/connlib/tunnel/src/tests/sim_gateway.rs | 34 ++- rust/connlib/tunnel/src/tests/stub_portal.rs | 48 ++++- rust/connlib/tunnel/src/tests/sut.rs | 11 +- rust/gateway/src/eventloop.rs | 24 ++- 9 files changed, 288 insertions(+), 108 deletions(-) diff --git a/rust/connlib/clients/shared/src/eventloop.rs b/rust/connlib/clients/shared/src/eventloop.rs index 6e06c68f4..e55c474e7 100644 --- a/rust/connlib/clients/shared/src/eventloop.rs +++ b/rust/connlib/clients/shared/src/eventloop.rs @@ -169,8 +169,8 @@ where let dns_servers = config.dns_by_sentinel.left_values().copied().collect(); self.callbacks.on_set_interface_config( - config.ip4, - config.ip6, + config.ip.v4, + config.ip.v6, dns_servers, Vec::from_iter(config.ipv4_routes), Vec::from_iter(config.ipv6_routes), diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index 1d1b0537e..15446e012 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -9,7 +9,7 @@ use crate::messages::{DnsServer, Interface as InterfaceConfig, IpDnsServer}; use crate::messages::{IceCredentials, SecretKey}; use crate::peer_store::PeerStore; use crate::unique_packet_buffer::UniquePacketBuffer; -use crate::{dns, p2p_control, TunConfig}; +use crate::{dns, p2p_control, IpConfig, TunConfig}; use anyhow::Context; use bimap::BiMap; use connlib_model::{ @@ -234,20 +234,15 @@ impl ClientState { } #[cfg(all(test, feature = "proptest"))] - pub(crate) fn tunnel_ip4(&self) -> Option { - Some(self.tun_config.as_ref()?.ip4) - } - - #[cfg(all(test, feature = "proptest"))] - pub(crate) fn tunnel_ip6(&self) -> Option { - Some(self.tun_config.as_ref()?.ip6) + pub(crate) fn tunnel_ip_config(&self) -> Option { + Some(self.tun_config.as_ref()?.ip) } #[cfg(all(test, feature = "proptest"))] pub(crate) fn tunnel_ip_for(&self, dst: IpAddr) -> Option { Some(match dst { - IpAddr::V4(_) => self.tunnel_ip4()?.into(), - IpAddr::V6(_) => self.tunnel_ip6()?.into(), + IpAddr::V4(_) => self.tunnel_ip_config()?.v4.into(), + IpAddr::V6(_) => self.tunnel_ip_config()?.v6.into(), }) } @@ -865,7 +860,7 @@ impl ClientState { }; self.tcp_dns_client - .set_source_interface(tun_config.ip4, tun_config.ip6); + .set_source_interface(tun_config.ip.v4, tun_config.ip.v6); let upstream_resolvers = self .dns_mapping @@ -991,8 +986,8 @@ impl ClientState { match self.tun_config.as_mut() { Some(existing) => { // We don't really expect these to change but let's update them anyway. - existing.ip4 = config.ipv4; - existing.ip6 = config.ipv6; + existing.ip.v4 = config.ipv4; + existing.ip.v6 = config.ipv6; } None => { let (ipv4_routes, ipv6_routes) = self.routes().partition_map(|route| match route { @@ -1000,8 +995,10 @@ impl ClientState { IpNetwork::V6(v6) => itertools::Either::Right(v6), }); let new_tun_config = TunConfig { - ip4: config.ipv4, - ip6: config.ipv6, + ip: IpConfig { + v4: config.ipv4, + v6: config.ipv6, + }, dns_by_sentinel: Default::default(), ipv4_routes, ipv6_routes, @@ -1596,8 +1593,7 @@ impl ClientState { }); let new_tun_config = TunConfig { - ip4: config.ip4, - ip6: config.ip6, + ip: config.ip, dns_by_sentinel: dns_mapping .iter() .map(|(sentinel_dns, effective_dns)| (*sentinel_dns, effective_dns.address())) diff --git a/rust/connlib/tunnel/src/gateway.rs b/rust/connlib/tunnel/src/gateway.rs index 78b563f16..821529dae 100644 --- a/rust/connlib/tunnel/src/gateway.rs +++ b/rust/connlib/tunnel/src/gateway.rs @@ -1,7 +1,7 @@ use crate::messages::gateway::ResourceDescription; use crate::messages::{Answer, IceCredentials, ResolveRequest, SecretKey}; use crate::utils::earliest; -use crate::{p2p_control, GatewayEvent}; +use crate::{p2p_control, GatewayEvent, IpConfig}; use crate::{peer::ClientOnGateway, peer_store::PeerStore}; use anyhow::{Context, Result}; use boringtun::x25519::PublicKey; @@ -42,6 +42,8 @@ pub struct GatewayState { /// When to next check whether a resource-access policy has expired. next_expiry_resources_check: Option, + tun_ip_config: Option, + buffered_events: VecDeque, buffered_transmits: VecDeque>, } @@ -71,6 +73,7 @@ impl GatewayState { next_expiry_resources_check: Default::default(), buffered_events: VecDeque::default(), buffered_transmits: VecDeque::default(), + tun_ip_config: None, } } @@ -227,8 +230,7 @@ impl GatewayState { preshared_key: SecretKey, client_ice: IceCredentials, gateway_ice: IceCredentials, - ipv4: Ipv4Addr, - ipv6: Ipv6Addr, + client_tun: IpConfig, expires_at: Option>, resource: ResourceDescription, now: Instant, @@ -248,7 +250,7 @@ impl GatewayState { now, )?; - let result = self.allow_access(client_id, ipv4, ipv6, expires_at, resource, None); + let result = self.allow_access(client_id, client_tun, expires_at, resource, None); debug_assert!( result.is_ok(), "`allow_access` should never fail without a `DnsResourceEntry`" @@ -260,16 +262,17 @@ impl GatewayState { pub fn allow_access( &mut self, client: ClientId, - ipv4: Ipv4Addr, - ipv6: Ipv6Addr, + client_tun: IpConfig, expires_at: Option>, resource: ResourceDescription, dns_resource_nat: Option, ) -> anyhow::Result<()> { + let gateway_tun = self.tun_ip_config.context("TUN device not configured")?; + let peer = self .peers .entry(client) - .or_insert_with(|| ClientOnGateway::new(client, ipv4, ipv6)); + .or_insert_with(|| ClientOnGateway::new(client, client_tun, gateway_tun)); peer.add_resource(resource.clone(), expires_at); @@ -282,8 +285,8 @@ impl GatewayState { )?; } - self.peers.add_ip(&client, &ipv4.into()); - self.peers.add_ip(&client, &ipv6.into()); + self.peers.add_ip(&client, &client_tun.v4.into()); + self.peers.add_ip(&client, &client_tun.v6.into()); Ok(()) } @@ -428,6 +431,10 @@ impl GatewayState { self.node.update_relays(to_remove, &to_add, now); self.drain_node_events() } + + pub fn update_tun_device(&mut self, config: IpConfig) { + self.tun_ip_config = Some(config); + } } fn handle_p2p_control_packet( diff --git a/rust/connlib/tunnel/src/lib.rs b/rust/connlib/tunnel/src/lib.rs index 3b182b772..d6352e289 100644 --- a/rust/connlib/tunnel/src/lib.rs +++ b/rust/connlib/tunnel/src/lib.rs @@ -304,8 +304,7 @@ pub enum ClientEvent { #[derive(Clone, derive_more::Debug, PartialEq, Eq)] pub struct TunConfig { - pub ip4: Ipv4Addr, - pub ip6: Ipv6Addr, + pub ip: IpConfig, /// The map of DNS servers that connlib will use. /// /// - The "left" values are the connlib-assigned, proxy (or "sentinel") IPs. @@ -320,6 +319,12 @@ pub struct TunConfig { pub ipv6_routes: BTreeSet, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct IpConfig { + pub v4: Ipv4Addr, + pub v6: Ipv6Addr, +} + #[derive(Debug)] pub enum GatewayEvent { AddedIceCandidates { diff --git a/rust/connlib/tunnel/src/peer.rs b/rust/connlib/tunnel/src/peer.rs index d4928b9ce..a4021e6a3 100644 --- a/rust/connlib/tunnel/src/peer.rs +++ b/rust/connlib/tunnel/src/peer.rs @@ -1,6 +1,6 @@ use std::collections::{hash_map, BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; use std::iter; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::net::IpAddr; use std::time::Instant; use crate::client::{IPV4_RESOURCES, IPV6_RESOURCES}; @@ -14,7 +14,7 @@ use ip_network_table::IpNetworkTable; use ip_packet::{IpPacket, Protocol, UnsupportedProtocol}; use crate::utils::network_contains_network; -use crate::GatewayEvent; +use crate::{GatewayEvent, IpConfig}; use anyhow::{bail, Context, Result}; use nat_table::{NatTable, TranslateIncomingResult}; @@ -50,8 +50,10 @@ impl GatewayOnClient { /// The state of one client on a gateway. pub struct ClientOnGateway { id: ClientId, - ipv4: Ipv4Addr, - ipv6: Ipv6Addr, + + client_tun: IpConfig, + gateway_tun: IpConfig, + resources: HashMap, /// Caches the existence of internet resource internet_resource_enabled: bool, @@ -62,11 +64,15 @@ pub struct ClientOnGateway { } impl ClientOnGateway { - pub(crate) fn new(id: ClientId, ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> ClientOnGateway { + pub(crate) fn new( + id: ClientId, + client_tun: IpConfig, + gateway_tun: IpConfig, + ) -> ClientOnGateway { ClientOnGateway { id, - ipv4, - ipv6, + client_tun, + gateway_tun, resources: HashMap::new(), filters: IpNetworkTable::new(), permanent_translations: Default::default(), @@ -80,7 +86,10 @@ impl ClientOnGateway { /// /// Failure to enforce this would allow one client to send traffic masquarading as a different client. fn allowed_ips(&self) -> [IpAddr; 2] { - [IpAddr::from(self.ipv4), IpAddr::from(self.ipv6)] + [ + IpAddr::from(self.client_tun.v4), + IpAddr::from(self.client_tun.v6), + ] } /// Setup the NAT for a particular domain within a wildcard DNS resource. @@ -259,7 +268,12 @@ impl ClientOnGateway { .translate_outgoing(&packet, state.resolved_ip, now)?; let mut packet = packet - .translate_destination(self.ipv4, self.ipv6, source_protocol, real_ip) + .translate_destination( + self.client_tun.v4, + self.client_tun.v6, + source_protocol, + real_ip, + ) .context("Failed to translate packet to new destination")?; packet.update_checksum(); @@ -271,6 +285,13 @@ impl ClientOnGateway { packet: IpPacket, now: Instant, ) -> anyhow::Result> { + // Traffic to our own IP is allowed. + match packet.destination() { + IpAddr::V4(dst) if dst == self.gateway_tun.v4 => return Ok(Some(packet)), + IpAddr::V6(dst) if dst == self.gateway_tun.v6 => return Ok(Some(packet)), + IpAddr::V4(_) | IpAddr::V6(_) => {} + } + // Filtering a packet is not an error. if let Err(e) = self.ensure_allowed_dst(&packet) { tracing::debug!(filtered_packet = ?packet, "{e:#}"); @@ -288,6 +309,13 @@ impl ClientOnGateway { packet: IpPacket, now: Instant, ) -> anyhow::Result> { + // Traffic from our own IP is allowed. + match packet.source() { + IpAddr::V4(src) if src == self.gateway_tun.v4 => return Ok(Some(packet)), + IpAddr::V6(src) if src == self.gateway_tun.v6 => return Ok(Some(packet)), + IpAddr::V4(_) | IpAddr::V6(_) => {} + } + let Some(packet) = self.transform_tun_to_network(packet, now)? else { return Ok(None); }; @@ -316,7 +344,7 @@ impl ClientOnGateway { tracing::debug!(dst = %prototype.outside_dst(), proxy_ip = %prototype.inside_dst(), error = ?prototype.error(), "Destination is unreachable"); let icmp_error = prototype - .into_packet(self.ipv4, self.ipv6) + .into_packet(self.client_tun.v4, self.client_tun.v6) .context("Failed to create `DestinationUnreachable` ICMP error")?; return Ok(Some(icmp_error)); @@ -337,7 +365,7 @@ impl ClientOnGateway { }; let mut packet = packet - .translate_source(self.ipv4, self.ipv6, proto, ip) + .translate_source(self.client_tun.v4, self.client_tun.v6, proto, ip) .context("Failed to translate packet to new source")?; packet.update_checksum(); @@ -590,6 +618,7 @@ mod tests { use crate::{ messages::gateway::{Filter, PortRange, ResourceDescription, ResourceDescriptionCidr}, peer::nat_table, + IpConfig, }; use chrono::Utc; use connlib_model::{ClientId, ResourceId}; @@ -599,7 +628,7 @@ mod tests { #[test] fn gateway_filters_expire_individually() { - let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr()); + let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun()); let now = Utc::now(); let then = now + Duration::from_secs(10); let after_then = then + Duration::from_secs(10); @@ -629,7 +658,7 @@ mod tests { ); let tcp_packet = ip_packet::make::tcp_packet( - source_v4_addr(), + client_tun_ipv4(), cidr_v4_resource().hosts().next().unwrap(), 5401, 80, @@ -638,7 +667,7 @@ mod tests { .unwrap(); let udp_packet = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), cidr_v4_resource().hosts().next().unwrap(), 5401, 80, @@ -674,9 +703,41 @@ mod tests { .is_err()); } + #[test] + fn allows_packets_for_and_from_gateway_tun_ip() { + let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun()); + + let request = ip_packet::make::tcp_packet( + client_tun_ipv4(), + gateway_tun_ipv4(), + 5401, + 80, + vec![0; 100], + ) + .unwrap(); + + let response = ip_packet::make::tcp_packet( + gateway_tun_ipv4(), + client_tun_ipv4(), + 80, + 5401, + vec![0; 100], + ) + .unwrap(); + + assert!(peer + .translate_outbound(request, Instant::now()) + .unwrap() + .is_some()); + assert!(peer + .translate_inbound(response, Instant::now()) + .unwrap() + .is_some()); + } + #[test] fn dns_and_cidr_filters_dot_mix() { - let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr()); + let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun()); peer.add_resource(foo_dns_resource(), None); peer.add_resource(bar_cidr_resource(), None); peer.setup_nat( @@ -690,7 +751,7 @@ mod tests { assert_eq!(bar_contained_ip(), foo_real_ip()); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), bar_contained_ip(), 1, bar_allowed_port(), @@ -701,7 +762,7 @@ mod tests { assert!(peer.translate_outbound(pkt, Instant::now()).is_ok()); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), bar_contained_ip(), 1, foo_allowed_port(), @@ -715,7 +776,7 @@ mod tests { .is_none()); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), foo_proxy_ip(), 1, bar_allowed_port(), @@ -729,7 +790,7 @@ mod tests { .is_none()); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), foo_proxy_ip(), 1, foo_allowed_port(), @@ -742,7 +803,7 @@ mod tests { #[test] fn internet_resource_doesnt_allow_all_traffic_for_dns_resources() { - let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr()); + let mut peer = ClientOnGateway::new(client_id(), client_tun(), gateway_tun()); peer.add_resource(foo_dns_resource(), None); peer.add_resource(internet_resource(), None); peer.setup_nat( @@ -754,7 +815,7 @@ mod tests { .unwrap(); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), foo_proxy_ip(), 1, foo_allowed_port(), @@ -765,7 +826,7 @@ mod tests { assert!(peer.translate_outbound(pkt, Instant::now()).is_ok()); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), foo_proxy_ip(), 1, 600, @@ -779,7 +840,7 @@ mod tests { .is_none()); let pkt = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), "1.1.1.1".parse().unwrap(), 1, 600, @@ -794,7 +855,7 @@ mod tests { fn dns_resource_packet_is_dropped_after_nat_session_expires() { let _guard = firezone_logging::test("trace"); - let mut peer = ClientOnGateway::new(client_id(), source_v4_addr(), source_v6_addr()); + 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(), @@ -805,7 +866,7 @@ mod tests { .unwrap(); let request = ip_packet::make::udp_packet( - source_v4_addr(), + client_tun_ipv4(), foo_proxy_ip(), 1, foo_allowed_port(), @@ -819,7 +880,7 @@ mod tests { let response = ip_packet::make::udp_packet( foo_real_ip(), - source_v4_addr(), + client_tun_ipv4(), foo_allowed_port(), 1, vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -836,7 +897,7 @@ mod tests { let response = ip_packet::make::udp_packet( foo_real_ip(), - source_v4_addr(), + client_tun_ipv4(), foo_allowed_port(), 1, vec![0, 0, 0, 0, 0, 0, 0, 0], @@ -916,14 +977,36 @@ mod tests { "10.0.0.0/24".parse().unwrap() } - fn source_v4_addr() -> Ipv4Addr { + fn client_tun() -> IpConfig { + IpConfig { + v4: client_tun_ipv4(), + v6: client_tun_ipv6(), + } + } + + fn client_tun_ipv4() -> Ipv4Addr { "100.64.0.1".parse().unwrap() } - fn source_v6_addr() -> Ipv6Addr { + fn client_tun_ipv6() -> Ipv6Addr { "fd00:2021:1111::1".parse().unwrap() } + pub fn gateway_tun() -> IpConfig { + IpConfig { + v4: gateway_tun_ipv4(), + v6: gateway_tun_ipv6(), + } + } + + pub fn gateway_tun_ipv4() -> Ipv4Addr { + "100.64.0.2".parse().unwrap() + } + + pub fn gateway_tun_ipv6() -> Ipv6Addr { + "fd00:2021:1111::2".parse().unwrap() + } + fn cidr_v4_resource() -> Ipv4Network { "10.0.0.0/24".parse().unwrap() } @@ -943,6 +1026,7 @@ mod tests { #[cfg(all(test, feature = "proptest"))] mod proptests { + use super::tests::*; use super::*; use crate::messages::gateway::{ Filter, PortRange, ResourceDescription, ResourceDescriptionCidr, @@ -957,6 +1041,7 @@ mod proptests { strategy::{Just, Strategy}, }; use rangemap::RangeInclusiveSet; + use std::net::{Ipv4Addr, Ipv6Addr}; use std::{collections::BTreeSet, ops::RangeInclusive}; use test_strategy::Arbitrary; @@ -968,22 +1053,29 @@ mod proptests { Protocol, IpAddr, )>, - #[strategy(any::())] src_v4: Ipv4Addr, - #[strategy(any::())] src_v6: Ipv6Addr, + #[strategy(any::())] client_v4: Ipv4Addr, + #[strategy(any::())] client_v6: Ipv6Addr, #[strategy(any::())] sport: u16, #[strategy(any::>())] payload: Vec, ) { // This test could be extended to test multiple src - let mut peer = ClientOnGateway::new(client_id, src_v4, src_v6); + let mut peer = ClientOnGateway::new( + client_id, + IpConfig { + v4: client_v4, + v6: client_v6, + }, + gateway_tun(), + ); for (resource, _, _) in &resources { peer.add_resource(resource.clone(), None); } for (_, protocol, dest) in &resources { let src = if dest.is_ipv4() { - src_v4.into() + client_v4.into() } else { - src_v6.into() + client_v6.into() }; let packet = match protocol { @@ -1002,8 +1094,8 @@ mod proptests { fn gateway_accepts_different_resources_with_same_ip_packet( #[strategy(client_id())] client_id: ClientId, #[strategy(collection::btree_set(resource_id(), 10))] resources_ids: BTreeSet, - #[strategy(any::())] src_v4: Ipv4Addr, - #[strategy(any::())] src_v6: Ipv6Addr, + #[strategy(any::())] client_v4: Ipv4Addr, + #[strategy(any::())] client_v6: Ipv6Addr, #[strategy(cidr_with_host())] config: (IpNetwork, IpAddr), #[strategy(collection::vec(filters_with_allowed_protocol(), 1..=10))] protocol_config: Vec< (Filters, Protocol), @@ -1013,11 +1105,18 @@ mod proptests { ) { let (resource_addr, dest) = config; let src = if dest.is_ipv4() { - src_v4.into() + client_v4.into() } else { - src_v6.into() + client_v6.into() }; - let mut peer = ClientOnGateway::new(client_id, src_v4, src_v6); + let mut peer = ClientOnGateway::new( + client_id, + IpConfig { + v4: client_v4, + v6: client_v6, + }, + gateway_tun(), + ); for ((filters, _), resource_id) in std::iter::zip(&protocol_config, resources_ids) { // This test could be extended to test multiple src @@ -1050,8 +1149,8 @@ mod proptests { fn gateway_reject_unallowed_packet( #[strategy(client_id())] client_id: ClientId, #[strategy(resource_id())] resource_id: ResourceId, - #[strategy(any::())] src_v4: Ipv4Addr, - #[strategy(any::())] src_v6: Ipv6Addr, + #[strategy(any::())] client_v4: Ipv4Addr, + #[strategy(any::())] client_v6: Ipv6Addr, #[strategy(cidr_with_host())] config: (IpNetwork, IpAddr), #[strategy(filters_with_rejected_protocol())] protocol_config: (Filters, Protocol), #[strategy(any::())] sport: u16, @@ -1059,13 +1158,20 @@ mod proptests { ) { let (resource_addr, dest) = config; let src = if dest.is_ipv4() { - src_v4.into() + client_v4.into() } else { - src_v6.into() + client_v6.into() }; let (filters, protocol) = protocol_config; // This test could be extended to test multiple src - let mut peer = ClientOnGateway::new(client_id, src_v4, src_v6); + let mut peer = ClientOnGateway::new( + client_id, + IpConfig { + v4: client_v4, + v6: client_v6, + }, + gateway_tun(), + ); let packet = match protocol { Protocol::Tcp { dport } => tcp_packet(src, dest, sport, dport, payload), Protocol::Udp { dport } => udp_packet(src, dest, sport, dport, payload), @@ -1093,8 +1199,8 @@ mod proptests { #[strategy(client_id())] client_id: ClientId, #[strategy(resource_id())] resource_id_allowed: ResourceId, #[strategy(resource_id())] resource_id_removed: ResourceId, - #[strategy(any::())] src_v4: Ipv4Addr, - #[strategy(any::())] src_v6: Ipv6Addr, + #[strategy(any::())] client_v4: Ipv4Addr, + #[strategy(any::())] client_v6: Ipv6Addr, #[strategy(cidr_with_host())] config: (IpNetwork, IpAddr), #[strategy(non_overlapping_non_empty_filters_with_allowed_protocol())] protocol_config: ( (Filters, Protocol), @@ -1105,14 +1211,21 @@ mod proptests { ) { let (resource_addr, dest) = config; let src = if dest.is_ipv4() { - src_v4.into() + client_v4.into() } else { - src_v6.into() + client_v6.into() }; let ((filters_allowed, protocol_allowed), (filters_removed, protocol_removed)) = protocol_config; // This test could be extended to test multiple src - let mut peer = ClientOnGateway::new(client_id, src_v4, src_v6); + let mut peer = ClientOnGateway::new( + client_id, + IpConfig { + v4: client_v4, + v6: client_v6, + }, + gateway_tun(), + ); let packet_allowed = match protocol_allowed { Protocol::Tcp { dport } => tcp_packet(src, dest, sport, dport, payload.clone()), diff --git a/rust/connlib/tunnel/src/tests/sim_gateway.rs b/rust/connlib/tunnel/src/tests/sim_gateway.rs index d77820f2a..34aabd8a5 100644 --- a/rust/connlib/tunnel/src/tests/sim_gateway.rs +++ b/rust/connlib/tunnel/src/tests/sim_gateway.rs @@ -7,7 +7,7 @@ use super::{ strategies::latency, unreachable_hosts::{IcmpError, UnreachableHosts}, }; -use crate::GatewayState; +use crate::{GatewayState, IpConfig}; use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use connlib_model::{GatewayId, RelayId}; @@ -16,7 +16,7 @@ use proptest::prelude::*; use snownet::Transmit; use std::{ collections::{BTreeMap, HashMap}, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, time::Instant, }; @@ -253,6 +253,8 @@ impl SimGateway { #[derive(Debug, Clone)] pub struct RefGateway { pub(crate) key: PrivateKey, + pub(crate) tunnel_ip4: Ipv4Addr, + pub(crate) tunnel_ip6: Ipv6Addr, } impl RefGateway { @@ -260,21 +262,39 @@ impl RefGateway { /// /// This simulates receiving the `init` message from the portal. pub(crate) fn init(self, id: GatewayId, now: Instant) -> SimGateway { - SimGateway::new(id, GatewayState::new(self.key.0, now)) // Cheating a bit here by reusing the key as seed. + let mut sut = GatewayState::new(self.key.0, now); + sut.update_tun_device(IpConfig { + v4: self.tunnel_ip4, + v6: self.tunnel_ip6, + }); + + SimGateway::new(id, sut) // Cheating a bit here by reusing the key as seed. } } -pub(crate) fn ref_gateway_host() -> impl Strategy> { +pub(crate) fn ref_gateway_host( + tunnel_ip4s: impl Strategy, + tunnel_ip6s: impl Strategy, +) -> impl Strategy> { host( dual_ip_stack(), any_port(), - ref_gateway(), + ref_gateway(tunnel_ip4s, tunnel_ip6s), latency(200), // We assume gateways have a somewhat decent Internet connection. ) } -fn ref_gateway() -> impl Strategy { - private_key().prop_map(move |key| RefGateway { key }) +fn ref_gateway( + tunnel_ip4s: impl Strategy, + tunnel_ip6s: impl Strategy, +) -> impl Strategy { + (private_key(), tunnel_ip4s, tunnel_ip6s).prop_map(move |(key, tunnel_ip4, tunnel_ip6)| { + RefGateway { + key, + tunnel_ip4, + tunnel_ip6, + } + }) } fn icmp_error_reply(packet: &IpPacket, error: IcmpError) -> Result { diff --git a/rust/connlib/tunnel/src/tests/stub_portal.rs b/rust/connlib/tunnel/src/tests/stub_portal.rs index d4825cd25..2b6f74d65 100644 --- a/rust/connlib/tunnel/src/tests/stub_portal.rs +++ b/rust/connlib/tunnel/src/tests/stub_portal.rs @@ -24,7 +24,10 @@ use std::{ /// Stub implementation of the portal. #[derive(Clone, derive_more::Debug)] pub(crate) struct StubPortal { - gateways_by_site: BTreeMap>, + client_tunnel_ipv4: Ipv4Addr, + client_tunnel_ipv6: Ipv6Addr, + + gateways_by_site: BTreeMap>, #[debug(skip)] sites_by_resource: BTreeMap, @@ -85,7 +88,32 @@ impl StubPortal { .id, )); + let mut tunnel_ip4s = tunnel_ip4s(); + let mut tunnel_ip6s = tunnel_ip6s(); + + let client_tunnel_ipv4 = tunnel_ip4s.next().unwrap(); + let client_tunnel_ipv6 = tunnel_ip6s.next().unwrap(); + + let gateways_by_site = gateways_by_site + .into_iter() + .map(|(site, gateways)| { + let gateways = gateways + .into_iter() + .map(|gateway| { + let ipv4_addr = tunnel_ip4s.next().unwrap(); + let ipv6_addr = tunnel_ip6s.next().unwrap(); + + (gateway, ipv4_addr, ipv6_addr) + }) + .collect(); + + (site, gateways) + }) + .collect(); + Self { + client_tunnel_ipv4, + client_tunnel_ipv6, gateways_by_site, gateway_selector, sites_by_resource: BTreeMap::from_iter( @@ -126,7 +154,7 @@ impl StubPortal { .expect("resource to be known"); let gateways = self.gateways_by_site.get(site_id).unwrap(); - let gateway = self.gateway_selector.select(gateways); + let (gateway, _, _) = self.gateway_selector.select(gateways); (*gateway, *site_id) } @@ -182,7 +210,7 @@ impl StubPortal { let sid = cidr_site.or(dns_site).or(internet_site)?; let gateways = self.gateways_by_site.get(&sid)?; - let gid = self.gateway_selector.try_select(gateways)?; + let (gid, _, _) = self.gateway_selector.try_select(gateways)?; Some(gid) } @@ -191,7 +219,12 @@ impl StubPortal { self.gateways_by_site .values() .flatten() - .map(|gid| (Just(*gid), ref_gateway_host())) // Map each ID to a strategy that samples a gateway. + .map(|(gid, ipv4_addr, ipv6_addr)| { + ( + Just(*gid), + ref_gateway_host(Just(*ipv4_addr), Just(*ipv6_addr)), + ) + }) // Map each ID to a strategy that samples a gateway. .collect::>() // A `Vec` implements `Strategy>` .prop_map(BTreeMap::from_iter) } @@ -201,12 +234,9 @@ impl StubPortal { system_dns: impl Strategy>, upstream_dns: impl Strategy>, ) -> impl Strategy> { - let client_tunnel_ip4 = tunnel_ip4s().next().unwrap(); - let client_tunnel_ip6 = tunnel_ip6s().next().unwrap(); - ref_client_host( - Just(client_tunnel_ip4), - Just(client_tunnel_ip6), + Just(self.client_tunnel_ipv4), + Just(self.client_tunnel_ipv6), system_dns, upstream_dns, ) diff --git a/rust/connlib/tunnel/src/tests/sut.rs b/rust/connlib/tunnel/src/tests/sut.rs index 404b814d7..57ef32485 100644 --- a/rust/connlib/tunnel/src/tests/sut.rs +++ b/rust/connlib/tunnel/src/tests/sut.rs @@ -233,8 +233,8 @@ impl TunnelTest { Transition::UpdateUpstreamDnsServers(servers) => { state.client.exec_mut(|c| { c.sut.update_interface_config(Interface { - ipv4: c.sut.tunnel_ip4().unwrap(), - ipv6: c.sut.tunnel_ip6().unwrap(), + ipv4: c.sut.tunnel_ip_config().unwrap().v4, + ipv6: c.sut.tunnel_ip_config().unwrap().v6, upstream_dns: servers, }) }); @@ -256,8 +256,8 @@ impl TunnelTest { }); } Transition::ReconnectPortal => { - let ipv4 = state.client.inner().sut.tunnel_ip4().unwrap(); - let ipv6 = state.client.inner().sut.tunnel_ip6().unwrap(); + let ipv4 = state.client.inner().sut.tunnel_ip_config().unwrap().v4; + let ipv6 = state.client.inner().sut.tunnel_ip_config().unwrap().v6; let upstream_dns = ref_state.client.inner().upstream_dns_resolvers(); let all_resources = ref_state.client.inner().all_resources(); @@ -715,8 +715,7 @@ impl TunnelTest { preshared_key.clone(), client_ice.clone(), gateway_ice.clone(), - self.client.inner().sut.tunnel_ip4().unwrap(), - self.client.inner().sut.tunnel_ip6().unwrap(), + self.client.inner().sut.tunnel_ip_config().unwrap(), None, resource, now, diff --git a/rust/gateway/src/eventloop.rs b/rust/gateway/src/eventloop.rs index 5e9e35d1d..06fa50f2a 100644 --- a/rust/gateway/src/eventloop.rs +++ b/rust/gateway/src/eventloop.rs @@ -11,7 +11,7 @@ use firezone_tunnel::messages::gateway::{ }; use firezone_tunnel::messages::{ConnectionAccepted, GatewayResponse, RelaysPresence}; use firezone_tunnel::{ - DnsResourceNatEntry, GatewayTunnel, ResolveDnsRequest, IPV4_PEERS, IPV6_PEERS, + DnsResourceNatEntry, GatewayTunnel, IpConfig, ResolveDnsRequest, IPV4_PEERS, IPV6_PEERS, }; use phoenix_channel::{PhoenixChannel, PublicKeyParam}; use std::collections::BTreeSet; @@ -235,8 +235,10 @@ impl Eventloop { msg.client.preshared_key, msg.client_ice_credentials, msg.gateway_ice_credentials, - msg.client.ipv4, - msg.client.ipv6, + IpConfig { + v4: msg.client.ipv4, + v6: msg.client.ipv6, + }, msg.expires_at, msg.resource, Instant::now(), @@ -354,6 +356,10 @@ impl Eventloop { firezone_tunnel::turn(&init.relays), Instant::now(), ); + self.tunnel.state_mut().update_tun_device(IpConfig { + v4: init.interface.ipv4, + v6: init.interface.ipv6, + }); if self .set_interface_tasks @@ -442,8 +448,10 @@ impl Eventloop { if let Err(e) = self.tunnel.state_mut().allow_access( req.client.id, - req.client.peer.ipv4, - req.client.peer.ipv6, + IpConfig { + v4: req.client.peer.ipv4, + v6: req.client.peer.ipv6, + }, req.expires_at, req.resource, req.client @@ -487,8 +495,10 @@ impl Eventloop { if let Err(e) = self.tunnel.state_mut().allow_access( req.client_id, - req.client_ipv4, - req.client_ipv6, + IpConfig { + v4: req.client_ipv4, + v6: req.client_ipv6, + }, req.expires_at, req.resource, req.payload.map(|r| DnsResourceNatEntry::new(r, addresses)),