forked from unom/punktfunk
ABI 19 -> 20. Wire protocol unchanged (still 2). Persisting the mgmt port (fe2bfeca) made a moved port survive mDNS going away, but mDNS was still the only SOURCE: a host that had never been seen on it — VPN-only, a routed subnet, or simply added by address on a network where multicast has never worked — had nothing to learn from and fell back to 47990. The `Welcome` now carries the port, so the client learns it over the connection it has already authenticated and discovery stops being involved at all. `Welcome.mgmt_port`, a trailing u16 after the cipher block, following the same additive discipline as the eight fields before it (compositor, gamepad, bitrate_kbps, bit_depth, color, chroma_format, audio_channels, codec): an older peer stops earlier and gets a documented default, in both directions, so WIRE_VERSION does not move. ⚠ THE TRAP, and why emitting the port forces the `cipher` placeholder: `cipher` is emitted only when non-default, so appending the port to an AES Welcome would land its LOW BYTE at offset 68 — exactly where every shipped 0.28.x client reads `cipher`, whose decode is deliberately fail-closed on an unknown id. 47991 is 0xBB57, so byte 68 would read 0x57 = 87, and EVERY current client would fail the handshake against a host that had merely moved its mgmt port. `encode` therefore writes an explicit cipher byte whenever a port rides along (the placeholder discipline `Hello::encode` already uses); a current client reads AES, a pre-cipher client stops before 68 regardless. The test pins the byte, both offsets (69 AES / 101 ChaCha), and that a host advertising no port still emits exactly 68 bytes — this field costs the common case nothing. Host: `mgmt::effective_port()` reads the same resolved bind `publish_endpoint` writes, so the wire, the endpoint file and the mDNS TXT cannot disagree — one lookup, not a fourth place to compute a port. `0` on the standalone punktfunk1-host binary, which has no management API: advertising 47990 from a host that is not serving it would be worse than saying nothing. Clients persist it on connect, feeding the store plumbing fe2bfeca already built: * Rust — `on_connected` grew the port alongside the fingerprint, plus `learn_mgmt_port_by_fp` (keyed by fingerprint alone, the identity a just-connected client is certain of). * Apple — `PunktfunkConnection.hostMgmtPort` + `updateMgmtPort` at the existing markConnected site. * Android — a new `nativeHostMgmtPort` JNI call, persisted where the session is constructed. Verified: Linux (pf-lxcheck2, amd64) `cargo check --all-targets` clean across punktfunk-core, pf-host-config, punktfunk-host, pf-client-core, pf-presenter, punktfunk-cli, punktfunk-client-linux and punktfunk-client-session, each confirmed genuinely compiled (counting `Compiling` as well as `Checking` — cargo prints the former for bin-only packages, which is what made an earlier gate look vacuous when it was not). punktfunk-core quic tests 76/76. Android: :kit+:app Kotlin, ParseRecordTest 12/12, and cargoNdkClippy clean for aarch64-linux-android. Apple: xcframework rebuilt at ABI 20, `swift build` complete. cargo fmt --all --check clean. NOT verified: the Windows client (192.168.1.133 unreachable).
punktfunk-core
The shared protocol core — the one place where punktfunk's transport, forward error correction, and crypto live. It's linked into the host and every native client, so there's exactly one implementation of the wire format everywhere.
Written in Rust with no async on the per-frame path (native threads only). It exposes both a normal Rust API and a stable, versioned C ABI, so the Swift and Kotlin clients — and any C embedder — link the same code as the Rust ones.
What's in here
- Transport & session (
session.rs,transport/,packet.rs) — thepunktfunk/1data plane over raw UDP: packetization, reassembly (with attacker-bounded limits), pacing, and socket tuning. - FEC (
fec/) — the wall-breaker. Two codes:- GF(2⁸) classic Reed–Solomon with the Cauchy generator matrix — byte-identical to the
nanorslibrary Moonlight uses, so our parity is decodable by a stock Moonlight client. - GF(2¹⁶) Leopard-RS (SIMD, O(n log n)) — up to 65535 shards/block, which removes the ~1 Gbps
FEC ceiling.
punktfunk/1negotiates this one.
- GF(2⁸) classic Reed–Solomon with the Cauchy generator matrix — byte-identical to the
- Crypto (
crypto.rs) — AES-128-GCM session encryption with per-direction nonce salts and sequence-as-AAD; SPAKE2 PIN pairing lives behind thequicfeature. - QUIC control plane (
quic.rs,client.rs, featurequic) — the Hello/Welcome/Start handshake, cert pinning/TOFU, reverse audio, and the embeddableNativeClientconnector. This is the only placetokio/quinnare allowed; the feature is off by default so the core stays runtime-free. - C ABI (
abi.rs) — the versioned surface (punktfunk_abi_version(),PunktfunkConfigcarrying its ownstruct_size) that generatesinclude/punktfunk_core.hvia cbindgen at build time.
Build outputs
The crate builds three ways at once (crate-type = ["lib", "cdylib", "staticlib"]):
| Output | Used by |
|---|---|
lib (rlib) |
the host, probe, and tools link it as a normal Rust crate |
cdylib (.so/.dylib) |
the Swift / Kotlin clients via the C ABI |
staticlib (.a) |
the C test harness and static embedding |
Test
cargo test -p punktfunk-core # unit + proptest + loopback
cargo run -p loss-harness # FEC loss-resilience sweep (no network needed)
bash crates/punktfunk-core/tests/c/run.sh # standalone C-ABI link + round-trip proof
Design invariants (do not regress)
- One core, linked everywhere — protocol/FEC/crypto live only here, behind the stable C ABI.
- No async on the hot path — the per-frame pipeline is native threads only;
quic(tokio/quinn) is control-plane only, feature-gated, off by default. - Security hardening stays intact — the reassembler bounds attacker-controlled fields before
allocating; AES-GCM keeps per-direction nonce salts + seq-as-AAD; the ABI checks
struct_size. Regression tests exist — keep them green.
Related
punktfunk-host— the streaming host built on this core- Clients — the apps that link this core over the C ABI (or directly, in Rust)
- punktfunk-planning:
implementation-plan.md(internal planning repo) — why GF(2¹⁶) FEC, the latency budget, and the architecture thesis