Skip to content

feat(android): add youtube_via_relay toggle to Advanced settings#3

Open
yyoyoian-pixel wants to merge 96 commits intomainfrom
feat/android-youtube-via-relay
Open

feat(android): add youtube_via_relay toggle to Advanced settings#3
yyoyoian-pixel wants to merge 96 commits intomainfrom
feat/android-youtube-via-relay

Conversation

@yyoyoian-pixel
Copy link
Copy Markdown
Owner

Summary

  • Add youtubeViaRelay field to MhrvConfig with JSON serialization (youtube_via_relay), deserialization, and config-sharing encode
  • Add a Switch toggle in the Android Advanced settings section, matching the desktop UI checkbox
  • Add EN and FA string resources for label and helper text

Parity fix: the desktop UI already has a youtube_via_relay checkbox (src/bin/ui.rs) but the Android UI was missing it — users had to hand-edit config.json to enable it on mobile.

🤖 Generated with Claude Code

dazzling-no-more and others added 30 commits April 25, 2026 16:49
…ecycle-reliability

fix(android): tighten VPN session lifecycle reliability
Five small but real Android-only fixes:

1. Connect/Disconnect button gated on VpnState.isRunning state-flow
   with 12s backstop, replacing the fixed 2s transitionCooldown
   timer. Closes the race where a tap-after-Stop hit "Address already
   in use" because the previous teardown's listener-socket release
   wasn't done.

2. Tun2proxy.stop() wrapped in 2s join() — if the native call hangs,
   bounded teardown still releases the listener port instead of
   holding the teardown thread.

3. fd-leak fixed between parcelFd.detachFd() and Thread.start(): an
   OOM-thrown Thread.start used to orphan the detached fd. Now
   adopted into a fresh ParcelFileDescriptor purely so we can close()
   it.

4. Misleading teardown doc-comment rewritten — the "step 2 closes
   the TUN fd to force EBADF on read" claim has been factually
   wrong since detachFd landed.

5. Recursive crash trap: Log.e in MhrvApp's uncaught handler now
   wrapped in try/catch so a logd failure during exception logging
   falls through to the previous handler with the real exception.

No Rust changes; 98 lib + 22 tunnel-node tests still pass.

Local Android build verified, APK installed on mhrv_test emulator,
launches cleanly with v1.6.1 in title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In `relay_parallel_range`, when a chunk failed validation
(`extract_exact_range_body` returned Err) OR the stitched body length
didn't match the advertised total, the fallback path called
`rewrite_206_to_200(&first)` — which converted the 256 KiB probe
response into HTTP 200 + Content-Length=262144 and returned that as
if it were the full file. Browsers saw a complete-looking 200 and
treated the download as finished at 256 KB.

Common triggers for the chunk-validation failure (per the user
reports):
- Apps Script's UrlFetchApp stripping `Content-Range` from chunk
  responses while preserving it on the probe
- Origin returning 200-OK on follow-up Range requests (some servers
  flatten ranges after the first one)
- Mismatched `total` field across chunks for paths behind a varying
  cache layer

The correct fallback is a single GET without any Range header —
Apps Script fetches the whole URL (up to its 50 MiB cap) and
returns a normal 200 with the complete body. Slower than parallel
for large files but produces a correct response, which is the
minimum bar.

Two independent reports (Ehsan in therealaleph#162, Recruit1992 confirming).
98 lib tests still pass; existing `validate_probe_range_rejects_*`
and `extract_exact_range_body_*` tests already cover the validation
side, the fallback path is observed integration-testing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buildNotif() hardcoded `proxyPort + 1` for the SOCKS5 line, ignoring
cfg.socks5Port entirely. With the default Android config
(listenPort=8080, socks5Port=1081) the foreground notification read
"Routing via SOCKS5 127.0.0.1:8081" but the real listener was on 1081 —
so users configuring per-app SOCKS5 (Telegram, etc.) against the
notification value silently failed.

Use the same `cfg.socks5Port ?: (cfg.listenPort + 1)` elvis fallback the
real listener uses, and surface both ports in the notification:
  HTTP 127.0.0.1:8080  ·  SOCKS5 127.0.0.1:1081

Reported by vpnineh and l3est (with netstat screenshots showing the
exact mismatch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The batch-build loop blocked on a 30 ms timeout for the first message,
then drained whatever else was in the channel via try_recv() and fired
the batch. Under any non-bursty workload, the channel queue was always
empty by the time the first op woke us up — so every "batch" had
exactly one op, defeating the entire batching premise. Reporter (w0l4i)
saw `batch: 1 ops → ..., rtt=6.3 s` repeating in logs even under high
concurrency.

Fix: after the first op lands, hold the buffer open for an 8 ms
coalescing window. Concurrent ops (parallel fetches, HTTP/2 stream
openings, etc.) now accumulate into the same batch. 8 ms is rounding
error against the 2–7 s Apps Script RTT we're amortizing, and restores
the multi-op-per-batch behavior the rest of the code already supports
(MAX_BATCH_OPS=50, MAX_BATCH_PAYLOAD_BYTES=4 MiB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: include twitter.com in X.com URL normalization (therealaleph#245)
- therealaleph#245 (@Parsa307): match twitter.com in X.com URL normalization
- therealaleph#255 (@dazzling-no-more): copy-logs button + selectable log lines on Android
- therealaleph#257 (@dazzling-no-more): bulk paste of multiple deployment IDs on Android
- therealaleph#256 (@dazzling-no-more): plain HTTP proxy passthrough in google_only mode
  (used to return 502; now falls through to direct TCP / upstream_socks5,
  matching the existing CONNECT behavior)

No protocol or wire-format changes; existing config and Apps Script
deployments work unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…table VoIP, faster browsing - needs new tunnel deployment for udpgw (therealaleph#222)

* feat: native udpgw protocol alongside existing UDP associate

Why udpgw is needed even with UDP associate:

UDP associate (udp_open/udp_data) creates one tunnel session per UDP
destination and polls each independently. On high-latency or shaky
networks this compounds — N simultaneous UDP flows need N separate
polling loops, each paying its own batch round-trip overhead. Google
Meet calls, which fire dozens of concurrent STUN + RTP flows, stall
or fail entirely because the per-destination polling can't keep up.

udpgw multiplexes ALL UDP over one persistent TCP-like session using
conn_id framing. One batch op carries frames for many destinations.
Persistent sockets per (conn_id, dest) with continuous reader tasks
keep source ports stable — critical for protocols like Telegram VoIP
and STUN that expect replies on the same port.

Both paths coexist — they serve different traffic:
  - UDP associate (SOCKS5): apps that negotiate SOCKS5 UDP relay
  - udpgw (198.18.0.1:7300): TUN-captured UDP (DNS, QUIC, Meet, etc.)

tun2proxy vendored as git submodule at v0.7.20 with one transparent
commit adding udpgw_server to the Android JNI run() function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: block QUIC (UDP 443) and DNS (UDP 53) from udpgw

QUIC through udpgw is slower than TCP/HTTP2 through the batch pipeline
— blocking it forces browsers to fall back to TCP, improving YouTube
and general browsing speed.

DNS is better handled by tun2proxy's virtual DNS / SOCKS5 UDP associate
path which is more reliable for single request-response exchanges.

VoIP (Telegram, Meet) still flows through udpgw normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace submodule with [patch.crates-io] for tun2proxy udpgw

Use the idiomatic Rust [patch.crates-io] mechanism instead of a git
submodule. Points to yyoyoian-pixel/tun2proxy fork with the udpgw
JNI parameter patch (upstream PR: tun2proxy/tun2proxy#247).

Will be removed once upstream ships the change in tun2proxy >= 0.8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: pin tun2proxy patch SHA in Cargo.lock

Locks tun2proxy at dfc24ed1 so the patch resolution is recorded and
any branch rewrite is visible in the lockfile diff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use AbortHandle for ConnSocket readers to prevent FD leaks

JoinHandle::drop detaches the task without aborting it. When
udpgw_server_task is cancelled (session close), the post-loop
cleanup never runs and per-(conn_id, dest) reader tasks become
zombies holding Arc<UdpSocket> file descriptors.

AbortHandle::drop aborts the task automatically, so cleanup is
correct by construction regardless of how the parent task exits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Highlights:
- Native udpgw protocol in Full mode (therealaleph#222) — Telegram voice/video
  calls and Google Meet now work in Full mode on Android. UDP flows
  through one persistent TCP tunnel (instead of session-per-destination)
  so STUN/RTP flow counts no longer stall. Requires redeploying the
  tunnel-node Docker image (ghcr.io/therealaleph/mhrv-tunnel-node:1.7.0).
- Android home screen restructure (therealaleph#258, closes therealaleph#246) — Connect button
  now pinned under Mode field, App picker shows pre-selected apps at
  top. With long deployment-ID lists, Connect no longer scrolls
  off-screen.
- release-drafter + prepare-release tooling (therealaleph#260) — incrementally
  drafts release notes from merged PR titles; manual workflow_dispatch
  prepares version bumps + changelog stubs.

No protocol breaking changes; existing apps_script-mode and Full-mode
deployments work unchanged. Full-mode users get udpgw automatically
once the tunnel-node Docker image is updated.

Thanks to @yyoyoian-pixel and @dazzling-no-more.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…te revocation (therealaleph#121)

* feat(cert): add --remove-cert flag and Remove CA button for clean-slate revocation

* fix(cert): testable euid-root branch + orphan enterprise_roots warning
mhrv-rs --remove-cert (CLI) and Remove CA button (UI) for verified
clean-slate revocation. Clears OS trust store, NSS browser stores
(Linux Firefox/Chrome), and the on-disk ca/ directory. config.json
and the Apps Script deployment are untouched.

By-name trust verification runs before browser-state mutation; OS
removal failures return RemovalIncomplete with browser state intact
so retries are idempotent. Sudo-aware on Unix (re-roots HOME to the
real user). 29 new unit tests on the pure logic (Firefox user.js
marker handling, getent passwd parsing, NSS stderr classification,
NssReport state rules).

Tested end-to-end on Windows by the contributor; macOS verified at
merge time on real hardware (login keychain delete + NSS-missing
fallback). Linux paths await user testing.

Closes therealaleph#121.
Thanks @dazzling-no-more.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Telegram release notifier used to post just the universal APK with
a single-document caption. This change ships the per-platform binaries
for macOS (amd64+arm64 CLI), Linux (amd64+arm64 CLI), Windows
(amd64 UI), and Android (universal APK) as a single Telegram media
group with one caption listing every filename + SHA-256.

Workflow side (.github/workflows/release.yml):
- The telegram job now downloads ALL artifacts (was: APK only).
- New `Prepare files for Telegram media group` step extracts the raw
  binaries out of each per-platform .tar.gz / .zip (no archive
  wrappers in the channel) and renames them with version suffixes
  (mhrv-rs-linux-amd64-v1.7.2, mhrv-rs-windows-amd64-ui-v1.7.2.exe,
  etc.). Per-platform extraction is best-effort: a missing artifact
  emits a `::warning::` and skips that platform rather than failing
  the whole post.
- The post step builds a `--files <path>` arg list from tg-files/,
  sorted for deterministic order across runs, and invokes the
  notifier without --with-changelog (the script auto-replies with
  changelog whenever --files is used).

Script side (.github/scripts/telegram_release_notify.py):
- New --files arg (repeatable). 2..=10 files → sendMediaGroup; 1 file
  → sendDocument with the same caption shape; 0 → error. Telegram's
  sendMediaGroup rejects single-item groups, so the 1-file fallback
  isn't optional.
- New build_media_group_caption() composes title + per-file
  filename+SHA list + repo/release URLs. Fits ~860 chars for a 6-file
  release; fallback to filename-only-list if a future swell pushes
  past Telegram's 1024-char caption cap.
- send_media_group() handles the multipart/form-data shape with each
  file referenced as `attach://fileN` from the media JSON. Caption is
  attached to file 0 only (Telegram clients render per-item captions
  inconsistently for media groups; first-item-only is the safe
  pattern).
- Legacy --apk path kept for any caller that hasn't migrated; either
  --apk or --files must be present (validated at startup).
- _content_type_for() picks application/vnd.android.package-archive
  for .apk and application/octet-stream for everything else, so
  Telegram clients label the APK with the Android icon and label
  desktop binaries by filename without a misleading icon.

Behavioural change for users:
- The Telegram channel now sees one grouped post per release with all
  primary platform binaries inline, instead of just the APK. macOS
  users wanting the gatekeeper-friendly .app.zip still grab it from
  the GitHub Releases page; the Telegram drop is for the "give me
  the binary, I'll run it" path.
- The Persian/English changelog reply that used to be opt-in (via
  TELEGRAM_INCLUDE_CHANGELOG=true) is now automatic in the --files
  path because the per-file SHA list eats the caption budget that
  previously held the FA brief-note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…therealaleph#266)

* feat(android): config import/export via clipboard, QR code, deep link, and share sheet

- Clipboard paste: banner auto-detects mhrv:// or raw JSON in clipboard,
  one tap to import. Clipboard cleared after successful import.
- Export dialog: QR code + compressed hash + copy button + Android share
  sheet (sends QR image + text together).
- QR scanner: ZXing embedded scanner in portrait orientation.
- Deep link: mhrv:// URIs auto-open the app and import the config.
- Compact encoding: only non-default fields included, DEFLATE compressed
  before base64. Accepts both compressed and raw JSON on import.
- ConfigStore.loadFromJson() deduplicated — shared by file load + import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: deep link requires confirmation, trust warning on import, mhrv-rs:// scheme

Security fix: deep link (mhrv-rs://) no longer auto-imports config.
Stashes decoded config for UI confirmation dialog — same flow as
clipboard paste and QR scan.

Import confirmation dialog now shows:
- Trust warning: "Importing routes your traffic through the deployment
  IDs in this config. Only import from trusted sources."
- Mode and deployment ID count with first 3 IDs previewed
- Explicit Import / Cancel buttons

Also:
- Renamed scheme from mhrv:// to mhrv-rs:// (less collision risk)
- Deduplicated import dialog into shared ImportConfirmDialog composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- mhrv-rs:// deep links, QR scanner, clipboard banner, share sheet
- DEFLATE-compressed base64 encoding (~200 chars vs ~800 raw)
- Every import path requires explicit user confirmation; the dialog
  shows the new deployment IDs and a trust warning so an attacker
  posting a malicious mhrv-rs:// link in a public channel can't
  silently overwrite a user's auth_key + script_ids
- ZXing for QR generation/scanning (no Google Play Services)

Closes therealaleph#266. Thanks @yyoyoian-pixel — the rebase from auto-import
to confirmation-gated import is exactly the right shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ph#271)

Uses tun2proxy_run_with_cli_args (the C API) via dlsym instead of
modifying the JNI run() signature. The upstream tun2proxy maintainer
recommended this path — the CLI API accepts --udpgw-server natively.

- Cargo.toml: enable udpgw feature, remove [patch.crates-io]
- MhrvVpnService.kt: build CLI args with --udpgw-server in full mode
- Native.kt + android_jni.rs: dlsym wrapper for the C API
- Tun2proxy.kt: reverted to upstream signature

No fork, no patch, no submodule.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move from yyoyoian-pixel/tun2proxy fork (with patched JNI signature)
to canonical tun2proxy 0.7.21 from crates.io with feature flag
"udpgw". Cargo.toml [patch.crates-io] section removed entirely.

The Android side now resolves tun2proxy_run_with_cli_args at runtime
via dlsym from libtun2proxy.so, which is the upstream maintainer's
recommended path for callers that need full CLI flexibility.
mhrv-rs builds the CLI string in MhrvVpnService and passes it through
Native.runTun2proxy → src/android_jni.rs → dlsym → tun2proxy.

Future tun2proxy upgrades are now a single Cargo version bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-therealaleph#275, youtube_via_relay=true routed every YouTube-related host
through Apps Script — including ytimg.com (thumbnails) and any
googlevideo.com chunk request the player issued. Two problems:

1. ytimg.com via Apps Script is wasted quota — image CDN, no
   Restricted Mode logic to bypass.
2. googlevideo.com wasn't even in SNI_REWRITE_SUFFIXES, so video
   chunks hit the relay regardless of the flag. A single chunk
   timeout aborted the whole video on Firefox; long videos risked
   the Apps Script 6-min execution cap mid-playback.

Fix: split YouTube into "API/HTML hosts" (where Restricted Mode
lives, gated by the flag) and "asset CDNs" (always direct). The
new YOUTUBE_RELAY_HOSTS list is youtube.com, youtu.be,
youtube-nocookie.com, youtubei.googleapis.com — those go through
relay when the flag is on. ytimg.com, googlevideo.com (added),
ggpht.com all stay on SNI rewrite.

The matches_sni_rewrite logic was also restructured: the carve-out
now runs FIRST before the SNI suffix match, so the broad
googleapis.com entry can't override the narrower
youtubei.googleapis.com decision.

Reported with detailed analysis by @amirabbas117. Will ship in v1.7.4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tunnel-docker job in v1.7.3 release failed with:

  error: failed to unpack package `serde_json v1.0.149`
  Caused by: failed to open `/usr/local/cargo/registry/src/.../serde_json-1.0.149/.cargo-ok`
  Caused by: File exists (os error 17)

Root cause: BuildKit's default cache-mount sharing is "shared" — both
linux/amd64 and linux/arm64 build stages mount the SAME on-disk cache
dir. Cargo's registry source extraction is non-atomic; both arches
race on `tar -xzf serde_json-1.0.149.crate` into the same destination,
and the loser hits EEXIST mid-unpack.

Fix: scope each cache mount with `id=cargo-registry-${TARGETPLATFORM}`
(and matching for cargo-git + target). BuildKit then keeps separate
on-disk caches per architecture — no race. Per-arch warm-build speedup
is preserved (each cache fills with that arch's pre-built deps); the
only loss is one cache miss per arch on the first build after this
change, which we already paid in v1.7.3.

The target/ mount is also platform-scoped since target/ holds compiled
object files for a single ABI; sharing across arches would either miss
or, worse, link wrong-ABI objects together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ative-cache + pre-warm)

therealaleph#275: youtube_via_relay no longer routes video/image CDNs through
Apps Script. The flag now correctly carves out only the API/HTML
hosts where Restricted Mode is enforced; video chunks come direct
from googlevideo.com (which was missing from the SNI rewrite list
entirely — fixed). Long videos no longer hit Apps Script's 6-min
execution cap, and single-chunk timeouts no longer abort playback.

therealaleph#280: TunnelMux now caches "destination unreachable" responses from
the tunnel-node (Network is unreachable / No route to host) for 30
seconds, short-circuiting subsequent CONNECTs to that destination
with 502 (HTTP) or 0x04 (SOCKS5). Saves ~5 batches/second on
IPv6-only host probes. Startup pre-warm pool grew 12→24.

143/143 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
w0l4i has been asking for client-side QUIC block since therealaleph#213. Now
implemented as a small config flag.

When `block_quic = true`, the SOCKS5 UDP relay drops any datagram
destined for port 443 — that's HTTP/3-over-UDP. The client's QUIC
stack retries a couple of times and then falls back to TCP/HTTPS
through the regular CONNECT path (which goes through the relay
normally).

Why client-side rather than server-side udpgw block: the udpgw
block in therealaleph#222 is bound to Full mode + Android tun2proxy. This
covers everyone — apps_script users, desktop, Full mode, all the
same path. Skipping at the SOCKS5 layer rather than the tunnel-node
layer also avoids paying 200–500 ms tunnel-node round-trip per
QUIC datagram drop, which compounds during browser retries.

Silent drop is the contractually correct shape: SOCKS5 UDP wire
has no `host unreachable` reply (RFC 1928 §6 only defines that for
TCP CONNECT). Browsers' QUIC stacks have a "no response → fall
back" timeout, so silent drop matches what the protocol expects.

Default false (opt-in) — udpgw mitigates QUIC partly via persistent
sockets, and a tiny minority of sites only support HTTP/3.

Will ship in v1.7.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resume the practice (dropped after v1.1.0) of committing prebuilt
binaries to the repo's releases/ folder. Iranian users behind state
network filtering frequently can't reach the GitHub Releases page
(/releases/tag/...) but CAN reach the static source tree via
Code → Download ZIP — that pulls the in-repo releases/ folder along
with the source. Telegram channel feedback explicitly requested
this be resumed.

The new commit-releases job:
1. Runs after release+build+android succeed.
2. Wipes existing binary artifacts from releases/ (.apk, .tar.gz,
   .zip) but preserves README.md and .gitattributes.
3. Copies all desktop archives (which already have stable
   platform-suffixed names like mhrv-rs-linux-amd64.tar.gz).
4. Copies all per-ABI Android APKs (so users on slow connections
   can grab the ~37 MB arm64-v8a APK instead of the ~110 MB
   universal).
5. sed-updates the "Current version" line and APK filename refs
   in releases/README.md (both English and Persian copies).
6. Commits as github-actions[bot] and pushes to main.

The GitHub Release page itself keeps the canonical versioned
artifacts as before — this in-repo folder is the fallback for
users who can't reach that URL.

Tag protection rules don't apply to refs/heads/main so the push
isn't gated. release-drafter.yml triggers on push-to-main but only
updates the next-release draft, no cycle risk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… folder

- Adds `block_quic = true` config flag for client-side QUIC drop.
  SOCKS5 UDP relay refuses UDP/443 datagrams; browsers fall back to
  TCP/HTTPS through the relay. Opt-in. Thanks @w0l4i
- Workflow now auto-refreshes the in-repo releases/ folder on each
  release tag, so Iranian users behind GitHub-Releases-page filtering
  can download via Code → Download ZIP. Practice was started before
  v1.1.0 then dropped; resumed at user request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot and others added 27 commits April 28, 2026 19:03
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
…p to 500ms

Apps like Telegram maintain persistent XMPP connections (:5222) and
Google Push uses :5228 — both rely on long-lived sessions with
periodic heartbeats. At the previous 5s long-poll deadline, the
tunnel-node returned empty responses frequently enough that Telegram
interpreted it as connection instability and rotated sessions. Each
reconnect costs a full TLS handshake (~4s through Apps Script),
causing visible video/voice interruptions and buffering.

Raising the long-poll deadline to 15s keeps these persistent
connections alive: the tunnel-node holds the response open until
server data actually arrives (push notification, chat message, media
chunk) rather than returning empty every 5s. Tested on censored
networks in Iran where users reported smoother Telegram video
playback and fewer session resets.

The straggler settle is now adaptive (40ms steps, 500ms max): after
the first session in a batch gets data, keep checking every 40ms
whether neighboring sessions also have data. Break early when all
sessions are ready — no fixed 500ms wait when data is already there.
On high-latency relays where each Apps Script call costs ~1.5s
overhead, packing more session responses into one batch saves quota
and reduces total round-trips.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the fixed 8ms batch coalesce window with an adaptive scheme:
after each new op arrives, wait another 40ms (step) for more ops.
The timer resets on every arrival, up to 1000ms (max) from the first
op. Both values are configurable via config JSON (coalesce_step_ms,
coalesce_max_ms) and two new sliders in the Android Advanced section.

Why this helps: Apps Script adds ~1.5s overhead per HTTP call. The
previous 8ms window barely caught any concurrent ops — most batches
carried just 1 op. With 40ms/1000ms, batches average 2-3 ops,
reducing total Apps Script calls for the same workload.

Tested on device in Iran:
  Before: P75=6.2s, 61% fast (<3s), ~1 op/batch
  After:  P75=3.0s, 74-85% fast, ~2-3 ops/batch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…herealaleph#446)

Tunnel-node stability fix from @yyoyoian-pixel based on field testing in Iran.

- LONGPOLL_DEADLINE 5s → 15s: persistent connections (Telegram XMPP :5222, Google Push :5228) stay alive instead of forcing re-handshakes every 5s
- Replace fixed 30ms straggler settle with adaptive 40ms-step / 500ms-max — packs more session responses into each batch, breaks early when all ready

Local verification: tunnel-node 30/30 tests pass, main crate 160/160 tests pass, both build clean.
…aleph#448)

Adaptive batch coalescing from @yyoyoian-pixel based on field testing in Iran.

Replace fixed 8ms batch coalesce with adaptive 40ms-step / 1000ms-max scheme. Apps Script adds ~1.5s overhead per HTTP call — packing more ops into each batch means fewer total calls. Field testing showed P75 RTT 6.2s → 3.0s, fast (<3s) batches 61% → 74-85%.

Both values configurable via config.json (coalesce_step_ms, coalesce_max_ms) and Android UI Advanced sliders (10-500ms / 100-2000ms).

Note: desktop UI's to_config() needs follow-up to round-trip the new fields. Filing immediately as a separate commit so v1.8.4 can ship both PRs together.
Two PRs from @yyoyoian-pixel field-tested in Iran:

- therealaleph#448: adaptive batch coalescing replaces fixed 8ms window. P75 RTT
  6.2s → 3.0s in Iran network testing. Configurable via
  coalesce_step_ms / coalesce_max_ms.

- therealaleph#446: tunnel-node long-poll 5s → 15s for Telegram/Google Push
  persistent connection stability. Adaptive 40ms-step / 500ms-max
  straggler settle replaces fixed 30ms.

Includes desktop UI build fix: round-trip the new coalesce_* fields
through to_config(). Desktop UI sliders queued for v1.8.x batch.
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
…gents

Mirror of ~/.claude/skills/mhrv-rs-maintainer/ — SKILL.md plus eight reference
files plus assets. Cloud-scheduled agents clone the repo fresh on each fire
and have no access to the maintainer's local home directory; embedding the
skill in docs/maintainer/ lets them read the same canonical context as the
local maintainer and produce replies indistinguishable from a local DOPR
session.

The local copy at ~/.claude/skills/mhrv-rs-maintainer/ remains the source of
truth; this directory mirrors it.
…d DOPR agents"

This reverts commit 9f821cc7580c8fe012346c451c86891b8b185485.
… Script body ceiling

@bankbunk reported (therealaleph#460) that on a 1 Gbps VPS, raw MP4 streams in Full
mode died with `batch JSON parse error: EOF while parsing a string at
line 1 column 52428685` minutes into playback. Root cause: drain_now
took the entire per-session read buffer in one shot. On high-bandwidth
VPS the reader task fills the buffer with tens of MiB between polls;
the resulting batch response (raw + base64 1.33× + JSON envelope)
exceeded Apps Script's ~50 MiB hard cap; Apps Script truncated mid-base64;
the client's serde_json parse hit EOF and the stream tore.

Fix: drain_now now returns at most TCP_DRAIN_MAX_BYTES (16 MiB) per call
and leaves the tail in the buffer for the next poll. EOF is held back
until the buffer is fully drained so partial drains don't tear the
session prematurely. Three regression tests cover the cap, the under-cap
pass-through, and the EOF-holdback case (33 tunnel-node tests passing).

@bankbunk's wondershaper rate-limit workaround (40 Mbps cap on the VPS
interface) is no longer necessary — high-bandwidth VPS users can run at
line rate again.
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
Change default `listen_host` from `127.0.0.1` to `0.0.0.0` so the
proxy is reachable from other devices on the same network. This
enables hotspot sharing: run the app on Android with hotspot enabled,
and an iPhone/iPad/laptop on the same WiFi can use the proxy by
pointing at the Android's hotspot IP (192.168.43.1:8080 for HTTP,
:1081 for SOCKS5).

On iOS, apps like Shadowrocket or Potatso can create a local VPN
that routes all device traffic through the SOCKS5 proxy — giving
full tunnel coverage without installing the app on the iOS device.

Added a "Sharing via hotspot" section to README with setup steps
for iOS, macOS, and Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…alaleph#483)

Closes therealaleph#481. Default `listen_host` from 127.0.0.1 to 0.0.0.0 so Android-phone-as-tunnel + iPhone/laptop on same hotspot can use it as proxy. Old configs with explicit `127.0.0.1` honored (not overwritten). By @yyoyoian-pixel.

Local verification: cargo test --lib 160/160 passing, both release builds clean.
…realaleph#494)

Edge DNS caching at the Apps Script layer using CacheService. By @dazzling-no-more.

Intercepts `udp_open`/port=53 ops in `_doTunnelBatch` and serves them from CacheService (cache hit) or DoH (cache miss). Cache hits drop typical first-hop DNS latency from ~600-1200ms to ~200-400ms.

- DoH fallback chain: Cloudflare → Google → Quad9 over RFC 8484 GET
- Per-qtype cache key keeps A and AAAA from colliding
- Min RR TTL clamped to [30s, 6h]; NXDOMAIN/SERVFAIL get 45s negative cache; NODATA-with-SOA honors SOA TTL per RFC 2308 §5
- Splice helper preserves op-index ordering across mixed TCP+DNS batches
- Default-on, opt-out via `ENABLE_EDGE_DNS_CACHE`; every failure mode falls through to existing tunnel-node forward path (zero regression)
- Privacy-aware: CacheService is volatile + has no on-disk artifact (vs Sheets which would persist a Drive-listed log of every domain users resolve)

11 pure-JS tests covering parsers, txid non-mutation, TTL high-bit clamp, NXDOMAIN-with-SOA TTL extraction, malformed/truncated input rejection, splice correctness for mixed batches. All 160 Rust lib tests still passing.
…ealaleph#488)

Generalizes the Google-edge SNI-rewrite trick to any multi-tenant CDN edge (Vercel, Fastly, …). By @dazzling-no-more, with credit to @patterniha for the original technique (MITM-DomainFronting).

New `fronting_groups: [{name, ip, sni, domains}]` config field — matched hosts get MITM-decrypted at the local CA and re-encrypted upstream against `ip` with `sni` as the TLS SNI. Works alongside the built-in Google fronting and `passthrough_hosts`.

Rename: `mode = "google_only"` → `mode = "direct"`. Old name kept as deprecated alias on parse — no existing config / saved settings break. UI dropdown updated, on-disk file migrates on next Save.

Review fixes folded in: SNI validated via rustls at config-load gate, Vec<Arc<>> refcount instead of clone-on-match, byte-level dot-anchored matcher (no per-match format!()), startup warnings for inert combos.

Working example at config.fronting-groups.example.json. Full doc at docs/fronting-groups.md including precedence rules + the cross-tenant Host-header leak warning.

Test plan: cargo build --release clean, cargo test --lib 169/169 passing (+8 new: dispatch matching, config validation, alias back-compat).

Per author's recommendation, this lands as the v1.9.0 headline — new top-level config field + public mode-string rename are minor-bump territory. xmux moves to v1.10.0.
…p + hotspot sharing

Three substantive PRs landed for this release plus an Iran-safe DoH default:

- therealaleph#488 by @dazzling-no-more (with credit to @patterniha): fronting_groups
  config field generalizes the Google-edge SNI-rewrite trick to any
  multi-tenant CDN edge (Vercel, Fastly, etc.). Renames `mode = "google_only"`
  → `mode = "direct"` with a deprecated alias keeping existing configs working.
  This is the v1.9.0 headline — new top-level config field + public mode-string
  rename are minor-bump territory. xmux moves to v1.10.0.

- therealaleph#494 by @dazzling-no-more: edge-cache DNS at Apps Script (CodeFull.gs)
  using CacheService. udp_open / port=53 ops served from cache or DoH
  fallback chain (Cloudflare → Google → Quad9). Cache hits drop typical
  first-hop DNS latency from 600-1200ms to 200-400ms. Default-on, opt-out
  via ENABLE_EDGE_DNS_CACHE; every failure mode falls through to existing
  tunnel-node forward path (zero regression).

- therealaleph#483 by @yyoyoian-pixel: default listen_host from 127.0.0.1 to 0.0.0.0
  so an Android phone running the tunnel + an iPhone/laptop on the same
  hotspot can use it as proxy. Old configs with explicit 127.0.0.1 are
  honored (not overwritten).

Plus: default `tunnel_doh: true` (flipped from false in v1.8.x) per therealaleph#468
— Iran ISPs filter direct connections to dns.google, chrome.cloudflare-dns.com,
and other pinned DoH hosts. The bypass-on default silently broke DNS for
the dominant Iranian userbase. The safe default keeps DoH inside the
tunnel; non-Iran users can opt back into the bypass for the latency win.
Backwards-compatible — any config with explicit tunnel_doh keeps its setting.

169 mhrv-rs lib tests + 33 tunnel-node tests + 11 edge-DNS JS tests all
passing. Clean release + UI builds.
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
Reddit serves images from redd.it (their CDN-style image host) which is
also on Fastly. Without this entry, the example config matches reddit.com
but image loads still fall back to direct, which is unreliable from Iran
ISPs. Suggested by @Shjpr9 in therealaleph#502.
…igurable batch timeout, MHRV_AUTH_KEY hint, run.bat CLI fallback

Four small fixes that address recurring user-issue patterns:

- src/config.rs / src/domain_fronter.rs: auto_blacklist_strikes,
  auto_blacklist_window_secs, auto_blacklist_cooldown_secs config fields
  (therealaleph#391, therealaleph#444). Previously 3 strikes / 30s window / 120s cooldown were
  hard-coded. Single-deployment users on flaky networks hit this too
  aggressively; multi-deployment users want tighter fail-fast. Defaults
  preserve historical behavior. Power-user file edit only — no UI
  control yet. Clamps to [1, 86400] for durations.

- src/config.rs / src/domain_fronter.rs / src/tunnel_client.rs:
  request_timeout_secs config field (therealaleph#430, masterking32 PR therealaleph#25).
  Replaces hard-coded BATCH_TIMEOUT 30s. DomainFronter::batch_timeout()
  exposes the value, fire_batch reads it. Clamped to [5s, 300s].

- tunnel-node/src/main.rs: detect MHRV_AUTH_KEY env var being set
  while TUNNEL_AUTH_KEY is unset, and emit a specific warning pointing
  at the right env var name. Catches the recurring therealaleph#391/therealaleph#444 docker
  run typo that made users chase phantom AUTH_KEY-mismatch decoys.

- assets/launchers/run.bat: when both UI renderers (glow + wgpu)
  fail on older Windows / RDP / VM-without-GPU, fall back to launching
  mhrv-rs.exe (CLI) instead of just printing "open an issue".
  Addresses therealaleph#417 / therealaleph#426 / therealaleph#487. CLI has the same proxy functionality
  on 127.0.0.1:8085 (HTTP) / :8086 (SOCKS5).

169 mhrv-rs lib tests + 33 tunnel-node tests still passing. UI build
clean. ConfigWire round-trips the new fields with skip-default-on-write
so unchanged configs stay clean.
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
…herealaleph#533)

Adds opt-in alternative backend for mode: "apps_script". Deploy Code.cfw.gs (new GAS variant) + worker.js (Cloudflare Worker), and Apps Script becomes a thin auth+forward layer that pushes the outbound fetch to CF's edge. By @dazzling-no-more.

Closes the audit task on the v1.9.x roadmap (therealaleph#380, therealaleph#393).

Pure docs + GAS/Worker addition; mhrv-rs Rust client unchanged. Same JSON envelope on the wire, same mode/script_id/auth_key — only difference is what the deployed Apps Script does after authentication.

Hardened over upstream denuitt1/mhr-cfw: per-request AUTH_KEY check, fail-closed on placeholder secret, x-relay-hop loop guard + self-host fetch block, SKIP_HEADERS parity with Code.gs, batch handler with Promise.all + soft cap MAX_BATCH_SIZE = 40 paired with WORKER_BATCH_CHUNK on GAS side.

Honest limitations called out in docs:
- Not compatible with mode: "full" (raw-TCP/UDP tunnel ops not ported)
- YouTube long-form gets worse (30s CF Worker wall vs Apps Script 6min — SABR cliff arrives sooner)
- Cloudflare anti-bot unaffected (Worker IP often stricter than Google IP)
- No day-one UrlFetchApp quota relief (batch path unreachable from current single-shape client)

English + Persian docs (assets/cloudflare/README.md + README.fa.md) covering setup, three-matching-AUTH_KEY security model, trade-off table, full-mode incompatibility section.

Local verification: cargo test --lib 169/169 passing, both release builds clean.
Pure docs + GAS/Worker addition shipped via PR therealaleph#533 (therealaleph#380 / therealaleph#393 audit
task). No Rust client changes. Bumping to v1.9.2 so users get the new
deploy assets in the next release tarball + Telegram channel binaries
include the updated docs.
@yyoyoian-pixel yyoyoian-pixel force-pushed the feat/android-youtube-via-relay branch from f0c5700 to d365392 Compare April 30, 2026 14:27
Expose the `youtube_via_relay` config flag in the Android UI, matching
the desktop checkbox. Adds the field to MhrvConfig with serialization
round-trip (toJson / loadFromJson / encode), a Switch in the Advanced
section, and EN + FA string resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@yyoyoian-pixel yyoyoian-pixel force-pushed the feat/android-youtube-via-relay branch from d365392 to 576dfa6 Compare April 30, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants