diff --git a/.github/sims-patchbay/integration/1to1-nat.toml b/.github/sims-patchbay/integration/1to1-nat.toml new file mode 100644 index 00000000000..432781c570c --- /dev/null +++ b/.github/sims-patchbay/integration/1to1-nat.toml @@ -0,0 +1,58 @@ +# 1-to-1 transfer through NAT with relay. +# Both: no direct connectivity expected. Fetcher/provider: holepunch should succeed. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +variant = ["both", "fetcher", "provider"] + +[matrix.params.variant] +both = { topo = "1to1-nat", expect_direct = "false" } +fetcher = { topo = "1to1-nat-fetcher", expect_direct = "true" } +provider = { topo = "1to1-nat-provider", expect_direct = "true" } + +[sim] +name = "1to1-nat-${matrix.variant}" +topology = "${matrix.topo}" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=20", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] + +[[step]] +when = "${matrix.expect_direct}" +action = "assert" +checks = [ + "fetcher.conn_type contains Ip", +] diff --git a/.github/sims-patchbay/integration/direct-adverse.toml b/.github/sims-patchbay/integration/direct-adverse.toml new file mode 100644 index 00000000000..b377cae95df --- /dev/null +++ b/.github/sims-patchbay/integration/direct-adverse.toml @@ -0,0 +1,47 @@ +# Direct transfer with adverse network conditions. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +cond = ["lossy", "throttled"] + +[matrix.params.cond] +lossy = { latency = "200", rate = "8000", loss = "1.0" } +throttled = { latency = "200", rate = "4000", loss = "0" } + +[sim] +name = "direct-${matrix.cond}" +topology = "1to1" + +[[step]] +action = "set-link-condition" +device = "fetcher" +condition = { latency_ms = "${matrix.latency}", rate_kbit = "${matrix.rate}", loss_pct = "${matrix.loss}" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +args = ["--no-pkarr-publish", "--no-dns-resolve"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=30", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", + "fetcher.conn_type contains Ip", +] diff --git a/.github/sims-patchbay/integration/interface-down-up.toml b/.github/sims-patchbay/integration/interface-down-up.toml new file mode 100644 index 00000000000..d07f99dbf63 --- /dev/null +++ b/.github/sims-patchbay/integration/interface-down-up.toml @@ -0,0 +1,60 @@ +[[extends]] +file = "../iroh-defaults.toml" + +[sim] +name = "interface-down-up" +topology = "1to1-nat-down-up" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=15", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-direct-address", "${provider.direct_addr}"] + +# After 5s, bring interface down +[[step]] +action = "wait" +duration = "5s" + +[[step]] +action = "link-down" +device = "fetcher" +interface = "eth0" + +# After 10s total, bring interface back up +[[step]] +action = "wait" +duration = "5s" + +[[step]] +action = "link-up" +device = "fetcher" +interface = "eth0" + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] diff --git a/.github/sims-patchbay/integration/intg-1to1-relay.toml b/.github/sims-patchbay/integration/intg-1to1-relay.toml new file mode 100644 index 00000000000..b8f17aa5227 --- /dev/null +++ b/.github/sims-patchbay/integration/intg-1to1-relay.toml @@ -0,0 +1,41 @@ +[[extends]] +file = "../iroh-defaults.toml" + +[sim] +name = "intg-1to1-relay" +topology = "1to1" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=20", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", + "fetcher.conn_type contains Ip", +] diff --git a/.github/sims-patchbay/integration/intg-direct.toml b/.github/sims-patchbay/integration/intg-direct.toml new file mode 100644 index 00000000000..131406791bd --- /dev/null +++ b/.github/sims-patchbay/integration/intg-direct.toml @@ -0,0 +1,38 @@ +# Integration test: single-provider direct transfer. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["1to1", "1to3"] + +[sim] +name = "intg-${matrix.topo}-public" +topology = "${matrix.topo}" + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +args = ["--no-pkarr-publish", "--no-dns-resolve"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=10", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", + "fetcher.conn_type contains Ip", +] diff --git a/.github/sims-patchbay/integration/intg-multi-direct.toml b/.github/sims-patchbay/integration/intg-multi-direct.toml new file mode 100644 index 00000000000..855e1218f9e --- /dev/null +++ b/.github/sims-patchbay/integration/intg-multi-direct.toml @@ -0,0 +1,31 @@ +# Integration test: multi-provider direct transfer. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["2to2", "2to4"] + +[sim] +name = "intg-${matrix.topo}-public" +topology = "${matrix.topo}" + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +args = ["--no-pkarr-publish", "--no-dns-resolve"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=10", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" diff --git a/.github/sims-patchbay/integration/intg-relay-dns-relay-only.toml b/.github/sims-patchbay/integration/intg-relay-dns-relay-only.toml new file mode 100644 index 00000000000..58107bd0de2 --- /dev/null +++ b/.github/sims-patchbay/integration/intg-relay-dns-relay-only.toml @@ -0,0 +1,50 @@ +# Integration test: relay-only transfer with full relay + DNS stack. +# Ported from iroh_full.json 1_to_1ro case. + +[[extends]] +file = "../iroh-defaults.toml" + +[sim] +name = "intg-1to1-relay-dns-relay-only" +topology = "1to1" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "dns-setup" +vars = { device = "dns" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready", "dns.ready"] +args = ["--relay-only", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--pkarr-relay-url", "http://$NETSIM_IP_dns:8080/pkarr", + "--dns-origin-domain", "$NETSIM_IP_dns:5300"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--relay-only", + "--size=1G", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--pkarr-relay-url", "http://$NETSIM_IP_dns:8080/pkarr", + "--dns-origin-domain", "$NETSIM_IP_dns:5300"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] diff --git a/.github/sims-patchbay/integration/intg-relay-dns.toml b/.github/sims-patchbay/integration/intg-relay-dns.toml new file mode 100644 index 00000000000..3cc6375728e --- /dev/null +++ b/.github/sims-patchbay/integration/intg-relay-dns.toml @@ -0,0 +1,52 @@ +# Integration test: relay + DNS discovery. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["1to1", "1to3"] + +[sim] +name = "intg-${matrix.topo}-relay-dns" +topology = "${matrix.topo}" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "dns-setup" +vars = { device = "dns" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready", "dns.ready"] +args = ["--relay-url", "https://$NETSIM_IP_relay:3340", + "--pkarr-relay-url", "http://$NETSIM_IP_dns:8080/pkarr", + "--dns-origin-domain", "$NETSIM_IP_dns:5300"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--size=1G", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--pkarr-relay-url", "http://$NETSIM_IP_dns:8080/pkarr", + "--dns-origin-domain", "$NETSIM_IP_dns:5300", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", + "fetcher.conn_type contains Ip", +] diff --git a/.github/sims-patchbay/integration/intg-relay-only.toml b/.github/sims-patchbay/integration/intg-relay-only.toml new file mode 100644 index 00000000000..3b41ff0202b --- /dev/null +++ b/.github/sims-patchbay/integration/intg-relay-only.toml @@ -0,0 +1,46 @@ +# Integration test: relay-only transfer. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["1to1", "1to3"] + +[sim] +name = "intg-${matrix.topo}-relay-only" +topology = "${matrix.topo}" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-only", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--relay-only", + "--size=1G", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] diff --git a/.github/sims-patchbay/integration/nat-adverse.toml b/.github/sims-patchbay/integration/nat-adverse.toml new file mode 100644 index 00000000000..f7e54eb0b84 --- /dev/null +++ b/.github/sims-patchbay/integration/nat-adverse.toml @@ -0,0 +1,54 @@ +# Both peers behind NAT with adverse network conditions. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +cond = ["lossy", "throttled"] + +[matrix.params.cond] +lossy = { latency = "200", rate = "8000", loss = "1.0" } +throttled = { latency = "200", rate = "4000", loss = "0" } + +[sim] +name = "nat-both-${matrix.cond}" +topology = "1to1-nat" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +action = "set-link-condition" +device = "fetcher" +condition = { latency_ms = "${matrix.latency}", rate_kbit = "${matrix.rate}", loss_pct = "${matrix.loss}" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=30", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] diff --git a/.github/sims-patchbay/integration/relay-dns-adverse.toml b/.github/sims-patchbay/integration/relay-dns-adverse.toml new file mode 100644 index 00000000000..e0253a61817 --- /dev/null +++ b/.github/sims-patchbay/integration/relay-dns-adverse.toml @@ -0,0 +1,61 @@ +# Relay+DNS transfer with adverse network conditions. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +cond = ["lossy", "throttled"] + +[matrix.params.cond] +lossy = { latency = "200", rate = "8000", loss = "1.0" } +throttled = { latency = "200", rate = "4000", loss = "0" } + +[sim] +name = "relay-dns-${matrix.cond}" +topology = "1to1" + +[[step]] +action = "set-link-condition" +device = "fetcher" +condition = { latency_ms = "${matrix.latency}", rate_kbit = "${matrix.rate}", loss_pct = "${matrix.loss}" } + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "dns-setup" +vars = { device = "dns" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready", "dns.ready"] +args = ["--relay-url", "https://$NETSIM_IP_relay:3340", + "--pkarr-relay-url", "http://$NETSIM_IP_dns:8080/pkarr", + "--dns-origin-domain", "$NETSIM_IP_dns:5300"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--duration=30", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--pkarr-relay-url", "http://$NETSIM_IP_dns:8080/pkarr", + "--dns-origin-domain", "$NETSIM_IP_dns:5300", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", + "fetcher.conn_type contains Ip", +] diff --git a/.github/sims-patchbay/integration/route-switch.toml b/.github/sims-patchbay/integration/route-switch.toml new file mode 100644 index 00000000000..f9044406a05 --- /dev/null +++ b/.github/sims-patchbay/integration/route-switch.toml @@ -0,0 +1,53 @@ +# Route switch mid-transfer: fetcher switches default route from eth0 to eth1. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +variant = ["multi-nat", "both-multi-nat"] + +[matrix.params.variant] +"multi-nat" = { topo = "1to1-multi-nat" } +"both-multi-nat" = { topo = "1to1-both-multi-nat" } + +[sim] +name = "route-switch-1to1-${matrix.variant}" +topology = "${matrix.topo}" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--duration=15", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-direct-address", "${provider.direct_addr}"] + +# After 5s, switch fetcher's default route from eth0 to eth1 +[[step]] +action = "wait" +duration = "5s" + +[[step]] +action = "set-default-route" +device = "fetcher" +to = "eth1" + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "60s" diff --git a/.github/sims-patchbay/iroh-defaults.toml b/.github/sims-patchbay/iroh-defaults.toml new file mode 100644 index 00000000000..cb4f2a82e93 --- /dev/null +++ b/.github/sims-patchbay/iroh-defaults.toml @@ -0,0 +1,144 @@ +# Shared defaults for iroh patchbay simulations. +# Defines binaries, step templates, and step groups used across all sims. + +# -- Prepare: build binaries from the iroh workspace -- + +[[prepare]] +examples = ["transfer"] +bins = ["iroh-relay", "iroh-dns-server"] +all-features = true + +# -- Binaries -- + +[[binary]] +name = "transfer" +mode = "target" +example = "transfer" + +[[binary]] +name = "relay" +mode = "target" +bin = "iroh-relay" + +[[binary]] +name = "dns-server" +mode = "target" +bin = "iroh-dns-server" + +# -- Relay setup step group -- +# Expands to: gen-certs → gen-file (relay config) → spawn relay. +# Caller supplies: vars.device + +[[step-group]] +name = "relay-setup" + +[[step-group.step]] +action = "gen-certs" +id = "${group.device}-cert" +device = "${group.device}" + +[[step-group.step]] +action = "gen-file" +id = "${group.device}-cfg" +content = """ +enable_relay = true +enable_metrics = true +enable_quic_addr_discovery = true +[tls] +https_bind_addr = "0.0.0.0:3340" +cert_mode = "Manual" +manual_cert_path = "${${group.device}-cert.cert_pem_path}" +manual_key_path = "${${group.device}-cert.key_pem_path}" +""" + +[[step-group.step]] +action = "spawn" +id = "${group.device}" +device = "${group.device}" +cmd = ["${binary.relay}", "--config-path", "${${group.device}-cfg.path}"] +env = { RUST_LOG = "iroh_relay=info" } +[step-group.step.captures.ready] +pipe = "stderr" +regex = "relay: serving on" + +# -- DNS server setup step group -- +# Expands to: gen-file (dns config) → spawn dns server. +# Caller supplies: vars.device + +[[step-group]] +name = "dns-setup" + +[[step-group.step]] +action = "gen-file" +id = "${group.device}-cfg" +content = """ +pkarr_put_rate_limit = "disabled" + +[http] +port = 8080 +bind_addr = "0.0.0.0" + +[https] +port = 8443 +bind_addr = "0.0.0.0" +domains = ["localhost"] +cert_mode = "self_signed" + +[dns] +port = 5300 +bind_addr = "0.0.0.0" +default_soa = "dns1.irohdns.example hostmaster.irohdns.example 0 10800 3600 604800 3600" +default_ttl = 900 +origins = ["irohdns.example.", "."] +rr_a = "0.0.0.0" +rr_ns = "ns1.irohdns.example." + +[mainline] +enabled = false +""" + +[[step-group.step]] +action = "spawn" +id = "${group.device}" +device = "${group.device}" +cmd = ["${binary.dns-server}", "--config", "${${group.device}-cfg.path}"] +env = { RUST_LOG = "iroh_dns_server=info" } +[step-group.step.captures.ready] +pipe = "stderr" +regex = "DNS server listening on" + +# -- Transfer provider template -- + +[[step-template]] +name = "transfer-provider" +action = "spawn" +parser = "ndjson" +cmd = ["${binary.transfer}", "--output", "json", "provide", "--env", "dev"] +env = { RUST_LOG = "iroh=info,iroh::_events=debug" } +[step-template.captures.endpoint_id] +match = { kind = "EndpointBound" } +pick = ".endpoint_id" +[step-template.captures.direct_addr] +match = { kind = "EndpointBound" } +pick = ".direct_addresses.0" + +# -- Transfer fetcher template -- + +[[step-template]] +name = "transfer-fetcher" +action = "spawn" +parser = "ndjson" +cmd = ["${binary.transfer}", "--output", "json", "fetch", "--env", "dev"] +env = { RUST_LOG = "iroh=info,iroh::_events=debug" } +[step-template.captures.size] +match = { kind = "DownloadComplete" } +pick = ".size" +[step-template.captures.duration] +match = { kind = "DownloadComplete" } +pick = ".duration" +[step-template.captures.conn_type] +match = { kind = "ConnectionTypeChanged", status = "Selected" } +pick = ".addr" +[step-template.results] +duration = ".duration" +down_bytes = ".size" diff --git a/.github/sims-patchbay/perf-summary.sh b/.github/sims-patchbay/perf-summary.sh new file mode 100755 index 00000000000..fe712abbec9 --- /dev/null +++ b/.github/sims-patchbay/perf-summary.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Generate a perf summary table from patchbay combined-results.json. +# Output format matches the old chuck netsim report table. +# +# Usage: perf-summary.sh [results-dir] +# results-dir defaults to .patchbay-work/latest + +set -euo pipefail + +RESULTS_DIR="${1:-.patchbay-work/latest}" +COMBINED="$RESULTS_DIR/combined-results.json" + +if [[ ! -f "$COMBINED" ]]; then + echo "No combined-results.json found in $RESULTS_DIR" + exit 0 +fi + +python3 - "$COMBINED" <<'PYEOF' +import json, sys + +with open(sys.argv[1]) as f: + data = json.load(f) + +rows = [] +for run_entry in data.get("runs", []): + sim = run_entry["sim"] + for step in run_entry.get("steps", []): + down_bytes = int(step.get("down_bytes") or 0) + duration_us = int(step.get("duration") or 0) + if duration_us == 0: + continue + elapsed_s = duration_us / 1_000_000 + mb_s = (down_bytes / 1_000_000) / elapsed_s if elapsed_s > 0 else 0 + gbps = (down_bytes * 8 / 1_000_000_000) / elapsed_s if elapsed_s > 0 else 0 + rows.append({ + "sim": sim, + "id": step.get("id", ""), + "down_bytes": down_bytes, + "elapsed_s": elapsed_s, + "mb_s": mb_s, + "gbps": gbps, + }) + +if not rows: + print("No perf results found.") + sys.exit(0) + +# Summary table +print("## Perf Summary\n") +print("| test | throughput (Gbps) | throughput (MB/s) | size (MB) | time (s) |") +print("| ---- | ----------------: | ----------------: | --------: | -------: |") +for r in sorted(rows, key=lambda r: r["sim"]): + size_mb = r["down_bytes"] / 1_000_000 + print(f"| {r['sim']} | {r['gbps']:.2f} | {r['mb_s']:.2f} | {size_mb:.0f} | {r['elapsed_s']:.2f} |") +PYEOF diff --git a/.github/sims-patchbay/perf/iroh-multi-relay-only.toml b/.github/sims-patchbay/perf/iroh-multi-relay-only.toml new file mode 100644 index 00000000000..df544c48360 --- /dev/null +++ b/.github/sims-patchbay/perf/iroh-multi-relay-only.toml @@ -0,0 +1,41 @@ +# Multi-provider relay-only transfer, varying topology. +# Fetchers connect round-robin: fetcher-i -> provider-(i % 2). + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["2to2", "2to4", "2to6", "2to10"] + +[sim] +name = "iroh-${matrix.topo}-relay-only" +topology = "${matrix.topo}" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-only", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--relay-only", + "--size=1G", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" diff --git a/.github/sims-patchbay/perf/iroh-multi.toml b/.github/sims-patchbay/perf/iroh-multi.toml new file mode 100644 index 00000000000..a0eb9de353a --- /dev/null +++ b/.github/sims-patchbay/perf/iroh-multi.toml @@ -0,0 +1,45 @@ +# Multi-provider direct transfer, varying topology and conditions. +# Fetchers connect round-robin: fetcher-i -> provider-(i % 2). + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["2to2", "2to4", "2to6", "2to10"] +cond = ["baseline", "20ms", "200ms", "10g"] + +[matrix.params.cond] +baseline = { size = "1G", latency = "0", rate = "0", impaired = "false" } +"20ms" = { size = "1G", latency = "20", rate = "100000", impaired = "true" } +"200ms" = { size = "100M", latency = "200", rate = "100000", impaired = "true" } +"10g" = { size = "10G", latency = "0", rate = "0", impaired = "false" } + +[sim] +name = "iroh-${matrix.topo}-${matrix.cond}" +topology = "${matrix.topo}" + +[[step]] +when = "${matrix.impaired}" +action = "set-link-condition" +device = "fetcher" +condition = { latency_ms = "${matrix.latency}", rate_kbit = "${matrix.rate}" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +args = ["--no-pkarr-publish", "--no-dns-resolve"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--size=${matrix.size}", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" diff --git a/.github/sims-patchbay/perf/iroh-single-relay-only.toml b/.github/sims-patchbay/perf/iroh-single-relay-only.toml new file mode 100644 index 00000000000..e5890dca0a0 --- /dev/null +++ b/.github/sims-patchbay/perf/iroh-single-relay-only.toml @@ -0,0 +1,46 @@ +# Single-provider relay-only transfer, varying topology. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["1to1", "1to3", "1to5", "1to10"] + +[sim] +name = "iroh-${matrix.topo}-relay-only" +topology = "${matrix.topo}" + +[[step]] +use = "relay-setup" +vars = { device = "relay" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +requires = ["relay.ready"] +args = ["--no-pkarr-publish", "--no-dns-resolve", + "--relay-only", + "--relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--relay-only", + "--size=1G", + "--relay-url", "https://$NETSIM_IP_relay:3340", + "--remote-relay-url", "https://$NETSIM_IP_relay:3340"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] diff --git a/.github/sims-patchbay/perf/iroh-single.toml b/.github/sims-patchbay/perf/iroh-single.toml new file mode 100644 index 00000000000..dbc321ea146 --- /dev/null +++ b/.github/sims-patchbay/perf/iroh-single.toml @@ -0,0 +1,50 @@ +# Single-provider direct transfer, varying topology and conditions. + +[[extends]] +file = "../iroh-defaults.toml" + +[matrix] +topo = ["1to1", "1to3", "1to5", "1to10"] +cond = ["baseline", "20ms", "200ms", "10g"] + +[matrix.params.cond] +baseline = { size = "1G", latency = "0", rate = "0", impaired = "false" } +"20ms" = { size = "1G", latency = "20", rate = "100000", impaired = "true" } +"200ms" = { size = "100M", latency = "200", rate = "100000", impaired = "true" } +"10g" = { size = "10G", latency = "0", rate = "0", impaired = "false" } + +[sim] +name = "iroh-${matrix.topo}-${matrix.cond}" +topology = "${matrix.topo}" + +[[step]] +when = "${matrix.impaired}" +action = "set-link-condition" +device = "fetcher" +condition = { latency_ms = "${matrix.latency}", rate_kbit = "${matrix.rate}" } + +[[step]] +use = "transfer-provider" +id = "provider" +device = "provider" +args = ["--no-pkarr-publish", "--no-dns-resolve"] + +[[step]] +use = "transfer-fetcher" +id = "fetcher" +device = "fetcher" +args = ["${provider.endpoint_id}", + "--no-pkarr-publish", "--no-dns-resolve", + "--size=${matrix.size}", + "--remote-direct-address", "${provider.direct_addr}"] + +[[step]] +action = "wait-for" +id = "fetcher" +timeout = "120s" + +[[step]] +action = "assert" +checks = [ + "fetcher.size matches [0-9]+", +] diff --git a/.github/sims-patchbay/topos/1to1-both-multi-nat.toml b/.github/sims-patchbay/topos/1to1-both-multi-nat.toml new file mode 100644 index 00000000000..953fe5f01fc --- /dev/null +++ b/.github/sims-patchbay/topos/1to1-both-multi-nat.toml @@ -0,0 +1,38 @@ +# Both peers multi-homed behind two NATs each +[[router]] +name = "dc" + +[[router]] +name = "lan-provider-1" +nat = "home" + +[[router]] +name = "lan-provider-2" +nat = "home" + +[[router]] +name = "lan-fetcher-1" +nat = "home" + +[[router]] +name = "lan-fetcher-2" +nat = "home" + +[device.relay.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "lan-provider-1" +impair = { latency_ms = 50, rate_kbit = 10000 } + +[device.provider.eth1] +gateway = "lan-provider-2" +impair = { latency_ms = 50, rate_kbit = 10000 } + +[device.fetcher.eth0] +gateway = "lan-fetcher-1" +impair = { latency_ms = 50, rate_kbit = 10000 } + +[device.fetcher.eth1] +gateway = "lan-fetcher-2" +impair = { latency_ms = 50, rate_kbit = 10000 } diff --git a/.github/sims-patchbay/topos/1to1-multi-nat.toml b/.github/sims-patchbay/topos/1to1-multi-nat.toml new file mode 100644 index 00000000000..a6e112415bf --- /dev/null +++ b/.github/sims-patchbay/topos/1to1-multi-nat.toml @@ -0,0 +1,30 @@ +# Provider behind NAT, fetcher multi-homed behind two NATs +[[router]] +name = "dc" + +[[router]] +name = "lan-provider" +nat = "home" + +[[router]] +name = "lan-fetcher-1" +nat = "home" + +[[router]] +name = "lan-fetcher-2" +nat = "home" + +[device.relay.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "lan-provider" + +# Multi-homed fetcher: two interfaces behind different NATs +[device.fetcher.eth0] +gateway = "lan-fetcher-1" +impair = { latency_ms = 50, rate_kbit = 10000 } + +[device.fetcher.eth1] +gateway = "lan-fetcher-2" +impair = { latency_ms = 50, rate_kbit = 10000 } diff --git a/.github/sims-patchbay/topos/1to1-nat-down-up.toml b/.github/sims-patchbay/topos/1to1-nat-down-up.toml new file mode 100644 index 00000000000..a0a5ea24ec4 --- /dev/null +++ b/.github/sims-patchbay/topos/1to1-nat-down-up.toml @@ -0,0 +1,21 @@ +# Both peers behind NAT with link conditions +[[router]] +name = "dc" + +[[router]] +name = "lan-provider" +nat = "home" + +[[router]] +name = "lan-fetcher" +nat = "home" + +[device.relay.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "lan-provider" + +[device.fetcher.eth0] +gateway = "lan-fetcher" +impair = { latency_ms = 50, rate_kbit = 10000 } diff --git a/.github/sims-patchbay/topos/1to1-nat-fetcher.toml b/.github/sims-patchbay/topos/1to1-nat-fetcher.toml new file mode 100644 index 00000000000..0695aac32b5 --- /dev/null +++ b/.github/sims-patchbay/topos/1to1-nat-fetcher.toml @@ -0,0 +1,15 @@ +[[router]] +name = "dc" + +[[router]] +name = "lan-fetcher" +nat = "home" + +[device.relay.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher.eth0] +gateway = "lan-fetcher" diff --git a/.github/sims-patchbay/topos/1to1-nat-provider.toml b/.github/sims-patchbay/topos/1to1-nat-provider.toml new file mode 100644 index 00000000000..46489a81822 --- /dev/null +++ b/.github/sims-patchbay/topos/1to1-nat-provider.toml @@ -0,0 +1,15 @@ +[[router]] +name = "dc" + +[[router]] +name = "lan-provider" +nat = "home" + +[device.relay.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "lan-provider" + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/1to1-nat.toml b/.github/sims-patchbay/topos/1to1-nat.toml new file mode 100644 index 00000000000..5883211cd3c --- /dev/null +++ b/.github/sims-patchbay/topos/1to1-nat.toml @@ -0,0 +1,19 @@ +[[router]] +name = "dc" + +[[router]] +name = "lan-provider" +nat = "home" + +[[router]] +name = "lan-fetcher" +nat = "home" + +[device.relay.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "lan-provider" + +[device.fetcher.eth0] +gateway = "lan-fetcher" diff --git a/.github/sims-patchbay/topos/1to1.toml b/.github/sims-patchbay/topos/1to1.toml new file mode 100644 index 00000000000..db1daf4c575 --- /dev/null +++ b/.github/sims-patchbay/topos/1to1.toml @@ -0,0 +1,14 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/1to10.toml b/.github/sims-patchbay/topos/1to10.toml new file mode 100644 index 00000000000..d5bd9a5f50b --- /dev/null +++ b/.github/sims-patchbay/topos/1to10.toml @@ -0,0 +1,17 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 10 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/1to3.toml b/.github/sims-patchbay/topos/1to3.toml new file mode 100644 index 00000000000..c1d863e4a00 --- /dev/null +++ b/.github/sims-patchbay/topos/1to3.toml @@ -0,0 +1,17 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 3 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/1to5.toml b/.github/sims-patchbay/topos/1to5.toml new file mode 100644 index 00000000000..b519f785dcb --- /dev/null +++ b/.github/sims-patchbay/topos/1to5.toml @@ -0,0 +1,17 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 5 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/2to10.toml b/.github/sims-patchbay/topos/2to10.toml new file mode 100644 index 00000000000..45c90f6e97d --- /dev/null +++ b/.github/sims-patchbay/topos/2to10.toml @@ -0,0 +1,20 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider] +count = 2 + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 10 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/2to2.toml b/.github/sims-patchbay/topos/2to2.toml new file mode 100644 index 00000000000..5b3f09b3265 --- /dev/null +++ b/.github/sims-patchbay/topos/2to2.toml @@ -0,0 +1,20 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider] +count = 2 + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 2 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/2to4.toml b/.github/sims-patchbay/topos/2to4.toml new file mode 100644 index 00000000000..0edbd1a246e --- /dev/null +++ b/.github/sims-patchbay/topos/2to4.toml @@ -0,0 +1,20 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider] +count = 2 + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 4 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/sims-patchbay/topos/2to6.toml b/.github/sims-patchbay/topos/2to6.toml new file mode 100644 index 00000000000..0312a400bb1 --- /dev/null +++ b/.github/sims-patchbay/topos/2to6.toml @@ -0,0 +1,20 @@ +[[router]] +name = "dc" + +[device.relay.eth0] +gateway = "dc" + +[device.dns.eth0] +gateway = "dc" + +[device.provider] +count = 2 + +[device.provider.eth0] +gateway = "dc" + +[device.fetcher] +count = 6 + +[device.fetcher.eth0] +gateway = "dc" diff --git a/.github/workflows/patchbay-runner.yaml b/.github/workflows/patchbay-runner.yaml new file mode 100644 index 00000000000..2a11615e65b --- /dev/null +++ b/.github/workflows/patchbay-runner.yaml @@ -0,0 +1,99 @@ +name: patchbay-runner + +on: + push: + branches: + - main + - Frando/patchbay-tomls # TODO: remove before merge + workflow_dispatch: + inputs: + branch: + description: "Branch to test" + required: false + type: string + default: "" + group: + description: "Which sim group to run: 'all', 'integration', or 'perf'" + required: false + type: string + default: "all" + patchbay_branch: + description: "Branch of netsim-rs/patchbay to use" + required: false + type: string + default: "main" + +env: + RUST_BACKTRACE: 1 + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + +jobs: + patchbay-runner: + permissions: write-all + name: Patchbay runner + timeout-minutes: 60 + runs-on: [self-hosted, linux, X64] + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Install rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Install sccache + uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Install patchbay + run: | + cargo install --git https://github.com/n0-computer/patchbay \ + --branch ${{ inputs.patchbay_branch || 'main' }} \ + --no-default-features \ + patchbay-runner + + - name: Prepare sims (build binaries) + run: patchbay prepare .github/sims-patchbay/integration .github/sims-patchbay/perf + + - name: Run integration sims + id: integration + if: ${{ inputs.group == 'all' || inputs.group == 'integration' || inputs.group == '' }} + continue-on-error: true + run: patchbay run --no-build -v .github/sims-patchbay/integration + + - name: Run perf sims + id: perf + if: ${{ inputs.group == 'all' || inputs.group == 'perf' || inputs.group == '' }} + continue-on-error: true + run: patchbay run --no-build -v .github/sims-patchbay/perf + + - name: Generate perf summary + if: always() + run: | + .github/sims-patchbay/perf-summary.sh .patchbay-work/latest >> $GITHUB_STEP_SUMMARY + + - name: Upload results + if: always() + uses: actions/upload-artifact@v6 + id: upload-results + with: + name: patchbay-results-${{ github.sha }} + path: .patchbay-work/latest/ + retention-days: 7 + + - name: Show combined results + if: always() + run: | + if [[ -f .patchbay-work/latest/combined-results.md ]]; then + cat .patchbay-work/latest/combined-results.md >> $GITHUB_STEP_SUMMARY + fi + + - name: Fail if sims failed + if: ${{ steps.integration.outcome == 'failure' || steps.perf.outcome == 'failure' }} + run: | + echo "One or more simulations failed." + if [[ -f .patchbay-work/latest/manifest.json ]]; then + cat .patchbay-work/latest/manifest.json + fi + exit 1 diff --git a/.github/workflows/patchbay.yml b/.github/workflows/patchbay.yml new file mode 100644 index 00000000000..e9cec5b5852 --- /dev/null +++ b/.github/workflows/patchbay.yml @@ -0,0 +1,109 @@ +name: Patchbay Tests + +on: + pull_request: + push: + branches: + - main + - Frando/netsim + +concurrency: + group: patchbay-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Dwarnings --cfg patchbay_tests" + SCCACHE_CACHE_SIZE: "10G" + IROH_FORCE_STAGING_RELAYS: "1" + +jobs: + patchbay_tests: + name: Patchbay Tests + timeout-minutes: 45 + runs-on: [self-hosted, linux, X64] + env: + RUSTC_WRAPPER: "sccache" + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - uses: mozilla-actions/sccache-action@v0.0.9 + + - name: Run patchbay tests + id: tests + run: cargo test --release -p iroh --test patchbay -- --test-threads=1 + env: + RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }} + + - name: Push results + if: always() + env: + PATCHBAY_URL: https://frando.gateway.lol + PATCHBAY_API_KEY: ${{ secrets.PATCHBAY_API_KEY }} + TEST_STATUS: ${{ steps.tests.outcome }} + run: | + set -euo pipefail + PROJECT="${{ github.event.repository.name }}" + TESTDIR="$(cargo metadata --format-version=1 --no-deps | jq -r .target_directory)/testdir-current" + [ ! -d "$TESTDIR" ] && echo "No testdir output, skipping" && exit 0 + + cat > "$TESTDIR/run.json" <> "$GITHUB_ENV" + echo "PATCHBAY_TEST_STATUS=$TEST_STATUS" >> "$GITHUB_ENV" + echo "Results: $VIEW_URL" + + - name: Comment on PR + if: always() && env.PATCHBAY_VIEW_URL + uses: actions/github-script@v7 + with: + script: | + // Find PR number: from event or by looking up open PRs for the branch + let prNumber = context.issue?.number; + if (!prNumber) { + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, repo: context.repo.repo, + head: `${context.repo.owner}:${{ github.ref_name }}`, + state: 'open', + }); + if (!prs.length) return; + prNumber = prs[0].number; + } + + const status = process.env.PATCHBAY_TEST_STATUS; + const icon = status === 'success' ? '\u2705' : '\u274c'; + const marker = ''; + const body = `${marker}\n${icon} **patchbay:** ${status} | ${process.env.PATCHBAY_VIEW_URL}`; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, + }); + const existing = comments.find(c => c.body.includes(marker)); + const params = { owner: context.repo.owner, repo: context.repo.repo }; + if (existing) { + await github.rest.issues.updateComment({ ...params, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ ...params, issue_number: prNumber, body }); + } diff --git a/Cargo.lock b/Cargo.lock index e442c188543..814e0fb0dff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ dependencies = [ "tracing", ] +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -147,15 +162,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "9ded5f9a03ac8f24d1b8a25101ee812cd32cdc8c50a4c50237de2c4915850e73" dependencies = [ "rustversion", ] @@ -376,6 +391,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base32" version = "0.5.1" @@ -420,9 +450,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blake3" @@ -458,9 +488,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "byteorder" @@ -474,6 +504,37 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cast" version = "0.3.0" @@ -482,9 +543,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -510,9 +571,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", "js-sys", @@ -561,9 +622,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -571,9 +632,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -595,9 +656,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "cobs" @@ -816,6 +877,22 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "ctr" version = "0.9.2" @@ -946,9 +1023,9 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" -version = "0.8.0" +version = "0.8.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" dependencies = [ "const-oid", "pem-rfc7468", @@ -971,9 +1048,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -1078,9 +1155,9 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags", "block2", @@ -1119,6 +1196,21 @@ dependencies = [ "litrs", ] +[[package]] +name = "dtor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404d02eeb088a82cfd873006cb713fe411306c7d182c344905e101fb1167d301" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "dyn-clone" version = "1.0.20" @@ -1221,7 +1313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1271,12 +1363,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1304,9 +1390,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", "tokio", @@ -1314,9 +1400,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -1329,9 +1415,9 @@ dependencies = [ [[package]] name = "futures-buffered" -version = "0.2.13" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +checksum = "a8e0e1f38ec07ba4abbde21eed377082f17ccb988be9d988a5adbf4bafc118fd" dependencies = [ "cordyceps", "diatomic-waker", @@ -1342,9 +1428,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -1352,15 +1438,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -1369,9 +1455,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -1388,9 +1474,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -1399,15 +1485,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -1417,9 +1503,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -1429,6 +1515,7 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", + "pin-utils", "slab", ] @@ -1479,24 +1566,11 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "ghash" version = "0.5.1" @@ -1507,6 +1581,12 @@ dependencies = [ "polyval", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "gloo-timers" version = "0.3.0" @@ -1587,15 +1667,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1604,7 +1675,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1783,9 +1854,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1" +checksum = "e1b229d73f5803b562cc26e4da0396c8610a4ee209f4fac8fa4f8d709166dc45" dependencies = [ "typenum", ] @@ -1847,7 +1918,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.3", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -1958,12 +2029,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2026,15 +2091,13 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", - "serde", - "serde_core", ] [[package]] name = "indicatif" -version = "0.18.4" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", @@ -2067,9 +2130,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" dependencies = [ "serde", ] @@ -2096,6 +2159,7 @@ dependencies = [ "clap", "console", "console_error_panic_hook", + "ctor", "data-encoding", "derive_more", "ed25519-dalek", @@ -2118,6 +2182,7 @@ dependencies = [ "noq-udp", "papaya", "parse-size", + "patchbay", "pin-project", "pkarr", "pkcs8", @@ -2134,10 +2199,12 @@ dependencies = [ "rustls-webpki", "serde", "serde_json", + "serial_test", "smallvec", - "strum", + "strum 0.27.2", "swarm-discovery", "sync_wrapper", + "testdir", "time", "tokio", "tokio-stream", @@ -2232,7 +2299,7 @@ dependencies = [ "serde", "serde_json", "struct_iterable", - "strum", + "strum 0.27.2", "tempfile", "tokio", "tokio-rustls", @@ -2252,9 +2319,9 @@ dependencies = [ [[package]] name = "iroh-metrics" -version = "0.38.3" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761b45ba046134b11eb3e432fa501616b45c4bf3a30c21717578bc07aa6461dd" +checksum = "c946095f060e6e59b9ff30cc26c75cdb758e7fb0cde8312c89e2144654989fcb" dependencies = [ "http-body-util", "hyper", @@ -2262,7 +2329,6 @@ dependencies = [ "iroh-metrics-derive", "itoa", "n0-error", - "portable-atomic", "postcard", "reqwest", "ryu", @@ -2329,7 +2395,7 @@ dependencies = [ "serde_json", "sha1", "simdutf8", - "strum", + "strum 0.27.2", "time", "tokio", "tokio-rustls", @@ -2405,17 +2471,11 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.183" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libm" @@ -2425,11 +2485,13 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ + "bitflags", "libc", + "redox_syscall 0.7.3", ] [[package]] @@ -2440,9 +2502,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2544,9 +2606,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -2570,6 +2632,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.1.1" @@ -2583,9 +2654,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.14" +version = "0.12.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -2604,6 +2675,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" dependencies = [ + "anyhow", "n0-error-macros", "spez", ] @@ -2650,7 +2722,7 @@ dependencies = [ "serde_json", "serde_with", "smallvec", - "strum", + "strum 0.27.2", ] [[package]] @@ -2698,7 +2770,7 @@ dependencies = [ "libc", "mac-addr", "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-route 0.29.0", "netlink-sys", "objc2-core-foundation", "objc2-system-configuration", @@ -2716,6 +2788,18 @@ dependencies = [ "paste", ] +[[package]] +name = "netlink-packet-route" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + [[package]] name = "netlink-packet-route" version = "0.29.0" @@ -2772,7 +2856,7 @@ dependencies = [ "n0-watcher", "netdev", "netlink-packet-core", - "netlink-packet-route", + "netlink-packet-route 0.29.0", "netlink-proto", "netlink-sys", "noq-udp", @@ -2780,7 +2864,7 @@ dependencies = [ "objc2-system-configuration", "pin-project-lite", "serde", - "socket2 0.6.3", + "socket2 0.6.2", "time", "tokio", "tokio-util", @@ -2791,6 +2875,18 @@ dependencies = [ "wmi", ] +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -2826,7 +2922,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tokio-stream", @@ -2871,11 +2967,20 @@ checksum = "bb9be4fedd6b98f3ba82ccd3506f4d0219fb723c3f97c67e12fe1494aa020e44" dependencies = [ "cfg_aliases", "libc", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", "windows-sys 0.61.2", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "ntimestamp" version = "1.0.0" @@ -2897,7 +3002,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2968,9 +3073,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" dependencies = [ "objc2-encode", ] @@ -3019,6 +3124,15 @@ dependencies = [ "objc2-security", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "oid-registry" version = "0.8.1" @@ -3096,7 +3210,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -3113,6 +3227,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "patchbay" +version = "0.1.0" +source = "git+https://github.com/n0-computer/patchbay.git?branch=feat%2Fserver-push#729bc67bbb9df8a95c0e690c4476eb32c3cd5203" +dependencies = [ + "anyhow", + "chrono", + "derive_more", + "futures", + "ipnet", + "libc", + "nix", + "rtnetlink", + "serde", + "serde_json", + "strum 0.28.0", + "tokio", + "tokio-util", + "toml", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "pem" version = "3.0.6" @@ -3150,18 +3288,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.11" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.11" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", @@ -3170,9 +3308,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -3214,9 +3352,9 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.11.0-rc.11" +version = "0.11.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +checksum = "b226d2cc389763951db8869584fd800cbbe2962bf454e2edeb5172b31ee99774" dependencies = [ "der", "spki", @@ -3280,9 +3418,6 @@ name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" -dependencies = [ - "serde", -] [[package]] name = "portmapper" @@ -3305,7 +3440,7 @@ dependencies = [ "rand", "serde", "smallvec", - "socket2 0.6.3", + "socket2 0.6.2", "time", "tokio", "tokio-util", @@ -3383,21 +3518,11 @@ dependencies = [ "yansi", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "proc-macro-crate" -version = "3.5.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -3473,7 +3598,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.3", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -3482,9 +3607,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3510,16 +3635,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.3", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] name = "quote" -version = "1.0.45" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -3530,12 +3655,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.9.2" @@ -3619,9 +3738,9 @@ dependencies = [ [[package]] name = "redb" -version = "3.1.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef99362319c782aa4639ad3a306b64c3bb90e12874e99b8df124cb679d988611" +checksum = "ae323eb086579a3769daa2c753bb96deb95993c534711e0dbe881b5192906a06" dependencies = [ "libc", ] @@ -3635,6 +3754,15 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + [[package]] name = "redox_users" version = "0.4.6" @@ -3671,9 +3799,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reloadable-core" @@ -3754,6 +3882,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rtnetlink" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b960d5d873a75b5be9761b1e73b146f52dddcd27bac75263f40fba686d4d7b5" +dependencies = [ + "futures-channel", + "futures-util", + "log", + "netlink-packet-core", + "netlink-packet-route 0.28.0", + "netlink-proto", + "netlink-sys", + "nix", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -3780,22 +3932,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" dependencies = [ "log", "once_cell", @@ -3880,7 +4032,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3920,9 +4072,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "same-file" @@ -3933,11 +4085,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] @@ -3954,11 +4115,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "security-framework" -version = "3.7.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags", "core-foundation", @@ -3969,9 +4136,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.17.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3984,7 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3998,6 +4165,10 @@ name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] [[package]] name = "send_wrapper" @@ -4112,9 +4283,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "serde_core", "serde_with_macros", @@ -4122,9 +4293,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -4132,6 +4303,32 @@ dependencies = [ "syn", ] +[[package]] +name = "serial_test" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +dependencies = [ + "futures-executor", + "futures-util", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.11.0-rc.4" @@ -4245,12 +4442,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4351,7 +4548,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -4366,6 +4572,18 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4381,7 +4599,7 @@ dependencies = [ "acto", "hickory-proto", "rand", - "socket2 0.6.3", + "socket2 0.6.2", "thiserror 2.0.18", "tokio", "tracing", @@ -4389,9 +4607,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -4418,6 +4636,20 @@ dependencies = [ "syn", ] +[[package]] +name = "sysinfo" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "winapi", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -4426,15 +4658,30 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.26.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", +] + +[[package]] +name = "testdir" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9ffa013be124f7e8e648876190de818e3a87088ed97ccd414a398b403aec8c8" +dependencies = [ + "anyhow", + "backtrace", + "cargo-platform", + "cargo_metadata", + "once_cell", + "sysinfo", + "whoami", ] [[package]] @@ -4557,9 +4804,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", @@ -4567,16 +4814,16 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.3", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -4679,12 +4926,21 @@ dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", "toml_writer", "winnow", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -4696,12 +4952,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] @@ -4828,6 +5084,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" @@ -4838,6 +5104,8 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", @@ -4845,6 +5113,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -4876,9 +5145,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -4947,11 +5216,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -5060,13 +5329,10 @@ dependencies = [ ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "wasite" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" @@ -5166,28 +5432,6 @@ version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -5212,18 +5456,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" version = "0.3.91" @@ -5262,6 +5494,17 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.1" @@ -5290,7 +5533,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5708,9 +5951,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.15" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -5730,94 +5973,12 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] [[package]] name = "wmi" -version = "0.18.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003e65f4934cf9449b9ce913ad822cd054a5af669d24f93db101fdb02856bb23" +checksum = "746791db82f029aaefc774ccbb8e61306edba18ef2c8998337cadccc0b8067f7" dependencies = [ "chrono", "futures", @@ -5932,18 +6093,18 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" [[package]] name = "zerocopy" -version = "0.8.42" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.42" +version = "0.8.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" dependencies = [ "proc-macro2", "quote", @@ -6026,6 +6187,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index 0d0681861f5..47e169874f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ missing_debug_implementations = "warn" # do. To enable for a crate set `#![cfg_attr(iroh_docsrs, # feature(doc_cfg))]` in the crate. # We also have our own `iroh_loom` cfg to enable tokio-rs/loom testing. -unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)", "cfg(iroh_loom)"] } +unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)", "cfg(iroh_loom)", "cfg(patchbay_tests)"] } [workspace.lints.clippy] unused-async = "warn" diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index b5b112730eb..b2de6dfac81 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -102,10 +102,12 @@ getrandom = { version = "0.3.2", features = ["wasm_js"] } # target-common test/dev dependencies [dev-dependencies] console_error_panic_hook = "0.1" +n0-error = { version = "0.1", features = ["anyhow"] } postcard = { version = "1.1.1", features = ["use-std"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } rand_chacha = "0.9" chrono = "0.4.43" +serial_test = "3.4.0" # *non*-wasm-in-browser test/dev dependencies [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dev-dependencies] @@ -128,6 +130,7 @@ n0-tracing-test = "0.3" clap = { version = "4", features = ["derive"] } tracing-subscriber = { version = "0.3", features = [ "env-filter", + "json", ] } indicatif = { version = "0.18", features = ["tokio"] } parse-size = { version = "1.1.0", features = ['std'] } @@ -139,6 +142,12 @@ console = { version = "0.16" } wasm-tracing = "2.1.0" wasm-bindgen-test = "0.3.62" +# patchbay netsim test dependencies (linux only) +[target.'cfg(target_os = "linux")'.dev-dependencies] +ctor = "0.6" +patchbay = { git = "https://github.com/n0-computer/patchbay.git", branch = "feat/server-push" } +testdir = "0.9" + [build-dependencies] cfg_aliases = { version = "0.2.1" } diff --git a/iroh/src/socket/remote_map/remote_state.rs b/iroh/src/socket/remote_map/remote_state.rs index 1ae4bc97961..a124ed70f36 100644 --- a/iroh/src/socket/remote_map/remote_state.rs +++ b/iroh/src/socket/remote_map/remote_state.rs @@ -529,6 +529,19 @@ impl RemoteStateActor { self.open_path(&remote); } } + + // Try paths that are still unknown so they can be evaluated quickly. + // This closes the timing window where only the initially selected path + // may be evaluated before additional address discovery happens. + let unknown_addrs = self + .paths + .unknown_paths() + .map(|(addr, _state)| addr) + .cloned() + .collect::>(); + for addr in unknown_addrs { + self.open_path(&addr); + } } self.trigger_holepunching(); } diff --git a/iroh/src/socket/remote_map/remote_state/path_state.rs b/iroh/src/socket/remote_map/remote_state/path_state.rs index 6460449dc35..e5672f8e69a 100644 --- a/iroh/src/socket/remote_map/remote_state/path_state.rs +++ b/iroh/src/socket/remote_map/remote_state/path_state.rs @@ -173,6 +173,17 @@ impl RemotePathState { self.paths.keys() } + /// Returns an iterator over addresses with Unknown path status, along with their path state. + /// + /// This exposes both the address and the path state metadata (including source information), + /// allowing callers to make decisions based on where the address came from. + pub(super) fn unknown_paths(&self) -> impl Iterator { + self.paths + .iter() + .filter(|entry| matches!(entry.1.status, PathStatus::Unknown)) + .map(|(addr, state)| (addr, state)) + } + /// Returns whether this stores any addresses. pub(super) fn is_empty(&self) -> bool { self.paths.is_empty() @@ -625,4 +636,33 @@ mod tests { assert_eq!(metrics.transport_ip_paths_added.get(), 2); assert_eq!(metrics.transport_ip_paths_removed.get(), 1); } + + #[test] + fn test_unknown_paths_exposes_sources_and_filters_status() { + let mut state = RemotePathState::new(Default::default()); + + let addr1 = ip_addr(1001); + let addr2 = ip_addr(1002); + let addr3 = ip_addr(1003); + + // Insert addr1 as open and addr2 as unknown + state.insert_open_path(addr1.clone(), Source::Udp); + state.insert_multiple([addr2.clone(), addr3.clone()].into_iter(), Source::Relay); + + // Now make addr3 open + state.insert_open_path(addr3.clone(), Source::Udp); + + // Get unknown paths + let unknown: Vec<_> = state.unknown_paths().collect(); + + // Should only have addr2 (Unknown status) + assert_eq!(unknown.len(), 1); + let (addr, path_state) = unknown[0]; + assert_eq!(addr, &addr2); + assert!(matches!(path_state.status, PathStatus::Unknown)); + + // Verify we can access source metadata + assert!(!path_state.sources.is_empty()); + assert!(path_state.sources.contains_key(&Source::Relay)); + } } diff --git a/iroh/tests/patchbay.rs b/iroh/tests/patchbay.rs new file mode 100644 index 00000000000..3803cc79757 --- /dev/null +++ b/iroh/tests/patchbay.rs @@ -0,0 +1,1168 @@ +//! Patchbay network simulation tests. +//! +//! These tests use the [`patchbay`] crate to create virtual network topologies +//! in Linux user namespaces, testing iroh's NAT traversal, holepunching, +//! and connectivity under various network conditions. +//! +//! These tests are disabled by default and only run when the `patchbay_tests` cfg is enabled. +//! They require Linux with user namespace support. On non-Linux systems, you can use +//! `patchbay-vm` to get a Linux VM with the required capabilities. See patchbay docs +//! for details. +//! +//! To run: +//! +//! ```sh +//! # On Linux (with user namespace support): +//! RUSTFLAGS="--cfg patchbay_tests" cargo test --release -p iroh --test patchbay -- --test-threads=1 +//! +//! # On macOS (via patchbay-vm): +//! RUSTFLAGS="--cfg patchbay_tests" patchbay-vm test --release -p iroh --test patchbay -- --test-threads=1 +//! ``` + +// patchbay only runs on linux +#![cfg(target_os = "linux")] +// Only compile these tests when the patchbay_tests cfg is enabled. +#![cfg(patchbay_tests)] + +use std::time::Duration; + +use iroh::{Endpoint, EndpointAddr, RelayMode, TransportAddr, Watcher}; +use n0_error::{Result, StackResultExt, ensure_any}; +use n0_tracing_test::traced_test; +use patchbay::{Firewall, LinkCondition, LinkLimits, Nat, RouterPreset, TestGuard}; +use testdir::testdir; +use tokio::sync::oneshot; +use tracing::{debug, info, warn}; + +use self::util::{Pair, PathWatcherExt, lab_with_relay, ping_accept, ping_open}; + +#[path = "patchbay/util.rs"] +mod util; + +/// Init the user namespace before any threads are spawned. +/// +/// This gives us all permissions we need for the patchbay tests. +#[ctor::ctor] +fn userns_ctor() { + patchbay::init_userns().expect("failed to init userns"); +} + +// --- +// Holepunch tests +// --- + +/// Simple holepunch: Two devices behind destination-independent NATs, +/// establish via relay, upgrade to direct. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn holepunch_simple() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, _conn| Ok(()), + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "connection started relayed"); + paths.wait_ip(timeout).await?; + info!("connection became direct"); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Two nodes connected over two independent LAN links. +/// +/// This uses relay-disabled endpoints and an explicit EndpointAddr with two IP addrs, +/// so the connection setup does not depend on public-IP probing. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn two_ip_paths_without_public_ip_probing() -> Result { + let mut opts = patchbay::LabOpts::default().outdir(patchbay::OutDir::Exact(testdir!())); + if let Some(name) = std::thread::current().name() { + opts = opts.label(name); + } + let lab = patchbay::Lab::with_opts(opts).await?; + let guard = lab.test_guard(); + + let lan1 = lab.add_router("lan1").build().await?; + let lan2 = lab.add_router("lan2").build().await?; + + let dev1 = lab + .add_device("dev1") + .iface("eth0", lan1.id(), None) + .iface("eth1", lan2.id(), None) + .build() + .await?; + let dev2 = lab + .add_device("dev2") + .iface("eth0", lan1.id(), None) + .iface("eth1", lan2.id(), None) + .build() + .await?; + + const ALPN: &[u8] = b"patchbay-two-ip"; + let timeout = Duration::from_secs(10); + let (addr_tx, addr_rx) = oneshot::channel(); + + let task1 = dev1.spawn(move |_dev| async move { + let ep = Endpoint::empty_builder(RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await?; + + let server_addr = ep.addr(); + let direct_addrs = server_addr.ip_addrs().cloned().collect::>(); + ensure_any!( + direct_addrs.len() >= 2, + "expected at least 2 direct addrs, got {:?}", + direct_addrs + ); + addr_tx.send(server_addr).ok(); + + let conn = ep.accept().await.unwrap().accept().anyerr()?.await?; + let mut paths = conn.paths(); + tokio::time::timeout(timeout, async { + loop { + let ip_paths = paths.get().iter().filter(|p| p.is_ip()).count(); + if ip_paths >= 2 { + break; + } + paths.updated().await?; + } + n0_error::Ok(()) + }) + .await + .anyerr()??; + + conn.close(0u32.into(), b""); + ep.close().await; + n0_error::Ok(()) + })?; + + let task2 = dev2.spawn(move |_dev| async move { + let ep = Endpoint::empty_builder(RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await?; + + let server_addr = addr_rx.await.anyerr()?; + let direct_addrs = server_addr.ip_addrs().cloned().collect::>(); + ensure_any!( + direct_addrs.len() >= 2, + "expected at least 2 direct addrs from server, got {:?}", + direct_addrs + ); + + let dst = EndpointAddr::new(server_addr.id) + .with_ip_addr(direct_addrs[0]) + .with_ip_addr(direct_addrs[1]); + let conn = ep.connect(dst, ALPN).await?; + + let mut paths = conn.paths(); + tokio::time::timeout(timeout, async { + loop { + let ip_paths = paths.get().iter().filter(|p| p.is_ip()).count(); + if ip_paths >= 2 { + break; + } + paths.updated().await?; + } + n0_error::Ok(()) + }) + .await + .anyerr()??; + + conn.close(0u32.into(), b""); + ep.close().await; + n0_error::Ok(()) + })?; + + task2.await.anyerr()??; + task1.await.anyerr()??; + guard.ok(); + Ok(()) +} + +/// Tests that changing the uplink of an interface works (i.e. switching wifis). +/// +/// For this we observe a change in the selected path's remote addr on the *other* side. +/// Whether the side that changes interfaces opens a new path or does an RFC9000-style migration +/// is an implementation detail which we won't test for. +/// +/// The test currently fails, but should pass. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +#[ignore = "known to still fail"] +async fn switch_uplink() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let nat3 = lab.add_router("nat3").nat(Nat::Home).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "connection started relayed"); + + // Wait until a first direct path is established. + let first = paths.wait_ip(timeout).await?; + info!(addr=?first.remote_addr(), "connection became direct, waiting for path change"); + + // Now wait until the direct path changes, which happens after the other endpoint + // changes its uplink. We check is_ip() explicitly to avoid triggering on a + // transient relay fallback during the network switch. + let second = paths + .wait_selected(timeout, |p| { + p.is_ip() && p.remote_addr() != first.remote_addr() + }) + .await + .context("did not switch paths")?; + info!(addr=?second.remote_addr(), "connection changed path, wait for ping"); + + ping_accept(&conn, timeout).await?; + info!("ping done"); + Ok(()) + }, + async move |dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "connection started relayed"); + + // Wait for conn to become direct. + paths + .wait_ip(timeout) + .await + .context("become direct")?; + + // Wait a little more and then switch wifis. + tokio::time::sleep(Duration::from_secs(1)).await; + info!("switch IP uplink"); + dev.replug_iface("eth0", nat3.id()).await?; + + // We don't assert any path changes here, because the remote stays identical, + // and PathInfo does not contain info on local addrs. Instead, the remote + // only accepts our ping after the path changed. + info!("send ping"); + ping_open(&conn, timeout) + .await + .context("failed at ping_open")?; + info!("ping done"); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Tests that changing the uplink from IPv4 to IPv6 works. +/// +/// Similar to `switch_uplink` but switches to an IPv6 only network. +/// +/// The test currently fails, but should pass. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +#[ignore = "known to still fail"] +async fn switch_uplink_ipv6() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let public = lab + .add_router("public") + .preset(RouterPreset::Public) + .build() + .await?; + let home = lab + .add_router("nat2") + .preset(RouterPreset::Home) + .build() + .await?; + let mobile = lab + .add_router("nat3") + .preset(RouterPreset::IspV6) + .build() + .await?; + let dev1 = lab.add_device("dev1").uplink(public.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(home.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "connection started relayed"); + + // Wait until a first direct path is established. + let first = paths + .wait_selected(timeout, |p| { + matches!(p.remote_addr(), TransportAddr::Ip(addr) if addr.ip().is_ipv4()) + }) + .await + .context("did not become direct")?; + info!(addr=?first.remote_addr(), "connection became direct, waiting for path change"); + + // Now wait until the direct path changes, which happens after the other endpoint + // changes its uplink. We check is_ip() explicitly to avoid triggering on a + // transient relay fallback during the network switch. + let second = paths + .wait_selected(timeout, |p| { + matches!(p.remote_addr(), TransportAddr::Ip(addr) if addr.ip().is_ipv6()) + }) + .await + .context("did not switch paths to v6")?; + info!(addr=?second.remote_addr(), "connection changed path, wait for ping"); + + ping_accept(&conn, timeout).await?; + info!("ping done"); + Ok(()) + }, + async move |dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "connection started relayed"); + + // Wait for conn to become direct. + paths + .wait_ip(timeout) + .await + .context("become direct")?; + + // Wait a little more and then switch wifis. + tokio::time::sleep(Duration::from_secs(1)).await; + info!("switch IP uplink"); + dev.replug_iface("eth0", mobile.id()).await?; + + // We don't assert any path changes here, because the remote stays identical, + // and PathInfo does not contain info on local addrs. Instead, the remote + // only accepts our ping after the path changed. + info!("send ping"); + ping_open(&conn, timeout) + .await + .context("failed at ping_open")?; + info!("ping done"); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Test that switching to a faster link works. +/// +/// Two devices, connected initially over holepunched NAT. Then mid connection +/// device 2 plugs a cable into device 1's router, i.e. they now have a LAN +/// connection. +/// +/// Verify we switch to the LAN connection. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn change_ifaces() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + + // dev2 has two uplinks (wifi=Mobile3G on eth0, LAN on eth1). eth1 starts down. + let dev1 = lab + .add_device("dev1") + .iface("eth0", nat1.id(), None) + .build() + .await?; + let dev2 = lab + .add_device("dev2") + .iface("eth0", nat2.id(), Some(LinkCondition::Mobile3G)) + .iface("eth1", nat1.id(), None) + .build() + .await?; + dev2.link_down("eth1").await?; + + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout) + .await + .context("failed at ping_accept")?; + Ok(()) + }, + async move |dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "connection started relayed"); + let first = paths + .wait_ip(timeout) + .await + .context("did not become direct")?; + info!(addr=?first.remote_addr(), "connection became direct"); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // Bring up the LAN interface to the other ep. + info!("bring up eth1"); + dev.link_up("eth1").await?; + + // Wait for a new direct path to be established. We check is_ip() explicitly + // to avoid triggering on a transient relay fallback during the switch. + let next = paths + .wait_selected(timeout, |p| { + p.is_ip() && p.remote_addr() != first.remote_addr() + }) + .await + .context("did not switch paths")?; + info!(addr=?next.remote_addr(), "new direct path established"); + + ping_open(&conn, timeout) + .await + .context("failed at ping_open")?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +// --- +// NAT type matrix: verify holepunching across different NAT combinations +// --- + +/// One peer behind Home NAT, the other on a public network. +/// Holepunching should succeed: EIM mapping means the public peer can reach +/// the NATted peer's mapped port once it learns the address via relay. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +#[ignore = "stays relayed, holepunch times out (deadline elapsed)"] +async fn holepunch_home_nat_one_side() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat = lab.add_router("nat").nat(Nat::Home).build().await?; + let public = lab.add_router("public").build().await?; + let dev1 = lab.add_device("dev1").uplink(nat.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(public.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths.wait_ip(timeout).await.context("did not holepunch")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Both peers behind CGNAT (EIM+EIF). The most permissive real-world NAT. +/// Holepunching should succeed easily since filtering is endpoint-independent. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +#[ignore = "stays relayed, holepunch times out (deadline elapsed)"] +async fn holepunch_cgnat_both() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Cgnat).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Cgnat).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths + .wait_ip(timeout) + .await + .context("did not holepunch through CGNAT")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Both peers behind FullCone NAT (EIM+EIF with hairpin). The most permissive +/// NAT type — any external host can send to the mapped port. Holepunching +/// always succeeds on the first try. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn holepunch_full_cone_both() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::FullCone).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::FullCone).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths + .wait_ip(timeout) + .await + .context("did not holepunch through full cone")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Both peers behind Corporate (symmetric/EDM) NAT. Each destination gets a +/// different external port, making holepunching impossible. The connection +/// must stay on the relay. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn symmetric_nat_stays_relayed() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Corporate).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Corporate).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "should start on relay"); + // Ping to verify the relay path works. + ping_open(&conn, timeout).await?; + // Give holepunching time to attempt and fail. + tokio::time::sleep(Duration::from_secs(8)).await; + assert!( + paths.is_relay(), + "should still be relayed — symmetric NAT blocks holepunching" + ); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// One peer behind Home NAT (EIM), the other behind Corporate/symmetric NAT +/// (EDM). Holepunching fails because the symmetric side allocates a different +/// port for each destination, so the Home peer's probes never reach the right +/// port. Connection stays relayed. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn mixed_home_vs_symmetric_stays_relayed() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let home = lab.add_router("home").nat(Nat::Home).build().await?; + let corp = lab.add_router("corp").nat(Nat::Corporate).build().await?; + let dev1 = lab.add_device("dev1").uplink(home.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(corp.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "should start on relay"); + ping_open(&conn, timeout).await?; + tokio::time::sleep(Duration::from_secs(8)).await; + assert!( + paths.is_relay(), + "should still be relayed — symmetric NAT on one side blocks holepunching" + ); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Both peers behind CloudNat (EDM+APDF), the symmetric NAT used by cloud +/// providers (AWS NAT Gateway, GCP Cloud NAT). Same as Corporate: holepunching +/// is impossible, connection stays relayed. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn cloud_nat_stays_relayed() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::CloudNat).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::CloudNat).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "should start on relay"); + ping_open(&conn, timeout).await?; + tokio::time::sleep(Duration::from_secs(8)).await; + assert!( + paths.is_relay(), + "should still be relayed — cloud symmetric NAT blocks holepunching" + ); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Double NAT: device behind a Home router, which itself sits behind an ISP +/// CGNAT router. This is a common real-world scenario (carrier-grade NAT + +/// consumer router). Both NATs use endpoint-independent mapping, so +/// holepunching should succeed. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +#[ignore = "stays relayed, holepunch times out (deadline elapsed)"] +async fn holepunch_double_nat() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + // ISP-level CGNAT routers + let isp1 = lab.add_router("isp1").nat(Nat::Cgnat).build().await?; + let isp2 = lab.add_router("isp2").nat(Nat::Cgnat).build().await?; + // Home routers behind ISPs + let home1 = lab + .add_router("home1") + .nat(Nat::Home) + .upstream(isp1.id()) + .build() + .await?; + let home2 = lab + .add_router("home2") + .nat(Nat::Home) + .upstream(isp2.id()) + .build() + .await?; + let dev1 = lab.add_device("dev1").uplink(home1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(home2.id()).build().await?; + let timeout = Duration::from_secs(15); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths + .wait_ip(timeout) + .await + .context("did not holepunch through double NAT")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +// --- +// Firewall and adverse conditions +// --- + +/// Corporate firewall blocks all UDP except DNS (port 53) and only allows TCP +/// on ports 80 and 443. Holepunching is impossible, but the relay connection +/// via HTTPS (TCP 443) must still work. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn corporate_firewall_relay_only() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let fw = lab + .add_router("fw") + .firewall(Firewall::Corporate) + .build() + .await?; + let public = lab.add_router("public").build().await?; + let dev1 = lab.add_device("dev1").uplink(fw.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(public.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "should start on relay"); + ping_open(&conn, timeout).await?; + tokio::time::sleep(Duration::from_secs(8)).await; + assert!( + paths.is_relay(), + "should still be relayed — corporate firewall blocks UDP" + ); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Holepunch through Home NATs with a degraded mobile link (100ms latency, +/// 30ms jitter, 2% loss). Connection should still upgrade to direct despite +/// the poor link quality. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn holepunch_mobile_3g() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let dev1 = lab + .add_device("dev1") + .iface("eth0", nat1.id(), Some(LinkCondition::Mobile3G)) + .build() + .await?; + let dev2 = lab + .add_device("dev2") + .iface("eth0", nat2.id(), Some(LinkCondition::Mobile3G)) + .build() + .await?; + let timeout = Duration::from_secs(20); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths + .wait_ip(timeout) + .await + .context("did not holepunch over 3G link")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Holepunch through Home NATs on a satellite link (high latency, moderate +/// jitter). Tests that iroh handles high-RTT environments without timing out +/// during NAT traversal. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn holepunch_satellite() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let dev1 = lab + .add_device("dev1") + .iface("eth0", nat1.id(), Some(LinkCondition::Satellite)) + .build() + .await?; + let dev2 = lab + .add_device("dev2") + .iface("eth0", nat2.id(), Some(LinkCondition::Satellite)) + .build() + .await?; + let timeout = Duration::from_secs(20); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths + .wait_ip(timeout) + .await + .context("did not holepunch over satellite link")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Brief link outage: after holepunching succeeds, the link goes down for 2 +/// seconds and comes back up. The connection should recover — either by +/// falling back to relay during the outage or by re-establishing the direct +/// path after recovery. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn link_outage_recovery() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let dev1 = lab.add_device("dev1").uplink(nat1.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(nat2.id()).build().await?; + let timeout = Duration::from_secs(15); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await.context("ping 1")?; + ping_accept(&conn, timeout).await.context("ping 2")?; + Ok(()) + }, + async move |dev, _ep, conn| { + let mut paths = conn.paths(); + paths.wait_ip(timeout).await.context("initial holepunch")?; + info!("holepunched, now killing link for 2s"); + + // Take the link down. + dev.link_down("eth0").await?; + tokio::time::sleep(Duration::from_secs(2)).await; + dev.link_up("eth0").await?; + info!("link restored, waiting for recovery"); + + // After link recovery, we should be able to ping — via relay + // fallback or re-established direct path. + ping_open(&conn, Duration::from_secs(20)) + .await + .context("ping after link recovery")?; + info!("connection recovered after link outage"); + + // Eventually the direct path should come back. + paths + .wait_ip(Duration::from_secs(20)) + .await + .context("did not re-establish direct path")?; + ping_open(&conn, timeout).await.context("ping on direct")?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Hotel WiFi: captive-portal firewall allows all outbound TCP but only UDP +/// port 53 (DNS). Similar to corporate firewall but less restrictive on TCP. +/// Relay via HTTPS should work, holepunching should not. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn hotel_wifi_relay_only() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let hotel = lab + .add_router("hotel") + .preset(RouterPreset::Hotel) + .build() + .await?; + let public = lab.add_router("public").build().await?; + let dev1 = lab.add_device("dev1").uplink(hotel.id()).build().await?; + let dev2 = lab.add_device("dev2").uplink(public.id()).build().await?; + let timeout = Duration::from_secs(10); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + assert!(paths.is_relay(), "should start on relay"); + ping_open(&conn, timeout).await?; + tokio::time::sleep(Duration::from_secs(8)).await; + assert!( + paths.is_relay(), + "should still be relayed — hotel firewall blocks UDP" + ); + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +/// Asymmetric link conditions: one peer on a fast LAN, the other on degraded +/// WiFi. Holepunching should still succeed, and the connection should use +/// the direct path despite the asymmetric quality. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn holepunch_asymmetric_links() -> Result { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let dev1 = lab + .add_device("dev1") + .iface("eth0", nat1.id(), Some(LinkCondition::Lan)) + .build() + .await?; + let dev2 = lab + .add_device("dev2") + .iface("eth0", nat2.id(), Some(LinkCondition::WifiBad)) + .build() + .await?; + let timeout = Duration::from_secs(15); + Pair::new(dev1, dev2, relay_map) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + paths + .wait_ip(timeout) + .await + .context("did not holepunch with asymmetric links")?; + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await?; + guard.ok(); + Ok(()) +} + +// --- +// Degradation ladder: find where holepunching breaks under worsening conditions +// --- + +/// Increasingly degraded link on one side, clean link on the other. +/// Each level adds more latency, loss, and reordering. The test runs each level +/// twice: once with the impaired side accepting, once connecting. +/// +/// Bump these thresholds as iroh's holepunching improves. +const DEGRADE_PASS_THRESHOLD_IMPAIRED_SERVER: usize = 7; +const DEGRADE_PASS_THRESHOLD_IMPAIRED_CLIENT: usize = 7; + +const DEGRADE_LEVELS: &[LinkLimits] = &[ + // 0: mild — good wifi + LinkLimits { + latency_ms: 10, + jitter_ms: 5, + loss_pct: 0.5, + reorder_pct: 0.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, + // 1: moderate — mediocre 4G + LinkLimits { + latency_ms: 40, + jitter_ms: 15, + loss_pct: 1.0, + reorder_pct: 1.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, + // 2: poor — bad wifi or 3G + LinkLimits { + latency_ms: 100, + jitter_ms: 30, + loss_pct: 3.0, + reorder_pct: 3.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, + // 3: bad — congested 3G + LinkLimits { + latency_ms: 200, + jitter_ms: 60, + loss_pct: 5.0, + reorder_pct: 5.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, + // 4: terrible — barely usable + LinkLimits { + latency_ms: 300, + jitter_ms: 80, + loss_pct: 8.0, + reorder_pct: 8.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, + // 5: extreme — GEO satellite with heavy loss + LinkLimits { + latency_ms: 500, + jitter_ms: 100, + loss_pct: 12.0, + reorder_pct: 12.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, + // 6: absurd — stress test + LinkLimits { + latency_ms: 800, + jitter_ms: 200, + loss_pct: 20.0, + reorder_pct: 20.0, + rate_kbit: 0, + duplicate_pct: 0.0, + corrupt_pct: 0.0, + }, +]; + +/// Run the degradation ladder: iterate through levels, creating fresh devices +/// each round but reusing the lab and relay. Returns the number of levels passed. +async fn run_degrade_ladder(impaired_is_server: bool) -> Result<(usize, TestGuard)> { + let (lab, relay_map, _relay_guard, guard) = lab_with_relay(testdir!()).await?; + let nat1 = lab.add_router("nat1").nat(Nat::Home).build().await?; + let nat2 = lab.add_router("nat2").nat(Nat::Home).build().await?; + let timeout = Duration::from_secs(15); + + let mut last_pass = 0; + for (level, limits) in DEGRADE_LEVELS.iter().enumerate() { + let impaired = Some(LinkCondition::Manual(*limits)); + let (server_cond, client_cond) = if impaired_is_server { + (impaired, None) + } else { + (None, impaired) + }; + let server_name = format!("{level}-server"); + let client_name = format!("{level}-client"); + debug!( + level, + latency_ms = limits.latency_ms, + loss_pct = limits.loss_pct, + reorder_pct = limits.reorder_pct, + impaired_is_server, + "starting level", + ); + let server = lab + .add_device(&server_name) + .iface("eth0", nat1.id(), server_cond) + .build() + .await?; + let client = lab + .add_device(&client_name) + .iface("eth0", nat2.id(), client_cond) + .build() + .await?; + + let server_id = server.id(); + let client_id = client.id(); + let result = Pair::new(server, client, relay_map.clone()) + .run( + async move |_dev, _ep, conn| { + ping_accept(&conn, timeout).await?; + Ok(()) + }, + async move |_dev, _ep, conn| { + let mut paths = conn.paths(); + if paths.wait_ip(timeout).await.is_err() { + n0_error::bail_any!("holepunch_timeout"); + } + ping_open(&conn, timeout).await?; + Ok(()) + }, + ) + .await; + + lab.remove_device(server_id)?; + lab.remove_device(client_id)?; + + let ok = match result { + Ok(()) => true, + Err(e) if e.to_string().contains("holepunch_timeout") => false, + Err(e) => return Err(e), + }; + + if ok { + info!( + level, + latency_ms = limits.latency_ms, + loss_pct = limits.loss_pct, + reorder_pct = limits.reorder_pct, + "PASSED", + ); + } else { + warn!( + level, + latency_ms = limits.latency_ms, + loss_pct = limits.loss_pct, + reorder_pct = limits.reorder_pct, + "FAILED", + ); + } + + if ok { + last_pass = level + 1; + } else { + break; + } + } + Ok((last_pass, guard)) +} + +/// Impaired side is the accepting (server) peer. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn degrade_ladder_impaired_server() -> Result { + let (passed, guard) = run_degrade_ladder(true).await?; + assert!( + passed >= DEGRADE_PASS_THRESHOLD_IMPAIRED_SERVER, + "holepunch should pass at least {DEGRADE_PASS_THRESHOLD_IMPAIRED_SERVER} levels \ + with impaired server, but only passed {passed}" + ); + guard.ok(); + Ok(()) +} + +/// Impaired side is the connecting (client) peer. +#[tokio::test] +#[traced_test] +#[serial_test::serial] +async fn degrade_ladder_impaired_client() -> Result { + let (passed, guard) = run_degrade_ladder(false).await?; + assert!( + passed >= DEGRADE_PASS_THRESHOLD_IMPAIRED_CLIENT, + "holepunch should pass at least {DEGRADE_PASS_THRESHOLD_IMPAIRED_CLIENT} levels \ + with impaired client, but only passed {passed}" + ); + guard.ok(); + Ok(()) +} diff --git a/iroh/tests/patchbay/netreport.rs b/iroh/tests/patchbay/netreport.rs new file mode 100644 index 00000000000..80037c7f9ae --- /dev/null +++ b/iroh/tests/patchbay/netreport.rs @@ -0,0 +1,341 @@ +// --- +// NetReport tests +// --- + +/// Home NAT (EIM+APDF): the most common consumer router. +/// Expect UDP v4, a NATted public address (different from the device's private IP), +/// relay reachability with measured latency, and no captive portal. +#[tokio::test] +#[traced_test] +async fn netreport_home_nat() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let nat = lab.add_router("nat").nat(Nat::Home).build().await?; + let dev = lab.add_device("dev").uplink(nat.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let report = run_net_report(dev, relay_map).await?; + assert!(report.udp_v4, "expected UDP v4 through home NAT"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_ne!( + *global_v4.ip(), + dev_ip, + "global IP should differ from device private IP behind NAT" + ); + let relay = report + .preferred_relay + .expect("expected relay to be reachable"); + assert!( + report.relay_latency.iter().any(|(_, url, _)| *url == relay), + "expected latency data for preferred relay" + ); + Ok(()) +} + +/// Corporate (symmetric) NAT: produces a different external port +/// per destination. Holepunching requires relay, but relay should be reachable. +#[tokio::test] +#[traced_test] +async fn netreport_corporate_nat() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let nat = lab.add_router("nat").nat(Nat::Corporate).build().await?; + let dev = lab.add_device("dev").uplink(nat.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let report = run_net_report(dev, relay_map).await?; + assert!(report.udp_v4, "expected UDP v4 through corporate NAT"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_ne!( + *global_v4.ip(), + dev_ip, + "global IP should differ from device private IP behind symmetric NAT" + ); + let relay = report + .preferred_relay + .expect("expected relay to be reachable"); + assert!( + report.relay_latency.iter().any(|(_, url, _)| *url == relay), + "expected latency data for preferred relay" + ); + Ok(()) +} + +/// Direct connection (no NAT). The reported global_v4 should equal the +/// device's own IP since there is no address translation. +#[tokio::test] +#[traced_test] +async fn netreport_direct() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let router = lab.add_router("direct").build().await?; // Nat::None by default + let dev = lab.add_device("dev").uplink(router.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let report = run_net_report(dev, relay_map).await?; + assert!(report.udp_v4, "expected UDP v4 on direct connection"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_eq!( + *global_v4.ip(), + dev_ip, + "without NAT, global IP should equal device's own IP" + ); + let relay = report + .preferred_relay + .expect("expected relay to be reachable"); + assert!( + report.relay_latency.iter().any(|(_, url, _)| *url == relay), + "expected latency data for preferred relay" + ); + Ok(()) +} + +// --- +// NetReport: additional NAT topologies +// --- + +/// Full cone NAT (EIM+EIF): most permissive NAT. Port-preserving, hairpin enabled. +/// Holepunching always succeeds. Same expectations as Home NAT for net_report. +#[tokio::test] +#[traced_test] +async fn netreport_full_cone() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let nat = lab.add_router("nat").nat(Nat::FullCone).build().await?; + let dev = lab.add_device("dev").uplink(nat.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let report = run_net_report(dev, relay_map).await?; + assert!(report.udp_v4, "expected UDP v4 through full cone NAT"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_ne!( + *global_v4.ip(), + dev_ip, + "global IP should differ from device private IP behind NAT" + ); + let relay = report + .preferred_relay + .expect("expected relay to be reachable"); + assert!( + report.relay_latency.iter().any(|(_, url, _)| *url == relay), + "expected latency data for preferred relay" + ); + assert_ne!( + report.captive_portal, + Some(true), + "no captive portal expected" + ); + Ok(()) +} + +/// Cloud NAT (EDM+APDF): symmetric NAT with randomized ports, similar to corporate +/// but with longer UDP timeout (350s). Common in cloud providers (GCP, AWS). +#[tokio::test] +#[traced_test] +async fn netreport_cloud_nat() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let nat = lab.add_router("nat").nat(Nat::CloudNat).build().await?; + let dev = lab.add_device("dev").uplink(nat.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let report = run_net_report(dev, relay_map).await?; + assert!(report.udp_v4, "expected UDP v4 through cloud NAT"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_ne!( + *global_v4.ip(), + dev_ip, + "global IP should differ from device private IP behind cloud NAT" + ); + let relay = report + .preferred_relay + .expect("expected relay to be reachable"); + assert!( + report.relay_latency.iter().any(|(_, url, _)| *url == relay), + "expected latency data for preferred relay" + ); + assert_ne!( + report.captive_portal, + Some(true), + "no captive portal expected" + ); + Ok(()) +} + +/// Standalone CGNAT (EIM+EIF): carrier-grade NAT without a home router in front. +/// Common for mobile carriers. More permissive filtering than Home NAT. +#[tokio::test] +#[traced_test] +async fn netreport_cgnat() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let nat = lab.add_router("nat").nat(Nat::Cgnat).build().await?; + let dev = lab.add_device("dev").uplink(nat.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let report = run_net_report(dev, relay_map).await?; + assert!(report.udp_v4, "expected UDP v4 through CGNAT"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_ne!( + *global_v4.ip(), + dev_ip, + "global IP should differ from device private IP behind CGNAT" + ); + let relay = report + .preferred_relay + .expect("expected relay to be reachable"); + assert!( + report.relay_latency.iter().any(|(_, url, _)| *url == relay), + "expected latency data for preferred relay" + ); + assert_ne!( + report.captive_portal, + Some(true), + "no captive portal expected" + ); + Ok(()) +} + +// --- +// NetReport: firewall scenarios +// --- + +/// Corporate firewall blocks all UDP except DNS (port 53). QAD probes fail, +/// but the relay is still reachable via HTTPS on port 443. +#[tokio::test] +#[traced_test] +async fn netreport_corporate_firewall() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let fw = lab + .add_router("fw") + .firewall(Firewall::Corporate) + .build() + .await?; + let dev = lab.add_device("dev").uplink(fw.id()).build().await?; + let report = run_net_report(dev, relay_map).await?; + assert!( + !report.udp_v4, + "UDP should be blocked by corporate firewall" + ); + assert!( + report.global_v4.is_none(), + "no global IPv4 without successful QAD probes" + ); + assert!( + report.preferred_relay.is_some(), + "relay should still be reachable via HTTPS (TCP 443)" + ); + assert_ne!( + report.captive_portal, + Some(true), + "no captive portal expected" + ); + Ok(()) +} + +// --- +// NetReport: dual-stack / IPv6 +// --- + +/// Dual-stack device on a direct (no NAT) connection with a dual-stack relay. +/// Both IPv4 and IPv6 QAD probes should succeed. Without NAT, the reported +/// global addresses should match the device's own addresses. +#[tokio::test] +#[traced_test] +async fn netreport_dual_stack_direct() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let router = lab + .add_router("direct") + .ip_support(IpSupport::DualStack) + .build() + .await?; + let dev = lab.add_device("dev").uplink(router.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let dev_ip6 = dev.ip6().expect("device has IPv6 on dual-stack router"); + info!(%dev_ip, %dev_ip6, "dual-stack device"); + let report = run_net_report(dev, relay_map).await?; + // v4 + assert!(report.udp_v4, "expected UDP v4 on direct dual-stack"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_eq!( + *global_v4.ip(), + dev_ip, + "without NAT, global IPv4 should equal device's own IP" + ); + // v6 + assert!(report.udp_v6, "expected UDP v6 on direct dual-stack"); + let global_v6 = report.global_v6.expect("expected global IPv6 address"); + assert_eq!( + *global_v6.ip(), + dev_ip6, + "without NAT, global IPv6 should equal device's own IP" + ); + assert!( + report.preferred_relay.is_some(), + "expected relay to be reachable" + ); + assert_ne!( + report.captive_portal, + Some(true), + "no captive portal expected" + ); + Ok(()) +} + +/// Dual-stack device behind a home NAT with no IPv6 NAT (NatV6Mode::None). +/// IPv4 is NATted (global differs from device IP). IPv6 uses global unicast +/// directly, so the reported global IPv6 should match the device's own address. +#[tokio::test] +#[traced_test] +#[ignore = "currently broken due to bug in patchbay"] +async fn netreport_dual_stack_home_nat() -> Result { + let (lab, relay_map, _relay_guard) = lab_with_relay(testdir!()).await?; + let nat = lab + .add_router("nat") + .nat(Nat::Home) + .ip_support(IpSupport::DualStack) + .build() + .await?; + let dev = lab.add_device("dev").uplink(nat.id()).build().await?; + let dev_ip = dev.ip().expect("device has IPv4"); + let dev_ip6 = dev.ip6().expect("device has IPv6 on dual-stack router"); + info!(%dev_ip, %dev_ip6, "dual-stack device behind home NAT"); + let report = run_net_report(dev, relay_map).await?; + println!("{report:#?}"); + // v4 is NATted. + assert!(report.udp_v4, "expected UDP v4 through home NAT"); + let global_v4 = report.global_v4.expect("expected global IPv4 address"); + assert_ne!( + *global_v4.ip(), + dev_ip, + "global IPv4 should differ from private IP behind NAT" + ); + // v6 passes through without translation (NatV6Mode::None = global unicast). + assert!(report.udp_v6, "expected UDP v6 with global unicast IPv6"); + let global_v6 = report.global_v6.expect("expected global IPv6 address"); + assert_eq!( + *global_v6.ip(), + dev_ip6, + "IPv6 has no NAT, global should equal device's own IP" + ); + assert!( + report.preferred_relay.is_some(), + "expected relay to be reachable" + ); + assert_ne!( + report.captive_portal, + Some(true), + "no captive portal expected" + ); + Ok(()) +} + +// --- +// NetReport helper +// --- + +/// Bind an endpoint in `dev`'s namespace, wait for the first net report, return it. +pub async fn run_net_report(dev: Device, relay_map: RelayMap) -> Result { + dev.spawn(move |dev| { + async move { + let endpoint = endpoint_builder(&dev, relay_map).bind().await?; + let mut watcher = endpoint.net_report(); + let report = tokio::time::timeout(Duration::from_secs(10), watcher.initialized()) + .await + .anyerr()?; + endpoint.close().await; + n0_error::Ok(report) + } + .instrument(error_span!("net_report")) + })? + .await + .anyerr()? +} diff --git a/iroh/tests/patchbay/util.rs b/iroh/tests/patchbay/util.rs new file mode 100644 index 00000000000..402965a0a7f --- /dev/null +++ b/iroh/tests/patchbay/util.rs @@ -0,0 +1,321 @@ +use std::{future::Future, path::PathBuf, time::Duration}; + +use iroh::{ + Endpoint, EndpointAddr, RelayMap, RelayMode, Watcher, + endpoint::{Connection, PathInfo, PathWatcher}, + tls::CaRootsConfig, +}; +use n0_error::{Result, StdResultExt, ensure_any}; +use n0_future::task::AbortOnDropHandle; +use patchbay::{Device, IpSupport, Lab, LabOpts, OutDir, TestGuard}; +use tokio::sync::oneshot; +use tracing::{Instrument, debug, error_span}; + +use self::relay::run_relay_server; + +const TEST_ALPN: &[u8] = b"test"; + +/// Create a lab with a dual-stack relay server. Returns the lab, relay map, a drop guard +/// that keeps the relay alive, and a [`TestGuard`] that records pass/fail. +/// +/// The relay binds on `[::]` and is reachable via `https://relay.test` (resolved +/// through lab-wide DNS entries for both IPv4 and IPv6). +pub async fn lab_with_relay( + path: PathBuf, +) -> Result<(Lab, RelayMap, AbortOnDropHandle<()>, TestGuard)> { + let mut opts = LabOpts::default().outdir(OutDir::Exact(path)); + if let Some(name) = std::thread::current().name() { + opts = opts.label(name); + } + let lab = Lab::with_opts(opts).await?; + let guard = lab.test_guard(); + let (relay_map, relay_guard) = spawn_relay(&lab).await?; + Ok((lab, relay_map, relay_guard, guard)) +} + +async fn spawn_relay(lab: &Lab) -> Result<(RelayMap, AbortOnDropHandle<()>)> { + let dc = lab + .add_router("dc") + .ip_support(IpSupport::DualStack) + .build() + .await?; + let dev_relay = lab.add_device("relay").uplink(dc.id()).build().await?; + + // Register both v4 and v6 addresses under "relay.test" lab-wide. + // Devices created after this will resolve "relay.test" to both addresses. + let relay_v4 = dev_relay.ip().expect("relay has IPv4"); + let relay_v6 = dev_relay.ip6().expect("relay has IPv6"); + lab.dns_entry("relay.test", relay_v4.into())?; + lab.dns_entry("relay.test", relay_v6.into())?; + + let (relay_map_tx, relay_map_rx) = oneshot::channel(); + let task_relay = dev_relay.spawn(async move |_ctx| { + let (relay_map, _server) = run_relay_server().await.unwrap(); + relay_map_tx.send(relay_map).unwrap(); + std::future::pending::<()>().await; + })?; + let relay_map = relay_map_rx.await.unwrap(); + Ok((relay_map, AbortOnDropHandle::new(task_relay))) +} + +// --- +// Pair: run two connected endpoints +// --- + +/// Two connected endpoints in the test lab, ready to run. +/// +/// `peer1` runs in `dev1`'s namespace as the accepting side. +/// `peer2` runs in `dev2`'s namespace as the connecting side. +/// +/// `peer1` awaits the connection to be closed afterwards, whereas `peer2` closes +/// the connection. +pub struct Pair { + dev1: Device, + dev2: Device, + relay_map: RelayMap, +} + +impl Pair { + pub fn new(dev1: Device, dev2: Device, relay_map: RelayMap) -> Self { + Self { + dev1, + dev2, + relay_map, + } + } + + pub async fn run(self, peer1: F1, peer2: F2) -> Result + where + F1: FnOnce(Device, Endpoint, Connection) -> Fut1 + Send + 'static, + Fut1: Future + Send, + F2: FnOnce(Device, Endpoint, Connection) -> Fut2 + Send + 'static, + Fut2: Future + Send, + { + let (addr_tx, addr_rx) = oneshot::channel(); + let relay_map2 = self.relay_map.clone(); + let task1 = self.dev1.spawn(move |dev| { + async move { + let endpoint = endpoint_builder(&dev, relay_map2).bind().await?; + endpoint.online().await; + addr_tx.send(addr_relay_only(endpoint.addr())).unwrap(); + let conn = endpoint.accept().await.unwrap().accept().anyerr()?.await?; + watch_selected_path(&conn); + peer1(dev, endpoint.clone(), conn.clone()).await?; + conn.closed().await; + endpoint.close().await; + n0_error::Ok(()) + } + .instrument(error_span!("ep-acpt")) + })?; + let task2 = self.dev2.spawn(move |dev| { + async move { + let endpoint = endpoint_builder(&dev, self.relay_map).bind().await?; + let addr = addr_rx.await.unwrap(); + let conn = endpoint.connect(addr, TEST_ALPN).await?; + watch_selected_path(&conn); + peer2(dev, endpoint.clone(), conn).await?; + endpoint.close().await; + n0_error::Ok(()) + } + .instrument(error_span!("ep-cnct")) + })?; + task2.await.anyerr()??; + task1.await.anyerr()??; + Ok(()) + } +} + +/// Extension methods on [`PathWatcher`] for common waiting patterns in tests. +#[allow(unused)] +pub trait PathWatcherExt { + async fn wait_selected( + &mut self, + timeout: Duration, + f: impl Fn(&PathInfo) -> bool, + ) -> Result; + + fn selected(&mut self) -> PathInfo; + + fn match_selected(&mut self, f: impl FnOnce(&PathInfo) -> bool) -> bool { + f(&self.selected()) + } + + fn is_ip(&mut self) -> bool { + self.match_selected(PathInfo::is_ip) + } + + fn is_relay(&mut self) -> bool { + self.match_selected(PathInfo::is_relay) + } + /// Wait until the selected path is a direct (IP) path. + async fn wait_ip(&mut self, timeout: Duration) -> Result { + self.wait_selected(timeout, PathInfo::is_ip).await + } + + /// Wait until the selected path is a relay path. + async fn wait_relay(&mut self, timeout: Duration) -> Result { + self.wait_selected(timeout, PathInfo::is_relay).await + } +} + +impl PathWatcherExt for PathWatcher { + fn selected(&mut self) -> PathInfo { + let p = self.get(); + p.iter() + .find(|p| p.is_selected()) + .cloned() + .expect("no selected path") + } + + async fn wait_selected( + &mut self, + timeout: Duration, + f: impl Fn(&PathInfo) -> bool, + ) -> Result { + tokio::time::timeout(timeout, async { + loop { + let selected = self.selected(); + if f(&selected) { + return n0_error::Ok(selected); + } + self.updated().await?; + } + }) + .await + .anyerr()? + } +} + +pub async fn ping_open(conn: &Connection, timeout: Duration) -> Result { + tokio::time::timeout(timeout, async { + let data: [u8; 8] = rand::random(); + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + let r = recv.read_to_end(8).await.anyerr()?; + ensure_any!(r == data, "reply matches"); + Ok(()) + }) + .await + .anyerr()? +} + +pub async fn ping_accept(conn: &Connection, timeout: Duration) -> Result { + tokio::time::timeout(timeout, async { + let (mut send, mut recv) = conn.accept_bi().await.anyerr()?; + let data = recv.read_to_end(8).await.anyerr()?; + send.write_all(&data).await.anyerr()?; + send.finish().anyerr()?; + Ok(()) + }) + .await + .anyerr()? +} + +fn watch_selected_path(conn: &Connection) { + let mut watcher = conn.paths(); + tokio::spawn( + async move { + let mut prev = None; + loop { + let paths = watcher.get(); + let selected = paths.iter().find(|p| p.is_selected()).unwrap(); + if Some(selected) != prev.as_ref() { + debug!( + "selected path: [{}] {:?} rtt {:?}", + selected.id(), + selected.remote_addr(), + selected.rtt().unwrap() + ); + prev = Some(selected.clone()); + } + if watcher.updated().await.is_err() { + break; + } + } + } + .instrument(tracing::Span::current()), + ); +} + +fn endpoint_builder(device: &Device, relay_map: RelayMap) -> iroh::endpoint::Builder { + #[allow(unused_mut)] + let mut builder = Endpoint::empty_builder(RelayMode::Custom(relay_map)) + .ca_roots_config(CaRootsConfig::insecure_skip_verify()) + .alpns(vec![TEST_ALPN.to_vec()]); + + #[cfg(not(feature = "qlog"))] + let _ = device; + + #[cfg(feature = "qlog")] + { + if let Some(path) = device.filepath("qlog") { + let prefix = path.file_name().unwrap().to_str().unwrap(); + let directory = path.parent().unwrap(); + let transport_config = iroh::endpoint::QuicTransportConfig::builder() + .qlog_from_path(directory, prefix) + .build(); + builder = builder.transport_config(transport_config); + } + } + + builder +} + +fn addr_relay_only(addr: EndpointAddr) -> EndpointAddr { + EndpointAddr::from_parts(addr.id, addr.addrs.into_iter().filter(|a| a.is_relay())) +} + +mod relay { + use std::net::{IpAddr, Ipv6Addr}; + + use iroh_base::RelayUrl; + use iroh_relay::{ + RelayConfig, RelayMap, RelayQuicConfig, + server::{ + AccessConfig, CertConfig, QuicConfig, RelayConfig as RelayServerConfig, Server, + ServerConfig, SpawnError, TlsConfig, + }, + }; + + /// Spawn a relay server bound on `[::]` that accepts both IPv4 and IPv6. + /// Uses `https://relay.test` as the URL — callers must set up lab-wide DNS + /// entries for `relay.test` pointing to the relay's v4 and v6 addresses. + pub async fn run_relay_server() -> Result<(RelayMap, Server), SpawnError> { + let bind_ip: IpAddr = Ipv6Addr::UNSPECIFIED.into(); + + let (certs, server_config) = + iroh_relay::server::testing::self_signed_tls_certs_and_config(); + + let tls = TlsConfig { + cert: CertConfig::<(), ()>::Manual { certs }, + https_bind_addr: (bind_ip, 443).into(), + quic_bind_addr: (bind_ip, 7842).into(), + server_config, + }; + let quic = Some(QuicConfig { + server_config: tls.server_config.clone(), + bind_addr: tls.quic_bind_addr, + }); + let config = ServerConfig { + relay: Some(RelayServerConfig { + http_bind_addr: (bind_ip, 80).into(), + tls: Some(tls), + limits: Default::default(), + key_cache_capacity: Some(1024), + access: AccessConfig::Everyone, + }), + quic, + ..Default::default() + }; + let server = Server::spawn(config).await?; + + let url: RelayUrl = "https://relay.test".parse().expect("valid relay url"); + let quic = server + .quic_addr() + .map(|addr| RelayQuicConfig { port: addr.port() }); + let relay_map: RelayMap = RelayConfig { url, quic }.into(); + + Ok((relay_map, server)) + } +}