mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 07:34:36 +03:00
9e2b8e5f3ec67b51dae9a2e46e06b06dc725c8b4
56 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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 (
|
||
|
|
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
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
b90b003cbc |
feat: add google_only bootstrap mode (#62)
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. |
||
|
|
5a108f73cb |
v1.1.5: merge upstream safety fixes + Telegram default = file + link only (#60)
Contains the three safety fixes from PRs #48/#49/#50 and the Persian README RTL polishing from #58, all squashed into main. Merge details already in their individual PR comments; summary: #48: reject truncated Content-Length relay responses (previously silently accepted whatever bytes arrived before EOF) #49: reject truncated or malformed (missing CRLF) chunked-encoding relay responses (same class of silent-acceptance bug) #50: restrict the SNI-rewrite tunnel dispatch to port 443. Plain HTTP (:80) targets that happened to match google.com / hosts override were being steered into the TLS tunnel and blocking waiting for a ClientHello that would never arrive. #58: trailing-whitespace line-breaks on Persian bullet lists in README so the RTL rendering doesn't collapse consecutive items into a single paragraph. Test suite grew from 54 to 58 passing (three new negative tests for the relay-reader correctness fixes + one SNI-rewrite port filter). Telegram CI notify default switched to file-plus-link: - script gains a `--with-changelog` flag; default OFF - workflow only passes it when `vars.TELEGRAM_INCLUDE_CHANGELOG=true` - every routine release now posts just the APK + short caption (title + SHA-256 + repo URL + release URL) with no long body To include bullets for a given release again: gh variable set TELEGRAM_INCLUDE_CHANGELOG --body true The existing `vars.TELEGRAM_NOTIFY_ENABLED` job-level gate remains — changelog toggle is orthogonal to enable/disable. Also closes PR #55 without merging; ads/analytics domains were being lumped under a YouTube-specific toggle, and the PR committed per- machine \`.cargo/config.toml\` + zig-cc cross-compile helpers that would have broken CI on actual Windows / macOS runners. |
||
|
|
8d2f90b0a7 |
v1.1.4: YouTube video streaming — expanded SNI-rewrite list + parallel Range fetcher (#56)
Users of the upstream Python port
(github.com/masterking32/MasterHttpRelayVPN) reported that YouTube
videos render fine through theirs while the Rust port stalls. Diff
against the Python source exposed two substantive gaps we were
missing:
1. SNI-rewrite list was much shorter than upstream. Added:
gvt1.com, gvt2.com — Google Video Transport CDN (YouTube
video chunks + Chrome auto-updates +
Play Store downloads)
doubleclick.net — ads
googlesyndication.com
googleadservices.com
google-analytics.com
googletagmanager.com
googletagservices.com
fonts.googleapis.com — already covered by the googleapis.com
suffix but mirrored explicitly for clarity
These are all on Google's GFE IP pool, so they route over the
existing SNI-rewrite tunnel (direct to `google_ip` with SNI
rewritten) instead of the quota-limited Apps Script relay.
2. No range-parallel download path. Apps Script's per-call latency
is ~flat (~1-2s regardless of payload), so a 10 MB single GET
takes ~10s round-trip; the player times out or stutters. Upstream
Python's `relay_parallel` probes with Range: bytes=0-262143, and
if the origin supports ranges, fetches the rest in parallel
256 KB chunks (up to 16 concurrent). Ported that logic as a new
`DomainFronter::relay_parallel_range` method, called from both
MITM-HTTPS and plain-HTTP handlers for GETs without a body. Rust
implementation uses `futures::stream::buffered` for ordered
bounded-concurrency fan-out; cache layer already skips Range
requests (added defensive check in relay() too).
The existing single-script fan-out (`parallel_relay` config) is
complementary — it races N script IDs for each individual chunk,
where the range-parallel path slices the overall download. Both are
active simultaneously when both are configured.
Helper functions for HTTP parsing (split_response,
parse_content_range_total, rewrite_206_to_200, assemble_full_200)
mirror the Python equivalents.
No behaviour change for non-GET requests; no cache-correctness
changes for GETs that don't return 206.
|
||
|
|
5a5139f6ea |
v1.1.3: portable-atomic polyfill so mipsel-softfloat compiles (#46)
v1.1.2 reached cargo build inside the mipsel docker this time (YAML-fold bug finally out of the way) and surfaced the real underlying problem: MIPS32 has no native 64-bit atomic instructions, so std::sync::atomic::AtomicU64 doesn't exist on mipsel-unknown-linux-musl. Three call sites (DomainFronter stats counters + the request-cache) failed to resolve the import. Fix: depend on `portable-atomic` with the `fallback` feature and import AtomicU64 from there instead of std. The API is identical (same associated methods, same Ordering accepted), so the two touched files change only the `use` line. On 64-bit targets portable-atomic compiles down to the native 64-bit atomic insns with no overhead; on MIPS32 it uses a global spinlock, which is fine for counter increments that happen a few times per relay. Cache.rs and domain_fronter.rs both updated. No other callers of AtomicU64 in non-cfg-gated code (android_jni.rs has it but is gated `#![cfg(target_os = "android")]`, so mipsel-linux-musl never sees it). `cargo test --lib` / `cargo build` still pass on host. |
||
|
|
383bea008e |
v1.1.2: actually-green mipsel-softfloat (YAML comment-fold bug) (#44)
v1.1.1 still failed the mipsel CI matrix for a non-obvious reason.
The `Build CLI (mipsel-softfloat via docker)` step passed a
multi-line argument to `sh -c "..."` with `\` line continuations
and inline `#` comments:
sh -c "set -eux; \
# The image ships with a pre-installed nightly ... \
rustup toolchain uninstall ... \
..."
YAML's `run: |` block-scalar folds that into a single line on the
shell side — backslash-newline collapses become spaces. The
payload handed to `sh -c` becomes one long line in which the
first `#` comments out everything that follows on that line, so
the only command that actually ran inside the container was
`set -eux;`. Everything after it was a comment. The container
exited successfully (set -eux + empty; is a zero-exit no-op),
the `target/` directory never got created, and the post-docker
`sudo chown -R "$(id -u):$(id -g)" target` failed with
chown: cannot access 'target': No such file or directory
Process completed with exit code 1.
which fooled me into thinking the toolchain logic failed, when
actually NO toolchain logic ran at all.
Fix: use bash with a single-quoted multi-line script. Single
quotes preserve newlines literally, so `#` stays a
line-terminating comment rather than collapsing. Heredoc-style
formatting; same commands as before.
No other changes. Version bumps only (Cargo + Android versionCode/
versionName). Telegram notify stays off via the repo-variable
gate we added yesterday.
|
||
|
|
a7b63ee53a |
v1.1.1: accounts.googl.com in SNI pool + mipsel-softfloat lands green (#43)
SNI rotation pool gains `accounts.googl.com` (issue #42). Reporter confirmed it passes DPI on Samantel and MCI — Iranian carriers that selectively block some of the longer google.com subdomain SNIs. `googl.com` is a Google-owned redirect alias served off the same GFE pool, so the TLS handshake works against `google_ip:443` without extra plumbing; we just present the name in the ClientHello for fingerprint diversity. Mirrored into the Android default pool too. The mipsel-softfloat target finally builds green in CI — two earlier bugs that compounded: messense doesn't publish a `:mipsel-musl-softfloat` image tag (fixed in main earlier by using `mipsel-musl` + `RUSTFLAGS=-C target-feature=+soft-float` + `-Z build-std`), and the pre-installed nightly in that image has a broken component state that rustup can't upgrade in place (fixed by uninstalling nightly first). Both fixes are in the tagged commit this time. Closes issue #26. Previous issues addressed in v1.1.0 that this release documents the closing of: - issue #28: "egui_glow requires opengl 2.0+" on old Windows / RDP / VMs — fixed via dual glow+wgpu compile + MHRV_RENDERER env var + run.bat auto-retry. - issue #37: connection-mode picker (VPN/TUN vs Proxy-only) so users who already run another VPN can still use mhrv-rs as a per-app HTTP/SOCKS5 proxy. Version bump: 1.1.0 → 1.1.1 (versionCode 110 → 111). |
||
|
|
28be8f67d5 |
v1.1.0: unified Connect button, proxy mode, app splitting, Persian UI, MIPS build (#41)
Major feature release across Android + desktop. Six items the user asked for, verified end-to-end on the emulator. Android ------- * Unified Connect/Disconnect button. Single large button swaps between green "Connect" (when the service is down) and red "Disconnect" (when it's up). Tracks the real service state via a new process-wide `VpnState` singleton flipped from the service's startEverything() / teardown() — not optimistic, the button only reports what the service actually did. * Connection mode dropdown (issue #37). Two options: VPN (TUN) — routes every app — and Proxy only — user configures per-app via Wi-Fi proxy to 127.0.0.1:8080 (HTTP) / :1081 (SOCKS5). PROXY_ONLY skips VpnService.prepare() entirely (no OS VPN grant prompt) and the service just keeps the foreground listeners up. Default is VPN_TUN so existing behaviour is preserved for users who upgrade without looking at the dropdown. * App splitting. In VPN_TUN mode you can pick All / Only selected / All except selected, with a picker dialog that lists installed user-visible apps (LazyColumn with search, "show system apps" toggle, multi-select checkboxes). ONLY calls `Builder.addAllowedApplication()` for each chosen package; EXCEPT calls `addDisallowedApplication()` additive to the mandatory self-exclude. Requires QUERY_ALL_PACKAGES — added to the manifest along with a `<queries>` launcher-intent filter so the picker rows can render app labels, not just package strings. * Persian/English UI toggle with RTL. Top-bar TextButton cycles AUTO → FA → EN → AUTO. Persian strings live in `res/values-fa/strings.xml`; English in `res/values/strings.xml`. `AppCompatDelegate.setApplicationLocales()` is used as the persistence layer (plus `AppLocalesMetadataHolderService` meta and `locales_config.xml` for the per-app-language OS entry on API 33+). MainActivity overrides `attachBaseContext` to wrap the context with the right locale at the earliest possible moment — otherwise a saved preference wouldn't apply until the SECOND process after toggling. RTL swaps automatically because Persian is script="Arab" in Android's locale database. * Collapsible How-to-use card. The big instruction block that used to dominate the bottom of the screen now lives inside a CollapsibleSection that starts expanded for a fresh install (empty deployment URLs / auth_key) and collapsed otherwise. * Update check auto-fires on first composition, silent-on-up-to-date, snackbar-only-if-available. Still surfaces via the version badge tap for manual checks. * MhrvVpnService teardown guard was kept from v1.0.2 — `AtomicBoolean` makes the second caller a no-op, which is the SIGSEGV fix for "tap Stop, app closes" from before. Stress-tested under rapid Connect/Disconnect cycles. Desktop ------- * Fix: Advanced section silently resetting on every Save. `ConfigWire` was missing `fetch_ips_from_api` / `max_ips_to_scan` / `scan_batch_size` / `google_ip_validation` — every persist dropped them, every reload fell back to the serde defaults, user saw their Advanced toggles reset. Added the fields to the wire struct (issue surfaced by the user as "Advanced resets after reopening the app"). * Windows renderer fallback (issue #28). `eframe` is now built with BOTH `glow` (OpenGL 2+) and `wgpu` (DX12/Vulkan/Metal); runtime defaults to glow for compat but honours `MHRV_RENDERER=wgpu` for boxes that crash with "egui_glow requires opengl 2.0+" — old Windows hardware, RDP sessions, VMs without GPU acceleration. `run.bat` auto-retries the UI with `MHRV_RENDERER=wgpu` if the first launch exits non-zero, so users don't need to know about the flag. CI -- * Added OpenWRT mipsel-softfloat build target (issue #26). MT7621 routers specifically need soft-float because the CPU has no FPU; a hard-float binary segfaults on first fp op. Built via `messense/rust-musl-cross:mipsel-musl-softfloat` docker image + nightly Rust with `-Z build-std` (mipsel is Rust tier 3 since 1.72, no pre-built std). Marked `continue-on-error: true` — the tier-3 target occasionally regresses and we'd rather ship the rest of the release than block on MT7621 support. Signature / versioning ---------------------- * versionCode 110, versionName 1.1.0; Cargo bumped to 1.1.0. * Release APK signed with the committed `release.jks` (same as v1.0.2), so v1.0.2 → v1.1.0 upgrades install in-place without the uninstall-first dance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
64409f6b41 |
v1.0.2: stable release signature, idempotent Stop, top-level Settings for CA install (#33)
Three fixes + one behaviour change from v1.0.1 reports. APK signature is now stable (release.jks committed) ---------------------------------------------------- v1.0.0 and v1.0.1 signed release APKs with Gradle's auto-generated debug keystore, which is randomly generated per machine and per CI runner. Result: every upgrade failed with INSTALL_FAILED_UPDATE_INCOMPATIBLE and users had to uninstall first. Unfixable without a stable key. android/app/release.jks now holds that key, committed to the repo with the password in plaintext in build.gradle.kts. This is fine for a FOSS sideload project without a Play Store identity — the trust model is "trust the source tree you pulled from," not "trust the key we hold." Anyone forking and shipping a rebranded build should generate their own key. One-time cost: v1.0.1 → v1.0.2 STILL requires uninstall, because we're switching signature keys. Every upgrade from v1.0.2 onward is clean. Stop no longer (sometimes) closes the app ----------------------------------------- teardown() is reachable from three paths on two threads: 1. ACTION_STOP onStartCommand branch (mhrv-teardown worker) 2. onDestroy after stopSelf (main thread) 3. VpnService revocation out-of-band (main thread) Running the full native cleanup sequence twice races the two threads through Tun2proxy.stop() → fd.close() → Native.stopProxy(handle) on state that's already been nullified — SIGSEGV source, user-visible as "tap Stop, app disappears." New AtomicBoolean `tornDown` gates entry: first caller wins, every subsequent caller logs "teardown: already done" and returns. onDestroy also wraps the call in try/catch — crashing out of onDestroy takes the whole process with it, which is exactly the bug we're trying to fix. Smoke-tested on emulator: teardown now logs teardown: begin caller=mhrv-teardown ... clean sequence ... teardown: done onDestroy entered teardown: already done, skipping (caller=main) onDestroy done with PID unchanged throughout. CA install now routes to the Settings search -------------------------------------------- Old flow: `Settings.ACTION_SECURITY_SETTINGS` deep-link, then walk "Encryption & credentials → Install a certificate → CA certificate". That path varies wildly between OEMs (Samsung buries it under "Biometrics and security → Other security settings"; Xiaomi under "Passwords & Security → Privacy"; Pixel splits it between "More security settings" and "Privacy controls" depending on Android version). Users got lost. New flow: open the top-level Settings app (`Settings.ACTION_SETTINGS`) and instruct the user to use the Settings search bar to find "CA certificate". Search is consistent across OEMs and Android versions; the menu paths are not. Dialog, snackbar, and `docs/android.md` copy all updated to match. Version bump: 1.0.1 → 1.0.2 (versionCode 101 → 102). releases/mhrv-rs-android-universal-v1.0.1.apk replaced with the v1.0.2 build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b734f41faa |
v1.0.1: auto-resolve google_ip, robust Stop, Check-for-updates, front_domain repair (#31)
Three reported issues from v1.0.0 — one real bug, two UX gaps. google_ip auto-resolve (THE FIX) -------------------------------- Google rotates the A record for www.google.com across their anycast pool. A hardcoded default IP breaks new installs on any network that isn't geo-homed to the same edge — symptom is "all SNIs time out" even with a fresh deployment. On Start and via a new "Auto-detect" button, we now do a JVM-side InetAddress lookup BEFORE establishing the VPN (so the resolver uses the underlying network, not our own Virtual-DNS TUN — avoids a loop), update the config, and continue. The auto-resolve lives in the HomeScreen click handler (not MainActivity) so it goes through the same `persist(cfg)` the text fields use. Previous iteration did `ConfigStore.load → modify → save` directly to disk, which left Compose's in-memory cfg stale and a subsequent field edit would overwrite the fresh IP. One source of truth now. Also defensively repairs front_domain: if it's been corrupted into an IP literal (bad paste, whatever) we restore "www.google.com" — the TLS SNI on the outbound leg has to be a hostname or the handshake lands on the wrong vhost. Robust Stop ----------- The Stop button now dispatches both ACTION_STOP (graceful: runs teardown, stops tun2proxy, closes TUN fd, shuts down Rust runtime) AND stopService() (defensive: covers force-closed-then-reopened zombie state where Android auto-restarted our START_STICKY service in a fresh process and the in-memory TUN reference is gone). Check-for-updates ----------------- Tapping the version badge in the top bar now runs the same update_check that the desktop UI uses, via a new `Native.checkUpdate()` JNI entry point. Returns a JSON blob the Kotlin side parses into an "Up to date", "Update available: v→v <url>", "Offline: ...", or "Check failed: ..." snackbar. Mirrors the desktop's behavior so a user doesn't have to manually poll GitHub for new builds. Crash visibility ---------------- New MhrvApp.kt registers a process-wide uncaught exception handler. Crashes are now stamped into logcat under the `mhrv-crash` tag with the thread name before the default handler kills the process — previously the JVM crash in coroutines / the log drain / the tun2proxy worker was invisible unless you caught the dropoff in real time. Version bump: 1.0.0 → 1.0.1 (versionCode 100 → 101). Release APK rebuilt and replaces the 1.0.0 copy in releases/; CI will regenerate on the v1.0.1 tag push. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
91015b0594 |
v1.0.0: multi-arch Android APK + GitHub Actions release job + install docs (#30)
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>
|
||
|
|
96d1352728 |
Add Android app with full TUN bridge + two proxy fixes the desktop also wants (#29)
The app is a Kotlin/Compose front-end that reuses the mhrv-rs crate
via JNI. It speaks VpnService to get a TUN fd, hands that to tun2proxy,
and funnels every app's traffic through the in-process SOCKS5 listener —
no per-app proxy setup on the device.
Two fixes in `src/proxy_server.rs` apply to desktop builds too:
* SNI peek via `LazyConfigAcceptor`. When a browser uses DoH (Chrome's
default), tun2proxy hands us a raw IP in the SOCKS5 CONNECT. Minting
a MITM cert for the IP produced `ERR_CERT_COMMON_NAME_INVALID` on
Cloudflare-fronted sites. We now read the ClientHello's SNI first
and use that both as the cert subject and as the upstream host for
the Apps Script relay (fetching `https://<IP>/...` with an IP in the
Host header gets rejected by CF anyway).
* Short-circuit CORS preflight at the MITM boundary. `UrlFetchApp.fetch()`
rejects `OPTIONS` with a Swedish "Ett attribut med ogiltigt värde
har angetts: method" error, which silently broke every fetch()/XHR
preflight and was the root cause of "JS doesn't load" on Discord,
Yahoo, and similar. Since we already terminate the TLS the browser
talks to, answering the preflight with a permissive 204 is safe —
the real request still goes through the relay.
Android-side capabilities (feature-parity with `mhrv-rs-ui` where it
fits on a phone):
* multi-deployment ID editor
* SNI rotation pool + per-SNI "Test" + "Test all" (JNI into scan_sni)
* live logs panel (JNI ring buffer drained on a 500 ms poll)
* Advanced section: verify_ssl, parallel_relay, log_level, upstream_socks5
* CA install flow that matches modern Android's reality: saves
`Downloads/mhrv-ca.crt` via MediaStore, deep-links Security settings,
then verifies post-hoc by fingerprint lookup in AndroidCAStore (the
KeyChain intent dead-ends with a Close-only dialog on Android 11+)
* Start/Stop debounced to dodge an emulator EGL renderer crash on
rapid taps
Theme matches the desktop palette exactly — always-dark, accent
`#4678B4`, card fill `#1C1E22`, 4dp button / 6dp card radii.
No dynamic color, no light scheme: the desktop is always dark and
we follow.
Build wiring:
* `Cargo.toml`: `cdylib` crate-type added; `jni` + `tun2proxy`
scoped to `cfg(target_os = "android")` so desktop builds pay
nothing.
* `src/data_dir.rs`: `set_data_dir()` override so the Android app's
private filesDir replaces the `directories` crate's desktop default.
* `src/android_jni.rs`: JNI entry points for start/stop/exportCa plus
a ring buffer draining to `Native.drainLogs()` and `testSni()` that
wraps `scan_sni::probe_one`.
* Gradle task chain runs `cargo ndk` before each assemble; post-step
normalizes tun2proxy's hash-suffixed cdylib to a stable filename
so `System.loadLibrary("tun2proxy")` works.
Verified end-to-end on an API 34 emulator: ipleak, yahoo, discord,
cloudflare.com all render; TLS is MITM-ed under our user-installed
CA; service survives rapid Stop/Start cycles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a9ad697b6a |
v0.9.4: actionable diagnostic when outbound TLS to Google edge fails (#18 follow-up)
@Behzad9 reports: after the EMFILE fix in v0.9.3 landed cleanly, the
relay now fails with a different error:
ERROR Relay failed: io: invalid peer certificate: UnknownIssuer
repeated on every request. This is rustls (via domain_fronter) rejecting
the server cert that whatever sits on our TLS connection to google_ip
presents. In practice this means one of three things, in decreasing
order of likelihood for an Iranian OpenWRT user:
1. The ISP / a middlebox is intercepting outbound TLS to Google IPs
and presenting its own cert. webpki-roots (Mozilla trust store,
baked in) correctly rejects it.
2. The user's google_ip setting points at a non-Google host.
3. Router clock is wildly off (NTP not synced), certs look not-yet-valid.
Before this change: one identical ERROR per failed relay, no guidance.
Log filled with the same line.
Now:
- New DomainFronter::log_relay_failure() detects cert-related error
strings (UnknownIssuer, CertificateExpired, CertNotValidYet,
NotValidForName, 'invalid peer certificate').
- First occurrence logs an ERROR with the three root causes and three
concrete fixes: run to find a working Google IP,
check the system clock, or as a LAST RESORT set verify_ssl=false
(with the explicit warning that traffic is then only protected by
the Apps Script auth_key, not outer TLS).
- Subsequent occurrences drop to debug so the log stays readable —
an AtomicBool gate on the DomainFronter instance tracks whether
the hint was shown. Resets on proxy restart.
- Non-cert errors still log at error level unchanged.
49 tests pass, no code-path regressions (log line content changed, not
behavior). Shipping so users hit this get actionable output.
|
||
|
|
93fac57f5f |
v0.9.3: accept-loop backoff on EMFILE + louder rlimit diagnostics (issue #18)
@Behzad9 on #18: the OpenWRT 'No file descriptors available' errors are back in v0.8.0+, this time logged as a wall of thousands of identical ERRORs within seconds of activating the proxy. Two real bugs, now fixed: === 1. accept() loop had no backoff === Previous code: loop { match listener.accept().await { Ok(x) => ..., Err(e) => { tracing::error!(...); continue; } // tight loop } } On EMFILE (RLIMIT_NOFILE exhausted), accept() returns synchronously, the match re-runs instantly, accept() EMFILEs again, forever. The tight loop ALSO starves the tokio runtime of CPU that existing connections need to finish and close their fds — so the problem never clears on its own. It's a self-sustaining meltdown. New accept_backoff() helper (in proxy_server.rs) wraps both the HTTP and SOCKS5 accept loops: - Detects EMFILE/ENFILE via raw_os_error (24 or 23). - Sleeps proportional to how long the pressure has lasted (50 ms first hit, ramping to a 2 s cap around hit #40). Gives existing connections a chance to finish and free fds. - Rate-limits the log line: one WARN on the first EMFILE with fix instructions, then one every 100 retries. No more walls of identical errors. - Resets the counter on the next successful accept. - Non-EMFILE errors (ECONNABORTED from clients that went away during handshake, etc.) get a plain single-line error + 5 ms sleep so we still don't tight-loop on any unexpected error. End-to-end verified: ran mhrv-rs under , flooded the SOCKS5 port with 247 concurrent connections to trip EMFILE. Before: log would have been 1000s of identical lines. After: exactly 1 warning, listener stayed quiet, fds drained, accept resumed. === 2. RLIMIT_NOFILE bump was too conservative + silent === Previous behavior: target 16384 soft, cap to existing hard limit, no log. On constrained systems where hard is already tiny, we'd stay at the tiny limit silently. rlimit.rs now: - Targets 65536 soft. - ALSO tries to raise the hard limit up to /proc/sys/fs/nr_open on Linux (Linux allows a non-privileged process to bump its own hard limit up to the kernel ceiling, usually 1048576 on modern kernels). On macOS/BSD we skip this — only bump soft. - Logs WARN on startup if soft ends up <4096 with the exact fix ('ulimit -n 65536' or use the procd init). No more silent failure. - Logs INFO with the before/after limits otherwise, so field bug reports tell us immediately whether the kernel cap is the real bottleneck. Moved the rlimit call from main() pre-logging to post-init_logging so its tracing output actually lands in the log panel + stderr. Small reorganization only. 49 tests pass, musl x86_64 cross-compile verified locally. |
||
|
|
9768cd9edb |
v0.9.2: update check tunnels through proxy + one-click asset download (#15 follow-up)
@zula-editor reported on issue #15 that the Check-for-updates button was returning HTTP 403 on their ISP — classic GitHub unauthenticated-API rate limit (60/hour per IP) on a shared NAT IP. They also asked for the update to actually be downloadable from the app, not just a page link. Both addressed: === Route update check through our own proxy when running === New mhrv_rs::update_check::Route enum: - Direct: straight rustls to api.github.com (existing behavior) - Proxy { host, port }: HTTP CONNECT through our local HTTP proxy listener → MITM → Apps Script → api.github.com. When the proxy is running, the UI automatically picks Proxy. From GitHub's POV the request now comes from Apps Script's IP range (a Google datacenter) — completely different rate-limit bucket from the user's ISP IP, AND works even if GitHub is blocked on their network. Routing over proxy means the MITM leaf for api.github.com has to be trusted in the update_check's TLS config. build_root_store() now conditionally adds our own CA cert from data_dir::ca_cert_path() to the webpki roots when Route::Proxy is in use. Direct path is unchanged. === Download button === The UpdateCheck::UpdateAvailable variant now carries an optional ReleaseAsset { name, download_url, size_bytes } picked by pick_asset_for_platform() from the GitHub API's assets[] array. Preference list per (OS, arch): - macOS arm64 → mhrv-rs-macos-arm64-app.zip, else tar.gz - macOS amd64 → mhrv-rs-macos-amd64-app.zip, else tar.gz - Windows → mhrv-rs-windows-amd64.zip - Linux aarch64 → mhrv-rs-linux-arm64.tar.gz - Linux armv7 → mhrv-rs-raspbian-armhf.tar.gz - Linux x86_64 → mhrv-rs-linux-amd64.tar.gz UI: when an update is available AND we have an asset, the transient status line grows an accent-blue 'Download X.Y MB' button. Clicking fires Cmd::DownloadUpdate, which pipes the asset through the same Route (proxy if running, direct otherwise), writes it to UserDirs::download_dir() (~/Downloads on most systems), and shows a 'show in folder' button that opens Finder / Explorer / xdg-open on the containing directory. Three new unit tests for asset-picking. The gated live test now takes a Route argument (Direct) so it keeps working across the API shape change. 49 tests pass. Also refreshed in-repo releases/ archives to v0.9.1 alongside. |
||
|
|
0beec6a277 |
v0.9.1: normalize X/Twitter GraphQL URLs for cache hit rate (issue #16)
User @barzamini pointed out an optimization from the Python community (originally from seramo_ir): X/Twitter GraphQL URLs look like https://x.com/i/api/graphql/{hash}/{op}?variables=...&features=...&fieldToggles=... The features and fieldToggles params change across sessions and even within a session, busting our 50 MB response cache on every request to the same logical query. Stripping everything after 'variables=' lets identical logical queries collapse into one cache entry, dramatically reducing quota usage when browsing Twitter through the relay. Implementation: - src/domain_fronter.rs: new normalize_x_graphql_url() helper. Matches exactly the Python patch's pattern (host == 'x.com', path starts with /i/api/graphql/, query starts with variables=). Truncates at the first '&' past the '?'. Applied at the top of relay() so the normalized URL feeds BOTH the cache key AND the request sent to Apps Script — so we save on Apps Script quota too, not just on return-trip bytes. - src/config.rs: new opt-in normalize_x_graphql bool (default false). Off by default because strict X endpoints may reject trimmed requests; user should flip it on and watch for regressions. - src/bin/ui.rs: checkbox in the Advanced section, 'Normalize X/Twitter GraphQL URLs', with tooltip explaining the trade-off and crediting the source. - Four new unit tests in domain_fronter::tests covering: the happy path trim, non-x.com hosts pass through unchanged, non-graphql x.com paths pass through unchanged, and idempotency. 48 tests total, all green. Credit: idea by seramo_ir, Python patch at https://gist.github.com/seramo/0ae9e5d30ac23a73d5eb3bd2710fcd67, implementation request by @barzamini in issue #16. |
||
|
|
3387d94ed9 |
v0.9.0: UI redesign + stricter end-to-end test verification
=== UI redesign (zero new deps, same binary size) ===
Entire App::update() rewritten around three ideas:
1. Section cards. Form rows are grouped inside rounded frames with
faint fills and small-caps headings:
- 'Apps Script relay' — Deployment IDs (textarea) + Auth key
- 'Network' — Google IP (+inline scan button), Front
domain, Listen host, HTTP+SOCKS5 ports
on one row, SNI pool button
- Collapsing 'Advanced' — upstream SOCKS5, parallel dispatch,
log level, verify SSL, show auth key.
Closed by default — most users never
touch these.
2. Clearer action hierarchy. Primary buttons are accent-filled and
larger:
- Start (green filled, ▶ glyph, 120x32)
- Stop (red filled, ■ glyph, 120x32)
- Save config (blue accent filled, path shown inline after →)
- SNI pool (blue accent filled, inside Network section)
- Test relay (neutral, tall)
Secondary actions (Install CA / Check CA / Check for updates)
moved to their own compact row below, no longer competing.
3. Status + log clarity.
- Header version links to GitHub: → repo, →
the release tag page.
- Running/stopped status is now a pill-shaped colored chip at the
right end of the header (green fill + green dot when running,
red when stopped).
- Traffic stats in a 2-column layout inside the Traffic card —
7 metrics fit in 4 rows instead of a 7-row vertical strip.
- One compact transient status line above the log that auto-hides
after 10 seconds — replaces the previous stack of permanent
ca_trusted / test_msg / update_check labels that were pushing
the log panel off-screen.
- Log panel now has its own bordered frame (darker fill), a
'[x] show' checkbox that hides it entirely when off, a 'save…'
button that writes the current log buffer to a timestamped
log-YYYYMMDD-HHMMSS.txt in the user-data dir, and a 'clear'
button. Empty state shows a muted placeholder instead of
silent void.
All helper functions (section, primary_button, form_row) live at the
top of ui.rs as small local helpers — no new modules, no new
dependencies.
=== Stricter end-to-end test (test_cmd.rs) ===
Previous test passed on any HTTP 200 status regardless of body.
After a user pointed out that the test reported PASS even after
they deleted their Apps Script deployment, updated the pass criteria:
1. Status must contain '200 OK'.
2. Body must parse as JSON.
3. JSON must have an 'ip' field with a valid IPv4 or IPv6.
Anything else → SUSPECT (returns false), with a specific log message
like 'HTML returned instead of JSON. The Apps Script deployment may
be deleted, not published to Anyone, or requires sign-in.'
Also now emits tracing::info!/warn!/error! alongside println!, so
the verdict + detail show up in the UI's Recent log panel instead
of disappearing to a stdout nobody sees.
One new unit test: looks_like_ip() accepts v4+v6, rejects empty,
rejects malformed, rejects overflowed octets. 44 tests total, all
green.
Verified locally end-to-end — UI launches clean, form loads config
cleanly, Start/Stop/Save all fire correctly, Test relay produces
the new PASS/SUSPECT verdict with the tracing detail visible in
the log panel, Check-for-updates hits GitHub and resolves with the
compact auto-hiding status line.
|
||
|
|
3534747e68 |
v0.8.6: armhf glibc pin + Check-for-updates button (#15)
=== PR #14 follow-up: armhf build runs on Pi Bookworm/Bullseye === PR #14 (merged earlier) added arm-unknown-linux-gnueabihf to the release matrix but pinned os=ubuntu-latest, which is 24.04 with GLIBC 2.39. Target armhf sysroot on 24.04 is Debian Trixie (GLIBC 2.39), far too new for a Raspberry Pi 2/3 on Bookworm (2.36) or Bullseye (2.31) — users would get 'GLIBC_2.39 not found' the same way the Linux-amd64 issue #2 folks did before we pinned them to 22.04. Fix: pin the armhf matrix entry to ubuntu-22.04, matching our other linux-gnu targets. Binary will link against GLIBC 2.35 max, which loads on Pi Bookworm and Bullseye. Also trimmed two trailing spaces. Locally verified the cross-compile: rust:latest + gcc-arm-linux- gnueabihf + proper CARGO_HOME config.toml produces a valid ARM 32-bit ELF (2.9 MB, armhf EABI5). === Issue #15: 'Check for updates' button in the UI === New src/update_check.rs module. On the user's click (no polling): 1. Tcp-probes github.com:443 with a 5s budget. If unreachable, we return Offline(reason) instead of a confusing 'update check failed' — distinguishes 'you're offline' from 'GitHub API misbehaved'. 2. HTTPS GET api.github.com/repos/.../releases/latest via the tokio + rustls stack (same hand-rolled HTTP pattern as domain_fronter — no new crate deps). Parses tag_name, strips the v-prefix, loose-semver-compares to env!(CARGO_PKG_VERSION). 3. Returns one of four UpdateCheck variants: Offline / Error / UpToDate / UpdateAvailable { release_url }. New UI wiring (src/bin/ui.rs): - Cmd::CheckUpdate enqueue variant - UiState::last_update_check { InFlight, Done(result) } - 'Check for updates' button next to the CA buttons - Result displayed as a colored small-text line under the CA info: green 'up to date', amber 'update available v0.8.5 → v0.8.6' with a clickable release-page hyperlink, red for offline/error. Verified end-to-end with a live github.com fetch (got a rate-limit HTTP 403 from my IP because I've been hitting the API a lot, but that's the expected Error() state — response classification was correct). Three unit tests for is_newer() and a gated live test for the full round-trip. 43 tests pass. |
||
|
|
014c2a8cd1 |
v0.8.5: Check-CA actually checks Windows now (follow-up to #13)
User on issue #13 reported that even after installing the CA (and seeing it in the Windows cert manager UI), our 'Check CA' button still said 'NOT trusted'. Root cause: is_ca_trusted() on Windows was just returning false unconditionally — Check-CA has never worked on Windows. Fix: is_trusted_windows() now shells out to certutil: certutil -user -store Root 'MasterHttpRelayVPN' certutil -store Root 'MasterHttpRelayVPN' Checks both the user store (where our install_windows puts it by default) and the machine store (fallback path when user-store install is blocked). Requires certutil to print the cert name in stdout AND exit 0 — belt-and-suspenders against locales where certutil exits 0 even on an empty match. Also made the Check-CA UI message point users at the CA file path for cross-device install — the same user reported their Android V2rayNG client getting cert errors on our MITM-signed TLS leaves, which is the expected 'the phone doesn't trust our CA' scenario. The message now calls out the ca.crt path explicitly, and notes the Android 7+ user-CA restriction (Firefox Android works, Chrome and most apps don't trust user-installed CAs regardless). Not addressed (by design): - Replacing our CA keypair with Python-generated PEM fails to parse via rcgen. User tried this as a workaround before reporting. rcgen expects PKCS#8 PEM; Python's cryptography commonly emits PKCS#1 ('BEGIN RSA PRIVATE KEY'). Even if parsing worked, mixing an external CA with our leaf-issuing code would break the key match. Users should stick with our generated CA — that's the supported flow. The Python cross-contamination experiment is expected to fail; we don't document it as supported. |
||
|
|
790212cf51 |
v0.8.4: config load failures are no longer silent (diagnoses the reset-on-reopen bug)
A user reported that after Save-config, closing the UI, and reopening,
every form field was blank — but the config.json on disk still had all
the right values.
The culprit in the UI was load_form()'s swallow-errors pattern:
let existing = if path.exists() {
Config::load(&path).ok() // .ok() threw away the error
} else { ... };
if let Some(c) = existing { /* populate form */ } else { /* defaults */ }
When Config::load returned an Err, .ok() silently converted to None,
the form went back to defaults, and the user had no signal at all
that the load had failed or WHY. On every platform I could test
(macOS / Linux) the round-trip works fine with a real round-trip test
added in config.rs (config::rt_tests::round_trip_all_current_fields
and round_trip_minimal_fields_only — both green). So whatever's
failing for this specific reporter is environment-specific (weird
filesystem encoding, partial write, different field shape from an
older version, … TBD). Without visibility we can't diagnose it.
Changes:
1. load_form() now returns (FormState, Option<String>). The String
is a user-facing error message (with the full path + the
underlying parse/validate reason) when Config::load fails on an
existing file.
2. main() plumbs that error into App's initial toast, which sticks
for 30 seconds (vs the normal 5 for regular toasts) so users who
only open the UI briefly still see it.
3. Added tracing::info! in load_form for the success path too —
the Recent log panel now always shows either 'config: loaded OK
from <path>' or 'Config at <path> failed to load: <reason>' on
startup, regardless of toast timing.
4. Added two regression-guard tests in config.rs covering the
full-fields and minimal-fields save shapes the UI emits.
Next time a user reports this: they'll have the exact error in the
toast + the Recent log panel, and we can fix the actual bug instead
of shooting blind.
|
||
|
|
5371bfc7d5 |
v0.8.3: UI log panel now captures tracing events + dispatch routing visibility (issue #12)
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.
|
||
|
|
e9a973fc0f |
v0.8.2: CA trust — Firefox enterprise_roots + Chrome ~/.pki/nssdb (follow-up to #11)
After v0.8.1 fixed the leaf cert extensions, users reported "still broken" — specifically Firefox showing: "Software is Preventing Firefox From Safely Connecting to This Site. drive.google.com ... This issue is caused by MasterHttpRelayVPN" for HSTS-preloaded sites. That error is Firefox's "MITM detected AND issuing CA isn't in my trust store" path combined with HSTS blocking the normal override button — so users were stuck with no workaround. Real root cause of the "still broken" reports: the CA was making it into the OS trust store (Windows cert store / update-ca-certificates on Linux) but NOT into the browser-specific trust stores that Firefox and Chrome use on every OS. Three additions: 1. Firefox: . For every Firefox profile we find, we now write this pref to the profile's user.js. It tells Firefox to trust the OS CA store, so our already-successful system-level install automatically covers Firefox on next startup. Critical on Windows (NSS certutil isn't on PATH there, so the certutil-based Firefox install never worked). Idempotent — checks for existing pref before writing and leaves a non-matching user value alone. 2. Chrome/Chromium on Linux: install into ~/.pki/nssdb. Linux Chrome uses its own shared NSS DB, independent of both the OS store (populated by update-ca-certificates) AND Firefox's per-profile NSS. Without this, users installed the CA via run.sh, Chrome still refused every HTTPS site, and they spiraled trying to re-install the CA. We now also initialize that DB with if it doesn't exist yet. 3. Refactored the NSS-install path so Firefox and Chrome share a single install_nss_in_dir() helper. Renamed the top-level entry from install_firefox_nss to install_nss_stores to match scope. Locally verified the cert itself is fine — openssl x509 -text shows Version 3, SAN, KeyUsage (critical), ExtendedKeyUsage, and passes. So the leaf is correct; what was failing was the trust-chain validation inside the specific browser because our CA wasn't in THAT browser's trust DB. Upgrade path: download v0.8.2 and run the launcher or `./mhrv-rs --install-cert`. Restart Firefox/Chrome after install — Firefox needs the restart to re-read user.js. |
||
|
|
27cda07f8d |
v0.8.1: fix MITM leaf cert extensions — HTTPS through the proxy now works (closes #11)
Multiple users reported the same thing (issue #11): they trusted the CA, then re-installed it, then deleted and re-generated it, and still every HTTPS site through the proxy failed in the browser. The python version of the same project doesn't have the issue. Root cause: rcgen's CertificateParams::default() produces a minimum-viable x509 cert that does NOT carry: - ExtendedKeyUsage extension with id-kp-serverAuth - KeyUsage extension with digitalSignature + keyEncipherment Modern Chrome / Firefox / Edge / Safari all reject TLS server leaves without those. The CA trust bit didn't matter — the browser's chain validator rejected the leaf itself with NET::ERR_CERT_INVALID before ever consulting the trust store. So 'reinstall the CA' was powerless to help. Fix in src/mitm.rs::issue_leaf: - Set params.extended_key_usages = [ServerAuth]. - Set params.key_usages = [DigitalSignature, KeyEncipherment]. - Backdate not_before by 5 min to absorb clock skew between the MITM process and a slightly-fast client clock. Same fix in the CA's own not_before. Also added src/mitm.rs::tests::leaf_has_serverauth_eku_and_key_usage as a permanent regression guard — it parses the DER with x509-parser and asserts the three extensions are present. Added x509-parser to dev-dependencies (already in the tree transitively via rcgen). Upgrade path for users affected by #11: download v0.8.1, run it. No CA reinstall required — the CA cert itself was fine, only the per- site leaves were broken. Verified end-to-end locally: curl --cacert <ca.crt> -x http://127.0.0.1:... https://httpbin.org/ip curl --cacert <ca.crt> -x socks5h://127.0.0.1:... https://httpbin.org/ip Both return JSON without cert errors, through the Apps Script relay path. 37 unit tests pass. |
||
|
|
501c03a873 |
ci fix: move [target.'cfg(unix)'.dependencies] after main [dependencies]
Placing a new table header before eframe silently scoped it into the unix-only target table, so Windows builds lost the dependency entirely: error[E0432]: unresolved import `eframe` use of unresolved module or unlinked crate `eframe` (Builds green on Mac/Linux because those hit cfg(unix) == true. Windows was the only casualty.) Moved the [target.'cfg(unix)'.dependencies] block to the end of Cargo.toml, after the optional eframe line, so the main [dependencies] table stays intact for all targets. Added a comment so this foot-gun can't return. |
||
|
|
5101a06a5d |
v0.8.0: dynamic IP discovery (from PR #9), OpenWRT fd fix (#8), Windows UI diagnostics (#7)
Three user-reported fixes / features in one release. === PR #9 — dynamic Google IP discovery (@v4g4b0nd-0x76) === Already merged in the previous commit. Opt-in via 'fetch_ips_from_api' in config. Pulls goog.json from www.gstatic.com, maps it against resolved IPs of well-known Google domains, samples from matching CIDRs, and validates each candidate with gws / x-google / alt-svc response-header checks. Graceful fallback to the static list if the fetch fails or nothing passes validation. Default is off so existing users are unaffected. Closes #10. === Issue #8 — OpenWRT: 'accept: No file descriptors available' === OpenWRT routers ship a very low RLIMIT_NOFILE (often 1024, sometimes 256 on constrained devices). A browser's burst of ~30 parallel sub- resource requests can fill the limit within seconds, after which accept(2) returns EMFILE and the proxy is effectively dead. Two-fold fix: 1. New assets/openwrt/mhrv-rs.init now sets procd limits nofile= "16384 16384" on the service. procd raises the per-process fd limit before the binary even starts. 2. New src/rlimit.rs best-effort-raises RLIMIT_NOFILE in the binary itself (Unix only, no new runtime deps — libc is already transitively present via tokio). Targets 16384 soft, capped to whatever hard limit the kernel already allows the user (so it doesn't need root). Both layers mean the fix applies whether the user runs via /etc/init.d/mhrv-rs start (procd limits kick in) or ./mhrv-rs --config ... (in-binary bump kicks in) or any other invocation path. Closes #8. === Issue #7 — Windows UI crashes silently === User report: on Win 11, run.bat prints 'Starting mhrv-rs UI...' and exits clean, but no UI window ever appears. Root cause: the old run.bat used 'start "" "mhrv-rs-ui.exe"' which returns immediately — if the UI binary dies at launch time (missing GPU driver, RDP without GL accel, AV blocking, …), the crash is invisible because start already disowned the child. Fix: run the UI in-place (not via 'start'), so its stderr and exit code land in the run.bat cmd window. On non-zero exit print a helpful checklist of common Windows launch failures and pause so the user can screenshot the output for an issue report. This doesn't fix the underlying crash for affected users, but it turns a ghost-crash bug into a self-diagnosing one so the next report includes actionable info. Closes-via-diag #7. === Fixes folded into the PR #9 merge === - src/scan_ips.rs: rand::thread_rng() held across an .await tripped the Send bound on the async fn. Scoped the rng in a block so it drops before the subsequent awaits. - src/scan_ips.rs: defend /0 and /32 CIDRs in cidr_to_ips and ip_in_cidr against 1u32 << 32 shift panic. All 36 unit tests pass. |
||
|
|
04661bfdec |
v0.7.1: Linux GLIBC 2.35 floor + scroll main window on short screens
Two user-reported issues.
=== GLIBC too new (reported via twitter) ===
Our linux-amd64 and linux-arm64 gnu builds were compiled on
ubuntu-latest (24.04, GLIBC 2.39), which means the resulting binaries
refuse to load on anything older:
./mhrv-rs: /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39'
not found (required by ./mhrv-rs)
Users on Ubuntu 22.04 / Mint 21 (GLIBC 2.35) — the typical user in Iran
where this project's target audience lives, and where they can't
dist-upgrade because they're behind exactly the kind of network
restriction this tool exists to bypass — could not run the gnu builds
at all.
Fix: pin the linux-gnu matrix entries to ubuntu-22.04 runners. GLIBC
2.35 is now the minimum; binaries load on Ubuntu 22.04, Mint 21,
Debian 12, Fedora 36+, RHEL 9+ and everything newer.
Users on older distros (Ubuntu 20.04, CentOS 7) can still use the
static musl builds (mhrv-rs-linux-musl-amd64.tar.gz et al.) which
have no GLIBC dependency at all.
=== Short-screen laptops — main window content clipped (PR #6) ===
Co-authored fix from @v4g4b0nd-0x76 in PR #6 (manually applied to
avoid pulling in 400 lines of unrelated cargo-fmt churn):
- Wrap the CentralPanel body in ScrollArea::vertical()
.auto_shrink([false; 2]) so everything stays reachable on short
screens.
- Lower the min_inner_size from [420, 540] to [420, 400] so laptops
with ~13" screens at default scaling can shrink the window without
clipping UI elements.
Closes #6.
Co-authored-by: v4g4b0nd-0x76 <v4g4b0nd-0x76@users.noreply.github.com>
|
||
|
|
bcdb3e7803 |
v0.7.0: editable SNI rotation pool with reachability probes
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.
|
||
|
|
0072b3aed9 |
v0.6.1: fix OpenWRT CA install + replace --user-less CI + perf pack artifacts
v0.6.0's release CI was cancelled before it could produce artifacts. This is a clean re-cut that also fixes a user-reported bug on OpenWRT. === OpenWRT CA install fix === User on issue #2 reported that --install-cert fails on an OpenWRT router with 'install failed on this platform'. Two problems: 1. Misclassification. The old distro detector did a substring-match over all of /etc/os-release, and OpenWRT's file contains lines like OPENWRT_DEVICE_ARCH=x86_64 and OPENWRT_ARCH=x86_64 — which contain the substring 'arch' — so we classified OpenWRT as Arch Linux and tried to install into /etc/ca-certificates/trust-source/ anchors/ (which doesn't exist there) and then run 'trust' (also missing). Predictable failure. 2. Even with correct classification, OpenWRT doesn't need the CA on the router itself. LAN clients are the ones terminating TLS through mhrv-rs's MITM; they're the ones that need to trust our root. The router is just forwarding packets. So emitting an error for the no-op case is misleading. Fixes: - Detect OpenWRT explicitly (/etc/openwrt_release marker file + ID=openwrt in os-release). - Rewrite the fallback os-release parser to look at ID / ID_LIKE token-wise instead of substring-matching the whole file. Added support for raspbian / rocky / almalinux / endeavouros while we're there. - For OpenWRT: install_linux returns Ok() with a clear message explaining that the CA needs to be installed on LAN clients, not on the router. No-op success instead of confusing error. - For unknown distros: the error message now points at the CA file path and the two most common anchor dirs so the user can install manually. - Extracted classify_os_release(&str) as a pure function and added 8 unit tests, including a regression guard with a real OpenWRT 23.05 os-release file so this specific substring-match bug can't return. === v0.6.0 perf pack (same as what cancelled CI was meant to ship) === - Connection pool pre-warm on startup (skip handshake on first request) - Per-connection SNI rotation across known Google-edge subdomains - Expanded SNI-rewrite suffix list (gstatic, googleusercontent, googleapis, ggpht, ytimg, blogspot, blogger) - Per-site stats tracker + UI drill-down table - Optional parallel script-ID dispatch (config field parallel_relay) - TCP_NODELAY audit + fix on SNI-rewrite outbound All 36 unit tests pass. Closes-via-fix #2 follow-up. |
||
|
|
3f0bbfdab0 |
v0.6.0: performance pack — pool prewarm, SNI rotation, per-site stats, parallel dispatch
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)
|
||
|
|
6c5b62e5e6 |
v0.5.1: static musl builds for OpenWRT (amd64 + arm64)
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. |
||
|
|
e575bf6bf4 |
v0.5.0: optional upstream SOCKS5 for non-HTTP traffic (Telegram et al.)
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). |
||
|
|
68532344d6 |
v0.4.4: multi-line Apps Script ID input (one per line)
Follow-up to v0.4.3 — users asked for one-ID-per-line instead of comma-separated, which is easier to edit in the UI. Field is now a 3-row textarea with a rolling hint underneath: - 0 or 1 IDs: 'Tip: add more IDs for round-robin with auto-failover' - 2+ IDs: 'N IDs — round-robin with auto-failover on quota' The parser accepts both newlines and commas as separators, so existing configs keep loading cleanly. Closes #1. |
||
|
|
d32c857502 |
v0.4.3: expose multi-script-ID support in the UI
The backend already supported a comma-separated list of Apps Script IDs with round-robin dispatch and automatic 10-minute sidelining on quota errors (429 / 403 / 'quota' in body). But the UI label just said 'Apps Script ID' (singular), so users didn't realize multiple IDs are allowed. - Rename the label to 'Apps Script ID(s)' - Hover tooltip explaining comma-separated input and quota auto-failover - Placeholder hint 'id1, id2, id3 …' - Live count line under the field when 2+ IDs are entered Requested by a user in Farsi: 'add the ability to set multiple script IDs; switch to the next when one hits its limit'. The feature existed, this just surfaces it. |
||
|
|
70d60f1951 |
v0.4.2: UI reads stats from the running proxy's fronter
The UI was creating its own DomainFronter instance and polling stats from it, while traffic actually went through the ProxyServer's own internal fronter. Result: stats grid stuck at zero even with traffic flowing. Fix: expose ProxyServer::fronter() and have the UI pick up that handle once the server is built, instead of constructing a parallel fronter. |
||
|
|
899ef06f4a |
v0.4.1: launcher scripts (run.sh / run.command / run.bat)
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. |
||
|
|
e4fe2b5939 |
v0.4.0: add cross-platform desktop UI (egui)
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.
|
||
|
|
c694073da8 |
Revert "v0.3.1: IP-literal destinations -> plain TCP passthrough (always)"
This reverts commit
|
||
|
|
eed64caf87 |
v0.3.1: IP-literal destinations -> plain TCP passthrough (always)
User reported log spam on Windows with many 'relay failed: خطای SSL' errors for IP-literal targets like 172.105.237.214:443. Root cause: xray/VLESS, torrent, SSH, and other app-level clients use raw IPs in CONNECT/SOCKS5 targets. Our previous logic would MITM these, see 'POST /' inside the xhttp wrapping, forward to Apps Script, which would then fail SSL-verifying the app's self-signed backend. New heuristic: if the CONNECT target is an IP literal, skip MITM entirely and do plain TCP passthrough. Reasoning: browsers never use raw IPs in CONNECT -- they always have a domain. Any client using an IP literal is using a custom protocol that we have no business MITMing. Effect: xray/VLESS tunnels now work through mhrv-rs SOCKS5 (the app's own TLS wrap passes through untouched). Browser HTTPS still MITM'd + relayed as before (domain CONNECTs). Also downgraded 'relay failed' logs from error to warn so they don't spam the ERROR channel on misrouted traffic. |