Repository navigation
[RFC] Roadmap to v1.9: xmux-style Stream Splitting, Temporal Jitter, and Resilience against Active Throttling #369
Description
Description:
The v1.8.0 release is a game-changer—the random padding and decoy HTML pages significantly raise the cost for passive and active censors. However, in highly restrictive network environments (specifically those exhibiting aggressive RST-injection and per-flow throttling), the current "One TCP Stream = One Deployment" model remains a single point of failure.
I would like to propose a technical path forward for v1.9 that evolves the Batch Mux into a Distributed Mux (xmux) to ensure session survivability.
- The Survival Case for xmux (Stream Splitting)
In the current architecture, if a specific Google account/Deployment ID is flagged or throttled by an ISP, any TCP session tied to that worker experiences immediate lag or a hard disconnect (via injected RST packets).
The Proposal: Implement True xmux (Stream Splitting). By fragmenting a single TCP stream (e.g., a 4K video or a large file download) and scattering the chunks across a pool of 5–10 different Google accounts, we achieve high survivability.
Result: Even if the ISP throttles 30% of the connections or injects RSTs into two of the workers, the reassembly layer at the tunnel-node ensures the end-user's session stays alive. This makes the tunnel virtually impossible to kill without the ISP blocking the entire Google Edge CIDR.
- Beyond Payload Padding: Temporal Fingerprinting
While v1.8 fixed the "Length-Distribution Fingerprint," advanced DPI also looks at Temporal Fingerprinting (the consistency of packet inter-arrival times).
The Issue: A static BATCH_COALESCE_WINDOW (currently 8ms) creates a predictable heartbeat that DPI can match.
The Fix: Introduce a Jitter Factor to the window. By making the coalesce window 8ms ± 25% (randomized per batch), we break the timing signature of the tunnel, making it look like sporadic, human-triggered API traffic.
- "Light xmux" as a Starting Point
To avoid the complexity of a full architectural rewrite, we could implement a Light xmux layer:
Instead of splitting every small packet, only split payloads that exceed a certain threshold (e.g., > 1200 bytes).
This keeps metadata/handshake packets on a primary lane while distributing the "heavy lifting" of data transfer across multiple workers, reducing the implementation surface while keeping the benefits of bandwidth aggregation.
- Burst-Aware Speculative Pre-fetching
To solve the GAS latency vs. Quota trade-off:
Implement a "Hot/Cold" state machine.
If the client detects active data transfer, it enters a "Hot State" and speculatively fires batches every ~150ms to "pull" data from the tunnel-node before the client asks for it.
If idle for > 5 seconds, it returns to "Cold State" to conserve the 30-concurrent-request limit and Google quota.
Summary
The transition from v1.8 to v1.9 should focus on Statistical Survivability. If we can decouple the user's session from a single worker and randomize the timing, we move from "Bypass" to "Invisibility."
I'm eager to help test these scenarios in environments where RST-injection is prevalent!
Activity
therealaleph commentedon Apr 28, 2026
@w0l4i — substantive RFC, working through each piece. Ranking by impact/feasibility for the Iran-RST scenario:
1. xmux stream splitting — accept, v1.9.x scope
Agreed this is the highest-leverage feature for the "ISP RST-injects 30% of connections" scenario from #313. The current "one TCP stream = one deployment" model means a single bad deployment kills any session bound to it; splitting across 5–10 deployments means we degrade gracefully instead of failing hard.
Implementation sketch:
Client side (mhrv-rs Rust):
{stream_id, seq, offset, len}in the batch opServer side (tunnel-node):
{client_id, stream_id}, ordered byseqCost estimate: ~800–1500 LOC + non-trivial test infrastructure. Need:
Compatibility: breaking protocol change between client and tunnel-node. Need either:
I'd want to ship this with a
xmux: falseconfig default for one release before flipping it on, so users can opt in + we can collect real-world failure modes before making it the default.Targeting v1.9.0 as the headline feature. Roughly 2–4 weeks of implementation + 1–2 weeks of testing + iteration before merge.
2. Temporal jitter — accept, v1.8.x scope
Agreed, packet inter-arrival timing is a real fingerprinting vector once length distribution is randomized (which v1.8.0 just did). Cheap to add:
With
BATCH_TEMPORAL_JITTER_MS = 50:Concern: if jitter is too high, throughput tanks because batches starve. 50 ms ceiling is conservative — even at maximum jitter, we add half of one Apps Script roundtrip, not multiple roundtrips.
Concern 2: correlation. If all sessions in a single mhrv-rs process jitter together, the fingerprint stays. Each session needs independent jitter (which my sketch above does —
thread_rnggives per-thread independence).Targeting v1.8.x point release alongside the Android UI batch. ~30 LOC, days not weeks.
3. Resilience against active throttling
This is more strategic than tactical. The core idea — "treat per-deployment health as a first-class signal in routing" — is what #319 (deployment auto-blacklist) already partially does. The roadmap continuation is:
These are smaller individually than xmux but architecturally compatible — they all live in the
next_script_id()/ dispatcher layer.Targeting v1.8.x or v1.9.x depending on sequencing. If I do these before xmux, the dispatcher gets richer, then xmux plugs into the richer dispatcher cleanly. If I do xmux first, the dispatcher needs a partial rewrite. Inclined to do small dispatcher improvements first.
Summary table
Adding to roadmap explicitly. xmux gets its own tracking issue once I start design work.
If you want to take temporal jitter as a small PR (~30 LOC + 1 test), happy to review same-day. xmux is too architecturally intertwined for a drive-by PR — better as a coordinated design discussion.
Thanks for the careful threat model — the v1.8.0 random padding came from your previous RFC and the next round of hardening is going to come from this one.
[reply via Anthropic Claude | reviewed by @therealaleph]
2bemoji commentedon Apr 28, 2026
This RFC identifies the most critical bottleneck in the current architecture: The fragility of the single-stream mapping. While v1.8’s padding addressed what the traffic looks like, these v1.9 proposals address how the traffic behaves and where it flows, which is the next frontier in bypassing state-level DPI.
The proposal for "True xmux" is a significant step toward Active Fault Tolerance.
Static timing is the "silent killer" of encrypted tunnels. Advanced DPI (like the GFW or sophisticated corporate middleboxes) uses Inter-Arrival Time (IAT) histograms to identify tunnels even when the payload is fully encrypted.
BATCH_COALESCE_WINDOWbreaks the regularity that machine-learning classifiers look for.The "Light xmux" approach for payloads
It avoids the overhead of fragmenting small ACK/handshake packets—which are sensitive to reordering—while effectively hiding the "Bulk Data" signature that triggers most active throttling.
The Hot/Cold State Machine is a clever solution to the Google Apps Script (GAS) concurrency limits. It ensures the 30-request quota is spent where it matters most: reducing the "Time to First Byte" during active usage.
The transition from "Bypass" to "Invisibility" requires exactly this kind of shift toward Statistical Survivability. If one worker dies, the session must live. If the timing is analyzed, it must appear random. I fully support this roadmap and would be interested in seeing the performance benchmarks for the "Hot State" pre-fetching logic.
Quick Question for the Devs: For the xmux reassembly layer, are we considering a sequence-number-based buffer at the tunnel-node to handle potential out-of-order delivery from the different Google workers?
@w0l4i @therealaleph
therealaleph commentedon Apr 28, 2026
@2bemoji — accurate framing. The "session persistence" angle on xmux is the right one — current architecture is single-stream-per-deployment, so any per-flow throttle or RST kills the entire session. Splitting across deployments at the byte-range level distributes the failure surface.
For v1.9.0 design, the four sub-decisions to settle:
Chunk size — too small (~4 KB) wastes Apps Script per-call overhead; too large (~256 KB) reduces failure-spread granularity. Sweet spot is probably ~64 KB chunks based on Apps Script's per-call latency floor (~250-500ms = chunk should take that long to be balanced).
Reassembly buffer bound — adversarial sequence numbers could OOM the tunnel-node if not capped. Bound: per-stream-id, per-client-id, max 8 MB out-of-order buffer + reject anything that would push past it. Stream is then implicitly capped at 8 MB of in-flight unreordered.
ACK granularity — chunk-level acks let the client retransmit only the failed chunks (which is the win), but require client-side retransmit logic. Stream-level acks are simpler but lose the "split across deployments" benefit on retransmit.
Backwards-compat negotiation — at the auth handshake, client signals "I speak xmux v1" and tunnel-node responds "I speak xmux v1 / fallback to single-stream". Both binaries need the protocol-version exchange before the protocol switch happens. Ship in v1.8.x point release first (just the negotiation), then v1.9.0 turns on xmux when both sides have it.
I'll start the design issue when I close out the v1.8.x batch (DNS, Android UI, googlevideo_ip, in-app updater). Tagging you + @w0l4i + @ipvsami as core reviewers — you've collectively contributed the most signal on the bottlenecks this is meant to fix.
ETA on the design issue: ~3-4 weeks (after v1.8.x ships). Implementation: ~4-6 weeks after the design freezes. Total to v1.9.0 GA: ~8-12 weeks if everything goes smoothly.
The "session persistence" framing is what I'll lead the design issue with — better than my earlier "high-survivability stream splitting" framing because it's user-facing.
[reply via Anthropic Claude | reviewed by @therealaleph]