One agent, one interface. The plugin no longer creates one, no longer
holds a TUN factory, no longer keeps a routing table and no longer
decides who owns an address. What is left of it is the protocol: a
WireGuard key per network, a tunnel per peer, encryption on the way out
and decryption on the way in.
The packet path is now explicit about where each decision lives. Out: the
agent's interface reads a packet, the routing table says whose
destination it is, and each protocol is asked in turn whether it can
carry it there. In: the protocol decrypts and hands the packet up with
the peer it came from attached, and the agent checks that peer is
entitled to the source address before writing it out. A protocol proves
who; only the system level knows what they may say.
Two things found by making it work.
`carry` sent the packet unencrypted at first. The encryption had lived in
the interface loop that moved to the core, so taking that out quietly
removed it — the receiving end rejected plaintext as a bad WireGuard
datagram and the counters said nothing at all. Encryption belongs with
the protocol and is now there, with packets dropped for having no session
yet counted apart, because a handful while a tunnel comes up is normal
and a number that keeps climbing is not.
An agent could impose a range it could not itself route. With one
interface two networks need different ranges, and "the lowest author's
range wins" would have carried one agent's colliding default to
everybody. The configured range is now reserved when a network is
activated — on the serialised path, so the answer does not depend on
which task ran first — and an agent that cannot have it proposes nothing
and adopts whatever the network settles on.
Leaving a network takes its address off the interface and leaves the
interface; the interface goes when the agent does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The overlay address was derived from a WireGuard key, which makes it the
protocol's address — and the whole point of the interface belonging to
the system level is that every protocol carries traffic for the *same*
addresses. A derived-per-protocol address cannot be that.
So the overlay is IPv4 only: allocated at the system level, signed by the
member that holds it, and the same address whichever protocol happens to
be moving the packets. The derivation, its ULA prefix and its constants
are gone, along with the collision rule that existed only because a
derived IPv4 address has too little room to be unique — an allocated one
is unique by construction.
A real loss came with it and is restored explicitly. The announcement was
bound to its network only as a side effect of checking the derived
address, so removing that check removed the binding. It now carries the
network id and rejects a mismatch. Strictly redundant, because a
capability arrives on a session that already proved membership, and kept
because losing a property silently is the wrong way to lose one.
An unlock falls out: the MTU floor of 1280 existed because Linux tears
IPv6 down below it. Without IPv6 the floor is 576, what every IPv4 host
must be able to reassemble, so a relayed path with small datagrams can be
matched rather than warned about. The default stays 1280.
The test that forged an overlay address now forges a network id, which is
what is left to lie about. One flaky assertion fixed while passing: it
waited for "the interface has some address", which is briefly true of the
leftover it was meant to see replaced.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First step of separating the layers. The library and the binary are now
crates/tsunagi and crates/tsunagi-cli, which means the plugin crate to
come can be told apart from the core by the compiler rather than by
discipline.
Falls out of it immediately: the CLI's dependencies stop being features
of the library. clap, anstream and tracing-subscriber were optional
dependencies behind a `cli` feature that every library user had to
remember to turn off; now they belong to the crate that uses them, and
the library defaults to no features at all.
The one test that drives the binary moved beside it — a library cannot
depend on a binary built from a crate that depends on the library — and
was rewritten against the public API instead of the test harness.
AGENTS.md said to prefer one crate. It now says the system level and its
plugins are separate crates, for the reason above, and that everything
else stays one crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>