The arm64 multiarch apt setup was failing because the Azure mirror on
GitHub runners only serves amd64. Since we don't build the UI for
aarch64-linux (server target), we only need the cross-compiler itself,
which is amd64-native. Drop the arm64 system libs and the best-effort
UI build step.
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.
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.
Ports the SOCKS5 + fallback-chain design from @masterking32's
MasterHTTP-WithSOCKS branch so xray / Telegram / app-level TCP
clients work through this proxy.
Changes:
- New SOCKS5 listener on listen_port+1 (configurable via socks5_port)
- RFC 1928 CONNECT handshake (v5, no-auth, ATYP IPv4/domain/IPv6)
- Shared smart dispatch with the HTTP-CONNECT path
- Unified dispatch_tunnel() used by both CONNECT entry points:
1. If host matches SNI-rewrite suffix or hosts override: go direct
to google_ip via the MITM+TLS tunnel (fast path for google.com,
youtube, etc.)
2. Peek the first byte (300ms timeout for server-first protocols):
- 0x16: TLS client hello -> MITM + relay via Apps Script (scheme=https)
- HTTP method signature: HTTP relay via Apps Script (scheme=http)
- Anything else or timeout: plain TCP passthrough to the target
- handle_mitm_request() now takes a scheme arg (http/https) so the
same code path handles both MITM'd HTTPS and port-80 plain HTTP
- New plain_tcp_passthrough helper: bidirectional TCP bridge used as
the final fallback (covers MTProto / raw TCP / server-first protos)
Config:
- Added optional socks5_port field; defaults to listen_port+1
README:
- Added browser vs xray/Telegram instructions under 'Step 6'
Live-tested: HTTP proxy, HTTP proxy -> HTTPS, SOCKS5 -> HTTP,
SOCKS5 -> HTTPS, Google search via SNI-tunnel (now returns full
JS page) all pass.
Context: user reported Google search showing no-JS fallback page
('JS is off apparently'). Root cause is Apps Script's fixed
'Google-Apps-Script; beanserver' User-Agent that UrlFetchApp.fetch
does not let you override. Google detects the bot UA and serves
the degraded HTML.
Fix: add google.com to SNI_REWRITE_SUFFIXES so google.com requests
bypass Apps Script entirely and go direct to Google's edge via the
MITM+TLS tunnel. Real browser UA is sent; full JS version is served.
Also documented this and other inherent limitations (WebSockets,
2FA 'unknown device', video chunk slowness, brotli stripping) in
the README under 'Known limitations' in English + Persian so users
aren't surprised. These are platform limits of Apps Script, not
bugs -- same issues exist in the original Python project.
Two bug fixes surfaced in user testing:
1. Invalid HTTP methods forwarded to Apps Script
- Browser/xray sent HTTP/2 PRI preface through our MITM despite ALPN
being set to http/1.1 only (some clients ignore ALPN).
- Our parser accepted 'PRI' as a method and forwarded to Apps Script,
which rejected it: 'Exception: parameter provided with invalid value: method'.
- Fix: validate method against the standard list (GET/POST/PUT/DELETE/
HEAD/OPTIONS/PATCH/TRACE/CONNECT) at parse time. Non-matching requests
close the connection cleanly instead of forwarding garbage.
2. YouTube video playback broken by over-broad SNI-rewrite list
- Previous list included googlevideo.com, ytimg.com, doubleclick.net,
etc. -- but these are served from SEPARATE CDN pools, NOT from
Google's 216.239.38.120 frontend. Rewriting sent traffic to the
wrong backend, which Google dropped.
- Shrunk to a conservative list that's actually served from the
main Google frontend: youtube.com, youtu.be, youtube-nocookie.com,
fonts.googleapis.com. Everything else falls through to MITM+relay
(slower but actually works).
- YouTube video chunks now route through Apps Script which is slow
and quota-limited. This is a known limitation inherent to the
approach; same issue exists in the original Python version.
test: one-shot end-to-end probe. Issues a GET to api.ipify.org through
the configured relay and prints status + body + timing. Clear pass/fail
with specific diagnostics for 502/504 (auth_key mismatch, quota, etc).
Verified live: 3.8s round-trip returning the caller's real IP.
scan-ips: parallel TLS probe of 28 known Google frontend IPs with
SNI=front_domain. Reports which are reachable and sorts by latency.
Users pick the fastest and paste into google_ip. Verified live:
7/28 reachable (the others were Windscribe'd out), top 3 ranked.
Both subcommands share the existing config.json and require no extra
flags. Default 'mhrv-rs' with no subcommand runs the proxy as before.
Tracks relay_calls, failures, bytes, coalesced requests, cache hit rate,
and active scripts (total minus blacklisted). Logs only if there's been
traffic since the last tick. Visible when running with RUST_LOG=info or
log_level=info in config.
After the OS trust store install, also try to add the MITM CA to all
discovered Firefox profiles via NSS certutil. Silently no-ops if:
- NSS certutil is not installed (macOS ships a different certutil; linux
needs libnss3-tools; Windows needs NSS binaries)
- No Firefox profiles exist
- Firefox is currently running (lock on cert.db)
Scans profiles in:
- macOS: ~/Library/Application Support/Firefox/Profiles
- Linux: ~/.mozilla/firefox and ~/snap/firefox/common/.mozilla/firefox
- Windows: %APPDATA%\Mozilla\Firefox\Profiles
Existing CA-install error path is unchanged; this is purely additive.
SNI-rewrite tunnels (src/proxy_server.rs):
- CONNECT to youtube.com / googlevideo.com / doubleclick / etc. now bypasses
the Apps Script relay entirely and goes direct to the Google edge IP
with SNI=front_domain.
- Accepts browser TLS with our MITM cert, opens outbound TLS to
config.google_ip with SNI=config.front_domain, bridges decrypted bytes.
- Matches Python's _do_sni_rewrite_tunnel behavior. Faster than relay for
large streams (video).
- Also respects config.hosts override map (custom IP per suffix).
gzip decode fix (src/domain_fronter.rs):
- Apps Script outer response is gzipped. Previous stub always failed,
causing 'non-utf8 json' errors. Swapped in flate2::GzDecoder.
- Verified end-to-end: HTTP and HTTPS requests through apps_script
relay succeed and return real Google IPs.
When a script returns 429, 403, or a quota/rate-limit error body,
drop it from the active rotation for 10 minutes. next_script_id
skips blacklisted IDs; if all are blacklisted, picks the one
coming off cooldown soonest.
Script IDs are masked in logs (prefix...suffix) to avoid leaking
the deployment ID even at info level.
First concurrent caller for a cache key does the relay; subsequent
callers subscribe to a broadcast channel and receive the same response.
Only applies to cacheable (GET/HEAD) requests without body.
- New cache.rs: FIFO-eviction cache with max_bytes cap
- Cacheable: GET/HEAD only, no-store/no-cache/private/Set-Cookie reject
- TTL from Cache-Control: max-age=, or heuristics by extension (css/js/fonts/images -> 1h)
- Hook in DomainFronter::relay: check cache before network, store after 2xx
- 10 new unit tests (23 total)