A field host streamed 1919 frames into a black screen while its own log blamed the client. Four faults, each of which alone makes the failure invisible.
The bug
The Windows firewall rules are localport=-scoped (47998-48010, 9777, 5353), but the media data plane binds an ephemeral port per session. No such rule can ever cover it, so Windows Firewall drops the client's hole-punch on every session on every Windows host — punched=false on the "data plane bound" line, in all six sessions across two field logs. The punch then never opens the return path, and video is sent blind to an address the client merely reported.
service install now also adds a program-scoped inbound UDP rule for the host executable, covering whatever port a session picks. Program-scoped rather than a pinned port: pinning into 47998-48010 would collide with Sunshine/Apollo. The port rules are kept as they are.
Why nobody could see it
loss_ppm is ambiguous at zero. Loss is a ratio over the packets that arrived, so a flawless link and a link delivering nothing both report 0. The host read total silence as perfect and decayed adaptive FEC to its floor. Clients now also send a DeliveryReport carrying the session's received-packet count.
It is a new type byte, not a field appended to LossReport — and that is load-bearing. LossReport::decode length-checks exactly, so lengthening it would make every already-shipped host reject the loss reports its FEC runs on. Mixed versions are normal here (the field case ran a current host against a months-old client). A test pins LossReport to its 9-byte form so this cannot be undone by accident.
Two opposite faults had the same period.NO_VIDEO_RETRY (client received nothing) and FLUSH_COOLDOWN (client drowning in frames) were both 2000 ms, so the host's cadence classifier could not tell them apart and picked the wrong one out loud. The no-video cooldown moves into core beside FLUSH_COOLDOWN at 2600 ms, and both the Android client and the host compare against the shared constant rather than a copy — the copy is what let them drift into being indistinguishable.
The diagnosis now leads with the delivery count: zero is an ERROR naming the data plane, a confirmed count keeps the old confident wording, and a client too old to answer gets a warning that says it cannot tell instead of guessing. A punch that never arrives is also its own warning now, rather than a debug field on an info line.
Noise budget
An older host warns per unknown control message, so the delivery report is sent every window while the count is zero, once when the first packets land, then never. A healthy session costs one message; a broken one keeps saying so.
Verification
480 core tests pass (up from 477): five new covering the wire form, the v1-compat guarantee, the two cooldowns staying distinguishable, and the send rule.
cargo fmt --check clean; clippy clean on punktfunk-core; punktfunk-probe compiles.
Host symbol resolution verified — the only cargo check -p punktfunk-host errors on macOS are the 10 pre-existing platform gaps (opus, procscan, vdisplay), none naming anything introduced here.
⚠ Needs a Windows build leg.add_data_plane_firewall_rule is cfg(windows) and scripts/xcheck.sh does not cover punktfunk-host; Docker was too congested locally to compile-check it. Verified by reading instead: run_quiet(&str, &[&str]) and firewall_profile_arg -> &'static str match the usage, rustfmt parses the file, and the existing rules already pass names containing spaces and parentheses through the same path.
Open items (mid-session delivery death, the QUIC re-route, Linux/firewalld parity) are written up separately in punktfunk-planning.
A field host streamed 1919 frames into a black screen while its own log blamed the client. Four faults, each of which alone makes the failure invisible.
## The bug
The Windows firewall rules are `localport=`-scoped (`47998-48010, 9777, 5353`), but the media data plane binds an **ephemeral** port per session. No such rule can ever cover it, so Windows Firewall drops the client's hole-punch on **every session on every Windows host** — `punched=false` on the "data plane bound" line, in all six sessions across two field logs. The punch then never opens the return path, and video is sent blind to an address the client merely *reported*.
`service install` now also adds a **program-scoped** inbound UDP rule for the host executable, covering whatever port a session picks. Program-scoped rather than a pinned port: pinning into `47998-48010` would collide with Sunshine/Apollo. The port rules are kept as they are.
## Why nobody could see it
**`loss_ppm` is ambiguous at zero.** Loss is a ratio over the packets that arrived, so a flawless link and a link delivering *nothing* both report `0`. The host read total silence as perfect and decayed adaptive FEC to its floor. Clients now also send a `DeliveryReport` carrying the session's received-packet count.
It is a **new type byte, not a field appended to `LossReport`** — and that is load-bearing. `LossReport::decode` length-checks exactly, so lengthening it would make every already-shipped host reject the loss reports its FEC runs on. Mixed versions are normal here (the field case ran a current host against a months-old client). A test pins `LossReport` to its 9-byte form so this cannot be undone by accident.
**Two opposite faults had the same period.** `NO_VIDEO_RETRY` (client received nothing) and `FLUSH_COOLDOWN` (client drowning in frames) were both 2000 ms, so the host's cadence classifier could not tell them apart and picked the wrong one out loud. The no-video cooldown moves into core beside `FLUSH_COOLDOWN` at 2600 ms, and both the Android client and the host compare against the **shared constant** rather than a copy — the copy is what let them drift into being indistinguishable.
**The diagnosis now leads with the delivery count**: zero is an `ERROR` naming the data plane, a confirmed count keeps the old confident wording, and a client too old to answer gets a warning that says it cannot tell instead of guessing. A punch that never arrives is also its own warning now, rather than a debug field on an info line.
## Noise budget
An older host warns per unknown control message, so the delivery report is sent every window while the count is zero, once when the first packets land, then never. A healthy session costs one message; a broken one keeps saying so.
## Verification
- **480 core tests pass** (up from 477): five new covering the wire form, the v1-compat guarantee, the two cooldowns staying distinguishable, and the send rule.
- `cargo fmt --check` clean; clippy clean on `punktfunk-core`; `punktfunk-probe` compiles.
- Host symbol resolution verified — the only `cargo check -p punktfunk-host` errors on macOS are the 10 pre-existing platform gaps (opus, procscan, vdisplay), none naming anything introduced here.
⚠ **Needs a Windows build leg.** `add_data_plane_firewall_rule` is `cfg(windows)` and `scripts/xcheck.sh` does not cover `punktfunk-host`; Docker was too congested locally to compile-check it. Verified by reading instead: `run_quiet(&str, &[&str])` and `firewall_profile_arg -> &'static str` match the usage, rustfmt parses the file, and the existing rules already pass names containing spaces and parentheses through the same path.
Open items (mid-session delivery death, the QUIC re-route, Linux/firewalld parity) are written up separately in `punktfunk-planning`.
A field host streamed 1919 frames into a black screen while its own log
blamed the client. Four faults, each of which alone makes the failure
invisible.
The Windows firewall rules are `localport=`-scoped (47998-48010, 9777,
5353), but the media data plane binds an EPHEMERAL port per session. No
such rule can ever cover it, so Windows Firewall drops the client's
hole-punch on EVERY session on EVERY Windows host — `punched=false` on
the "data plane bound" line, in all six sessions of two field logs. The
punch then never opens the return path and video is sent blind to an
address the client merely reported. `service install` now also adds a
program-scoped inbound UDP rule for the host executable, which covers
whatever port a session picks. Program-scoped rather than a pinned port:
pinning into 47998-48010 would collide with Sunshine/Apollo.
`LossReport` carried only `loss_ppm`, which is ambiguous at zero — loss
is a ratio over the packets that arrived, so a flawless link and a link
delivering NOTHING both report 0. The host read total silence as perfect
and decayed adaptive FEC to its floor. Clients now also send a
`DeliveryReport` with the session's received-packet count. It is a new
type byte, NOT a field appended to `LossReport`: that message is
length-checked exactly, so lengthening it would make every shipped host
reject the loss reports its FEC runs on. Sent every window while the
count is zero, once when the first packets land, then never — an older
host warns per unknown message and must not be flooded on a good
session.
`NO_VIDEO_RETRY` (client got nothing) and `FLUSH_COOLDOWN` (client
drowning) were both 2000 ms, so the host's cadence classifier could not
tell two opposite faults apart and picked the wrong one out loud. The
no-video cooldown moves to core beside `FLUSH_COOLDOWN` at 2600 ms, and
both sides compare against the shared constant rather than a copy.
The diagnosis now leads with the delivery count: zero is an error naming
the data plane, a confirmed count keeps the old confident wording, and an
old client that cannot answer gets a warning that says so instead of
guessing. A punch that never arrives is also its own warning now, rather
than a debug field on an info line.
`check-docs-drift.sh` scans for `PUNKTFUNK_*` identifiers and asks that
each be documented in docs-site or explicitly baselined. It cannot tell
an env knob from a cbindgen-exported `#define`, so the new
`PUNKTFUNK_MSG_DELIVERY_REPORT` header constant tripped it.
It is a wire message type byte, not a knob an operator can set — same as
every other `PUNKTFUNK_MSG_*`, all of which are already in the baseline.
Added in sorted position beside them.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A field host streamed 1919 frames into a black screen while its own log blamed the client. Four faults, each of which alone makes the failure invisible.
The bug
The Windows firewall rules are
localport=-scoped (47998-48010, 9777, 5353), but the media data plane binds an ephemeral port per session. No such rule can ever cover it, so Windows Firewall drops the client's hole-punch on every session on every Windows host —punched=falseon the "data plane bound" line, in all six sessions across two field logs. The punch then never opens the return path, and video is sent blind to an address the client merely reported.service installnow also adds a program-scoped inbound UDP rule for the host executable, covering whatever port a session picks. Program-scoped rather than a pinned port: pinning into47998-48010would collide with Sunshine/Apollo. The port rules are kept as they are.Why nobody could see it
loss_ppmis ambiguous at zero. Loss is a ratio over the packets that arrived, so a flawless link and a link delivering nothing both report0. The host read total silence as perfect and decayed adaptive FEC to its floor. Clients now also send aDeliveryReportcarrying the session's received-packet count.It is a new type byte, not a field appended to
LossReport— and that is load-bearing.LossReport::decodelength-checks exactly, so lengthening it would make every already-shipped host reject the loss reports its FEC runs on. Mixed versions are normal here (the field case ran a current host against a months-old client). A test pinsLossReportto its 9-byte form so this cannot be undone by accident.Two opposite faults had the same period.
NO_VIDEO_RETRY(client received nothing) andFLUSH_COOLDOWN(client drowning in frames) were both 2000 ms, so the host's cadence classifier could not tell them apart and picked the wrong one out loud. The no-video cooldown moves into core besideFLUSH_COOLDOWNat 2600 ms, and both the Android client and the host compare against the shared constant rather than a copy — the copy is what let them drift into being indistinguishable.The diagnosis now leads with the delivery count: zero is an
ERRORnaming the data plane, a confirmed count keeps the old confident wording, and a client too old to answer gets a warning that says it cannot tell instead of guessing. A punch that never arrives is also its own warning now, rather than a debug field on an info line.Noise budget
An older host warns per unknown control message, so the delivery report is sent every window while the count is zero, once when the first packets land, then never. A healthy session costs one message; a broken one keeps saying so.
Verification
cargo fmt --checkclean; clippy clean onpunktfunk-core;punktfunk-probecompiles.cargo check -p punktfunk-hosterrors on macOS are the 10 pre-existing platform gaps (opus, procscan, vdisplay), none naming anything introduced here.⚠ Needs a Windows build leg.
add_data_plane_firewall_ruleiscfg(windows)andscripts/xcheck.shdoes not coverpunktfunk-host; Docker was too congested locally to compile-check it. Verified by reading instead:run_quiet(&str, &[&str])andfirewall_profile_arg -> &'static strmatch the usage, rustfmt parses the file, and the existing rules already pass names containing spaces and parentheses through the same path.Open items (mid-session delivery death, the QUIC re-route, Linux/firewalld parity) are written up separately in
punktfunk-planning.