feat(control): advertise and decode zstd-compressed map responses#257
Conversation
Set Compress to zstd on every map request and decompress each streaming map-poll frame before deserializing, matching the Go control client which sets request.Compress unconditionally and zstd-decodes every response frame. An empty Compress is an observable not-Go fingerprint, and a control plane honoring the request replies with per-frame zstd that the reader must decode. The frame reader recognizes the zstd magic, stream-decompresses with a separate decoded-size bound (64 MiB zip-bomb guard, independent of the on-wire compressed-frame cap), and falls back to parsing an uncompressed body verbatim when control ignores the request. ruzstd is pure-Rust, so the ring-only, musl-static egress posture is unchanged. Also prunes the now-dead stun-rs/dashmap lock entries left orphaned after the StunProber removal so a locked build matches the manifests. Tests: a foreign-encoder interop KAT (reference zstd CLI frame), self- and uncompressed-frame decode, malformed-frame and zip-bomb rejection, and the builder Compress assertion. Verified live against Tailscale SaaS: the node joined and decoded the full real-zstd netmap (17 peers, self-node, MagicDNS). Signed-off-by: GeiserX <9169332+GeiserX@users.noreply.github.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds Changeszstd Map-Poll Decompression
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ests Review follow-up. The MAX_DECODED_NETMAP doc claimed ruzstd rejects an over-large declared window at frame-init; that guard lives in the decoder reuse path, not the one-shot StreamingDecoder::new path this code takes. Verified in ruzstd source that the ring buffer allocates lazily from zero, so a declared multi-terabyte window drives no eager allocation and the take(N+1) output cap is the binding bound. Reword to state that accurately, including the transient peak-memory characteristic, and document why the output Vec is grown on demand rather than pre-reserved to the cap. Add two edge-case tests: a large in-bounds decoded frame is accepted (pins no off-by-one wrongly rejects a big netmap) and a good-then-malformed two-frame stream ends cleanly after the first frame was delivered. Signed-off-by: GeiserX <9169332+GeiserX@users.noreply.github.com>
What
Make the streaming map poll negotiate and decode zstd-compressed
MapResponseframes, matching the Go control client.MapRequestBuilder::newnow setsCompress = "zstd"on every map request. Go'scontrol/controlclient/direct.gosetsrequest.Compress = "zstd"unconditionally for all client builds; an emptyCompressis an observable not-Go fingerprint.tokio::map_stream) decompresses each frame before deserializing, mirroring Go's per-framezstdframe.AppendDecode.How
decompress_framehelper recognizes the zstd magic (0x28 0xb5 0x2f 0xfd) and stream-decompresses viaruzstd::decoding::StreamingDecoder.MAX_DECODED_NETMAP(64 MiB) bounds the decompressed output as a zip-bomb / decompression-amplification guard (reads one byte past the limit and rejects). ruzstd additionally rejects an over-large declared window at frame init, so neither the window header nor the output can drive an unbounded allocation.Compressrequest and replied uncompressed) is parsed verbatim — no silent stall, and zero wire-fingerprint cost since the request is byte-identical either way.ruzstdis pure-Rust (no Czstd-sys, noaws-lc/openssl/ring), so the ring-only / musl-static egress posture is unchanged. Decode-only.stun-rs/dashmaplock entries left orphaned after the earlier StunProber removal, so a--lockedbuild matches the manifests.Tests
zstdCLI v1.5.7) decodes to the original JSON and drives aStateUpdate. This is the property that matters: we decode control's (Go's) output, not just frames our own encoder round-trips.Compress = "zstd"assertion (value + wire key).controlplane.tailscale.com, was assigned a CGNAT IPv4, and decoded the full real Go-encoded zstd netmap — 17 peers, self-node, status, and MagicDNS resolution all correct.Gates (local, all green)
cargo test -p geiserx_ts_control(240) ·-p geiserx_ts_runtime(328) ·clippy --all-targets -- -D warnings·fmt --check·cargo doc(broken_intra_doc_links=deny) ·cargo run -p checks·--features tuncross-build ·cargo deny check all(advisories/bans/licenses/sources ok).Signed-off-by: Sergio sergio@geiser.cloud
Created using Claude Code (Opus 4.8)
Cargo.lock note
Beyond adding
ruzstd/twox-hash, the lockfile diff also (a) prunesstun-rs/dashmap/precis-*entries orphaned by the earlier StunProber removal and (b) resyncs the workspace-crate versions0.32.0→0.39.0that had drifted stale onmain(the manifest already declares 0.39.0). Both are correct reconciliations that anycargobuild regenerates; surfaced here so the lock diff's size is expected.cargo metadata --lockedpasses.Review pass
Reviewed by a 3-lens panel (security / correctness+parity / simplicity) — verdict APPROVE, zero critical/high. Follow-up commit corrected the
MAX_DECODED_NETMAPdoc (the zip-bomb guard is thetake(N+1)output cap; ruzstd allocates its window lazily from zero, verified in source + empirically — a multi-TB declared window drives no eager allocation) and added accept-side-boundary + mid-stream-failure tests.Summary by CodeRabbit
New Features
Tests