Files
tsunagi/docs/testing.md
T
tsunagiandClaude Opus 5 ea7aaa2b69 Implement the WireGuard data plane plugin
The first IP plugin, built on the data plane boundary the core already had.

Plugin:
- one X25519 key per network in the plugin's own wireguard.sqlite, separate
  from the iroh identity and from the network secret; a damaged store is an
  error, never a silently regenerated identity
- deterministic IPv6 ULA overlay: every member derives the same /64 from the
  network id and its own /128 from its WireGuard public key, so no
  coordinator allocates addresses
- AllowedIPs are derived locally, never taken from a peer's announcement, so
  a member cannot claim another member's overlay address; a mismatched claim
  is rejected
- bounded, versioned, validated announcement carried as the existing opaque
  capability payload, which the core still never parses
- each agent builds its own full-mesh configuration (N-1 peers) and
  reconciles on every change and on a timer, repairing drift
- WireguardBackend abstraction: RecordingBackend in memory, and WgToolBackend
  driving real wg/ip on Linux, split into a pure planner plus parsers and a
  thin executor so everything interesting is testable without root

Core, three generic additions the plugin needed:
- IpPlugin::on_network_activated, so per-network state is ready before peers
- PluginContext for re-announcements and error reports from plugin tasks,
  with errors counted by the owning network runtime
- IpPlugin::shutdown, awaited with a grace period, so system objects go away

94 tests pass offline with no privileges: 35 new WireGuard unit tests and 12
integration tests over real iroh connections. The real wg/ip backend needs
root and is behind --ignored in tests/wireguard_system.rs; it was not run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 11:07:31 +01:00

5.3 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 plugin over real iroh connections with the in-memory backend: a pair and a three-agent mesh converge to N - 1 peers with locally derived AllowedIPs, a departing peer is removed, two networks get separate interfaces, keys and overlays, reconciliation repairs a configuration edited by hand, a backend failure leaves the control plane untouched, a restart keeps the WireGuard identity, shutdown removes every interface, a member claiming another member's overlay address is rejected, and the core carries the payload without interpreting it.

tests/wireguard_system.rs exercises the real wg/ip backend. It is ignored by default because it changes the host's network and needs Linux, wireguard-tools and CAP_NET_ADMIN.

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 the parts that would otherwise need root: key clamping against the RFC 7748 vector, overlay derivation, announcement validation including the address-hijack attempt, configuration building and rendering, the exact command plan the real backend would run, and parsing wg showconf and ip address show output.

tests/end_to_end.rs is the vertical slice: persistent identity → network space → discovery → iroh → authentication → message exchange.

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.