Skip to content

feat: add optional pinned-mutual-TLS QUIC transport - #18

Merged
raiseCatError merged 10 commits into
dev/opendisplay-nextfrom
claude/cool-galileo-bkgznr
Sep 27, 2026
Merged

raiseCatError merged 10 commits into
dev/opendisplay-nextfrom
claude/cool-galileo-bkgznr

Conversation

@raiseCatError

@raiseCatError raiseCatError commented Sep 27, 2026 •

Copy link
Copy Markdown
Owner

What changed

Adds QUIC as an optional second secure transport on network routes (LAN / AWDL / Remote). TCP remains fully supported and unchanged on the wire; USB never uses QUIC.

Transport

  • One pinned mutual-TLS QUIC connection per session with three reliable, sender-opened streams: Control (bidirectional), Video and Audio (Mac → receiver). Each stream starts with an 8-byte MEOW preface (version 1, channel byte, zero flags). The existing [UInt32 BE length][payload] framing follows. Channel identity never comes from stream IDs.
  • Security is the TCP code shared: TLSConfigurator.pinnedQUICOptions uses the same identity, the same client-certificate requirement on the listener and the same SPKI verify block, with ALPN meowdisplay-quic/1. Session tickets and resumption are disabled, so there is no 0-RTT.
  • The Control stream is the session "connection", so hello/welcome, the hello identity/current-pin check, invitation/admission and per-connection input grants all run the existing code. The Mac's hello SPKI and the receiver's peer resolution now read TLS or QUIC metadata (TLSConfigurator.authenticatedPeerSPKI(of:)).
  • Sends are channel-aware (send(channel:content:)). Over TCP every channel is the same connection with identical bytes. Video backpressure (pendingSends) now counts Video only, Audio is tracked separately and Control is never gated. Every completion carries a transport epoch, so a retired connection can't touch the new connection's counters.

Selection (Mac, per device: Auto / QUIC / TCP, default Auto)

  • TransportProtocolSelector runs inside the already-chosen route; route arbitration is unchanged.
  • Auto uses QUIC only with authenticated capability. On a local route, a compatible _meowdisp-q._udp hint is enough to attempt QUIC. Auto uses TCP when there's no capability or a cooldown is active. Whichever protocol first completes the authenticated hello is sticky for the logical session: no re-challenge, no metric switching.
  • Only an ordinary reachability failure lets Auto fall back to TCP; that also starts a 10-minute in-memory cooldown and keeps TCP for the rest of the session.
    • Security failures stop the session and are never downgraded: a pin rejection reported by the verify block, any TLS error, or identity mismatch.
    • An authenticated protocol violation also stops the session.
    • Ambiguous errors retry QUIC. Once the receiver's pin was accepted, nothing is classified as reachability.
  • A live QUIC loss gets one QUIC recovery dial. Explicit QUIC never uses TCP; it reports "unavailable" instead.
  • Changing the setting live: explicit TCP/QUIC migrate through the existing switchTransport hot swap (input cancelled, authority invalidated, re-authentication, re-admission, keyframe resync, new audio generation) only when the protocol differs. Auto keeps the current protocol.
  • The Settings device detail gets a Network Transport picker with "Currently: QUIC · LAN" and similar. QUIC is hidden until the device supports it.

Receiver

  • A QUIC listener on UDP 9001 runs beside the TLS listener and shares its lifecycle via TLSListenerState.quic: pairing suppression, trust refresh and teardown.
    • It advertises _meowdisp-q._udp with id/pv/qv only (never the cr token).
    • Hello lists quic only while the listener is bound.
  • Hardening:
    • At most 4 live QUIC connections.
    • 3 sender-opened bidirectional streams per connection; no unidirectional streams.
    • Duplicate, unknown and wrong-direction streams are rejected.
    • Preface timeout 5 s; Control-stream timeout 10 s.
    • Frame caps are checked before buffering: Control 1 MiB, Audio 1 MiB, Video 32 MiB (derived from the 4096-pixel decode ceilings).
    • Media must go on the right channel.
    • Video/Audio are refused until the session is admitted (ReceiverMediaAdmissionGate).
    • Media for stale generations is ignored.
  • ReceiverPipelineActor retires a QUIC connection whenever it lets go of its Control stream (replace, drop, revoke, failure). Forget/Block also closes connections that haven't presented a session yet.

Compatibility

  • WireProtocol.version 22; minSupportedPeer stays 1; pairing version unchanged.
  • An explicit authenticated capability (transports, qv) is added to hello/welcome. A missing capability means TCP only.
  • The one check that compared a peer against the build's current version (streamingProfileRequest) is pinned to 21, so pv 21 receivers keep working.

Docs: PROTOCOL.md §2.4 (new), COMPATIBILITY.md, SECURITY.md, ARCHITECTURE.md, and one README bullet. No performance claims are made.

Why

Today Control, Video and Audio share one ordered TCP/TLS byte stream, so a large Video write can hold back input and audio. Separate reliable QUIC streams remove that cross-channel head-of-line blocking. Every existing trust and session rule is kept, and TCP remains available.

Verification

  • CI (macos-26) on the current head f67b75d8b73f43d7988ba379bf7c140deb7af23b is green:

    • xcodegen
    • macOS sender build + 1,629 tests, 0 failures
    • Mac Receiver build (macOS 12 floor): passed
    • generic iOS receiver build (iOS 16.4 floor): passed
    • reserved-region SDK gate: passed
    • PR title check: passed
  • Capability-cache regression fix (f67b75d): an authenticated hello with a pre-QUIC pv (below 22) clears any remembered QUIC support for that peer. A pv 22+ hello that lists TCP only (for example, the receiver's QUIC listener is momentarily unbound) may keep a previously authenticated capability. The decision uses the authenticated hello's own pv, never Bonjour. TransportProtocolSelectionTests covers these cases:

    • learned QUIC, then a pv 21 hello, clears it
    • learned QUIC, then a pv 22 TCP-only hello, keeps it
    • an incompatible qv is still remembered as incompatible
    • an old peer with nothing learned stays TCP-only
  • SWIFT_VERSION is unchanged and strict concurrency is still on. There are no new warnings in production files. The QUIC tests use the deprecated SecKeychainCreate API to build throwaway keychains.

  • New tests:

    • TransportProtocolSelectionTests: selection, no-downgrade, one live recovery, anti-thrash, cooldown, stores, Forget, classifier.
    • QUICStreamProtocolTests: preface, split/multi/truncated/oversized frames, bounded buffering, topology, capability, hint.
    • QUICReceiverAuthorityTests: connection bound, retirement with the Control stream, revoke, parked media, admission-gated media, strict Control framing, stale generations.
    • QUICMigrationTests: fresh authentication/admission/input grant across TCP↔QUIC, epoch-guarded counters, no shared send gate, repeated switches.
    • QUICSecureTransportTests: real loopback QUIC between isolated temporary keychain identities using the production options. Covers correct mutual pins, Control arriving past 8 MiB of unread Video, wrong server pin (reported as security), wrong client pin, unknown pin, missing client certificate, ALPN mismatch, and peer-ID mismatch.
  • Not done here: physical TCP-vs-QUIC latency/CPU/energy A/B on real devices and networks.

  • The change is focused, without unrelated refactors

  • Relevant behaviour was verified (tests and/or on device) — tests and loopback QUIC in CI; on-device A/B still pending

  • Documentation updated if needed

  • No credentials, secrets or signing material included

  • No unrelated generated or build artifacts

QUIC is additive: TCP stays first class and USB is unchanged. One QUIC
connection carries three reliable streams (Control, Video, Audio), each
opened with an explicit 8-byte MEOW preface, using the existing identity,
SPKI pins and verify block (ALPN meowdisplay-quic/1, no tickets or
resumption, so no 0-RTT). Protocol selection (Auto/QUIC/TCP per device)
runs inside the chosen network route, falls back to secure TCP only after
an ordinary reachability failure in Auto, and never after a security or
protocol failure. Receivers add a QUIC listener alongside the TLS
listener with bounded connections, streams and frame sizes, and hand the
Control stream to the existing session pipeline. Wire version 22 adds an
explicit authenticated transports/qv capability; minSupportedPeer stays 1.
@raiseCatError
raiseCatError force-pushed the claude/cool-galileo-bkgznr branch from 3c842f5 to ae5ebec Compare September 27, 2026 08:11
@raiseCatError
raiseCatError marked this pull request as ready for review September 27, 2026 08:22
@raiseCatError
raiseCatError merged commit db5a7c4 into dev/opendisplay-next Sep 27, 2026
3 checks passed
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