Added DHT peer resolver, fixed MTU

This commit is contained in:
ab
2026-09-22 15:44:33 +03:00
parent 72eaf9a228
commit 9698f21d55
27 changed files with 2041 additions and 161 deletions
+90
View File
@@ -0,0 +1,90 @@
# Mainline rendezvous
`tsunagi up` enables public Mainline DHT discovery. `--no-dht` disables it;
`--dht` explicitly selects the default. `--reach local` always disables it.
Manual `--peer` entries work alongside DHT or on their own. Library callers
opt in with `AgentConfig::with_dht(MainlineDiscovery::default())`; merely
constructing a configuration opens no DHT socket.
One Mainline client serves an agent's active networks. Each network gets its
own BEP44 signing seed, derived with the new `mainline-rendezvous-write-v1`
HKDF label. Existing network IDs, discovery keys and handshake keys are
unchanged. The semi-public `DiscoveryKey` is not used as signing material.
No network name, secret, or signing seed is included in a record or log.
## First contact and recovery
Each active network starts publication and lookup independently. Lookup
streams candidates to the existing dial loop, starting connections before
all slots have been read. Failed/empty rounds retry with increasing delay
(5 to 30 seconds, with jitter). An empty lookup never proves a network empty.
The first successful membership handshake, including an incoming handshake,
stops lookup. Publication continues while there are connected peers.
After the last authenticated session ends, known peers continue to be retried.
If none connects within 60 seconds, DHT lookup resumes. A successful handshake
stops it again. This policy is per network. DHT does not heal a partition in
which every component still has an authenticated neighbor.
Publisher and lookup tasks run outside the network actor, with deadlines,
bounded candidate delivery, and cancellation on deactivation or shutdown.
An unreachable DHT never prevents status, normal sessions or manual bootstrap.
The client socket is released on agent shutdown. The library uses no global
client, runtime, or mutable rendezvous table.
Client clones share that lifetime: create a new `MainlineDiscovery` for a new
agent after shutdown, including when injecting a local test client.
## Records and concurrency
The protocol uses 16 BEP44 salts (`tsunagi-rendezvous-v1` followed by the slot
byte) under the network's signing key. Every publisher chooses two distinct
slots deterministically from SHA-256 of its endpoint ID. Each value describes
only its writer: a format version, publication time, endpoint ID, one relay
URL and up to eight IP addresses. Bencoded values stay within BEP44's 1000-byte
limit. Decoding bounds every field before allocation and rejects unknown
versions, malformed records, timestamps too far in the future and stale data.
Slots hold a changing sample, not the complete membership. Overwrites are
expected and 16 is not a network-size limit. Publishing reads the latest
sequence, increments it, and uses BEP44 CAS with bounded retry on conflict.
CAS is local to each storage node and is not a distributed lock. Valid records
encountered during publication may become candidates on the next explicit
lookup; they never cause connected networks to start dialing from publication.
Lookup queries at most four slots concurrently and returns up to 16 distinct
candidates. At most 16 discovery candidates are retained by default; failed
candidates can be replaced rather than permanently excluding later results.
Values are signed but not encrypted. Possession of a discovery result grants
no network access: iroh authenticates the device and the control handshake
proves membership. Public rendezvous is not an anonymity mechanism.
## Freshness and durable state
Every five minutes (20% jitter), and on an observed endpoint-address change,
the agent publishes fresh data. The address monitor checks every five seconds
by default. Readers accept records for 15 minutes, allowing up to two minutes
of future clock skew. The host clock must therefore be reasonably accurate.
This is application freshness, not a request for DHT nodes to delete data.
BEP44 has no deletion operation. Deactivation stops publication; it never
deletes a shared slot that another member may now occupy. Records expire from
our readers even if storage nodes keep them. A restart restores identity and
active networks from `state.sqlite`, computes the same rendezvous location,
and publishes the current address. Old DHT entries and disposable cache are
not required. A publish reads sequence numbers from DHT, so restarts do not
reset the sequence of a surviving mutable item.
## Tests
The default suite uses Mainline Testnet nodes bound to loopback, real iroh
connections, real membership handshakes, and SQLite in temporary directories.
It covers concurrent publication, colliding slots, network-secret isolation,
record validation, bootstrap after all DHT records and cache disappear,
connected-state lookup suppression, isolation recovery and hung backends.
Public DHT and relay checks are opt-in and are not part of the offline suite.
Run the optional public check with:
```sh
cargo test --locked -p tsunagi --test mainline public_dht_finds_and_authenticates_two_agents -- --ignored --exact
```
+24 -4
View File
@@ -135,13 +135,13 @@ event does not report a network id.
## The data plane protocol
IP plugin packets never travel on a control connection. They use their own
ALPN, `tsunagi/data/1`, on their own iroh connection:
ALPN, `tsunagi/data/3`, on their own iroh connection:
```text
initiator -> responder : (the same membership handshake as above)
initiator -> responder : DataOpen { protocol }
initiator -> responder : DataOpen { protocol, max_datagram }
initiator <- responder : DataOpenAck { accepted, max_datagram }
thereafter : QUIC datagrams carrying that plugin's packets
thereafter : QUIC datagrams carrying fragments of opaque payloads
```
The membership handshake is identical and bound to the same network, so a data
@@ -154,7 +154,27 @@ never open two channels for the same thing.
Packets ride as QUIC **datagrams**: unreliable and unordered, which is what a
tunnelled protocol wants, and free of the head-of-line blocking a stream would
add. The datagram limit is what caps a plugin's MTU.
add. `max_datagram` negotiates the reassembled payload limit (at most 65536
bytes), independently of the current QUIC path MTU. The transport reads QUIC's
current datagram capacity for every fragment, including after migration.
Every datagram starts with a 16-byte header: packet ID (`u64`), total payload
length (`u32`), and byte offset (`u32`), all big endian. The payload is the
complete peer-relay envelope containing opaque plugin bytes. Relays reassemble
before forwarding and fragment again for their outgoing path; they never
decrypt the plugin's payload. Small packets use the same framing in one part.
Reassembly tolerates reordering and exact duplicates. Conflicting lengths,
overlapping data and excessive fragment counts discard that packet. Each link
allows at most 64 incomplete packets, 256 KiB of their payload buffers, and 128
fragments per packet. Incomplete packets expire after five seconds, without
holding up subsequent packets. Packet IDs are scoped to a QUIC connection;
reassembly is created only after the membership handshake. All limits live in
`config.rs`. There is no transport-layer retransmission added by this framing.
Version 3 is incompatible with previous data ALPNs. Upgrade both endpoints
and intermediate peers together. The control protocol, network secret,
device identity and stored network configuration are unchanged.
Separate connections mean separate congestion control, so a saturated data
plane cannot delay control messages, and a data plane failure cannot take the
+17 -1
View File
@@ -10,7 +10,7 @@ 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
The suite runs with no internet, no public 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.
@@ -55,6 +55,14 @@ keeping the WireGuard identity, shutdown removing every interface, a forged
overlay claim being rejected, and the core carrying the payload without
interpreting it.
The WireGuard traffic tests constrain real QUIC to a 1200-byte path MTU with
PMTU discovery disabled, so large loopback MTUs cannot hide Internet failures.
Checksummed IPv4/TCP packets up to the default 1280-byte interface MTU cross in
both directions without changing DF or packet contents, including through an
intermediate peer. Fragment tests cover reordering, duplicates, loss, changing
fragment sizes, malformed input, timeout and memory bounds. These are packet
transport checks, not a claim to have run an SSH server or a host TCP stack.
`crates/tsunagi/src/dataplane/relay.rs` has its own tests for the way
through a peer in the middle: the wrapping and what a malformed one does, a
direct path being preferred over a hop, the middle passing a datagram on
@@ -114,6 +122,14 @@ 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.
Mainline discovery tests use an isolated loopback `mainline::Testnet`.
`tests/mainline.rs` restores two agents after their DHT and disposable cache
have disappeared. `tests/discovery_lifecycle.rs` checks that publication
continues while connected, lookup resumes after isolation, and a hung backend
does not block the network actor. Unit tests cover record bounds, timestamps,
colliding publications, wrong secrets and the full recovery delay with a
paused clock. The public Mainline/relay smoke test is ignored by default.
## Not covered, and not claimed to be
Listed in [sync-model.md](sync-model.md#future-tests): snapshots, revocations,
+18 -16
View File
@@ -167,24 +167,26 @@ than silent non-connectivity.
## MTU
Two constraints pull against each other.
The default interface MTU is 1280. The IPv4 overlay permits explicit values
down to 576, but a small physical path does not require lowering the interface
MTU. WireGuard adds 32 bytes; peer relaying also has an envelope. Previously,
a path advertising only 1129 bytes dropped large packets despite the tunnel
being established: ping worked while TCP connections stalled.
**IPv6 sets a floor of 1280 bytes** (RFC 8200), and Linux enforces it
brutally: an interface whose MTU drops below 1280 loses IPv6 entirely — its
`/proc/sys/net/ipv6/conf/<dev>` directory disappears and `ip -6 address add`
answers `Invalid argument`. So the overlay MTU cannot go below 1280, and the
plugin refuses a smaller one at startup instead of letting it fail obscurely.
The iroh transport now fragments the opaque, encrypted payload according to
the current QUIC datagram limit. It reassembles before handing the ciphertext
to WireGuard or forwarding it through an intermediate peer. This happens below
IP: the inner TCP segment, checksums and DF flag are unchanged. No MSS rewriting
or special SSH settings are needed. This follows the application responsibility
described in [RFC 9221 section 5](https://www.rfc-editor.org/rfc/rfc9221.html#section-5):
QUIC DATAGRAM frames themselves cannot fragment.
**The transport sets a ceiling.** Every packet rides in one datagram and
WireGuard adds 32 bytes, so a link must carry `mtu + 32` = 1312 bytes. A direct
QUIC path typically offers around 1380, which fits. A relayed path can offer
less, and then full-size packets do not fit: they are dropped and counted as
`dropped_oversize`, never truncated, and the plugin reports the exact numbers
when the tunnel is set up.
There is no room left to trade, so the default MTU is exactly 1280.
Fragmenting a packet across several datagrams would lift the ceiling and is
not implemented.
Reassembly is bounded and expires incomplete packets; one lost fragment loses
one packet, without blocking unrelated traffic. The logical data payload limit
is 64 KiB, with the relay envelope subtracted before it reaches the plugin.
The new framing uses data ALPN `tsunagi/data/3`; both ends and intermediate
peers need the updated binary. Network identities and saved state do not change.
See [protocol.md](protocol.md#the-data-plane-protocol) for the wire format.
## Lifecycle