Derived IPv4 addresses could not survive anything: they changed with the range, and there was no way for a member to come back to the one it had. Addresses are now allocated and recorded as signed facts, which is the first slice of the model in docs/sync-model.md. src/state/ holds one record per author per network, carrying that author's complete current statement, signed with its persistent device key over a length-prefixed canonical encoding. Merging follows the model's rules: a higher version wins, an older one never rolls back a newer, duplicates are idempotent, absence from a snapshot is not deletion, and a same-version conflict is resolved identically on every replica and reported rather than letting replicas diverge. Records are persisted in state.sqlite, with the record and the author's version counter committed in one transaction before anything is announced, and distributed as a State control message that is merged into what the receiver already holds. No vote, deliberately, despite the request. A majority is not a trust root here — anyone with the secret can mint identities — and a quorum would stall with one peer online and diverge across a partition. Signatures plus a deterministic merge converge without either failure mode: two members claiming one address at once are resolved by the lower endpoint id, and the loser allocates again with a higher version. The range moved from the plugin to the agent, defaults to 10.13.37.0/24, and is now agreed rather than configured per member: a joining agent adopts what the network already uses, so --ipv4-range only matters for whoever starts it. The announcement went back to identity only (version 3) since the range travels in signed records now. A release tombstone exists and merges correctly, but nothing emits one yet. 116 tests. The headline ones: an address survives restarting both agents, three members get three distinct addresses, and a member started with a different range adopts the one in use. Confirmed by hand with two CLI agents restarted end to end. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
101 lines
6.1 KiB
Markdown
101 lines
6.1 KiB
Markdown
# Testing
|
|
|
|
How to run everything is in [../README.md](../README.md). Rules for writing
|
|
tests are in [../AGENTS.md](../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.
|
|
|
|
Unit tests in `src/state/` cover the signed record model directly: tampering
|
|
with any field breaks verification, a newer version wins while an older one
|
|
never rolls back, two authors claiming one address resolve the same way no
|
|
matter the merge order, one key used in two places is reported rather than
|
|
silently merged, a release survives a late-arriving old claim, a bad record in
|
|
a batch does not stop the rest, and allocation is deterministic, spread out,
|
|
walks past everything taken and reports a full range instead of handing out a
|
|
duplicate.
|
|
|
|
`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](sync-model.md#future-tests): 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
|
|
|
|
```bash
|
|
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.
|