86 Commits

Author SHA1 Message Date
therealaleph ae948f4075 v1.7.1: ship cert removal (#121)
mhrv-rs --remove-cert (CLI) and Remove CA button (UI) for verified
clean-slate revocation. Clears OS trust store, NSS browser stores
(Linux Firefox/Chrome), and the on-disk ca/ directory. config.json
and the Apps Script deployment are untouched.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:34:19 +03:00
therealaleph 2c8fcc75aa v1.6.3: fix Android notification SOCKS5 port mismatch (#211)
buildNotif() hardcoded `proxyPort + 1` for the SOCKS5 line, ignoring
cfg.socks5Port entirely. With the default Android config
(listenPort=8080, socks5Port=1081) the foreground notification read
"Routing via SOCKS5 127.0.0.1:8081" but the real listener was on 1081 —
so users configuring per-app SOCKS5 (Telegram, etc.) against the
notification value silently failed.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 17:14:00 +03:00
therealaleph 14e7dfc7d7 v1.6.1: Android VPN session lifecycle reliability (#187)
Five small but real Android-only fixes:

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:17:07 +03:00
therealaleph 1b22dce568 v1.6.0: end-to-end UDP support in Full Tunnel mode
Ships PR #183. SOCKS5 UDP ASSOCIATE → tunnel-mux udp_open/udp_data ops
→ tunnel-node UDP sessions → real UDP egress. QUIC/HTTP3, DNS, and
STUN now traverse the tunnel instead of falling back to TCP or
leaking outside it.

- 256-session-per-associate cap with FIFO eviction
- 9 KB datagram size guard (DNS/STUN tiny, QUIC max ~1452, leaves
  IPv6 PMTUD headroom without burning Apps Script quota on rogue
  traffic)
- Source-IP pinned to the control TCP peer; port locked to first
  parseable datagram (malformed datagrams from the right IP no
  longer DoS the legitimate flow)
- Event-driven UDP drain reusing v1.5.0's long-poll knobs

Backward compat: TunnelResponse.pkts is `Option<Vec<String>>` with
serde default; v1.5.0 clients hitting v1.6.0 tunnel-nodes ignore
the new field; v1.6.0 clients hitting v1.5.0 tunnel-nodes get
UNSUPPORTED_OP on udp_open and the existing fallback path takes
over (TCP-only). Apps Script CodeFull.gs is opaque to the new ops
— no redeploy needed; just doc-comment update.

98 lib tests + 22 tunnel-node tests pass (was 92 + 17 before).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 15:42:52 +03:00
dazzling-no-more 40c2b6c509 feat(udp): SOCKS5 UDP ASSOCIATE relay through full tunnel
Adds end-to-end UDP support: SOCKS5 client UDP ASSOCIATE → tunnel-mux
udp_open/udp_data ops → tunnel-node UDP sessions → real UDP to upstream.
QUIC/HTTP3, DNS, and STUN now traverse full mode without falling back to
TCP or leaking outside the tunnel.

Apps Script proxies the new ops opaquely through the existing batch
endpoint; CodeFull.gs only gets a doc-comment update.

Highlights:
- proxy_server.rs: SOCKS5 UDP ASSOCIATE handler with per-session task,
  bounded uplink mpsc channel, adaptive empty-poll backoff (500 ms → 30 s),
  source-IP validation against the control TCP peer, port-locking on
  first valid datagram, and self-removal from the dispatch map on eof.
- tunnel_client.rs: UdpOpen / UdpData / close_session mux variants
  alongside the existing TCP plumbing; pkts decoder helper.
- tunnel-node: UdpSessionInner with bounded VecDeque queue, drop-oldest
  on overflow with queue_drops counter and warn-then-throttled logs,
  last_active refreshed only on real activity (uplink send or upstream
  recv — empty polls do not refresh), independent TCP/UDP drain in
  handle_batch Phase 2, separate active-drain (150 ms) and retry
  (250 ms) windows for UDP, idle long-poll (5 s).
- Tests: SOCKS5 UDP packet parser (IPv4/IPv6/DOMAIN round-trips,
  truncation rejects, fragmented rejects), UDP queue overflow drop +
  counter, regression test that batch with both UDP and TCP-data ops
  still runs the TCP retry pass.

Docs: README + android.{md,fa.md} updated to reflect UDP availability
in full mode; tunnel-node/README documents the new ops.
2026-04-25 16:19:23 +04:00
therealaleph fb552c227d v1.5.0: long-poll Full Tunnel + Docker tunnel-node + brief FA release notes
Ships PR #173 (event-driven drain) plus three operational improvements:

PR #173 — long-poll tunnel mode. The tunnel-node's batch drain
switched from a fixed 150 ms sleep to an event-driven Notify wait;
idle sessions long-poll up to 5 s and wake on the first byte from
upstream. Push notifications and chat messages now arrive in roughly
RTT instead of waiting for the next client poll tick. Backward compat
with pre-#173 tunnel-nodes is automatic via a sticky AtomicBool that
detects fast empty replies and reverts to the legacy cadence.
92 client tests + 17 tunnel-node tests pass, including end-to-end
TCP-pair verification of the notify wiring.

Docker image for tunnel-node. Adds a hardened Dockerfile (BuildKit
cache mounts, non-root runtime user, ca-certificates for HTTPS
upstreams) and a .dockerignore to keep build context small. New
`tunnel-docker` job in the release workflow builds + pushes
multi-arch (linux/amd64 + linux/arm64) to
ghcr.io/therealaleph/mhrv-tunnel-node with `:latest`, `:1.5`, and
`:1.5.0` tags on every release. Setting up Full Tunnel mode goes
from "rustup + cargo build on a 1 GB VPS" (which fails on memory
half the time) to a one-liner. tunnel-node/README.md updated with
prebuilt-image + docker-compose recipes.

Brief Persian release note in Telegram caption. The release-post
caption now leads with a `<blockquote>`-wrapped FA bullet headlines
extracted from `docs/changelog/v<ver>.md`, above the existing two
links (repo + release). Markdown links → Telegram HTML <a> for
clickability. Cap-budget-aware truncation at bullet boundaries
keeps total caption under Telegram's 1024-char limit. Headlines-only
rather than full bullets so multiple "what's new" items fit
comfortably (the full bullets remain on the GH release page and as
the optional --with-changelog reply-threaded message).

GitHub Releases page bodies now lead with the changelog content
(Persian section + `---` + English) instead of just a Full Changelog
comparison link. The auto comparison link is appended at the bottom
via `append_body: true` rather than removed.

Workflow changes:
- New `permissions: packages: write` at the workflow level (required
  for ghcr push via docker/login-action).
- New `tunnel-docker` job needs `build` (not the full matrix) to
  serialize the QEMU buildx layer with the matrix cache.
- Release job composes the body from `docs/changelog/v${VER}.md`
  in a pre-step that handles both tag-push and workflow_dispatch
  paths (uses inputs.version || github.ref_name like the rest of
  the workflow).

Tested locally:
- `cargo test` — 92 lib tests pass
- `cargo test -p mhrv-tunnel-node` — 17 tests pass
- `docker build` of tunnel-node Dockerfile — 32 MB image, runs as
  non-root, /health returns "ok", auth rejection works correctly,
  legitimate requests open sessions to remote hosts
- Telegram script `--dry-run` mode added; rendered captions for
  v1.4.0, v1.4.1, v1.5.0 all fit under 900 chars

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:56:41 +03:00
therealaleph 17addeda06 v1.4.1: Test Relay aware of Full mode + ship missing mipsel artifact
Patch release covering:

- #160 (deniz_us): Test Relay returned Google datacenter IPs even in
  Full Tunnel mode, because test_cmd::run unconditionally used
  fronter.relay() (apps_script path) regardless of configured mode.
  The user's tunnel-node was actually working — whatismyipaddress.com
  in their browser showed the correct VPS IP — but Test Relay
  contradicted it. Now Test Relay refuses cleanly in Full mode with
  a clear message and points users at the right verification path.
  A real Full-mode test through the tunnel mux is enhancement-tracked.

- mhrv-rs-openwrt-mipsel-softfloat artifact lands natively (commit
  febeeca). v1.4.0 had a build break on the 32-bit MIPS target due to
  PR #153's std::sync::atomic::AtomicU64 import — switched to
  portable_atomic::AtomicU64 which is already the project's
  convention for that reason. The artifact was hot-published to the
  v1.4.0 release page via workflow_dispatch yesterday; v1.4.1 ships
  it the normal way.

91 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:52:58 +03:00
therealaleph 8221d4280b v1.4.0: connect_data RTT win, Android ONLY-mode edge case, range-cap
Rolls up #150 + #151 + #153 (all merged on main) into a tagged release.

Highlights:

- Full Tunnel mode opens new HTTPS connections ~500-2000 ms faster
  (#153). The new connect_data tunnel op bundles the client's first
  bytes (typically TLS ClientHello) with the CONNECT call, eliminating
  one full Apps Script round trip per new flow. Backward compat is
  handled via UNSUPPORTED_OP detection + sticky AtomicBool fallback +
  pending_client_data replay so older deployments keep working without
  byte loss. New `connect_data preread: X win / Y loss / Z skip`
  metric in logs lets us measure the win ratio empirically.
- Android ONLY-mode split fix (#150): when the allow-list contained
  only mhrv-rs or stale uninstalled packages, every
  addAllowedApplication call silently failed and Android applied the
  TUN to every app — looping our own proxy traffic. Now we count
  successful adds; if zero, we fall back to ALL-mode self-exclusion.
  Complements PR #143 which fixed the empty-list case.
- Memory-safety cap on relay range stitching (#151): a hostile or
  buggy origin could advertise an absurd Content-Range total
  (e.g. 10 GiB) and force range-parallel to plan millions of chunks
  and preallocate a huge stitched buffer. Now capped at 64 MiB; larger
  totals fall back to a normal single GET.

Tests: 91 lib tests pass (was 82; +1 from #151, +8 from #153).
Tunnel-node: 6 tests pass (all new from #153).

Local Android build verified — universal + four per-ABI APKs all
produced at expected sizes (universal 53 MB, arm64-v8a 21 MB,
armeabi-v7a 18 MB, x86_64 23 MB, x86 22 MB). Installed on mhrv_test
emulator, app launches and renders correctly with v1.4.0 in title.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:21:05 +03:00
therealaleph 5bb26a4961 v1.3.0: per-deployment concurrency, ABI-split APKs, ONLY-mode fix
Rolls up the four post-v1.2.14 commits on main into a single tagged
release. Highlights:

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:11:52 +03:00
therealaleph d01e9f0f2f v1.2.14: Usage today (estimated) card + Persian guide localization
Adds a daily-budget visualization for users worried about hitting the
Apps Script free-tier quota (20,000 UrlFetchApp calls/day).

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:15:52 +03:00
therealaleph a05fc5d612 v1.2.13: Android Full Tunnel Mode requires credentials (fix #124)
Bug fix release. My v1.2.12 merge of Mode::Full bypassed the
deployment-ID + auth-key check on Android, but Full mode talks to
CodeFull.gs on Apps Script and needs those same credentials.

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

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

Android-only diff; Rust side is byte-identical to v1.2.12.
2026-04-24 17:16:09 +03:00
therealaleph 9e2b8e5f3e v1.2.12: Full Tunnel Mode (#94 merged)
Rollup of PR #94 — Mode::Full dispatch + batch tunnel client. Ships
the long-awaited no-MITM path that was the motivating fix for half
the open issues this week.

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

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

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

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

Previous releases still advertised the URL-truncation fix as working
but it only matched exact Host: x.com, which never happens in real
traffic. v1.2.11 widens the matcher to x.com + *.x.com so www.x.com,
api.x.com, and any future x.com subdomain all get the shortened URL
through Apps Script's URL length cap.
2026-04-24 10:01:46 +03:00
therealaleph 29777ce5b2 v1.2.10: proxy Stop actually stops now (fix issue #99)
Single-focus release. The Stop button in the UI previously only
stopped new connections from being accepted — in-flight clients kept
running on the old DomainFronter, which meant:

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

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

Finding 1 of #99 (quota-exceeded → "timeout" instead of the real
502 body) is a separate pool-staleness issue and is NOT addressed
in this release.
2026-04-24 04:33:29 +03:00
therealaleph 1d2eb19295 v1.2.9: fix UI build for youtube_via_relay (v1.2.8 CI abandoned)
v1.2.8 tagged cleanly but CI failed compiling mhrv-rs-ui with:

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

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

Fixed here:

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

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

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

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

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

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

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

73 tests pass (up from 67 — three new chunked-decode tests + one
XFF-filter + two youtube_via_relay branches).
2026-04-24 01:58:25 +03:00
therealaleph 0a29cf0740 v1.2.7: SNI cert fix mirrored to Android + tunnel-node scaffold (via #92 + #93)
- Android DEFAULT_SNI_POOL: mirror the Rust-side fix from #92 —
  accounts.googl.com replaced by accounts.google.com. Same cert-SAN
  mismatch that was failing every Nth rotation in the Rust client
  affected the Android user's sniHosts population; both pools need
  to stay in sync by design.

- Release rolls up PR #92 (cert fix) and PR #93 (tunnel-node +
  CodeFull.gs scaffolding). PR #93 adds a standalone binary under
  tunnel-node/ plus an Apps Script companion; no main-crate changes,
  so this is a zero-risk merge. Users who want to deploy a tunnel
  node can start today. The dispatch that activates `mode: full` is
  still in review in PR #94.
2026-04-23 23:38:10 +03:00
therealaleph 658e72fe0d v1.2.6: rust-cache bin pruning fix + PR #83 scan-sni
v1.2.4 and v1.2.5 both cut clean tags but CI failed downstream for
different self-hosted reasons:

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

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

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

Same user-facing code as v1.2.4/v1.2.5 (PRs #78, #79, README Android
note) plus PR #83 and the CI fixes on top.
2026-04-23 21:22:17 +03:00
therealaleph af44abbcd3 v1.2.5: CI self-hosted apt-lock fix (v1.2.4 release was incomplete)
v1.2.4 tagged cleanly but its CI failed — parallel Linux matrix jobs
on the self-hosted runners all raced on `/var/lib/apt/lists/lock` and
failed the `sudo apt-get install` step within ~20s. v1.2.4's release
job therefore skipped and no assets were published.

Fix:

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

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

Same code changes as what v1.2.4 was meant to ship: PR #78 range-
parallel validation, PR #79 port-collision rejection, README note
on Android 7+ user-CA trust.
2026-04-23 21:06:25 +03:00
therealaleph cb07311cf1 v1.2.4: range-parallel validation + port-collision guard + README Android note
- PR #78: validate Content-Range on 206 responses in the range-parallel
  path before stitching. Prevents malformed partials from being combined
  into a fake 200 OK. Invalid probe falls back to a normal single GET;
  invalid later chunks fall back to the validated probe response
  instead of shipping truncated/wrong data.

- PR #79: reject configs with listen_port == socks5_port at validation
  time (both config-load and UI form) instead of letting the second
  bind fail at runtime with a less clear error.

- README: add an explicit note about the Android 7+ user-CA trust
  limitation so future reporters (#74, #81, and the next dozen) find
  the answer in the docs instead of in a support thread. The previous
  "every app routes through the proxy" line was misleading — TUN
  captures all IP traffic but HTTPS still needs app-level trust of
  our MITM CA, which most non-browser apps don't grant.

Running through the new self-hosted CI pipeline. Warm rust-cache should
bring the full matrix in under ~7 minutes.
2026-04-23 20:52:06 +03:00
therealaleph 15e3e38745 v1.2.3: move CI to self-hosted runners + rust-cache
Linux / Android / mipsel build jobs now run on two self-hosted runners
on a Hetzner 8-core / 31 GB Ubuntu 24.04 box with Rust, Android SDK+NDK
r26c, all cross-compile toolchains and Docker pre-installed. macOS and
Windows still run on GitHub-hosted — we don't self-host those OSes and
the free minutes on a public repo are plenty.

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

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

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

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

- chromewebstore.google.com added to DEFAULT_GOOGLE_SNI_POOL and
  DEFAULT_SNI_POOL (#75). Same family as the rest of the pool —
  wildcard cert, GFE-hosted.
2026-04-23 19:49:40 +03:00
therealaleph 1d5d13d63d v1.2.1: IP-literal fast-fail + more SNIs + x.com GraphQL fix + Android SNI paste
Rollup of the three upstream-Python ports plus an Android UX polish:

- plain_tcp_passthrough: 4s connect timeout for IP literals (10s for
  hostnames). Halves Telegram DC-rotation latency when the current DC
  is DPI-dropped.
- DEFAULT_GOOGLE_SNI_POOL / DEFAULT_SNI_POOL: +maps, chat, translate,
  play, lens.google.com. More fingerprint spread, and maps/play pass
  DPI on some carriers where shorter *.google.com names don't.
- handle_mitm_request: x.com GraphQL URL truncation — strip everything
  after the first & when the path matches /i/api/graphql/.../?variables=.
  x.com's variables+features+fieldToggles blob overflows Apps Script's
  URL cap; `variables=` alone renders the timeline.
- Android SNI editor: paste-and-add now accepts a full list separated
  by whitespace / commas / newlines, dedupes, and merges with existing
  selection. Closes the "add them all at once" ask from #47.
- rlimit.rs: fence the example error log in a `text` code block so
  rustdoc stops trying to compile it.
2026-04-23 16:44:09 +03:00
dazzling-no-more 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.
2026-04-23 15:28:47 +03:00
Shin (Former Aleph) 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).
2026-04-23 10:45:00 +03:00
therealaleph d51719b7b5 ci: auto-post CI-built Android APK to Telegram on every release tag
New `telegram:` job in release.yml downloads the Android artifact
uploaded by the `android:` job, posts the APK with a short caption
(Telegram caps captions at 1024 chars, we blow past that), then
replies with the full changelog in two quote blocks — Persian first,
English second — matching the format the user wants.

Changelog content lives in `docs/changelog/v<tag>.md`. The file has
a comment header explaining the format, then:
  - Persian bullets
  - a bare `---` separator line
  - English bullets
The workflow splits on that separator. No emojis. Missing changelog
file = the reply is skipped (doc post still lands).

Telegram credentials come from repo secrets:
  TELEGRAM_BOT_TOKEN  (set)
  TELEGRAM_CHAT_ID    (set)
Missing either = job logs a notice and returns 0. A forker who hasn't
set up Telegram gets a clean release with no notify attempt.

Also includes v1.1.0's changelog file so the first run of this job
has something to post.
2026-04-23 09:52:41 +03:00
Shin (Former Aleph) be698f4928 docs(android, fa): Persian translation of Android guide + README section (#35)
Mirrors docs/android.md for Persian-speaking users who land on the
Persian half of the README. Same structure — TOC, requirements table,
six-step setup, UI reference, known limitations, troubleshooting
table, log-collection snippet — rewritten in Persian RTL.

README's Persian section gets a new "اجرا روی اندروید" subsection
right before the FAQ, with a five-step quickstart and links to the
full Persian and English Android docs. The English preamble
(above the fold) also gains a فارسی link next to the English
Android-doc reference so bilingual readers see both options
immediately.

No code touched. Cross-references only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 04:42:33 +03:00
Shin (Former Aleph) 63a397cca8 docs(android): rewrite install guide for clarity + v1.0.2 accuracy (#34)
The old version landed each release cycle with a different "step 5
says to open Security settings but the code now opens top-level
Settings" kind of drift. Swept the whole page and rebuilt it around
what the app actually does on v1.0.2:

* adds a table of contents at the top — the guide is now scan-first
* requirements moved into a table so the phone/SDK/quota constraints
  are all visible at once
* Apps Script deploy step uses a table for the New-deployment form
  fields (less prose to read)
* step 4 (SNI tester) explains each possible row outcome in a table,
  with the concrete "tap Auto-detect" action for the common failure
* step 5 (MITM CA) now matches the v1.0.2 flow: top-level Settings
  app + search "CA certificate", not a Security-settings deep-link.
  Search is more portable across Pixel/Samsung/Xiaomi than naming
  the menu path
* new "UI quick reference" table mapping each control to what it does
  — helps users who skipped the setup prose
* Known limitations tightened: Cloudflare Turnstile loop explained
  with the (IP, UA, JA3) binding table; IPv6 leak, UDP/QUIC,
  per-script quota, and the Android-7+ user-CA opt-out all kept
* Troubleshooting is now a single table with symptom → cause → fix
  columns, including the INSTALL_FAILED_UPDATE_INCOMPATIBLE one-time
  note for the v1.0.1 → v1.0.2 upgrade path
* new "Collecting a useful log" section: one copy-pasteable
  adb logcat command that captures the tags that matter
  (MhrvVpnService, mhrv_rs, mhrv-crash, tun2proxy)
* removed the stub Persian section at the bottom — it said
  "file an issue if you want a translation" which is noise;
  re-adding only if someone actually asks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 04:28:28 +03:00
Shin (Former Aleph) 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>
2026-04-23 04:19:52 +03:00
Shin (Former Aleph) 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>
2026-04-23 02:56:39 +03:00
therealaleph 40b008cde3 docs: strip EXIF chunk from UI screenshot
The PNG carried an eXIf chunk (image dimensions — no GPS/camera/author
data, but metadata nonetheless). Re-encoded keeping only image-critical
chunks (IHDR, sRGB, IDAT, IEND).
2026-04-21 22:39:50 +03:00
therealaleph 54d317ae2c docs: add UI screenshot + releases/ folder with explainer
- docs/ui-screenshot.png: running UI with live traffic stats
- releases/README.md: documents the in-repo prebuilt binaries for users
  who cannot reach the GitHub Releases page (English + Persian)
- README: embed the screenshot in the 'What's in a release' section
2026-04-21 22:37:42 +03:00