Make the protocol a crate of its own

tsunagi-wg-quic. The line between a protocol and the system level is now
drawn by the compiler: nothing in it can reach into tsunagi beyond what
tsunagi makes public, and it carries its own version — which is not the
version peers compare.

Two things the compiler found the moment the boundary was real. The key
store was reaching into the core's `pub(crate)` file-permission helpers;
those are a legitimate service of the system level, because a protocol
keeping keys on disk has the same obligation the agent does, so they are
public now with that said. And the test harness was about to be copied
into a second crate, which is how two copies start to drift; it is a
`testing` feature of the core instead, which is also what anybody writing
a protocol would need.

The bridges put up while things were moving are gone: the error
conversion between the two levels, and the re-exports of the system
level's types from the protocol crate. Imports now say which level they
come from, which is the point.

One deliberate deviation, stated rather than hidden. The authenticated
transport stayed in the core. Moving it would have meant handing a
protocol the network's keys so it could prove membership itself, and a
plugin that can authenticate on the control plane is a worse trade than
a module boundary is worth. So the core proves who is at the other end
and the protocol owns what is said over it — the same separation, without
the secret crossing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
tsunagi
2026-09-21 19:55:30 +01:00
co-authored by Claude Opus 5
parent ff7e235414
commit 142fdf995c
31 changed files with 289 additions and 234 deletions
+24 -8
View File
@@ -18,18 +18,31 @@ Design accordingly: a majority is not a root of trust.
Keep these separate. Crossing them is the main thing to review for.
- **Control plane vs data plane.** The separation is **logical, not physical**.
The control protocol in `crates/tsunagi/src/proto/` knows nothing about packets, and
`crates/tsunagi/src/dataplane/` knows nothing about the control protocol; either can be
replaced on its own. Both may ride on iroh — refusing to would throw away
iroh's NAT traversal and force the data plane to reimplement it. They use
different ALPNs and different connections, so a busy or broken data plane
cannot disturb control traffic.
The control protocol in `crates/tsunagi/src/proto/` knows nothing about
packets, and a protocol crate knows nothing about the control protocol;
either can be replaced on its own. Both may ride on iroh — refusing to
would throw away iroh's NAT traversal and force the data plane to
reimplement it. They use different ALPNs and different connections, so a
busy or broken data plane cannot disturb control traffic.
- **One agent, one interface.** It belongs to the system level, along with
the addresses on it and the decision of whose packet is whose. Several
protocols may be carrying traffic at once and none of them owns the thing
they carry it for. A protocol is handed a routed packet and hands back a
decrypted one; it never creates an interface and never picks an address.
- **A protocol is a separate crate with its own version.** The version peers
compare is the *wire* version, never the software version: two peers on
different releases work together for as long as the bytes between them
have not changed. Nothing negotiated may be derived from anything that
moves with a release.
- **A protocol never sees the network secret.** It proves who is at the other
end of a tunnel; proving membership of a network stays in the core, which
is why the authenticated transport does too.
- **Plugins never learn reachability.** An `IpPlugin` is handed a `PacketLink`
per peer and moves datagrams over it. Addresses, hole punching and relays
belong to `crates/tsunagi/src/dataplane/transport/`. A plugin announcement says *who*, never
*where*.
- **The core never parses a plugin payload.** See `crates/tsunagi/src/dataplane/mod.rs`. Only
`crates/tsunagi/src/dataplane/wireguard/announcement.rs` interprets WireGuard payloads, and
`crates/tsunagi-wg-quic/src/announcement.rs` interprets `wg-quic` payloads, and
only after bounding every field. A data plane failure must never stop the
control plane.
- **Derived, not claimed.** A peer's overlay address is derived from its public
@@ -106,7 +119,10 @@ Keep these separate. Crossing them is the main thing to review for.
| `crates/tsunagi/src/net.rs` | iroh endpoint adapter and observability snapshots |
| `crates/tsunagi/src/agent/` | agent lifecycle, per-network runtimes, sessions, events, status |
| `crates/tsunagi/src/state/` | signed records that outlive a session, their merge rules and address allocation |
| `crates/tsunagi/src/dataplane/` | the plugin contract, the packet transport, and the WireGuard plugin |
| `crates/tsunagi/src/overlay/` | the one interface an agent owns: provisioning, the TUN, routing, source checks |
| `crates/tsunagi/src/dataplane/` | the protocol contract and the authenticated packet transport |
| `crates/tsunagi/src/dns/` | the DNS view of a network: zone, server, resolver publication |
| `crates/tsunagi-wg-quic/` | the `wg-quic` protocol: its keys, its announcement, its tunnels |
| `crates/tsunagi-cli/` | the command line agent; the only place that owns a runtime, a logger and signals |
| `tests/` | integration tests; `tests/common/` is the shared harness |