Added DHT peer resolver, fixed MTU
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user