Commit Graph

359 Commits

Author SHA1 Message Date
therealaleph 5bb26a4961 v1.3.0: per-deployment concurrency, ABI-split APKs, ONLY-mode fix
Rolls up the four post-v1.2.14 commits on main into a single tagged
release. Highlights:

- Per-deployment concurrency (#142): each deployment ID gets its own
  30-permit semaphore, so setups with deployments across multiple
  Google accounts get a genuine 30×N throughput ceiling. Single-account
  setups still cap at Google's per-account 30-simultaneous limit —
  docs (EN + FA) updated to call that out.
- Android app-splitting ONLY-mode bug fix (#143): the previous code
  called both addAllowedApplication and addDisallowedApplication,
  which Android documents as mutually exclusive. ONLY mode was
  silently failing establish(). Now fixed.
- Per-ABI Android APKs (#136): ships four split APKs (arm64-v8a ~21 MB,
  armeabi-v7a ~18 MB, x86_64 ~23 MB, x86 ~22 MB) alongside the ~53 MB
  universal. Huge distribution win for users on unreliable
  censorship-tunnel paths — the 21 MB arm64-v8a download succeeds
  where the universal doesn't.
- Honest IP-exposure note in Security Posture (#148): clarified that
  v1.2.9's forwarded-header stripping only covers the client-side leg;
  what Google's own infrastructure may add on the UrlFetchApp.fetch()
  second leg is outside this client's control. Full Tunnel mode is
  the recommendation for threat models where that matters.
- Telegram release-post format: added Persian preambles above both
  links (GitHub repo + full Persian guide; release page + desktop/
  router builds) so channel readers see the intent at a glance.

82 tests pass. Desktop + Android builds both verified clean locally
across the v1.2.15+ commit series.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.3.0
2026-04-24 23:11:52 +03:00
freeinternet865 a148a7a1c6 fix(android): keep empty ONLY split lists self-excluded
When ONLY mode contains only this app or stale packages, no allowed application is successfully added. Android then treats the VPN as applying to every app, which can route more traffic than requested and can also include our own proxy traffic in the TUN path. Count successful addAllowedApplication calls and fall back to the existing ALL-mode self-exclusion behavior when none are usable.
2026-04-24 20:09:28 +00:00
therealaleph d73639a42c docs: acknowledge IP-exposure caveat for apps_script mode (fix #148)
@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>
2026-04-24 22:59:04 +03:00
Shin (Former Aleph) 6d9bc2be6d Merge pull request #143 from dazzling-no-more/fix/split_tunneling_android
fix(android): app splitting ONLY mode don't mix allowed/disallowed apps
2026-04-24 22:57:48 +03:00
therealaleph cc04558437 android: publish per-ABI APKs in addition to universal (fix #136)
GitHub Releases is filtered from inside IR, and the 50 MB universal APK
is a bottleneck for users on slow/unstable censorship-tunnel paths that
can't reliably pull that much data. Per-ABI APKs are ~18–23 MB each —
small enough to succeed where the universal fails.

Build changes:
- android/app/build.gradle.kts: enabled `splits { abi { ... } }` with
  `isUniversalApk = true`, producing five release APKs:
    app-universal-release.apk     ~53 MB  (all 4 ABIs)
    app-arm64-v8a-release.apk     ~21 MB  (95%+ of modern devices)
    app-armeabi-v7a-release.apk   ~18 MB  (older 32-bit ARM)
    app-x86_64-release.apk        ~23 MB  (emulators, Chromebooks)
    app-x86-release.apk           ~22 MB  (legacy 32-bit Intel)
  abiFilters is retained for the universal build; splits.abi layers on
  per-ABI outputs without removing it.

- .github/workflows/release.yml: rename step now copies all 5 APKs to
  dist/ under versioned names (mhrv-rs-android-{abi}-v{VER}.apk),
  logs a warning if any per-ABI APK is missing, and hard-fails only if
  the universal is missing. Universal keeps its existing download path
  and filename so Telegram mirrors / previous-version update prompts
  keep working.

The release + telegram aggregation jobs downstream don't need changes —
they already use `files: dist/*` and `mhrv-rs-android-universal` artifact
name respectively.

Local build verified: clean assembleRelease produces all 5 APKs with the
expected size ratios (arm64-v8a is 41% the universal size).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:27:00 +03:00
dazzling-no-more 4ccd864e72 fix(android): app splitting ONLY mode — don't mix allowed/disallowed apps 2026-04-24 23:16:26 +04:00
Shin (Former Aleph) 7c102b4f63 Merge pull request #142 from vahidlazio/feat/per-deployment-concurrency
feat: per-deployment concurrency (30 req/account)
2026-04-24 22:14:40 +03:00
vahidlazio 2715991afa feat: per-deployment concurrency (30 req/account) instead of global pipeline depth
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>
2026-04-24 21:10:06 +02:00
Shin (Former Aleph) 81d1883f45 Update README.md 2026-04-24 21:20:34 +03:00
therealaleph 8466a2622b docs: add SF_README.md (short, beginner-friendly guide) — fix #135
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>
2026-04-24 20:50:30 +03:00
Shin (Former Aleph) 40c9e2f9ba Merge pull request #131 from vahidlazio/feat/uncap-pipeline-depth
feat: remove pipeline depth cap — scales with deployment count
2026-04-24 20:30:16 +03:00
Shin (Former Aleph) c5f3fb8cf9 Merge pull request #117 from acdasdcasdcasdcasdc/feature/youtube-sni-relay
Feature/youtube sni relay
2026-04-24 20:29:17 +03:00
therealaleph d01e9f0f2f v1.2.14: Usage today (estimated) card + Persian guide localization
Adds a daily-budget visualization for users worried about hitting the
Apps Script free-tier quota (20,000 UrlFetchApp calls/day).

Usage today card (desktop + Android):
- today_calls / today_bytes / today_key / today_reset_secs atomics on
  DomainFronter, hooked into the bytes_relayed fetch_add path so we only
  count successful relays (matching what Google actually billed)
- Daily rollover at 00:00 UTC, std-only date math (Hinnant's
  civil_from_days) — no chrono/time dep pull
- StatsSnapshot extended with the four new fields + to_json() for the
  Android JNI bridge
- Desktop UI renders the card right under the existing Traffic stats
  with a hyperlink to https://script.google.com/home/usage for the
  authoritative Google-side number
- Android UI renders the same card via Compose, polling
  Native.statsJson(handle) once a second only while the proxy is up,
  with an Intent(ACTION_VIEW, …) opening the dashboard URL

JNI / state plumbing:
- New Java_…_statsJson reads the Arc<DomainFronter> kept in slot_map
- VpnState.proxyHandle StateFlow so HomeScreen knows which handle to
  poll without poking into the service's internal state
- MhrvVpnService publishes the handle on start, zeroes on teardown

Persian localization:
- HowToUseBody (5-step guide + Cloudflare Turnstile note) was
  hardcoded English even when locale=FA. Ported to a string resource
  with a full FA translation in values-fa/strings.xml. Persian users
  no longer drop to English at the bottom of the screen.

Also lands the deferred Android ConfigStore.kt wiring for
passthrough_hosts (commit fe9328e shipped the Rust + desktop side).

82 tests pass (added: unix_to_ymd_utc_handles_known_epochs,
seconds_until_utc_midnight_is_bounded). Built and visually verified on
both desktop and Android emulator (mhrv_test AVD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.2.14
2026-04-24 20:15:52 +03:00
vahidlazio d091c48da6 feat: remove pipeline depth cap — scales with deployment count
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>
2026-04-24 19:00:21 +02:00
therealaleph c416cd3639 domain_fronter: surface quota-exhaustion hint in timeout error (#99)
The 504 "Relay timeout" message was too terse — users hit it after
their daily UrlFetchApp quota exhausts and had no idea why a setup
that worked yesterday suddenly started failing.

New message explicitly names the most common cause (daily quota
reset at 00:00 UTC) and points at the script's Executions tab for
the real server-side error. Same three-line explanation #99, #111,
#105 each independently got from me in issue comments — now it's in
the browser page body where the user actually sees it.
2026-04-24 18:16:56 +03:00
therealaleph fe9328e77c feat: user-configurable passthrough_hosts (fix #39, #127)
New config field `passthrough_hosts: Vec<String>` that lists hostnames
which should bypass Apps Script relay entirely and pass through as
plain TCP (via upstream_socks5 if set). Applies across all modes —
apps_script, google_only, and full — because it expresses user intent
("never relay this host") that should win over the default routing.

Matching rules:
- Exact: "example.com" matches only example.com
- Suffix: ".example.com" matches example.com AND any subdomain
- Case-insensitive; trailing dots normalized
- Empty / whitespace-only entries are ignored

Dispatch order is now:
  0. passthrough_hosts  ← new, highest priority
  1. Mode::Full         → batch tunnel
  2. SNI-rewrite        → direct Google edge
  3. Mode::GoogleOnly   → plain-tcp
  4. Mode::AppsScript   → peek + MITM/relay/plain-tcp

Wired through:
- src/config.rs: new Config field with serde default
- src/proxy_server.rs: RewriteCtx.passthrough_hosts + matches_passthrough()
  helper + dispatch check as step 0 + 5 new unit tests
- src/bin/ui.rs: FormState + ConfigWire round-trip so the desktop UI
  preserves user entries across save/load

Android ConfigStore.kt wiring and a UI editor will land in a follow-up.

80 tests pass (75 → 80 with the new passthrough match tests).
2026-04-24 18:14:45 +03:00
therealaleph 1db6845c15 readme: add Support button + sections (EN + FA)
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.
2026-04-24 18:08:16 +03:00
therealaleph 8dea9d38a4 docs: mirror the Persian FAQ clarification for deployment IDs (#126 follow-up)
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.
2026-04-24 17:35:50 +03:00
therealaleph fe84db0fe0 docs: clarify full-tunnel deployment IDs — same account is fine (#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.
2026-04-24 17:34:37 +03:00
therealaleph a05fc5d612 v1.2.13: Android Full Tunnel Mode requires credentials (fix #124)
Bug fix release. My v1.2.12 merge of Mode::Full bypassed the
deployment-ID + auth-key check on Android, but Full mode talks to
CodeFull.gs on Apps Script and needs those same credentials.

Users selecting "Full tunnel (no cert)" with empty fields would see
the VPN service bail silently instead of surfacing a clear "config
incomplete" error. Vahidlazio's fix changes the gate from
`mode == APPS_SCRIPT` to `mode != GOOGLE_ONLY` and removes the
Mode.FULL bypass in the Start button's enabled-state.

Also includes a UX refactor of the Deployment IDs editor (per-row
rows with add/remove buttons instead of raw newline-separated text),
making multi-deployment setups easier to manage on Android — useful
now that Full Tunnel Mode users routinely scale to 5+ deployments
per their Google accounts.

Android-only diff; Rust side is byte-identical to v1.2.12.
v1.2.13
2026-04-24 17:16:09 +03:00
vahidlazio f9f4845567 fix: Android full tunnel mode requires credentials + deployment IDs UI refactor (#124)
Real bug I introduced in #94: Full mode was skipping the credential check that apps_script mode enforces, but Full mode does talk to CodeFull.gs on Apps Script and needs the same auth_key + deployment ID. Users flipping to Full mode with empty fields would silently fail.

Two sites fixed:
- MhrvVpnService.kt — changed `mode == APPS_SCRIPT` gate to `mode != GOOGLE_ONLY`
- HomeScreen.kt — removed the `cfg.mode == Mode.FULL` bypass in the Start button's enabled-state

Also includes a UX improvement for the Deployment IDs editor (per-row field with add/remove buttons instead of raw newline-separated text), which makes multi-deployment setups easier to manage on Android.

Rust-side 75 tests still green, Kotlin compiles clean. Android-only diff so no Rust CI impact.
2026-04-24 17:15:12 +03:00
root 7cac31fc1a fix(proxy): remove redundant SNI list split
Keep PR scoped to UI checkbox for existing youtube_via_relay field.
Drop behavior-neutral proxy refactor and config whitespace-only diff.

Made-with: Cursor
2026-04-24 12:23:32 +00:00
root 18fd293a2a Merge upstream main into feature/youtube-sni-relay
Made-with: Cursor
2026-04-24 10:06:40 +00:00
root 746bc978b4 fix sni suffix categorization and remove local toolchain helpers
Keep ads/analytics domains in the core SNI rewrite list so the YouTube skip toggle only affects YouTube paths, and drop machine-specific .cargo cross-compile helpers that break CI portability.

Made-with: Cursor
2026-04-24 10:02:51 +00:00
therealaleph 9e2b8e5f3e v1.2.12: Full Tunnel Mode (#94 merged)
Rollup of PR #94 — Mode::Full dispatch + batch tunnel client. Ships
the long-awaited no-MITM path that was the motivating fix for half
the open issues this week.

User-facing: add `"mode": "full"` to config.json, deploy CodeFull.gs
as a second Apps Script alongside your existing one, deploy
tunnel-node (tunnel-node/README.md) on a VPS, and traffic is tunneled
end-to-end: client → mhrv-rs → script.google.com → your tunnel node →
destination. Browser speaks TLS directly with the destination; we
never see plaintext. No CA needed on the client device.

Android side gets a "Full tunnel (no cert)" dropdown option; toggling
it writes `"mode": "full"` to config.json.

Safety: Mode::AppsScript and Mode::GoogleOnly dispatch paths are
unchanged — Full mode is an additive branch at the top of
dispatch_tunnel. Existing users on the default apps_script mode see
zero behaviour change.

Testing status: compiles clean on all 10 CI targets; 75 tests pass
(+2 new config-validation tests for Full mode); end-to-end real-VPS
testing will come post-release from @Feiabyte and others who opt in.
Any Full-mode regression gets a fast-follow fix.
v1.2.12
2026-04-24 12:51:10 +03:00
vahidlazio b73bbe2106 feat: Mode::Full + batch tunnel client (#94)
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.
2026-04-24 12:48:56 +03:00
therealaleph 259431b44f v1.2.11: x.com URL truncation now fires for www.x.com (fix #64)
Single-bug release. Unblocks x.com browsing for users whose browsers
resolve to www.x.com rather than bare x.com — i.e. essentially
everyone using Firefox / Chrome / Safari.

Previous releases still advertised the URL-truncation fix as working
but it only matched exact Host: x.com, which never happens in real
traffic. v1.2.11 widens the matcher to x.com + *.x.com so www.x.com,
api.x.com, and any future x.com subdomain all get the shortened URL
through Apps Script's URL length cap.
v1.2.11
2026-04-24 10:01:46 +03:00
therealaleph cb4cde1702 proxy: match www.x.com + subdomains for GraphQL URL truncation (#64)
The x.com GraphQL URL-length fix added in v1.2.1 (08fe691) only
matched exact host "x.com". But browsers actually navigate to
www.x.com, and api.x.com serves GraphQL endpoints too — the original
fix never fired for real traffic.

@pourya-p's log in #64 made this unambiguous:

  relay GET https://www.x.com/i/api/graphql/<hash>/HomeTimeline?variables=...&features=...
  ...
  ERROR Relay failed: relay error: Exception: بیش از حد مجاز: طول نشانی وب URLFetch.

(That Persian text is Apps Script's "URLFetch URL length exceeded"
error, which is exactly what the truncation was supposed to prevent.)

Widened the host matcher to `host == "x.com" || host ends with
".x.com"` so www.x.com / api.x.com / any future x.com subdomain all
hit the rewrite. The path-pattern constraint
(`/i/api/graphql/... ?variables=`) already filters to the right
endpoints.

73 tests still pass.
2026-04-24 10:00:53 +03:00
therealaleph 29777ce5b2 v1.2.10: proxy Stop actually stops now (fix issue #99)
Single-focus release. The Stop button in the UI previously only
stopped new connections from being accepted — in-flight clients kept
running on the old DomainFronter, which meant:

- Pages kept loading after Stop (users thought they'd stopped)
- Auth-key changes didn't take effect for domains with a live
  keep-alive to the proxy
- Apps Script quota could still be consumed post-Stop

Fix (7338e76): wrap per-client spawns in a tokio::task::JoinSet
inside each accept loop. On shutdown, aborting the accept task drops
the JoinSet, which aborts every in-flight client. Sockets close,
the old fronter's TLS pool drops, and a subsequent Start builds a
clean new state.

Finding 1 of #99 (quota-exceeded → "timeout" instead of the real
502 body) is a separate pool-staleness issue and is NOT addressed
in this release.
v1.2.10
2026-04-24 04:33:29 +03:00
therealaleph 7338e765d6 proxy: track per-client tasks in JoinSet so shutdown actually stops them (#99)
Before: `ProxyServer::run()` aborted only the two accept tasks on
shutdown (`http_task`, `socks_task`), but every per-client task was
spawned as a bare `tokio::spawn(...)` whose JoinHandle was discarded.
Aborting the accept loop stopped taking new connections, but in-flight
clients kept running on the runtime with their captured (stale)
`Arc<DomainFronter>`.

User-visible symptoms reported by @r-safavi in #99:

1. Hitting Stop in the UI didn't actually stop serving: Firefox still
   reached x.com through the proxy even though the user expected a
   "connection refused."
2. Starting again with a changed auth_key worked for NEW domains
   (yahoo.com) but not for domains with a live keep-alive (x.com) —
   because the old child task was still using the old fronter with the
   old key.
3. Apps Script quota could be consumed after the user thought they'd
   stopped. Arguably the worst of the three.

Fix: wrap per-client spawns in a `tokio::task::JoinSet<()>` scoped
inside each accept task. When the accept task is aborted on shutdown,
the JoinSet is dropped, and `JoinSet::drop` aborts every still-running
child — closing their sockets and dropping their Arc clones of the
fronter, which in turn drops the pool.

Also added an opportunistic `try_join_next()` drain before each
accept() so the JoinSet doesn't grow unbounded with completed-task
handles on long-running proxies.

Covers Finding 2 of #99. Finding 1 (quota-exceeded → timeout instead
of surfacing Apps Script's 502) is a separate pool-staleness issue and
stays open for now.
2026-04-24 04:32:39 +03:00
therealaleph 1d2eb19295 v1.2.9: fix UI build for youtube_via_relay (v1.2.8 CI abandoned)
v1.2.8 tagged cleanly but CI failed compiling mhrv-rs-ui with:

  error[E0063]: missing field `youtube_via_relay` in initializer of
  `mhrv_rs::config::Config`

When I added the youtube_via_relay field to the main Config struct
in 09f1f5f, I missed the struct-literal construction in src/bin/ui.rs
(FormState::save_to_config) and the ConfigWire serializer.

Fixed here:

- Added youtube_via_relay field to FormState (line 214), read path
  (line 291), default path (line 316), and the save path (line 451)
- Added youtube_via_relay field to ConfigWire (line 493) with
  skip_serializing_if on false, plus its From impl (line 544)

UI still doesn't expose a checkbox for the toggle — it's config-only
for now, same treatment as normalize_x_graphql. A future PR can add
the checkbox to the Advanced pane.

v1.2.8 tag exists but has no GitHub Release (release job skipped
on failure); v1.2.9 is the clean cut. Same payload as v1.2.8 plus
this fix.
v1.2.9
2026-04-24 02:16:32 +03:00
therealaleph 6fdbfe3966 v1.2.8: real-IP-leak fix + youtube_via_relay + scan_sni hardening
Rollup of four merged fixes since v1.2.7:

- security: strip identity-revealing forwarding headers in the Apps
  Script relay path. Closes the XFF leak vector from issue #104 —
  users chained behind xray/v2rayNG or running browser extensions
  that inject X-Forwarded-For / Forwarded / Via / CF-Connecting-IP
  etc. would previously have those forwarded to the origin via the
  relay. Now stripped to 16 header variants with a regression test.

- proxy: new `youtube_via_relay` config toggle (#102). Routes
  YouTube family suffixes through Apps Script instead of the
  SNI-rewrite tunnel. Trades SafeSearch-on-SNI for Apps Script's
  fixed User-Agent + quota cost. Off by default.

- scan_sni: decode chunked dns.google DoH responses (#97, from
  @freeinternet865). Without this, PTR lookups always failed and
  scan-sni discovered zero domains.

- scan_sni: verify dns.google TLS with webpki roots (#98, from
  @freeinternet865). The DoH request is a normal public HTTPS call
  — an on-path MITM should not be able to forge PTR answers and
  poison the suggested SNI pool.

73 tests pass (up from 67 — three new chunked-decode tests + one
XFF-filter + two youtube_via_relay branches).
v1.2.8
2026-04-24 01:58:25 +03:00
therealaleph 09f1f5fecd proxy: add youtube_via_relay config toggle (#102)
Ports the upstream Python `youtube_via_relay` flag (commit a0fd8a0 in
masterking32/MasterHttpRelayVPN). When enabled, YouTube-family
suffixes (youtube.com, youtu.be, youtube-nocookie.com, ytimg.com)
opt out of the SNI-rewrite tunnel and fall through to the Apps Script
relay path.

Why it helps some users: when YouTube is reached via SNI-rewrite to
google_ip with SNI=www.google.com, Google's frontend can enforce
SafeSearch / Restricted Mode based on the SNI name, causing "video
restricted" errors on some regular videos. Routing through Apps
Script bypasses that specific filter at the cost of (a) UrlFetchApp's
fixed `User-Agent: Google-Apps-Script`, and (b) counting YouTube
traffic against the script's daily quota.

Off by default so existing behaviour is unchanged. Users who hit the
SafeSearch-on-SNI issue can set `"youtube_via_relay": true` in their
config.json and observe.

Explicit `hosts` overrides always beat the toggle — that's a user
choice and should win over the default policy. Added tests for all
three branches (youtube_via_relay off, on, and with hosts override).

Matching Android-side UI toggle deferred — `normalize_x_graphql` is
also config-only on Android today; users can edit config.json
directly if needed.
2026-04-24 01:07:08 +03:00
therealaleph 26d2d36223 security: strip identity-revealing forwarding headers in relay path (#104)
filter_forwarded_headers was stripping hop-by-hop headers (Host,
Connection, Content-Length, etc.) but not identity-revealing
forwarding headers. If a user sat behind another proxy or ran a
browser extension that inserts any of:

  X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto,
  X-Forwarded-Port, X-Forwarded-Server, X-Forwarded-Ssl,
  Forwarded, Via, X-Real-IP, X-Client-IP, X-Originating-IP,
  True-Client-IP, CF-Connecting-IP, Fastly-Client-IP,
  X-Cluster-Client-IP, Client-IP

those would carry the client's real IP all the way through the Apps
Script relay to the origin server. Stripping them so the origin only
ever sees whatever source IP the Apps Script / GFE path terminates on.

This covers the Apps Script relay path (the main leak vector). The
SNI-rewrite tunnel path is a raw TLS byte bridge — it doesn't parse
HTTP at all — so any headers the client emits there pass through as
opaque bytes to the Google edge that terminates TLS. In practice
that's narrower (origin sees GFE) but documenting the caveat on the
issue thread.

Adds a focused regression test that locks in every stripped header.

Reported in #104.
2026-04-24 01:01:15 +03:00
freeinternet865 ccfa62d0d5 scan_sni: verify dns.google TLS (#98)
The scan-sni DoH client to dns.google was using NoVerify — an on-path MITM could forge PTR answers and poison the discovered SNI pool. This is a public HTTPS request, not a fronted probe, so certificate validation belongs ON. Switched to the normal webpki root store.
2026-04-24 00:58:02 +03:00
freeinternet865 9116839507 scan_sni: decode chunked dns.google responses (#97)
dns.google replies with Transfer-Encoding: chunked; the raw payload was being handed to serde_json with chunk framing still embedded, so every PTR parse failed and scan-sni discovered nothing. Parses the HTTP response (chunked + Content-Length) before JSON decode. Includes 3 new unit tests.
2026-04-24 00:57:24 +03:00
therealaleph 0a29cf0740 v1.2.7: SNI cert fix mirrored to Android + tunnel-node scaffold (via #92 + #93)
- Android DEFAULT_SNI_POOL: mirror the Rust-side fix from #92 —
  accounts.googl.com replaced by accounts.google.com. Same cert-SAN
  mismatch that was failing every Nth rotation in the Rust client
  affected the Android user's sniHosts population; both pools need
  to stay in sync by design.

- Release rolls up PR #92 (cert fix) and PR #93 (tunnel-node +
  CodeFull.gs scaffolding). PR #93 adds a standalone binary under
  tunnel-node/ plus an Apps Script companion; no main-crate changes,
  so this is a zero-risk merge. Users who want to deploy a tunnel
  node can start today. The dispatch that activates `mode: full` is
  still in review in PR #94.
v1.2.7
2026-04-23 23:38:10 +03:00
vahidlazio 31ae569aa2 feat: tunnel-node service + CodeFull.gs (#93)
Standalone Rust/axum HTTP server + Apps Script-side CodeFull.gs for users who want to deploy a remote tunnel node. All new files; no changes to the main Rust crate. This is part 1 of 3 of the full-tunnel feature — it adds scaffolding that users can opt into once the Rust-side Mode::Full lands in #94.
2026-04-23 23:34:51 +03:00
vahidlazio 39537e6689 fix: replace accounts.googl.com with accounts.google.com in SNI pool (#92)
The googl.com shortener domain is NOT in Google's GFE certificate SAN list — verified via `openssl s_client -verify_hostname accounts.googl.com` returning hostname mismatch. Every Nth connection where the rotation landed on this entry was failing cert validation with `verify_ssl=true`. Replaced with accounts.google.com which is covered by *.google.com wildcard.
2026-04-23 23:33:11 +03:00
therealaleph 658e72fe0d v1.2.6: rust-cache bin pruning fix + PR #83 scan-sni
v1.2.4 and v1.2.5 both cut clean tags but CI failed downstream for
different self-hosted reasons:

- v1.2.4 failed on parallel apt-lock race (fixed)
- v1.2.5 failed with "TOML parse error at line 5 column 9" because
  rust-cache v2's default cache-bin=true prunes $CARGO_HOME/bin of
  any binary not installed via `cargo install`. `rustup` itself is
  installed by rustup-init, not cargo install, so it got flagged as
  "unknown" and deleted on cache save. Next job hits the cargo
  symlink that points at a missing rustup, which resolves somehow
  to a very old cargo that can't parse our Cargo.toml.

Fix:
- Set `cache-bin: "false"` on every Swatinem/rust-cache@v2 call.
  We still cache target/ + registry (the big win), just not bin/.
  Binaries are stable across runs on our self-hosted box anyway.
- Reinstalled rustup inside each per-runner CARGO_HOME on the server
  to recover from the broken state.

Also in this release:
- PR #83: new `mhrv-rs scan-sni` subcommand. Pulls Google's
  published IP ranges, does PTR lookups via dns.google on each IP,
  filters to Google-related hostnames, then TLS-probes each
  discovered SNI against the configured google_ip to see which ones
  bypass DPI. Useful for rebuilding a working SNI pool on a new ISP.
  Adds the `url` crate dep.

Same user-facing code as v1.2.4/v1.2.5 (PRs #78, #79, README Android
note) plus PR #83 and the CI fixes on top.
v1.2.6
2026-04-23 21:22:17 +03:00
v4g4b0nd_0x76 ca10f775dc scan-sni: add automated SNI discovery + DPI validation (#83)
New `mhrv-rs scan-sni` subcommand: pulls Google's published IP ranges, issues PTR lookups via dns.google, filters results to Google-related hostnames, then TLS-probes each discovered SNI against the user's configured `google_ip`. Prints the SNIs that pass DPI for the user to paste into `sni_hosts`. Also expands the hardcoded FAMOUS_GOOGLE_DOMAINS list the existing scan-ips command already used.

Adds `url` crate for URL parsing in the DNS-over-HTTPS client. No other behavioural changes.
2026-04-23 21:18:12 +03:00
therealaleph af44abbcd3 v1.2.5: CI self-hosted apt-lock fix (v1.2.4 release was incomplete)
v1.2.4 tagged cleanly but its CI failed — parallel Linux matrix jobs
on the self-hosted runners all raced on `/var/lib/apt/lists/lock` and
failed the `sudo apt-get install` step within ~20s. v1.2.4's release
job therefore skipped and no assets were published.

Fix:

- Pre-installed every apt dependency the workflow needs on both
  self-hosted runners (eframe system libs, gcc-aarch64-linux-gnu,
  gcc-arm-linux-gnueabihf).
- Seeded per-runner cargo linker configs at
  /home/ghrunner/cargo-{01,02}/config.toml so the "echo
  [target.xxx] linker = ..." workflow step is also unnecessary.
- Gated the "Install Linux eframe system deps" and the two cross-
  compile-toolchain steps on `runner.environment == 'github-hosted'`
  so only hosted runners call apt-get; self-hosted runners skip the
  whole thing and use pre-installed tooling.

Re-tagging as v1.2.5 since v1.2.4 is an abandoned tag (git tag exists
but no GitHub Release was cut for it).

Same code changes as what v1.2.4 was meant to ship: PR #78 range-
parallel validation, PR #79 port-collision rejection, README note
on Android 7+ user-CA trust.
v1.2.5
2026-04-23 21:06:25 +03:00
therealaleph cb07311cf1 v1.2.4: range-parallel validation + port-collision guard + README Android note
- 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.
v1.2.4
2026-04-23 20:52:06 +03:00
freeinternet865 13593528ff config: reject HTTP and SOCKS5 port collisions (#79)
Reject configs that set HTTP and SOCKS5 listeners to the same port. Enforced both at config-load and in the UI form so users get a clear error before bind-time failure. Adds a focused regression test.
2026-04-23 20:48:12 +03:00
freeinternet865 2cd6b56ec3 domain_fronter: validate range-parallel 206 responses before stitching (#78)
Validate Content-Range in the range-parallel path before stitching. Malformed 206s are no longer combined into a fake 200 OK; invalid probes fall back to a normal single GET, invalid later chunks fall back to the validated probe response.
2026-04-23 20:48:07 +03:00
therealaleph 15e3e38745 v1.2.3: move CI to self-hosted runners + rust-cache
Linux / Android / mipsel build jobs now run on two self-hosted runners
on a Hetzner 8-core / 31 GB Ubuntu 24.04 box with Rust, Android SDK+NDK
r26c, all cross-compile toolchains and Docker pre-installed. macOS and
Windows still run on GitHub-hosted — we don't self-host those OSes and
the free minutes on a public repo are plenty.

Adds Swatinem/rust-cache@v2 to every cargo-using job so target/ + cargo
registry survive between runs. With warm caches the Linux jobs take
~1min each and the Android job ~3-4min; cold runs are ~9min for
Android and ~2min for everything else. Release wall time before this
change was ~13m consistently; it should now sit around 6-7m.

No new user-facing code in this release — primarily an infra change
exercised by an actual tag-push so we verify the full pipeline works
end-to-end from the new runners.
v1.2.3
2026-04-23 20:23:10 +03:00
therealaleph e48a8f6add v1.2.2: Android Start crash fix + google_ip preservation + chromewebstore SNI
Three user-facing fixes:

- Android Start crash in google_only mode (#73): every early-return
  path in startEverything now satisfies Android 8+'s foreground-service
  contract by calling startForeground before stopSelf. Previously if
  you opened the app, selected google_only mode, and tapped Connect
  without filling deployment ID + auth key (which google_only doesn't
  need anyway), the service crashed with
  ForegroundServiceDidNotStartInTimeException. Also gated the
  deployment-ID requirement on mode == APPS_SCRIPT.

- google_ip auto-overwrite on Start (#71): some carriers serve poisoned
  DNS for www.google.com that resolves but refuses TLS, clobbering
  working IPs users had manually set. DNS lookup now only fires when
  the field is blank — manual configs are preserved across Connect.
  Explicit "Auto-detect" button still refreshes on demand.

- chromewebstore.google.com added to DEFAULT_GOOGLE_SNI_POOL and
  DEFAULT_SNI_POOL (#75). Same family as the rest of the pool —
  wildcard cert, GFE-hosted.
v1.2.2
2026-04-23 19:49:40 +03:00
freeinternet865 9ff887abaa proxy: fix google_only plain HTTP content length (#70)
Body is exactly 120 bytes; header was advertising 128. Some clients treat that as truncated and hang waiting for extra bytes. Regression from #62.
2026-04-23 19:42:06 +03:00
therealaleph 1d5d13d63d v1.2.1: IP-literal fast-fail + more SNIs + x.com GraphQL fix + Android SNI paste
Rollup of the three upstream-Python ports plus an Android UX polish:

- plain_tcp_passthrough: 4s connect timeout for IP literals (10s for
  hostnames). Halves Telegram DC-rotation latency when the current DC
  is DPI-dropped.
- DEFAULT_GOOGLE_SNI_POOL / DEFAULT_SNI_POOL: +maps, chat, translate,
  play, lens.google.com. More fingerprint spread, and maps/play pass
  DPI on some carriers where shorter *.google.com names don't.
- handle_mitm_request: x.com GraphQL URL truncation — strip everything
  after the first & when the path matches /i/api/graphql/.../?variables=.
  x.com's variables+features+fieldToggles blob overflows Apps Script's
  URL cap; `variables=` alone renders the timeline.
- Android SNI editor: paste-and-add now accepts a full list separated
  by whitespace / commas / newlines, dedupes, and merges with existing
  selection. Closes the "add them all at once" ask from #47.
- rlimit.rs: fence the example error log in a `text` code block so
  rustdoc stops trying to compile it.
v1.2.1
2026-04-23 16:44:09 +03:00
therealaleph 08fe6911b3 port upstream connection-quality fixes (masterking32/MasterHttpRelayVPN)
Three ports from the upstream Python repo — all straightforward wins for
user-facing connection reliability:

- plain-tcp: 4s connect timeout for IP literals (10s for hostnames).
  Ported from upstream 7b1812c. When Telegram MTProto (or any protocol
  that CONNECTs to a raw IP) hits a DPI-dropped DC, failing fast lets
  the client rotate to the next DC roughly twice as quickly. Users
  previously sat on "connecting..." for nearly a minute walking DC1→DC3.

- SNI rotation pool: add maps/chat/translate/play/lens.google.com to
  both the Rust DEFAULT_GOOGLE_SNI_POOL and the Android DEFAULT_SNI_POOL.
  Ported from upstream 57738ec. Extra fingerprint spread plus a couple
  of SNIs (maps, play) that reliably pass DPI where shorter
  *.google.com names don't.

- x.com GraphQL URL truncation: when the path matches
  /i/api/graphql/<hash>/<op>?variables=..., drop everything from the
  first `&` onward. The combined variables+features+fieldToggles
  query string regularly exceeds Apps Script's URL length cap and
  returns a generic relay error; `variables=` alone is enough for
  x.com's timeline to render. Ported from upstream 2d959d4.

No version bump — these are low-risk infrastructure patches; they'll
fold into the next release.
2026-04-23 16:32:04 +03:00