Every member now also derives an IPv4 address, from the same inputs as its IPv6 one, into 100.64.0.0/10 by default. The range is configurable and IPv4 can be turned off with --no-ipv4. IPv4 is honestly weaker than IPv6 here and the code says so. A 64 bit interface identifier makes an IPv6 collision impossible in practice; IPv4 has nothing like that room, and in a /10 with 50 members two will derive the same address about 0.03% of the time. A mesh with no coordinator cannot allocate around that, so a collision is detected and resolved instead: the member whose public key sorts lower keeps the address, a rule every member computes identically and therefore agrees on. The other keeps IPv6 and is flagged in the status. IPv6 always works; IPv4 almost always works and degrades predictably. Routing and address-ownership enforcement now cover both families: a packet goes to the peer that owns its destination, and a decrypted packet is dropped unless its source is an address derived for the peer that sent it, IPv4 included. Six new tests, among them a real IPv4 packet crossing a tunnel next to an IPv6 one, a spoofed IPv4 source being dropped, and an IPv6-only overlay. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.6 KiB
Testing
How to run everything is in ../README.md. Rules for writing tests are in ../AGENTS.md.
Ground rules
Tests use real iroh endpoints on loopback, real handshakes, real SQLite in per-test temporary directories, and independent agent instances. Discovery is substitutable; iroh, authentication, message passing and persistent storage are not.
The suite runs with no internet, no DHT, no public relay, no administrator
rights and no changes to OS network settings: endpoints bind 127.0.0.1:0 and
[::1]:0, relays are disabled, address lookup is cleared, port mapping is
disabled, and net-report probing is reduced to its minimum.
Synchronisation is always "wait for a specific event or condition under one
overall deadline" (wait_event, wait_until, 30 s). settle() exists only
for asserting that something did not happen. Ports are dynamic and
directories are isolated, so tests run in parallel.
Several library instances in one process is exactly that. It is not a test of several system processes, and is not presented as one.
What is covered
| # | scenario | file |
|---|---|---|
| 1 | deterministic identity: same name + secret ⇒ same space on different devices; a changed name or secret changes it; hostname, device key and restart do not | tests/identity.rs |
| 2 | four agents find each other, authenticate for real and exchange distinguishable messages; a late joiner is picked up; opaque plugin capabilities cross the control plane | tests/multi_peer.rs |
| 3 | an attacker who knows the address and the correct public NetworkId but not the secret is rejected at the handshake |
tests/authentication.rs |
| 4 | one agent in two networks: statuses and messages do not mix; a session authenticated for one network cannot speak for the other; deactivating one leaves the other running | tests/network_isolation.rs |
| 5 | full stop and recreation from the same database: identity and settings survive, sessions come back automatically, a new local UDP port does not break recovery | tests/restart.rs |
| 6 | changing the secret through the library API: device identity survives, old sessions and messages get no access to the new space, and the retired space stays retired across a restart | tests/restart.rs |
| 7 | missing, corrupt and stale cache do not prevent connecting; a corrupt mandatory store is a clear error and never a fresh identity; a newer schema is refused; secrets stay out of status and Debug |
tests/cache_and_state.rs |
| 8 | a dead candidate and a vanished peer do not block the others; retries are bounded and stop when the network is deactivated | tests/resilience.rs |
| 9 | wrong version, a message before authentication, a proof replayed on another connection, an oversized frame and a Hello for an inactive network are all rejected without taking the agent down |
tests/authentication.rs |
| 10 | a second agent on the same state directory gets a clear error; after a clean stop the directory reopens; shutdown ends background tasks and refuses further work; independent agents coexist in one process | tests/resilience.rs |
tests/wireguard.rs drives the WireGuard data plane over real iroh
connections. Everything is real except the packet interface: real agents, real
control plane, real data links, real WireGuard handshakes and encryption from
boringtun, with an in-memory TUN device so none of it needs privileges. It
covers real IPv6 packets travelling both ways through a tunnel, a three-agent
mesh, a peer that sends from an address it does not own being dropped, packets
for unowned addresses being counted rather than broadcast, a departing peer
losing its tunnel, two networks keeping separate interfaces and keys, restart
keeping the WireGuard identity, shutdown removing every interface, a forged
overlay claim being rejected, and the core carrying the payload without
interpreting it.
tests/local_control.rs covers the local control socket end to end: a client
asking a running agent for status over a real Unix socket, a leftover socket
file being replaced while a live one is not, and the derived socket path
staying short enough to bind.
tests/discovery.rs covers the discovery contract itself: a static bootstrap
candidate is enough to join, several backends compose, entries are withdrawn
when a network stops, and a forgotten network stays forgotten across a restart.
Unit tests in src/proto/handshake.rs cover the transcript construction
itself: role separation, channel binding, identity and network binding,
unambiguous encoding, and rejection under the wrong key.
Unit tests in src/dataplane/wireguard/ cover key clamping against the RFC
7748 vector, overlay derivation, announcement validation including the
address-hijack attempt, interface naming, and IP header parsing against
truncated and nonsense input.
tests/end_to_end.rs is the vertical slice: persistent identity → network
space → discovery → iroh → authentication → message exchange.
What the default suite does not cover is the real TUN interface, because
that needs CAP_NET_ADMIN. Everything above it does run.
Not covered, and not claimed to be
Listed in sync-model.md: snapshots, revocations, long partitions, hostname renames, recovery of a returning participant, NAT traversal, relay fallback, and multi-process or multi-host deployment. None of these are implemented, and none are marked as passing.
Debugging a test
TSUNAGI_TEST_LOG=tsunagi=debug cargo test --test multi_peer -- --nocapture
The library never installs a global subscriber; the harness opts in only when that variable is set.