forked from unom/punktfunk
Three wire and ABI faults. An out-of-range pad index reached one rumble consumer and not the other. It skipped the reorder gate — the per-pad seq cursor has no slot for it — and was handed to the legacy queue, while the policy engine discarded it on its own bounds check, so the comment promising both consumers are fed was false for exactly these. An embedder draining the queue could be handed an index it would use to subscript its own per-pad array. The host never emits one, so it is malformed or hostile either way; both consumers now agree by dropping it before either sees it. The adaptive-trigger effect was the only variable-length wire field bounded on neither side. Encode appended whatever it was handed and decode took the whole tail, while its sibling raw-report field had been bounded both ways all along; there is now one constant both sides clamp to. Worse than the missing bound was the empty case: a body with no effect bytes decoded as an EMPTY effect, and downstream an empty block is written as an all-zero trigger report, which is mode 0x00 — release. A truncated datagram could therefore silently cancel the trigger effect a game was holding. That shape is now rejected outright; a genuine release is a full-length zero block and still decodes. The C ABI history had a hole and a symbol nobody versioned. v11 shipped without its line, and the rumble policy engine's C surface was added while the version constant still read 7, with no bump at all — so every core since has exported those symbols while advertising a number that never promised them. A shipped binary says what it says, so that cannot be corrected backwards; v15 instead establishes the floor that guarantees the surface, and the v11 line is written down. No code changed for the bump and nothing moved on the wire.
144 lines
9.0 KiB
Rust
144 lines
9.0 KiB
Rust
//! # punktfunk-core
|
||
//!
|
||
//! The shared protocol / transport / FEC core for the punktfunk low-latency streaming
|
||
//! stack. It is compiled exactly once and linked by every host and client — directly
|
||
//! as a Rust `lib`, or across the [C ABI](crate::abi) by Swift / Kotlin / C clients.
|
||
//!
|
||
//! Everything platform-specific (capture, encode, decode, present, input injection)
|
||
//! lives *outside* this crate. What lives *here*:
|
||
//!
|
||
//! - [`fec`] — erasure coding. GF(2⁸) for GameStream/Moonlight compatibility (P1) and
|
||
//! GF(2¹⁶) Leopard-RS (P2) which removes the ~1 Gbps per-frame shard-count ceiling.
|
||
//! - [`packet`] — `#[repr(C)]` zero-copy wire framing: splitting an access unit into
|
||
//! FEC blocks of MTU-sized shards and reassembling them on the far side.
|
||
//! - [`crypto`] — AES-128-GCM session sealing, matching GameStream in P1.
|
||
//! - [`session`] — the host (submit frame → FEC → packetize → seal → send) and client
|
||
//! (recv → open → reorder → FEC recover → reassemble) state machines.
|
||
//! - [`transport`] — pluggable packet I/O (in-process loopback for tests; UDP for real).
|
||
//! - [`abi`] — the `extern "C"` surface and `cbindgen`-generated `punktfunk_core.h`.
|
||
//! - [`config`] / [`error`] / [`stats`] — session configuration, the shared error/status
|
||
//! vocabulary, and the counters snapshot.
|
||
//! - [`input`] — the wire input-event vocabulary (keyboard/mouse/touch, gamepad snapshots).
|
||
//! - [`reject`] — typed application-close rejection codes · [`reanchor`] — the post-loss
|
||
//! freeze-until-reanchor client gate · [`render_scale`] — the shared render-scale setting ·
|
||
//! [`audio`] — Opus PCM decode for C-ABI embedders · [`wol`] — Wake-on-LAN.
|
||
//! - `quic` (feature `quic`) — the punktfunk/1 control plane: handshake, typed control
|
||
//! messages, pairing (SPAKE2), the datagram plane codecs, and clock sync. With it come
|
||
//! `client` (the embeddable NativeClient worker), `abr` (the adaptive-bitrate
|
||
//! controller), and `clipboard` (the shared-clipboard transport task). `tls`
|
||
//! (feature `tls`) — the pinned-fingerprint certificate verifier.
|
||
//!
|
||
//! ## Threading contract
|
||
//!
|
||
//! Nothing in the per-frame path touches an async runtime. `tokio`/`quinn` are gated
|
||
//! behind the off-by-default `quic` feature and used only for the control plane.
|
||
|
||
// Unsafe-proof program: every `unsafe {}` / `unsafe impl` in this crate carries a `// SAFETY:`
|
||
// proof. The bulk lives in `abi.rs`, whose sites are instances of the ABI contract stated once at
|
||
// the top of that file rather than 141 independent arguments.
|
||
#![deny(clippy::undocumented_unsafe_blocks)]
|
||
#![forbid(unsafe_op_in_unsafe_fn)]
|
||
|
||
pub mod abi;
|
||
#[cfg(feature = "quic")]
|
||
mod abr;
|
||
pub mod audio;
|
||
#[cfg(feature = "quic")]
|
||
pub mod client;
|
||
/// Client-side shared-clipboard transport: the per-session task that runs the fetch-stream accept
|
||
/// loop, drives outbound fetches, and serves inbound ones — surfaced to the embedder as poll
|
||
/// events. Wire codecs live in [`quic`]; the OS pasteboard integration lives in the native client.
|
||
#[cfg(feature = "quic")]
|
||
pub mod clipboard;
|
||
pub mod config;
|
||
pub mod crypto;
|
||
pub mod error;
|
||
pub mod fec;
|
||
pub mod input;
|
||
pub mod packet;
|
||
pub mod phase;
|
||
#[cfg(feature = "quic")]
|
||
pub mod quic;
|
||
pub mod reanchor;
|
||
pub mod reject;
|
||
pub mod render_scale;
|
||
pub mod session;
|
||
pub mod stats;
|
||
#[cfg(feature = "tls")]
|
||
pub mod tls;
|
||
pub mod transport;
|
||
pub mod wol;
|
||
|
||
pub use config::{CompositorPref, Config, FecConfig, FecScheme, Mode, ProtocolPhase, Role};
|
||
pub use error::{PunktfunkError, PunktfunkStatus, Result};
|
||
pub use session::{Frame, Session};
|
||
pub use stats::Stats;
|
||
|
||
/// Bump on any breaking change to the [C ABI](crate::abi). Mirrors
|
||
/// `punktfunk_abi_version()` and is checked by clients before use.
|
||
///
|
||
/// v2: `punktfunk_connect` gained `client_cert_pem`/`client_key_pem` (pairing identities);
|
||
/// added `punktfunk_pair` / `punktfunk_generate_identity` / `punktfunk_connection_request_mode`.
|
||
/// v3: added `punktfunk_wake_on_lan` (Wake-on-LAN magic packet; the host's wake MAC(s) reach
|
||
/// clients out-of-band via the mDNS `mac` TXT record, so no connection is required to wake).
|
||
/// v4: added `punktfunk_probe` (bounded, trust-agnostic, mDNS-independent reachability handshake —
|
||
/// the display-side companion to dial-first, so saved-host "online" pips reflect real reachability).
|
||
/// v5: added `punktfunk_connection_next_rumble2` (rumble pull that also yields the self-terminating
|
||
/// TTL of a v2 envelope; `punktfunk_connection_next_rumble` is unchanged and drops it). Additive —
|
||
/// the wire is backward-compatible (the envelope is a length-tolerant tail on 0xCA), so
|
||
/// [`WIRE_VERSION`] is unchanged.
|
||
/// v6: added the `punktfunk_reanchor_gate_*` surface (post-loss freeze-until-reanchor gate for the
|
||
/// Swift client; Rust embedders use [`reanchor::ReanchorGate`] directly). Additive, client-local —
|
||
/// no wire change, so [`WIRE_VERSION`] is unchanged.
|
||
/// v7: added `punktfunk_connect_ex8` (`status_out` — typed connect-failure reporting, including
|
||
/// the host-rejection block `PUNKTFUNK_STATUS_REJECTED_*` decoded from the host's QUIC
|
||
/// application close) and the `PunktfunkStatus` −20 block itself. Additive — the close codes are
|
||
/// new application-close vocabulary an old peer simply never sends/reads, so [`WIRE_VERSION`] is
|
||
/// unchanged.
|
||
/// v8: added the shared-clipboard client surface — `punktfunk_connection_host_caps` and
|
||
/// `punktfunk_connection_clipboard_{control,offer,fetch,serve,cancel}` +
|
||
/// `punktfunk_connection_next_clipboard`. Additive; the wire grows only backward-compatible control
|
||
/// messages (0x40-0x44) and a new `Welcome::host_caps` bit, so [`WIRE_VERSION`] is unchanged.
|
||
/// v9: `PunktfunkFrame` grew `received_ns` — the reassembly-completion receipt stamp, so
|
||
/// embedders stop stamping receipt at the hand-off pull (which folds the pre-decode queue wait
|
||
/// into apparent network latency). Struct-size change on the frame poll surface = a hard ABI
|
||
/// break for embedders reading `PunktfunkFrame`; nothing on the wire moved, so [`WIRE_VERSION`]
|
||
/// is unchanged.
|
||
/// v10: added `punktfunk_connection_clock_offset_now_ns` — the LIVE (mid-stream re-synced)
|
||
/// clock offset ongoing latency math must use; the connect-time getter stays frozen by
|
||
/// contract. Additive, client-local — no wire change, so [`WIRE_VERSION`] is unchanged.
|
||
/// v11: added `punktfunk_connect_ex9` — `connect_ex8` plus a `client_caps` bitfield
|
||
/// (`PUNKTFUNK_CLIENT_CAP_CURSOR`, later `…_PHASE_LOCK`), which is how a client tells the host it
|
||
/// renders the pointer itself. Additive; the caps ride the existing Hello, so [`WIRE_VERSION`] is
|
||
/// unchanged. (Documented late — the bump shipped without its line here.)
|
||
/// v12: added `punktfunk_connection_set_cursor_render` — the mid-stream cursor-render flip
|
||
/// (design/remote-desktop-sweep.md §8): the client's mouse-model chord tells the host who
|
||
/// renders the pointer. Additive; rides the existing control stream (a new message TYPE, which
|
||
/// pre-§8 hosts ignore), so [`WIRE_VERSION`] is unchanged.
|
||
/// v13: added `punktfunk_connection_send_pen` — the stylus wire plane
|
||
/// (design/pen-tablet-input.md): a client sends `RICH_PEN` sample batches once the host
|
||
/// advertises `HOST_CAP_PEN`. Additive and capability-gated, so [`WIRE_VERSION`] is unchanged.
|
||
/// v14: added `punktfunk_connection_report_phase` + the `PUNKTFUNK_CLIENT_CAP_PHASE_LOCK` mirror
|
||
/// — the phase-locked capture reporter (design/phase-locked-capture.md): a client that advertises
|
||
/// the cap reports its next display latch (already converted to host clock), the panel period, an
|
||
/// uncertainty and the circular arrival-lead statistic the host's controller steers on. Additive;
|
||
/// the wire grows only a new control message (`PhaseReport`, 0x32) an old host never reads and a
|
||
/// strict-prefix append on the 0xCF host-timing tail, so [`WIRE_VERSION`] is unchanged.
|
||
/// v15: versions the shared rumble policy engine's C surface —
|
||
/// `punktfunk_connection_next_rumble_cmd`, `punktfunk_connection_set_rumble_quirks` and the
|
||
/// `PUNKTFUNK_RUMBLE_QUIRK_*` bits. These symbols are NOT new: they landed while this constant
|
||
/// still read 7 and no bump was made, so every core since has exported them while advertising a
|
||
/// version that never promised them. That cannot be corrected retroactively — a shipped binary
|
||
/// says what it says — so v15 is the floor that *guarantees* them: at or above it the surface is
|
||
/// present, below it an embedder must probe for the symbol. Purely a version statement; no code
|
||
/// changed with this bump, and no wire change, so [`WIRE_VERSION`] is unchanged.
|
||
pub const ABI_VERSION: u32 = 15;
|
||
|
||
/// The punktfunk/1 **wire** version — what `Hello`/`Welcome` carry and hosts equality-check.
|
||
/// Deliberately its own constant: [`ABI_VERSION`] tracks the embeddable **C surface**
|
||
/// (functions a client links), which can grow without changing a single wire byte — v3's
|
||
/// `punktfunk_wake_on_lan` is client-local, and riding the C-ABI bump onto the wire locked
|
||
/// every new client out of every deployed host ("ABI mismatch: client 3 host 2", observed
|
||
/// live). Bump this ONLY when the handshake/planes actually change incompatibly.
|
||
pub const WIRE_VERSION: u32 = 2;
|