Skip to content

Reset the protocol format - #13

Merged
kevinmcconnell merged 3 commits into
mainfrom
protocol-version-1
Sep 24, 2026
Merged

kevinmcconnell merged 3 commits into
mainfrom
protocol-version-1

Conversation

@kevinmcconnell

@kevinmcconnell kevinmcconnell commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

gliff hasn't shipped, so this is the moment to settle a wire format we can
evolve after release without breaking peers from other builds. Nothing here
is compatible with earlier builds, and that's intended; the version starts
over at 1.

What changes

  • CBOR instead of postcard. postcard is positional, so any new field or
    message broke every older peer. Messages are now CBOR via minicbor, with
    every variant and field numbered (#[n]) and encoded as arrays. Unknown
    fields are ignored, absent optional ones decode as None, and renames
    never touch the wire.
  • Skippable frames. The header is u32 body_len | u32 payload_len, so a
    reader skips a message it doesn't know, payload and all. An unknown value
    inside a known message still fails, since the rules forbid sending one.
  • Feature lists in ClientCaps and HelloAck. Anything new goes only
    to a peer that listed the feature introducing it. FEATURES is empty
    today.
  • Version handling. PROTOCOL_VERSION is 1. The server reads Hello's
    version before the rest, so a mismatch is reported even when a future
    Hello has changed shape. The client shows "Client version is too old" or
    "Server version is too old" and stops auto-reconnecting; error codes 1-255
    are all treated as incompatible this way. Error carries the server's
    version.
  • Lenient where it must be. Hello arrives before features are agreed, so
    its codec and chroma lists drop values this build doesn't know. Optional
    enum fields use a helper, because minicbor's own Option decoding misreads
    the next field on an unknown value.
  • Limits both ways. The writer refuses frames the reader would reject,
    and file offers cap their summed path length so the largest fits in a
    frame.

The rules for future changes are at the top of
crates/gliff-proto/src/msg.rs; PROTOCOL_VERSION only changes for a break
they can't absorb.

Cost

Measured against postcard over whole sessions, video included: +0.02% on a
30 s LAN bench, +0.09% in scripts/e2e.sh, +0.35% on the 30 s satellite
bench (2.5 Mbit/s, 550 ms, 10% loss). A VideoFrame header body is 32 bytes
and a FrameAck 15.

Why this format

  • CBOR is RFC 8949; integer keys are how COSE, CWT and CTAP2 use it.
  • minicbor over ciborium: minicbor is actively released and is what
    AWS's aws-smithy-cbor uses in production. ciborium hasn't released
    since January 2024, and most of its downloads come in via criterion.
  • The design mirrors SPICE (capabilities, unknown messages skipped) and RDP
    capability sets, and avoids RFB's weakness where an unknown message ends
    the session. Protobuf evolves as well but needs codegen and conversion
    layers, which isn't worth it with Rust at both ends.

Review

Two rounds of independent review (Codex and a Fable agent). Their findings
are folded in: Hello's version read first, the
Option enum pitfall, required fields documented as permanent, the server
checking codecs before any setup, and writer-side limits. Two fixes they
found outside the format have their own commits: a client writer that
failed silently, and a clipboard offer cap.

For the macOS client (#5): Hello.keymap stays XKB text or empty. Layout
names can arrive later in a new optional field, which older servers ignore
and fall back to their default keymap.

Testing

Golden bytes pin the frame layout and key messages. Tests cover a Hello
from another version, messages from a newer peer, skipped and nested
unknowns, limits, and read_msg resuming after cancellation. Clippy, the
unit tests and scripts/e2e.sh pass on AMD RADV.

@kevinmcconnell kevinmcconnell changed the title Reset the protocol version to 1 Reset the protocol format Sep 23, 2026
gliff hasn't shipped, so settle a wire format that later releases can
extend without breaking each other. postcard is positional, so any new
field or message broke every older peer.

Messages are now CBOR via minicbor, with every variant and field
numbered and encoded as arrays: unknown fields are ignored, absent
optional ones decode as None, and names never reach the wire. The frame
header carries the payload length too, so a reader skips a message it
does not know, payload and all, and the writer refuses frames the
reader would reject. The handshake exchanges feature lists; anything
new goes only to a peer that listed its feature. The rules for future
changes are at the top of gliff-proto's msg.rs.

The protocol version starts over at 1. The server reads Hello's version
before the rest, so a mismatch is always reported, and it refuses a
client with no codec or chroma mode in common before setting anything
up. Error codes 1 to 255 mean the ends cannot work together: the client
says which end is too old and stops reconnecting.
The writer task stopped on its first error without telling anyone, so
input and frame acks went nowhere while the session looked alive.
An offer of 10,000 long paths could exceed the 1 MiB frame body limit.
Cap the summed path length, so the largest offer the other caps allow
still fits.
@kevinmcconnell
kevinmcconnell merged commit 3375cca into main Sep 24, 2026
4 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