diff --git a/.github/codespellrc b/.github/codespellrc index 703770d4a..e1deffe39 100644 --- a/.github/codespellrc +++ b/.github/codespellrc @@ -1,3 +1,3 @@ [codespell] -skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast +skip = ./**/*.svg,./elixir/deps,./**/*.min.js,./kotlin/android/app/build,./kotlin/android/build,./e2e/pnpm-lock.yaml,./website/.next,./website/pnpm-lock.yaml,./rust/target,Cargo.lock,./website/docs/reference/api/*.mdx,./**/erl_crash.dump,./cover,./vendor,*.json,seeds.exs,./**/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./_build,*.cast,./**/proptest-regressions ignore-words-list = optin,crate,keypair,keypairs,iif,statics,wee,anull,commitish,inout,fo,superceded diff --git a/rust/connlib/shared/src/messages.rs b/rust/connlib/shared/src/messages.rs index 646ac5811..2eab152a8 100644 --- a/rust/connlib/shared/src/messages.rs +++ b/rust/connlib/shared/src/messages.rs @@ -15,7 +15,7 @@ pub use key::{Key, SecretKey}; use crate::DomainName; -#[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] +#[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct GatewayId(Uuid); #[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -57,7 +57,7 @@ impl GatewayId { } } -#[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq)] +#[derive(Hash, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct ClientId(Uuid); impl FromStr for ClientId { diff --git a/rust/connlib/tunnel/proptest-regressions/tests.txt b/rust/connlib/tunnel/proptest-regressions/tests.txt index bc7d90c34..603d509ab 100644 --- a/rust/connlib/tunnel/proptest-regressions/tests.txt +++ b/rust/connlib/tunnel/proptest-regressions/tests.txt @@ -44,3 +44,6 @@ cc 83703ff8437242d56e8abec8d4ffcaf32d85b7153b419879125c85cc09cfc265 # shrinks to cc 4c15aa7f37217bce3154f4f6ca1a1579b461eb937c388d64831c66f663882f0c cc 262342e77a34c4a08fe867d854781f151f2c7231f90ea56f25b62ed9335863b6 # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 17305, tv_nsec: 737348607 }, utc_now: 2024-06-26T21:32:34.480938981Z, client: SimNode { id: ClientId(00000000-0000-0000-0000-000000000000), state: (PrivateKey("0000000000000000000000000000000000000000000000000000000000000000"), {"aaa.aaa": [::ffff:127.0.0.1]}), ip4_socket: None, ip6_socket: Some([::ffff:0.0.0.0]:1), tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, old_sockets: [] }, gateway: SimNode { id: GatewayId(00000000-0000-0000-0000-000000000000), state: PrivateKey("0000000000000000000000000000000000000000000000000000000000000001"), ip4_socket: Some(84.138.146.254:56110), ip6_socket: None, tunnel_ip4: 100.75.120.215, tunnel_ip6: fd00:2021:1111::1:dd0b, old_sockets: [] }, relay: SimRelay { id: RelayId(63bc6618-8999-2fbc-a204-e257844ee4d4), ip_stack: Dual { ip4: 81.119.217.210, ip6: 91cb:1a8e:3f71:5de:d4a2:dd49:5626:de01 }, allocations: {} }, system_dns_resolvers: [::ffff:103.18.46.46, ::ffff:247.21.53.3], upstream_dns_resolvers: [], client_cidr_resources: {}, client_dns_resources: {}, client_dns_records: {}, client_connected_cidr_resources: {}, client_connected_dns_resources: {}, global_dns_records: {Name(tqh.ben.fty.): {::ffff:5.237.62.230, 127.0.0.1, 376c:675e:e691:82bc:5872:4819:2562:dc99, 205.232.164.63}, Name(kkol.ndyw.): {195.254.153.223, 234.79.60.66, 148.51.73.22}, Name(otg.owzr.pacxaz.): {::ffff:219.98.127.214, ::ffff:5.180.115.58, 0.0.0.0, 200.137.176.253, 511:6fe2:67e5:4c8e:fb19:e368:a906:5c5a}, Name(odo.xpwgi.): {127.0.0.1, fc56:12b5:c100:bdf5:1b92:eda3:18ea:9b9d, 7.11.48.238}, Name(mejfpp.zshfg.): {127.0.0.1, ::ffff:0.0.0.0}}, client_known_host_records: {"aaa.aaa": [::ffff:127.0.0.1]}, expected_icmp_handshakes: [], expected_dns_handshakes: [] }, [AddCidrResource(ResourceDescriptionCidr { id: ResourceId(00000000-0000-0000-0000-000000000000), address: V6(Ipv6Network { network_address: ::ffff:127.0.0.1, netmask: 128 }), name: "aaaa", address_description: Some("qzrvjlnjc"), sites: [Site { name: "glinrb", id: SiteId(4a345393-f36b-8477-e4e6-40c68ef2cb23) }, Site { name: "tveygh", id: SiteId(89c85f6f-5450-9e41-b83b-c749507c7fc1) }] }), SendICMPPacketToCidrResource { src: fd00:2021:1111::2:d44f, dst: ::ffff:127.0.0.1, seq: 0, identifier: 0 }], None) cc 7f664f0f323f6ee66ce3646f2dcee3060cd44a0ba6cc5dac93b6e05b506a8b77 # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 60244, tv_nsec: 797631694 }, utc_now: 2024-07-02T00:18:43.920582885Z, client: SimNode { id: ClientId(00000000-0000-0000-0000-000000000000), state: (PrivateKey("0000000000000000000000000000000000000000000000000000000000000000"), {}), ip4_socket: Some(127.0.0.1:1), ip6_socket: None, tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, old_sockets: [] }, gateway: SimNode { id: GatewayId(00000000-0000-0000-0000-000000000000), state: PrivateKey("0000000000000000000000000000000000000000000000000000000000000001"), ip4_socket: None, ip6_socket: Some([::ffff:0.0.0.0]:61794), tunnel_ip4: 100.89.192.223, tunnel_ip6: fd00:2021:1111::8:ed1b, old_sockets: [] }, relay: SimRelay { id: RelayId(30f17601-66de-fd6c-930b-06a01f14a7e8), ip_stack: Dual { ip4: 70.81.191.107, ip6: ::ffff:16.45.127.194 }, allocations: {} }, system_dns_resolvers: [::ffff:196.171.111.168, ::ffff:190.161.152.104, ::ffff:0.0.0.0], upstream_dns_resolvers: [IpPort(IpDnsServer { address: 47.254.17.116:53 }), IpPort(IpDnsServer { address: 0.0.0.0:53 }), IpPort(IpDnsServer { address: [::ffff:30.69.55.56]:53 }), IpPort(IpDnsServer { address: [::ffff:146.129.237.12]:53 })], client_cidr_resources: {}, client_dns_resources: {}, client_dns_records: {}, client_connected_cidr_resources: {}, client_connected_dns_resources: {}, global_dns_records: {Name(hbsn.nzdjjh.): {127.0.0.1, 102.11.111.15, 83ba:ac7:4e3a:62f9:d50b:1b78:1bd1:c89a, 92.152.125.191}, Name(bmwkee.sqjk.): {65.31.222.66, 44.217.247.108, ::ffff:127.0.0.1, 145.5.128.220, 38.147.74.20}, Name(bco.qyai.tpca.): {11.63.209.112, 127.0.0.1}, Name(ksdxhm.azpqe.ussyxv.): {26.0.98.95, ::ffff:127.0.0.1, ::ffff:104.65.62.14, ::ffff:163.200.249.253, ::ffff:0.0.0.0}, Name(wcxca.vtbie.): {::ffff:127.0.0.1}, Name(cwadr.xtjgoe.): {::ffff:112.224.160.23, bfac:316a:bec8:8eca:7a0c:8ddf:4576:f628}, Name(iwfot.yic.): {af86:ac7:a0ea:83c1:6287:649a:c43c:c4c3, 1.135.213.184, 136.243.79.47}}, client_known_host_records: {}, expected_icmp_handshakes: [], expected_dns_handshakes: [] }, [AddCidrResource(ResourceDescriptionCidr { id: ResourceId(00000000-0000-0000-0000-000000000000), address: V4(Ipv4Network { network_address: 0.0.0.0, netmask: 32 }), name: "aaaa", address_description: Some("ocmwvjnj"), sites: [Site { name: "gnldbxpqk", id: SiteId(31612141-bdbd-b74d-eced-45176e6b9b1e) }, Site { name: "coxr", id: SiteId(ea6e943f-7514-ff2f-79f2-b5a8957d0e62) }] }), SendDnsQuery { domain: Name(hbsn.nzdjjh.), r_type: A, query_id: 0, dns_server: 0.0.0.0:53 }], None) +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) diff --git a/rust/connlib/tunnel/src/tests.rs b/rust/connlib/tunnel/src/tests.rs index 44cd79bfa..4a6d4d228 100644 --- a/rust/connlib/tunnel/src/tests.rs +++ b/rust/connlib/tunnel/src/tests.rs @@ -4,6 +4,7 @@ use proptest::test_runner::Config; mod assertions; mod composite_strategy; mod reference; +mod sim_net; mod sim_node; mod sim_portal; mod sim_relay; diff --git a/rust/connlib/tunnel/src/tests/assertions.rs b/rust/connlib/tunnel/src/tests/assertions.rs index c7eed964e..118685379 100644 --- a/rust/connlib/tunnel/src/tests/assertions.rs +++ b/rust/connlib/tunnel/src/tests/assertions.rs @@ -58,6 +58,7 @@ pub(crate) fn assert_icmp_packets_properties(state: &TunnelTest, ref_state: &Ref gateway_received_request.source(), ref_state .client + .inner() .tunnel_ip(gateway_received_request.source()), "ICMP request on gateway to originate from client" ); diff --git a/rust/connlib/tunnel/src/tests/reference.rs b/rust/connlib/tunnel/src/tests/reference.rs index 4e94280e8..b60ee89eb 100644 --- a/rust/connlib/tunnel/src/tests/reference.rs +++ b/rust/connlib/tunnel/src/tests/reference.rs @@ -1,6 +1,6 @@ use super::{ - composite_strategy::CompositeStrategy, sim_node::*, sim_relay::*, strategies::*, transition::*, - IcmpIdentifier, IcmpSeq, QueryId, + composite_strategy::CompositeStrategy, sim_net::*, sim_node::*, sim_relay::*, strategies::*, + transition::*, IcmpIdentifier, IcmpSeq, QueryId, }; use chrono::{DateTime, Utc}; use connlib_shared::{ @@ -29,9 +29,10 @@ use std::{ pub(crate) struct ReferenceState { pub(crate) now: Instant, pub(crate) utc_now: DateTime, - pub(crate) client: SimNode>)>, - pub(crate) gateway: SimNode, - pub(crate) relay: SimRelay, + #[allow(clippy::type_complexity)] + pub(crate) client: Host>)>>, + pub(crate) gateway: Host>, + pub(crate) relay: Host>, /// The DNS resolvers configured on the client outside of connlib. pub(crate) system_dns_resolvers: Vec, @@ -67,6 +68,8 @@ pub(crate) struct ReferenceState { pub(crate) expected_icmp_handshakes: VecDeque<(ResourceDst, IcmpSeq, IcmpIdentifier)>, /// The expected DNS handshakes. pub(crate) expected_dns_handshakes: VecDeque, + + pub(crate) network: RoutingTable, } #[derive(Debug, Clone)] @@ -85,61 +88,85 @@ impl ReferenceStateMachine for ReferenceState { type Transition = Transition; fn init_state() -> proptest::prelude::BoxedStrategy { + 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, + ); + let relay_prototype = sim_relay_prototype(); + ( - sim_node_prototype(client_id(), client_state()), - sim_node_prototype(gateway_id(), gateway_state()), - sim_relay_prototype(), + client_prototype, + gateway_prototype, + relay_prototype, system_dns_servers(), upstream_dns_servers(), 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( - "client and gateway priv key must be different", - |(c, g, _, _, _, _, _, _)| c.state.0 != g.state, - ) - .prop_filter( - "client, gateway and relay ip must be different", - |(c, g, r, _, _, _, _, _)| { - let c4 = c.ip4_socket.map(|s| *s.ip()); - let g4 = g.ip4_socket.map(|s| *s.ip()); - let r4 = r.ip_stack().as_v4().copied(); + .prop_filter_map( + "network IPs must be unique", + |(c, g, r, system_dns, upstream_dns, global_dns, now, utc_now)| { + let mut routing_table = RoutingTable::default(); - let c6 = c.ip6_socket.map(|s| *s.ip()); - let g6 = g.ip6_socket.map(|s| *s.ip()); - let r6 = r.ip_stack().as_v6().copied(); + if !routing_table.add_host(&c) { + return None; + } + if !routing_table.add_host(&g) { + return None; + }; + if !routing_table.add_host(&r) { + return None; + }; - let c4_eq_g4 = c4.is_some_and(|c| g4.is_some_and(|g| c == g)); - let c6_eq_g6 = c6.is_some_and(|c| g6.is_some_and(|g| c == g)); - let c4_eq_r4 = c4.is_some_and(|c| r4.is_some_and(|r| c == r)); - let c6_eq_r6 = c6.is_some_and(|c| r6.is_some_and(|r| c == r)); - let g4_eq_r4 = g4.is_some_and(|g| r4.is_some_and(|r| g == r)); - let g6_eq_r6 = g6.is_some_and(|g| r6.is_some_and(|r| g == r)); - - !c4_eq_g4 && !c6_eq_g6 && !c4_eq_r4 && !c6_eq_r6 && !g4_eq_r4 && !g6_eq_r6 + Some(( + c, + g, + r, + system_dns, + upstream_dns, + 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, _, _, _)| { + |(c, _, _, system_dns, upstream_dns, _, _, _, _)| { // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! if !upstream_dns.is_empty() { - if c.ip4_socket.is_none() && upstream_dns.iter().all(|s| s.ip().is_ipv4()) { + if c.ip4.is_none() && upstream_dns.iter().all(|s| s.ip().is_ipv4()) { return false; } - if c.ip6_socket.is_none() && upstream_dns.iter().all(|s| s.ip().is_ipv6()) { + if c.ip6.is_none() && upstream_dns.iter().all(|s| s.ip().is_ipv6()) { return false; } return true; } - if c.ip4_socket.is_none() && system_dns.iter().all(|s| s.is_ipv4()) { + if c.ip4.is_none() && system_dns.iter().all(|s| s.is_ipv4()) { return false; } - if c.ip6_socket.is_none() && system_dns.iter().all(|s| s.is_ipv6()) { + if c.ip6.is_none() && system_dns.iter().all(|s| s.is_ipv6()) { return false; } @@ -156,6 +183,7 @@ impl ReferenceStateMachine for ReferenceState { global_dns_records, now, utc_now, + network, )| Self { now, utc_now, @@ -165,7 +193,8 @@ impl ReferenceStateMachine for ReferenceState { system_dns_resolvers, upstream_dns_resolvers, global_dns_records, - client_known_host_records: client.state.1, + 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(), @@ -210,25 +239,25 @@ impl ReferenceStateMachine for ReferenceState { ) .with_if_not_empty(10, state.ipv4_cidr_resource_dsts(), |ip4_resources| { icmp_to_cidr_resource( - packet_source_v4(state.client.tunnel_ip4), + 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.tunnel_ip6), + 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.tunnel_ip4), + 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.tunnel_ip6), + packet_source_v6(state.client.inner().tunnel_ip6), sample::select(dns_v6_domains), ) }) @@ -237,7 +266,7 @@ impl ReferenceStateMachine for ReferenceState { ( state.all_domains(), state.v4_dns_servers(), - state.client.ip4_socket, + state.client.ip4, ), |(domains, v4_dns_servers, _)| { dns_query(sample::select(domains), sample::select(v4_dns_servers)) @@ -248,7 +277,7 @@ impl ReferenceStateMachine for ReferenceState { ( state.all_domains(), state.v6_dns_servers(), - state.client.ip6_socket, + state.client.ip6, ), |(domains, v6_dns_servers, _)| { dns_query(sample::select(domains), sample::select(v6_dns_servers)) @@ -259,7 +288,7 @@ impl ReferenceStateMachine for ReferenceState { state.resolved_ip4_for_non_resources(), |resolved_non_resource_ip4s| { ping_random_ip( - packet_source_v4(state.client.tunnel_ip4), + packet_source_v4(state.client.inner().tunnel_ip4), sample::select(resolved_non_resource_ip4s), ) }, @@ -269,7 +298,7 @@ impl ReferenceStateMachine for ReferenceState { state.resolved_ip6_for_non_resources(), |resolved_non_resource_ip6s| { ping_random_ip( - packet_source_v6(state.client.tunnel_ip6), + packet_source_v6(state.client.inner().tunnel_ip6), sample::select(resolved_non_resource_ip6s), ) }, @@ -373,12 +402,11 @@ impl ReferenceStateMachine for ReferenceState { Transition::UpdateUpstreamDnsServers { servers } => { state.upstream_dns_resolvers.clone_from(servers); } - Transition::RoamClient { - ip4_socket, - ip6_socket, - } => { - state.client.ip4_socket.clone_from(ip4_socket); - state.client.ip6_socket.clone_from(ip6_socket); + Transition::RoamClient { ip4, ip6, .. } => { + state.network.remove_host(&state.client); + state.client.ip4.clone_from(ip4); + state.client.ip6.clone_from(ip6); + debug_assert!(state.network.add_host(&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(); @@ -395,11 +423,11 @@ impl ReferenceStateMachine for ReferenceState { Transition::AddCidrResource(r) => { // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! - if r.address.is_ipv6() && state.gateway.ip6_socket.is_none() { + if r.address.is_ipv6() && state.gateway.ip6.is_none() { return false; } - if r.address.is_ipv4() && state.gateway.ip4_socket.is_none() { + if r.address.is_ipv4() && state.gateway.ip4.is_none() { return false; } @@ -470,10 +498,10 @@ impl ReferenceStateMachine for ReferenceState { Transition::UpdateSystemDnsServers { servers } => { // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! - if state.client.ip4_socket.is_none() && servers.iter().all(|s| s.is_ipv4()) { + if state.client.ip4.is_none() && servers.iter().all(|s| s.is_ipv4()) { return false; } - if state.client.ip6_socket.is_none() && servers.iter().all(|s| s.is_ipv6()) { + if state.client.ip6.is_none() && servers.iter().all(|s| s.is_ipv6()) { return false; } @@ -482,10 +510,10 @@ impl ReferenceStateMachine for ReferenceState { Transition::UpdateUpstreamDnsServers { servers } => { // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! - if state.client.ip4_socket.is_none() && servers.iter().all(|s| s.ip().is_ipv4()) { + if state.client.ip4.is_none() && servers.iter().all(|s| s.ip().is_ipv4()) { return false; } - if state.client.ip6_socket.is_none() && servers.iter().all(|s| s.ip().is_ipv6()) { + if state.client.ip6.is_none() && servers.iter().all(|s| s.ip().is_ipv6()) { return false; } @@ -501,18 +529,14 @@ impl ReferenceStateMachine for ReferenceState { state.client_cidr_resources.iter().any(|(_, r)| &r.id == id) || state.client_dns_resources.contains_key(id) } - Transition::RoamClient { - ip4_socket, - ip6_socket, - } => { + 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. - let is_previous_ip4_socket = ip4_socket - .is_some_and(|s| state.client.old_sockets.contains(&SocketAddr::V4(s))); - let is_previous_ip6_socket = ip6_socket - .is_some_and(|s| state.client.old_sockets.contains(&SocketAddr::V6(s))); + let is_assigned_ip4 = ip4.is_some_and(|ip| state.network.contains(ip)); + let is_assigned_ip6 = ip6.is_some_and(|ip| state.network.contains(ip)); + let is_previous_port = state.client.old_ports.contains(port); - !is_previous_ip4_socket && !is_previous_ip6_socket + !is_assigned_ip4 && !is_assigned_ip6 && !is_previous_port } } } @@ -554,7 +578,7 @@ impl ReferenceState { tracing::Span::current().record("resource", tracing::field::display(resource.id)); if self.client_connected_cidr_resources.contains(&resource.id) - && self.client.is_tunnel_ip(src) + && self.client.inner().is_tunnel_ip(src) { tracing::debug!("Connected to CIDR resource, expecting packet to be routed"); self.expected_icmp_handshakes @@ -581,7 +605,7 @@ impl ReferenceState { if self .client_connected_dns_resources .contains(&(resource, dst.clone())) - && self.client.is_tunnel_ip(src) + && self.client.inner().is_tunnel_ip(src) { tracing::debug!("Connected to DNS resource, expecting packet to be routed"); self.expected_icmp_handshakes diff --git a/rust/connlib/tunnel/src/tests/sim_net.rs b/rust/connlib/tunnel/src/tests/sim_net.rs new file mode 100644 index 000000000..ceccadab8 --- /dev/null +++ b/rust/connlib/tunnel/src/tests/sim_net.rs @@ -0,0 +1,345 @@ +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 ip_network_table::IpNetworkTable; +use ip_packet::MutableIpPacket; +use rand::rngs::StdRng; +use snownet::Transmit; +use std::{ + collections::HashSet, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + time::Instant, +}; +use tracing::Span; + +#[derive(Clone, derivative::Derivative)] +#[derivative(Debug)] +pub(crate) struct Host { + inner: T, + + pub(crate) ip4: Option, + pub(crate) ip6: Option, + + // In production, we always rebind to a new port. + // To mimic this, we track the used ports here to not sample an existing one. + pub(crate) old_ports: HashSet, + + default_port: u16, + allocated_ports: HashSet<(u16, AddressFamily)>, + + #[derivative(Debug = "ignore")] + span: Span, +} + +impl Host { + pub(crate) fn new(inner: T) -> Self { + Self { + inner, + ip4: None, + ip6: None, + span: Span::none(), + default_port: 0, + allocated_ports: HashSet::default(), + old_ports: HashSet::default(), + } + } + + pub(crate) fn inner(&self) -> &T { + &self.inner + } + + /// Mutable access to `T` must go via this function to ensure the corresponding span is active and tracks all state modifications. + pub(crate) fn exec_mut(&mut self, f: impl FnOnce(&mut T) -> R) -> R { + self.span.in_scope(|| f(&mut self.inner)) + } + + pub(crate) fn sending_socket_for(&self, dst: impl Into) -> Option { + let ip = match dst.into() { + IpAddr::V4(_) => self.ip4?.into(), + IpAddr::V6(_) => self.ip6?.into(), + }; + + Some(SocketAddr::new(ip, self.default_port)) + } + + pub(crate) fn allocate_port(&mut self, port: u16, family: AddressFamily) { + self.allocated_ports.insert((port, family)); + } + + pub(crate) fn deallocate_port(&mut self, port: u16, family: AddressFamily) { + self.allocated_ports.remove(&(port, family)); + } + + pub(crate) fn update_interface( + &mut self, + ip4: Option, + ip6: Option, + port: u16, + ) { + // 1. Remember what the current port was. + self.old_ports.insert(self.default_port); + + // 2. Update to the new IPs. + self.ip4 = ip4; + self.ip6 = ip6; + + // 3. Allocate the new port. + self.default_port = port; + + self.deallocate_port(port, AddressFamily::V4); + self.deallocate_port(port, AddressFamily::V6); + + if ip4.is_some() { + self.allocate_port(port, AddressFamily::V4); + } + if ip6.is_some() { + 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> { + 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 Host +where + T: Clone, +{ + pub(crate) fn map( + &self, + f: impl FnOnce(T, Option, Option) -> S, + span: Span, + ) -> Host { + Host { + inner: f(self.inner.clone(), self.ip4, self.ip6), + ip4: self.ip4, + ip6: self.ip6, + span, + default_port: self.default_port, + allocated_ports: self.allocated_ports.clone(), + old_ports: self.old_ports.clone(), + } + } +} + +#[allow(private_bounds)] +impl Host +where + T: PollTransmit, +{ + pub(crate) fn poll_transmit(&mut self) -> Option> { + let _guard = self.span.enter(); + let transmit = self.span.in_scope(|| self.inner.poll_transmit())?; + + self.set_transmit_src(transmit) + } +} + +#[allow(private_bounds)] +impl Host +where + T: Encapsulate, +{ + pub(crate) fn encapsulate( + &mut self, + packet: MutableIpPacket<'_>, + now: Instant, + ) -> Option> { + 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>; +} + +impl Encapsulate for SimNode { + fn encapsulate(&mut self, packet: MutableIpPacket<'_>, now: Instant) -> Option> { + self.state.encapsulate(packet, now) + } +} + +impl Encapsulate for SimNode { + fn encapsulate(&mut self, packet: MutableIpPacket<'_>, now: Instant) -> Option> { + self.state.encapsulate(packet, now) + } +} + +trait PollTransmit { + fn poll_transmit(&mut self) -> Option>; +} + +impl PollTransmit for SimNode { + fn poll_transmit(&mut self) -> Option> { + self.state.poll_transmit() + } +} + +impl PollTransmit for SimNode { + fn poll_transmit(&mut self) -> Option> { + self.state.poll_transmit() + } +} + +impl PollTransmit for SimRelay> { + fn poll_transmit(&mut self) -> Option> { + None + } +} + +#[derive(Debug, Clone)] +pub(crate) struct RoutingTable { + routes: IpNetworkTable, +} + +impl Default for RoutingTable { + fn default() -> Self { + Self { + routes: IpNetworkTable::new(), + } + } +} + +impl RoutingTable { + #[allow(private_bounds)] + pub(crate) fn add_host(&mut self, host: &Host) -> bool + where + T: Id, + { + match (host.ip4, host.ip6) { + (None, None) => panic!("Node must have at least one network IP"), + (None, Some(ip6)) => { + if self.contains(ip6) { + return false; + } + + self.routes.insert(ip6, host.inner.id()); + } + (Some(ip4), None) => { + if self.contains(ip4) { + return false; + } + + self.routes.insert(ip4, host.inner.id()); + } + (Some(ip4), Some(ip6)) => { + if self.contains(ip4) { + return false; + } + if self.contains(ip6) { + return false; + } + + self.routes.insert(ip4, host.inner.id()); + self.routes.insert(ip6, host.inner.id()); + } + } + + true + } + + #[allow(private_bounds)] + pub(crate) fn remove_host(&mut self, host: &Host) { + match (host.ip4, host.ip6) { + (None, None) => panic!("Node must have at least one network IP"), + (None, Some(ip6)) => { + debug_assert!(self.contains(ip6), "Cannot remove a non-existing host"); + + self.routes.insert(ip6, HostId::Stale); + } + (Some(ip4), None) => { + debug_assert!(self.contains(ip4), "Cannot remove a non-existing host"); + + self.routes.insert(ip4, HostId::Stale); + } + (Some(ip4), Some(ip6)) => { + debug_assert!(self.contains(ip4), "Cannot remove a non-existing host"); + debug_assert!(self.contains(ip6), "Cannot remove a non-existing host"); + + self.routes.insert(ip4, HostId::Stale); + self.routes.insert(ip6, HostId::Stale); + } + } + } + + pub(crate) fn contains(&self, ip: impl Into) -> bool { + self.routes.exact_match(ip).is_some() + } + + pub(crate) fn host_by_ip(&self, ip: IpAddr) -> Option { + self.routes.exact_match(ip).copied() + } +} + +trait Id { + fn id(&self) -> HostId; +} + +impl Id for SimNode +where + TId: Into + Copy, +{ + fn id(&self) -> HostId { + self.id.into() + } +} + +impl Id for SimRelay { + fn id(&self) -> HostId { + self.id.into() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Hash)] +pub(crate) enum HostId { + Client(ClientId), + Gateway(GatewayId), + Relay(RelayId), + Stale, +} + +impl From for HostId { + fn from(v: RelayId) -> Self { + Self::Relay(v) + } +} + +impl From for HostId { + fn from(v: GatewayId) -> Self { + Self::Gateway(v) + } +} + +impl From for HostId { + fn from(v: ClientId) -> Self { + Self::Client(v) + } +} diff --git a/rust/connlib/tunnel/src/tests/sim_node.rs b/rust/connlib/tunnel/src/tests/sim_node.rs index b94559bbd..8068324af 100644 --- a/rust/connlib/tunnel/src/tests/sim_node.rs +++ b/rust/connlib/tunnel/src/tests/sim_node.rs @@ -1,57 +1,41 @@ -use super::sim_relay::SimRelay; +use super::{ + sim_net::Host, + sim_relay::SimRelay, + strategies::{host_ip4s, host_ip6s}, +}; use crate::{ClientState, GatewayState}; use connlib_shared::{ - messages::{ - client::ResourceDescription, ClientId, DnsServer, GatewayId, Interface, ResourceId, - }, + messages::{ClientId, DnsServer, GatewayId, Interface}, proptest::domain_name, StaticSecret, }; +use firezone_relay::IpStack; use ip_network::{Ipv4Network, Ipv6Network}; -use proptest::{collection, prelude::*, sample}; +use proptest::{collection, prelude::*}; use rand::rngs::StdRng; use std::{ collections::{HashMap, HashSet}, fmt, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, time::Instant, }; -use tracing::Span; -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct SimNode { pub(crate) id: ID, pub(crate) state: S, - pub(crate) ip4_socket: Option, - pub(crate) ip6_socket: Option, - - pub(crate) old_sockets: Vec, - pub(crate) tunnel_ip4: Ipv4Addr, pub(crate) tunnel_ip6: Ipv6Addr, - - pub(crate) span: Span, } impl SimNode { - pub(crate) fn new( - id: ID, - state: S, - ip4_socket: Option, - ip6_socket: Option, - tunnel_ip4: Ipv4Addr, - tunnel_ip6: Ipv6Addr, - ) -> Self { + pub(crate) fn new(id: ID, state: S, tunnel_ip4: Ipv4Addr, tunnel_ip6: Ipv6Addr) -> Self { Self { id, state, - ip4_socket, - ip6_socket, tunnel_ip4, tunnel_ip6, - span: Span::none(), - old_sockets: Default::default(), } } } @@ -61,16 +45,12 @@ where ID: Copy, S: Clone, { - pub(crate) fn map_state(&self, f: impl FnOnce(S) -> T, span: Span) -> SimNode { + pub(crate) fn map(&self, f: impl FnOnce(S) -> T) -> SimNode { SimNode { id: self.id, state: f(self.state.clone()), - ip4_socket: self.ip4_socket, - ip6_socket: self.ip6_socket, tunnel_ip4: self.tunnel_ip4, tunnel_ip6: self.tunnel_ip6, - old_sockets: self.old_sockets.clone(), - span, } } } @@ -81,58 +61,20 @@ impl SimNode { relays: [&SimRelay>; N], now: Instant, ) { - self.span.in_scope(|| { - self.state.update_relays( - HashSet::default(), - HashSet::from(relays.map(|r| r.explode("client"))), - now, - ) - }); + self.state.update_relays( + HashSet::default(), + HashSet::from(relays.map(|r| r.explode("client"))), + now, + ) } pub(crate) fn update_upstream_dns(&mut self, upstream_dns_resolvers: Vec) { - self.span.in_scope(|| { - let _ = self.state.update_interface_config(Interface { - ipv4: self.tunnel_ip4, - ipv6: self.tunnel_ip6, - upstream_dns: upstream_dns_resolvers, - }); + let _ = self.state.update_interface_config(Interface { + ipv4: self.tunnel_ip4, + ipv6: self.tunnel_ip6, + upstream_dns: upstream_dns_resolvers, }); } - - pub(crate) fn update_system_dns(&mut self, system_dns_resolvers: Vec) { - self.span.in_scope(|| { - let _ = self.state.update_system_resolvers(system_dns_resolvers); - }); - } - - pub(crate) fn add_resource(&mut self, resource: ResourceDescription) { - self.span.in_scope(|| { - self.state.add_resources(&[resource]); - }) - } - - pub(crate) fn remove_resource(&mut self, resource: ResourceId) { - self.span.in_scope(|| { - self.state.remove_resources(&[resource]); - }) - } - - pub(crate) fn roam( - &mut self, - ip4_socket: Option, - ip6_socket: Option, - ) { - // 1. Remember what the current sockets were. - self.old_sockets.extend(self.ip4_socket.map(SocketAddr::V4)); - self.old_sockets.extend(self.ip6_socket.map(SocketAddr::V6)); - - // 2. Update to the new sockets. - self.ip4_socket = ip4_socket; - self.ip6_socket = ip6_socket; - - self.state.reset(); - } } impl SimNode { @@ -141,29 +83,15 @@ impl SimNode { relays: [&SimRelay>; N], now: Instant, ) { - self.span.in_scope(|| { - self.state.update_relays( - HashSet::default(), - HashSet::from(relays.map(|r| r.explode("gateway"))), - now, - ) - }); + self.state.update_relays( + HashSet::default(), + HashSet::from(relays.map(|r| r.explode("gateway"))), + now, + ) } } impl SimNode { - pub(crate) fn wants(&self, dst: SocketAddr) -> bool { - self.ip4_socket.is_some_and(|s| SocketAddr::V4(s) == dst) - || self.ip6_socket.is_some_and(|s| SocketAddr::V6(s) == dst) - } - - pub(crate) fn sending_socket_for(&self, dst: impl Into) -> Option { - Some(match dst.into() { - IpAddr::V4(_) => self.ip4_socket?.into(), - IpAddr::V6(_) => self.ip6_socket?.into(), - }) - } - pub(crate) fn tunnel_ip(&self, dst: impl Into) -> IpAddr { match dst.into() { IpAddr::V4(_) => IpAddr::from(self.tunnel_ip4), @@ -176,20 +104,6 @@ impl SimNode { } } -impl fmt::Debug for SimNode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SimNode") - .field("id", &self.id) - .field("state", &self.state) - .field("ip4_socket", &self.ip4_socket) - .field("ip6_socket", &self.ip6_socket) - .field("tunnel_ip4", &self.tunnel_ip4) - .field("tunnel_ip6", &self.tunnel_ip6) - .field("old_sockets", &self.old_sockets) - .finish() - } -} - #[derive(Clone, Copy, PartialEq)] pub(crate) struct PrivateKey([u8; 32]); @@ -210,64 +124,54 @@ impl fmt::Debug for PrivateKey { pub(crate) fn sim_node_prototype( id: impl Strategy, state: impl Strategy, -) -> impl Strategy> + tunnel_ip4s: &mut impl Iterator, + tunnel_ip6s: &mut impl Iterator, +) -> impl Strategy>> 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, - firezone_relay::proptest::any_ip_stack(), // We are re-using the strategy here because it is exactly what we need although we are generating a node here and not a relay. + socket_ips, any::().prop_filter("port must not be 0", |p| *p != 0), - any::().prop_filter("port must not be 0", |p| *p != 0), - tunnel_ip4(), - tunnel_ip6(), ) - .prop_filter_map( - "must have at least one socket address", - |(id, state, ip_stack, v4_port, v6_port, tunnel_ip4, tunnel_ip6)| { - let ip4_socket = ip_stack.as_v4().map(|ip| SocketAddrV4::new(*ip, v4_port)); - let ip6_socket = ip_stack - .as_v6() - .map(|ip| SocketAddrV6::new(*ip, v6_port, 0, 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); - Some(SimNode::new( - id, state, ip4_socket, ip6_socket, tunnel_ip4, tunnel_ip6, - )) - }, - ) + host + }) } -/// Generates an IPv4 address for the tunnel interface. +/// An [`Iterator`] over the possible IPv4 addresses of a tunnel interface. /// /// We use the CG-NAT range for IPv4. /// See . -pub(crate) fn tunnel_ip4() -> impl Strategy { - any::().prop_map(|idx| { - let cgnat_block = Ipv4Network::new(Ipv4Addr::new(100, 64, 0, 0), 11).unwrap(); - - let mut hosts = cgnat_block.hosts(); - - hosts.nth(idx.index(hosts.len())).unwrap() - }) +pub(crate) fn tunnel_ip4s() -> impl Iterator { + Ipv4Network::new(Ipv4Addr::new(100, 64, 0, 0), 11) + .unwrap() + .hosts() } -/// Generates an IPv6 address for the tunnel interface. +/// An [`Iterator`] over the possible IPv6 addresses of a tunnel interface. /// /// See . -pub(crate) fn tunnel_ip6() -> impl Strategy { - any::().prop_map(|idx| { - let cgnat_block = - Ipv6Network::new(Ipv6Addr::new(64_768, 8_225, 4_369, 0, 0, 0, 0, 0), 107).unwrap(); - - let mut subnets = cgnat_block.subnets_with_prefix(128); - - subnets - .nth(idx.index(subnets.len())) - .unwrap() - .network_address() - }) +pub(crate) fn tunnel_ip6s() -> impl Iterator { + Ipv6Network::new(Ipv6Addr::new(64_768, 8_225, 4_369, 0, 0, 0, 0, 0), 107) + .unwrap() + .subnets_with_prefix(128) + .map(|n| n.network_address()) } fn private_key() -> impl Strategy { diff --git a/rust/connlib/tunnel/src/tests/sim_relay.rs b/rust/connlib/tunnel/src/tests/sim_relay.rs index 6a5de6c94..1fbda7739 100644 --- a/rust/connlib/tunnel/src/tests/sim_relay.rs +++ b/rust/connlib/tunnel/src/tests/sim_relay.rs @@ -1,3 +1,5 @@ +use super::sim_net::Host; +use super::strategies::{host_ip4s, host_ip6s}; use connlib_shared::messages::RelayId; use firezone_relay::{AddressFamily, AllocationPort, ClientSocket, IpStack, PeerSocket}; use proptest::prelude::*; @@ -5,23 +7,22 @@ use rand::rngs::StdRng; use snownet::{RelaySocket, Transmit}; use std::{ borrow::Cow, - collections::{HashSet, VecDeque}, - fmt, + collections::HashSet, net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}, time::{Duration, Instant, SystemTime}, }; -use tracing::Span; -#[derive(Clone)] +#[derive(Clone, derivative::Derivative)] +#[derivative(Debug)] pub(crate) struct SimRelay { pub(crate) id: RelayId, pub(crate) state: S, ip_stack: firezone_relay::IpStack, pub(crate) allocations: HashSet<(AddressFamily, AllocationPort)>, - buffer: Vec, - pub(crate) span: Span, + #[derivative(Debug = "ignore")] + buffer: Vec, } impl SimRelay { @@ -32,12 +33,15 @@ impl SimRelay { ip_stack, allocations: Default::default(), buffer: vec![0u8; (1 << 16) - 1], - span: Span::none(), } } - pub(crate) fn ip_stack(&self) -> IpStack { - self.ip_stack + pub(crate) fn ip4(&self) -> Option { + self.ip_stack.as_v4().copied().map(|i| i.into()) + } + + pub(crate) fn ip6(&self) -> Option { + self.ip_stack.as_v6().copied().map(|i| i.into()) } } @@ -45,41 +49,18 @@ impl SimRelay where S: Copy, { - pub(crate) fn map_state(&self, f: impl FnOnce(S, IpStack) -> T, span: Span) -> SimRelay { + pub(crate) fn map(&self, f: impl FnOnce(S) -> T) -> SimRelay { SimRelay { id: self.id, - state: f(self.state, self.ip_stack), - ip_stack: self.ip_stack, + state: f(self.state), allocations: self.allocations.clone(), buffer: self.buffer.clone(), - span, + ip_stack: self.ip_stack, } } } impl SimRelay> { - pub(crate) fn wants(&self, dst: SocketAddr) -> bool { - let is_direct = self.matching_listen_socket(dst).is_some_and(|s| s == dst); - let is_allocation_port = self.allocations.contains(&match dst { - SocketAddr::V4(_) => (AddressFamily::V4, AllocationPort::new(dst.port())), - SocketAddr::V6(_) => (AddressFamily::V6, AllocationPort::new(dst.port())), - }); - let is_allocation_ip = self - .matching_listen_socket(dst) - .is_some_and(|s| s.ip() == dst.ip()); - - is_direct || (is_allocation_port && is_allocation_ip) - } - - pub(crate) fn sending_socket_for(&self, dst: SocketAddr, port: u16) -> Option { - Some(match dst { - SocketAddr::V4(_) => SocketAddr::V4(SocketAddrV4::new(*self.ip_stack.as_v4()?, port)), - SocketAddr::V6(_) => { - SocketAddr::V6(SocketAddrV6::new(*self.ip_stack.as_v6()?, port, 0, 0)) - } - }) - } - pub(crate) fn explode(&self, username: &str) -> (RelayId, RelaySocket, String, String, String) { let relay_socket = match self.ip_stack { firezone_relay::IpStack::Ip4(ip4) => RelaySocket::V4(SocketAddrV4::new(ip4, 3478)), @@ -110,32 +91,21 @@ impl SimRelay> { } } - pub(crate) fn ip4(&self) -> Option { - self.ip_stack.as_v4().copied().map(|i| i.into()) - } - - pub(crate) fn ip6(&self) -> Option { - self.ip_stack.as_v6().copied().map(|i| i.into()) - } - pub(crate) fn handle_packet( &mut self, payload: &[u8], sender: SocketAddr, dst: SocketAddr, now: Instant, - buffered_transmits: &mut VecDeque<(Transmit<'static>, Option)>, - ) { + ) -> Option> { if self.matching_listen_socket(dst).is_some_and(|s| s == dst) { - self.handle_client_input(payload, ClientSocket::new(sender), now, buffered_transmits); - return; + return self.handle_client_input(payload, ClientSocket::new(sender), now); } self.handle_peer_traffic( payload, PeerSocket::new(sender), AllocationPort::new(dst.port()), - buffered_transmits, ) } @@ -144,60 +114,42 @@ impl SimRelay> { payload: &[u8], client: ClientSocket, now: Instant, - buffered_transmits: &mut VecDeque<(Transmit<'static>, Option)>, - ) { - if let Some((port, peer)) = self - .span - .in_scope(|| self.state.handle_client_input(payload, client, now)) - { - let payload = &payload[4..]; + ) -> Option> { + let (port, peer) = self.state.handle_client_input(payload, client, now)?; - // The `dst` of the relayed packet is what TURN calls a "peer". - let dst = peer.into_socket(); + let payload = &payload[4..]; - // The `src_ip` is the relay's IP - let src_ip = match dst { - SocketAddr::V4(_) => { - assert!( - self.allocations.contains(&(AddressFamily::V4, port)), - "IPv4 allocation to be present if we want to send to an IPv4 socket" - ); + // The `dst` of the relayed packet is what TURN calls a "peer". + let dst = peer.into_socket(); - self.ip4().expect("listen on IPv4 if we have an allocation") - } - SocketAddr::V6(_) => { - assert!( - self.allocations.contains(&(AddressFamily::V6, port)), - "IPv6 allocation to be present if we want to send to an IPv6 socket" - ); + // The `src_ip` is the relay's IP + let src_ip = match dst { + SocketAddr::V4(_) => { + assert!( + self.allocations.contains(&(AddressFamily::V4, port)), + "IPv4 allocation to be present if we want to send to an IPv4 socket" + ); - self.ip6().expect("listen on IPv6 if we have an allocation") - } - }; - - // The `src` of the relayed packet is the relay itself _from_ the allocated port. - let src = SocketAddr::new(src_ip, port.value()); - - // Check if we need to relay to ourselves (from one allocation to another) - if self.wants(dst) { - // When relaying to ourselves, we become our own peer. - let peer_socket = PeerSocket::new(src); - // The allocation that the data is arriving on is the `dst`'s port. - let allocation_port = AllocationPort::new(dst.port()); - - self.handle_peer_traffic(payload, peer_socket, allocation_port, buffered_transmits); - return; + self.ip4().expect("listen on IPv4 if we have an allocation") } + SocketAddr::V6(_) => { + assert!( + self.allocations.contains(&(AddressFamily::V6, port)), + "IPv6 allocation to be present if we want to send to an IPv6 socket" + ); - buffered_transmits.push_back(( - Transmit { - src: Some(src), - dst, - payload: Cow::Owned(payload.to_vec()), - }, - Some(src), - )); - } + self.ip6().expect("listen on IPv6 if we have an allocation") + } + }; + + // The `src` of the relayed packet is the relay itself _from_ the allocated port. + let src = SocketAddr::new(src_ip, port.value()); + + Some(Transmit { + src: Some(src), + dst, + payload: Cow::Owned(payload.to_vec()), + }) } fn handle_peer_traffic( @@ -205,31 +157,24 @@ impl SimRelay> { payload: &[u8], peer: PeerSocket, port: AllocationPort, - buffered_transmits: &mut VecDeque<(Transmit<'static>, Option)>, - ) { - if let Some((client, channel)) = self - .span - .in_scope(|| self.state.handle_peer_traffic(payload, peer, port)) - { - let full_length = firezone_relay::ChannelData::encode_header_to_slice( - channel, - payload.len() as u16, - &mut self.buffer[..4], - ); - self.buffer[4..full_length].copy_from_slice(payload); + ) -> Option> { + let (client, channel) = self.state.handle_peer_traffic(payload, peer, port)?; - let receiving_socket = client.into_socket(); - let sending_socket = self.matching_listen_socket(receiving_socket).unwrap(); + let full_length = firezone_relay::ChannelData::encode_header_to_slice( + channel, + payload.len() as u16, + &mut self.buffer[..4], + ); + self.buffer[4..full_length].copy_from_slice(payload); - buffered_transmits.push_back(( - Transmit { - src: Some(sending_socket), - dst: receiving_socket, - payload: Cow::Owned(self.buffer[..full_length].to_vec()), - }, - Some(sending_socket), - )); - } + let receiving_socket = client.into_socket(); + let sending_socket = self.matching_listen_socket(receiving_socket).unwrap(); + + Some(Transmit { + src: Some(sending_socket), + dst: receiving_socket, + payload: Cow::Owned(self.buffer[..full_length].to_vec()), + }) } fn make_credentials(&self, username: &str) -> (String, String) { @@ -247,21 +192,14 @@ impl SimRelay> { } } -impl fmt::Debug for SimRelay { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("SimRelay") - .field("id", &self.id) - .field("ip_stack", &self.ip_stack) - .field("allocations", &self.allocations) - .finish() - } -} +pub(crate) fn sim_relay_prototype() -> impl Strategy>> { + // 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 }); -pub(crate) fn sim_relay_prototype() -> impl Strategy> { - ( - any::(), - firezone_relay::proptest::dual_ip_stack(), // For this test, our relays always run in dual-stack mode to ensure connectivity! - any::(), - ) - .prop_map(|(seed, ip_stack, id)| SimRelay::new(RelayId::from_u128(id), seed, ip_stack)) + (any::(), socket_ips, any::()).prop_map(move |(seed, ip_stack, id)| { + let mut host = Host::new(SimRelay::new(RelayId::from_u128(id), seed, ip_stack)); + host.update_interface(ip_stack.as_v4().copied(), ip_stack.as_v6().copied(), 3478); + + host + }) } diff --git a/rust/connlib/tunnel/src/tests/strategies.rs b/rust/connlib/tunnel/src/tests/strategies.rs index c4045bc47..046234fea 100644 --- a/rust/connlib/tunnel/src/tests/strategies.rs +++ b/rust/connlib/tunnel/src/tests/strategies.rs @@ -1,5 +1,7 @@ use connlib_shared::{messages::DnsServer, proptest::domain_name, DomainName}; -use proptest::{collection, prelude::*}; +use ip_network::{Ipv4Network, Ipv6Network}; +use itertools::Itertools as _; +use proptest::{collection, prelude::*, sample}; use std::{ collections::{BTreeMap, HashMap, HashSet}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, @@ -49,6 +51,33 @@ pub(crate) fn upstream_dns_servers() -> impl Strategy> { ] } +/// 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 { + 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 { + 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> { collection::vec(any::(), 1..4) // Always need at least 1 system DNS server. TODO: Should we test what happens if we don't? } diff --git a/rust/connlib/tunnel/src/tests/sut.rs b/rust/connlib/tunnel/src/tests/sut.rs index e7e718a94..7429847a4 100644 --- a/rust/connlib/tunnel/src/tests/sut.rs +++ b/rust/connlib/tunnel/src/tests/sut.rs @@ -1,4 +1,5 @@ use super::reference::ReferenceState; +use super::sim_net::{Host, HostId, RoutingTable}; use super::sim_node::SimNode; use super::sim_portal::SimPortal; use super::sim_relay::SimRelay; @@ -15,6 +16,7 @@ use connlib_shared::{ }, DomainName, }; +use firezone_relay::IpStack; use hickory_proto::{ op::{MessageType, Query}, rr::{rdata, RData, Record, RecordType}, @@ -31,12 +33,12 @@ use std::collections::{BTreeMap, BTreeSet}; use std::{ collections::{HashMap, HashSet, VecDeque}, net::{IpAddr, SocketAddr}, - ops::ControlFlow, str::FromStr as _, sync::Arc, time::{Duration, Instant}, }; -use tracing::{debug_span, subscriber::DefaultGuard}; +use tracing::debug_span; +use tracing::subscriber::DefaultGuard; use tracing_subscriber::{util::SubscriberInitExt as _, EnvFilter}; /// The actual system-under-test. @@ -46,9 +48,9 @@ pub(crate) struct TunnelTest { now: Instant, utc_now: DateTime, - client: SimNode, - gateway: SimNode, - relay: SimRelay>, + client: Host>, + gateway: Host>, + relay: Host>>, portal: SimPortal, /// The DNS records created on the client as a result of received DNS responses. @@ -66,6 +68,8 @@ pub(crate) struct TunnelTest { pub(crate) client_received_icmp_replies: HashMap<(u16, u16), IpPacket<'static>>, pub(crate) gateway_received_icmp_requests: VecDeque>, + network: RoutingTable, + #[allow(dead_code)] logger: DefaultGuard, } @@ -85,36 +89,45 @@ impl StateMachineTest for TunnelTest { .set_default(); // Construct client, gateway and relay from the initial state. - let mut client = ref_state - .client - .map_state(|(k, h)| ClientState::new(k, h), debug_span!("client")); - let mut gateway = ref_state - .gateway - .map_state(GatewayState::new, debug_span!("gateway")); - let relay = ref_state.relay.map_state( - |seed, ip_stack| { - firezone_relay::Server::new( - ip_stack, - rand::rngs::StdRng::seed_from_u64(seed), - 3478, - 49152, - 65535, - ) + let mut client = ref_state.client.map( + |sim_node, _, _| sim_node.map(|(k, h)| ClientState::new(k, h)), + debug_span!("client"), + ); + let mut gateway = ref_state.gateway.map( + |sim_node, _, _| sim_node.map(GatewayState::new), + debug_span!("gateway"), + ); + + let relay = ref_state.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, + ) + }) }, debug_span!("relay"), ); - let portal = SimPortal::new(client.id, gateway.id, relay.id); + let portal = SimPortal::new(client.inner().id, gateway.inner().id, relay.inner().id); // Configure client and gateway with the relay. - client.init_relays([&relay], ref_state.now); - gateway.init_relays([&relay], ref_state.now); + client.exec_mut(|c| c.init_relays([relay.inner()], ref_state.now)); + gateway.exec_mut(|g| g.init_relays([relay.inner()], ref_state.now)); - client.update_upstream_dns(ref_state.upstream_dns_resolvers.clone()); - client.update_system_dns(ref_state.system_dns_resolvers.clone()); + 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()) + }); let mut this = Self { now: ref_state.now, utc_now: ref_state.utc_now, + network: ref_state.network.clone(), client, gateway, portal, @@ -146,12 +159,16 @@ impl StateMachineTest for TunnelTest { // Act: Apply the transition match transition { Transition::AddCidrResource(r) => { - state.client.add_resource(ResourceDescription::Cidr(r)) + state + .client + .exec_mut(|c| c.state.add_resources(&[ResourceDescription::Cidr(r)])); } Transition::AddDnsResource { resource, .. } => state .client - .add_resource(ResourceDescription::Dns(resource)), - Transition::RemoveResource(id) => state.client.remove_resource(id), + .exec_mut(|c| c.state.add_resources(&[ResourceDescription::Dns(resource)])), + Transition::RemoveResource(id) => { + state.client.exec_mut(|c| c.state.remove_resources(&[id])) + } Transition::SendICMPPacketToNonResourceIp { src, dst, @@ -205,19 +222,24 @@ impl StateMachineTest for TunnelTest { state.now += Duration::from_millis(millis); } Transition::UpdateSystemDnsServers { servers } => { - state.client.update_system_dns(servers); + state + .client + .exec_mut(|c| c.state.update_system_resolvers(servers)); } Transition::UpdateUpstreamDnsServers { servers } => { - state.client.update_upstream_dns(servers); + state.client.exec_mut(|c| c.update_upstream_dns(servers)); } - Transition::RoamClient { - ip4_socket, - ip6_socket, - } => { - state.client.roam(ip4_socket, ip6_socket); + Transition::RoamClient { ip4, ip6, port } => { + state.network.remove_host(&state.client); + state.client.update_interface(ip4, ip6, port); + debug_assert!(state.network.add_host(&state.client)); - // In prod, we reconnect to the portal and receive a new `init` message. - state.client.init_relays([&state.relay], ref_state.now); + state.client.exec_mut(|c| { + c.state.reset(); + + // In prod, we reconnect to the portal and receive a new `init` message. + c.init_relays([state.relay.inner()], ref_state.now); + }); } }; state.advance(ref_state, &mut buffered_transmits); @@ -248,34 +270,27 @@ impl TunnelTest { /// /// For our tests to work properly, each [`Transition`] needs to advance the state as much as possible. /// For example, upon the first packet to a resource, we need to trigger the connection intent and fully establish a connection. - /// Dispatching a [`Transmit`] (read: packet) to a component can trigger more packets, i.e. receiving a STUN request may trigger a STUN response. + /// 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 component can make progress at which point we consider the [`Transition`] complete. + /// 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>, Option)>, + buffered_transmits: &mut VecDeque>, ) { loop { - if let Some((transmit, sending_socket)) = buffered_transmits.pop_front() { - self.dispatch_transmit( - transmit, - sending_socket, - buffered_transmits, - &ref_state.global_dns_records, - ); + if let Some(transmit) = buffered_transmits.pop_front() { + self.dispatch_transmit(transmit, buffered_transmits, &ref_state.global_dns_records); continue; } - if let Some(transmit) = self.client.state.poll_transmit() { - let sending_socket = self.client.sending_socket_for(transmit.dst.ip()); - - buffered_transmits.push_back((transmit, sending_socket)); + if let Some(transmit) = self.client.poll_transmit() { + buffered_transmits.push_back(transmit); continue; } - if let Some(event) = self.client.state.poll_event() { + if let Some(event) = self.client.exec_mut(|c| c.state.poll_event()) { self.on_client_event( - self.client.id, + self.client.inner().id, event, &ref_state.client_cidr_resources, &ref_state.client_dns_resources, @@ -283,56 +298,53 @@ impl TunnelTest { ); continue; } - if let Some(query) = self.client.state.poll_dns_queries() { + if let Some(query) = self + .client + .exec_mut(|client| client.state.poll_dns_queries()) + { self.on_forwarded_dns_query(query, ref_state); continue; } - if let Some(packet) = self.client.state.poll_packets() { + if let Some(packet) = self.client.exec_mut(|client| client.state.poll_packets()) { self.on_client_received_packet(packet); continue; } - if let Some(transmit) = self.gateway.state.poll_transmit() { - let sending_socket = self.gateway.sending_socket_for(transmit.dst.ip()); - - buffered_transmits.push_back((transmit, sending_socket)); + if let Some(transmit) = self.gateway.poll_transmit() { + buffered_transmits.push_back(transmit); continue; } - if let Some(event) = self.gateway.state.poll_event() { - self.on_gateway_event(self.gateway.id, event); + if let Some(event) = self.gateway.exec_mut(|gateway| gateway.state.poll_event()) { + self.on_gateway_event(self.gateway.inner().id, event); continue; } - if let Some(message) = self.relay.state.next_command() { + if let Some(message) = self.relay.exec_mut(|relay| relay.state.next_command()) { match message { firezone_relay::Command::SendMessage { payload, recipient } => { let dst = recipient.into_socket(); let src = self .relay - .sending_socket_for(dst, 3478) + .sending_socket_for(dst.ip()) .expect("relay to never emit packets without a matching socket"); - if let ControlFlow::Break(_) = self.try_handle_client(dst, src, &payload) { - continue; - } - - if let ControlFlow::Break(_) = self.try_handle_gateway( + buffered_transmits.push_back(Transmit { + src: Some(src), dst, - src, - &payload, - buffered_transmits, - &ref_state.global_dns_records, - ) { - continue; - } + payload: payload.into(), + }); - panic!("Unhandled packet: {src} -> {dst}") + continue; } firezone_relay::Command::CreateAllocation { port, family } => { - self.relay.allocations.insert((family, port)); + self.relay.allocate_port(port.value(), family); + self.relay + .exec_mut(|r| r.allocations.insert((family, port))); } firezone_relay::Command::FreeAllocation { port, family } => { - self.relay.allocations.remove(&(family, port)); + self.relay.deallocate_port(port.value(), family); + self.relay + .exec_mut(|r| r.allocations.remove(&(family, port))); } } continue; @@ -354,34 +366,42 @@ impl TunnelTest { .collect() } - /// Forwards time to the given instant iff the corresponding component would like that (i.e. returns a timestamp <= from `poll_timeout`). + /// 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`. fn handle_timeout(&mut self, now: Instant, utc_now: DateTime) -> bool { let mut any_advanced = false; - if self.client.state.poll_timeout().is_some_and(|t| t <= now) { + if self + .client + .exec_mut(|client| client.state.poll_timeout()) + .is_some_and(|t| t <= now) + { any_advanced = true; self.client - .span - .in_scope(|| self.client.state.handle_timeout(now)); + .exec_mut(|client| client.state.handle_timeout(now)); }; - if self.gateway.state.poll_timeout().is_some_and(|t| t <= now) { + if self + .gateway + .exec_mut(|gateway| gateway.state.poll_timeout()) + .is_some_and(|t| t <= now) + { any_advanced = true; self.gateway - .span - .in_scope(|| self.gateway.state.handle_timeout(now, utc_now)) + .exec_mut(|gateway| gateway.state.handle_timeout(now, utc_now)) }; - if self.relay.state.poll_timeout().is_some_and(|t| t <= now) { + if self + .relay + .exec_mut(|relay| relay.state.poll_timeout()) + .is_some_and(|t| t <= now) + { any_advanced = true; - self.relay - .span - .in_scope(|| self.relay.state.handle_timeout(now)) + self.relay.exec_mut(|relay| relay.state.handle_timeout(now)) }; any_advanced @@ -390,7 +410,7 @@ impl TunnelTest { fn send_ip_packet_client_to_gateway( &mut self, packet: MutableIpPacket<'_>, - ) -> Option<(Transmit<'static>, Option)> { + ) -> Option> { { let packet = packet.to_owned().into_immutable(); @@ -418,173 +438,75 @@ impl TunnelTest { } } - let transmit = self - .client - .span - .in_scope(|| self.client.state.encapsulate(packet, self.now))?; - let transmit = transmit.into_owned(); - let sending_socket = self.client.sending_socket_for(transmit.dst.ip()); - - Some((transmit, sending_socket)) + self.client.encapsulate(packet, self.now) } fn send_ip_packet_gateway_to_client( &mut self, packet: MutableIpPacket<'_>, - ) -> Option<(Transmit<'static>, Option)> { - let transmit = self - .gateway - .span - .in_scope(|| self.gateway.state.encapsulate(packet, self.now))?; - let transmit = transmit.into_owned(); - let sending_socket = self.gateway.sending_socket_for(transmit.dst.ip()); - - Some((transmit, sending_socket)) + ) -> Option> { + self.gateway.encapsulate(packet, self.now) } - /// Dispatches a [`Transmit`] to the correct component. + /// Dispatches a [`Transmit`] to the correct host. /// /// This function is basically the "network layer" of our tests. - /// It takes a [`Transmit`] and checks, which component accepts it, i.e. has configured the correct IP address. - /// Our tests don't have a concept of a network topology. - /// This means, components can have IP addresses in completely different subnets, yet this function will still "route" them correctly. + /// It takes a [`Transmit`] and checks, which host accepts it, i.e. has configured the correct IP address. + /// + /// Currently, the network topology of our tests are a single subnet without NAT. fn dispatch_transmit( &mut self, transmit: Transmit, - sending_socket: Option, - buffered_transmits: &mut VecDeque<(Transmit<'static>, Option)>, + buffered_transmits: &mut VecDeque>, global_dns_records: &BTreeMap>, ) { + let src = transmit + .src + .expect("`src` should always be set in these tests"); let dst = transmit.dst; let payload = &transmit.payload; - let Some(src) = sending_socket else { - tracing::warn!("Dropping packet to {dst}: no socket"); - return; + let Some(host) = self.network.host_by_ip(dst.ip()) else { + panic!("Unhandled packet: {src} -> {dst}") }; - if self - .try_handle_relay(dst, src, payload, buffered_transmits) - .is_break() - { - return; - } + let mut buf = [0u8; 1000]; - let src = transmit - .src - .expect("all packets without src should have been handled via relays"); + match host { + HostId::Client(_) => { + let Some(packet) = self + .client + .exec_mut(|c| c.state.decapsulate(dst, src, payload, self.now, &mut buf)) + else { + return; + }; - if self.try_handle_client(dst, src, payload).is_break() { - return; - } - - if self - .try_handle_gateway(dst, src, payload, buffered_transmits, global_dns_records) - .is_break() - { - return; - } - - panic!("Unhandled packet: {src} -> {dst}") - } - - fn try_handle_relay( - &mut self, - dst: SocketAddr, - src: SocketAddr, - payload: &[u8], - buffered_transmits: &mut VecDeque<(Transmit<'static>, Option)>, - ) -> ControlFlow<()> { - if !self.relay.wants(dst) { - return ControlFlow::Continue(()); - } - - self.relay - .handle_packet(payload, src, dst, self.now, buffered_transmits); - - ControlFlow::Break(()) - } - - fn try_handle_client( - &mut self, - dst: SocketAddr, - src: SocketAddr, - payload: &[u8], - ) -> ControlFlow<()> { - let mut buffer = [0u8; 2000]; - - if self.client.old_sockets.contains(&dst) { - tracing::debug!("Dropping packet to {dst} because the client roamed away from this network interface"); - return ControlFlow::Break(()); - } - - if !self.client.wants(dst) { - return ControlFlow::Continue(()); - } - - if let Some(packet) = self.client.span.in_scope(|| { - self.client - .state - .decapsulate(dst, src, payload, self.now, &mut buffer) - }) { - self.on_client_received_packet(packet); - }; - - ControlFlow::Break(()) - } - - fn try_handle_gateway( - &mut self, - dst: SocketAddr, - src: SocketAddr, - payload: &[u8], - buffered_transmits: &mut VecDeque<(Transmit<'static>, Option)>, - global_dns_records: &BTreeMap>, - ) -> ControlFlow<()> { - let mut buffer = [0u8; 2000]; - - if !self.gateway.wants(dst) { - return ControlFlow::Continue(()); - } - - if let Some(packet) = self.gateway.span.in_scope(|| { - self.gateway - .state - .decapsulate(dst, src, payload, self.now, &mut buffer) - }) { - 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 ControlFlow::Break(()); + self.on_client_received_packet(packet); } + HostId::Gateway(_) => { + let Some(packet) = self + .gateway + .exec_mut(|g| g.state.decapsulate(dst, src, payload, self.now, &mut buf)) + else { + 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 ControlFlow::Break(()); + self.on_gateway_received_packet(packet, global_dns_records, buffered_transmits); } + HostId::Relay(_) => { + let Some(transmit) = self + .relay + .exec_mut(|r| r.handle_packet(payload, src, dst, self.now)) + else { + return; + }; - panic!("Unhandled packet") - }; - - ControlFlow::Break(()) + buffered_transmits.push_back(transmit); + } + HostId::Stale => { + tracing::debug!(%dst, "Dropping packet because host roamed away or is offline"); + } + } } fn on_client_event( @@ -597,18 +519,16 @@ impl TunnelTest { ) { match event { ClientEvent::AddedIceCandidates { candidates, .. } => { - self.gateway.span.in_scope(|| { + self.gateway.exec_mut(|gateway| { for candidate in candidates { - self.gateway - .state - .add_ice_candidate(src, candidate, self.now) + gateway.state.add_ice_candidate(src, candidate, self.now) } }) } ClientEvent::RemovedIceCandidates { candidates, .. } => { - self.gateway.span.in_scope(|| { + self.gateway.exec_mut(|gateway| { for candidate in candidates { - self.gateway.state.remove_ice_candidate(src, candidate) + gateway.state.remove_ice_candidate(src, candidate) } }) } @@ -625,12 +545,7 @@ impl TunnelTest { let request = self .client - .span - .in_scope(|| { - self.client - .state - .create_or_reuse_connection(resource, gateway, site) - }) + .exec_mut(|c| c.state.create_or_reuse_connection(resource, gateway, site)) .unwrap() .unwrap(); @@ -655,10 +570,9 @@ impl TunnelTest { Request::NewConnection(new_connection) => { let answer = self .gateway - .span - .in_scope(|| { - self.gateway.state.accept( - self.client.id, + .exec_mut(|gateway| { + gateway.state.accept( + self.client.exec_mut(|c| c.id), snownet::Offer { session_key: new_connection .client_preshared_key @@ -676,9 +590,9 @@ impl TunnelTest { .password, }, }, - self.client.state.public_key(), - self.client.tunnel_ip4, - self.client.tunnel_ip6, + 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), new_connection .client_payload .domain @@ -691,9 +605,8 @@ impl TunnelTest { .unwrap(); self.client - .span - .in_scope(|| { - self.client.state.accept_answer( + .exec_mut(|client| { + client.state.accept_answer( snownet::Answer { credentials: snownet::Credentials { username: answer.username, @@ -701,7 +614,7 @@ impl TunnelTest { }, }, resource_id, - self.gateway.state.public_key(), + self.gateway.exec_mut(|g| g.state.public_key()), self.now, ) }) @@ -709,11 +622,10 @@ impl TunnelTest { } Request::ReuseConnection(reuse_connection) => { self.gateway - .span - .in_scope(|| { - self.gateway.state.allow_access( + .exec_mut(|gateway| { + gateway.state.allow_access( resource, - self.client.id, + self.client.exec_mut(|c| c.id), None, reuse_connection.payload.map(|r| (r.name, r.proxy_ips)), self.now, @@ -743,11 +655,10 @@ impl TunnelTest { ); self.gateway - .span - .in_scope(|| { - self.gateway.state.allow_access( + .exec_mut(|gateway| { + gateway.state.allow_access( resource, - self.client.id, + self.client.exec_mut(|c| c.id), None, reuse_connection.payload.map(|r| (r.name, r.proxy_ips)), self.now, @@ -767,19 +678,15 @@ impl TunnelTest { fn on_gateway_event(&mut self, src: GatewayId, event: GatewayEvent) { match event { - GatewayEvent::AddedIceCandidates { candidates, .. } => { - self.client.span.in_scope(|| { - for candidate in candidates { - self.client - .state - .add_ice_candidate(src, candidate, self.now) - } - }) - } + GatewayEvent::AddedIceCandidates { candidates, .. } => self.client.exec_mut(|client| { + for candidate in candidates { + client.state.add_ice_candidate(src, candidate, self.now) + } + }), GatewayEvent::RemovedIceCandidates { candidates, .. } => { - self.client.span.in_scope(|| { + self.client.exec_mut(|client| { for candidate in candidates { - self.client.state.remove_ice_candidate(src, candidate) + client.state.remove_ice_candidate(src, candidate) } }) } @@ -834,13 +741,52 @@ impl TunnelTest { unimplemented!("Unhandled packet") } + /// Process an IP packet received on the gateway. + fn on_gateway_received_packet( + &mut self, + packet: IpPacket<'_>, + global_dns_records: &BTreeMap>, + buffered_transmits: &mut VecDeque>, + ) { + 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>, Option)> { + ) -> Option> { let dns_server = *self .client_dns_by_sentinel .get_by_right(&dns_server) @@ -848,7 +794,7 @@ impl TunnelTest { let name = domain_to_hickory_name(domain); - let src = self.client.tunnel_ip(dns_server); + let src = self.client.exec_mut(|c| c.tunnel_ip(dns_server)); let packet = ip_packet::make::dns_query( name, @@ -885,13 +831,15 @@ impl TunnelTest { .map(|rdata| Record::from_rdata(name.clone(), 86400_u32, rdata)) .collect::>(); - self.client.state.on_dns_result( - query, - Ok(Ok(Ok(Lookup::new_with_max_ttl( - Query::query(name, requested_type), - record_data, - )))), - ); + self.client.exec_mut(|c| { + c.state.on_dns_result( + query, + Ok(Ok(Ok(Lookup::new_with_max_ttl( + Query::query(name, requested_type), + record_data, + )))), + ) + }) } } diff --git a/rust/connlib/tunnel/src/tests/transition.rs b/rust/connlib/tunnel/src/tests/transition.rs index 29f387ea6..d47517399 100644 --- a/rust/connlib/tunnel/src/tests/transition.rs +++ b/rust/connlib/tunnel/src/tests/transition.rs @@ -7,11 +7,12 @@ use connlib_shared::{ proptest::*, DomainName, }; +use firezone_relay::IpStack; use hickory_proto::rr::RecordType; use proptest::{prelude::*, sample}; use std::{ collections::{HashMap, HashSet}, - net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, }; /// The possible transitions of the state machine. @@ -75,8 +76,9 @@ pub(crate) enum Transition { /// Roam the client to a new pair of sockets. RoamClient { - ip4_socket: Option, - ip6_socket: Option, + ip4: Option, + ip6: Option, + port: u16, }, } @@ -214,20 +216,19 @@ pub(crate) fn question_mark_wildcard_dns_resource() -> impl Strategy impl Strategy { + 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 }) + ]; + ( - firezone_relay::proptest::any_ip_stack(), // We are re-using the strategy here because it is exactly what we need although we are generating a node here and not a relay. - any::().prop_filter("port must not be 0", |p| *p != 0), + ip_stack, any::().prop_filter("port must not be 0", |p| *p != 0), ) - .prop_map(move |(ip_stack, v4_port, v6_port)| { - let ip4_socket = ip_stack.as_v4().map(|ip| SocketAddrV4::new(*ip, v4_port)); - let ip6_socket = ip_stack - .as_v6() - .map(|ip| SocketAddrV6::new(*ip, v6_port, 0, 0)); - - Transition::RoamClient { - ip4_socket, - ip6_socket, - } + .prop_map(move |(ip_stack, port)| Transition::RoamClient { + ip4: ip_stack.as_v4().copied(), + ip6: ip_stack.as_v6().copied(), + port, }) } diff --git a/rust/relay/src/lib.rs b/rust/relay/src/lib.rs index bd6ade3ce..b5eab3d25 100644 --- a/rust/relay/src/lib.rs +++ b/rust/relay/src/lib.rs @@ -73,6 +73,17 @@ impl From<(Ipv4Addr, Ipv6Addr)> for IpStack { } } +impl From<(Option, Option)> for IpStack { + fn from((ip4, ip6): (Option, Option)) -> Self { + match (ip4, ip6) { + (None, None) => panic!("must have at least one IP"), + (None, Some(ip6)) => IpStack::from(ip6), + (Some(ip4), None) => IpStack::from(ip4), + (Some(ip4), Some(ip6)) => IpStack::from((ip4, ip6)), + } + } +} + /// New-type for a client's socket. /// /// From the [spec](https://www.rfc-editor.org/rfc/rfc8656#section-2-4.4): diff --git a/rust/relay/src/proptest.rs b/rust/relay/src/proptest.rs index bb1612cb9..0f0b64298 100644 --- a/rust/relay/src/proptest.rs +++ b/rust/relay/src/proptest.rs @@ -1,13 +1,9 @@ use crate::Binding; use crate::ChannelData; -use crate::IpStack; use proptest::arbitrary::any; -use proptest::prop_oneof; use proptest::strategy::Just; use proptest::strategy::Strategy; use proptest::string::string_regex; -use std::net::Ipv4Addr; -use std::net::Ipv6Addr; use std::time::Duration; use stun_codec::rfc5766::attributes::{ChannelNumber, Lifetime, RequestedTransport}; use stun_codec::TransactionId; @@ -68,29 +64,3 @@ pub fn username_salt() -> impl Strategy { pub fn nonce() -> impl Strategy { any::().prop_map(Uuid::from_u128) } - -pub fn any_ip_stack() -> impl Strategy { - dual_ip_stack().prop_flat_map(|ip_stack| { - prop_oneof![ - Just(IpStack::Ip4(*ip_stack.as_v4().unwrap())), - Just(IpStack::Ip6(*ip_stack.as_v6().unwrap())), - Just(ip_stack), - ] - }) -} - -pub fn dual_ip_stack() -> impl Strategy { - ( - any::().prop_filter("must be normal ip", |ip| { - !ip.is_broadcast() - && !ip.is_unspecified() - && !ip.is_documentation() - && !ip.is_link_local() - && !ip.is_multicast() - }), - any::().prop_filter("must be normal ip", |ip| { - !ip.is_unspecified() && !ip.is_multicast() - }), - ) - .prop_map(|(ip4, ip6)| IpStack::Dual { ip4, ip6 }) -}