net/transport/iroh: iroh transport with relay fallback - #768
Open
cheggaaa wants to merge 10 commits into
Open
Conversation
Adds a transport over github.com/tmc/go-iroh (QUIC, relay fallback, hole punching) for direct client-to-client sync across the internet. - endpoint identity is the device peer key: any-sync peer id and iroh endpoint id convert both ways (ident.go), so a dial verifies the ticket id against the expected peer id and inbound connections map RemoteID to a peer id before the any-sync handshake - the any-sync handshake runs on the first stream, as for QUIC, so identity, proto version and credentials are unchanged - addresses are endpoint tickets: "iroh://<ticket>"; Ticket() exposes the relay-only ticket of this endpoint (direct IPs when no relay is configured), TicketUpdates wakes on change - SetIncomingFilter gates inbound peers before the handshake - peerservice: iroh ranks after every LAN/node scheme, skips the local address probe, and is dialed only when the ctx opts in with CtxWithGlobalDial, so LAN and node dial paths never wait on a relay - go 1.26 (required by go-iroh) SYN-190
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…ckets The relay drops frames for endpoints it has not registered, so a relay ticket is published only once Online succeeds and a relay dial waits for it (no-op once up). Adds a round trip through an embedded relay.
- accept takes incoming attempts before their QUIC handshake and finishes each on its own goroutine, bounded globally (MaxInflightAccepts) and per source IP; excess attempts are refused before any handshake work - Run requires an incoming filter: the relay makes the endpoint reachable from the whole internet - a dial waits for HandshakeComplete before the any-sync handshake (a resumed session returns early with 0-RTT) and checks the verified remote id; ticket addresses are capped and filtered before Connect - relay URLs must be https with a host; http only with InsecureRelay - net reports are enabled with relays so hole punching has candidates; keep-alive defaults to 25s like quic; InitialPacketSize is configurable - a direct ticket for an unspecified bind carries the interface addresses - peerservice looks the transport up by transport.IrohCName so binaries that never register it do not link go-iroh; tickets are shortened in logs; PeerIdFromTicket and RelayConnected exported for consumers
- CloseWithError blocks on connection teardown, so Close waits at most CloseTimeoutSec for it, as the quic transport does - an unspecified [::] bind expands to both address families, 0.0.0.0 to IPv4 only, so a default bind stays dialable over IPv4 - irohNetConn routes ReadFrom through Write so io.Copy honours the write deadline and the byte counter - the accept loop exits on a dead transport; a home relay that never connects is logged
A row is addressed by key+"-"+peerId, but the id came from the proto unchecked: a member could plant a row at another peer's id with a far-future timestamp and block that peer's genuine rows on every replica. KeyValueFromProto now rejects an id that differs from the signed key and peer.
A relay-backed connection costs a full handshake to re-establish, and the pool's one-minute idle TTL was closing accepted iroh peers between sync ticks. The transport now puts a TTL into the connection context (PeerTTLSec, default 30 min) and NewPeer applies it on both sides.
TryClose closed a peer with no sub conns one hardcoded minute after creation, so SetTTL never applied to a connection held for reachability alone — relay-backed global peers were dropped and re-dialed every GC pass. The idle bound is now max(ttl, minute).
…orPeer - DeriveDiscoveryKeys derives from the identity seed the ed25519 key that addresses and signs the account's device-discovery record (slip-10 m/99999'/2', frozen) and the symmetric key that encrypts it, so every device and a fresh restore agree while the identity key never signs the record - the iroh transport gains a handshake-stage filter that sees the remote's proven identity, and TicketForPeer builds the relay-only ticket of a peer from its peer id and home relay
- HandshakeInbound verifies the remote's signed identity when the ctx carries CtxAllowAccountCheck, independent of the service default; the iroh transport sets it on both sides, so a client accepting internet connections always learns a proven account and a filter can admit by it; a connection without one is refused when a filter is set - the handshake filter receives the identity as crypto.PubKey - TicketForPeer validates the relay URL like a configured relay - DeriveDiscoveryKeys checks the key type; known-answer test pins the frozen derivation; the root seed is documented as the identity's 32-byte ed25519 seed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
New transport over tmc/go-iroh v0.1.0 (QUIC + relay fallback + hole punching) for direct client-to-client sync beyond the LAN. Nodes are unaffected and never link it; the SDK wires it (separate PR) and discovers peers through per-space key-value records.
What
net/transport/iroh:transport.Transport+app.ComponentRunnable. Endpoint identity is the device peer key, so any-sync peer id ↔ iroh endpoint id convert both ways (ident.go,PeerIdFromTicket); a dial verifies the ticket id against the expected peer id and re-checks the TLS-verified remote id after the handshake completes (a resumed session returns early with 0-RTT; the any-sync handshake never goes out as early data).iroh://<ticket>.Ticket()is the relay-only ticket of this endpoint, published only once the home-relay session is up (direct interface addresses when no relay is configured);TicketUpdates()wakes on change;RelayConnected()for status.SetIncomingFilteris mandatory (Runfails without it) — the relay makes the endpoint reachable from the whole internet. Attempts are taken pre-handshake and finished on their own goroutines, bounded byMaxInflightAccepts(64) globally and 8 per source; excess is refused before any handshake work.https://with a host;InsecureRelayadmitshttp://for dev/test relays (go-iroh's owncmd/iroh-relayhas no TLS). Net reports are enabled with relays so hole punching has reflexive candidates.peerservice: iroh ranks after every LAN/node scheme, never runs the local-address probe (a ticket is not a host), and is dialed only when the ctx opts in viaCtxWithGlobalDial— LAN and node dial paths can never wait on a relay dial. Tickets are shortened in logs. The component is looked up bytransport.IrohCName, so binaries that never register it do not link go-iroh.go 1.26— required by go-iroh's owngodirective; every downstream module (node, coordinator, filenode, heart, mobile) moves with it. go-iroh is pure Go with one indirect dep (coder/websocket, ISC — needs a license decision inanyproto/open); it vendors a quic-go fork, so the binary carries a second QUIC stack.Also
peer.CtxWithTTL/CtxTTL: a transport can set the pool TTL of its peers through the connection context;NewPeerapplies it on both sides. The iroh transport setsPeerTTLSec(30 min): the pool's 1-min idle GC was closing accepted relay connections between sync ticks, at a full handshake (≈13 KB) each time.peer.TryClose: a peer without sub conns now lives formax(ttl, 1 min)instead of a hardcoded minute —SetTTLnever applied to a connection held for reachability alone, so relay-backed peers were closed and re-dialed every GC pass (seen in the cross-machine hand-test: ~70 s up / ~50 s down, 13 KB per re-handshake).keyvalue:KeyValueFromProtonow binds the row id to the signed key + peer (KeyPeerId(key, peerId)). The id came from the proto unchecked, so a member could plant a row at another peer's id with a far-future timestamp and block that peer's genuine rows on every replica — the global-p2p discovery record relies on one row per device.Known upstream limitation
go-iroh's magic socket grows its per-source and relay-mapped address tables without eviction for untracked sources (
internal/socket/transport.go,socket.go). Relay-only tickets keep direct exposure low; the version is pinned so an upgrade past a fix is deliberate.Tests
-race): loopback round trip through the accepter, relay round trip through an embeddedrelayserver, handshake ctx (peer id / identity / proto), expected-peer-id mismatch, bad ticket, incoming filter, filter required, close →transport.ErrConnClosed, dial timeout on a dead ticket, close during dial, ticket coalescing, direct tickets for0.0.0.0/[::]binds, ticket address caps, in-flight accounting,io.Copycounted, byte counters, identity round trip.SYN-190
🤖 Generated with Claude Code