Skip to content

feat: connect vpc — remote gVPC membership over iroh - #32

Open
drewr wants to merge 18 commits into
developfrom
vpc-894
Open

drewr wants to merge 18 commits into
developfrom
vpc-894

Conversation

@drewr

@drewr drewr commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Extends the connect plugin with a vpc verb (datumctl connect vpc join): creates a local TUN interface that joins a Datum Cloud galactic VPC as a remote member over iroh — tunnel's sibling, carrying raw IPv6 packets to a galactic-side router instead of HTTP to Envoy. Implements datum-cloud/enhancements#894.

  • Upgrades iroh to 1.x; metrics repointed to iroh-services.
  • connect-lib: VPCAttachment CRD type, vpc module (TUN, iroh accept/dial, packet framing, wg-quick-style routing modes). WireGuard's interface/routing model is the reference; iroh subsumes its crypto/handshake/keepalive.
  • connect-plugin: vpc join cobra command.
  • mock-galactic-router + containerlab labs. Control-plane wiring and the real galactic router are still scaffolding — see design/vpc-attachment.md §5–6.

Test plan

  • cargo test/clippy --workspace, go build/vet/test ./...
  • deploy/containerlab/vpc/test-3region.sh: 3-region VPC, local joins through the router (dialed by endpoint id, no ip/port), full mesh (5 devices, 20 pairs) all reachable

How to test

Everything runs on plain Docker (no containerlab/root needed for the all-in-containers path); labs live in deploy/containerlab/vpc/. The router dials the client purely by iroh EndpointId — no ip/port — via discovery, as production does.

All-in-containers full mesh (one shot):

cd deploy/containerlab/vpc
./test-3region.sh          # build image, stand up, verify full mesh, tear down

Stands up a 3-region VPC + router + local client (all containers), then pings every ordered pair (5 devices, 20 pairs) and exits non-zero on any failure. --keep leaves it up; --down tears down.

Client on your host, VPC in containers (proves a real machine joining the containerized VPC over iroh). Three steps, because the host TUN needs your root and the client must start first (it's the iroh accept side that prints the endpoint id the router dials):

cd deploy/containerlab/vpc
./test-host-client.sh up                    # stand up the containerized VPC; prints the exact host command

# run the client on THIS host as root (verbatim command comes from `up`):
sudo env DATUM_CONNECT_DIR=/tmp/vpchost-datum DATUM_SESSION=lab \
  DATUM_CREDENTIALS_HELPER=$PWD/fake-credentials-helper.sh \
  DATUM_API_HOST=https://api.lab.invalid \
  ../../../connect-lib/target/debug/datum-connect --json vpc join \
    --vpc lab-host --address fd00:cafe:100::2 --prefix-len 64 \
    --mode vpc-only --vpc-prefix fd00:cafe::/32
# note the printed endpoint_id; leave it running

./test-host-client.sh dial <endpoint-id>    # router dials the host by id; pings host<->VPC both ways
./test-host-client.sh down                   # tear down (Ctrl+C the host client first)

Both ends reach iroh discovery + relays over normal outbound internet.

The canonical containerlab topology (vpc-3region.clab.yaml) expresses the same all-in-containers topology for containerlab deploy (needs root); see the lab README.md.

@drewr

drewr commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Example just showing the (fake) router fronting traffic to the containerlab VPC:

% bash ./deploy/containerlab/vpc/test-3region.sh
==> Tearing down
==> Creating networks
==> Starting region devices
==> Starting local device (iroh accept side)
==> Starting router (L3 hub) and attaching it to every regional segment
==> local: joining the VPC through the router
==> client endpoint 8a2c185832ac8db4380b6950ff774cb9a84318462a71137cbe5a00821ef75436 listening on 172.30.0.2:39728
==> router: dialing the client to bring the tunnel up
==> Waiting for connectivity to converge
==> Full-mesh ping matrix (5 devices, 20 ordered pairs)
PASS local -> router (fd00:cafe:100::1)
PASS local -> region-a (fd00:cafe:a::1)
PASS local -> region-b (fd00:cafe:b::1)
PASS local -> region-c (fd00:cafe:c::1)
PASS router -> local (fd00:cafe:100::2)
PASS router -> region-a (fd00:cafe:a::1)
PASS router -> region-b (fd00:cafe:b::1)
PASS router -> region-c (fd00:cafe:c::1)
PASS region-a -> local (fd00:cafe:100::2)
PASS region-a -> router (fd00:cafe:100::1)
PASS region-a -> region-b (fd00:cafe:b::1)
PASS region-a -> region-c (fd00:cafe:c::1)
PASS region-b -> local (fd00:cafe:100::2)
PASS region-b -> router (fd00:cafe:100::1)
PASS region-b -> region-a (fd00:cafe:a::1)
PASS region-b -> region-c (fd00:cafe:c::1)
PASS region-c -> local (fd00:cafe:100::2)
PASS region-c -> router (fd00:cafe:100::1)
PASS region-c -> region-a (fd00:cafe:a::1)
PASS region-c -> region-b (fd00:cafe:b::1)

PASS all devices can reach all other devices
==> Tearing down

@drewr

drewr commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Example attaching a real VM host interface to the fake VPC in containerlab:

1323: datum-vpc0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none
    inet6 fd00:cafe:100::2/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::cdcb:ac26:c24a:7345/64 scope link stable-privacy
       valid_lft forever preferred_lft forever
% ./test-host-client.sh dial 1d9fbdde9446679c879cf83da61a67fb722e6489574f291bad06bbcb0c16d74a
==> router: dialing the host client by endpoint id (via iroh discovery)
==> Waiting for the tunnel to come up (host -> region-a)
==> Reachability from this host into the VPC
PASS host -> region-a (fd00:cafe:a::1)
PASS host -> region-b (fd00:cafe:b::1)
PASS host -> region-c (fd00:cafe:c::1)
PASS host -> router (fd00:cafe:100::1)
==> Reachability from the VPC back to this host (fd00:cafe:100::2)
PASS region-a -> host (fd00:cafe:100::2)
PASS region-b -> host (fd00:cafe:100::2)
PASS region-c -> host (fd00:cafe:100::2)

PASS host client is a full member of the containerized VPC

@drewr

drewr commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Validated cross-machine: Mac at home ↔ VPC on a DC VM, over the public internet

Client on a bare-metal macOS laptop at home, the VPC (router + 3 regions) on a Linux VM in a data center, dialed by iroh EndpointId — no ip/port anywhere. Both directions green.

                HOME                                        DATA-CENTER VM (Linux, dockerd)
┌──────────────────────────────────┐             ┌───────────────────────────────────────────────────────┐
│  MacBook (macOS)                 │             │    ┌─────────────────────────────────────────────────┐│
│   datum-connect vpc join         │             │    │ router container (mock-galactic-router)         ││
│   (iroh ACCEPT side)             │             │    │   IPv6 forwarding = on                          ││
│                                  │             │    │   mock-vpc0 (TUN)  fd00:cafe:1100::1            ││
│   utun8  fd00:cafe:1100::2/64    │             │    │        │  kernel-forwards between the tunnel    ││
│      │ route fd00:cafe::/32      │             │    │        │  TUN and the three region segments     ││
│      ▼   dev utun8               │             │    │   eth1 ─── eth2 ─── eth3                        ││
│   [TUN ⇄ iroh forwarder]         │             │    │   fd00:d0c:1{a,b,c}::ff  (router links)         ││
│      │                           │             │    └────┼───────┼───────┼────────────────────────────┘│
│      │  iroh/QUIC bi-stream      │             │         │docker │bridges│  fd00:d0c:1{a,b,c}::/64     │
│      │  ALPN datum-connect/vpc/0 │─────────────────────▶ │       │       │                             │
└──────┼───────────────────────────┘    ▲        │     ┌───┴──┐ ┌──┴───┐ ┌─┴────┐  region containers     │
       │      dialed by EndpointId      │        │     │region│ │region│ │region│  VPC addrs:            │
       └─────── (no ip/port — iroh ─────┘        │     │  -a  │ │  -b  │ │  -c  │  cafe:1a::1            │
               discovery + relays)               │     └──────┘ └──────┘ └──────┘  cafe:1b::1 cafe:1c::1 │
          PUBLIC  INTERNET                       └───────────────────────────────────────────────────────┘

VPC address space      = fd00:cafe::/32          reachable from the laptop ONLY through the iroh tunnel
docker "native" fabric = fd00:d0c:1{a,b,c}::/64  router⇄region links; the laptop has no route to these

On the DC VM (docker) — stand up the containerized VPC:

% ./test-host-client.sh up
==> Tearing down containers/networks (host client, if running, must be stopped separately)
==> Creating networks
==> Starting region devices
==> Starting router (L3 hub) and attaching it to every regional segment

==> Containerized VPC is up.

On the Mac — join the VPC (dialed by id), then verify client→VPC:

% nix build
% sudo env DATUM_CONNECT_DIR=/tmp/vpchost-datum DATUM_SESSION=lab \
    DATUM_CREDENTIALS_HELPER="$PWD/deploy/containerlab/vpc/fake-credentials-helper.sh" \
    DATUM_API_HOST=https://api.lab.invalid \
    ./result/bin/datum-connect --json vpc join --vpc lab-host \
    --address fd00:cafe:1100::2 --prefix-len 64 --mode vpc-only --vpc-prefix fd00:cafe::/32
{"address":"fd00:cafe:1100::2","bound_addrs":["0.0.0.0:60114","[::]:56325"],"endpoint_id":"9968ba066a6f01d0fec0f41e1a81a32ddd2af58def83178c4531b46539038eea","mode":"vpc-only","tun_name":"utun8","type":"vpc_ready","vpc":"lab-host"}

% deploy/containerlab/vpc/test-host-client.sh client-check
==> Waiting for the tunnel to settle (client -> router over iroh)
==> Reachability from the client into the VPC
PASS client -> region-a (fd00:cafe:1a::1)
PASS client -> region-b (fd00:cafe:1b::1)
PASS client -> region-c (fd00:cafe:1c::1)
PASS client -> router (fd00:cafe:1100::1)

PASS client reaches the whole VPC over the tunnel

Back on the VM — router dials the Mac by endpoint id, verifies VPC→client:

% ./test-host-client.sh dial 9968ba066a6f01d0fec0f41e1a81a32ddd2af58def83178c4531b46539038eea
==> router: dialing the client by endpoint id (via iroh discovery)
==> Waiting for the tunnel to settle (region-a -> client over iroh, up to 90s)
==> Reachability from the VPC to the client (fd00:cafe:1100::2)
PASS region-a -> client (fd00:cafe:1100::2)
PASS region-b -> client (fd00:cafe:1100::2)
PASS region-c -> client (fd00:cafe:1100::2)

PASS VPC -> client reachable. Run './test-host-client.sh client-check' ON THE CLIENT for the client -> VPC direction.

Full write-up in design/vpc-attachment.md §8.

Bumps iroh/iroh-base/iroh-relay 0.95->1.2.0, iroh-tickets 0.2->1.0, and
iroh-proxy-utils to its current HEAD (the first rev with real iroh-1.0
support). Mechanical fallout from the major bump:

- discovery -> address_lookup: iroh 1.x replaced the pluggable Discovery
  trait with AddressLookup/.address_lookup(...). Datum's custom-origin
  DNS discovery has a direct successor in
  address_lookup::DnsAddressLookup::builder(origin), so
  DiscoveryMode::{Dns,Hybrid,Default} behavior is preserved.
- Endpoint::builder(..).bind() now hard-errors without an explicit
  crypto_provider (no more implicit global-default fallback); connect-lib
  now depends on rustls directly to supply one.
- iroh_tickets::Ticket renamed to_bytes/from_bytes to
  encode_bytes/decode_bytes; SecretKey::generate() dropped its RNG arg.
- iroh-n0des doesn't support iroh 1.x (still pinned to iroh ^0.96) and
  briefly regressed metrics collection; n0-computer's 1.x-compatible
  successor is published as iroh-services (same Client/ApiSecret shapes),
  so ListenNode/ConnectNode's n0des plumbing now points at that crate
  instead. N0DES_API_SECRET/BUILD_N0DES_API_SECRET env vars are unchanged.

Verified with cargo build/test/clippy --workspace (91/91 tests passing).
Adds the client-side data plane for datum-cloud/enhancements#894: a
`datumctl connect vpc join` verb that creates a local TUN interface and
joins a galactic VPC as a remote member over an iroh connection, the
sibling of `tunnel` but carrying raw IPv6 packets to a galactic-side
router instead of HTTP requests to Envoy.

connect-lib:
- datum_apis/vpc_attachment.rs: the VPCAttachment CRD type (spec +
  status contract — assigned address, advertised prefixes, MTU, claiming
  router's iroh EndpointId), a Layer-3 analogue of ConnectorAdvertisement
  over the same reusable Connector primitive.
- vpc/{mod,transport,routing}.rs: TUN creation, VpcListener/
  VpcAcceptHandler (accept side — the client is dialed in, as tunnel's
  client is dialed by Envoy), VpcDialer (dial side), length-prefixed
  packet framing over one iroh bidi stream on a new IROH_VPC_ALPN, and
  wg-quick-style routing-mode installation (vpc-only vs default-route
  with the ::/1+8000::/1 split and the anti-loop gateway pin).
- The WireGuard-linux interface/routing model is the reference; its
  crypto/handshake/keepalive/roaming are all subsumed by iroh. AllowedIPs
  collapses to a mode toggle (one peer) plus a single-EndpointId source
  check.
- A unit test exercises the framing via tokio::io::duplex (no root).

bin: `vpc join` subcommand; new mock-galactic-router crate stands in for
the (imagined, not-yet-built) galactic-side router for testing.

connect-plugin: `vpc` cobra tree mirroring tunnel's supervisor pattern.

--router-id is optional (trust-on-first-connect) so the client can start
unattended before any router has claimed its attachment — the real
control-plane flow, where the client necessarily starts first. Once
CRD-backed, "no router yet" must mean "don't accept," not "accept
anyone" (flagged in code and design/vpc-attachment.md §6).

Privileged ops (TUN creation, `ip` commands) fail with a CAP_NET_ADMIN
hint instead of elevating — nothing invokes sudo.

deploy/containerlab/vpc: a lab proving the data plane end to end (TUN
creation, iroh accept/dial handshake, framing, routing) against the mock
router. Verified via docker run: real TUN devices across two containers,
0% packet loss pinging both directions.

Not yet wired: `vpc join` takes the attachment's effects as flags rather
than watching a real VPCAttachment; the galactic-side router is imagined,
not built (see design/vpc-attachment.md §5–6).
Adds a 3-region VPC lab (mirroring galactic's dfw/sjc/iad model): a router
sits on the VPC, the local device joins through it over iroh, and a
full-mesh ping matrix proves every device reaches every other.

- deploy/containerlab/vpc/test-3region.sh: self-contained on plain Docker
  (no containerlab/root beyond Docker access), idempotent, non-zero exit on
  any failure. The router is a plain IPv6 L3 hub — kernel forwarding moves
  packets between the three regional /64 segments and the tunnel TUN, while
  mock-galactic-router only pumps the tunnel. That needed no code changes:
  it's the same split a real galactic router has. Verified: all 12 ordered
  pairs reachable, repeatable across runs.
- deploy/containerlab/vpc/vpc-3region.clab.yaml: the same topology in
  containerlab form (parses/checks clean; deploy needs root, supply it
  yourself).
- routing.rs: correct install_routes' doc — routes are plain dev routes out
  the tunnel (one peer, the router, so no next-hop gateway is needed), and
  the advertised prefix must actually cover the target subnets. During
  bring-up a /48 aggregate silently excluded fd00:cafe:{a,b,c}::/64 (whose
  third 16-bit group differs), so the lab uses /32; documented in the script
  and design doc §7 so it doesn't bite again.

No sudo anywhere; privileged ops still fail with a CAP_NET_ADMIN hint.
The router joins the full-mesh matrix at its tunnel-side address
(fd00:cafe:100::1), so its own stack is verified reachable from every
device and as a source to every device — 5 devices, 20 ordered pairs, all
passing. Previously the router was only exercised implicitly as the
forwarding hub on the client<->region paths.
test-host-client.sh runs the `vpc join` client on the host (real TUN in the
host netns) against the 3-region VPC in containers. The router dials the host
over iroh at the docker transport gateway (verified reachable); the host TUN
step needs the caller's root, so it's a three-step up / (sudo join) / dial
flow. Verifies host<->VPC reachability both directions.
The mock router previously required --peer-addr <ip:port>, which defeats the
point of iroh — you dial by identity and discovery resolves reachability.
mock-galactic-router now builds its endpoint with connect-lib's build_endpoint
(same relays + discovery every client gets) and dials by EndpointId; the
client is resolvable because it already publishes to iroh discovery. Both
test scripts drop the port: they scrape only the endpoint id and the router
dials by id. --peer-addr stays as an optional offline/same-host pin.

Verified: 3-region full mesh (20 pairs) with the router connecting purely by
endpoint id via discovery. Matches the production design (§5): the real
router dials the client's EndpointId published under its Connector status.
…ab subnets

- Both labs now retry each reachability ping (5x/1s) instead of a single
  shot. Dialing by endpoint id brings the connection up via a relay path
  first, then upgrades to a direct path; a brief blip during that upgrade can
  drop a packet or two, which shouldn't fail a reachability check. A genuinely
  unreachable pair still fails after all attempts, so this doesn't mask breaks.
  (Fixes an intermittent `host -> router` failure seen in the host-client lab;
  the all-containers lab's local->router is 15/15 solid, confirming the mock
  logic is stable once the path settles.)
- test-host-client.sh now uses fd00:cafe:1{a,b,c}::/64 for its regions,
  distinct from test-3region.sh's fd00:cafe:{a,b,c}::/64, so the two labs'
  docker networks can't overlap ("Pool overlaps" errors) across leftover runs.
The host is the docker host, so it had a direct bridge route to every region
network — host->region pings shortcut the tunnel entirely, and the convergence
gate (which pinged a region) passed instantly via that bridge, letting the
matrix run before the iroh path had settled. That surfaced as an intermittent
host->router failure (the one genuinely tunnel-only check); once the relay->
direct path upgrade completes it's a clean ~0.8ms direct path.

Fixes, matching the intended architecture (docker plays no part in host<->VPC):
- Split the docker router<->region fabric (fd00:d0c:1{a,b,c}::/64) from the VPC
  prefix. Each region's VPC address (fd00:cafe:1{a,b,c}::1) is now a /128 atop
  its docker link, routed only through the router. The host has no route into
  fd00:cafe:: except the TUN, so every host->VPC packet goes through iroh.
- Gate on the real tunnel path (host -> router's tunnel address) with a 90s
  settle window, instead of a region ping that never touched the tunnel.

Verified the VPC fabric (router->region, region->region over the /128 routes)
container-side; the host->tunnel path needs the caller's sudo to exercise.
The client and the VPC no longer have to be on the same machine — a laptop can
join a VPC running on a remote VM. Split the docker-side and client-side steps
so the client side needs no docker (it was failing with "docker: command not
found" when run off-host):

- dial (VM/docker) now drives the router and verifies only the VPC->client
  direction (region -> client TUN, via docker exec), gating on that instead of
  a "ping from this host" that assumed the client was local.
- New client-check (client machine, no docker) pings the whole VPC from the
  client for the client->VPC direction.
- up's output and the README explain which command runs where, how to place the
  binary + credentials helper on a remote client, and that only one client may
  hold the VPC address at a time.

client-check verified to run with docker absent from PATH.
The iroh 1.x upgrade bumped iroh-proxy-utils to 0.3.0 at rev e87788bb, but the
flake's cargoLock outputHashes still keyed the old iroh-proxy-utils-0.1.0 hash,
so `nix build` failed ("A hash was specified for iroh-proxy-utils-0.1.0, but
there is no corresponding git dependency"). Rekey to 0.3.0 with the vendored
hash nix reports for the new rev.
The vpc client only worked on Linux: it forced a `datum-vpc0` TUN name (macOS
requires utunN → "invalid device tun name") and shelled out to iproute2 (`ip`),
which macOS doesn't have. Since issue #894 targets the Mac, add macOS support:

- create_tun_device: on macOS, let the kernel assign the utunN name (a non-utun
  name is rejected) rather than forcing one. New device_name() reads the actual
  kernel-assigned name back; both binaries now configure routes/addresses using
  that name and report it.
- routing.rs: OS-gate configure_interface/install_routes — iproute2 on Linux,
  ifconfig + route on macOS (address assignment, interface routes, the
  DefaultRoute host-route pin, and default-route lookup all have macOS forms).
- No data-plane change: the tun crate hides the macOS utun 4-byte protocol
  header, so the packet pump is identical.

Linux builds/tests/clippy pass; the macOS path is written per-target but not yet
run on real hardware (no Mac in CI here) — see design/vpc-attachment.md §6.
client-check ran the Linux ping form (`ping -6 ... -W2`) on the client machine.
On macOS `-W` is milliseconds (Linux: seconds), so `-W2` timed out at ~2ms and
every probe failed — "tunnel did not settle" — even though the tunnel was up
(the VM-side dial checks passed because they ping inside Linux containers).

Add host_ping1(): on macOS use `ping6 -c1 -W2000` (2000ms), on Linux
`ping -6 -c1 -W2`. hping and the client-check settle gate use it; container
pings (cping, dial's gate) stay on Linux ping.
On macOS, assigning the utun address with a subnet prefix (/64) made the
kernel install a connected route whose next hop is the utun's own link-local,
and it then chose that link-local as the source for destinations in that
subnet — notably the router's own tunnel address (fd00:cafe:1100::1) — which
the router can't answer across the tunnel. Symptom: client-check's
`client -> router` failed while `client -> region` (a different /prefix,
routed via the explicit VPC route) and `ping6 -S <utun-addr> <router>` both
worked.

Assign the address as a host (/128) on macOS so no connected subnet route is
created; every VPC address then follows the explicit VPC-prefix route with the
correct global source. The tunnel is point-to-point (one peer), so an on-link
subnet on the interface isn't needed. Linux keeps the caller's prefix length.
The /128 attempt regressed the working direction (region -> client broke), so
the /64 assignment is restored — with /64 the data plane worked in both
directions; the only failure was an auto-source `ping6` to the router's own
tunnel address, which macOS sources from the utun link-local (a source the
router can't answer) because that address shares the client's on-link subnet.

Rather than re-architect the interface addressing, client-check now forces the
source to the client's VPC address on macOS (`ping6 -S <vpc-addr>`), which the
earlier manual test confirmed works. Reaching VPC members in other prefixes was
never affected. Documented the on-link source-selection quirk and a cleaner
follow-up (on-link route as an interface route) in design/vpc-attachment.md §6.
macOS ping6's -W is a valueless flag, not a milliseconds waittime like Linux
ping / macOS ping(8), so `-W2000` parsed as invalid ("invalid option -- 2")
and every client-check probe errored out. macOS ping6 has no per-packet reply
timeout flag; drop it and use `ping6 -c1 -S <vpc-addr> <dst>`, which returns
promptly when the tunnel is up (confirmed: `ping6 -c3 -S <addr> <router>` gets
replies). Linux keeps `ping -6 -c1 -W2`.
…d run

Records the end-to-end validation with the client on a bare-metal macOS laptop
and the VPC on a Linux VM, connected over the public internet dialed by iroh
EndpointId — a topology diagram plus the up / vpc join + client-check / dial
transcripts.
…UN <-> iroh forwarder

Lock all box borders to consistent columns (left/right walls, nested
router box, docker-bridge and region-box connectors) and relabel the
client-side box from the internal '[packet pump]' to '[TUN <-> iroh
forwarder]' so it reads without knowing the code.
@grealish

Copy link
Copy Markdown
Member

Thanks, going to give this a spin

The router now allocates each client's IPv6 address from a pool and
sends it as an Assignment frame (JSON, length-prefixed) on the iroh
stream before packet pumping begins — the wire analogue of the future
VPCAttachmentStatus.assignedAddress CRD flow. Clients no longer need
--address, --prefix-len, or --vpc-prefix; the TUN is created
unaddressed and configured from the assignment.

Wire protocol: bumps ALPN to datum-connect/vpc/1. The first frame on
every stream is a JSON-encoded Assignment{address, prefix_len,
vpc_prefixes}, followed by raw length-prefixed packets as before.

Transport (connect-lib/lib): extracts write_frame/read_frame helpers
from pump, adds Assignment + send/recv_assignment, VpcAcceptHandler
gains on_assign callback, VpcDialer gains dial_and_send_assignment
returning raw stream halves for N:1 mux.

Mock router: rewrites from single-peer/single-pump to N clients with
--peer-id (repeatable), --pool (CIDR), --advertise (VPC prefixes).
Per-client task bridges frames between the client stream and a shared
TUN; TUN reader demuxes by IPv6 destination to the matching client.

Client (Rust + Go): drops --address/--prefix-len/--vpc-prefix from
both binaries. Emits vpc_listening (with endpoint_id) immediately,
then vpc_ready (with assigned address) after assignment.

Labs: test-3region.sh and test-host-client.sh updated; dial now
accepts multiple peer IDs for multi-client testing.

Tested: cargo test (94 pass), go vet clean, test-3region.sh full-mesh
20/20 PASS with router-allocated addressing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants