Skip to content

feat: LAN-direct — Locator seam + QUIC + mDNS (C1 + C2)#42

Closed
frahlg wants to merge 8 commits into
b1.4-b1.5-wallet-wirefrom
c1-c2-lan-direct
Closed

feat: LAN-direct — Locator seam + QUIC + mDNS (C1 + C2)#42
frahlg wants to merge 8 commits into
b1.4-b1.5-wallet-wirefrom
c1-c2-lan-direct

Conversation

@frahlg

@frahlg frahlg commented Jun 13, 2026

Copy link
Copy Markdown
Member

Implements docs/superpowers/specs/2026-06-13-c1-c2-lan-direct-locator-design.md
(approved 2026-06-13). CLI-only, Go-only — the browser keeps using the relay; no web
changes, no byte-identical crypto gate touched.

Stacked on #41 (B1.4+B1.5). It reuses ownerPubFromBinding, the cached wallet
binding, and the base58 owner_id. Base is b1.4-b1.5-wallet-wire; once #41 merges to
main I'll rebase this onto main and retarget.

What this delivers

mir attach reaches a mir up node on the same LAN with no relay — zero-config mDNS
discovery + a direct QUIC transport — by inserting a Locator seam under the unchanged
Noise-KK session. The relay/WAN path is untouched.

How it works

  • C1 — Locator seam (pure refactor). A Locator (package client) turns a Machine
    into a live peer.MsgConn; Attach composes [LAN, relay] and runs Noise-KK over the
    first that connects. Today's relay path moves verbatim into relayLocator. Relay e2e
    stays green
    (behavior-preserving).
  • C2 — LAN-direct (LANLocator).
    • Discovery: mir up advertises _miranda._udp (instance = machine_id); the client
      browses + matches. Discovery yields only an address — never trust.
    • Transport: QUIC with an ephemeral self-signed cert + skip-verify; the real auth
      is Noise-KK + the wallet binding that runs inside the stream (internal/quicmsg, a
      length-framed peer.MsgConn).
    • Wire: the wallet binding is frame 0; the agent checks IsOwnerPinned +
      VerifyBinding (reusing ownerPubFromBinding), pins binding.x25519, then runs the
      same authenticated PTY session as the relay path (serveAuthenticated).
    • Default + escapes: LAN-first with a ~600 ms budget then relay fallback;
      mir attach --relay-only and mir up --no-lan.

Trust (unchanged) + new surface

Locate but never impersonate: Noise-KK pins host_pub, the agent pins the wallet. mDNS
spoofing / a rogue LAN host yields at worst a failed handshake (DoS) — never
impersonation or plaintext. New exposure: the agent now accepts inbound LAN connections
(bounded by the same pre-auth limiter) and mDNS reveals an opaque machine_id. Both off
with --no-lan. Documented in SECURITY.md.

QUIC-over-WAN is the destination, not this step

WAN's hard part is NAT traversal (ICE), which QUIC alone doesn't solve; WebRTC already does
and ships. A future QUICHolePunchLocator (DCUtR + circuit-relay, north-star C4) drops into
the same seam — captured as a stated future, not built.

Tests

  • internal/quicmsg: length-framed round-trip (incl. 70 KB frame) + ctx-cancel.
  • client: lanLocator sends frame0 binding; ErrUnreachable on miss/no-wallet;
    attachLocators composition; dialFirst fallthrough; skippable live-mDNS resolve.
  • agent: real-shell echo over QUIC, no relay; unpinned binding rejected pre-Noise;
    relay e2e still green after the serveAuthenticated extraction.
  • go test ./..., go vet, gofmt clean. Real mir binary exposes --relay-only /
    --no-lan.

New deps (non-crypto, Go-only)

github.com/quic-go/quic-go v0.60.0, github.com/grandcat/zeroconf v1.0.0.

Follow-up (not in this PR)

  • deploy/netsim LAN path (cross-container mDNS); the Go tests already prove the wire.
  • happy-eyeballs race to remove the ~600 ms LAN-miss penalty (kept sequential here to avoid
    adding concurrency to the working relay path).

🤖 Generated with Claude Code

frahlg and others added 7 commits June 13, 2026 13:07
Approved 2026-06-13. CLI-only, Go-only. A Locator seam (in package client)
turns a Machine into a live peer.MsgConn; Attach composes [LAN, Relay].
LAN uses QUIC (self-signed + skip-verify; real auth = Noise-KK + binding)
and mDNS discovery. Relay/WAN path unchanged. QUIC-over-WAN (DCUtR) is a
stated future locator, not built now.

Stacked on the B1.4+B1.5 branch (reuses ownerPubFromBinding, the cached
binding, and wallet owner_id).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a Locator interface (package client) that turns a Machine into a
live peer.MsgConn; Attach composes [relayLocator] and runs Noise-KK over
whatever connects. Today's relay path moves verbatim into relayLocator.
Attach now returns peer.MsgConn (was *peer.DataChannel; consumers use only
Send/Recv). Behavior-preserving: the relay e2e tests stay green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A length-framed QUIC bidi stream as a peer.MsgConn, shared by client (LAN
dial) and agent (LAN listen). QUIC TLS is dumb transport: the real auth is
Noise-KK + the wallet binding that runs inside, so ClientTLS skips
verification. ServerTLS uses an ephemeral self-signed cert. ALPN
miranda/lan/v1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A lanLocator that resolves a machine_id to a LAN address (mDNS in prod,
injectable resolver in tests), QUIC-dials it, and sends the wallet binding
as frame 0 before Noise-KK. Returns ErrUnreachable on miss / no-wallet so
Attach falls through to the relay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…accept

mir up can accept LAN-direct QUIC connections (advertised via mDNS as
_miranda._udp). The wallet binding arrives as frame 0; the agent checks
IsOwnerPinned + verifies the binding (reusing ownerPubFromBinding) and pins
binding.x25519, then runs the SAME authenticated PTY session as the relay
path via the extracted serveAuthenticated helper. admit() bounds pre-auth
handshakes. Not yet auto-started (wiring + --no-lan is next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-only

mir attach now tries LAN-direct (mDNS+QUIC) first with a ~600ms budget,
then falls back to the relay; --relay-only skips LAN. mir up starts the
QUIC listener + mDNS advertisement (in Up, after the paired-owner guard)
unless --no-lan; LAN start failure is non-fatal — the relay path always
serves. attachLocators() makes the LAN-first-vs-relay-only decision unit-
testable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lve test

SECURITY.md gains an honest LAN-direct residual-exposure note (new inbound
listener surface + mDNS leak; same Noise-KK + binding trust, --no-lan to
disable). README documents LAN-direct + the flags. A skippable live-mDNS
test validates the prod resolver where multicast is available. Spec notes
happy-eyeballs as the future latency refinement and netsim as a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Replace the sequential LAN-then-relay dial with dialStaggered: the LAN
locator starts immediately, the relay only after a ~200ms head start; the
first live MsgConn wins, the loser is cancelled and any late-connecting
loser is cleaned up. On the LAN, LAN-direct wins inside the head start so
the relay is never contacted (a successful LAN attach stays relay-free).
Remote attaches pay only the head start, not the full ~600ms LAN budget.
Staggered (not a naive simultaneous race) on purpose: it keeps the
relay-free property that is the point of LAN-direct. Race-tested (-race).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant