feat(core,host): per-family MTU shard sizing — the IPv6 gating item
deb / build-publish (push) Successful in 11m48s
apple / swift (push) Successful in 1m10s
windows-host / package (push) Successful in 8m41s
release / apple (push) Successful in 8m47s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m52s
arch / build-publish (push) Successful in 10m51s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m46s
android / android (push) Successful in 12m52s
audit / cargo-audit (push) Successful in 2m3s
audit / bun-audit (push) Successful in 15s
ci / rust (push) Failing after 25s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 1m11s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m12s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 1m24s
apple / screenshots (push) Successful in 5m37s
ci / bench (push) Successful in 7m12s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 48s
flatpak / build-publish (push) Failing after 4m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m48s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m6s
docker / deploy-docs (push) Successful in 29s
deb / build-publish (push) Successful in 11m48s
apple / swift (push) Successful in 1m10s
windows-host / package (push) Successful in 8m41s
release / apple (push) Successful in 8m47s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m52s
arch / build-publish (push) Successful in 10m51s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 1m46s
android / android (push) Successful in 12m52s
audit / cargo-audit (push) Successful in 2m3s
audit / bun-audit (push) Successful in 15s
ci / rust (push) Failing after 25s
windows / build (aarch64-pc-windows-msvc) (push) Failing after 1m11s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m12s
windows / build (x86_64-pc-windows-msvc) (push) Failing after 1m24s
apple / screenshots (push) Successful in 5m37s
ci / bench (push) Successful in 7m12s
decky / build-publish (push) Successful in 14s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 5s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 5s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 48s
flatpak / build-publish (push) Failing after 4m13s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m48s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m6s
docker / deploy-docs (push) Successful in 29s
Phase 1 of the dual-stack plan (design/client-parity-and-network-resilience.md, plan 5):
the host sizes each session's shard_payload from the QUIC remote's address family instead
of assuming IPv4 — 1408 over v4 (unchanged), 1388 over v6 (40-byte header). Rides the
existing Welcome::shard_payload negotiation, so there is zero wire change and old clients
simply follow.
This has to land before any v6 data path exists: the v4-maximal 1408 makes every sealed
video datagram overshoot a 1500-MTU IPv6 hop, and v6 routers never fragment — that's a
blackhole (every datagram dropped), not the graceful-ish degradation of the b5c30df v4
fragmentation saga. IPv4-mapped v6 remotes (::ffff:a.b.c.d, what a dual-stack [::] socket
reports for a v4 client) correctly keep the v4 size — they ride IPv4 on the wire.
New mtu1500_shard_payload_v6()/mtu1500_shard_payload_for() in core config with the same
pinned never-fragments/maximality tests as the v4 constant, plus a family-selection test.
Verified: 82 core lib tests + loopback/c_abi green and host check/clippy clean on Linux
(home-worker-2); core tests green on macOS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,32 @@ pub const fn mtu1500_shard_payload() -> usize {
|
||||
p - p % 2 // FEC requires even shards
|
||||
}
|
||||
|
||||
/// The IPv6 sibling of [`mtu1500_shard_payload`]: largest **even** shard payload whose sealed wire
|
||||
/// datagram fits an unfragmented IPv6/UDP packet on a standard 1500-byte MTU:
|
||||
/// `1500 − 40 (IPv6) − 8 (UDP) − HEADER_LEN − CRYPTO_OVERHEAD` = 1388. The 20 extra header bytes
|
||||
/// matter MORE here than on v4: IPv6 routers never fragment — an oversized datagram gets an ICMPv6
|
||||
/// Packet-Too-Big at best and a silent blackhole at worst — so streaming the v4 size (1408) to a
|
||||
/// v6 client wouldn't degrade the way v4 fragmentation did (the b5c30df saga), it would drop every
|
||||
/// video datagram on any 1500-MTU hop.
|
||||
pub const fn mtu1500_shard_payload_v6() -> usize {
|
||||
let p = 1500 - 40 - 8 - HEADER_LEN - CRYPTO_OVERHEAD;
|
||||
p - p % 2 // FEC requires even shards
|
||||
}
|
||||
|
||||
/// The MTU-safe shard payload for a session streaming to `peer` (the QUIC remote — the data plane
|
||||
/// dials the same address family): v6 sizing for a genuine IPv6 remote, v4 sizing otherwise —
|
||||
/// including IPv4-mapped IPv6 addresses (`::ffff:a.b.c.d`, what a dual-stack `[::]` socket reports
|
||||
/// for a v4 client), which ride IPv4 on the wire. Hosts pass this through
|
||||
/// `Welcome::shard_payload`, so per-family sizing needs no wire change and old clients simply
|
||||
/// follow the negotiated value.
|
||||
pub fn mtu1500_shard_payload_for(peer: core::net::IpAddr) -> usize {
|
||||
match peer {
|
||||
core::net::IpAddr::V4(_) => mtu1500_shard_payload(),
|
||||
core::net::IpAddr::V6(v6) if v6.to_ipv4_mapped().is_some() => mtu1500_shard_payload(),
|
||||
core::net::IpAddr::V6(_) => mtu1500_shard_payload_v6(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Everything needed to construct a [`Session`](crate::session::Session).
|
||||
///
|
||||
/// `Debug` is implemented by hand to redact `key`/`salt`, and `key`/`salt` are zeroized
|
||||
@@ -418,6 +444,32 @@ mod tests {
|
||||
assert!(HEADER_LEN + (p + 2) + CRYPTO_OVERHEAD > 1472, "not maximal");
|
||||
}
|
||||
|
||||
/// Pin the IPv6 wire math the same way: the sealed datagram must fit 1452 (1500 − IPv6 40 −
|
||||
/// UDP 8 — v6 routers don't fragment, so overshooting blackholes rather than degrades) and one
|
||||
/// shard-step above must not.
|
||||
#[test]
|
||||
fn mtu1500_shard_payload_v6_never_blackholes() {
|
||||
let p = mtu1500_shard_payload_v6();
|
||||
assert_eq!(p % 2, 0, "FEC requires even shards");
|
||||
assert!(p <= max_shard_payload());
|
||||
let wire = HEADER_LEN + p + CRYPTO_OVERHEAD;
|
||||
assert!(wire <= 1452, "sealed datagram {wire} B exceeds a 1500-MTU IPv6 hop");
|
||||
assert!(HEADER_LEN + (p + 2) + CRYPTO_OVERHEAD > 1452, "not maximal");
|
||||
}
|
||||
|
||||
/// Family selection: genuine v6 remotes get the v6 size; v4 — including the IPv4-mapped v6
|
||||
/// form a dual-stack `[::]` socket reports for a v4 client — keeps the v4 size.
|
||||
#[test]
|
||||
fn shard_payload_follows_peer_family() {
|
||||
use core::net::IpAddr;
|
||||
let v4: IpAddr = "192.168.1.50".parse().unwrap();
|
||||
let v6: IpAddr = "fd00::50".parse().unwrap();
|
||||
let mapped: IpAddr = "::ffff:192.168.1.50".parse().unwrap();
|
||||
assert_eq!(mtu1500_shard_payload_for(v4), mtu1500_shard_payload());
|
||||
assert_eq!(mtu1500_shard_payload_for(mapped), mtu1500_shard_payload());
|
||||
assert_eq!(mtu1500_shard_payload_for(v6), mtu1500_shard_payload_v6());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_block_exceeding_scheme_ceiling() {
|
||||
let mut c = Config::p1_defaults(Role::Host); // Gf8, ceiling 255
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use punktfunk_core::config::{
|
||||
mtu1500_shard_payload, CompositorPref, FecConfig, FecScheme, GamepadPref, Role,
|
||||
mtu1500_shard_payload_for, CompositorPref, FecConfig, FecScheme, GamepadPref, Role,
|
||||
};
|
||||
use punktfunk_core::input::{InputEvent, InputKind};
|
||||
use punktfunk_core::packet::{FLAG_PIC, FLAG_PROBE, FLAG_SOF};
|
||||
@@ -977,13 +977,16 @@ async fn serve_session(
|
||||
max_data_per_block: 4096,
|
||||
},
|
||||
// The largest even payload whose sealed datagram (header + shard + crypto) fits an
|
||||
// unfragmented IPv4/UDP packet on a 1500 MTU — 1408, giving 1472 = the exact ceiling.
|
||||
// The previous 1452 overshot it (its math forgot the header/crypto ride inside the UDP
|
||||
// payload) and silently IP-fragmented EVERY video datagram, doubling per-datagram loss
|
||||
// on Wi-Fi — the "100 Mbps badly fails on the phone" root cause. Negotiated, so the
|
||||
// client follows. Jumbo (≈8900) is a future negotiated bump (needs MAX_DATAGRAM_BYTES
|
||||
// raised + end-to-end 9000 MTU).
|
||||
shard_payload: mtu1500_shard_payload() as u16,
|
||||
// unfragmented UDP packet on a 1500 MTU for THIS client's address family — 1408 over
|
||||
// IPv4 (1472 = the exact ceiling), 1388 over IPv6 (40-byte header, and v6 routers
|
||||
// don't fragment: overshooting there blackholes instead of degrading). The data plane
|
||||
// dials the same family as this QUIC connection, so the remote decides. The previous
|
||||
// hardcoded 1452 overshot the v4 ceiling (its math forgot the header/crypto ride
|
||||
// inside the UDP payload) and silently IP-fragmented EVERY video datagram, doubling
|
||||
// per-datagram loss on Wi-Fi — the "100 Mbps badly fails on the phone" root cause.
|
||||
// Negotiated, so the client follows. Jumbo (≈8900) is a future negotiated bump (needs
|
||||
// MAX_DATAGRAM_BYTES raised + end-to-end 9000 MTU).
|
||||
shard_payload: mtu1500_shard_payload_for(peer.ip()) as u16,
|
||||
encrypt: true,
|
||||
key,
|
||||
salt: *b"pkf1",
|
||||
|
||||
Reference in New Issue
Block a user