feat(3b): junk/decoy packets + traffic-shaping — blur the flow-shape fingerprint - #47
Merged
Conversation
…c-shaping Approved brainstorming output for 3b. Blurs yip's residual FLOW-SHAPE fingerprint (the statistical silhouette 3a leaves — 2-packet handshake opener + constant 30ms Control pulse) with latency-free junk/decoy traffic, all behind the existing obf_psk and reusing the 3a yip-obf envelope (no new crate, no wire-format change). Three techniques: (1) Jc/Jmin/Jmax junk burst around the handshake (automatic), (2) opt-in idle cover traffic (cover_traffic_ms, default off), (3) jitter the un-jittered 30ms Control-feedback cadence. New JUNK_TYPE=6 ptype, silent-drop on receive (inert, zero reflection surface). Fast xorshift PRNG for junk bodies/lengths/counts (masked anyway), getrandom only for the nonce. Data-path timing shaping stays OUT (latency) -> deferred paranoid mode. obf_psk absent = byte-identical. Tested by nDPI no-regression gate + a lightweight deterministic flow-shape structural check (not an ML harness). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7-task TDD plan from the approved 3b spec. Reuses the merged 3a yip-obf envelope: (1) JUNK_TYPE=6 + a fast xorshift PRNG in yip-obf; (2) junk-datagram builder + silent-drop JUNK_TYPE demux arm in peer_manager; (3) handshake junk burst (Jc 3..=12 around begin_handshake); (4) opt-in idle cover traffic (cover_traffic_ms); (5) jitter the un-jittered 30ms Control-feedback cadence; (6) netns junk-doesn't-break-connectivity + obf-off no-regression both drivers; (7) nDPI oracle no-regression + a lightweight deterministic flow-shape structural check (pre-data packet count varies across sessions). Latency-free throughout; obf_psk absent = byte-identical; junk never touches Noise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two primitives for junk/decoy packet obfuscation: - JUNK_TYPE (const u8 = 6): packet-type discriminant for junk datagrams - XorShift64: fast non-cryptographic PRNG for generating junk content XorShift64 is seeded once from OS RNG, then uses pure userspace xorshift64* algorithm for cheap byte generation. Content is keystream-masked by obfuscate(), so randomness quality only needs to be fast, not cryptographic. Includes 4 new tests covering type distinctness, range generation bounds, byte filling variation, and round-trip obfuscation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds PeerManager::build_junk (random-length throwaway body, keystream-masked under yip_obf::JUNK_TYPE) and the receive-side silent-drop arm in deobf_ingress (both the session-key and network-key branches): a datagram that unmasks to JUNK_TYPE is dropped inertly (no reply, no state mutation, no fall-through). Junk emission itself lands in Tasks 3/4.
begin_handshake now returns Vec<EgressDatagram>: when obfuscation is on and the handshake is direct (not relayed), the real Init is preceded by a randomized Jc in [JUNK_BURST_MIN, JUNK_BURST_MAX] burst of junk datagrams, so the flow no longer opens with a countable "2 packets then data". Relay path and obf-off path are unchanged (single datagram, byte-identical). Updates all 8 begin_handshake call sites from push(dg) to extend(dgs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_egress (3b) build_junk was pre-obfuscating its output, and every egress datagram then flowed through obf_egress, which wrapped it again — double-obfuscating junk. The receiver's single deobfuscate recovered a garbage ptype (the inner envelope's random nonce byte), so junk was dropped via the generic unrecognized-ptype path instead of the dedicated JUNK_TYPE arm. build_junk now returns a plaintext [JUNK_TYPE][random body] datagram, like every other producer; obf_egress performs the single wrap (network key for a handshake-burst dst, session key for an established-peer cover dst). Updated the Task 2/3 tests to assert the plaintext form and added an end-to-end test that drives on_tun through the real obf_egress pass and proves every emitted datagram recovers under exactly one deobfuscate call.
Emit one session-keyed junk datagram per interval to each Established peer that has been idle (no real Data sent or received), so an obfuscated flow never goes tellingly silent. Config key cover_traffic_ms (default off, 0 rejected); PeerManager tracks per-peer last_activity_ms/last_cover_ms and gates emission on both being idle. No-op unless obf_psk is also configured.
…ire) (3b) A relay-reached peer (relay == true, established via the passive relayed_handshake_init/relayed_handshake_resp path) keeps a stale candidate `endpoint` from before relay took over. The idle-cover block fired at that stale address for any Established peer regardless of the relay flag, missing the actual peer every interval and leaking periodic junk to an unrelated address. Gate cover emission on !relay, mirroring the real-Data egress arm above, matching Task 3's direct-path-only scope for handshake junk. Adds tick_emits_no_cover_for_relay_peer alongside the existing four cover tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reshuffles WHEN DataPlane::tick emits its 30 ms loss-feedback Control packet by ±25% when obfuscation is on, reusing peer_manager::jitter_ms (the same stored-jittered-interval pattern 3a used for retry_ms/reg_ms). obf-off keeps the cadence byte-identical to pre-3b (exactly 30 ms) — never delays a data packet, only the reporting timer's own schedule.
Prove 3b junk/decoy traffic doesn't break connectivity: obfuscated_ping_with_cover (OBF_PSK + cover_traffic_ms) pings across; the existing obf-on tests (direct/relay/punch/discovery) now exercise the automatic handshake junk burst + Control-cadence jitter. COVER_MS env-guard in run-netns-tunnel.sh (no-op when unset => byte-identical). Gate: 6 obf-on + 10 obf-off, both drivers (poll + io_uring), all green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k (3b) Confirms 3b's junk/jitter doesn't reintroduce a DPI signature: re-ran the existing run-ndpi-oracle.sh against the 3b-junk-laden capture — still Unknown by content, no NDPI_OBFUSCATED_TRAFFIC (unchanged, no regression). Adds run-flowshape-check.sh, a deterministic packet-count structural check (the flow-shape analogue of 3a's no_byte_position_is_constant): for 8 independent obf-on sessions, measures the handshake-phase datagram count via inter-packet-gap cutoff (burst = sub-ms gaps, steady-state Control-feedback cadence = ~10-30ms gaps) and asserts it's >2 (junk present) and takes >1 distinct value across sessions (Jc burst redrawn per handshake). Along the way: discovered `initiate=` is a dead config key (dropped in 2a) — both peers actually bootstrap-initiate and glare-resolve, so both sides emit independent junk bursts rather than a single initiator; the gap-based count is robust to that instead of assuming a fixed initiator role. Also found and fixed a capture-timing bug where a too-short kill grace raced tcpdump's pcap writer, silently producing empty captures. Wires flowshape_not_obviously_constant into the netns-tunnel-test loop (both poll and io_uring drivers). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RVP6NnbDMAg1iTsMMTfL86
…+junk floor (3b) Gate (a) asserted handshake-phase count > 2 to prove "junk present," but both peers glare-initiate at bring-up (each self-initiates; the loser's Init still hits the wire before glare-resolution), so a junk-free two-sided-glare handshake already yields 3 datagrams (Init(A) + Init(B) + Resp) — a >2 threshold would pass even with junk disabled. Raise the threshold to >4: strictly above the junk-free glare baseline (with a +1 retransmit margin) and strictly below the observed junk-present minimum (~7-8, clipped from the ~9 theoretical floor). Rewrite the overclaiming comments in run-flowshape-check.sh and tunnel_netns.rs to state the real baseline and call out gate (b) as the primary non-vacuous proof of randomization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RVP6NnbDMAg1iTsMMTfL86
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sub-project #3, milestone 3b: junk/decoy packets + traffic-shaping
Blurs yip's residual flow-shape fingerprint — the statistical silhouette 3a leaves (a 2-packet handshake opener + a constant 30 ms Control pulse) — with latency-free junk/decoy traffic, all behind the existing
obf_pskand reusing the 3ayip-obfenvelope (no new crate, no wire-format change). Builds on merged 3a.Spec:
docs/superpowers/specs/2026-07-08-antidpi-junk-traffic-shaping-3b-design.md· Plan:docs/superpowers/plans/2026-07-08-antidpi-junk-traffic-shaping-3b.mdWhat it does
A new
JUNK_TYPE=6ptype: junk is plaintext[JUNK_TYPE][random body]thatobf_egresswraps once (so it's byte-indistinguishable from real traffic to anyone withoutobf_psk); the receiver silently drops it (inert — no reply, zero reflection/amplification). Three latency-free techniques:obf_psk) —Jc ∈ 3..=12decoys around the direct-pathHandshakeInit, so the flow no longer opens with a countable "2 packets then data."cover_traffic_ms, default off) — one session-keyed decoy per interval to idle direct peers, so the flow never goes tellingly silent.A tiny handwritten xorshift PRNG (seeded once from
getrandom) fills junk bodies/lengths/counts;getrandomstays only for the per-packet nonce. The junk body is keystream-masked, so its content is irrelevant to indistinguishability.Invariants (opus whole-branch review: READY TO MERGE)
obf_pskabsent ⇒ byte-identical to merged 3a/2a/2b/2c (every path gated onobf_key.is_some()).JUNK_TYPE ⇒ drop; no reply, no state change, no panic from any junk/garbage datagram.Testing
obfuscated_ping_with_cover) + 10 obf-off (byte-identical no-regression) × both drivers (poll + io_uring), all green.Unknownwith noNDPI_OBFUSCATED_TRAFFIC.Scope / non-goals (later milestones)
Data-path timing shaping / inter-packet delay (would add latency) → deferred opt-in "paranoid mode" / sub-project #4 · entropy shaping (
SUSPICIOUS_ENTROPY, still report-only in the oracle) → 3c TLS/QUIC mimicry · port plausibility → 3d. Relay-path junk is out of scope (handshake junk is direct-only; cover skips relay peers).🤖 Generated with Claude Code