diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9b7d120e7..59d21d43c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3416,7 +3416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.48.5", ] [[package]] @@ -4825,9 +4825,8 @@ dependencies = [ [[package]] name = "proptest" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +version = "1.5.0" +source = "git+https://github.com/thomaseizinger/proptest?branch=fix/always-check-acceptable-current-state#26c036b5ca832726d7f0c8438a750c8efa2f0f8b" dependencies = [ "bit-set", "bit-vec", @@ -4846,8 +4845,7 @@ dependencies = [ [[package]] name = "proptest-state-machine" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28278d6a11102264b0c569c33dbe5286ba00d2dd6d96ff2a94296e0e5b3d1e04" +source = "git+https://github.com/thomaseizinger/proptest?branch=fix/always-check-acceptable-current-state#26c036b5ca832726d7f0c8438a750c8efa2f0f8b" dependencies = [ "proptest", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 8466d367c..634e6a102 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -69,6 +69,8 @@ rustdoc.private-intra-doc-links = "allow" # We don't publish any of our docs but boringtun = { git = "https://github.com/cloudflare/boringtun", branch = "master" } str0m = { git = "https://github.com/firezone/str0m", branch = "main" } ip_network_table = { git = "https://github.com/edmonds/ip_network_table", branch = "some-useful-traits" } # For `Debug` and `Clone` +proptest = { git = "https://github.com/thomaseizinger/proptest", branch = "fix/always-check-acceptable-current-state" } +proptest-state-machine = { git = "https://github.com/thomaseizinger/proptest", branch = "fix/always-check-acceptable-current-state" } [profile.release] strip = true diff --git a/rust/connlib/shared/Cargo.toml b/rust/connlib/shared/Cargo.toml index 8572bdc79..cf92af06e 100644 --- a/rust/connlib/shared/Cargo.toml +++ b/rust/connlib/shared/Cargo.toml @@ -32,7 +32,7 @@ domain = { workspace = true } libc = "0.2" phoenix-channel = { workspace = true } ip-packet = { workspace = true } -proptest = { version = "1.4.0", optional = true } +proptest = { version = "1", optional = true } itertools = "0.13" # Needed for Android logging until tracing is working diff --git a/rust/connlib/tunnel/Cargo.toml b/rust/connlib/tunnel/Cargo.toml index 8c0e10d54..6e51430f3 100644 --- a/rust/connlib/tunnel/Cargo.toml +++ b/rust/connlib/tunnel/Cargo.toml @@ -30,7 +30,7 @@ socket2 = { version = "0.5" } snownet = { workspace = true } quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch = "main"} hex = "0.4.3" -proptest = { version = "1.4.0", optional = true } +proptest = { version = "1", optional = true } ip-packet = { workspace = true } rangemap = "1.5.1" anyhow = "1.0" diff --git a/rust/connlib/tunnel/proptest-regressions/tests.txt b/rust/connlib/tunnel/proptest-regressions/tests.txt index 7bdc3040b..84f60b40e 100644 --- a/rust/connlib/tunnel/proptest-regressions/tests.txt +++ b/rust/connlib/tunnel/proptest-regressions/tests.txt @@ -48,3 +48,5 @@ cc 7f024279e0477056e37717e4b886d5016acc9fab8964611b58f86587767bba2e # shrinks to 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) +cc aaec1b83fd17af0121fa9aafac28ffa0d8796c64db2ce90b8f801ae3beedf0a8 # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 64914, tv_nsec: 707953650 }, utc_now: 2024-07-10T08:16:45.739589222Z, client: Host { inner: RefClient { id: ClientId(00000000-0000-0000-0000-000000000000), key: PrivateKey("0000000000000000000000000000000000000000000000000000000000000000"), known_hosts: {}, tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, system_dns_resolvers: [::ffff:232.152.236.170], upstream_dns_resolvers: [], cidr_resources: {}, dns_resources: {}, dns_records: {}, connected_cidr_resources: {}, connected_dns_resources: {}, expected_icmp_handshakes: {}, expected_dns_handshakes: [], gateways_by_resource: {} }, ip4: None, ip6: Some(2001:db80::16), old_ports: {0}, default_port: 53704, allocated_ports: {(53704, V6)} }, gateways: {GatewayId(fb08aaa7-57cc-6914-16cc-c3cd743b23da): Host { inner: RefGateway { key: PrivateKey("98e9a07c61800a1087c32e24aaf88ebf5ec42a9910b1be321088d952471474b6") }, ip4: None, ip6: Some(2001:db80::3d), old_ports: {0}, default_port: 19871, allocated_ports: {(19871, V6)} }, GatewayId(63830e95-2b85-c78b-d229-eef55906817a): Host { inner: RefGateway { key: PrivateKey("76afa947789a491ac61430d3d8cc0f64fd86cd5420dfebbf6f153c0c81b4be46") }, ip4: None, ip6: Some(2001:db80::2e), old_ports: {0}, default_port: 23786, allocated_ports: {(23786, V6)} }, GatewayId(0258d893-afcf-9128-81b5-f6386ac210f3): Host { inner: RefGateway { key: PrivateKey("df75cac4bdb1a6e24a805355315088d3866d856d1887f786e961913325df5756") }, ip4: Some(203.0.113.61), ip6: None, old_ports: {0}, default_port: 39666, allocated_ports: {(39666, V4)} }}, relays: {RelayId(f67ce1ca-c24d-690e-4cfb-7754d12e62e7): Host { inner: 3390697360695300624, ip4: Some(203.0.113.30), ip6: Some(2001:db80::f), old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V4), (3478, V6)} }, RelayId(11d05eed-7ff4-9c0c-c329-ef69c4a40a82): Host { inner: 12081680568791051376, ip4: Some(203.0.113.89), ip6: Some(2001:db80::25), old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V6), (3478, V4)} }}, global_dns_records: {Name(yhnexe.bgjvnx.): {36.103.85.5}, Name(vltos.grx.cjsu.): {38.227.52.252, 44.72.31.169, 127.0.0.1, 11.63.191.131, ::ffff:127.0.0.1}, Name(xsig.trizqe.ggyu.): {d265:b3f5:ec25:56c1:da27:9d2:1841:d455, 109.10.61.83}, Name(cmuv.vzb.hucfjk.): {::ffff:44.137.83.29, 127.0.0.1}, Name(yya.isolt.): {216.124.168.240, 6e47:2748:3fa7:ed8f:bede:128c:85a8:9c26, 106.190.54.36, 60.181.74.79, 134.137.203.109}, Name(fav.jxbyay.): {127.0.0.1, 105.71.73.127}, Name(sadf.necne.kbyd.): {::ffff:141.215.73.93}, Name(cdp.wfyycf.nbv.): {e7f7:b6da:98ed:c26c:c82f:1077:f34d:b2ef, ::ffff:55.83.208.41, ::ffff:185.2.34.85, 107.131.88.121, 127.0.0.1}, Name(gsvdk.qqb.): {81c6:ec85:7a9b:66af:7ac7:7170:2fbd:811a, 168.236.236.206, 209.5.50.60, 252.196.43.239}, Name(vwbmfz.abmz.til.): {::ffff:127.0.0.1, 33.42.150.166, ::ffff:152.125.15.116}, Name(bum.xogdom.): {234.125.21.51, ::ffff:169.44.108.170, ::ffff:207.220.131.144, 127.0.0.1}, Name(bdzhic.sfnhtp.ynnc.): {0.0.0.0, 47.110.12.41, 15ab:d546:df0d:d023:9d:7916:c386:29ee, 251.1.234.83}, Name(wvaue.qfhajk.zxybs.): {7cae:db08:c76c:fd79:ff52:b535:ce91:bb2e, ::ffff:100.207.145.182}}, network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.30, netmask: 32 }), Relay(RelayId(f67ce1ca-c24d-690e-4cfb-7754d12e62e7))), (V4(Ipv4Network { network_address: 203.0.113.61, netmask: 32 }), Gateway(GatewayId(0258d893-afcf-9128-81b5-f6386ac210f3))), (V4(Ipv4Network { network_address: 203.0.113.89, netmask: 32 }), Relay(RelayId(11d05eed-7ff4-9c0c-c329-ef69c4a40a82))), (V6(Ipv6Network { network_address: 2001:db80::f, netmask: 128 }), Relay(RelayId(f67ce1ca-c24d-690e-4cfb-7754d12e62e7))), (V6(Ipv6Network { network_address: 2001:db80::16, netmask: 128 }), Client(ClientId(00000000-0000-0000-0000-000000000000))), (V6(Ipv6Network { network_address: 2001:db80::25, netmask: 128 }), Relay(RelayId(11d05eed-7ff4-9c0c-c329-ef69c4a40a82))), (V6(Ipv6Network { network_address: 2001:db80::2e, netmask: 128 }), Gateway(GatewayId(63830e95-2b85-c78b-d229-eef55906817a))), (V6(Ipv6Network { network_address: 2001:db80::3d, netmask: 128 }), Gateway(GatewayId(fb08aaa7-57cc-6914-16cc-c3cd743b23da)))} } }, [AddCidrResource { resource: ResourceDescriptionCidr { id: ResourceId(00000000-0000-0000-0000-000000000000), address: V6(Ipv6Network { network_address: bb95:e714:62f4:314d:48c8:be35:c8f0:6800, netmask: 126 }), name: "aaaa", address_description: Some("prvqqlr"), sites: [Site { name: "rvfuwu", id: SiteId(db14e076-ed99-6119-96a0-1f8fa1d5c1fe) }, Site { name: "featxrkyau", id: SiteId(f9e7a68b-4552-4488-9931-74572e5baf34) }] }, gateway: GatewayId(63830e95-2b85-c78b-d229-eef55906817a) }, AddDnsResource { resource: ResourceDescriptionDns { id: ResourceId(a093e088-29ba-2f94-3c0f-eff341fa1ca4), address: "?.aouiwu.doy.swn", name: "gqedauemz", address_description: None, sites: [Site { name: "tfky", id: SiteId(d39441ff-40f6-ea3c-5b2f-d8d8dd9e78cc) }, Site { name: "dfgw", id: SiteId(579b65d1-cffc-bac7-a14b-248ee4323413) }] }, records: {Name(gqpqof.aouiwu.doy.swn.): {::ffff:127.0.0.1}}, gateway: GatewayId(fb08aaa7-57cc-6914-16cc-c3cd743b23da) }, SendICMPPacketToCidrResource { src: fd00:2021:1111::, dst: bb95:e714:62f4:314d:48c8:be35:c8f0:6800, seq: 0, identifier: 0 }, SendDnsQuery { domain: Name(gqpqof.aouiwu.doy.swn.), r_type: A, query_id: 33683, dns_server: [::ffff:232.152.236.170]:53 }, SendICMPPacketToCidrResource { src: fd00:2021:1111::, dst: bb95:e714:62f4:314d:48c8:be35:c8f0:6800, seq: 0, identifier: 0 }, SendICMPPacketToDnsResource { src: 100.64.0.1, dst: Name(gqpqof.aouiwu.doy.swn.), seq: 1, identifier: 34217 }, SendICMPPacketToDnsResource { src: 100.64.0.1, dst: Name(gqpqof.aouiwu.doy.swn.), seq: 1, identifier: 13650 }], None) +cc 1a4e8700581d410cd49b6c22f04eca4a826a40cfee42544924780e57337e899f # shrinks to (initial_state, transitions, seen_counter) = (ReferenceState { now: Instant { tv_sec: 3541, tv_nsec: 78137141 }, utc_now: 2024-07-10T23:32:12.144470410Z, client: Host { inner: RefClient { id: ClientId(00354212-307d-1d01-f53e-3b71ae011744), key: PrivateKey("3496f05c0e9e07db52955eb888b381f21523ca10d027856396243ebbec20a12c"), known_hosts: {}, tunnel_ip4: 100.64.0.1, tunnel_ip6: fd00:2021:1111::, system_dns_resolvers: [af9a:14f3:8b43:123e:a328:4ecc:5f6d:da7, ::ffff:201.62.105.47, 0.0.0.0], upstream_dns_resolvers: [], cidr_resources: {}, dns_resources: {}, dns_records: {}, connected_cidr_resources: {}, connected_dns_resources: {}, expected_icmp_handshakes: {}, expected_dns_handshakes: [], gateways_by_resource: {} }, ip4: None, ip6: Some(2001:db80::1f), old_ports: {0}, default_port: 39445, allocated_ports: {(39445, V6)} }, gateways: {GatewayId(74c4c273-1eed-4652-6c72-94cbde5b3465): Host { inner: RefGateway { key: PrivateKey("e6a2e5f76b850a4a635fc69e071289756853d4c17cb11a88006b48ec77ac009a") }, ip4: None, ip6: Some(2001:db80::9), old_ports: {0}, default_port: 16166, allocated_ports: {(16166, V6)} }, GatewayId(3e16c5f5-67cd-7f70-7711-5717e76eb3f6): Host { inner: RefGateway { key: PrivateKey("fe8b67f134387e377b280d51e2846372d9949bccfdc930ccdf636ca8caf7a996") }, ip4: Some(203.0.113.6), ip6: Some(2001:db80::5b), old_ports: {0}, default_port: 57390, allocated_ports: {(57390, V4), (57390, V6)} }, GatewayId(ea803cda-1883-7cdf-3938-56df16c7075c): Host { inner: RefGateway { key: PrivateKey("2406940c57dcb953356566a5da03120fb50b2eeb0f336c1ede1c217249463dc2") }, ip4: None, ip6: Some(2001:db80::e), old_ports: {0}, default_port: 53016, allocated_ports: {(53016, V6)} }}, relays: {RelayId(daf159e7-dd39-a2c4-0f08-133945ec7bc9): Host { inner: 13758148335897264806, ip4: Some(203.0.113.18), ip6: Some(2001:db80::2e), old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V4), (3478, V6)} }, RelayId(8aa7c847-7a12-63de-9957-e43bfe9ced38): Host { inner: 7406554664360166243, ip4: Some(203.0.113.57), ip6: Some(2001:db80::4), old_ports: {0}, default_port: 3478, allocated_ports: {(3478, V6), (3478, V4)} }}, global_dns_records: {}, network: RoutingTable { routes: {(V4(Ipv4Network { network_address: 203.0.113.6, netmask: 32 }), Gateway(GatewayId(3e16c5f5-67cd-7f70-7711-5717e76eb3f6))), (V4(Ipv4Network { network_address: 203.0.113.18, netmask: 32 }), Relay(RelayId(daf159e7-dd39-a2c4-0f08-133945ec7bc9))), (V4(Ipv4Network { network_address: 203.0.113.57, netmask: 32 }), Relay(RelayId(8aa7c847-7a12-63de-9957-e43bfe9ced38))), (V6(Ipv6Network { network_address: 2001:db80::4, netmask: 128 }), Relay(RelayId(8aa7c847-7a12-63de-9957-e43bfe9ced38))), (V6(Ipv6Network { network_address: 2001:db80::9, netmask: 128 }), Gateway(GatewayId(74c4c273-1eed-4652-6c72-94cbde5b3465))), (V6(Ipv6Network { network_address: 2001:db80::e, netmask: 128 }), Gateway(GatewayId(ea803cda-1883-7cdf-3938-56df16c7075c))), (V6(Ipv6Network { network_address: 2001:db80::1f, netmask: 128 }), Client(ClientId(00354212-307d-1d01-f53e-3b71ae011744))), (V6(Ipv6Network { network_address: 2001:db80::2e, netmask: 128 }), Relay(RelayId(daf159e7-dd39-a2c4-0f08-133945ec7bc9))), (V6(Ipv6Network { network_address: 2001:db80::5b, netmask: 128 }), Gateway(GatewayId(3e16c5f5-67cd-7f70-7711-5717e76eb3f6)))} } }, [AddCidrResource { resource: ResourceDescriptionCidr { id: ResourceId(00000000-0000-0000-0000-000000000000), address: V6(Ipv6Network { network_address: ::ffff:79.63.49.20, netmask: 126 }), name: "aaaa", address_description: Some("wrrwwwbgo"), sites: [Site { name: "tivjcpp", id: SiteId(fe3a5fb1-e8aa-2c24-a889-c8df99766828) }, Site { name: "hqpxhwq", id: SiteId(b52d1dd1-1d55-43ab-017d-a22c6b2faa6e) }] }, gateway: GatewayId(ea803cda-1883-7cdf-3938-56df16c7075c) }, AddCidrResource { resource: ResourceDescriptionCidr { id: ResourceId(00000000-0000-0000-0000-000000000000), address: V4(Ipv4Network { network_address: 88.56.242.52, netmask: 31 }), name: "aaaa", address_description: Some("qebdh"), sites: [Site { name: "zfsgu", id: SiteId(43253bc8-020b-c8a9-f007-9bf1770fd1e3) }, Site { name: "txzs", id: SiteId(4ddcace5-6dd0-4f44-8a87-95225a720cb9) }, Site { name: "jbrmtdxl", id: SiteId(3f74264f-bc0f-d8f2-a84f-a4372cabad77) }] }, gateway: GatewayId(3e16c5f5-67cd-7f70-7711-5717e76eb3f6) }, AddDnsResource { resource: ResourceDescriptionDns { id: ResourceId(000006f2-c434-0b37-03f9-9881f69dd6c9), address: "mksyjr.ssc.qpl", name: "yikyt", address_description: Some("ytvn"), sites: [Site { name: "aacr", id: SiteId(7c06cd9e-b3f5-132c-8371-2e1ad3e9187b) }] }, records: {Name(mksyjr.ssc.qpl.): {226.107.181.165, 223.72.146.230, 4542:5f78:650e:9243:db7c:6163:2d7f:ed72, ::ffff:171.171.77.247}}, gateway: GatewayId(3e16c5f5-67cd-7f70-7711-5717e76eb3f6) }, SendICMPPacketToCidrResource { src: fd00:2021:1111::, dst: ::ffff:79.63.49.21, seq: 49603, identifier: 55474 }, SendICMPPacketToCidrResource { src: 100.64.0.1, dst: 88.56.242.52, seq: 25149, identifier: 2250 }, SendICMPPacketToCidrResource { src: 100.64.0.1, dst: 88.56.242.52, seq: 49638, identifier: 37506 }], None) diff --git a/rust/connlib/tunnel/src/tests/assertions.rs b/rust/connlib/tunnel/src/tests/assertions.rs index 20659450a..5b26a3d83 100644 --- a/rust/connlib/tunnel/src/tests/assertions.rs +++ b/rust/connlib/tunnel/src/tests/assertions.rs @@ -3,7 +3,7 @@ use super::{ sim_gateway::SimGateway, }; use crate::tests::reference::ResourceDst; -use connlib_shared::DomainName; +use connlib_shared::{messages::GatewayId, DomainName}; use ip_packet::IpPacket; use pretty_assertions::assert_eq; use std::{ @@ -20,11 +20,15 @@ use std::{ pub(crate) fn assert_icmp_packets_properties( ref_client: &RefClient, sim_client: &SimClient, - sim_gateway: &SimGateway, + sim_gateways: HashMap, global_dns_records: &BTreeMap>, ) { let unexpected_icmp_replies = find_unexpected_entries( - &ref_client.expected_icmp_handshakes, + &ref_client + .expected_icmp_handshakes + .values() + .flatten() + .collect(), &sim_client.received_icmp_replies, |(_, seq_a, id_a), (seq_b, id_b)| seq_a == seq_b && id_a == id_b, ); @@ -34,56 +38,66 @@ pub(crate) fn assert_icmp_packets_properties( "Unexpected ICMP replies on client" ); - assert_eq!( - ref_client.expected_icmp_handshakes.len(), - sim_gateway.received_icmp_requests.len(), - "Unexpected ICMP requests on gateway" - ); + for (id, expected_icmp_handshakes) in ref_client.expected_icmp_handshakes.iter() { + let gateway = sim_gateways.get(id).unwrap(); - tracing::info!(target: "assertions", "✅ Performed the expected {} ICMP handshakes", sim_gateway.received_icmp_requests.len()); + assert_eq!( + expected_icmp_handshakes.len(), + gateway.received_icmp_requests.len(), + "Unexpected ICMP requests on gateway {id}" + ); + + tracing::info!(target: "assertions", "✅ Performed the expected {} ICMP handshakes with gateway {id}", expected_icmp_handshakes.len()); + } let mut mapping = HashMap::new(); - for ((resource_dst, seq, identifier), gateway_received_request) in ref_client - .expected_icmp_handshakes - .iter() - .zip(sim_gateway.received_icmp_requests.iter()) - { - let _guard = tracing::info_span!(target: "assertions", "icmp", %seq, %identifier).entered(); + // Assert properties of the individual ICMP handshakes per gateway. + // Due to connlib's implementation of NAT64, we cannot match the packets sent by the client to the packets arriving at the resource by port or ICMP identifier. + // Thus, we rely on the _order_ here which is why the packets are indexed by gateway in the `RefClient`. + for (gateway, expected_icmp_handshakes) in &ref_client.expected_icmp_handshakes { + let received_icmp_requests = &sim_gateways.get(gateway).unwrap().received_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 = &sim_client - .received_icmp_replies - .get(&(*seq, *identifier)) - .expect("to have ICMP reply on client"); + for ((resource_dst, seq, identifier), gateway_received_request) in + expected_icmp_handshakes.iter().zip(received_icmp_requests) + { + let _guard = + tracing::info_span!(target: "assertions", "icmp", %seq, %identifier).entered(); - assert_correct_src_and_dst_ips(client_sent_request, client_received_reply); + let client_sent_request = &sim_client + .sent_icmp_requests + .get(&(*seq, *identifier)) + .expect("to have ICMP request on client"); + let client_received_reply = &sim_client + .received_icmp_replies + .get(&(*seq, *identifier)) + .expect("to have ICMP reply on client"); - assert_eq!( - gateway_received_request.source(), - ref_client.tunnel_ip_for(gateway_received_request.source()), - "ICMP request on gateway to originate from client" - ); + assert_correct_src_and_dst_ips(client_sent_request, client_received_reply); - match resource_dst { - ResourceDst::Cidr(resource_dst) => { - assert_destination_is_cdir_resource(gateway_received_request, resource_dst) - } - ResourceDst::Dns(domain) => { - assert_destination_is_dns_resource( - gateway_received_request, - global_dns_records, - domain, - ); + assert_eq!( + gateway_received_request.source(), + ref_client.tunnel_ip_for(gateway_received_request.source()), + "ICMP request on gateway to originate from client" + ); - assert_proxy_ip_mapping_is_stable( - client_sent_request, - gateway_received_request, - &mut mapping, - ) + match resource_dst { + ResourceDst::Cidr(resource_dst) => { + assert_destination_is_cdir_resource(gateway_received_request, resource_dst) + } + ResourceDst::Dns(domain) => { + assert_destination_is_dns_resource( + gateway_received_request, + global_dns_records, + domain, + ); + + assert_proxy_ip_mapping_is_stable( + client_sent_request, + gateway_received_request, + &mut mapping, + ) + } } } } diff --git a/rust/connlib/tunnel/src/tests/reference.rs b/rust/connlib/tunnel/src/tests/reference.rs index 8e6714257..acf249269 100644 --- a/rust/connlib/tunnel/src/tests/reference.rs +++ b/rust/connlib/tunnel/src/tests/reference.rs @@ -3,14 +3,18 @@ use super::{ strategies::*, transition::*, }; use chrono::{DateTime, Utc}; -use connlib_shared::{messages::RelayId, proptest::*, DomainName, StaticSecret}; +use connlib_shared::{ + messages::{GatewayId, RelayId}, + proptest::*, + DomainName, StaticSecret, +}; use hickory_proto::rr::RecordType; use prop::collection; use proptest::{prelude::*, sample}; use proptest_state_machine::ReferenceStateMachine; use std::{ collections::{BTreeMap, HashMap, HashSet}, - fmt, + fmt, iter, net::IpAddr, time::{Duration, Instant}, }; @@ -23,7 +27,7 @@ pub(crate) struct ReferenceState { pub(crate) now: Instant, pub(crate) utc_now: DateTime, pub(crate) client: Host, - pub(crate) gateway: Host, + pub(crate) gateways: HashMap>, pub(crate) relays: HashMap>, /// All IP addresses a domain resolves to in our test. @@ -49,29 +53,31 @@ impl ReferenceStateMachine for ReferenceState { type State = Self; type Transition = Transition; - fn init_state() -> proptest::prelude::BoxedStrategy { + fn init_state() -> BoxedStrategy { let mut tunnel_ip4s = tunnel_ip4s(); let mut tunnel_ip6s = tunnel_ip6s(); ( ref_client_host(&mut tunnel_ip4s, &mut tunnel_ip6s), - ref_gateway_host(), - collection::hash_map(relay_id(), relay_prototype(), 2), + collection::hash_map(gateway_id(), ref_gateway_host(), 1..=3), + collection::hash_map(relay_id(), relay_prototype(), 1..=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, global_dns, now, utc_now)| { + |(c, gateways, relays, global_dns, now, utc_now)| { let mut routing_table = RoutingTable::default(); if !routing_table.add_host(c.inner().id, &c) { return None; } - if !routing_table.add_host(g.inner().id, &g) { - return None; - }; + for (id, gateway) in &gateways { + if !routing_table.add_host(*id, gateway) { + return None; + }; + } for (id, relay) in &relays { if !routing_table.add_host(*id, relay) { @@ -79,19 +85,27 @@ impl ReferenceStateMachine for ReferenceState { }; } - Some((c, g, relays, global_dns, now, utc_now, routing_table)) + Some((c, gateways, relays, global_dns, now, utc_now, routing_table)) }, ) .prop_filter( - "client and gateway priv key must be different", - |(c, g, _, _, _, _, _)| c.inner().key != g.inner().key, + "private keys must be unique", + |(c, gateways, _, _, _, _, _)| { + let different_keys = gateways + .iter() + .map(|(_, g)| g.inner().key) + .chain(iter::once(c.inner().key)) + .collect::>(); + + different_keys.len() == gateways.len() + 1 + }, ) .prop_map( - |(client, gateway, relays, global_dns_records, now, utc_now, network)| Self { + |(client, gateways, relays, global_dns_records, now, utc_now, network)| Self { now, utc_now, client, - gateway, + gateways, relays, global_dns_records, network, @@ -104,7 +118,7 @@ impl ReferenceStateMachine for ReferenceState { /// /// This is invoked by proptest repeatedly to explore further state transitions. /// Here, we should only generate [`Transition`]s that make sense for the current state. - fn transitions(state: &Self::State) -> proptest::prelude::BoxedStrategy { + fn transitions(state: &Self::State) -> BoxedStrategy { CompositeStrategy::default() .with( 1, @@ -120,14 +134,19 @@ impl ReferenceStateMachine for ReferenceState { upstream_dns_servers() .prop_map(|servers| Transition::UpdateUpstreamDnsServers { servers }), ) - .with(1, cidr_resource(8).prop_map(Transition::AddCidrResource)) + .with( + 1, + (cidr_resource(8), sample::select(state.all_gateways())).prop_map( + |(resource, gateway)| Transition::AddCidrResource { resource, gateway }, + ), + ) .with(1, roam_client()) .with( 1, prop_oneof![ - non_wildcard_dns_resource(), - star_wildcard_dns_resource(), - question_mark_wildcard_dns_resource(), + non_wildcard_dns_resource(sample::select(state.all_gateways())), + star_wildcard_dns_resource(sample::select(state.all_gateways())), + question_mark_wildcard_dns_resource(sample::select(state.all_gateways())), ], ) .with_if_not_empty( @@ -229,10 +248,13 @@ impl ReferenceStateMachine for ReferenceState { /// Here is where we implement the "expected" logic. fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State { match transition { - Transition::AddCidrResource(r) => { - state - .client - .exec_mut(|client| client.cidr_resources.insert(r.address, r.clone())); + Transition::AddCidrResource { resource, gateway } => { + state.client.exec_mut(|client| { + client + .cidr_resources + .insert(resource.address, resource.clone()); + client.gateways_by_resource.insert(resource.id, *gateway); + }); } Transition::RemoveResource(id) => { state @@ -248,8 +270,12 @@ impl ReferenceStateMachine for ReferenceState { Transition::AddDnsResource { resource: new_resource, records, + gateway, } => { let existing_resource = state.client.exec_mut(|client| { + client + .gateways_by_resource + .insert(new_resource.id, *gateway); client .dns_resources .insert(new_resource.id, new_resource.clone()) @@ -371,28 +397,38 @@ impl ReferenceStateMachine for ReferenceState { /// Any additional checks on whether a particular [`Transition`] can be applied to a certain state. fn preconditions(state: &Self::State, transition: &Self::Transition) -> bool { match transition { - Transition::AddCidrResource(r) => { + Transition::AddCidrResource { resource, gateway } => { + let Some(gateway) = state.gateways.get(gateway) else { + return false; + }; + // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! - if r.address.is_ipv6() && state.gateway.ip6.is_none() { + if resource.address.is_ipv6() && gateway.ip6.is_none() { return false; } - if r.address.is_ipv4() && state.gateway.ip4.is_none() { + if resource.address.is_ipv4() && gateway.ip4.is_none() { return false; } // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! for dns_resolved_ip in state.global_dns_records.values().flat_map(|ip| ip.iter()) { // If the CIDR resource overlaps with an IP that a DNS record resolved to, we have problems ... - if r.address.contains(*dns_resolved_ip) { + if resource.address.contains(*dns_resolved_ip) { return false; } } true } - Transition::AddDnsResource { records, .. } => { + Transition::AddDnsResource { + records, gateway, .. + } => { + if !state.gateways.contains_key(gateway) { + return false; + }; + // TODO: Should we allow adding a DNS resource if we don't have an DNS resolvers? // TODO: For these tests, we assign the resolved IP of a DNS resource as part of this transition. @@ -439,8 +475,18 @@ impl ReferenceStateMachine for ReferenceState { is_valid_icmp_packet && !is_cidr_resource } Transition::SendICMPPacketToCidrResource { - seq, identifier, .. - } => state.client.inner().is_valid_icmp_packet(seq, identifier), + seq, + identifier, + dst, + .. + } => { + let ref_client = state.client.inner(); + + ref_client.is_valid_icmp_packet(seq, identifier) + && ref_client + .gateway_by_cidr_resource_ip(*dst) + .is_some_and(|g| state.gateways.contains_key(&g)) + } Transition::SendICMPPacketToDnsResource { seq, identifier, @@ -448,16 +494,16 @@ impl ReferenceStateMachine for ReferenceState { src, .. } => { - state.client.inner().is_valid_icmp_packet(seq, identifier) - && state - .client - .inner() - .dns_records - .get(dst) - .is_some_and(|r| match src { - IpAddr::V4(_) => r.contains(&RecordType::A), - IpAddr::V6(_) => r.contains(&RecordType::AAAA), - }) + let ref_client = state.client.inner(); + + ref_client.is_valid_icmp_packet(seq, identifier) + && ref_client.dns_records.get(dst).is_some_and(|r| match src { + IpAddr::V4(_) => r.contains(&RecordType::A), + IpAddr::V6(_) => r.contains(&RecordType::AAAA), + }) + && ref_client + .gateway_by_domain_name(dst) + .is_some_and(|g| state.gateways.contains_key(&g)) } Transition::UpdateSystemDnsServers { servers } => { // TODO: PRODUCTION CODE DOES NOT HANDLE THIS! @@ -521,6 +567,10 @@ impl ReferenceState { ) .collect() } + + fn all_gateways(&self) -> Vec { + self.gateways.keys().copied().collect() + } } fn matches_domain(resource_address: &str, domain: &DomainName) -> bool { @@ -539,7 +589,7 @@ pub(crate) fn private_key() -> impl Strategy { any::<[u8; 32]>().prop_map(PrivateKey) } -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub(crate) struct PrivateKey([u8; 32]); impl From for StaticSecret { diff --git a/rust/connlib/tunnel/src/tests/sim_client.rs b/rust/connlib/tunnel/src/tests/sim_client.rs index 69b7e7608..fa8270754 100644 --- a/rust/connlib/tunnel/src/tests/sim_client.rs +++ b/rust/connlib/tunnel/src/tests/sim_client.rs @@ -10,7 +10,7 @@ use bimap::BiMap; use connlib_shared::{ messages::{ client::{ResourceDescriptionCidr, ResourceDescriptionDns}, - ClientId, DnsServer, Interface, ResourceId, + ClientId, DnsServer, GatewayId, Interface, ResourceId, }, proptest::{client_id, domain_name}, DomainName, @@ -83,10 +83,12 @@ impl SimClient { dns_server: SocketAddr, now: Instant, ) -> Option> { - let dns_server = *self - .dns_by_sentinel - .get_by_right(&dns_server) - .expect("to have a sentinel DNS server for the sampled one"); + let Some(dns_server) = self.dns_by_sentinel.get_by_right(&dns_server).copied() else { + tracing::error!(%dns_server, "Unknown DNS server"); + return None; + }; + + tracing::debug!(%dns_server, %domain, "Sending DNS query"); let name = domain_to_hickory_name(domain); @@ -208,6 +210,9 @@ impl SimClient { } /// Reference state for a particular client. +/// +/// The reference state machine is designed to be as abstract as possible over connlib's functionality. +/// For example, we try to model connectivity to _resources_ and don't really care, which gateway is being used to route us there. #[derive(Debug, Clone)] pub struct RefClient { pub(crate) id: ClientId, @@ -239,9 +244,14 @@ pub struct RefClient { pub(crate) connected_dns_resources: HashSet<(ResourceId, DomainName)>, /// The expected ICMP handshakes. - pub(crate) expected_icmp_handshakes: VecDeque<(ResourceDst, IcmpSeq, IcmpIdentifier)>, + /// + /// This is indexed by gateway because our assertions rely on the order of the sent packets. + pub(crate) expected_icmp_handshakes: + HashMap>, /// The expected DNS handshakes. pub(crate) expected_dns_handshakes: VecDeque, + + pub(crate) gateways_by_resource: HashMap, } impl RefClient { @@ -291,9 +301,13 @@ impl RefClient { }; tracing::Span::current().record("resource", tracing::field::display(resource.id)); + let gateway = *self.gateways_by_resource.get(&resource.id).unwrap(); + 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 + .entry(gateway) + .or_default() .push_back((ResourceDst::Cidr(dst), seq, identifier)); return; } @@ -320,6 +334,8 @@ impl RefClient { tracing::Span::current().record("resource", tracing::field::display(resource)); + let gateway = *self.gateways_by_resource.get(&resource).unwrap(); + if self .connected_dns_resources .contains(&(resource, dst.clone())) @@ -327,6 +343,8 @@ impl RefClient { { tracing::debug!("Connected to DNS resource, expecting packet to be routed"); self.expected_icmp_handshakes + .entry(gateway) + .or_default() .push_back((ResourceDst::Dns(dst), seq, identifier)); return; } @@ -404,11 +422,25 @@ impl RefClient { /// 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)| { + self.expected_icmp_handshakes.values().flatten().all( + |(_, existing_seq, existing_identifer)| { existing_seq != seq && existing_identifer != identifier - }) + }, + ) + } + + pub(crate) fn gateway_by_cidr_resource_ip(&self, dst: IpAddr) -> Option { + let resource_id = self.cidr_resource_by_ip(dst)?; + let gateway_id = self.gateways_by_resource.get(&resource_id)?; + + Some(*gateway_id) + } + + pub(crate) fn gateway_by_domain_name(&self, dst: &DomainName) -> Option { + let resource_id = self.dns_resource_by_domain(dst)?; + let gateway_id = self.gateways_by_resource.get(&resource_id)?; + + Some(*gateway_id) } pub(crate) fn resolved_v4_domains(&self) -> Vec { @@ -626,6 +658,7 @@ fn ref_client( connected_dns_resources: Default::default(), expected_icmp_handshakes: Default::default(), expected_dns_handshakes: Default::default(), + gateways_by_resource: Default::default(), }, ) } diff --git a/rust/connlib/tunnel/src/tests/sim_gateway.rs b/rust/connlib/tunnel/src/tests/sim_gateway.rs index a5be49721..c16cc3691 100644 --- a/rust/connlib/tunnel/src/tests/sim_gateway.rs +++ b/rust/connlib/tunnel/src/tests/sim_gateway.rs @@ -3,7 +3,7 @@ use super::{ 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 connlib_shared::DomainName; use ip_packet::IpPacket; use proptest::prelude::*; use snownet::Transmit; @@ -15,7 +15,6 @@ use std::{ /// Simulation state for a particular client. pub(crate) struct SimGateway { - pub(crate) id: GatewayId, pub(crate) sut: GatewayState, pub(crate) received_icmp_requests: VecDeque>, @@ -24,9 +23,8 @@ pub(crate) struct SimGateway { } impl SimGateway { - pub(crate) fn new(id: GatewayId, sut: GatewayState) -> Self { + pub(crate) fn new(sut: GatewayState) -> Self { Self { - id, sut, received_icmp_requests: Default::default(), buffer: vec![0u8; (1 << 16) - 1], @@ -88,7 +86,6 @@ impl SimGateway { /// Reference state for a particular gateway. #[derive(Debug, Clone)] pub struct RefGateway { - pub(crate) id: GatewayId, pub(crate) key: PrivateKey, } @@ -97,7 +94,7 @@ impl RefGateway { /// /// This simulates receiving the `init` message from the portal. pub(crate) fn init(self) -> SimGateway { - SimGateway::new(self.id, GatewayState::new(self.key)) + SimGateway::new(GatewayState::new(self.key)) } } @@ -106,5 +103,5 @@ pub(crate) fn ref_gateway_host() -> impl Strategy> { } fn ref_gateway() -> impl Strategy { - (gateway_id(), private_key()).prop_map(move |(id, key)| RefGateway { id, key }) + private_key().prop_map(move |key| RefGateway { key }) } diff --git a/rust/connlib/tunnel/src/tests/sim_portal.rs b/rust/connlib/tunnel/src/tests/sim_portal.rs index 7c7fd3656..e9fb87c9d 100644 --- a/rust/connlib/tunnel/src/tests/sim_portal.rs +++ b/rust/connlib/tunnel/src/tests/sim_portal.rs @@ -1,22 +1,27 @@ use connlib_shared::messages::{ client::{ResourceDescriptionCidr, ResourceDescriptionDns, SiteId}, - ClientId, GatewayId, ResourceId, + GatewayId, ResourceId, }; use ip_network_table::IpNetworkTable; -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; /// Stub implementation of the portal. /// /// Currently, we only simulate a connection between a single client and a single gateway on a single site. #[derive(Debug, Clone)] pub(crate) struct SimPortal { - _client: ClientId, - gateway: GatewayId, + gateways_by_site: HashMap, // TODO: Technically, this is wrong because a site has multiple gateways but not the other way round. } impl SimPortal { - pub(crate) fn new(_client: ClientId, gateway: GatewayId) -> Self { - Self { _client, gateway } + pub(crate) fn new() -> Self { + Self { + gateways_by_site: HashMap::default(), + } + } + + pub(crate) fn register_site(&mut self, site: SiteId, gateway: GatewayId) { + self.gateways_by_site.insert(site, gateway); } /// Picks, which gateway and site we should connect to for the given resource. @@ -38,11 +43,12 @@ impl SimPortal { .get(&resource) .and_then(|r| Some(r.sites.first()?.id)); - ( - self.gateway, - cidr_site - .or(dns_site) - .expect("resource to be a known CIDR or DNS resource"), - ) + let site_id = cidr_site + .or(dns_site) + .expect("resource to be a known CIDR or DNS resource"); + + let gateway_id = self.gateways_by_site.get(&site_id).unwrap(); + + (*gateway_id, site_id) } } diff --git a/rust/connlib/tunnel/src/tests/sim_relay.rs b/rust/connlib/tunnel/src/tests/sim_relay.rs index 8193c4a42..44a84c057 100644 --- a/rust/connlib/tunnel/src/tests/sim_relay.rs +++ b/rust/connlib/tunnel/src/tests/sim_relay.rs @@ -20,7 +20,7 @@ pub(crate) struct SimRelay { pub(crate) fn map_explode<'a>( relays: impl Iterator)> + 'a, - username: &'static str, + username: &'a str, ) -> impl Iterator + 'a { relays.map(move |(id, r)| { let (socket, username, password, realm) = r.inner().explode( diff --git a/rust/connlib/tunnel/src/tests/sut.rs b/rust/connlib/tunnel/src/tests/sut.rs index 6169218d0..909830443 100644 --- a/rust/connlib/tunnel/src/tests/sut.rs +++ b/rust/connlib/tunnel/src/tests/sut.rs @@ -48,7 +48,7 @@ pub(crate) struct TunnelTest { utc_now: DateTime, pub(crate) client: Host, - pub(crate) gateway: Host, + pub(crate) gateways: HashMap>, relays: HashMap>, portal: SimPortal, @@ -76,10 +76,19 @@ impl StateMachineTest for TunnelTest { let mut client = ref_state .client .map(|ref_client, _, _| ref_client.init(), debug_span!("client")); - let mut gateway = ref_state.gateway.map( - |ref_gateway, _, _| ref_gateway.init(), - debug_span!("gateway"), - ); + + let mut gateways = ref_state + .gateways + .iter() + .map(|(id, gateway)| { + let gateway = gateway.map( + |ref_gateway, _, _| ref_gateway.init(), + debug_span!("gateway", gid = %id), + ); + + (*id, gateway) + }) + .collect::>(); let relays = ref_state .relays @@ -100,7 +109,6 @@ impl StateMachineTest for TunnelTest { (*id, relay) }) .collect::>(); - let portal = SimPortal::new(client.inner().id, gateway.inner().id); // Configure client and gateway with the relays. client.exec_mut(|c| { @@ -110,21 +118,23 @@ impl StateMachineTest for TunnelTest { ref_state.now, ) }); - gateway.exec_mut(|g| { - g.sut.update_relays( - HashSet::default(), - HashSet::from_iter(map_explode(relays.iter(), "gateway")), - ref_state.now, - ) - }); + for (id, gateway) in &mut gateways { + gateway.exec_mut(|g| { + g.sut.update_relays( + HashSet::default(), + HashSet::from_iter(map_explode(relays.iter(), &format!("gateway_{id}"))), + ref_state.now, + ) + }); + } let mut this = Self { now: ref_state.now, utc_now: ref_state.utc_now, network: ref_state.network.clone(), client, - gateway, - portal, + gateways, + portal: SimPortal::new(), logger, relays, }; @@ -147,14 +157,26 @@ impl StateMachineTest for TunnelTest { // Act: Apply the transition match transition { - Transition::AddCidrResource(r) => { + Transition::AddCidrResource { resource, gateway } => { + for site in &resource.sites { + state.portal.register_site(site.id, gateway) + } + state .client - .exec_mut(|c| c.sut.add_resources(&[ResourceDescription::Cidr(r)])); + .exec_mut(|c| c.sut.add_resources(&[ResourceDescription::Cidr(resource)])); + } + Transition::AddDnsResource { + resource, gateway, .. + } => { + for site in &resource.sites { + state.portal.register_site(site.id, gateway) + } + + state + .client + .exec_mut(|c| c.sut.add_resources(&[ResourceDescription::Dns(resource)])) } - Transition::AddDnsResource { resource, .. } => state - .client - .exec_mut(|c| c.sut.add_resources(&[ResourceDescription::Dns(resource)])), Transition::RemoveResource(id) => { state.client.exec_mut(|c| c.sut.remove_resources(&[id])) } @@ -267,13 +289,17 @@ impl StateMachineTest for TunnelTest { ) { let ref_client = ref_state.client.inner(); let sim_client = state.client.inner(); - let sim_gateway = state.gateway.inner(); + let sim_gateways = state + .gateways + .iter() + .map(|(id, g)| (*id, g.inner())) + .collect(); // Assert our properties: Check that our actual state is equivalent to our expectation (the reference state). assert_icmp_packets_properties( ref_client, sim_client, - sim_gateway, + sim_gateways, &ref_state.global_dns_records, ); assert_dns_packets_properties(ref_client, sim_client); @@ -295,7 +321,7 @@ impl TunnelTest { /// /// 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 BufferedTransmits) { - loop { + 'outer: loop { if let Some(transmit) = buffered_transmits.pop() { self.dispatch_transmit(transmit, buffered_transmits, &ref_state.global_dns_records); continue; @@ -325,24 +351,27 @@ impl TunnelTest { } }); - 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(|g| g.sut.poll_event()) { - self.on_gateway_event(self.gateway.inner().id, event); - continue; + for (_, gateway) in self.gateways.iter_mut() { + if let Some(transmit) = gateway.exec_mut(|g| g.sut.poll_transmit()) { + buffered_transmits.push(transmit, gateway); + continue 'outer; + } } - let mut any_relay_advanced = false; + for (id, gateway) in self.gateways.iter_mut() { + let Some(event) = gateway.exec_mut(|g| g.sut.poll_event()) else { + continue; + }; + + on_gateway_event(*id, event, &mut self.client, self.now); + continue 'outer; + } for (_, relay) in self.relays.iter_mut() { let Some(message) = relay.exec_mut(|r| r.sut.next_command()) else { continue; }; - any_relay_advanced = true; - match message { firezone_relay::Command::SendMessage { payload, recipient } => { let dst = recipient.into_socket(); @@ -369,10 +398,8 @@ impl TunnelTest { relay.exec_mut(|r| r.allocations.remove(&(family, port))); } } - } - if any_relay_advanced { - continue; + continue 'outer; } if self.handle_timeout(self.now, self.utc_now) { @@ -399,16 +426,16 @@ impl TunnelTest { self.client.exec_mut(|c| c.sut.handle_timeout(now)); }; - if self - .gateway - .exec_mut(|g| g.sut.poll_timeout()) - .is_some_and(|t| t <= now) - { - any_advanced = true; + for (_, gateway) in self.gateways.iter_mut() { + if gateway + .exec_mut(|g| g.sut.poll_timeout()) + .is_some_and(|t| t <= now) + { + any_advanced = true; - self.gateway - .exec_mut(|g| g.sut.handle_timeout(now, utc_now)) - }; + gateway.exec_mut(|g| g.sut.handle_timeout(now, utc_now)) + }; + } for (_, relay) in self.relays.iter_mut() { if relay @@ -451,15 +478,16 @@ impl TunnelTest { self.client .exec_mut(|c| c.handle_packet(payload, src, dst, self.now)); } - HostId::Gateway(_) => { - let Some(transmit) = self - .gateway + HostId::Gateway(id) => { + let gateway = self.gateways.get_mut(&id).expect("unknown gateway"); + + let Some(transmit) = gateway .exec_mut(|g| g.handle_packet(global_dns_records, payload, src, dst, self.now)) else { return; }; - buffered_transmits.push(transmit, &self.gateway); + buffered_transmits.push(transmit, gateway); } HostId::Relay(id) => { let relay = self.relays.get_mut(&id).expect("unknown relay"); @@ -487,16 +515,30 @@ impl TunnelTest { global_dns_records: &BTreeMap>, ) { match event { - 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::AddedIceCandidates { + candidates, + conn_id, + } => { + let gateway = self.gateways.get_mut(&conn_id).expect("unknown gateway"); + + gateway.exec_mut(|g| { + for candidate in candidates { + g.sut.add_ice_candidate(src, candidate, self.now) + } + }) + } + ClientEvent::RemovedIceCandidates { + candidates, + conn_id, + } => { + let gateway = self.gateways.get_mut(&conn_id).expect("unknown gateway"); + + gateway.exec_mut(|g| { + for candidate in candidates { + g.sut.remove_ice_candidate(src, candidate) + } + }) + } ClientEvent::ConnectionIntent { resource, connected_gateway_ids, @@ -533,8 +575,13 @@ impl TunnelTest { match request { Request::NewConnection(new_connection) => { - let answer = self - .gateway + let Some(gateway) = self.gateways.get_mut(&new_connection.gateway_id) + else { + tracing::error!("Unknown gateway"); + return; + }; + + let answer = gateway .exec_mut(|g| { g.sut.accept( self.client.inner().id, @@ -579,14 +626,19 @@ impl TunnelTest { }, }, resource_id, - self.gateway.inner().sut.public_key(), + gateway.inner().sut.public_key(), self.now, ) }) .unwrap(); } Request::ReuseConnection(reuse_connection) => { - self.gateway + let gateway = self + .gateways + .get_mut(&reuse_connection.gateway_id) + .expect("unknown gateway"); + + gateway .exec_mut(|g| { g.sut.allow_access( resource, @@ -603,6 +655,11 @@ impl TunnelTest { ClientEvent::SendProxyIps { connections } => { for reuse_connection in connections { + let gateway = self + .gateways + .get_mut(&reuse_connection.gateway_id) + .expect("unknown gateway"); + let resolved_ips = reuse_connection .payload .as_ref() @@ -619,7 +676,7 @@ impl TunnelTest { reuse_connection.resource_id, ); - self.gateway + gateway .exec_mut(|g| { g.sut.allow_access( resource, @@ -642,22 +699,6 @@ impl TunnelTest { } } - fn on_gateway_event(&mut self, src: GatewayId, event: GatewayEvent) { - match event { - GatewayEvent::AddedIceCandidates { candidates, .. } => self.client.exec_mut(|c| { - for candidate in candidates { - 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::RefreshDns { .. } => todo!(), - } - } - // TODO: Should we vary the following things via proptests? // - Forwarded DNS query timing out? // - hickory error? @@ -694,6 +735,27 @@ impl TunnelTest { } } +fn on_gateway_event( + src: GatewayId, + event: GatewayEvent, + client: &mut Host, + now: Instant, +) { + match event { + GatewayEvent::AddedIceCandidates { candidates, .. } => client.exec_mut(|c| { + for candidate in candidates { + c.sut.add_ice_candidate(src, candidate, now) + } + }), + GatewayEvent::RemovedIceCandidates { candidates, .. } => client.exec_mut(|c| { + for candidate in candidates { + c.sut.remove_ice_candidate(src, candidate) + } + }), + GatewayEvent::RefreshDns { .. } => todo!(), + } +} + fn map_client_resource_to_gateway_resource( client_cidr_resources: &IpNetworkTable, client_dns_resources: &BTreeMap, diff --git a/rust/connlib/tunnel/src/tests/transition.rs b/rust/connlib/tunnel/src/tests/transition.rs index b532d8363..3316bb40e 100644 --- a/rust/connlib/tunnel/src/tests/transition.rs +++ b/rust/connlib/tunnel/src/tests/transition.rs @@ -5,7 +5,7 @@ use super::{ use connlib_shared::{ messages::{ client::{ResourceDescriptionCidr, ResourceDescriptionDns}, - DnsServer, ResourceId, + DnsServer, GatewayId, ResourceId, }, proptest::*, DomainName, @@ -23,7 +23,10 @@ use std::{ #[allow(clippy::large_enum_variant)] pub(crate) enum Transition { /// Add a new CIDR resource to the client. - AddCidrResource(ResourceDescriptionCidr), + AddCidrResource { + resource: ResourceDescriptionCidr, + gateway: GatewayId, + }, /// Send an ICMP packet to non-resource IP. SendICMPPacketToNonResourceIp { src: IpAddr, @@ -54,6 +57,7 @@ pub(crate) enum Transition { resource: ResourceDescriptionDns, /// The DNS records to add together with the resource. records: HashMap>, + gateway: GatewayId, }, /// Send a DNS query. SendDnsQuery { @@ -178,17 +182,22 @@ where ) } -pub(crate) fn non_wildcard_dns_resource() -> impl Strategy { - (dns_resource(), resolved_ips()).prop_map(|(resource, resolved_ips)| { +pub(crate) fn non_wildcard_dns_resource( + gateway: impl Strategy, +) -> impl Strategy { + (dns_resource(), resolved_ips(), gateway).prop_map(|(resource, resolved_ips, gateway)| { Transition::AddDnsResource { records: HashMap::from([(resource.address.parse().unwrap(), resolved_ips)]), resource, + gateway, } }) } -pub(crate) fn star_wildcard_dns_resource() -> impl Strategy { - dns_resource().prop_flat_map(move |r| { +pub(crate) fn star_wildcard_dns_resource( + gateway: impl Strategy, +) -> impl Strategy { + (dns_resource(), gateway).prop_flat_map(move |(r, gateway)| { let wildcard_address = format!("*.{}", r.address); let records = subdomain_records(r.address, domain_name(1..3)); @@ -197,13 +206,20 @@ pub(crate) fn star_wildcard_dns_resource() -> impl Strategy ..r }); - (resource, records) - .prop_map(|(resource, records)| Transition::AddDnsResource { records, resource }) + (resource, records, Just(gateway)).prop_map(|(resource, records, gateway)| { + Transition::AddDnsResource { + records, + resource, + gateway, + } + }) }) } -pub(crate) fn question_mark_wildcard_dns_resource() -> impl Strategy { - dns_resource().prop_flat_map(move |r| { +pub(crate) fn question_mark_wildcard_dns_resource( + gateway: impl Strategy, +) -> impl Strategy { + (dns_resource(), gateway).prop_flat_map(move |(r, gateway)| { let wildcard_address = format!("?.{}", r.address); let records = subdomain_records(r.address, domain_label()); @@ -212,8 +228,13 @@ pub(crate) fn question_mark_wildcard_dns_resource() -> impl Strategy