The val.town founder asked us not to promote using their service. This
commit removes every val.town reference from the codebase and rewrites
the exit-node guides to be platform-agnostic.
Changes:
- Renamed assets/exit_node/valtown.ts → assets/exit_node/exit_node.ts.
TypeScript itself is unchanged — same web-standard Request/Response/
fetch API that runs on any serverless runtime.
- Rewrote assets/exit_node/README.md and README.fa.md to recommend
Deno Deploy as the primary host for users who want a free serverless
TS endpoint, with fly.io and your-own-VPS as alternatives. CF Workers
is explicitly called out as not-helpful (CF outbound is still on
CF's flagged IP space).
- Updated all val.town mentions in source comments (src/config.rs,
src/domain_fronter.rs, src/bin/ui.rs) to neutral wording.
- Updated config.exit-node.example.json `_comment` strings and the
example URL.
- Updated main README.md FAQ entries (Persian + English) and
docs/guide.md / docs/guide.fa.md.
- Old changelog files (v1.9.4 / v1.9.5 / v1.9.9) had val.town mentions
retroactively replaced too — same redaction principle.
- Bumped to v1.9.10 with a changelog noting the rename + Telegram
channel brief format from earlier today.
Users who already have an exit node deployed (on whichever host they
picked) don't need to change anything — the wire protocol is identical
and the renamed script is byte-identical to the old one.
Tests: 179 lib + 35 tunnel-node green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the YouTube thumbnail + caption + separate text-guide line with a single centered, RTL-directed paragraph containing two Persian-numerated items: ۱ for the video (YouTube) and ۲ for Kian Irani's text guide. Cleaner, less vertical space, and the (YouTube) suffix on item ۱ tells the reader where the link points without needing the big thumbnail to imply it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetric with the text-guide caption right below it ("راهنمای جامع متنی…") — تصویری/متنی parallel makes the video-vs-text distinction obvious at a glance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two links on one line: "راهنمای جامع متنی راه اندازی به زبان فارسی" → his guide, "Kian Irani" → his GitHub. Plain "با تشکر از" between them. Cleaner than the previous two-line + sub-text version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a centered link below the video pointing to https://kian-irani.github.io/mhrv-setup-full-tunell/, with credit to @KIAN-IRANi (https://github.com/KIAN-IRANi). Both links open in a new tab. Persian-speaking users now have three escalating depths of guide right at the top of the README: the 5-min Quick Start, the YouTube walk-through, and Kian Irani's full long-form guide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds target="_blank" + rel="noopener noreferrer" so the click doesn't navigate the README away from the repo. The `rel` value is the conventional safety pair: `noopener` blocks the new tab from accessing `window.opener`, `noreferrer` strips the Referer header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
YouTube iframe embeds get sanitized by GitHub's README renderer, so we use the standard "thumbnail-image-linking-to-youtube.com" pattern instead — visually identical (Play overlay + auto-loaded by YouTube's CDN), survives the sanitizer, and one click opens the video on YouTube.
Caption: "راهنمای راه اندازی به فارسی:".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Server-side (Apps Script) fixes — users replace their Code.gs with assets/apps_script/Code.gs (or CodeFull.gs for full mode) and Manage deployments → ✏️ → New version → Deploy:
- Removed duplicate doGet in Code.gs (HtmlService one was overriding ContentService one due to JS hoisting → every GET to /exec returned a goog.script.init iframe instead of the placeholder HTML)
- CodeFull.gs doGet switched from HtmlService to ContentService (same reason)
- SKIP_HEADERS now strips X-Forwarded-* / Forwarded / Via family — second line of defense to v1.2.9's client-side stripping (#104), in case a misconfigured upstream proxy adds these
- _doBatch fallback when UrlFetchApp.fetchAll() throws as a whole — per-item fetch on safe methods so one bad URL no longer poisons the entire batch (port from masterking32@3094288)
Client-side (Rust) defense-in-depth:
- parse_relay_json now unwraps goog.script.init("...userHtml...") if any deployment returns the iframe-wrapped form (legacy Code.gs, or a redirect that GETs doGet). New extract_apps_script_user_html + decode_js_string_escapes helpers. Tested against a real deployment's doGet response.
Docs:
- README rewritten as short bilingual landing page (English + Persian RTL) targeting normal users; advanced reference moved to docs/guide.md + docs/guide.fa.md.
Tests: 3 new regression tests. 176 lib + 33 tunnel-node tests passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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>
Adds end-to-end UDP support: SOCKS5 client UDP ASSOCIATE → tunnel-mux
udp_open/udp_data ops → tunnel-node UDP sessions → real UDP to upstream.
QUIC/HTTP3, DNS, and STUN now traverse full mode without falling back to
TCP or leaking outside the tunnel.
Apps Script proxies the new ops opaquely through the existing batch
endpoint; CodeFull.gs only gets a doc-comment update.
Highlights:
- proxy_server.rs: SOCKS5 UDP ASSOCIATE handler with per-session task,
bounded uplink mpsc channel, adaptive empty-poll backoff (500 ms → 30 s),
source-IP validation against the control TCP peer, port-locking on
first valid datagram, and self-removal from the dispatch map on eof.
- tunnel_client.rs: UdpOpen / UdpData / close_session mux variants
alongside the existing TCP plumbing; pkts decoder helper.
- tunnel-node: UdpSessionInner with bounded VecDeque queue, drop-oldest
on overflow with queue_drops counter and warn-then-throttled logs,
last_active refreshed only on real activity (uplink send or upstream
recv — empty polls do not refresh), independent TCP/UDP drain in
handle_batch Phase 2, separate active-drain (150 ms) and retry
(250 ms) windows for UDP, idle long-poll (5 s).
- Tests: SOCKS5 UDP packet parser (IPv4/IPv6/DOMAIN round-trips,
truncation rejects, fragmented rejects), UDP queue overflow drop +
counter, regression test that batch with both UDP and TCP-data ops
still runs the TCP retry pass.
Docs: README + android.{md,fa.md} updated to reflect UDP availability
in full mode; tunnel-node/README documents the new ops.
Ships PR #173 (event-driven drain) plus three operational improvements:
PR #173 — long-poll tunnel mode. The tunnel-node's batch drain
switched from a fixed 150 ms sleep to an event-driven Notify wait;
idle sessions long-poll up to 5 s and wake on the first byte from
upstream. Push notifications and chat messages now arrive in roughly
RTT instead of waiting for the next client poll tick. Backward compat
with pre-#173 tunnel-nodes is automatic via a sticky AtomicBool that
detects fast empty replies and reverts to the legacy cadence.
92 client tests + 17 tunnel-node tests pass, including end-to-end
TCP-pair verification of the notify wiring.
Docker image for tunnel-node. Adds a hardened Dockerfile (BuildKit
cache mounts, non-root runtime user, ca-certificates for HTTPS
upstreams) and a .dockerignore to keep build context small. New
`tunnel-docker` job in the release workflow builds + pushes
multi-arch (linux/amd64 + linux/arm64) to
ghcr.io/therealaleph/mhrv-tunnel-node with `:latest`, `:1.5`, and
`:1.5.0` tags on every release. Setting up Full Tunnel mode goes
from "rustup + cargo build on a 1 GB VPS" (which fails on memory
half the time) to a one-liner. tunnel-node/README.md updated with
prebuilt-image + docker-compose recipes.
Brief Persian release note in Telegram caption. The release-post
caption now leads with a `<blockquote>`-wrapped FA bullet headlines
extracted from `docs/changelog/v<ver>.md`, above the existing two
links (repo + release). Markdown links → Telegram HTML <a> for
clickability. Cap-budget-aware truncation at bullet boundaries
keeps total caption under Telegram's 1024-char limit. Headlines-only
rather than full bullets so multiple "what's new" items fit
comfortably (the full bullets remain on the GH release page and as
the optional --with-changelog reply-threaded message).
GitHub Releases page bodies now lead with the changelog content
(Persian section + `---` + English) instead of just a Full Changelog
comparison link. The auto comparison link is appended at the bottom
via `append_body: true` rather than removed.
Workflow changes:
- New `permissions: packages: write` at the workflow level (required
for ghcr push via docker/login-action).
- New `tunnel-docker` job needs `build` (not the full matrix) to
serialize the QEMU buildx layer with the matrix cache.
- Release job composes the body from `docs/changelog/v${VER}.md`
in a pre-step that handles both tag-push and workflow_dispatch
paths (uses inputs.version || github.ref_name like the rest of
the workflow).
Tested locally:
- `cargo test` — 92 lib tests pass
- `cargo test -p mhrv-tunnel-node` — 17 tests pass
- `docker build` of tunnel-node Dockerfile — 32 MB image, runs as
non-root, /health returns "ok", auth rejection works correctly,
legitimate requests open sessions to remote hosts
- Telegram script `--dry-run` mode added; rendered captions for
v1.4.0, v1.4.1, v1.5.0 all fit under 900 chars
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@creep247 raised a fair concern: v1.2.9's forwarded-header stripping
handles the client-side leg (browser extensions / local proxies
inserting X-Forwarded-For before the request reaches Apps Script),
but it cannot cover whatever Google's infrastructure may add when
the Apps Script runtime's subsequent UrlFetchApp.fetch() hits the
target server — that leg is outside this client's control.
Added a paragraph to both the English and Persian "Security posture"
sections making the model honest:
- what v1.2.9's stripping DOES cover (client-side added headers)
- what it DOES NOT cover (Google's internal header chain on the
fetch from Apps Script runtime → destination)
- recommendation: users whose threat model requires the destination
site cannot under any circumstances learn their IP should use
Full Tunnel mode, which exits via the user's own VPS end-to-end
No code change — the privacy claim is narrower than a naive reading
of "v1.2.9 fixed the IP leak" might suggest, so the docs should say
so explicitly rather than let users over-trust the apps_script mode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apps Script enforces 30 concurrent executions per account. The old pipeline
used a single global semaphore sized to the number of deployments, meaning
1 in-flight batch per deployment. Now each deployment ID gets its own
semaphore with 30 permits — matching the actual per-account limit.
With N deployments the system can sustain 30×N concurrent batch requests
instead of N.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a Straightforward README that's short, plain-language, and skips
the technical "how it works" diagrams. Covers the same setup flow as
the main README but with friendlier wording, plus a "Common issues"
section that surfaces the most-asked-about gotchas (YouTube SafeSearch
loop #61, Cloudflare Turnstile loop, 504 Relay timeout, daily quota,
"connected but nothing loads"). Both English and Persian.
The main README's index line now offers four links instead of two:
- Quick Start (EN)
- Full English README
- راهنمای خلاصه فارسی
- راهنمای کامل فارسی
So users who don't have a deep networking background can land on a
guide tailored to them, and users who want the full picture still
have a single click.
Closes#135.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pipeline depth was artificially capped at 12. Users with 20+
deployments across multiple accounts were wasting pipeline capacity.
Now: pipeline_depth = num_scripts (minimum 2, no upper cap).
The connection pool (80) is the natural ceiling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds:
- Shield badge in the top badge row linking to https://sh1n.org/donate
- Dedicated "Support this project" section in English (above the
ltr/rtl divider) explaining what donations cover
- Dedicated "حمایت از پروژه" section in Persian mirroring the same
content inside the RTL block
Donations cover hosting, self-hosted CI runner costs, and continued
maintenance. Starring the repo remains the free equivalent.
My earlier commit fe84db0 fixed the English quick-start but missed
that vahidlazio's PR #126 also rewrote the Persian FAQ entry
\"چند Deployment ID لازم دارم؟\" with the same correction. Mirroring
that here so both language tracks say the same thing.
Persian wording credit to @vahidlazio from #126.
Vahidlazio flagged that the README's full-mode quick-start read as
"you need 3-12 separate Google accounts," which is wrong — you need
3-12 deployment IDs, which can all live on one Google account (each
"New deployment" produces its own ID). Going multi-account only
buys daily-quota headroom; the pipeline depth itself scales fine on
one account up to Apps Script's simultaneous-execution ceiling.
Also rewrites the accompanying recipe into three concrete tiers
(solo / small group / large group) instead of waving vaguely at a
range.
Closes#126. Also obsoletes my earlier analysis on #61 where I told
@Feiabyte that same-account multi-deployment "does not give
throughput" — that was wrong; it does, because simultaneous Apps
Script executions aren't bottlenecked by a per-deployment limit
until you pile up more than ~30 concurrent executions per account,
which 12 deployments don't come close to. Posting a correction on
#61 separately.
Adds a new `mode: full` that tunnels ALL traffic end-to-end through Apps Script → a remote tunnel node. Browser does TLS directly with the destination. No MITM, no CA installation needed on the client device.
Ships as part of the 3-PR series: #93 (tunnel-node service + CodeFull.gs, merged) + this (Rust-side Mode::Full + batch tunnel client) + #95 (Android UI dropdown, now rolled into this PR post-rebase).
### Architecture
- Client → mhrv-rs → script.google.com (Apps Script fetch) → tunnel-node on user's VPS → real destination
- Apps Script is the transport to reach the VPS; works even when the ISP blocks direct VPS IPs
- Batch multiplexer collects data from all active sessions and ships one Apps Script request per tick
### Safety properties of this merge
- AppsScript + GoogleOnly dispatch paths are **unchanged**; Full mode is an additive branch at the top of `dispatch_tunnel`.
- `tunnel_client.rs` is a new isolated module (387 LOC).
- `tunnel_request()` is a new method on `DomainFronter`, no change to `relay()` / `relay_parallel_range()`.
- Config: additive `Mode::Full` variant + validation tests (2 new); existing validation rules untouched.
- Local build: clean compile. `cargo test --quiet`: 75 passed (73 → 75 with 2 new config tests).
### Closes
Unblocks the feature requested in #61, #69, #100, #105, #110, #111, #113, #116.
### Testing
vahidlazio has iterated on prior review feedback. End-to-end testing with a real tunnel-node deployment will follow post-merge from @Feiabyte (volunteered in #61). Post-merge CI will exercise compile + full test matrix across all targets; any regression caught there gets a fast-follow fix.
- PR #78: validate Content-Range on 206 responses in the range-parallel
path before stitching. Prevents malformed partials from being combined
into a fake 200 OK. Invalid probe falls back to a normal single GET;
invalid later chunks fall back to the validated probe response
instead of shipping truncated/wrong data.
- PR #79: reject configs with listen_port == socks5_port at validation
time (both config-load and UI form) instead of letting the second
bind fail at runtime with a less clear error.
- README: add an explicit note about the Android 7+ user-CA trust
limitation so future reporters (#74, #81, and the next dozen) find
the answer in the docs instead of in a support thread. The previous
"every app routes through the proxy" line was misleading — TUN
captures all IP traffic but HTTPS still needs app-level trust of
our MITM CA, which most non-browser apps don't grant.
Running through the new self-hosted CI pipeline. Warm rust-cache should
bring the full matrix in under ~7 minutes.
- README: Persian FAQ was claiming ~2 million UrlFetchApp calls/day.
Real free-tier quota is 20,000/day (100,000 on paid Workspace) per
https://developers.google.com/apps-script/guides/services/quotas.
Closes#63.
- DEFAULT_GOOGLE_SNI_POOL (Rust) + DEFAULT_SNI_POOL (Android): add
scholar.google.com. Reported in #47 as another SNI that reliably
passes DPI on MCI / Samantel where plain *.google.com subdomains are
selectively blocked. Same mechanism as accounts.googl.com.
Second operating mode for users whose network already blocks
script.google.com and therefore cannot reach it to deploy Code.gs
in the first place. In google_only, the client runs only the
SNI-rewrite tunnel to *.google.com and the other Google-edge
suffixes that are already allowlisted; non-Google traffic falls
through to direct TCP. No script_id or auth_key is required. Once
Code.gs is deployed, the user switches to apps_script mode and
pastes the Deployment ID.
- config: Mode enum, relaxed validation when mode is google_only
- proxy_server: mode check in dispatch_tunnel; DomainFronter is now
Option<Arc<_>> so it is not constructed in google_only
- desktop UI and Android app: Mode dropdown, Apps Script fields
disable in google_only
- README: bootstrap subsection in English and Persian
- config.google-only.example.json
- version bump to 1.2.0 + changelog entry
Backward compatible with existing apps_script configs.
Mirrors docs/android.md for Persian-speaking users who land on the
Persian half of the README. Same structure — TOC, requirements table,
six-step setup, UI reference, known limitations, troubleshooting
table, log-collection snippet — rewritten in Persian RTL.
README's Persian section gets a new "اجرا روی اندروید" subsection
right before the FAQ, with a five-step quickstart and links to the
full Persian and English Android docs. The English preamble
(above the fold) also gains a فارسی link next to the English
Android-doc reference so bilingual readers see both options
immediately.
No code touched. Cross-references only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Version bump reflects the scope — a unified Rust core that now ships
for desktop (Linux/macOS/Windows) AND Android from the same crate.
Android changes:
- build.gradle.kts: ABI filters expanded to arm64-v8a + armeabi-v7a
+ x86_64 + x86. cargoBuild{Debug,Release} pass all four ABIs to
cargo-ndk in a single invocation. normalizeTun2proxySo() walks every
ABI dir now (was arm64-only).
- Release buildType signs with the debug keystore — no Play Store
target, so signature identity doesn't matter, installability does.
Gradle auto-provisions ~/.android/debug.keystore if absent, so CI
runners inherit this without extra setup.
- versionName 1.0.0, versionCode 100 (room to bump monotonically).
CI:
- release.yml gets a dedicated `android:` job that sets up JDK 17,
Android SDK/NDK 26, all four rust-android targets, installs
cargo-ndk, runs assembleRelease, and uploads a single universal APK
named `mhrv-rs-android-universal-v<version>.apk` into the same
`dist/` collected by the release job downstream.
- `release:` job now gates on `needs: [build, android]` so tagging
v1.0.0 triggers both build matrices before cutting the GitHub
release.
Docs:
- docs/android.md — full 10-step install walk-through: APK sideload,
Apps Script deployment (with "Advanced → Go to (unsafe) → Allow"
reality check), config paste, SNI reachability test, MITM CA
install with OEM-specific nav paths (Pixel / Samsung / Xiaomi),
Start, troubleshooting common failure modes. Also documents the
known limitations — Cloudflare Turnstile loops (inherent to the
Apps Script egress IP pool), UDP/QUIC not tunnelled, IPv6 leaks,
Apps Script daily quota — so users know what to expect before
trying it on a site that won't work.
- releases/README.md — APK row added to the English and Persian
tables, version bumped everywhere to v1.0.0.
- Top-level README — Android listed under Platforms with a link
to docs/android.md.
Release artifact:
- releases/mhrv-rs-android-universal-v1.0.0.apk — 38 MB universal
APK built locally from this tree. Installs + launches on API 24+.
The CI job will regenerate it on tag push; this is the copy
committed for users who can't reach GitHub Releases.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two reasons to pin a copy in the repo:
1. Users on networks where raw.githubusercontent.com is intermittent
can still get the deploy-ready file via a repo ZIP / clone.
2. The Apps Script relay protocol between mhrv-rs and Code.gs is
informal — upstream changes can silently break us. Keeping a
snapshot lets future-us diff against what we tested against
when diagnosing protocol-drift bugs.
Fetched verbatim from:
https://raw.githubusercontent.com/masterking32/MasterHttpRelayVPN/refs/heads/python_testing/apps_script/Code.gs
Credit stays with @masterking32. The assets/apps_script/README.md
next to it calls out that we don't modify this file — users deploy
it as-is into their own Google Apps Script project.
Updated the Setup Guide link in both the English and Persian
sections so offline / restricted-network users have a fallback path.
Two reported issues:
1. Log level in the form had no visible effect — trace produced the
same panel output as warn.
2. upstream_socks5 was reported as never being attempted.
(1) was because the UI binary never installed a tracing subscriber.
Every tracing::info!/debug!/trace! from the proxy was discarded; only
the handful of manual push_log() calls for start/stop/test reached
the 'Recent log' panel. Swapping the log level in the combo-box just
rewrote the config field — nothing consumed it.
Fix: install_ui_tracing() at startup registers a tracing_subscriber
fmt layer with a custom MakeWriter that mirrors each formatted event
line into shared.state.log. Respects RUST_LOG, defaults to 'info'
with hyper pinned to warn so the panel isn't swamped by low-level
HTTP chatter. Now the log level switch actually filters panel
output, and routing decisions show up live.
(2) is a documentation / visibility issue more than a bug. Our
upstream_socks5 routing is intentionally scoped to raw-TCP traffic
(non-HTTP, non-TLS) — HTTPS goes through the Apps Script relay,
which is the whole reason mhrv-rs exists. But without working logs,
it looks like upstream_socks5 is dead code.
Fix: every branch of dispatch_tunnel now emits a tracing::info! that
says exactly which path the connection took and, where applicable,
whether upstream_socks5 was used:
dispatch api.telegram.org:443 -> raw-tcp (127.0.0.1:50529)
dispatch www.google.com:443 -> sni-rewrite tunnel (Google edge direct)
dispatch httpbin.org:443 -> MITM + Apps Script relay (TLS detected)
Combined with (1), users can now see in real time whether their
traffic is hitting upstream_socks5. If it says 'raw-tcp (direct)'
after they set the field, that's evidence of a real bug; if it
never reaches the raw-tcp branch at all, that's the documented
design (HTTPS → Apps Script).
Also per user request, updated README:
- Shields.io badges up top: latest release, total downloads, CI
status, license, stars.
- Short 'Heads up on authorship' note crediting Anthropic's Claude
for the bulk of the Rust port (with the human-on-every-commit
caveat). English and Persian mirrors both have it.
All 37 unit tests pass.
Two user complaints:
- English words mixed inline in the Persian section were breaking the
RTL text flow, making paragraphs hard to read.
- Language was too technical for non-developer users.
Fixes:
1. Every English / technical term is now wrapped in backticks
(`Apps Script`, `MITM`, `SOCKS5`, `Deployment ID`, …). GitHub
renders these as monospace LTR islands, which the browser's
bidirectional text algorithm treats as embedded strong-LTR runs
and doesn't let them flip the surrounding RTL paragraph direction.
2. Rewrote most paragraphs as shorter, plainer Persian sentences.
Replaced jargon (run-time, on-the-fly, rewrite, trust store…)
with everyday wording.
3. Converted dense prose into tables where it helped (download
table by OS, config fields table, per-OS CA install table).
4. Added a 5-step walkthrough (script deploy → download → first
run → config in UI → browser setup) that a non-technical user
can follow top-to-bottom.
5. New 'How do I know it's working?' quick verification section.
6. New big FAQ at the bottom — covers the questions that actually
come up: certificate install safety, how to remove the cert,
how many Deployment IDs to use, YouTube / ChatGPT caveats,
the GLIBC 2.39 issue, and CLI usage for power users.
7. Telegram pairing section reworded — explains the WHY first
(Apps Script can't speak MTProto), then the one-line fix.
8. SNI pool editor flow written as numbered steps mirroring the
actual UI buttons the user clicks.
English section unchanged.
New feature — users can now edit exactly which SNI names are rotated
through the outbound Google-edge tunnel, and probe each one's
reachability. Useful when an ISP selectively blocks individual Google
subdomains (e.g. mail.google.com in Iran at various times).
=== Data model ===
Config gains an optional 'sni_hosts' field:
"sni_hosts": ["www.google.com", "drive.google.com"]
Precedence in domain_fronter::build_sni_pool_for():
1. If sni_hosts is set & non-empty, use that list verbatim.
2. Else, if front_domain is one of the default Google-edge names,
auto-expand to {www, mail, drive, docs, calendar}.google.com.
3. Else, use just [front_domain].
Empty / all-disabled list saves as None so the backend falls back to
the defaults instead of having zero names to rotate through.
=== New scan_sni module ===
probe_one(ip, sni) / probe_all(ip, snis) does, for each candidate:
1. DNS lookup on the SNI (catches typos / non-existent names — Google
GFE returns a valid wildcard cert for ANY *.google.com, so the
TLS handshake alone can't tell apart a real name from gibberish).
2. TCP connect to google_ip:443 (3s timeout).
3. TLS handshake with the candidate SNI (3s timeout). RST mid-
handshake signals DPI block.
4. Small HTTP HEAD over the tunnel to confirm it's still speaking
HTTP (catches weird misroutes).
Returns ProbeResult { latency_ms, error } per candidate.
=== New 'test-sni' CLI subcommand ===
$ mhrv-rs test-sni
Probing 5 SNI candidates against google_ip=216.239.38.120 ...
SNI LATENCY STATUS
www.google.com 142 ms ok
drive.google.com 138 ms ok
mail.google.com - handshake RST (SNI may be blocked)
...
Working: 3 / 5
Exit 0 if >=1 passed, non-zero otherwise. Uses the same probe logic
the UI uses.
=== UI editor ===
New 'SNI pool... (active/total)' button in the main form, styled with
a solid blue fill + white text so it's clearly actionable. Opens a
floating egui::Window (resizable, movable, closable) with:
- Action bar: 'Test all' | 'Keep working only' | 'Enable all' |
'Clear status' | 'Reset to defaults'
- Scrollable list of rows, each: checkbox, monospaced name editor
(230px), status cell (150px, 'ok 142 ms' green / 'fail <reason>'
red / 'testing...' gray / 'untested' gray), per-row 'Test' and
'remove' buttons
- Bottom: text input + '+ Add' that auto-probes the newly added name
immediately (instead of leaving it silently 'untested')
All rendered with ASCII status text instead of unicode check/cross
glyphs, since egui's default font doesn't ship them on some hosts
and they rendered as a missing-glyph box.
Changes only commit when the user hits Save config in the main window;
probe state is held in UiState::sni_probe so it survives opening and
closing the editor.
=== README ===
English + Persian 'SNI pool editor' sections with the two workflows
(UI button + 'sni_hosts' config field), plus a 'test-sni' line added
to the Diagnostics section. Feature list updated.
Tier-1 perf changes from the brainstorm, all on by default except where
they change semantics (parallel_relay is opt-in).
Connection pool pre-warm (domain_fronter.rs):
On startup, open 3 TLS connections to Google edge in parallel and
park them in the pool. First user request skips the ~300-500 ms
handshake cost. Best-effort: warm failures are logged at debug and
ignored. Triggered from ProxyServer::run() in a fire-and-forget
tokio spawn.
SNI rotation (domain_fronter.rs):
Replace the single sni_host String with a Vec<String> plus an atomic
round-robin index. When front_domain is one of the known Google-edge
subdomains, build_sni_pool() expands it to include the other four
(www/mail/drive/docs/calendar.google.com), so outbound TLS connection
counts get spread across names instead of concentrating on one. Custom
front_domain values are preserved as the single entry (we can't verify
siblings of a non-Google edge).
Expanded SNI-rewrite suffix list (proxy_server.rs):
Added gstatic.com, googleusercontent.com, googleapis.com, ggpht.com,
ytimg.com, blogspot.com, blogger.com to the list of domains routed
directly via the Google-edge tunnel instead of through the Apps Script
relay. Bigger bypass = less UA-locking, less quota burn on static CDN
content.
Per-site stats (domain_fronter.rs + ui.rs):
New HostStat struct {requests, cache_hits, bytes, total_latency_ns}
tracked per URL host. Records on both cache hits and relay calls, not
on SNI-rewrite bypasses (those never touch the fronter). UI renders
a collapsible table under the existing stats grid with the top 60
hosts sorted by request count, showing req count, cache hit %, bytes,
avg latency ms.
Parallel script-ID dispatch (config.rs, domain_fronter.rs, ui.rs):
New config field parallel_relay: u8 (default 0 = off). When >= 2 and
there are enough non-blacklisted IDs, do_relay_with_retry fans out
the request to N script instances concurrently via futures_util's
select_ok, returns first success, cancels the rest. Kills long-tail
latency when one Apps Script instance happens to be slow, at the
cost of N× quota per request. UI exposes it as a DragValue 0-8.
TCP_NODELAY audit (proxy_server.rs):
Added the missing set_nodelay(true) call on the SNI-rewrite outbound
TCP stream. All six TcpStream::connect sites in the user traffic path
now disable Nagle.
Expanded feature list in README, added futures-util dep, added unit
tests for extract_host and build_sni_pool.
Verified end-to-end locally:
- Pool pre-warm log line appears on startup: 'pool pre-warmed with 3
connection(s)'.
- Static asset hit 3x: first = 2.2s (Apps Script), 2-3 = 6ms (cache).
- youtube.com / google.com: SNI-rewrite tunnel (unchanged).
- All 28 unit tests pass.
Deferred (not in this release, each needs its own cycle):
- uTLS / Chrome fingerprint mimicry (TLS stack swap)
- QUIC/HTTP3 transport (new transport)
- ETag / If-None-Match revalidation (needs cache schema change)
- JSON envelope gzip on request (needs Code.gs change)
- Firebase Cloud Functions as alt backend (new architecture)
- MSS clamp / TCP Fast Open (platform-specific, marginal)
A user on OpenWRT x86_64 reported the linux release doesn't run there —
root cause was glibc vs musl mismatch (our gnu binary was looking for a
dynamic linker that doesn't exist on router userlands). Add two musl
targets that produce fully static PIE binaries:
- x86_64-unknown-linux-musl -> mhrv-rs-linux-musl-amd64.tar.gz
- aarch64-unknown-linux-musl -> mhrv-rs-linux-musl-arm64.tar.gz
CI uses the messense/rust-musl-cross docker images (better-maintained
than cargo-zigbuild with a pinned zig, which has version regressions
on the ar wrapper between 0.13 and 0.16).
Locally verified:
- both archs cross-compile green in docker
- resulting x86_64 binary (3.3 MB) runs in an alpine:latest container,
--version / --help work, no dynamic lib requirements
The musl archive skips the UI (routers are headless) and swaps run.sh
for a procd init script (assets/openwrt/mhrv-rs.init) expecting the
binary at /usr/bin/mhrv-rs and config at /etc/mhrv-rs/config.json.
Side effect: switched tokio-rustls to default-features=false + ring
(was pulling aws-lc-rs transitively, which can't easily cross-compile
for musl). The main crate already uses ring explicitly, so no runtime
behavior change.
README gets a 'Running on OpenWRT (or any musl distro)' section in
both English and Persian with scp + procd enable/start recipe.
Closes#2.
The Apps Script relay is HTTP-only, and the SNI-rewrite tunnel only
works for Google-hosted domains — so MTProto / IMAP / SSH / anything
else used to drop to a direct-TCP passthrough, which provides zero
circumvention. Users behind a DPI that blocks Telegram saw constant
disconnect/reconnect loops because the raw TCP ran right into the
block.
Fix: add an optional 'upstream_socks5' config field. When set, the
raw-TCP fallback chains the flow into that SOCKS5 proxy (typically a
local xray / v2ray / sing-box with a VLESS / Trojan / Shadowsocks
outbound to your own VPS) instead of connecting directly. The whole
rest of the pipeline is unchanged:
- HTTP / HTTPS still MITMs and relays via Apps Script
- SNI-rewrite suffixes (google.com, youtube.com, …) still hit the
direct Google-edge tunnel (so YouTube stays fast)
- Only the raw-TCP bucket (Telegram MTProto, SSH, IMAP, …) gets the
new upstream chain
Changes:
- config.rs: add Option<String> upstream_socks5 field
- proxy_server.rs: thread it through RewriteCtx; rewrite
plain_tcp_passthrough to call a new socks5_connect_via() helper
when configured, with graceful fallback to direct
- ui.rs: new 'Upstream SOCKS5' input with tooltip + placeholder,
ConfigWire round-trip
- README.md: new 'Pair with xray for Telegram' section (EN + FA)
with the architecture diagram and example config
Verified end-to-end in Docker: xray with the user's working VLESS
Reality config, mhrv-rs with upstream_socks5 pointing at it.
- HTTPS via mhrv-rs SOCKS5: origin = Google IP (Apps Script path) ✓
- Raw TCP to 3 Telegram DCs + api.telegram.org: all SOCKS5 rep=0, log
shows 'tcp via upstream-socks5 127.0.0.1:50529 -> …' ✓
- youtube.com / google.com: 'SNI-rewrite tunnel' (unchanged) ✓
- Real Telegram Desktop stayed connected cleanly (user-confirmed).
- docs/ui-screenshot.png: running UI with live traffic stats
- releases/README.md: documents the in-repo prebuilt binaries for users
who cannot reach the GitHub Releases page (English + Persian)
- README: embed the screenshot in the 'What's in a release' section
Users reasonably get nervous when an installer adds a root CA. Spell
out what the install actually does and does not do, in both the English
and Persian sections:
- CA keypair is generated locally in the user-data dir
- Only the public cert is added to the trust store
- Private key never leaves the machine; no network side is involved
- Clear revocation steps
- Manual CLI fallback if the launcher isn't wanted
- Firefox NSS note in case certutil best-effort misses
- Top-of-page anchor is راهنمای فارسی (not URL-encoded)
- Document the launcher scripts as the recommended first run
- Update CA path (user-data dir, not ./ca)
- Mirror the English and Persian sections feature-for-feature
- Add a short security posture section
- Wrap the Persian block in <div dir=rtl> for correct rendering
First run needs the CLI to install the MITM CA into the system trust
store (sudo/admin prompt), which the UI alone can't do reliably from a
double-click. Add a small launcher for each platform that runs the CLI
with --install-cert once, then starts the UI. Each release archive now
contains a run.* script alongside the binaries.
New bin 'mhrv-rs-ui' behind the 'ui' feature flag. CLI users pay
zero egui compile cost; UI users get a single static binary.
UI features:
- Config form (Apps Script ID, auth key, Google IP, front domain,
ports, log level, verify_ssl)
- Start/Stop buttons that spawn the proxy on a dedicated tokio thread
- Live stats (relay calls, failures, cache hit rate, bytes relayed,
blacklisted scripts) polled every ~700ms
- Test button (end-to-end relay probe)
- Install CA / Check CA buttons
- Recent log panel (last 200 lines)
- Dense, dark, utility-look: no emojis, no cards, no gradients
Architecture:
- Refactored crate into lib + two bins (mhrv-rs, mhrv-rs-ui).
src/lib.rs exposes all modules, main.rs uses them via 'use mhrv_rs::...'
- New src/data_dir.rs: platform-appropriate user data dir
(~/Library/Application Support/mhrv-rs on macOS,
~/.config/mhrv-rs on Linux, %APPDATA%\mhrv-rs on Windows).
CLI falls back to ./config.json for backward compat.
- CA moves to {data_dir}/ca/ca.crt (was ./ca/ca.crt).
- UI background thread owns the tokio runtime and proxy handle;
communicates with UI via std::mpsc commands + Arc<Mutex<UiState>>.
- macOS .app bundle: assets/macos/Info.plist template + build-app.sh
that assembles .app from the binary. Bundled into release zips.
- CI: Linux system libs (libxkbcommon, libwayland, libxcb*, libx11,
libgl, libgtk-3) installed on Ubuntu runners for eframe. aarch64
Linux UI is best-effort cross-compile. Windows MinGW, macOS native.
25 lib tests still pass. 5MB release UI binary on macOS.
Ports the SOCKS5 + fallback-chain design from @masterking32's
MasterHTTP-WithSOCKS branch so xray / Telegram / app-level TCP
clients work through this proxy.
Changes:
- New SOCKS5 listener on listen_port+1 (configurable via socks5_port)
- RFC 1928 CONNECT handshake (v5, no-auth, ATYP IPv4/domain/IPv6)
- Shared smart dispatch with the HTTP-CONNECT path
- Unified dispatch_tunnel() used by both CONNECT entry points:
1. If host matches SNI-rewrite suffix or hosts override: go direct
to google_ip via the MITM+TLS tunnel (fast path for google.com,
youtube, etc.)
2. Peek the first byte (300ms timeout for server-first protocols):
- 0x16: TLS client hello -> MITM + relay via Apps Script (scheme=https)
- HTTP method signature: HTTP relay via Apps Script (scheme=http)
- Anything else or timeout: plain TCP passthrough to the target
- handle_mitm_request() now takes a scheme arg (http/https) so the
same code path handles both MITM'd HTTPS and port-80 plain HTTP
- New plain_tcp_passthrough helper: bidirectional TCP bridge used as
the final fallback (covers MTProto / raw TCP / server-first protos)
Config:
- Added optional socks5_port field; defaults to listen_port+1
README:
- Added browser vs xray/Telegram instructions under 'Step 6'
Live-tested: HTTP proxy, HTTP proxy -> HTTPS, SOCKS5 -> HTTP,
SOCKS5 -> HTTPS, Google search via SNI-tunnel (now returns full
JS page) all pass.
Context: user reported Google search showing no-JS fallback page
('JS is off apparently'). Root cause is Apps Script's fixed
'Google-Apps-Script; beanserver' User-Agent that UrlFetchApp.fetch
does not let you override. Google detects the bot UA and serves
the degraded HTML.
Fix: add google.com to SNI_REWRITE_SUFFIXES so google.com requests
bypass Apps Script entirely and go direct to Google's edge via the
MITM+TLS tunnel. Real browser UA is sent; full JS version is served.
Also documented this and other inherent limitations (WebSockets,
2FA 'unknown device', video chunk slowness, brotli stripping) in
the README under 'Known limitations' in English + Persian so users
aren't surprised. These are platform limits of Apps Script, not
bugs -- same issues exist in the original Python project.