Skip to content

Implement HTTP/2 over Noise instead of treating Noise records as control RPC boundaries #65

Description

@forkwright

Finding

After the TS2021 handshake, Hamma sends custom length-prefixed JSON directly in Noise records. The actual control protocol runs HTTP/2 as a byte stream inside Noise.

Evidence

  • crates/dictyon/src/transport.rs:1-15,54-58,169-213 describes every post-handshake message as an application-level Noise frame.
  • crates/dictyon/src/wire.rs:176-270 exposes whole-record send_message / recv_message operations.
  • crates/dictyon/src/control/mod.rs:204-219,280-315 sends raw [4-byte LE length][JSON] records for registration and map traffic.
  • crates/dictyon/tests/wire_integration.rs:384-435,626-674 repeats that invented protocol in the mock server.
  • Tailscale's control/ts2021/client.go explicitly exposes an HTTP client and forces HTTP/2 inside the Noise connection.
  • Reference registration is POST /machine/register with HTTP status/body handling; map is POST /machine/map with a streamed HTTP response body in control/controlclient/direct.go.
  • The reference control/controlbase.Conn implements net.Conn: writes are chunked into at most 4,077 plaintext bytes and reads reassemble arbitrary application bytes. Hamma instead permits a single 4,096-byte plaintext and rejects larger writes.

A real server expects the HTTP/2 connection preface and frames after Noise. Hamma's first plaintext bytes are a little-endian JSON length.

Why this matters

Registration and map streaming cannot interoperate with tailscale.com or a conforming implementation. The current mock is a self-paired oracle, so green integration tests actively conceal the protocol substitution.

Open #51 is downstream of the same false premise: real registration is an HTTP JSON response, not a map-style four-byte control frame. Implementing #51 as written would move farther from compatibility.

Required mechanism

  • Make the established Noise channel an AsyncRead + AsyncWrite byte stream.
  • Chunk writes at the actual 4,077-byte plaintext ceiling and reassemble reads independent of record boundaries.
  • Treat any partial-write/decryption desynchronization as fatal.
  • Layer an HTTP/2 client over that stream.
  • Model registration and map as real HTTP endpoint, status, header, request-body, and streamed-response semantics.
  • Add an independent upstream-compatible HTTP/2-over-Noise oracle or captured interop fixture; the test server must not import Hamma's framing assumptions.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugUnintended behavior or production defectseverity:highSignificant breakage with no available workaround

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions