mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
test(connlib): correctly scope state within tunnel_test (#5809)
Currently, the type hierarchy within `tunnel_test` is already quite nested: We have a `Host` that wraps a `SimNode` which wraps a `ClientState` or `GatewayState`. Additionally, a lot of state that is actually _per_ client or _per_ gateway is tracked in the root of `ReferenceState` and `TunnelTest`. That makes it difficult to introduce multiple gateways / clients to this test. To fix this, we introduce dedicated `RefClient` and `RefGateway` states. Those track the expected state of a particular client / gateway. Similarly, we introduce dedicated `SimClient` and `SimGateway` structs that track the simulation state by wrapping the corresponding system-under-test: `ClientState` a `GatewayState`. This ends up moving a lot of code around but has the great benefit that all the state is now scoped to a particular instance of a client or a gateway, paving the way for creating multiple clients & gateways in a single test.
This commit is contained in:
@@ -431,7 +431,7 @@ impl Allocation {
|
||||
let maybe_candidate = message.attributes().find_map(|a| srflx_candidate(local, a));
|
||||
update_candidate(maybe_candidate, current_srflx_candidate, &mut self.events);
|
||||
|
||||
self.log_update();
|
||||
self.log_update(now);
|
||||
|
||||
// Second, check if we have already determined which socket to use for this relay.
|
||||
// We send 2 BINDING requests to start with (one for each IP version) and the first one coming back wins.
|
||||
@@ -485,7 +485,7 @@ impl Allocation {
|
||||
&mut self.events,
|
||||
);
|
||||
|
||||
self.log_update();
|
||||
self.log_update(now);
|
||||
|
||||
while let Some(peer) = self.buffered_channel_bindings.pop() {
|
||||
debug_assert!(
|
||||
@@ -503,7 +503,7 @@ impl Allocation {
|
||||
|
||||
self.allocation_lifetime = Some((now, lifetime.lifetime()));
|
||||
|
||||
self.log_update();
|
||||
self.log_update(now);
|
||||
}
|
||||
CHANNEL_BIND => {
|
||||
let Some(channel) = original_request
|
||||
@@ -759,13 +759,13 @@ impl Allocation {
|
||||
self.credentials.is_some()
|
||||
}
|
||||
|
||||
fn log_update(&self) {
|
||||
fn log_update(&self, now: Instant) {
|
||||
tracing::info!(
|
||||
srflx_ip4 = ?self.ip4_srflx_candidate.as_ref().map(|c| c.addr()),
|
||||
srflx_ip6 = ?self.ip6_srflx_candidate.as_ref().map(|c| c.addr()),
|
||||
relay_ip4 = ?self.ip4_allocation.as_ref().map(|c| c.addr()),
|
||||
relay_ip6 = ?self.ip6_allocation.as_ref().map(|c| c.addr()),
|
||||
lifetime = ?self.allocation_lifetime,
|
||||
remaining_lifetime = ?self.allocation_lifetime.and_then(|(created_at, d)| d.checked_sub(now.checked_duration_since(created_at)?)),
|
||||
"Updated allocation"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,3 +47,4 @@ cc 7f664f0f323f6ee66ce3646f2dcee3060cd44a0ba6cc5dac93b6e05b506a8b77 # shrinks to
|
||||
cc 7f024279e0477056e37717e4b886d5016acc9fab8964611b58f86587767bba2e # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 29920, tv_nsec: 368456206 }, utc_now: 2024-07-08T07:58:51.159038594Z, client: Host { inner: SimNode { id: ClientId(00000000-0000-0000-0000-000000000000), state: (PrivateKey("0000000000000000000000000000000000000000000000000000000000000000"), {}), tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111:: }, ip4: Some(203.0.113.1), ip6: None, old_ips: [], old_ports: {0}, default_port: 1, allocated_ports: {(1, V4)} }, gateway: Host { inner: SimNode { id: GatewayId(00000000-0000-0000-0000-000000000000), state: PrivateKey("0000000000000000000000000000000000000000000000000000000000000001"), tunnel_ip4: 100.64.0.2, tunnel_ip6: fd00:2021:1111::1 }, ip4: Some(203.0.113.2), ip6: None, old_ips: [], old_ports: {0}, default_port: 28202, allocated_ports: {(28202, V4)} }, relay: Host { inner: SimRelay { id: RelayId(447095dd-0f99-00f7-9cdf-14db9370f920), state: 12549090075203197752, ip_stack: Dual { ip4: 203.0.113.3, ip6: 2001:db80::2 }, allocations: {} }, ip4: Some(203.0.113.3), ip6: Some(2001:db80::2), old_ips: [], old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V4), (3478, V6)} }, system_dns_resolvers: [247.236.135.67], upstream_dns_resolvers: [IpPort(IpDnsServer { address: 220.197.151.19:53 }), IpPort(IpDnsServer { address: 89.28.131.88:53 }), IpPort(IpDnsServer { address: 127.0.0.1:53 }), IpPort(IpDnsServer { address: [::ffff:127.0.0.1]:53 }), IpPort(IpDnsServer { address: [d567:8313:60b:e85d:365f:a699:b9d3:e1dd]:53 }), IpPort(IpDnsServer { address: [::ffff:185.167.149.64]:53 })], client_cidr_resources: {}, client_dns_resources: {}, client_dns_records: {}, client_connected_cidr_resources: {}, client_connected_dns_resources: {}, global_dns_records: {Name(crmoi.ecmx.cwku.): {0.0.0.0, ::ffff:48.3.194.225}, Name(hmtwct.hzpvl.ehgvx.): {17.184.192.139, 78.22.114.111, 6d72:a7e2:d73b:4f0b:50e0:ba1:22b8:ac48, 32.78.117.29}, Name(mahy.ekzr.): {c24a:a44c:7cbf:99e:37a3:e6d2:6f61:ab88}, Name(yeth.fbjqx.fnbhvt.): {127.0.0.1}, Name(ouwbw.kznvif.): {46.29.147.154, ::ffff:127.0.0.1, 127.0.0.1, ::ffff:232.178.178.113}, Name(nai.hem.ldep.): {::ffff:127.0.0.1}, Name(sbwn.qkckf.ltura.): {a59f:3fed:f882:3e0d:c967:564f:3f45:68f, 100.103.142.95, 145.154.71.75}, Name(lcqdtu.mot.): {1004:28c1:2c31:9817:cef2:5c3d:9956:d716, ::ffff:50.209.39.213}, Name(ipeyg.hvld.mpsik.): {90.2.106.81}, Name(mlte.snvnft.): {4.133.196.171}, Name(fvdig.sxuuco.): {::ffff:141.228.199.134, 191.77.153.166, 127.0.0.1}, Name(rel.rzveiw.xgd.): {127.0.0.1, 61.187.72.87, 200.81.188.150, 37.142.30.190, ::ffff:118.191.216.138}, Name(kgq.xlj.): {58d1:fd65:d116:b285:d0d8:169b:2249:1edb, 127.0.0.1}}, client_known_host_records: {}, expected_icmp_handshakes: [], expected_dns_handshakes: [] }, [AddDnsResource { resource: ResourceDescriptionDns { id: ResourceId(d8b923a2-6a73-df2d-4baf-557089dcf022), address: "*.tfq.swmt", name: "fkkx", address_description: Some("rzatphisen"), sites: [Site { name: "xewrde", id: SiteId(5ce8469f-4883-b33d-09ff-a468dfe73513) }, Site { name: "gttfyjb", id: SiteId(bc45737f-02af-c129-e90f-52d76e040d63) }] }, records: {Name(kzb.oatiu.tfq.swmt.): {::ffff:132.15.20.125, 165.213.243.89, 2f93:9571:7862:df97:7a8e:79c7:2e87:eb78, ::ffff:237.141.93.128}, Name(jehqf.iicd.tfq.swmt.): {::ffff:127.0.0.1, 126.122.243.61, 8158:f77:e6af:40a3:51ed:b37f:401e:fe16, 29c3:8a5c:43d7:2563:d400:e50a:c999:45f9}} }, SendDnsQuery { domain: Name(jehqf.iicd.tfq.swmt.), r_type: AAAA, query_id: 1402, dns_server: 89.28.131.88:53 }, SendICMPPacketToDnsResource { src: fd00:2021:1111::, dst: Name(jehqf.iicd.tfq.swmt.), seq: 0, identifier: 0 }, RoamClient { ip4: None, ip6: Some(2001:db80::3), port: 1 }, AddCidrResource(ResourceDescriptionCidr { id: ResourceId(00000000-0000-0000-0000-000000000000), address: V6(Ipv6Network { network_address: ::ffff:0.0.0.0, netmask: 127 }), name: "aaaa", address_description: Some("zjmvzpv"), sites: [Site { name: "mvjorgubk", id: SiteId(807103d6-5141-8196-c36c-cd6539fb3210) }] }), SendICMPPacketToDnsResource { src: fd00:2021:1111::, dst: Name(jehqf.iicd.tfq.swmt.), seq: 0, identifier: 0 }], None)
|
||||
cc b51827a79947ba53609819934c11750a46302b58ca9cd993c24a7f1312ef2a0b # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 32296, tv_nsec: 202806172 }, utc_now: 2024-07-08T08:38:26.993388469Z, client: Host { inner: SimNode { id: ClientId(00000000-0000-0000-0000-000000000000), state: (PrivateKey("0000000000000000000000000000000000000000000000000000000000000000"), {}), tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111:: }, ip4: Some(203.0.113.1), ip6: None, old_ips: [], old_ports: {0}, default_port: 1, allocated_ports: {(1, V4)} }, gateway: Host { inner: SimNode { id: GatewayId(00000000-0000-0000-0000-000000000000), state: PrivateKey("0000000000000000000000000000000000000000000000000000000000000001"), tunnel_ip4: 100.64.0.2, tunnel_ip6: fd00:2021:1111::1 }, ip4: Some(203.0.113.2), ip6: None, old_ips: [], old_ports: {0}, default_port: 1, allocated_ports: {(1, V4)} }, relay: Host { inner: SimRelay { id: RelayId(00000000-0000-0000-0000-000000000000), state: 0, ip_stack: Dual { ip4: 203.0.113.3, ip6: 2001:db80:: }, allocations: {} }, ip4: Some(203.0.113.3), ip6: Some(2001:db80::), old_ips: [], old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V6), (3478, V4)} }, system_dns_resolvers: [0.0.0.0], upstream_dns_resolvers: [], client_cidr_resources: {}, client_dns_resources: {}, client_dns_records: {}, client_connected_cidr_resources: {}, client_connected_dns_resources: {}, global_dns_records: {}, client_known_host_records: {}, expected_icmp_handshakes: [], expected_dns_handshakes: [], network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.1, netmask: 32 }), Client(ClientId(00000000-0000-0000-0000-000000000000))), (V4(Ipv4Network { network_address: 203.0.113.2, netmask: 32 }), Gateway(GatewayId(00000000-0000-0000-0000-000000000000))), (V4(Ipv4Network { network_address: 203.0.113.3, netmask: 32 }), Relay(RelayId(00000000-0000-0000-0000-000000000000))), (V6(Ipv6Network { network_address: 2001:db80::, netmask: 128 }), Relay(RelayId(00000000-0000-0000-0000-000000000000)))} } }, [RoamClient { ip4: Some(203.0.113.1), ip6: Some(2001:db80::34), port: 1 }], None)
|
||||
cc 6012a980592466d46f51f36bf511aeca295d174a188cd6ae9e6e799b2eeb4bdd # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 32727, tv_nsec: 104744448 }, utc_now: 2024-07-08T08:45:37.895326745Z, client: Host { inner: SimNode { id: ClientId(00000000-0000-0000-0000-000000000000), state: (PrivateKey("0000000000000000000000000000000000000000000000000000000000000000"), {}), tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111:: }, ip4: Some(203.0.113.1), ip6: None, old_ips: [], old_ports: {0}, default_port: 25710, allocated_ports: {(25710, V4)} }, gateway: Host { inner: SimNode { id: GatewayId(0a90cfcb-9a3f-9f65-601c-8675fc305ce4), state: PrivateKey("3b0a23bbb0d745be5d50cb4c7dcad3a3761a55353133417707f93a6abbfdc255"), tunnel_ip4: 100.64.0.2, tunnel_ip6: fd00:2021:1111::1 }, ip4: None, ip6: Some(2001:db80::24), old_ips: [], old_ports: {0}, default_port: 12684, allocated_ports: {(12684, V6)} }, relay: Host { inner: SimRelay { id: RelayId(7cb9f334-4ccc-019d-85f2-2390848e66c1), state: 5611803068338389074, ip_stack: Dual { ip4: 203.0.113.40, ip6: 2001:db80::58 }, allocations: {} }, ip4: Some(203.0.113.40), ip6: Some(2001:db80::58), old_ips: [], old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V4), (3478, V6)} }, system_dns_resolvers: [127.0.0.1, ::ffff:247.177.145.232, ::ffff:179.131.145.140], upstream_dns_resolvers: [], client_cidr_resources: {}, client_dns_resources: {}, client_dns_records: {}, client_connected_cidr_resources: {}, client_connected_dns_resources: {}, global_dns_records: {}, client_known_host_records: {}, expected_icmp_handshakes: [], expected_dns_handshakes: [], network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.1, netmask: 32 }), Client(ClientId(00000000-0000-0000-0000-000000000000))), (V4(Ipv4Network { network_address: 203.0.113.40, netmask: 32 }), Relay(RelayId(7cb9f334-4ccc-019d-85f2-2390848e66c1))), (V6(Ipv6Network { network_address: 2001:db80::24, netmask: 128 }), Gateway(GatewayId(0a90cfcb-9a3f-9f65-601c-8675fc305ce4))), (V6(Ipv6Network { network_address: 2001:db80::58, netmask: 128 }), Relay(RelayId(7cb9f334-4ccc-019d-85f2-2390848e66c1)))} } }, [AddDnsResource { resource: ResourceDescriptionDns { id: ResourceId(00000000-0000-0000-0000-000000000000), address: "wqgecw.ymss.bdyrft", name: "aaaa", address_description: Some("jydrm"), sites: [Site { name: "xttyddtwg", id: SiteId(deca6d49-b6f5-cff2-8a83-9f3626d89bf6) }] }, records: {Name(wqgecw.ymss.bdyrft.): {229.251.45.251, ::ffff:59.2.60.9}} }, RoamClient { ip4: Some(203.0.113.1), ip6: Some(2001:db80::49), port: 10114 }, SendDnsQuery { domain: Name(wqgecw.ymss.bdyrft.), r_type: A, query_id: 0, dns_server: 127.0.0.1:53 }, RoamClient { ip4: None, ip6: Some(2001:db80::49), port: 64407 }, SendICMPPacketToDnsResource { src: 100.64.0.1, dst: Name(wqgecw.ymss.bdyrft.), seq: 0, identifier: 0 }, SendICMPPacketToDnsResource { src: 100.64.0.1, dst: Name(wqgecw.ymss.bdyrft.), seq: 0, identifier: 0 }], None)
|
||||
cc 4e94fe9054e7d0a8d3b0cdc9564e562e7a20e33e6969312e8088601600f28101 # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 50453, tv_nsec: 990736170 }, utc_now: 2024-07-10T04:15:45.022371741Z, client: Host { inner: RefClient { key: PrivateKey("6308b982f16e56b13315d94d5b028af3aaf089a46fd9364f28bdbad6839c5c5b"), known_hosts: {"sog.iuv": [35.0.92.197, 127.0.0.1], "vziabd.xmwhup.pungj": [ffd6:9c77:debb:b85a:b249:547e:a9a2:cf79, 73.50.5.200], "mbjw.mtq.favlrp": [127.0.0.1, 191.162.14.155], "icjv.adsl.pxd": [::ffff:139.251.78.60, 108.203.183.88, ::ffff:127.0.0.1, 172.85.230.143, a14c:3bc0:da24:1562:8d35:5703:c164:38dd], "spyhqn.jeq.tsrqyz": [159.196.134.93, 127.0.0.1, 93.234.19.166, ::ffff:0.0.0.0], "gxrk.uglvuf": [343d:c04:5b36:3e6b:eeab:ce4a:1f9b:957e, 127.0.0.1, 138.47.94.195], "xlgd.ire": [::ffff:106.75.98.27, 185.25.235.30, 238.179.216.1, ::ffff:86.200.52.17, ::ffff:187.107.84.34], "hpkq.sicp.jxfzq": [231.237.65.176, 127.0.0.1], "zua.irezz": [::ffff:0.0.0.0], "exl.onofs.xwt": [::ffff:172.91.84.123], "pvqerk.wicwl.gycnoo": [::ffff:210.196.90.97, ::ffff:236.5.9.242, 24.90.160.237, 33fb:f5e:78d5:6fcf:2679:804e:4cb4:8685]}, tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, system_dns_resolvers: [127.0.0.1, ::ffff:236.238.31.35, 191.46.28.254], upstream_dns_resolvers: [], cidr_resources: {}, dns_resources: {}, dns_records: {}, connected_cidr_resources: {}, connected_dns_resources: {}, expected_icmp_handshakes: [], expected_dns_handshakes: [] }, sim_state: ClientId(d16f3c0b-0ba2-1880-fd7c-fd5d42eb30e0), ip4: None, ip6: Some(2001:db80::63), old_ports: {0}, default_port: 57958, allocated_ports: {(57958, V6)} }, gateway: Host { inner: RefGateway { key: PrivateKey("a2b756447eeed8fef6f097182747a36f8c04e4e16757c660c41330e0638060e5") }, sim_state: GatewayId(f1e32f0c-bea4-415c-f839-e414140b6a7b), ip4: Some(203.0.113.77), ip6: None, old_ports: {0}, default_port: 3275, allocated_ports: {(3275, V4)} }, relays: {RelayId(a1570084-8b27-bfc0-f65a-e6d9828ee8e9): Host { inner: 13930734621424630275, sim_state: (), ip4: Some(203.0.113.9), ip6: Some(2001:db80::55), old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V4), (3478, V6)} }, RelayId(0d3f6d84-4d2c-22cd-7449-f34df43d91cf): Host { inner: 13962067921649706663, sim_state: (), ip4: Some(203.0.113.76), ip6: Some(2001:db80::1), old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V4), (3478, V6)} }}, global_dns_records: {Name(xkaxzj.qcku.wmbrrt.): {::ffff:127.0.0.1, 16.64.128.18}}, network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.9, netmask: 32 }), Relay(RelayId(a1570084-8b27-bfc0-f65a-e6d9828ee8e9))), (V4(Ipv4Network { network_address: 203.0.113.76, netmask: 32 }), Relay(RelayId(0d3f6d84-4d2c-22cd-7449-f34df43d91cf))), (V4(Ipv4Network { network_address: 203.0.113.77, netmask: 32 }), Gateway(GatewayId(f1e32f0c-bea4-415c-f839-e414140b6a7b))), (V6(Ipv6Network { network_address: 2001:db80::1, netmask: 128 }), Relay(RelayId(0d3f6d84-4d2c-22cd-7449-f34df43d91cf))), (V6(Ipv6Network { network_address: 2001:db80::55, netmask: 128 }), Relay(RelayId(a1570084-8b27-bfc0-f65a-e6d9828ee8e9))), (V6(Ipv6Network { network_address: 2001:db80::63, netmask: 128 }), Client(ClientId(d16f3c0b-0ba2-1880-fd7c-fd5d42eb30e0)))} } }, [AddDnsResource { resource: ResourceDescriptionDns { id: ResourceId(7db7a1da-c996-3b3d-bae1-665f356f28f4), address: "?.fnhvin.pei", name: "gmhyshdhlp", address_description: Some("wezrfwmh"), sites: [Site { name: "swkmncjeae", id: SiteId(cadbbea8-4aba-d0a9-e6f4-fc38a36231eb) }] }, records: {Name(fthl.fnhvin.pei.): {175.242.230.69}} }, SendDnsQuery { domain: Name(fthl.fnhvin.pei.), r_type: AAAA, query_id: 27157, dns_server: [::ffff:236.238.31.35]:53 }, SendICMPPacketToDnsResource { src: fd00:2021:1111::, dst: Name(fthl.fnhvin.pei.), seq: 47, identifier: 6323 }, SendICMPPacketToDnsResource { src: fd00:2021:1111::, dst: Name(fthl.fnhvin.pei.), seq: 14926, identifier: 8427 }], None)
|
||||
|
||||
@@ -317,6 +317,24 @@ impl ClientState {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tunnel_ip4(&self) -> Option<Ipv4Addr> {
|
||||
Some(self.interface_config.as_ref()?.ipv4)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tunnel_ip6(&self) -> Option<Ipv6Addr> {
|
||||
Some(self.interface_config.as_ref()?.ipv6)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn tunnel_ip_for(&self, dst: IpAddr) -> Option<IpAddr> {
|
||||
Some(match dst {
|
||||
IpAddr::V4(_) => self.tunnel_ip4()?.into(),
|
||||
IpAddr::V6(_) => self.tunnel_ip6()?.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn resources(&self) -> Vec<callbacks::ResourceDescription> {
|
||||
self.resource_ids
|
||||
.values()
|
||||
|
||||
@@ -4,8 +4,9 @@ use proptest::test_runner::Config;
|
||||
mod assertions;
|
||||
mod composite_strategy;
|
||||
mod reference;
|
||||
mod sim_client;
|
||||
mod sim_gateway;
|
||||
mod sim_net;
|
||||
mod sim_node;
|
||||
mod sim_portal;
|
||||
mod sim_relay;
|
||||
mod strategies;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::{reference::ReferenceState, TunnelTest};
|
||||
use super::{
|
||||
sim_client::{RefClient, SimClient},
|
||||
sim_gateway::SimGateway,
|
||||
};
|
||||
use crate::tests::reference::ResourceDst;
|
||||
use connlib_shared::DomainName;
|
||||
use ip_packet::IpPacket;
|
||||
@@ -14,10 +17,15 @@ use std::{
|
||||
/// - For CIDR resources, that is the actual CIDR resource IP.
|
||||
/// - For DNS resources, the IP must match one of the resolved IPs for the domain.
|
||||
/// 3. For DNS resources, the mapping of proxy IP to actual resource IP must be stable.
|
||||
pub(crate) fn assert_icmp_packets_properties(state: &TunnelTest, ref_state: &ReferenceState) {
|
||||
pub(crate) fn assert_icmp_packets_properties(
|
||||
ref_client: &RefClient,
|
||||
sim_client: &SimClient,
|
||||
sim_gateway: &SimGateway,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
) {
|
||||
let unexpected_icmp_replies = find_unexpected_entries(
|
||||
&ref_state.expected_icmp_handshakes,
|
||||
&state.client_received_icmp_replies,
|
||||
&ref_client.expected_icmp_handshakes,
|
||||
&sim_client.received_icmp_replies,
|
||||
|(_, seq_a, id_a), (seq_b, id_b)| seq_a == seq_b && id_a == id_b,
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -27,28 +35,28 @@ pub(crate) fn assert_icmp_packets_properties(state: &TunnelTest, ref_state: &Ref
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ref_state.expected_icmp_handshakes.len(),
|
||||
state.gateway_received_icmp_requests.len(),
|
||||
ref_client.expected_icmp_handshakes.len(),
|
||||
sim_gateway.received_icmp_requests.len(),
|
||||
"Unexpected ICMP requests on gateway"
|
||||
);
|
||||
|
||||
tracing::info!(target: "assertions", "✅ Performed the expected {} ICMP handshakes", state.gateway_received_icmp_requests.len());
|
||||
tracing::info!(target: "assertions", "✅ Performed the expected {} ICMP handshakes", sim_gateway.received_icmp_requests.len());
|
||||
|
||||
let mut mapping = HashMap::new();
|
||||
|
||||
for ((resource_dst, seq, identifier), gateway_received_request) in ref_state
|
||||
for ((resource_dst, seq, identifier), gateway_received_request) in ref_client
|
||||
.expected_icmp_handshakes
|
||||
.iter()
|
||||
.zip(state.gateway_received_icmp_requests.iter())
|
||||
.zip(sim_gateway.received_icmp_requests.iter())
|
||||
{
|
||||
let _guard = tracing::info_span!(target: "assertions", "icmp", %seq, %identifier).entered();
|
||||
|
||||
let client_sent_request = &state
|
||||
.client_sent_icmp_requests
|
||||
let client_sent_request = &sim_client
|
||||
.sent_icmp_requests
|
||||
.get(&(*seq, *identifier))
|
||||
.expect("to have ICMP request on client");
|
||||
let client_received_reply = &state
|
||||
.client_received_icmp_replies
|
||||
let client_received_reply = &sim_client
|
||||
.received_icmp_replies
|
||||
.get(&(*seq, *identifier))
|
||||
.expect("to have ICMP reply on client");
|
||||
|
||||
@@ -56,10 +64,7 @@ pub(crate) fn assert_icmp_packets_properties(state: &TunnelTest, ref_state: &Ref
|
||||
|
||||
assert_eq!(
|
||||
gateway_received_request.source(),
|
||||
ref_state
|
||||
.client
|
||||
.inner()
|
||||
.tunnel_ip(gateway_received_request.source()),
|
||||
ref_client.tunnel_ip_for(gateway_received_request.source()),
|
||||
"ICMP request on gateway to originate from client"
|
||||
);
|
||||
|
||||
@@ -70,7 +75,7 @@ pub(crate) fn assert_icmp_packets_properties(state: &TunnelTest, ref_state: &Ref
|
||||
ResourceDst::Dns(domain) => {
|
||||
assert_destination_is_dns_resource(
|
||||
gateway_received_request,
|
||||
&ref_state.global_dns_records,
|
||||
global_dns_records,
|
||||
domain,
|
||||
);
|
||||
|
||||
@@ -84,18 +89,18 @@ pub(crate) fn assert_icmp_packets_properties(state: &TunnelTest, ref_state: &Ref
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn assert_known_hosts_are_valid(state: &TunnelTest, ref_state: &ReferenceState) {
|
||||
for (record, actual_addrs) in &state.client_dns_records {
|
||||
if let Some(expected_addrs) = ref_state.client_known_host_records.get(&record.to_string()) {
|
||||
pub(crate) fn assert_known_hosts_are_valid(ref_client: &RefClient, sim_client: &SimClient) {
|
||||
for (record, actual_addrs) in &sim_client.dns_records {
|
||||
if let Some(expected_addrs) = ref_client.known_hosts.get(&record.to_string()) {
|
||||
assert_eq!(actual_addrs, expected_addrs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn assert_dns_packets_properties(state: &TunnelTest, ref_state: &ReferenceState) {
|
||||
pub(crate) fn assert_dns_packets_properties(ref_client: &RefClient, sim_client: &SimClient) {
|
||||
let unexpected_icmp_replies = find_unexpected_entries(
|
||||
&ref_state.expected_dns_handshakes,
|
||||
&state.client_received_dns_responses,
|
||||
&ref_client.expected_dns_handshakes,
|
||||
&sim_client.received_dns_responses,
|
||||
|id_a, id_b| id_a == id_b,
|
||||
);
|
||||
|
||||
@@ -105,15 +110,15 @@ pub(crate) fn assert_dns_packets_properties(state: &TunnelTest, ref_state: &Refe
|
||||
"Unexpected DNS replies on client"
|
||||
);
|
||||
|
||||
for query_id in ref_state.expected_dns_handshakes.iter() {
|
||||
for query_id in ref_client.expected_dns_handshakes.iter() {
|
||||
let _guard = tracing::info_span!(target: "assertions", "dns", %query_id).entered();
|
||||
|
||||
let client_sent_query = state
|
||||
.client_sent_dns_queries
|
||||
let client_sent_query = sim_client
|
||||
.sent_dns_queries
|
||||
.get(query_id)
|
||||
.expect("to have DNS query on client");
|
||||
let client_received_response = state
|
||||
.client_received_dns_responses
|
||||
let client_received_response = sim_client
|
||||
.received_dns_responses
|
||||
.get(query_id)
|
||||
.expect("to have DNS response on client");
|
||||
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
use super::{
|
||||
composite_strategy::CompositeStrategy, sim_net::*, sim_node::*, sim_relay::*, strategies::*,
|
||||
transition::*, IcmpIdentifier, IcmpSeq, QueryId,
|
||||
composite_strategy::CompositeStrategy, sim_client::*, sim_gateway::*, sim_net::*, sim_relay::*,
|
||||
strategies::*, transition::*,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use connlib_shared::{
|
||||
messages::{
|
||||
client::{ResourceDescriptionCidr, ResourceDescriptionDns},
|
||||
ClientId, DnsServer, GatewayId, RelayId, ResourceId,
|
||||
},
|
||||
proptest::*,
|
||||
DomainName,
|
||||
};
|
||||
use connlib_shared::{messages::RelayId, proptest::*, DomainName, StaticSecret};
|
||||
use hickory_proto::rr::RecordType;
|
||||
use ip_network_table::IpNetworkTable;
|
||||
use itertools::Itertools;
|
||||
use prop::collection;
|
||||
use proptest::{prelude::*, sample};
|
||||
use proptest_state_machine::ReferenceStateMachine;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
fmt,
|
||||
net::IpAddr,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
@@ -30,46 +22,15 @@ use std::{
|
||||
pub(crate) struct ReferenceState {
|
||||
pub(crate) now: Instant,
|
||||
pub(crate) utc_now: DateTime<Utc>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) client: Host<SimNode<ClientId, (PrivateKey, HashMap<String, Vec<IpAddr>>)>>,
|
||||
pub(crate) gateway: Host<SimNode<GatewayId, PrivateKey>>,
|
||||
pub(crate) relays: HashMap<RelayId, Host<SimRelay<u64>>>,
|
||||
|
||||
/// The DNS resolvers configured on the client outside of connlib.
|
||||
pub(crate) system_dns_resolvers: Vec<IpAddr>,
|
||||
/// The upstream DNS resolvers configured in the portal.
|
||||
pub(crate) upstream_dns_resolvers: Vec<DnsServer>,
|
||||
|
||||
/// The CIDR resources the client is aware of.
|
||||
pub(crate) client_cidr_resources: IpNetworkTable<ResourceDescriptionCidr>,
|
||||
/// The DNS resources the client is aware of.
|
||||
pub(crate) client_dns_resources: BTreeMap<ResourceId, ResourceDescriptionDns>,
|
||||
|
||||
/// The client's DNS records.
|
||||
///
|
||||
/// The IPs assigned to a domain by connlib are an implementation detail that we don't want to model in these tests.
|
||||
/// Instead, we just remember what _kind_ of records we resolved to be able to sample a matching src IP.
|
||||
client_dns_records: BTreeMap<DomainName, HashSet<RecordType>>,
|
||||
|
||||
/// The CIDR resources the client is connected to.
|
||||
client_connected_cidr_resources: HashSet<ResourceId>,
|
||||
|
||||
/// The DNS resources the client is connected to.
|
||||
client_connected_dns_resources: HashSet<(ResourceId, DomainName)>,
|
||||
pub(crate) client: Host<RefClient>,
|
||||
pub(crate) gateway: Host<RefGateway>,
|
||||
pub(crate) relays: HashMap<RelayId, Host<u64>>,
|
||||
|
||||
/// All IP addresses a domain resolves to in our test.
|
||||
///
|
||||
/// This is used to e.g. mock DNS resolution on the gateway.
|
||||
pub(crate) global_dns_records: BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
|
||||
/// Ips that the client know about without querying the global records
|
||||
pub(crate) client_known_host_records: HashMap<String, Vec<IpAddr>>,
|
||||
|
||||
/// The expected ICMP handshakes.
|
||||
pub(crate) expected_icmp_handshakes: VecDeque<(ResourceDst, IcmpSeq, IcmpIdentifier)>,
|
||||
/// The expected DNS handshakes.
|
||||
pub(crate) expected_dns_handshakes: VecDeque<QueryId>,
|
||||
|
||||
pub(crate) network: RoutingTable,
|
||||
}
|
||||
|
||||
@@ -92,31 +53,17 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
let mut tunnel_ip4s = tunnel_ip4s();
|
||||
let mut tunnel_ip6s = tunnel_ip6s();
|
||||
|
||||
let client_prototype = sim_node_prototype(
|
||||
client_id(),
|
||||
client_state(),
|
||||
&mut tunnel_ip4s,
|
||||
&mut tunnel_ip6s,
|
||||
);
|
||||
let gateway_prototype = sim_node_prototype(
|
||||
gateway_id(),
|
||||
gateway_state(),
|
||||
&mut tunnel_ip4s,
|
||||
&mut tunnel_ip6s,
|
||||
);
|
||||
(
|
||||
client_prototype,
|
||||
gateway_prototype,
|
||||
collection::hash_map(relay_id(), sim_relay_prototype(), 2),
|
||||
system_dns_servers(),
|
||||
upstream_dns_servers(),
|
||||
ref_client_host(&mut tunnel_ip4s, &mut tunnel_ip6s),
|
||||
ref_gateway_host(),
|
||||
collection::hash_map(relay_id(), relay_prototype(), 2),
|
||||
global_dns_records(), // Start out with a set of global DNS records so we have something to resolve outside of DNS resources.
|
||||
Just(Instant::now()),
|
||||
Just(Utc::now()),
|
||||
)
|
||||
.prop_filter_map(
|
||||
"network IPs must be unique",
|
||||
|(c, g, relays, system_dns, upstream_dns, global_dns, now, utc_now)| {
|
||||
|(c, g, relays, global_dns, now, utc_now)| {
|
||||
let mut routing_table = RoutingTable::default();
|
||||
|
||||
if !routing_table.add_host(c.inner().id, &c) {
|
||||
@@ -132,78 +79,22 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
};
|
||||
}
|
||||
|
||||
Some((
|
||||
c,
|
||||
g,
|
||||
relays,
|
||||
system_dns,
|
||||
upstream_dns,
|
||||
global_dns,
|
||||
now,
|
||||
utc_now,
|
||||
routing_table,
|
||||
))
|
||||
Some((c, g, relays, global_dns, now, utc_now, routing_table))
|
||||
},
|
||||
)
|
||||
.prop_filter(
|
||||
"client and gateway priv key must be different",
|
||||
|(c, g, _, _, _, _, _, _, _)| c.inner().state.0 != g.inner().state,
|
||||
)
|
||||
.prop_filter(
|
||||
"at least one DNS server needs to be reachable",
|
||||
|(c, _, _, system_dns, upstream_dns, _, _, _, _)| {
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS!
|
||||
|
||||
if !upstream_dns.is_empty() {
|
||||
if c.ip4.is_none() && upstream_dns.iter().all(|s| s.ip().is_ipv4()) {
|
||||
return false;
|
||||
}
|
||||
if c.ip6.is_none() && upstream_dns.iter().all(|s| s.ip().is_ipv6()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if c.ip4.is_none() && system_dns.iter().all(|s| s.is_ipv4()) {
|
||||
return false;
|
||||
}
|
||||
if c.ip6.is_none() && system_dns.iter().all(|s| s.is_ipv6()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
},
|
||||
|(c, g, _, _, _, _, _)| c.inner().key != g.inner().key,
|
||||
)
|
||||
.prop_map(
|
||||
|(
|
||||
|(client, gateway, relays, global_dns_records, now, utc_now, network)| Self {
|
||||
now,
|
||||
utc_now,
|
||||
client,
|
||||
gateway,
|
||||
relays,
|
||||
system_dns_resolvers,
|
||||
upstream_dns_resolvers,
|
||||
global_dns_records,
|
||||
now,
|
||||
utc_now,
|
||||
network,
|
||||
)| Self {
|
||||
now,
|
||||
utc_now,
|
||||
client: client.clone(),
|
||||
gateway,
|
||||
relays,
|
||||
system_dns_resolvers,
|
||||
upstream_dns_resolvers,
|
||||
global_dns_records,
|
||||
client_known_host_records: client.inner().state.1.clone(),
|
||||
network,
|
||||
client_cidr_resources: IpNetworkTable::new(),
|
||||
client_connected_cidr_resources: Default::default(),
|
||||
expected_icmp_handshakes: Default::default(),
|
||||
client_dns_resources: Default::default(),
|
||||
client_dns_records: Default::default(),
|
||||
expected_dns_handshakes: Default::default(),
|
||||
client_connected_dns_resources: Default::default(),
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
@@ -239,35 +130,51 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
question_mark_wildcard_dns_resource(),
|
||||
],
|
||||
)
|
||||
.with_if_not_empty(10, state.ipv4_cidr_resource_dsts(), |ip4_resources| {
|
||||
icmp_to_cidr_resource(
|
||||
packet_source_v4(state.client.inner().tunnel_ip4),
|
||||
sample::select(ip4_resources),
|
||||
)
|
||||
})
|
||||
.with_if_not_empty(10, state.ipv6_cidr_resource_dsts(), |ip6_resources| {
|
||||
icmp_to_cidr_resource(
|
||||
packet_source_v6(state.client.inner().tunnel_ip6),
|
||||
sample::select(ip6_resources),
|
||||
)
|
||||
})
|
||||
.with_if_not_empty(10, state.resolved_v4_domains(), |dns_v4_domains| {
|
||||
icmp_to_dns_resource(
|
||||
packet_source_v4(state.client.inner().tunnel_ip4),
|
||||
sample::select(dns_v4_domains),
|
||||
)
|
||||
})
|
||||
.with_if_not_empty(10, state.resolved_v6_domains(), |dns_v6_domains| {
|
||||
icmp_to_dns_resource(
|
||||
packet_source_v6(state.client.inner().tunnel_ip6),
|
||||
sample::select(dns_v6_domains),
|
||||
)
|
||||
})
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
state.client.inner().ipv4_cidr_resource_dsts(),
|
||||
|ip4_resources| {
|
||||
icmp_to_cidr_resource(
|
||||
packet_source_v4(state.client.inner().tunnel_ip4),
|
||||
sample::select(ip4_resources),
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
state.client.inner().ipv6_cidr_resource_dsts(),
|
||||
|ip6_resources| {
|
||||
icmp_to_cidr_resource(
|
||||
packet_source_v6(state.client.inner().tunnel_ip6),
|
||||
sample::select(ip6_resources),
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
state.client.inner().resolved_v4_domains(),
|
||||
|dns_v4_domains| {
|
||||
icmp_to_dns_resource(
|
||||
packet_source_v4(state.client.inner().tunnel_ip4),
|
||||
sample::select(dns_v4_domains),
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
state.client.inner().resolved_v6_domains(),
|
||||
|dns_v6_domains| {
|
||||
icmp_to_dns_resource(
|
||||
packet_source_v6(state.client.inner().tunnel_ip6),
|
||||
sample::select(dns_v6_domains),
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
(
|
||||
state.all_domains(),
|
||||
state.v4_dns_servers(),
|
||||
state.all_domains(state.client.inner()),
|
||||
state.client.inner().v4_dns_servers(),
|
||||
state.client.ip4,
|
||||
),
|
||||
|(domains, v4_dns_servers, _)| {
|
||||
@@ -277,8 +184,8 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
.with_if_not_empty(
|
||||
10,
|
||||
(
|
||||
state.all_domains(),
|
||||
state.v6_dns_servers(),
|
||||
state.all_domains(state.client.inner()),
|
||||
state.client.inner().v6_dns_servers(),
|
||||
state.client.ip6,
|
||||
),
|
||||
|(domains, v6_dns_servers, _)| {
|
||||
@@ -287,7 +194,10 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
)
|
||||
.with_if_not_empty(
|
||||
1,
|
||||
state.resolved_ip4_for_non_resources(),
|
||||
state
|
||||
.client
|
||||
.inner()
|
||||
.resolved_ip4_for_non_resources(&state.global_dns_records),
|
||||
|resolved_non_resource_ip4s| {
|
||||
ping_random_ip(
|
||||
packet_source_v4(state.client.inner().tunnel_ip4),
|
||||
@@ -297,7 +207,10 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
)
|
||||
.with_if_not_empty(
|
||||
1,
|
||||
state.resolved_ip6_for_non_resources(),
|
||||
state
|
||||
.client
|
||||
.inner()
|
||||
.resolved_ip6_for_non_resources(&state.global_dns_records),
|
||||
|resolved_non_resource_ip6s| {
|
||||
ping_random_ip(
|
||||
packet_source_v6(state.client.inner().tunnel_ip6),
|
||||
@@ -305,7 +218,7 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
)
|
||||
},
|
||||
)
|
||||
.with_if_not_empty(1, state.all_resources(), |resources| {
|
||||
.with_if_not_empty(1, state.client.inner().all_resources(), |resources| {
|
||||
sample::select(resources).prop_map(Transition::RemoveResource)
|
||||
})
|
||||
.boxed()
|
||||
@@ -317,20 +230,30 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
|
||||
match transition {
|
||||
Transition::AddCidrResource(r) => {
|
||||
state.client_cidr_resources.insert(r.address, r.clone());
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.cidr_resources.insert(r.address, r.clone()));
|
||||
}
|
||||
Transition::RemoveResource(id) => {
|
||||
state.client_cidr_resources.retain(|_, r| &r.id != id);
|
||||
state.client_connected_cidr_resources.remove(id);
|
||||
state.client_dns_resources.remove(id);
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.cidr_resources.retain(|_, r| &r.id != id));
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.connected_cidr_resources.remove(id));
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.dns_resources.remove(id));
|
||||
}
|
||||
Transition::AddDnsResource {
|
||||
resource: new_resource,
|
||||
records,
|
||||
} => {
|
||||
let existing_resource = state
|
||||
.client_dns_resources
|
||||
.insert(new_resource.id, new_resource.clone());
|
||||
let existing_resource = state.client.exec_mut(|client| {
|
||||
client
|
||||
.dns_resources
|
||||
.insert(new_resource.id, new_resource.clone())
|
||||
});
|
||||
|
||||
// For the client, there is no difference between a DNS resource and a truly global DNS name.
|
||||
// We store all records in the same map to follow the same model.
|
||||
@@ -339,7 +262,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
// If a resource is updated (i.e. same ID but different address) and we are currently connected, we disconnect from it.
|
||||
if let Some(resource) = existing_resource {
|
||||
if new_resource.address != resource.address {
|
||||
state.client_connected_cidr_resources.remove(&resource.id);
|
||||
state.client.exec_mut(|client| {
|
||||
client.connected_cidr_resources.remove(&resource.id)
|
||||
});
|
||||
|
||||
state
|
||||
.global_dns_records
|
||||
@@ -348,9 +273,11 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
// TODO: IN PRODUCTION, WE CANNOT DO THIS.
|
||||
// CHANGING A DNS RESOURCE BREAKS CLIENT UNTIL THEY DECIDE TO RE-QUERY THE RESOURCE.
|
||||
// WE DO THIS HERE TO ENSURE THE TEST DOESN'T RUN INTO THIS.
|
||||
state
|
||||
.client_dns_records
|
||||
.retain(|name, _| !matches_domain(&resource.address, name));
|
||||
state.client.exec_mut(|client| {
|
||||
client
|
||||
.dns_records
|
||||
.retain(|name, _| !matches_domain(&resource.address, name))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,22 +287,30 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
dns_server,
|
||||
query_id,
|
||||
..
|
||||
} => match state.dns_query_via_cidr_resource(dns_server.ip(), domain) {
|
||||
} => match state
|
||||
.client
|
||||
.inner()
|
||||
.dns_query_via_cidr_resource(dns_server.ip(), domain)
|
||||
{
|
||||
Some(resource)
|
||||
if !state.client_connected_cidr_resources.contains(&resource)
|
||||
&& !state
|
||||
.client_known_host_records
|
||||
.contains_key(&domain.to_string()) =>
|
||||
if !state.client.inner().is_connected_to_cidr(resource)
|
||||
&& !state.client.inner().is_known_host(&domain.to_string()) =>
|
||||
{
|
||||
state.client_connected_cidr_resources.insert(resource);
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.connected_cidr_resources.insert(resource));
|
||||
}
|
||||
Some(_) | None => {
|
||||
state.client.exec_mut(|client| {
|
||||
client
|
||||
.dns_records
|
||||
.entry(domain.clone())
|
||||
.or_default()
|
||||
.insert(*r_type)
|
||||
});
|
||||
state
|
||||
.client_dns_records
|
||||
.entry(domain.clone())
|
||||
.or_default()
|
||||
.insert(*r_type);
|
||||
state.expected_dns_handshakes.push_back(*query_id);
|
||||
.client
|
||||
.exec_mut(|client| client.expected_dns_handshakes.push_back(*query_id));
|
||||
}
|
||||
},
|
||||
Transition::SendICMPPacketToNonResourceIp { .. } => {
|
||||
@@ -388,7 +323,9 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
identifier,
|
||||
..
|
||||
} => {
|
||||
state.on_icmp_packet_to_cidr(*src, *dst, *seq, *identifier);
|
||||
state.client.exec_mut(|client| {
|
||||
client.on_icmp_packet_to_cidr(*src, *dst, *seq, *identifier)
|
||||
});
|
||||
}
|
||||
Transition::SendICMPPacketToDnsResource {
|
||||
src,
|
||||
@@ -396,13 +333,19 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
seq,
|
||||
identifier,
|
||||
..
|
||||
} => state.on_icmp_packet_to_dns(*src, dst.clone(), *seq, *identifier),
|
||||
} => state.client.exec_mut(|client| {
|
||||
client.on_icmp_packet_to_dns(*src, dst.clone(), *seq, *identifier)
|
||||
}),
|
||||
Transition::Tick { millis } => state.now += Duration::from_millis(*millis),
|
||||
Transition::UpdateSystemDnsServers { servers } => {
|
||||
state.system_dns_resolvers.clone_from(servers);
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.system_dns_resolvers.clone_from(servers));
|
||||
}
|
||||
Transition::UpdateUpstreamDnsServers { servers } => {
|
||||
state.upstream_dns_resolvers.clone_from(servers);
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.upstream_dns_resolvers.clone_from(servers));
|
||||
}
|
||||
Transition::RoamClient { ip4, ip6, .. } => {
|
||||
state.network.remove_host(&state.client);
|
||||
@@ -413,8 +356,12 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
.add_host(state.client.inner().id, &state.client));
|
||||
|
||||
// When roaming, we are not connected to any resource and wait for the next packet to re-establish a connection.
|
||||
state.client_connected_cidr_resources.clear();
|
||||
state.client_connected_dns_resources.clear();
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.connected_cidr_resources.clear());
|
||||
state
|
||||
.client
|
||||
.exec_mut(|client| client.connected_dns_resources.clear());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -457,9 +404,14 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
}
|
||||
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS.
|
||||
let any_real_ip_overlaps_with_cidr_resource = resolved_ips
|
||||
.iter()
|
||||
.any(|resolved_ip| state.cidr_resource_by_ip(*resolved_ip).is_some());
|
||||
let any_real_ip_overlaps_with_cidr_resource =
|
||||
resolved_ips.iter().any(|resolved_ip| {
|
||||
state
|
||||
.client
|
||||
.inner()
|
||||
.cidr_resource_by_ip(*resolved_ip)
|
||||
.is_some()
|
||||
});
|
||||
|
||||
if any_real_ip_overlaps_with_cidr_resource {
|
||||
return false;
|
||||
@@ -475,14 +427,20 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
identifier,
|
||||
..
|
||||
} => {
|
||||
let is_valid_icmp_packet = state.is_valid_icmp_packet(seq, identifier);
|
||||
let is_cidr_resource = state.client_cidr_resources.longest_match(*dst).is_some();
|
||||
let is_valid_icmp_packet =
|
||||
state.client.inner().is_valid_icmp_packet(seq, identifier);
|
||||
let is_cidr_resource = state
|
||||
.client
|
||||
.inner()
|
||||
.cidr_resources
|
||||
.longest_match(*dst)
|
||||
.is_some();
|
||||
|
||||
is_valid_icmp_packet && !is_cidr_resource
|
||||
}
|
||||
Transition::SendICMPPacketToCidrResource {
|
||||
seq, identifier, ..
|
||||
} => state.is_valid_icmp_packet(seq, identifier),
|
||||
} => state.client.inner().is_valid_icmp_packet(seq, identifier),
|
||||
Transition::SendICMPPacketToDnsResource {
|
||||
seq,
|
||||
identifier,
|
||||
@@ -490,9 +448,11 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
src,
|
||||
..
|
||||
} => {
|
||||
state.is_valid_icmp_packet(seq, identifier)
|
||||
state.client.inner().is_valid_icmp_packet(seq, identifier)
|
||||
&& state
|
||||
.client_dns_records
|
||||
.client
|
||||
.inner()
|
||||
.dns_records
|
||||
.get(dst)
|
||||
.is_some_and(|r| match src {
|
||||
IpAddr::V4(_) => r.contains(&RecordType::A),
|
||||
@@ -527,12 +487,13 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
domain, dns_server, ..
|
||||
} => {
|
||||
state.global_dns_records.contains_key(domain)
|
||||
&& state.expected_dns_servers().contains(dns_server)
|
||||
}
|
||||
Transition::RemoveResource(id) => {
|
||||
state.client_cidr_resources.iter().any(|(_, r)| &r.id == id)
|
||||
|| state.client_dns_resources.contains_key(id)
|
||||
&& state
|
||||
.client
|
||||
.inner()
|
||||
.expected_dns_servers()
|
||||
.contains(dns_server)
|
||||
}
|
||||
Transition::RemoveResource(id) => state.client.inner().all_resources().contains(id),
|
||||
Transition::RoamClient { ip4, ip6, port } => {
|
||||
// In production, we always rebind to a new port so we never roam to our old existing IP / port combination.
|
||||
|
||||
@@ -546,262 +507,20 @@ impl ReferenceStateMachine for ReferenceState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pub(crate) functions used across the test suite.
|
||||
impl ReferenceState {
|
||||
/// Returns the DNS servers that we expect connlib to use.
|
||||
///
|
||||
/// If there are upstream DNS servers configured in the portal, it should use those.
|
||||
/// Otherwise it should use whatever was configured on the system prior to connlib starting.
|
||||
pub(crate) fn expected_dns_servers(&self) -> BTreeSet<SocketAddr> {
|
||||
if !self.upstream_dns_resolvers.is_empty() {
|
||||
return self
|
||||
.upstream_dns_resolvers
|
||||
.iter()
|
||||
.map(|s| s.address())
|
||||
.collect();
|
||||
}
|
||||
|
||||
self.system_dns_resolvers
|
||||
.iter()
|
||||
.map(|ip| SocketAddr::new(*ip, 53))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Several helper functions to make the reference state more readable.
|
||||
impl ReferenceState {
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(dst, resource))]
|
||||
fn on_icmp_packet_to_cidr(&mut self, src: IpAddr, dst: IpAddr, seq: u16, identifier: u16) {
|
||||
tracing::Span::current().record("dst", tracing::field::display(dst));
|
||||
|
||||
// Second, if we are not yet connected, check if we have a resource for this IP.
|
||||
let Some((_, resource)) = self.client_cidr_resources.longest_match(dst) else {
|
||||
tracing::debug!("No resource corresponds to IP");
|
||||
return;
|
||||
};
|
||||
tracing::Span::current().record("resource", tracing::field::display(resource.id));
|
||||
|
||||
if self.client_connected_cidr_resources.contains(&resource.id)
|
||||
&& self.client.inner().is_tunnel_ip(src)
|
||||
{
|
||||
tracing::debug!("Connected to CIDR resource, expecting packet to be routed");
|
||||
self.expected_icmp_handshakes
|
||||
.push_back((ResourceDst::Cidr(dst), seq, identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a resource, the first packet will initiate a connection to the gateway.
|
||||
tracing::debug!("Not connected to resource, expecting to trigger connection intent");
|
||||
self.client_connected_cidr_resources.insert(resource.id);
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(dst, resource))]
|
||||
fn on_icmp_packet_to_dns(&mut self, src: IpAddr, dst: DomainName, seq: u16, identifier: u16) {
|
||||
tracing::Span::current().record("dst", tracing::field::display(&dst));
|
||||
|
||||
let Some(resource) = self.dns_resource_by_domain(&dst) else {
|
||||
tracing::debug!("No resource corresponds to IP");
|
||||
return;
|
||||
};
|
||||
|
||||
tracing::Span::current().record("resource", tracing::field::display(resource));
|
||||
|
||||
if self
|
||||
.client_connected_dns_resources
|
||||
.contains(&(resource, dst.clone()))
|
||||
&& self.client.inner().is_tunnel_ip(src)
|
||||
{
|
||||
tracing::debug!("Connected to DNS resource, expecting packet to be routed");
|
||||
self.expected_icmp_handshakes
|
||||
.push_back((ResourceDst::Dns(dst), seq, identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
self.client_dns_records.iter().any(|(name, _)| name == &dst),
|
||||
"Should only sample ICMPs to domains that we resolved"
|
||||
);
|
||||
|
||||
tracing::debug!("Not connected to resource, expecting to trigger connection intent");
|
||||
self.client_connected_dns_resources.insert((resource, dst));
|
||||
}
|
||||
|
||||
fn ipv4_cidr_resource_dsts(&self) -> Vec<Ipv4Addr> {
|
||||
let mut ips = vec![];
|
||||
|
||||
// This is an imperative loop on purpose because `ip-network` appears to have a bug with its `size_hint` and thus `.extend` does not work reliably?
|
||||
for (network, _) in self.client_cidr_resources.iter_ipv4() {
|
||||
if network.netmask() == 31 || network.netmask() == 32 {
|
||||
ips.push(network.network_address());
|
||||
} else {
|
||||
for ip in network.hosts() {
|
||||
ips.push(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
fn ipv6_cidr_resource_dsts(&self) -> Vec<Ipv6Addr> {
|
||||
let mut ips = vec![];
|
||||
|
||||
// This is an imperative loop on purpose because `ip-network` appears to have a bug with its `size_hint` and thus `.extend` does not work reliably?
|
||||
for (network, _) in self.client_cidr_resources.iter_ipv6() {
|
||||
if network.netmask() == 127 || network.netmask() == 128 {
|
||||
ips.push(network.network_address());
|
||||
} else {
|
||||
for ip in network
|
||||
.subnets_with_prefix(128)
|
||||
.map(|i| i.network_address())
|
||||
{
|
||||
ips.push(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
fn resolved_v4_domains(&self) -> Vec<DomainName> {
|
||||
self.resolved_domains()
|
||||
.filter_map(|(domain, records)| {
|
||||
records
|
||||
.iter()
|
||||
.any(|r| matches!(r, RecordType::A))
|
||||
.then_some(domain)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolved_v6_domains(&self) -> Vec<DomainName> {
|
||||
self.resolved_domains()
|
||||
.filter_map(|(domain, records)| {
|
||||
records
|
||||
.iter()
|
||||
.any(|r| matches!(r, RecordType::AAAA))
|
||||
.then_some(domain)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn all_domains(&self) -> Vec<DomainName> {
|
||||
fn all_domains(&self, client: &RefClient) -> Vec<DomainName> {
|
||||
self.global_dns_records
|
||||
.keys()
|
||||
.cloned()
|
||||
.chain(
|
||||
self.client_known_host_records
|
||||
client
|
||||
.known_hosts
|
||||
.keys()
|
||||
.map(|h| DomainName::vec_from_str(h).unwrap()),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolved_domains(&self) -> impl Iterator<Item = (DomainName, HashSet<RecordType>)> + '_ {
|
||||
self.client_dns_records
|
||||
.iter()
|
||||
.filter(|(domain, _)| self.dns_resource_by_domain(domain).is_some())
|
||||
.map(|(domain, ips)| (domain.clone(), ips.clone()))
|
||||
}
|
||||
|
||||
/// An ICMP packet is valid if we didn't yet send an ICMP packet with the same seq and identifier.
|
||||
fn is_valid_icmp_packet(&self, seq: &u16, identifier: &u16) -> bool {
|
||||
self.expected_icmp_handshakes
|
||||
.iter()
|
||||
.all(|(_, existing_seq, existing_identifer)| {
|
||||
existing_seq != seq && existing_identifer != identifier
|
||||
})
|
||||
}
|
||||
|
||||
fn v4_dns_servers(&self) -> Vec<SocketAddrV4> {
|
||||
self.expected_dns_servers()
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
SocketAddr::V4(v4) => Some(v4),
|
||||
SocketAddr::V6(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn v6_dns_servers(&self) -> Vec<SocketAddrV6> {
|
||||
self.expected_dns_servers()
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
SocketAddr::V6(v6) => Some(v6),
|
||||
SocketAddr::V4(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn dns_resource_by_domain(&self, domain: &DomainName) -> Option<ResourceId> {
|
||||
self.client_dns_resources
|
||||
.values()
|
||||
.filter(|r| is_subdomain(&domain.to_string(), &r.address))
|
||||
.sorted_by_key(|r| r.address.len())
|
||||
.rev()
|
||||
.map(|r| r.id)
|
||||
.next()
|
||||
}
|
||||
|
||||
fn cidr_resource_by_ip(&self, ip: IpAddr) -> Option<ResourceId> {
|
||||
self.client_cidr_resources
|
||||
.longest_match(ip)
|
||||
.map(|(_, r)| r.id)
|
||||
}
|
||||
|
||||
fn resolved_ip4_for_non_resources(&self) -> Vec<Ipv4Addr> {
|
||||
self.resolved_ips_for_non_resources()
|
||||
.filter_map(|ip| match ip {
|
||||
IpAddr::V4(v4) => Some(v4),
|
||||
IpAddr::V6(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolved_ip6_for_non_resources(&self) -> Vec<Ipv6Addr> {
|
||||
self.resolved_ips_for_non_resources()
|
||||
.filter_map(|ip| match ip {
|
||||
IpAddr::V6(v6) => Some(v6),
|
||||
IpAddr::V4(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolved_ips_for_non_resources(&self) -> impl Iterator<Item = IpAddr> + '_ {
|
||||
self.client_dns_records
|
||||
.iter()
|
||||
.filter_map(|(domain, _)| {
|
||||
self.dns_resource_by_domain(domain)
|
||||
.is_none()
|
||||
.then_some(self.global_dns_records.get(domain))
|
||||
})
|
||||
.flatten()
|
||||
.flatten()
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Returns the CIDR resource we will forward the DNS query for the given name to.
|
||||
///
|
||||
/// DNS servers may be resources, in which case queries that need to be forwarded actually need to be encapsulated.
|
||||
fn dns_query_via_cidr_resource(
|
||||
&self,
|
||||
dns_server: IpAddr,
|
||||
domain: &DomainName,
|
||||
) -> Option<ResourceId> {
|
||||
// If we are querying a DNS resource, we will issue a connection intent to the DNS resource, not the CIDR resource.
|
||||
if self.dns_resource_by_domain(domain).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.cidr_resource_by_ip(dns_server)
|
||||
}
|
||||
|
||||
fn all_resources(&self) -> Vec<ResourceId> {
|
||||
let cidr_resources = self.client_cidr_resources.iter().map(|(_, r)| r.id);
|
||||
let dns_resources = self.client_dns_resources.keys().copied();
|
||||
|
||||
Vec::from_iter(cidr_resources.chain(dns_resources))
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_domain(resource_address: &str, domain: &DomainName) -> bool {
|
||||
@@ -816,21 +535,23 @@ fn matches_domain(resource_address: &str, domain: &DomainName) -> bool {
|
||||
name == resource_address
|
||||
}
|
||||
|
||||
fn is_subdomain(name: &str, record: &str) -> bool {
|
||||
if name == record {
|
||||
return true;
|
||||
}
|
||||
let Some((first, end)) = record.split_once('.') else {
|
||||
return false;
|
||||
};
|
||||
match first {
|
||||
"*" => name.ends_with(end) && name.strip_suffix(end).is_some_and(|n| n.ends_with('.')),
|
||||
"?" => {
|
||||
name.ends_with(end)
|
||||
&& name
|
||||
.strip_suffix(end)
|
||||
.is_some_and(|n| n.ends_with('.') && n.matches('.').count() == 1)
|
||||
}
|
||||
_ => false,
|
||||
pub(crate) fn private_key() -> impl Strategy<Value = PrivateKey> {
|
||||
any::<[u8; 32]>().prop_map(PrivateKey)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) struct PrivateKey([u8; 32]);
|
||||
|
||||
impl From<PrivateKey> for StaticSecret {
|
||||
fn from(key: PrivateKey) -> Self {
|
||||
StaticSecret::from(key.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PrivateKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("PrivateKey")
|
||||
.field(&hex::encode(self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
639
rust/connlib/tunnel/src/tests/sim_client.rs
Normal file
639
rust/connlib/tunnel/src/tests/sim_client.rs
Normal file
@@ -0,0 +1,639 @@
|
||||
use super::{
|
||||
reference::{private_key, PrivateKey, ResourceDst},
|
||||
sim_net::{any_ip_stack, any_port, host, Host},
|
||||
strategies::{system_dns_servers, upstream_dns_servers},
|
||||
sut::domain_to_hickory_name,
|
||||
IcmpIdentifier, IcmpSeq, QueryId,
|
||||
};
|
||||
use crate::{tests::sut::hickory_name_to_domain, ClientState};
|
||||
use bimap::BiMap;
|
||||
use connlib_shared::{
|
||||
messages::{
|
||||
client::{ResourceDescriptionCidr, ResourceDescriptionDns},
|
||||
ClientId, DnsServer, Interface, ResourceId,
|
||||
},
|
||||
proptest::{client_id, domain_name},
|
||||
DomainName,
|
||||
};
|
||||
use hickory_proto::{
|
||||
op::MessageType,
|
||||
rr::{rdata, RData, RecordType},
|
||||
serialize::binary::BinDecodable as _,
|
||||
};
|
||||
use ip_network_table::IpNetworkTable;
|
||||
use ip_packet::{IpPacket, MutableIpPacket, Packet as _};
|
||||
use itertools::Itertools as _;
|
||||
use prop::collection;
|
||||
use proptest::prelude::*;
|
||||
use snownet::Transmit;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// Simulation state for a particular client.
|
||||
pub(crate) struct SimClient {
|
||||
pub(crate) id: ClientId,
|
||||
|
||||
pub(crate) sut: ClientState,
|
||||
|
||||
/// The DNS records created on the client as a result of received DNS responses.
|
||||
///
|
||||
/// This contains results from both, queries to DNS resources and non-resources.
|
||||
pub(crate) dns_records: HashMap<DomainName, Vec<IpAddr>>,
|
||||
|
||||
/// Bi-directional mapping between connlib's sentinel DNS IPs and the effective DNS servers.
|
||||
pub(crate) dns_by_sentinel: BiMap<IpAddr, SocketAddr>,
|
||||
|
||||
pub(crate) sent_dns_queries: HashMap<QueryId, IpPacket<'static>>,
|
||||
pub(crate) received_dns_responses: HashMap<QueryId, IpPacket<'static>>,
|
||||
|
||||
pub(crate) sent_icmp_requests: HashMap<(u16, u16), IpPacket<'static>>,
|
||||
pub(crate) received_icmp_replies: HashMap<(u16, u16), IpPacket<'static>>,
|
||||
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SimClient {
|
||||
pub(crate) fn new(id: ClientId, sut: ClientState) -> Self {
|
||||
Self {
|
||||
id,
|
||||
sut,
|
||||
dns_records: Default::default(),
|
||||
dns_by_sentinel: Default::default(),
|
||||
sent_dns_queries: Default::default(),
|
||||
received_dns_responses: Default::default(),
|
||||
sent_icmp_requests: Default::default(),
|
||||
received_icmp_replies: Default::default(),
|
||||
buffer: vec![0u8; (1 << 16) - 1],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the _effective_ DNS servers that connlib is using.
|
||||
pub(crate) fn effective_dns_servers(&self) -> BTreeSet<SocketAddr> {
|
||||
self.dns_by_sentinel.right_values().copied().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn send_dns_query_for(
|
||||
&mut self,
|
||||
domain: DomainName,
|
||||
r_type: RecordType,
|
||||
query_id: u16,
|
||||
dns_server: SocketAddr,
|
||||
now: Instant,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let dns_server = *self
|
||||
.dns_by_sentinel
|
||||
.get_by_right(&dns_server)
|
||||
.expect("to have a sentinel DNS server for the sampled one");
|
||||
|
||||
let name = domain_to_hickory_name(domain);
|
||||
|
||||
let src = self
|
||||
.sut
|
||||
.tunnel_ip_for(dns_server)
|
||||
.expect("tunnel should be initialised");
|
||||
|
||||
let packet = ip_packet::make::dns_query(
|
||||
name,
|
||||
r_type,
|
||||
SocketAddr::new(src, 9999), // An application would pick a random source port that is free.
|
||||
SocketAddr::new(dns_server, 53),
|
||||
query_id,
|
||||
);
|
||||
|
||||
self.encapsulate(packet, now)
|
||||
}
|
||||
|
||||
pub(crate) fn encapsulate(
|
||||
&mut self,
|
||||
packet: MutableIpPacket<'static>,
|
||||
now: Instant,
|
||||
) -> Option<snownet::Transmit<'static>> {
|
||||
{
|
||||
let packet = packet.to_owned().into_immutable();
|
||||
|
||||
if let Some(icmp) = packet.as_icmp() {
|
||||
let echo_request = icmp.as_echo_request().expect("to be echo request");
|
||||
|
||||
self.sent_icmp_requests
|
||||
.insert((echo_request.sequence(), echo_request.identifier()), packet);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let packet = packet.to_owned().into_immutable();
|
||||
|
||||
if let Some(udp) = packet.as_udp() {
|
||||
if let Ok(message) = hickory_proto::op::Message::from_bytes(udp.payload()) {
|
||||
debug_assert_eq!(
|
||||
message.message_type(),
|
||||
MessageType::Query,
|
||||
"every DNS message sent from the client should be a DNS query"
|
||||
);
|
||||
|
||||
self.sent_dns_queries.insert(message.id(), packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(self.sut.encapsulate(packet, now)?.into_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn handle_packet(
|
||||
&mut self,
|
||||
payload: &[u8],
|
||||
src: SocketAddr,
|
||||
dst: SocketAddr,
|
||||
now: Instant,
|
||||
) {
|
||||
let Some(packet) = self
|
||||
.sut
|
||||
.decapsulate(dst, src, payload, now, &mut self.buffer)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let packet = packet.to_owned();
|
||||
|
||||
self.on_received_packet(packet);
|
||||
}
|
||||
|
||||
/// Process an IP packet received on the client.
|
||||
pub(crate) fn on_received_packet(&mut self, packet: IpPacket<'_>) {
|
||||
if let Some(icmp) = packet.as_icmp() {
|
||||
let echo_reply = icmp.as_echo_reply().expect("to be echo reply");
|
||||
|
||||
self.received_icmp_replies.insert(
|
||||
(echo_reply.sequence(), echo_reply.identifier()),
|
||||
packet.to_owned(),
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(udp) = packet.as_udp() {
|
||||
if udp.get_source() == 53 {
|
||||
let mut message = hickory_proto::op::Message::from_bytes(udp.payload())
|
||||
.expect("ip packets on port 53 to be DNS packets");
|
||||
|
||||
self.received_dns_responses
|
||||
.insert(message.id(), packet.to_owned());
|
||||
|
||||
for record in message.take_answers().into_iter() {
|
||||
let domain = hickory_name_to_domain(record.name().clone());
|
||||
|
||||
let ip = match record.data() {
|
||||
Some(RData::A(rdata::A(ip4))) => IpAddr::from(*ip4),
|
||||
Some(RData::AAAA(rdata::AAAA(ip6))) => IpAddr::from(*ip6),
|
||||
unhandled => {
|
||||
panic!("Unexpected record data: {unhandled:?}")
|
||||
}
|
||||
};
|
||||
|
||||
self.dns_records.entry(domain).or_default().push(ip);
|
||||
}
|
||||
|
||||
// Ensure all IPs are always sorted.
|
||||
for ips in self.dns_records.values_mut() {
|
||||
ips.sort()
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unimplemented!("Unhandled packet")
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference state for a particular client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefClient {
|
||||
pub(crate) id: ClientId,
|
||||
pub(crate) key: PrivateKey,
|
||||
pub(crate) known_hosts: HashMap<String, Vec<IpAddr>>,
|
||||
pub(crate) tunnel_ip4: Ipv4Addr,
|
||||
pub(crate) tunnel_ip6: Ipv6Addr,
|
||||
|
||||
/// The DNS resolvers configured on the client outside of connlib.
|
||||
pub(crate) system_dns_resolvers: Vec<IpAddr>,
|
||||
/// The upstream DNS resolvers configured in the portal.
|
||||
pub(crate) upstream_dns_resolvers: Vec<DnsServer>,
|
||||
|
||||
/// The CIDR resources the client is aware of.
|
||||
pub(crate) cidr_resources: IpNetworkTable<ResourceDescriptionCidr>,
|
||||
/// The DNS resources the client is aware of.
|
||||
pub(crate) dns_resources: BTreeMap<ResourceId, ResourceDescriptionDns>,
|
||||
|
||||
/// The client's DNS records.
|
||||
///
|
||||
/// The IPs assigned to a domain by connlib are an implementation detail that we don't want to model in these tests.
|
||||
/// Instead, we just remember what _kind_ of records we resolved to be able to sample a matching src IP.
|
||||
pub(crate) dns_records: BTreeMap<DomainName, HashSet<RecordType>>,
|
||||
|
||||
/// The CIDR resources the client is connected to.
|
||||
pub(crate) connected_cidr_resources: HashSet<ResourceId>,
|
||||
|
||||
/// The DNS resources the client is connected to.
|
||||
pub(crate) connected_dns_resources: HashSet<(ResourceId, DomainName)>,
|
||||
|
||||
/// The expected ICMP handshakes.
|
||||
pub(crate) expected_icmp_handshakes: VecDeque<(ResourceDst, IcmpSeq, IcmpIdentifier)>,
|
||||
/// The expected DNS handshakes.
|
||||
pub(crate) expected_dns_handshakes: VecDeque<QueryId>,
|
||||
}
|
||||
|
||||
impl RefClient {
|
||||
/// Initialize the [`ClientState`].
|
||||
///
|
||||
/// This simulates receiving the `init` message from the portal.
|
||||
pub(crate) fn init(self) -> SimClient {
|
||||
let mut client_state = ClientState::new(self.key, self.known_hosts);
|
||||
let _ = client_state.update_interface_config(Interface {
|
||||
ipv4: self.tunnel_ip4,
|
||||
ipv6: self.tunnel_ip6,
|
||||
upstream_dns: self.upstream_dns_resolvers,
|
||||
});
|
||||
let _ = client_state.update_system_resolvers(self.system_dns_resolvers);
|
||||
|
||||
SimClient::new(self.id, client_state)
|
||||
}
|
||||
|
||||
pub(crate) fn is_tunnel_ip(&self, ip: IpAddr) -> bool {
|
||||
match ip {
|
||||
IpAddr::V4(ip4) => self.tunnel_ip4 == ip4,
|
||||
IpAddr::V6(ip6) => self.tunnel_ip6 == ip6,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn tunnel_ip_for(&self, dst: IpAddr) -> IpAddr {
|
||||
match dst {
|
||||
IpAddr::V4(_) => self.tunnel_ip4.into(),
|
||||
IpAddr::V6(_) => self.tunnel_ip6.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(dst, resource))]
|
||||
pub(crate) fn on_icmp_packet_to_cidr(
|
||||
&mut self,
|
||||
src: IpAddr,
|
||||
dst: IpAddr,
|
||||
seq: u16,
|
||||
identifier: u16,
|
||||
) {
|
||||
tracing::Span::current().record("dst", tracing::field::display(dst));
|
||||
|
||||
// Second, if we are not yet connected, check if we have a resource for this IP.
|
||||
let Some((_, resource)) = self.cidr_resources.longest_match(dst) else {
|
||||
tracing::debug!("No resource corresponds to IP");
|
||||
return;
|
||||
};
|
||||
tracing::Span::current().record("resource", tracing::field::display(resource.id));
|
||||
|
||||
if self.is_connected_to_cidr(resource.id) && self.is_tunnel_ip(src) {
|
||||
tracing::debug!("Connected to CIDR resource, expecting packet to be routed");
|
||||
self.expected_icmp_handshakes
|
||||
.push_back((ResourceDst::Cidr(dst), seq, identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a resource, the first packet will initiate a connection to the gateway.
|
||||
tracing::debug!("Not connected to resource, expecting to trigger connection intent");
|
||||
self.connected_cidr_resources.insert(resource.id);
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, fields(dst, resource))]
|
||||
pub(crate) fn on_icmp_packet_to_dns(
|
||||
&mut self,
|
||||
src: IpAddr,
|
||||
dst: DomainName,
|
||||
seq: u16,
|
||||
identifier: u16,
|
||||
) {
|
||||
tracing::Span::current().record("dst", tracing::field::display(&dst));
|
||||
|
||||
let Some(resource) = self.dns_resource_by_domain(&dst) else {
|
||||
tracing::debug!("No resource corresponds to IP");
|
||||
return;
|
||||
};
|
||||
|
||||
tracing::Span::current().record("resource", tracing::field::display(resource));
|
||||
|
||||
if self
|
||||
.connected_dns_resources
|
||||
.contains(&(resource, dst.clone()))
|
||||
&& self.is_tunnel_ip(src)
|
||||
{
|
||||
tracing::debug!("Connected to DNS resource, expecting packet to be routed");
|
||||
self.expected_icmp_handshakes
|
||||
.push_back((ResourceDst::Dns(dst), seq, identifier));
|
||||
return;
|
||||
}
|
||||
|
||||
debug_assert!(
|
||||
self.dns_records.iter().any(|(name, _)| name == &dst),
|
||||
"Should only sample ICMPs to domains that we resolved"
|
||||
);
|
||||
|
||||
tracing::debug!("Not connected to resource, expecting to trigger connection intent");
|
||||
self.connected_dns_resources.insert((resource, dst));
|
||||
}
|
||||
|
||||
pub(crate) fn ipv4_cidr_resource_dsts(&self) -> Vec<Ipv4Addr> {
|
||||
let mut ips = vec![];
|
||||
|
||||
// This is an imperative loop on purpose because `ip-network` appears to have a bug with its `size_hint` and thus `.extend` does not work reliably?
|
||||
for (network, _) in self.cidr_resources.iter_ipv4() {
|
||||
if network.netmask() == 31 || network.netmask() == 32 {
|
||||
ips.push(network.network_address());
|
||||
} else {
|
||||
for ip in network.hosts() {
|
||||
ips.push(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
pub(crate) fn ipv6_cidr_resource_dsts(&self) -> Vec<Ipv6Addr> {
|
||||
let mut ips = vec![];
|
||||
|
||||
// This is an imperative loop on purpose because `ip-network` appears to have a bug with its `size_hint` and thus `.extend` does not work reliably?
|
||||
for (network, _) in self.cidr_resources.iter_ipv6() {
|
||||
if network.netmask() == 127 || network.netmask() == 128 {
|
||||
ips.push(network.network_address());
|
||||
} else {
|
||||
for ip in network
|
||||
.subnets_with_prefix(128)
|
||||
.map(|i| i.network_address())
|
||||
{
|
||||
ips.push(ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ips
|
||||
}
|
||||
|
||||
pub(crate) fn is_connected_to_cidr(&self, id: ResourceId) -> bool {
|
||||
self.connected_cidr_resources.contains(&id)
|
||||
}
|
||||
|
||||
pub(crate) fn is_known_host(&self, name: &str) -> bool {
|
||||
self.known_hosts.contains_key(name)
|
||||
}
|
||||
|
||||
fn dns_resource_by_domain(&self, domain: &DomainName) -> Option<ResourceId> {
|
||||
self.dns_resources
|
||||
.values()
|
||||
.filter(|r| is_subdomain(&domain.to_string(), &r.address))
|
||||
.sorted_by_key(|r| r.address.len())
|
||||
.rev()
|
||||
.map(|r| r.id)
|
||||
.next()
|
||||
}
|
||||
|
||||
fn resolved_domains(&self) -> impl Iterator<Item = (DomainName, HashSet<RecordType>)> + '_ {
|
||||
self.dns_records
|
||||
.iter()
|
||||
.filter(|(domain, _)| self.dns_resource_by_domain(domain).is_some())
|
||||
.map(|(domain, ips)| (domain.clone(), ips.clone()))
|
||||
}
|
||||
|
||||
/// An ICMP packet is valid if we didn't yet send an ICMP packet with the same seq and identifier.
|
||||
pub(crate) fn is_valid_icmp_packet(&self, seq: &u16, identifier: &u16) -> bool {
|
||||
self.expected_icmp_handshakes
|
||||
.iter()
|
||||
.all(|(_, existing_seq, existing_identifer)| {
|
||||
existing_seq != seq && existing_identifer != identifier
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_v4_domains(&self) -> Vec<DomainName> {
|
||||
self.resolved_domains()
|
||||
.filter_map(|(domain, records)| {
|
||||
records
|
||||
.iter()
|
||||
.any(|r| matches!(r, RecordType::A))
|
||||
.then_some(domain)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_v6_domains(&self) -> Vec<DomainName> {
|
||||
self.resolved_domains()
|
||||
.filter_map(|(domain, records)| {
|
||||
records
|
||||
.iter()
|
||||
.any(|r| matches!(r, RecordType::AAAA))
|
||||
.then_some(domain)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the DNS servers that we expect connlib to use.
|
||||
///
|
||||
/// If there are upstream DNS servers configured in the portal, it should use those.
|
||||
/// Otherwise it should use whatever was configured on the system prior to connlib starting.
|
||||
pub(crate) fn expected_dns_servers(&self) -> BTreeSet<SocketAddr> {
|
||||
if !self.upstream_dns_resolvers.is_empty() {
|
||||
return self
|
||||
.upstream_dns_resolvers
|
||||
.iter()
|
||||
.map(|s| s.address())
|
||||
.collect();
|
||||
}
|
||||
|
||||
self.system_dns_resolvers
|
||||
.iter()
|
||||
.map(|ip| SocketAddr::new(*ip, 53))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn v4_dns_servers(&self) -> Vec<SocketAddrV4> {
|
||||
self.expected_dns_servers()
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
SocketAddr::V4(v4) => Some(v4),
|
||||
SocketAddr::V6(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn v6_dns_servers(&self) -> Vec<SocketAddrV6> {
|
||||
self.expected_dns_servers()
|
||||
.into_iter()
|
||||
.filter_map(|s| match s {
|
||||
SocketAddr::V6(v6) => Some(v6),
|
||||
SocketAddr::V4(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn cidr_resource_by_ip(&self, ip: IpAddr) -> Option<ResourceId> {
|
||||
self.cidr_resources.longest_match(ip).map(|(_, r)| r.id)
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_ip4_for_non_resources(
|
||||
&self,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
) -> Vec<Ipv4Addr> {
|
||||
self.resolved_ips_for_non_resources(global_dns_records)
|
||||
.filter_map(|ip| match ip {
|
||||
IpAddr::V4(v4) => Some(v4),
|
||||
IpAddr::V6(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_ip6_for_non_resources(
|
||||
&self,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
) -> Vec<Ipv6Addr> {
|
||||
self.resolved_ips_for_non_resources(global_dns_records)
|
||||
.filter_map(|ip| match ip {
|
||||
IpAddr::V6(v6) => Some(v6),
|
||||
IpAddr::V4(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolved_ips_for_non_resources<'a>(
|
||||
&'a self,
|
||||
global_dns_records: &'a BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
) -> impl Iterator<Item = IpAddr> + 'a {
|
||||
self.dns_records
|
||||
.iter()
|
||||
.filter_map(|(domain, _)| {
|
||||
self.dns_resource_by_domain(domain)
|
||||
.is_none()
|
||||
.then_some(global_dns_records.get(domain))
|
||||
})
|
||||
.flatten()
|
||||
.flatten()
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Returns the CIDR resource we will forward the DNS query for the given name to.
|
||||
///
|
||||
/// DNS servers may be resources, in which case queries that need to be forwarded actually need to be encapsulated.
|
||||
pub(crate) fn dns_query_via_cidr_resource(
|
||||
&self,
|
||||
dns_server: IpAddr,
|
||||
domain: &DomainName,
|
||||
) -> Option<ResourceId> {
|
||||
// If we are querying a DNS resource, we will issue a connection intent to the DNS resource, not the CIDR resource.
|
||||
if self.dns_resource_by_domain(domain).is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.cidr_resource_by_ip(dns_server)
|
||||
}
|
||||
|
||||
pub(crate) fn all_resources(&self) -> Vec<ResourceId> {
|
||||
let cidr_resources = self.cidr_resources.iter().map(|(_, r)| r.id);
|
||||
let dns_resources = self.dns_resources.keys().copied();
|
||||
|
||||
Vec::from_iter(cidr_resources.chain(dns_resources))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_subdomain(name: &str, record: &str) -> bool {
|
||||
if name == record {
|
||||
return true;
|
||||
}
|
||||
let Some((first, end)) = record.split_once('.') else {
|
||||
return false;
|
||||
};
|
||||
match first {
|
||||
"*" => name.ends_with(end) && name.strip_suffix(end).is_some_and(|n| n.ends_with('.')),
|
||||
"?" => {
|
||||
name.ends_with(end)
|
||||
&& name
|
||||
.strip_suffix(end)
|
||||
.is_some_and(|n| n.ends_with('.') && n.matches('.').count() == 1)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ref_client_host(
|
||||
tunnel_ip4s: &mut impl Iterator<Item = Ipv4Addr>,
|
||||
tunnel_ip6s: &mut impl Iterator<Item = Ipv6Addr>,
|
||||
) -> impl Strategy<Value = Host<RefClient>> {
|
||||
host(
|
||||
any_ip_stack(),
|
||||
any_port(),
|
||||
ref_client(tunnel_ip4s, tunnel_ip6s),
|
||||
)
|
||||
.prop_filter("at least one DNS server needs to be reachable", |host| {
|
||||
// TODO: PRODUCTION CODE DOES NOT HANDLE THIS!
|
||||
|
||||
let upstream_dns_resolvers = &host.inner().upstream_dns_resolvers;
|
||||
let system_dns = &host.inner().system_dns_resolvers;
|
||||
|
||||
if !upstream_dns_resolvers.is_empty() {
|
||||
if host.ip4.is_none() && upstream_dns_resolvers.iter().all(|s| s.ip().is_ipv4()) {
|
||||
return false;
|
||||
}
|
||||
if host.ip6.is_none() && upstream_dns_resolvers.iter().all(|s| s.ip().is_ipv6()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if host.ip4.is_none() && system_dns.iter().all(|s| s.is_ipv4()) {
|
||||
return false;
|
||||
}
|
||||
if host.ip6.is_none() && system_dns.iter().all(|s| s.is_ipv6()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
fn ref_client(
|
||||
tunnel_ip4s: &mut impl Iterator<Item = Ipv4Addr>,
|
||||
tunnel_ip6s: &mut impl Iterator<Item = Ipv6Addr>,
|
||||
) -> impl Strategy<Value = RefClient> {
|
||||
let tunnel_ip4 = tunnel_ip4s.next().unwrap();
|
||||
let tunnel_ip6 = tunnel_ip6s.next().unwrap();
|
||||
|
||||
(
|
||||
client_id(),
|
||||
private_key(),
|
||||
known_hosts(),
|
||||
system_dns_servers(),
|
||||
upstream_dns_servers(),
|
||||
)
|
||||
.prop_map(
|
||||
move |(id, key, known_hosts, system_dns_resolvers, upstream_dns_resolvers)| RefClient {
|
||||
id,
|
||||
key,
|
||||
known_hosts,
|
||||
tunnel_ip4,
|
||||
tunnel_ip6,
|
||||
system_dns_resolvers,
|
||||
upstream_dns_resolvers,
|
||||
cidr_resources: IpNetworkTable::new(),
|
||||
dns_resources: Default::default(),
|
||||
dns_records: Default::default(),
|
||||
connected_cidr_resources: Default::default(),
|
||||
connected_dns_resources: Default::default(),
|
||||
expected_icmp_handshakes: Default::default(),
|
||||
expected_dns_handshakes: Default::default(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn known_hosts() -> impl Strategy<Value = HashMap<String, Vec<IpAddr>>> {
|
||||
collection::hash_map(
|
||||
domain_name(2..4).prop_map(|d| d.parse().unwrap()),
|
||||
collection::vec(any::<IpAddr>(), 1..6),
|
||||
0..15,
|
||||
)
|
||||
}
|
||||
110
rust/connlib/tunnel/src/tests/sim_gateway.rs
Normal file
110
rust/connlib/tunnel/src/tests/sim_gateway.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use super::{
|
||||
reference::{private_key, PrivateKey},
|
||||
sim_net::{any_ip_stack, any_port, host, Host},
|
||||
};
|
||||
use crate::{tests::sut::hickory_name_to_domain, GatewayState};
|
||||
use connlib_shared::{messages::GatewayId, proptest::gateway_id, DomainName};
|
||||
use ip_packet::IpPacket;
|
||||
use proptest::prelude::*;
|
||||
use snownet::Transmit;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet, VecDeque},
|
||||
net::{IpAddr, SocketAddr},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// Simulation state for a particular client.
|
||||
pub(crate) struct SimGateway {
|
||||
pub(crate) id: GatewayId,
|
||||
pub(crate) sut: GatewayState,
|
||||
|
||||
pub(crate) received_icmp_requests: VecDeque<IpPacket<'static>>,
|
||||
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SimGateway {
|
||||
pub(crate) fn new(id: GatewayId, sut: GatewayState) -> Self {
|
||||
Self {
|
||||
id,
|
||||
sut,
|
||||
received_icmp_requests: Default::default(),
|
||||
buffer: vec![0u8; (1 << 16) - 1],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handle_packet(
|
||||
&mut self,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
payload: &[u8],
|
||||
src: SocketAddr,
|
||||
dst: SocketAddr,
|
||||
now: Instant,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let packet = self
|
||||
.sut
|
||||
.decapsulate(dst, src, payload, now, &mut self.buffer)?
|
||||
.to_owned();
|
||||
|
||||
self.on_received_packet(global_dns_records, packet, now)
|
||||
}
|
||||
|
||||
/// Process an IP packet received on the gateway.
|
||||
fn on_received_packet(
|
||||
&mut self,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
packet: IpPacket<'_>,
|
||||
now: Instant,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let packet = packet.to_owned();
|
||||
|
||||
if packet.as_icmp().is_some() {
|
||||
self.received_icmp_requests.push_back(packet.clone());
|
||||
|
||||
let echo_response = ip_packet::make::icmp_response_packet(packet);
|
||||
let transmit = self.sut.encapsulate(echo_response, now)?.into_owned();
|
||||
|
||||
return Some(transmit);
|
||||
}
|
||||
|
||||
if packet.as_udp().is_some() {
|
||||
let response = ip_packet::make::dns_ok_response(packet, |name| {
|
||||
global_dns_records
|
||||
.get(&hickory_name_to_domain(name.clone()))
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
let transmit = self.sut.encapsulate(response, now)?.into_owned();
|
||||
|
||||
return Some(transmit);
|
||||
}
|
||||
|
||||
panic!("Unhandled packet")
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference state for a particular gateway.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefGateway {
|
||||
pub(crate) id: GatewayId,
|
||||
pub(crate) key: PrivateKey,
|
||||
}
|
||||
|
||||
impl RefGateway {
|
||||
/// Initialize the [`GatewayState`].
|
||||
///
|
||||
/// This simulates receiving the `init` message from the portal.
|
||||
pub(crate) fn init(self) -> SimGateway {
|
||||
SimGateway::new(self.id, GatewayState::new(self.key))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ref_gateway_host() -> impl Strategy<Value = Host<RefGateway>> {
|
||||
host(any_ip_stack(), any_port(), ref_gateway())
|
||||
}
|
||||
|
||||
fn ref_gateway() -> impl Strategy<Value = RefGateway> {
|
||||
(gateway_id(), private_key()).prop_map(move |(id, key)| RefGateway { id, key })
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
use super::{sim_node::SimNode, sim_relay::SimRelay};
|
||||
use crate::{ClientState, GatewayState};
|
||||
use connlib_shared::messages::{ClientId, GatewayId, RelayId};
|
||||
use firezone_relay::AddressFamily;
|
||||
use ip_network::IpNetwork;
|
||||
use firezone_relay::{AddressFamily, IpStack};
|
||||
use ip_network::{IpNetwork, Ipv4Network, Ipv6Network};
|
||||
use ip_network_table::IpNetworkTable;
|
||||
use ip_packet::MutableIpPacket;
|
||||
use rand::rngs::StdRng;
|
||||
use snownet::Transmit;
|
||||
use itertools::Itertools as _;
|
||||
use prop::sample;
|
||||
use proptest::prelude::*;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
time::Instant,
|
||||
num::NonZeroU16,
|
||||
};
|
||||
use tracing::Span;
|
||||
|
||||
@@ -98,39 +97,17 @@ impl<T> Host<T> {
|
||||
self.allocate_port(port, AddressFamily::V6);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the `src` of the given [`Transmit`] in case it is missing.
|
||||
///
|
||||
/// The `src` of a [`Transmit`] is empty if we want to send if via the default interface.
|
||||
/// In production, the kernel does this for us.
|
||||
/// In this test, we need to always set a `src` so that the remote peer knows where the packet is coming from.
|
||||
fn set_transmit_src(&self, transmit: Transmit<'static>) -> Option<Transmit<'static>> {
|
||||
if transmit.src.is_some() {
|
||||
return Some(transmit);
|
||||
}
|
||||
|
||||
let Some(src) = self.sending_socket_for(transmit.dst.ip()) else {
|
||||
tracing::debug!(dst = %transmit.dst, "No socket");
|
||||
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Transmit {
|
||||
src: Some(src),
|
||||
..transmit
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Host<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
pub(crate) fn map<S>(
|
||||
pub(crate) fn map<U>(
|
||||
&self,
|
||||
f: impl FnOnce(T, Option<Ipv4Addr>, Option<Ipv6Addr>) -> S,
|
||||
f: impl FnOnce(T, Option<Ipv4Addr>, Option<Ipv6Addr>) -> U,
|
||||
span: Span,
|
||||
) -> Host<S> {
|
||||
) -> Host<U> {
|
||||
Host {
|
||||
inner: f(self.inner.clone(), self.ip4, self.ip6),
|
||||
ip4: self.ip4,
|
||||
@@ -143,78 +120,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(private_bounds)]
|
||||
impl<T> Host<T>
|
||||
where
|
||||
T: PollTransmit,
|
||||
{
|
||||
pub(crate) fn poll_transmit(&mut self) -> Option<Transmit<'static>> {
|
||||
let _guard = self.span.enter();
|
||||
let transmit = self.span.in_scope(|| self.inner.poll_transmit())?;
|
||||
|
||||
self.set_transmit_src(transmit)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(private_bounds)]
|
||||
impl<T> Host<T>
|
||||
where
|
||||
T: Encapsulate,
|
||||
{
|
||||
pub(crate) fn encapsulate(
|
||||
&mut self,
|
||||
packet: MutableIpPacket<'_>,
|
||||
now: Instant,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let _guard = self.span.enter();
|
||||
|
||||
let transmit = self
|
||||
.span
|
||||
.in_scope(|| self.inner.encapsulate(packet, now))?
|
||||
.into_owned();
|
||||
|
||||
self.set_transmit_src(transmit)
|
||||
}
|
||||
}
|
||||
|
||||
trait Encapsulate {
|
||||
fn encapsulate(&mut self, packet: MutableIpPacket<'_>, now: Instant) -> Option<Transmit<'_>>;
|
||||
}
|
||||
|
||||
impl<TId> Encapsulate for SimNode<TId, ClientState> {
|
||||
fn encapsulate(&mut self, packet: MutableIpPacket<'_>, now: Instant) -> Option<Transmit<'_>> {
|
||||
self.state.encapsulate(packet, now)
|
||||
}
|
||||
}
|
||||
|
||||
impl<TId> Encapsulate for SimNode<TId, GatewayState> {
|
||||
fn encapsulate(&mut self, packet: MutableIpPacket<'_>, now: Instant) -> Option<Transmit<'_>> {
|
||||
self.state.encapsulate(packet, now)
|
||||
}
|
||||
}
|
||||
|
||||
trait PollTransmit {
|
||||
fn poll_transmit(&mut self) -> Option<Transmit<'static>>;
|
||||
}
|
||||
|
||||
impl<TId> PollTransmit for SimNode<TId, ClientState> {
|
||||
fn poll_transmit(&mut self) -> Option<Transmit<'static>> {
|
||||
self.state.poll_transmit()
|
||||
}
|
||||
}
|
||||
|
||||
impl<TId> PollTransmit for SimNode<TId, GatewayState> {
|
||||
fn poll_transmit(&mut self) -> Option<Transmit<'static>> {
|
||||
self.state.poll_transmit()
|
||||
}
|
||||
}
|
||||
|
||||
impl PollTransmit for SimRelay<firezone_relay::Server<StdRng>> {
|
||||
fn poll_transmit(&mut self) -> Option<Transmit<'static>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RoutingTable {
|
||||
routes: IpNetworkTable<HostId>,
|
||||
@@ -323,3 +228,62 @@ impl From<ClientId> for HostId {
|
||||
Self::Client(v)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn host<T>(
|
||||
socket_ips: impl Strategy<Value = IpStack>,
|
||||
default_port: impl Strategy<Value = u16>,
|
||||
state: impl Strategy<Value = T>,
|
||||
) -> impl Strategy<Value = Host<T>>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
{
|
||||
(state, socket_ips, default_port).prop_map(move |(state, ip_stack, port)| {
|
||||
let mut host = Host::new(state);
|
||||
host.update_interface(ip_stack.as_v4().copied(), ip_stack.as_v6().copied(), port);
|
||||
|
||||
host
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn any_port() -> impl Strategy<Value = u16> {
|
||||
any::<NonZeroU16>().prop_map(|v| v.into())
|
||||
}
|
||||
|
||||
pub(crate) fn any_ip_stack() -> impl Strategy<Value = IpStack> {
|
||||
prop_oneof![
|
||||
host_ip4s().prop_map(IpStack::Ip4),
|
||||
host_ip6s().prop_map(IpStack::Ip6),
|
||||
dual_ip_stack()
|
||||
]
|
||||
}
|
||||
|
||||
pub(crate) fn dual_ip_stack() -> impl Strategy<Value = IpStack> {
|
||||
(host_ip4s(), host_ip6s()).prop_map(|(ip4, ip6)| IpStack::Dual { ip4, ip6 })
|
||||
}
|
||||
|
||||
/// A [`Strategy`] of [`Ipv4Addr`]s used for routing packets between hosts within our test.
|
||||
///
|
||||
/// This uses the `TEST-NET-3` (`203.0.113.0/24`) address space reserved for documentation and examples in [RFC5737](https://datatracker.ietf.org/doc/html/rfc5737).
|
||||
pub(crate) fn host_ip4s() -> impl Strategy<Value = Ipv4Addr> {
|
||||
let ips = Ipv4Network::new(Ipv4Addr::new(203, 0, 113, 0), 24)
|
||||
.unwrap()
|
||||
.hosts()
|
||||
.take(100)
|
||||
.collect_vec();
|
||||
|
||||
sample::select(ips)
|
||||
}
|
||||
|
||||
/// A [`Strategy`] of [`Ipv6Addr`]s used for routing packets between hosts within our test.
|
||||
///
|
||||
/// This uses the `2001:DB8::/32` address space reserved for documentation and examples in [RFC3849](https://datatracker.ietf.org/doc/html/rfc3849).
|
||||
pub(crate) fn host_ip6s() -> impl Strategy<Value = Ipv6Addr> {
|
||||
let ips = Ipv6Network::new(Ipv6Addr::new(0x2001, 0xDB80, 0, 0, 0, 0, 0, 0), 32)
|
||||
.unwrap()
|
||||
.subnets_with_prefix(128)
|
||||
.map(|n| n.network_address())
|
||||
.take(100)
|
||||
.collect_vec();
|
||||
|
||||
sample::select(ips)
|
||||
}
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
use super::{
|
||||
sim_net::Host,
|
||||
sim_relay::SimRelay,
|
||||
strategies::{host_ip4s, host_ip6s},
|
||||
};
|
||||
use crate::{ClientState, GatewayState};
|
||||
use connlib_shared::{
|
||||
messages::{ClientId, DnsServer, GatewayId, Interface, RelayId},
|
||||
proptest::domain_name,
|
||||
StaticSecret,
|
||||
};
|
||||
use firezone_relay::IpStack;
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use proptest::{collection, prelude::*};
|
||||
use rand::rngs::StdRng;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct SimNode<ID, S> {
|
||||
pub(crate) id: ID,
|
||||
pub(crate) state: S,
|
||||
|
||||
pub(crate) tunnel_ip4: Ipv4Addr,
|
||||
pub(crate) tunnel_ip6: Ipv6Addr,
|
||||
}
|
||||
|
||||
impl<ID, S> SimNode<ID, S> {
|
||||
pub(crate) fn new(id: ID, state: S, tunnel_ip4: Ipv4Addr, tunnel_ip6: Ipv6Addr) -> Self {
|
||||
Self {
|
||||
id,
|
||||
state,
|
||||
tunnel_ip4,
|
||||
tunnel_ip6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ID, S> SimNode<ID, S>
|
||||
where
|
||||
ID: Copy,
|
||||
S: Clone,
|
||||
{
|
||||
pub(crate) fn map<T>(&self, f: impl FnOnce(S) -> T) -> SimNode<ID, T> {
|
||||
SimNode {
|
||||
id: self.id,
|
||||
state: f(self.state.clone()),
|
||||
tunnel_ip4: self.tunnel_ip4,
|
||||
tunnel_ip6: self.tunnel_ip6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SimNode<ClientId, ClientState> {
|
||||
pub(crate) fn init_relays<'a>(
|
||||
&mut self,
|
||||
relays: impl Iterator<
|
||||
Item = (
|
||||
&'a RelayId,
|
||||
&'a Host<SimRelay<firezone_relay::Server<StdRng>>>,
|
||||
),
|
||||
>,
|
||||
now: Instant,
|
||||
) {
|
||||
self.state.update_relays(
|
||||
HashSet::default(),
|
||||
HashSet::from_iter(relays.map(|(id, r)| {
|
||||
let (socket, username, password, realm) = r.inner().explode("client");
|
||||
|
||||
(*id, socket, username, password, realm)
|
||||
})),
|
||||
now,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn update_upstream_dns(&mut self, upstream_dns_resolvers: Vec<DnsServer>) {
|
||||
let _ = self.state.update_interface_config(Interface {
|
||||
ipv4: self.tunnel_ip4,
|
||||
ipv6: self.tunnel_ip6,
|
||||
upstream_dns: upstream_dns_resolvers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl SimNode<GatewayId, GatewayState> {
|
||||
pub(crate) fn init_relays<'a>(
|
||||
&mut self,
|
||||
relays: impl Iterator<
|
||||
Item = (
|
||||
&'a RelayId,
|
||||
&'a Host<SimRelay<firezone_relay::Server<StdRng>>>,
|
||||
),
|
||||
>,
|
||||
now: Instant,
|
||||
) {
|
||||
self.state.update_relays(
|
||||
HashSet::default(),
|
||||
HashSet::from_iter(relays.map(|(id, r)| {
|
||||
let (socket, username, password, realm) = r.inner().explode("gateway");
|
||||
|
||||
(*id, socket, username, password, realm)
|
||||
})),
|
||||
now,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<ID, S> SimNode<ID, S> {
|
||||
pub(crate) fn tunnel_ip(&self, dst: impl Into<IpAddr>) -> IpAddr {
|
||||
match dst.into() {
|
||||
IpAddr::V4(_) => IpAddr::from(self.tunnel_ip4),
|
||||
IpAddr::V6(_) => IpAddr::from(self.tunnel_ip6),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_tunnel_ip(&self, ip: IpAddr) -> bool {
|
||||
self.tunnel_ip(ip) == ip
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) struct PrivateKey([u8; 32]);
|
||||
|
||||
impl From<PrivateKey> for StaticSecret {
|
||||
fn from(key: PrivateKey) -> Self {
|
||||
StaticSecret::from(key.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for PrivateKey {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("PrivateKey")
|
||||
.field(&hex::encode(self.0))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sim_node_prototype<ID, S>(
|
||||
id: impl Strategy<Value = ID>,
|
||||
state: impl Strategy<Value = S>,
|
||||
tunnel_ip4s: &mut impl Iterator<Item = Ipv4Addr>,
|
||||
tunnel_ip6s: &mut impl Iterator<Item = Ipv6Addr>,
|
||||
) -> impl Strategy<Value = Host<SimNode<ID, S>>>
|
||||
where
|
||||
ID: fmt::Debug,
|
||||
S: fmt::Debug,
|
||||
{
|
||||
let socket_ips = prop_oneof![
|
||||
host_ip4s().prop_map(IpStack::Ip4),
|
||||
host_ip6s().prop_map(IpStack::Ip6),
|
||||
(host_ip4s(), host_ip6s()).prop_map(|(ip4, ip6)| IpStack::Dual { ip4, ip6 })
|
||||
];
|
||||
|
||||
let tunnel_ip4 = tunnel_ip4s.next().unwrap();
|
||||
let tunnel_ip6 = tunnel_ip6s.next().unwrap();
|
||||
|
||||
(
|
||||
id,
|
||||
state,
|
||||
socket_ips,
|
||||
any::<u16>().prop_filter("port must not be 0", |p| *p != 0),
|
||||
)
|
||||
.prop_map(move |(id, state, ip_stack, port)| {
|
||||
let mut host = Host::new(SimNode::new(id, state, tunnel_ip4, tunnel_ip6));
|
||||
host.update_interface(ip_stack.as_v4().copied(), ip_stack.as_v6().copied(), port);
|
||||
|
||||
host
|
||||
})
|
||||
}
|
||||
|
||||
/// An [`Iterator`] over the possible IPv4 addresses of a tunnel interface.
|
||||
///
|
||||
/// We use the CG-NAT range for IPv4.
|
||||
/// See <https://github.com/firezone/firezone/blob/81dfa90f38299595e14ce9e022d1ee919909f124/elixir/apps/domain/lib/domain/network.ex#L7>.
|
||||
pub(crate) fn tunnel_ip4s() -> impl Iterator<Item = Ipv4Addr> {
|
||||
Ipv4Network::new(Ipv4Addr::new(100, 64, 0, 0), 11)
|
||||
.unwrap()
|
||||
.hosts()
|
||||
}
|
||||
|
||||
/// An [`Iterator`] over the possible IPv6 addresses of a tunnel interface.
|
||||
///
|
||||
/// See <https://github.com/firezone/firezone/blob/81dfa90f38299595e14ce9e022d1ee919909f124/elixir/apps/domain/lib/domain/network.ex#L8>.
|
||||
pub(crate) fn tunnel_ip6s() -> impl Iterator<Item = Ipv6Addr> {
|
||||
Ipv6Network::new(Ipv6Addr::new(0xfd00, 0x2021, 0x1111, 0, 0, 0, 0, 0), 107)
|
||||
.unwrap()
|
||||
.subnets_with_prefix(128)
|
||||
.map(|n| n.network_address())
|
||||
}
|
||||
|
||||
fn private_key() -> impl Strategy<Value = PrivateKey> {
|
||||
any::<[u8; 32]>().prop_map(PrivateKey)
|
||||
}
|
||||
|
||||
pub(crate) fn gateway_state() -> impl Strategy<Value = PrivateKey> {
|
||||
private_key()
|
||||
}
|
||||
|
||||
pub(crate) fn client_state() -> impl Strategy<Value = (PrivateKey, HashMap<String, Vec<IpAddr>>)> {
|
||||
(private_key(), known_hosts())
|
||||
}
|
||||
|
||||
pub(crate) fn known_hosts() -> impl Strategy<Value = HashMap<String, Vec<IpAddr>>> {
|
||||
collection::hash_map(
|
||||
domain_name(2..4).prop_map(|d| d.parse().unwrap()),
|
||||
collection::vec(any::<IpAddr>(), 1..6),
|
||||
0..15,
|
||||
)
|
||||
}
|
||||
@@ -1,64 +1,54 @@
|
||||
use super::sim_net::Host;
|
||||
use super::strategies::{host_ip4s, host_ip6s};
|
||||
use super::sim_net::{dual_ip_stack, host, Host};
|
||||
use connlib_shared::messages::RelayId;
|
||||
use firezone_relay::{AddressFamily, AllocationPort, ClientSocket, IpStack, PeerSocket};
|
||||
use proptest::prelude::*;
|
||||
use rand::rngs::StdRng;
|
||||
use secrecy::SecretString;
|
||||
use snownet::{RelaySocket, Transmit};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashSet,
|
||||
net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
|
||||
time::{Duration, Instant, SystemTime},
|
||||
};
|
||||
|
||||
#[derive(Clone, derivative::Derivative)]
|
||||
#[derivative(Debug)]
|
||||
pub(crate) struct SimRelay<S> {
|
||||
pub(crate) state: S,
|
||||
|
||||
ip_stack: firezone_relay::IpStack,
|
||||
pub(crate) struct SimRelay {
|
||||
pub(crate) sut: firezone_relay::Server<StdRng>,
|
||||
pub(crate) allocations: HashSet<(AddressFamily, AllocationPort)>,
|
||||
|
||||
#[derivative(Debug = "ignore")]
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<S> SimRelay<S> {
|
||||
pub(crate) fn new(state: S, ip_stack: firezone_relay::IpStack) -> Self {
|
||||
pub(crate) fn map_explode<'a>(
|
||||
relays: impl Iterator<Item = (&'a RelayId, &'a Host<SimRelay>)> + 'a,
|
||||
username: &'static str,
|
||||
) -> impl Iterator<Item = (RelayId, RelaySocket, String, String, String)> + 'a {
|
||||
relays.map(move |(id, r)| {
|
||||
let (socket, username, password, realm) = r.inner().explode(
|
||||
username,
|
||||
r.inner().sut.auth_secret(),
|
||||
r.inner().sut.public_address(),
|
||||
);
|
||||
|
||||
(*id, socket, username, password, realm)
|
||||
})
|
||||
}
|
||||
|
||||
impl SimRelay {
|
||||
pub(crate) fn new(sut: firezone_relay::Server<StdRng>) -> Self {
|
||||
Self {
|
||||
state,
|
||||
ip_stack,
|
||||
sut,
|
||||
allocations: Default::default(),
|
||||
buffer: vec![0u8; (1 << 16) - 1],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ip4(&self) -> Option<IpAddr> {
|
||||
self.ip_stack.as_v4().copied().map(|i| i.into())
|
||||
}
|
||||
|
||||
pub(crate) fn ip6(&self) -> Option<IpAddr> {
|
||||
self.ip_stack.as_v6().copied().map(|i| i.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> SimRelay<S>
|
||||
where
|
||||
S: Copy,
|
||||
{
|
||||
pub(crate) fn map<T>(&self, f: impl FnOnce(S) -> T) -> SimRelay<T> {
|
||||
SimRelay {
|
||||
state: f(self.state),
|
||||
allocations: self.allocations.clone(),
|
||||
buffer: self.buffer.clone(),
|
||||
ip_stack: self.ip_stack,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
pub(crate) fn explode(&self, username: &str) -> (RelaySocket, String, String, String) {
|
||||
let relay_socket = match self.ip_stack {
|
||||
fn explode(
|
||||
&self,
|
||||
username: &str,
|
||||
auth_secret: &SecretString,
|
||||
public_address: IpStack,
|
||||
) -> (RelaySocket, String, String, String) {
|
||||
let relay_socket = match public_address {
|
||||
firezone_relay::IpStack::Ip4(ip4) => RelaySocket::V4(SocketAddrV4::new(ip4, 3478)),
|
||||
firezone_relay::IpStack::Ip6(ip6) => {
|
||||
RelaySocket::V6(SocketAddrV6::new(ip6, 3478, 0, 0))
|
||||
@@ -69,15 +59,19 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
},
|
||||
};
|
||||
|
||||
let (username, password) = self.make_credentials(username);
|
||||
let (username, password) = self.make_credentials(username, auth_secret);
|
||||
|
||||
(relay_socket, username, password, "firezone".to_owned())
|
||||
}
|
||||
|
||||
fn matching_listen_socket(&self, other: SocketAddr) -> Option<SocketAddr> {
|
||||
fn matching_listen_socket(
|
||||
&self,
|
||||
other: SocketAddr,
|
||||
public_address: IpStack,
|
||||
) -> Option<SocketAddr> {
|
||||
match other {
|
||||
SocketAddr::V4(_) => Some(SocketAddr::new((*self.ip_stack.as_v4()?).into(), 3478)),
|
||||
SocketAddr::V6(_) => Some(SocketAddr::new((*self.ip_stack.as_v6()?).into(), 3478)),
|
||||
SocketAddr::V4(_) => Some(SocketAddr::new((*public_address.as_v4()?).into(), 3478)),
|
||||
SocketAddr::V6(_) => Some(SocketAddr::new((*public_address.as_v6()?).into(), 3478)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +82,10 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
dst: SocketAddr,
|
||||
now: Instant,
|
||||
) -> Option<Transmit<'static>> {
|
||||
if self.matching_listen_socket(dst).is_some_and(|s| s == dst) {
|
||||
if self
|
||||
.matching_listen_socket(dst, self.sut.public_address())
|
||||
.is_some_and(|s| s == dst)
|
||||
{
|
||||
return self.handle_client_input(payload, ClientSocket::new(sender), now);
|
||||
}
|
||||
|
||||
@@ -105,7 +102,7 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
client: ClientSocket,
|
||||
now: Instant,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let (port, peer) = self.state.handle_client_input(payload, client, now)?;
|
||||
let (port, peer) = self.sut.handle_client_input(payload, client, now)?;
|
||||
|
||||
let payload = &payload[4..];
|
||||
|
||||
@@ -120,7 +117,9 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
"IPv4 allocation to be present if we want to send to an IPv4 socket"
|
||||
);
|
||||
|
||||
self.ip4().expect("listen on IPv4 if we have an allocation")
|
||||
self.sut
|
||||
.public_ip4()
|
||||
.expect("listen on IPv4 if we have an allocation")
|
||||
}
|
||||
SocketAddr::V6(_) => {
|
||||
assert!(
|
||||
@@ -128,7 +127,9 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
"IPv6 allocation to be present if we want to send to an IPv6 socket"
|
||||
);
|
||||
|
||||
self.ip6().expect("listen on IPv6 if we have an allocation")
|
||||
self.sut
|
||||
.public_ip6()
|
||||
.expect("listen on IPv6 if we have an allocation")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -148,7 +149,7 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
peer: PeerSocket,
|
||||
port: AllocationPort,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let (client, channel) = self.state.handle_peer_traffic(payload, peer, port)?;
|
||||
let (client, channel) = self.sut.handle_peer_traffic(payload, peer, port)?;
|
||||
|
||||
let full_length = firezone_relay::ChannelData::encode_header_to_slice(
|
||||
channel,
|
||||
@@ -158,7 +159,9 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
self.buffer[4..full_length].copy_from_slice(payload);
|
||||
|
||||
let receiving_socket = client.into_socket();
|
||||
let sending_socket = self.matching_listen_socket(receiving_socket).unwrap();
|
||||
let sending_socket = self
|
||||
.matching_listen_socket(receiving_socket, self.sut.public_address())
|
||||
.unwrap();
|
||||
|
||||
Some(Transmit {
|
||||
src: Some(sending_socket),
|
||||
@@ -167,7 +170,7 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
})
|
||||
}
|
||||
|
||||
fn make_credentials(&self, username: &str) -> (String, String) {
|
||||
fn make_credentials(&self, username: &str, auth_secret: &SecretString) -> (String, String) {
|
||||
let expiry = SystemTime::now() + Duration::from_secs(60);
|
||||
|
||||
let secs = expiry
|
||||
@@ -175,21 +178,16 @@ impl SimRelay<firezone_relay::Server<StdRng>> {
|
||||
.expect("expiry must be later than UNIX_EPOCH")
|
||||
.as_secs();
|
||||
|
||||
let password =
|
||||
firezone_relay::auth::generate_password(self.state.auth_secret(), expiry, username);
|
||||
let password = firezone_relay::auth::generate_password(auth_secret, expiry, username);
|
||||
|
||||
(format!("{secs}:{username}"), password)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sim_relay_prototype() -> impl Strategy<Value = Host<SimRelay<u64>>> {
|
||||
// For this test, our relays always run in dual-stack mode to ensure connectivity!
|
||||
let socket_ips = (host_ip4s(), host_ip6s()).prop_map(|(ip4, ip6)| IpStack::Dual { ip4, ip6 });
|
||||
|
||||
(any::<u64>(), socket_ips).prop_map(move |(seed, ip_stack)| {
|
||||
let mut host = Host::new(SimRelay::new(seed, ip_stack));
|
||||
host.update_interface(ip_stack.as_v4().copied(), ip_stack.as_v6().copied(), 3478);
|
||||
|
||||
host
|
||||
})
|
||||
pub(crate) fn relay_prototype() -> impl Strategy<Value = Host<u64>> {
|
||||
host(
|
||||
dual_ip_stack(), // For this test, our relays always run in dual-stack mode to ensure connectivity!
|
||||
Just(3478),
|
||||
any::<u64>(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use connlib_shared::{messages::DnsServer, proptest::domain_name, DomainName};
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use itertools::Itertools as _;
|
||||
use proptest::{collection, prelude::*, sample};
|
||||
use proptest::{collection, prelude::*};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
@@ -51,33 +50,6 @@ pub(crate) fn upstream_dns_servers() -> impl Strategy<Value = Vec<DnsServer>> {
|
||||
]
|
||||
}
|
||||
|
||||
/// A [`Strategy`] of [`Ipv4Addr`]s used for routing packets between hosts within our test.
|
||||
///
|
||||
/// This uses the `TEST-NET-3` (`203.0.113.0/24`) address space reserved for documentation and examples in [RFC5737](https://datatracker.ietf.org/doc/html/rfc5737).
|
||||
pub(crate) fn host_ip4s() -> impl Strategy<Value = Ipv4Addr> {
|
||||
let ips = Ipv4Network::new(Ipv4Addr::new(203, 0, 113, 0), 24)
|
||||
.unwrap()
|
||||
.hosts()
|
||||
.take(100)
|
||||
.collect_vec();
|
||||
|
||||
sample::select(ips)
|
||||
}
|
||||
|
||||
/// A [`Strategy`] of [`Ipv6Addr`]s used for routing packets between hosts within our test.
|
||||
///
|
||||
/// This uses the `2001:DB8::/32` address space reserved for documentation and examples in [RFC3849](https://datatracker.ietf.org/doc/html/rfc3849).
|
||||
pub(crate) fn host_ip6s() -> impl Strategy<Value = Ipv6Addr> {
|
||||
let ips = Ipv6Network::new(Ipv6Addr::new(0x2001, 0xDB80, 0, 0, 0, 0, 0, 0), 32)
|
||||
.unwrap()
|
||||
.subnets_with_prefix(128)
|
||||
.map(|n| n.network_address())
|
||||
.take(100)
|
||||
.collect_vec();
|
||||
|
||||
sample::select(ips)
|
||||
}
|
||||
|
||||
pub(crate) fn system_dns_servers() -> impl Strategy<Value = Vec<IpAddr>> {
|
||||
collection::vec(any::<IpAddr>(), 1..4) // Always need at least 1 system DNS server. TODO: Should we test what happens if we don't?
|
||||
}
|
||||
@@ -103,3 +75,23 @@ pub(crate) fn packet_source_v6(client: Ipv6Addr) -> impl Strategy<Value = Ipv6Ad
|
||||
1 => any::<Ipv6Addr>()
|
||||
]
|
||||
}
|
||||
|
||||
/// An [`Iterator`] over the possible IPv4 addresses of a tunnel interface.
|
||||
///
|
||||
/// We use the CG-NAT range for IPv4.
|
||||
/// See <https://github.com/firezone/firezone/blob/81dfa90f38299595e14ce9e022d1ee919909f124/elixir/apps/domain/lib/domain/network.ex#L7>.
|
||||
pub(crate) fn tunnel_ip4s() -> impl Iterator<Item = Ipv4Addr> {
|
||||
Ipv4Network::new(Ipv4Addr::new(100, 64, 0, 0), 11)
|
||||
.unwrap()
|
||||
.hosts()
|
||||
}
|
||||
|
||||
/// An [`Iterator`] over the possible IPv6 addresses of a tunnel interface.
|
||||
///
|
||||
/// See <https://github.com/firezone/firezone/blob/81dfa90f38299595e14ce9e022d1ee919909f124/elixir/apps/domain/lib/domain/network.ex#L8>.
|
||||
pub(crate) fn tunnel_ip6s() -> impl Iterator<Item = Ipv6Addr> {
|
||||
Ipv6Network::new(Ipv6Addr::new(0xfd00, 0x2021, 0x1111, 0, 0, 0, 0, 0), 107)
|
||||
.unwrap()
|
||||
.subnets_with_prefix(128)
|
||||
.map(|n| n.network_address())
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use super::reference::ReferenceState;
|
||||
use super::sim_client::SimClient;
|
||||
use super::sim_gateway::SimGateway;
|
||||
use super::sim_net::{Host, HostId, RoutingTable};
|
||||
use super::sim_node::SimNode;
|
||||
use super::sim_portal::SimPortal;
|
||||
use super::sim_relay::SimRelay;
|
||||
use super::QueryId;
|
||||
use crate::tests::assertions::*;
|
||||
use crate::tests::sim_relay::map_explode;
|
||||
use crate::tests::transition::Transition;
|
||||
use crate::{dns::DnsQuery, ClientEvent, ClientState, GatewayEvent, GatewayState, Request};
|
||||
use bimap::BiMap;
|
||||
use crate::{dns::DnsQuery, ClientEvent, GatewayEvent, Request};
|
||||
use chrono::{DateTime, Utc};
|
||||
use connlib_shared::messages::RelayId;
|
||||
use connlib_shared::messages::{Interface, RelayId};
|
||||
use connlib_shared::{
|
||||
messages::{
|
||||
client::{ResourceDescription, ResourceDescriptionCidr, ResourceDescriptionDns},
|
||||
@@ -19,21 +19,19 @@ use connlib_shared::{
|
||||
};
|
||||
use firezone_relay::IpStack;
|
||||
use hickory_proto::{
|
||||
op::{MessageType, Query},
|
||||
rr::{rdata, RData, Record, RecordType},
|
||||
serialize::binary::BinDecodable as _,
|
||||
op::Query,
|
||||
rr::{RData, Record, RecordType},
|
||||
};
|
||||
use hickory_resolver::lookup::Lookup;
|
||||
use ip_network_table::IpNetworkTable;
|
||||
use ip_packet::{IpPacket, MutableIpPacket, Packet as _};
|
||||
use proptest_state_machine::{ReferenceStateMachine, StateMachineTest};
|
||||
use rand::{rngs::StdRng, SeedableRng as _};
|
||||
use rand::SeedableRng as _;
|
||||
use secrecy::ExposeSecret as _;
|
||||
use snownet::Transmit;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeMap;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
net::{IpAddr, SocketAddr},
|
||||
net::IpAddr,
|
||||
str::FromStr as _,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -49,26 +47,11 @@ pub(crate) struct TunnelTest {
|
||||
now: Instant,
|
||||
utc_now: DateTime<Utc>,
|
||||
|
||||
client: Host<SimNode<ClientId, ClientState>>,
|
||||
gateway: Host<SimNode<GatewayId, GatewayState>>,
|
||||
relays: HashMap<RelayId, Host<SimRelay<firezone_relay::Server<StdRng>>>>,
|
||||
pub(crate) client: Host<SimClient>,
|
||||
pub(crate) gateway: Host<SimGateway>,
|
||||
relays: HashMap<RelayId, Host<SimRelay>>,
|
||||
portal: SimPortal,
|
||||
|
||||
/// The DNS records created on the client as a result of received DNS responses.
|
||||
///
|
||||
/// This contains results from both, queries to DNS resources and non-resources.
|
||||
pub(crate) client_dns_records: HashMap<DomainName, Vec<IpAddr>>,
|
||||
|
||||
/// Bi-directional mapping between connlib's sentinel DNS IPs and the effective DNS servers.
|
||||
client_dns_by_sentinel: BiMap<IpAddr, SocketAddr>,
|
||||
|
||||
pub(crate) client_sent_dns_queries: HashMap<QueryId, IpPacket<'static>>,
|
||||
pub(crate) client_received_dns_responses: HashMap<QueryId, IpPacket<'static>>,
|
||||
|
||||
pub(crate) client_sent_icmp_requests: HashMap<(u16, u16), IpPacket<'static>>,
|
||||
pub(crate) client_received_icmp_replies: HashMap<(u16, u16), IpPacket<'static>>,
|
||||
pub(crate) gateway_received_icmp_requests: VecDeque<IpPacket<'static>>,
|
||||
|
||||
network: RoutingTable,
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -90,12 +73,11 @@ impl StateMachineTest for TunnelTest {
|
||||
.set_default();
|
||||
|
||||
// Construct client, gateway and relay from the initial state.
|
||||
let mut client = ref_state.client.map(
|
||||
|sim_node, _, _| sim_node.map(|(k, h)| ClientState::new(k, h)),
|
||||
debug_span!("client"),
|
||||
);
|
||||
let mut client = ref_state
|
||||
.client
|
||||
.map(|ref_client, _, _| ref_client.init(), debug_span!("client"));
|
||||
let mut gateway = ref_state.gateway.map(
|
||||
|sim_node, _, _| sim_node.map(GatewayState::new),
|
||||
|ref_gateway, _, _| ref_gateway.init(),
|
||||
debug_span!("gateway"),
|
||||
);
|
||||
|
||||
@@ -104,16 +86,14 @@ impl StateMachineTest for TunnelTest {
|
||||
.iter()
|
||||
.map(|(id, relay)| {
|
||||
let relay = relay.map(
|
||||
|relay, ip4, ip6| {
|
||||
relay.map(|seed| {
|
||||
firezone_relay::Server::new(
|
||||
IpStack::from((ip4, ip6)),
|
||||
rand::rngs::StdRng::seed_from_u64(seed),
|
||||
3478,
|
||||
49152,
|
||||
65535,
|
||||
)
|
||||
})
|
||||
|seed, ip4, ip6| {
|
||||
SimRelay::new(firezone_relay::Server::new(
|
||||
IpStack::from((ip4, ip6)),
|
||||
rand::rngs::StdRng::seed_from_u64(seed),
|
||||
3478,
|
||||
49152,
|
||||
65535,
|
||||
))
|
||||
},
|
||||
debug_span!("relay", rid = %id),
|
||||
);
|
||||
@@ -124,13 +104,19 @@ impl StateMachineTest for TunnelTest {
|
||||
let portal = SimPortal::new(client.inner().id, gateway.inner().id);
|
||||
|
||||
// Configure client and gateway with the relays.
|
||||
client.exec_mut(|c| c.init_relays(relays.iter(), ref_state.now));
|
||||
gateway.exec_mut(|g| g.init_relays(relays.iter(), ref_state.now));
|
||||
|
||||
client.exec_mut(|c| c.update_upstream_dns(ref_state.upstream_dns_resolvers.clone()));
|
||||
client.exec_mut(|c| {
|
||||
c.state
|
||||
.update_system_resolvers(ref_state.system_dns_resolvers.clone())
|
||||
c.sut.update_relays(
|
||||
HashSet::default(),
|
||||
HashSet::from_iter(map_explode(relays.iter(), "client")),
|
||||
ref_state.now,
|
||||
)
|
||||
});
|
||||
gateway.exec_mut(|g| {
|
||||
g.sut.update_relays(
|
||||
HashSet::default(),
|
||||
HashSet::from_iter(map_explode(relays.iter(), "gateway")),
|
||||
ref_state.now,
|
||||
)
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
@@ -142,16 +128,9 @@ impl StateMachineTest for TunnelTest {
|
||||
portal,
|
||||
logger,
|
||||
relays,
|
||||
client_dns_records: Default::default(),
|
||||
client_dns_by_sentinel: Default::default(),
|
||||
client_sent_icmp_requests: Default::default(),
|
||||
client_received_icmp_replies: Default::default(),
|
||||
gateway_received_icmp_requests: Default::default(),
|
||||
client_received_dns_responses: Default::default(),
|
||||
client_sent_dns_queries: Default::default(),
|
||||
};
|
||||
|
||||
let mut buffered_transmits = VecDeque::new();
|
||||
let mut buffered_transmits = BufferedTransmits::default();
|
||||
this.advance(ref_state, &mut buffered_transmits); // Perform initial setup before we apply the first transition.
|
||||
|
||||
debug_assert!(buffered_transmits.is_empty());
|
||||
@@ -165,20 +144,20 @@ impl StateMachineTest for TunnelTest {
|
||||
ref_state: &<Self::Reference as ReferenceStateMachine>::State,
|
||||
transition: <Self::Reference as ReferenceStateMachine>::Transition,
|
||||
) -> Self::SystemUnderTest {
|
||||
let mut buffered_transmits = VecDeque::new();
|
||||
let mut buffered_transmits = BufferedTransmits::default();
|
||||
|
||||
// Act: Apply the transition
|
||||
match transition {
|
||||
Transition::AddCidrResource(r) => {
|
||||
state
|
||||
.client
|
||||
.exec_mut(|c| c.state.add_resources(&[ResourceDescription::Cidr(r)]));
|
||||
.exec_mut(|c| c.sut.add_resources(&[ResourceDescription::Cidr(r)]));
|
||||
}
|
||||
Transition::AddDnsResource { resource, .. } => state
|
||||
.client
|
||||
.exec_mut(|c| c.state.add_resources(&[ResourceDescription::Dns(resource)])),
|
||||
.exec_mut(|c| c.sut.add_resources(&[ResourceDescription::Dns(resource)])),
|
||||
Transition::RemoveResource(id) => {
|
||||
state.client.exec_mut(|c| c.state.remove_resources(&[id]))
|
||||
state.client.exec_mut(|c| c.sut.remove_resources(&[id]))
|
||||
}
|
||||
Transition::SendICMPPacketToNonResourceIp {
|
||||
src,
|
||||
@@ -194,7 +173,11 @@ impl StateMachineTest for TunnelTest {
|
||||
} => {
|
||||
let packet = ip_packet::make::icmp_request_packet(src, dst, seq, identifier);
|
||||
|
||||
buffered_transmits.extend(state.send_ip_packet_client_to_gateway(packet));
|
||||
let transmit = state
|
||||
.client
|
||||
.exec_mut(|sim| sim.encapsulate(packet, state.now));
|
||||
|
||||
buffered_transmits.push(transmit, &state.client);
|
||||
}
|
||||
Transition::SendICMPPacketToDnsResource {
|
||||
src,
|
||||
@@ -203,21 +186,26 @@ impl StateMachineTest for TunnelTest {
|
||||
identifier,
|
||||
resolved_ip,
|
||||
} => {
|
||||
let available_ips =
|
||||
state
|
||||
.client_dns_records
|
||||
.get(&dst)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|ip| match ip {
|
||||
IpAddr::V4(_) => src.is_ipv4(),
|
||||
IpAddr::V6(_) => src.is_ipv6(),
|
||||
});
|
||||
let available_ips = state
|
||||
.client
|
||||
.inner()
|
||||
.dns_records
|
||||
.get(&dst)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|ip| match ip {
|
||||
IpAddr::V4(_) => src.is_ipv4(),
|
||||
IpAddr::V6(_) => src.is_ipv6(),
|
||||
});
|
||||
let dst = *resolved_ip.select(available_ips);
|
||||
|
||||
let packet = ip_packet::make::icmp_request_packet(src, dst, seq, identifier);
|
||||
|
||||
buffered_transmits.extend(state.send_ip_packet_client_to_gateway(packet));
|
||||
let transmit = state
|
||||
.client
|
||||
.exec_mut(|sim| Some(sim.encapsulate(packet, state.now)?.into_owned()));
|
||||
|
||||
buffered_transmits.push(transmit, &state.client);
|
||||
}
|
||||
Transition::SendDnsQuery {
|
||||
domain,
|
||||
@@ -225,9 +213,11 @@ impl StateMachineTest for TunnelTest {
|
||||
query_id,
|
||||
dns_server,
|
||||
} => {
|
||||
let transmit = state.send_dns_query_for(domain, r_type, query_id, dns_server);
|
||||
let transmit = state.client.exec_mut(|sim| {
|
||||
sim.send_dns_query_for(domain, r_type, query_id, dns_server, state.now)
|
||||
});
|
||||
|
||||
buffered_transmits.extend(transmit)
|
||||
buffered_transmits.push(transmit, &state.client);
|
||||
}
|
||||
Transition::Tick { millis } => {
|
||||
state.now += Duration::from_millis(millis);
|
||||
@@ -235,10 +225,16 @@ impl StateMachineTest for TunnelTest {
|
||||
Transition::UpdateSystemDnsServers { servers } => {
|
||||
state
|
||||
.client
|
||||
.exec_mut(|c| c.state.update_system_resolvers(servers));
|
||||
.exec_mut(|c| c.sut.update_system_resolvers(servers));
|
||||
}
|
||||
Transition::UpdateUpstreamDnsServers { servers } => {
|
||||
state.client.exec_mut(|c| c.update_upstream_dns(servers));
|
||||
state.client.exec_mut(|c| {
|
||||
c.sut.update_interface_config(Interface {
|
||||
ipv4: c.sut.tunnel_ip4().unwrap(),
|
||||
ipv6: c.sut.tunnel_ip6().unwrap(),
|
||||
upstream_dns: servers,
|
||||
})
|
||||
});
|
||||
}
|
||||
Transition::RoamClient { ip4, ip6, port } => {
|
||||
state.network.remove_host(&state.client);
|
||||
@@ -248,10 +244,14 @@ impl StateMachineTest for TunnelTest {
|
||||
.add_host(state.client.inner().id, &state.client));
|
||||
|
||||
state.client.exec_mut(|c| {
|
||||
c.state.reset();
|
||||
c.sut.reset();
|
||||
|
||||
// In prod, we reconnect to the portal and receive a new `init` message.
|
||||
c.init_relays(state.relays.iter(), ref_state.now);
|
||||
c.sut.update_relays(
|
||||
HashSet::default(),
|
||||
HashSet::from_iter(map_explode(state.relays.iter(), "client")),
|
||||
ref_state.now,
|
||||
)
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -266,13 +266,22 @@ impl StateMachineTest for TunnelTest {
|
||||
state: &Self::SystemUnderTest,
|
||||
ref_state: &<Self::Reference as ReferenceStateMachine>::State,
|
||||
) {
|
||||
let ref_client = ref_state.client.inner();
|
||||
let sim_client = state.client.inner();
|
||||
let sim_gateway = state.gateway.inner();
|
||||
|
||||
// Assert our properties: Check that our actual state is equivalent to our expectation (the reference state).
|
||||
assert_icmp_packets_properties(state, ref_state);
|
||||
assert_dns_packets_properties(state, ref_state);
|
||||
assert_known_hosts_are_valid(state, ref_state);
|
||||
assert_icmp_packets_properties(
|
||||
ref_client,
|
||||
sim_client,
|
||||
sim_gateway,
|
||||
&ref_state.global_dns_records,
|
||||
);
|
||||
assert_dns_packets_properties(ref_client, sim_client);
|
||||
assert_known_hosts_are_valid(ref_client, sim_client);
|
||||
assert_eq!(
|
||||
state.effective_dns_servers(),
|
||||
ref_state.expected_dns_servers(),
|
||||
sim_client.effective_dns_servers(),
|
||||
ref_client.expected_dns_servers(),
|
||||
"Effective DNS servers should match either system or upstream DNS"
|
||||
);
|
||||
}
|
||||
@@ -286,48 +295,42 @@ impl TunnelTest {
|
||||
/// Dispatching a [`Transmit`] (read: packet) to a host can trigger more packets, i.e. receiving a STUN request may trigger a STUN response.
|
||||
///
|
||||
/// Consequently, this function needs to loop until no host can make progress at which point we consider the [`Transition`] complete.
|
||||
fn advance(
|
||||
&mut self,
|
||||
ref_state: &ReferenceState,
|
||||
buffered_transmits: &mut VecDeque<Transmit<'static>>,
|
||||
) {
|
||||
fn advance(&mut self, ref_state: &ReferenceState, buffered_transmits: &mut BufferedTransmits) {
|
||||
loop {
|
||||
if let Some(transmit) = buffered_transmits.pop_front() {
|
||||
if let Some(transmit) = buffered_transmits.pop() {
|
||||
self.dispatch_transmit(transmit, buffered_transmits, &ref_state.global_dns_records);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(transmit) = self.client.poll_transmit() {
|
||||
buffered_transmits.push_back(transmit);
|
||||
if let Some(transmit) = self.client.exec_mut(|sim| sim.sut.poll_transmit()) {
|
||||
buffered_transmits.push(transmit, &self.client);
|
||||
continue;
|
||||
}
|
||||
if let Some(event) = self.client.exec_mut(|c| c.state.poll_event()) {
|
||||
if let Some(event) = self.client.exec_mut(|c| c.sut.poll_event()) {
|
||||
self.on_client_event(
|
||||
self.client.inner().id,
|
||||
event,
|
||||
&ref_state.client_cidr_resources,
|
||||
&ref_state.client_dns_resources,
|
||||
&ref_state.client.inner().cidr_resources,
|
||||
&ref_state.client.inner().dns_resources,
|
||||
&ref_state.global_dns_records,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if let Some(query) = self
|
||||
.client
|
||||
.exec_mut(|client| client.state.poll_dns_queries())
|
||||
{
|
||||
if let Some(query) = self.client.exec_mut(|client| client.sut.poll_dns_queries()) {
|
||||
self.on_forwarded_dns_query(query, ref_state);
|
||||
continue;
|
||||
}
|
||||
if let Some(packet) = self.client.exec_mut(|client| client.state.poll_packets()) {
|
||||
self.on_client_received_packet(packet);
|
||||
continue;
|
||||
}
|
||||
self.client.exec_mut(|sim| {
|
||||
while let Some(packet) = sim.sut.poll_packets() {
|
||||
sim.on_received_packet(packet)
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(transmit) = self.gateway.poll_transmit() {
|
||||
buffered_transmits.push_back(transmit);
|
||||
if let Some(transmit) = self.gateway.exec_mut(|g| g.sut.poll_transmit()) {
|
||||
buffered_transmits.push(transmit, &self.gateway);
|
||||
continue;
|
||||
}
|
||||
if let Some(event) = self.gateway.exec_mut(|gateway| gateway.state.poll_event()) {
|
||||
if let Some(event) = self.gateway.exec_mut(|g| g.sut.poll_event()) {
|
||||
self.on_gateway_event(self.gateway.inner().id, event);
|
||||
continue;
|
||||
}
|
||||
@@ -335,7 +338,7 @@ impl TunnelTest {
|
||||
let mut any_relay_advanced = false;
|
||||
|
||||
for (_, relay) in self.relays.iter_mut() {
|
||||
let Some(message) = relay.exec_mut(|relay| relay.state.next_command()) else {
|
||||
let Some(message) = relay.exec_mut(|r| r.sut.next_command()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -348,11 +351,14 @@ impl TunnelTest {
|
||||
.sending_socket_for(dst.ip())
|
||||
.expect("relay to never emit packets without a matching socket");
|
||||
|
||||
buffered_transmits.push_back(Transmit {
|
||||
src: Some(src),
|
||||
dst,
|
||||
payload: payload.into(),
|
||||
});
|
||||
buffered_transmits.push(
|
||||
Transmit {
|
||||
src: Some(src),
|
||||
dst,
|
||||
payload: payload.into(),
|
||||
},
|
||||
relay,
|
||||
);
|
||||
}
|
||||
|
||||
firezone_relay::Command::CreateAllocation { port, family } => {
|
||||
@@ -378,14 +384,6 @@ impl TunnelTest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the _effective_ DNS servers that connlib is using.
|
||||
fn effective_dns_servers(&self) -> BTreeSet<SocketAddr> {
|
||||
self.client_dns_by_sentinel
|
||||
.right_values()
|
||||
.copied()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Forwards time to the given instant iff the corresponding host would like that (i.e. returns a timestamp <= from `poll_timeout`).
|
||||
///
|
||||
/// Tying the forwarding of time to the result of `poll_timeout` gives us better coverage because in production, we suspend until the value of `poll_timeout`.
|
||||
@@ -394,81 +392,39 @@ impl TunnelTest {
|
||||
|
||||
if self
|
||||
.client
|
||||
.exec_mut(|client| client.state.poll_timeout())
|
||||
.exec_mut(|c| c.sut.poll_timeout())
|
||||
.is_some_and(|t| t <= now)
|
||||
{
|
||||
any_advanced = true;
|
||||
|
||||
self.client
|
||||
.exec_mut(|client| client.state.handle_timeout(now));
|
||||
self.client.exec_mut(|c| c.sut.handle_timeout(now));
|
||||
};
|
||||
|
||||
if self
|
||||
.gateway
|
||||
.exec_mut(|gateway| gateway.state.poll_timeout())
|
||||
.exec_mut(|g| g.sut.poll_timeout())
|
||||
.is_some_and(|t| t <= now)
|
||||
{
|
||||
any_advanced = true;
|
||||
|
||||
self.gateway
|
||||
.exec_mut(|gateway| gateway.state.handle_timeout(now, utc_now))
|
||||
.exec_mut(|g| g.sut.handle_timeout(now, utc_now))
|
||||
};
|
||||
|
||||
for (_, relay) in self.relays.iter_mut() {
|
||||
if relay
|
||||
.exec_mut(|relay| relay.state.poll_timeout())
|
||||
.exec_mut(|r| r.sut.poll_timeout())
|
||||
.is_some_and(|t| t <= now)
|
||||
{
|
||||
any_advanced = true;
|
||||
|
||||
relay.exec_mut(|relay| relay.state.handle_timeout(now))
|
||||
relay.exec_mut(|r| r.sut.handle_timeout(now))
|
||||
};
|
||||
}
|
||||
|
||||
any_advanced
|
||||
}
|
||||
|
||||
fn send_ip_packet_client_to_gateway(
|
||||
&mut self,
|
||||
packet: MutableIpPacket<'_>,
|
||||
) -> Option<Transmit<'static>> {
|
||||
{
|
||||
let packet = packet.to_owned().into_immutable();
|
||||
|
||||
if let Some(icmp) = packet.as_icmp() {
|
||||
let echo_request = icmp.as_echo_request().expect("to be echo request");
|
||||
|
||||
self.client_sent_icmp_requests
|
||||
.insert((echo_request.sequence(), echo_request.identifier()), packet);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let packet = packet.to_owned().into_immutable();
|
||||
|
||||
if let Some(udp) = packet.as_udp() {
|
||||
if let Ok(message) = hickory_proto::op::Message::from_bytes(udp.payload()) {
|
||||
debug_assert_eq!(
|
||||
message.message_type(),
|
||||
MessageType::Query,
|
||||
"every DNS message sent from the client should be a DNS query"
|
||||
);
|
||||
|
||||
self.client_sent_dns_queries.insert(message.id(), packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.client.encapsulate(packet, self.now)
|
||||
}
|
||||
|
||||
fn send_ip_packet_gateway_to_client(
|
||||
&mut self,
|
||||
packet: MutableIpPacket<'_>,
|
||||
) -> Option<Transmit<'static>> {
|
||||
self.gateway.encapsulate(packet, self.now)
|
||||
}
|
||||
|
||||
/// Dispatches a [`Transmit`] to the correct host.
|
||||
///
|
||||
/// This function is basically the "network layer" of our tests.
|
||||
@@ -478,7 +434,7 @@ impl TunnelTest {
|
||||
fn dispatch_transmit(
|
||||
&mut self,
|
||||
transmit: Transmit,
|
||||
buffered_transmits: &mut VecDeque<Transmit<'static>>,
|
||||
buffered_transmits: &mut BufferedTransmits,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
) {
|
||||
let src = transmit
|
||||
@@ -491,28 +447,20 @@ impl TunnelTest {
|
||||
panic!("Unhandled packet: {src} -> {dst}")
|
||||
};
|
||||
|
||||
let mut buf = [0u8; 1000];
|
||||
|
||||
match host {
|
||||
HostId::Client(_) => {
|
||||
let Some(packet) = self
|
||||
.client
|
||||
.exec_mut(|c| c.state.decapsulate(dst, src, payload, self.now, &mut buf))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.on_client_received_packet(packet);
|
||||
self.client
|
||||
.exec_mut(|c| c.handle_packet(payload, src, dst, self.now));
|
||||
}
|
||||
HostId::Gateway(_) => {
|
||||
let Some(packet) = self
|
||||
let Some(transmit) = self
|
||||
.gateway
|
||||
.exec_mut(|g| g.state.decapsulate(dst, src, payload, self.now, &mut buf))
|
||||
.exec_mut(|g| g.handle_packet(global_dns_records, payload, src, dst, self.now))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.on_gateway_received_packet(packet, global_dns_records, buffered_transmits);
|
||||
buffered_transmits.push(transmit, &self.gateway);
|
||||
}
|
||||
HostId::Relay(id) => {
|
||||
let relay = self.relays.get_mut(&id).expect("unknown relay");
|
||||
@@ -523,7 +471,7 @@ impl TunnelTest {
|
||||
return;
|
||||
};
|
||||
|
||||
buffered_transmits.push_back(transmit);
|
||||
buffered_transmits.push(transmit, relay);
|
||||
}
|
||||
HostId::Stale => {
|
||||
tracing::debug!(%dst, "Dropping packet because host roamed away or is offline");
|
||||
@@ -540,20 +488,16 @@ impl TunnelTest {
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
) {
|
||||
match event {
|
||||
ClientEvent::AddedIceCandidates { candidates, .. } => {
|
||||
self.gateway.exec_mut(|gateway| {
|
||||
for candidate in candidates {
|
||||
gateway.state.add_ice_candidate(src, candidate, self.now)
|
||||
}
|
||||
})
|
||||
}
|
||||
ClientEvent::RemovedIceCandidates { candidates, .. } => {
|
||||
self.gateway.exec_mut(|gateway| {
|
||||
for candidate in candidates {
|
||||
gateway.state.remove_ice_candidate(src, candidate)
|
||||
}
|
||||
})
|
||||
}
|
||||
ClientEvent::AddedIceCandidates { candidates, .. } => self.gateway.exec_mut(|g| {
|
||||
for candidate in candidates {
|
||||
g.sut.add_ice_candidate(src, candidate, self.now)
|
||||
}
|
||||
}),
|
||||
ClientEvent::RemovedIceCandidates { candidates, .. } => self.gateway.exec_mut(|g| {
|
||||
for candidate in candidates {
|
||||
g.sut.remove_ice_candidate(src, candidate)
|
||||
}
|
||||
}),
|
||||
ClientEvent::ConnectionIntent {
|
||||
resource,
|
||||
connected_gateway_ids,
|
||||
@@ -567,7 +511,7 @@ impl TunnelTest {
|
||||
|
||||
let request = self
|
||||
.client
|
||||
.exec_mut(|c| c.state.create_or_reuse_connection(resource, gateway, site))
|
||||
.exec_mut(|c| c.sut.create_or_reuse_connection(resource, gateway, site))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
@@ -592,9 +536,9 @@ impl TunnelTest {
|
||||
Request::NewConnection(new_connection) => {
|
||||
let answer = self
|
||||
.gateway
|
||||
.exec_mut(|gateway| {
|
||||
gateway.state.accept(
|
||||
self.client.exec_mut(|c| c.id),
|
||||
.exec_mut(|g| {
|
||||
g.sut.accept(
|
||||
self.client.inner().id,
|
||||
snownet::Offer {
|
||||
session_key: new_connection
|
||||
.client_preshared_key
|
||||
@@ -612,9 +556,9 @@ impl TunnelTest {
|
||||
.password,
|
||||
},
|
||||
},
|
||||
self.client.exec_mut(|c| c.state.public_key()),
|
||||
self.client.exec_mut(|c| c.tunnel_ip4),
|
||||
self.client.exec_mut(|c| c.tunnel_ip6),
|
||||
self.client.inner().sut.public_key(),
|
||||
self.client.inner().sut.tunnel_ip4().unwrap(),
|
||||
self.client.inner().sut.tunnel_ip6().unwrap(),
|
||||
new_connection
|
||||
.client_payload
|
||||
.domain
|
||||
@@ -627,8 +571,8 @@ impl TunnelTest {
|
||||
.unwrap();
|
||||
|
||||
self.client
|
||||
.exec_mut(|client| {
|
||||
client.state.accept_answer(
|
||||
.exec_mut(|c| {
|
||||
c.sut.accept_answer(
|
||||
snownet::Answer {
|
||||
credentials: snownet::Credentials {
|
||||
username: answer.username,
|
||||
@@ -636,7 +580,7 @@ impl TunnelTest {
|
||||
},
|
||||
},
|
||||
resource_id,
|
||||
self.gateway.exec_mut(|g| g.state.public_key()),
|
||||
self.gateway.inner().sut.public_key(),
|
||||
self.now,
|
||||
)
|
||||
})
|
||||
@@ -644,10 +588,10 @@ impl TunnelTest {
|
||||
}
|
||||
Request::ReuseConnection(reuse_connection) => {
|
||||
self.gateway
|
||||
.exec_mut(|gateway| {
|
||||
gateway.state.allow_access(
|
||||
.exec_mut(|g| {
|
||||
g.sut.allow_access(
|
||||
resource,
|
||||
self.client.exec_mut(|c| c.id),
|
||||
self.client.inner().id,
|
||||
None,
|
||||
reuse_connection.payload.map(|r| (r.name, r.proxy_ips)),
|
||||
self.now,
|
||||
@@ -677,10 +621,10 @@ impl TunnelTest {
|
||||
);
|
||||
|
||||
self.gateway
|
||||
.exec_mut(|gateway| {
|
||||
gateway.state.allow_access(
|
||||
.exec_mut(|g| {
|
||||
g.sut.allow_access(
|
||||
resource,
|
||||
self.client.exec_mut(|c| c.id),
|
||||
self.client.inner().id,
|
||||
None,
|
||||
reuse_connection.payload.map(|r| (r.name, r.proxy_ips)),
|
||||
self.now,
|
||||
@@ -693,142 +637,28 @@ impl TunnelTest {
|
||||
tracing::warn!("Unimplemented");
|
||||
}
|
||||
ClientEvent::DnsServersChanged { dns_by_sentinel } => {
|
||||
self.client_dns_by_sentinel = dns_by_sentinel;
|
||||
self.client
|
||||
.exec_mut(|c| c.dns_by_sentinel = dns_by_sentinel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_gateway_event(&mut self, src: GatewayId, event: GatewayEvent) {
|
||||
match event {
|
||||
GatewayEvent::AddedIceCandidates { candidates, .. } => self.client.exec_mut(|client| {
|
||||
GatewayEvent::AddedIceCandidates { candidates, .. } => self.client.exec_mut(|c| {
|
||||
for candidate in candidates {
|
||||
client.state.add_ice_candidate(src, candidate, self.now)
|
||||
c.sut.add_ice_candidate(src, candidate, self.now)
|
||||
}
|
||||
}),
|
||||
GatewayEvent::RemovedIceCandidates { candidates, .. } => self.client.exec_mut(|c| {
|
||||
for candidate in candidates {
|
||||
c.sut.remove_ice_candidate(src, candidate)
|
||||
}
|
||||
}),
|
||||
GatewayEvent::RemovedIceCandidates { candidates, .. } => {
|
||||
self.client.exec_mut(|client| {
|
||||
for candidate in candidates {
|
||||
client.state.remove_ice_candidate(src, candidate)
|
||||
}
|
||||
})
|
||||
}
|
||||
GatewayEvent::RefreshDns { .. } => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process an IP packet received on the client.
|
||||
fn on_client_received_packet(&mut self, packet: IpPacket<'_>) {
|
||||
if let Some(icmp) = packet.as_icmp() {
|
||||
let echo_reply = icmp.as_echo_reply().expect("to be echo reply");
|
||||
|
||||
self.client_received_icmp_replies.insert(
|
||||
(echo_reply.sequence(), echo_reply.identifier()),
|
||||
packet.to_owned(),
|
||||
);
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(udp) = packet.as_udp() {
|
||||
if udp.get_source() == 53 {
|
||||
let mut message = hickory_proto::op::Message::from_bytes(udp.payload())
|
||||
.expect("ip packets on port 53 to be DNS packets");
|
||||
|
||||
self.client_received_dns_responses
|
||||
.insert(message.id(), packet.to_owned());
|
||||
|
||||
for record in message.take_answers().into_iter() {
|
||||
let domain = hickory_name_to_domain(record.name().clone());
|
||||
|
||||
let ip = match record.data() {
|
||||
Some(RData::A(rdata::A(ip4))) => IpAddr::from(*ip4),
|
||||
Some(RData::AAAA(rdata::AAAA(ip6))) => IpAddr::from(*ip6),
|
||||
unhandled => {
|
||||
panic!("Unexpected record data: {unhandled:?}")
|
||||
}
|
||||
};
|
||||
|
||||
self.client_dns_records.entry(domain).or_default().push(ip);
|
||||
}
|
||||
|
||||
// Ensure all IPs are always sorted.
|
||||
for ips in self.client_dns_records.values_mut() {
|
||||
ips.sort()
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unimplemented!("Unhandled packet")
|
||||
}
|
||||
|
||||
/// Process an IP packet received on the gateway.
|
||||
fn on_gateway_received_packet(
|
||||
&mut self,
|
||||
packet: IpPacket<'_>,
|
||||
global_dns_records: &BTreeMap<DomainName, HashSet<IpAddr>>,
|
||||
buffered_transmits: &mut VecDeque<Transmit<'static>>,
|
||||
) {
|
||||
let packet = packet.to_owned();
|
||||
|
||||
if packet.as_icmp().is_some() {
|
||||
self.gateway_received_icmp_requests
|
||||
.push_back(packet.clone());
|
||||
|
||||
let echo_response = ip_packet::make::icmp_response_packet(packet);
|
||||
let maybe_transmit = self.send_ip_packet_gateway_to_client(echo_response);
|
||||
|
||||
buffered_transmits.extend(maybe_transmit);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if packet.as_udp().is_some() {
|
||||
let response = ip_packet::make::dns_ok_response(packet, |name| {
|
||||
global_dns_records
|
||||
.get(&hickory_name_to_domain(name.clone()))
|
||||
.cloned()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
});
|
||||
|
||||
let maybe_transmit = self.send_ip_packet_gateway_to_client(response);
|
||||
buffered_transmits.extend(maybe_transmit);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
panic!("Unhandled packet")
|
||||
}
|
||||
|
||||
fn send_dns_query_for(
|
||||
&mut self,
|
||||
domain: DomainName,
|
||||
r_type: RecordType,
|
||||
query_id: u16,
|
||||
dns_server: SocketAddr,
|
||||
) -> Option<Transmit<'static>> {
|
||||
let dns_server = *self
|
||||
.client_dns_by_sentinel
|
||||
.get_by_right(&dns_server)
|
||||
.expect("to have a sentinel DNS server for the sampled one");
|
||||
|
||||
let name = domain_to_hickory_name(domain);
|
||||
|
||||
let src = self.client.exec_mut(|c| c.tunnel_ip(dns_server));
|
||||
|
||||
let packet = ip_packet::make::dns_query(
|
||||
name,
|
||||
r_type,
|
||||
SocketAddr::new(src, 9999), // An application would pick a random source port that is free.
|
||||
SocketAddr::new(dns_server, 53),
|
||||
query_id,
|
||||
);
|
||||
|
||||
self.send_ip_packet_client_to_gateway(packet)
|
||||
}
|
||||
|
||||
// TODO: Should we vary the following things via proptests?
|
||||
// - Forwarded DNS query timing out?
|
||||
// - hickory error?
|
||||
@@ -854,7 +684,7 @@ impl TunnelTest {
|
||||
.collect::<Arc<_>>();
|
||||
|
||||
self.client.exec_mut(|c| {
|
||||
c.state.on_dns_result(
|
||||
c.sut.on_dns_result(
|
||||
query,
|
||||
Ok(Ok(Ok(Lookup::new_with_max_ttl(
|
||||
Query::query(name, requested_type),
|
||||
@@ -896,7 +726,7 @@ fn map_client_resource_to_gateway_resource(
|
||||
.expect("resource to be a known CIDR or DNS resource")
|
||||
}
|
||||
|
||||
fn hickory_name_to_domain(mut name: hickory_proto::rr::Name) -> DomainName {
|
||||
pub(crate) fn hickory_name_to_domain(mut name: hickory_proto::rr::Name) -> DomainName {
|
||||
name.set_fqdn(false); // Hack to work around hickory always parsing as FQ
|
||||
let name = name.to_string();
|
||||
|
||||
@@ -906,7 +736,7 @@ fn hickory_name_to_domain(mut name: hickory_proto::rr::Name) -> DomainName {
|
||||
domain
|
||||
}
|
||||
|
||||
fn domain_to_hickory_name(domain: DomainName) -> hickory_proto::rr::Name {
|
||||
pub(crate) fn domain_to_hickory_name(domain: DomainName) -> hickory_proto::rr::Name {
|
||||
let domain = domain.to_string();
|
||||
|
||||
let name = hickory_proto::rr::Name::from_str(&domain).unwrap();
|
||||
@@ -914,3 +744,44 @@ fn domain_to_hickory_name(domain: DomainName) -> hickory_proto::rr::Name {
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct BufferedTransmits {
|
||||
inner: VecDeque<Transmit<'static>>,
|
||||
}
|
||||
|
||||
impl BufferedTransmits {
|
||||
fn push<T>(&mut self, transmit: impl Into<Option<Transmit<'static>>>, sending_host: &Host<T>) {
|
||||
let Some(transmit) = transmit.into() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if transmit.src.is_some() {
|
||||
self.inner.push_back(transmit);
|
||||
return;
|
||||
}
|
||||
|
||||
// The `src` of a [`Transmit`] is empty if we want to send if via the default interface.
|
||||
// In production, the kernel does this for us.
|
||||
// In this test, we need to always set a `src` so that the remote peer knows where the packet is coming from.
|
||||
|
||||
let Some(src) = sending_host.sending_socket_for(transmit.dst.ip()) else {
|
||||
tracing::debug!(dst = %transmit.dst, "No socket");
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
self.inner.push_back(Transmit {
|
||||
src: Some(src),
|
||||
..transmit
|
||||
});
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<Transmit<'static>> {
|
||||
self.inner.pop_front()
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::strategies::*;
|
||||
use super::{
|
||||
sim_net::{any_ip_stack, any_port},
|
||||
strategies::*,
|
||||
};
|
||||
use connlib_shared::{
|
||||
messages::{
|
||||
client::{ResourceDescriptionCidr, ResourceDescriptionDns},
|
||||
@@ -7,7 +10,6 @@ use connlib_shared::{
|
||||
proptest::*,
|
||||
DomainName,
|
||||
};
|
||||
use firezone_relay::IpStack;
|
||||
use hickory_proto::rr::RecordType;
|
||||
use proptest::{prelude::*, sample};
|
||||
use std::{
|
||||
@@ -216,19 +218,9 @@ pub(crate) fn question_mark_wildcard_dns_resource() -> impl Strategy<Value = Tra
|
||||
}
|
||||
|
||||
pub(crate) fn roam_client() -> impl Strategy<Value = Transition> {
|
||||
let ip_stack = prop_oneof![
|
||||
host_ip4s().prop_map(IpStack::Ip4),
|
||||
host_ip6s().prop_map(IpStack::Ip6),
|
||||
(host_ip4s(), host_ip6s()).prop_map(|(ip4, ip6)| IpStack::Dual { ip4, ip6 })
|
||||
];
|
||||
|
||||
(
|
||||
ip_stack,
|
||||
any::<u16>().prop_filter("port must not be 0", |p| *p != 0),
|
||||
)
|
||||
.prop_map(move |(ip_stack, port)| Transition::RoamClient {
|
||||
ip4: ip_stack.as_v4().copied(),
|
||||
ip6: ip_stack.as_v6().copied(),
|
||||
port,
|
||||
})
|
||||
(any_ip_stack(), any_port()).prop_map(move |(ip_stack, port)| Transition::RoamClient {
|
||||
ip4: ip_stack.as_v4().copied(),
|
||||
ip6: ip_stack.as_v6().copied(),
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -205,6 +205,18 @@ where
|
||||
&self.auth_secret
|
||||
}
|
||||
|
||||
pub fn public_address(&self) -> IpStack {
|
||||
self.public_address
|
||||
}
|
||||
|
||||
pub fn public_ip4(&self) -> Option<IpAddr> {
|
||||
Some(IpAddr::V4(*self.public_address.as_v4()?))
|
||||
}
|
||||
|
||||
pub fn public_ip6(&self) -> Option<IpAddr> {
|
||||
Some(IpAddr::V6(*self.public_address.as_v6()?))
|
||||
}
|
||||
|
||||
pub fn listen_port(&self) -> u16 {
|
||||
self.listen_port
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user