Commit Graph

350 Commits

Author SHA1 Message Date
therealaleph e81974c204 Revert "ci: post macOS/Linux/Windows/Android binaries as Telegram media group"
This reverts commit e9ce03e697.
2026-04-26 21:10:50 +03:00
therealaleph b18d9ab604 v1.7.2: ship Android config import/export (#266)
- mhrv-rs:// deep links, QR scanner, clipboard banner, share sheet
- DEFLATE-compressed base64 encoding (~200 chars vs ~800 raw)
- Every import path requires explicit user confirmation; the dialog
  shows the new deployment IDs and a trust warning so an attacker
  posting a malicious mhrv-rs:// link in a public channel can't
  silently overwrite a user's auth_key + script_ids
- ZXing for QR generation/scanning (no Google Play Services)

Closes #266. Thanks @yyoyoian-pixel — the rebase from auto-import
to confirmation-gated import is exactly the right shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.7.2
2026-04-26 20:26:05 +03:00
yyoyoian-pixel 1c9d288962 feat(android): config import/export — clipboard, QR, deep link, share (#266)
* feat(android): config import/export via clipboard, QR code, deep link, and share sheet

- Clipboard paste: banner auto-detects mhrv:// or raw JSON in clipboard,
  one tap to import. Clipboard cleared after successful import.
- Export dialog: QR code + compressed hash + copy button + Android share
  sheet (sends QR image + text together).
- QR scanner: ZXing embedded scanner in portrait orientation.
- Deep link: mhrv:// URIs auto-open the app and import the config.
- Compact encoding: only non-default fields included, DEFLATE compressed
  before base64. Accepts both compressed and raw JSON on import.
- ConfigStore.loadFromJson() deduplicated — shared by file load + import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: deep link requires confirmation, trust warning on import, mhrv-rs:// scheme

Security fix: deep link (mhrv-rs://) no longer auto-imports config.
Stashes decoded config for UI confirmation dialog — same flow as
clipboard paste and QR scan.

Import confirmation dialog now shows:
- Trust warning: "Importing routes your traffic through the deployment
  IDs in this config. Only import from trusted sources."
- Mode and deployment ID count with first 3 IDs previewed
- Explicit Import / Cancel buttons

Also:
- Renamed scheme from mhrv:// to mhrv-rs:// (less collision risk)
- Deduplicated import dialog into shared ImportConfirmDialog composable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 20:22:14 +03:00
therealaleph e9ce03e697 ci: post macOS/Linux/Windows/Android binaries as Telegram media group
The Telegram release notifier used to post just the universal APK with
a single-document caption. This change ships the per-platform binaries
for macOS (amd64+arm64 CLI), Linux (amd64+arm64 CLI), Windows
(amd64 UI), and Android (universal APK) as a single Telegram media
group with one caption listing every filename + SHA-256.

Workflow side (.github/workflows/release.yml):
- The telegram job now downloads ALL artifacts (was: APK only).
- New `Prepare files for Telegram media group` step extracts the raw
  binaries out of each per-platform .tar.gz / .zip (no archive
  wrappers in the channel) and renames them with version suffixes
  (mhrv-rs-linux-amd64-v1.7.2, mhrv-rs-windows-amd64-ui-v1.7.2.exe,
  etc.). Per-platform extraction is best-effort: a missing artifact
  emits a `::warning::` and skips that platform rather than failing
  the whole post.
- The post step builds a `--files <path>` arg list from tg-files/,
  sorted for deterministic order across runs, and invokes the
  notifier without --with-changelog (the script auto-replies with
  changelog whenever --files is used).

Script side (.github/scripts/telegram_release_notify.py):
- New --files arg (repeatable). 2..=10 files → sendMediaGroup; 1 file
  → sendDocument with the same caption shape; 0 → error. Telegram's
  sendMediaGroup rejects single-item groups, so the 1-file fallback
  isn't optional.
- New build_media_group_caption() composes title + per-file
  filename+SHA list + repo/release URLs. Fits ~860 chars for a 6-file
  release; fallback to filename-only-list if a future swell pushes
  past Telegram's 1024-char caption cap.
- send_media_group() handles the multipart/form-data shape with each
  file referenced as `attach://fileN` from the media JSON. Caption is
  attached to file 0 only (Telegram clients render per-item captions
  inconsistently for media groups; first-item-only is the safe
  pattern).
- Legacy --apk path kept for any caller that hasn't migrated; either
  --apk or --files must be present (validated at startup).
- _content_type_for() picks application/vnd.android.package-archive
  for .apk and application/octet-stream for everything else, so
  Telegram clients label the APK with the Android icon and label
  desktop binaries by filename without a misleading icon.

Behavioural change for users:
- The Telegram channel now sees one grouped post per release with all
  primary platform binaries inline, instead of just the APK. macOS
  users wanting the gatekeeper-friendly .app.zip still grab it from
  the GitHub Releases page; the Telegram drop is for the "give me
  the binary, I'll run it" path.
- The Persian/English changelog reply that used to be opt-in (via
  TELEGRAM_INCLUDE_CHANGELOG=true) is now automatic in the --files
  path because the per-file SHA list eats the caption budget that
  previously held the FA brief-note.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 20:12:00 +03:00
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>
v1.7.1
2026-04-26 19:25:32 +03:00
dazzling-no-more 1d14930887 feat(cert): add --remove-cert flag and Remove CA button for clean-slate revocation (#121)
* feat(cert): add --remove-cert flag and Remove CA button for clean-slate revocation

* fix(cert): testable euid-root branch + orphan enterprise_roots warning
2026-04-26 19:20:08 +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>
v1.7.0
2026-04-26 18:29:59 +03:00
dazzling-no-more 4b728058bd ci: add release-drafter + prepare-release for faster releases (#260) 2026-04-26 18:23:23 +03:00
dazzling-no-more 81e01d73c8 feat: shorten android home screen for long deployment-ID lists (#258) 2026-04-26 18:23:19 +03:00
yyoyoian-pixel 1057797109 feat: native udpgw without QUIC/DNS - QUIC/DNS with udp associate — stable VoIP, faster browsing - needs new tunnel deployment for udpgw (#222)
* feat: native udpgw protocol alongside existing UDP associate

Why udpgw is needed even with UDP associate:

UDP associate (udp_open/udp_data) creates one tunnel session per UDP
destination and polls each independently. On high-latency or shaky
networks this compounds — N simultaneous UDP flows need N separate
polling loops, each paying its own batch round-trip overhead. Google
Meet calls, which fire dozens of concurrent STUN + RTP flows, stall
or fail entirely because the per-destination polling can't keep up.

udpgw multiplexes ALL UDP over one persistent TCP-like session using
conn_id framing. One batch op carries frames for many destinations.
Persistent sockets per (conn_id, dest) with continuous reader tasks
keep source ports stable — critical for protocols like Telegram VoIP
and STUN that expect replies on the same port.

Both paths coexist — they serve different traffic:
  - UDP associate (SOCKS5): apps that negotiate SOCKS5 UDP relay
  - udpgw (198.18.0.1:7300): TUN-captured UDP (DNS, QUIC, Meet, etc.)

tun2proxy vendored as git submodule at v0.7.20 with one transparent
commit adding udpgw_server to the Android JNI run() function.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: block QUIC (UDP 443) and DNS (UDP 53) from udpgw

QUIC through udpgw is slower than TCP/HTTP2 through the batch pipeline
— blocking it forces browsers to fall back to TCP, improving YouTube
and general browsing speed.

DNS is better handled by tun2proxy's virtual DNS / SOCKS5 UDP associate
path which is more reliable for single request-response exchanges.

VoIP (Telegram, Meet) still flows through udpgw normally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace submodule with [patch.crates-io] for tun2proxy udpgw

Use the idiomatic Rust [patch.crates-io] mechanism instead of a git
submodule. Points to yyoyoian-pixel/tun2proxy fork with the udpgw
JNI parameter patch (upstream PR: tun2proxy/tun2proxy#247).

Will be removed once upstream ships the change in tun2proxy >= 0.8.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: pin tun2proxy patch SHA in Cargo.lock

Locks tun2proxy at dfc24ed1 so the patch resolution is recorded and
any branch rewrite is visible in the lockfile diff.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use AbortHandle for ConnSocket readers to prevent FD leaks

JoinHandle::drop detaches the task without aborting it. When
udpgw_server_task is cancelled (session close), the post-loop
cleanup never runs and per-(conn_id, dest) reader tasks become
zombies holding Arc<UdpSocket> file descriptors.

AbortHandle::drop aborts the task automatically, so cleanup is
correct by construction regardless of how the parent task exits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 17:08:45 +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>
v1.6.5
2026-04-26 16:58:49 +03:00
dazzling-no-more 75bec213de fix: route plain HTTP proxy requests direct in google_only mode (#256) 2026-04-26 16:52:36 +03:00
dazzling-no-more a028df8619 feat: added bulk parser for ids on android (#257) 2026-04-26 16:52:33 +03:00
dazzling-no-more b963851645 feat: added ability to copy logs in android (#255) 2026-04-26 16:52:31 +03:00
Parsa307 8b022c2b8d fix: add twitter.com (#245)
fix: include twitter.com in X.com URL normalization (#245)
2026-04-26 16:52:29 +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>
v1.6.4
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>
v1.6.3
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>
v1.6.2
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>
v1.6.1
2026-04-25 16:17:07 +03:00
Shin (Former Aleph) aa906e345f Merge pull request #187 from dazzling-no-more/fix/vpn-lifecycle-reliability
fix(android): tighten VPN session lifecycle reliability
2026-04-25 16:10:52 +03:00
dazzling-no-more 0102e5542d fix(android): tighten VPN session lifecycle reliability 2026-04-25 16:49:04 +04: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>
v1.6.0
2026-04-25 15:42:52 +03:00
Shin (Former Aleph) 52947e275f Merge pull request #183 from dazzling-no-more/feature/udp-support
Feature: udp support
2026-04-25 15:40:56 +03:00
dazzling-no-more 27a1aada40 fix(udp): skip session insert when udp_open returns eof 2026-04-25 16:19:24 +04:00
dazzling-no-more 39e8c53fba fix(udp): drop pre-parse client lock + bound session/task lifecycle 2026-04-25 16:19:24 +04:00
dazzling-no-more bf6fab31ab fix(udp): surface upstream eof + bound sessions and payload size 2026-04-25 16:19:23 +04: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>
v1.5.0
2026-04-25 11:56:41 +03:00
Shin (Former Aleph) 7efd12d8d3 Merge pull request #173 from dazzling-no-more/feature/event-driven-drain
feat(tunnel): event-driven drain with adaptive long-poll
2026-04-25 11:49:54 +03:00
dazzling-no-more 1d45dba2c2 feat(tunnel): event-driven drain with adaptive long-poll 2026-04-25 12:14:33 +04: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>
v1.4.1
2026-04-25 10:52:58 +03:00
therealaleph 1770920a71 fix(test): refuse Test Relay in full mode rather than silently apps_script (fix #160)
Issue #160 (deniz_us): Test Relay returned a Google-datacenter IP
even when mode=full and the user's tunnel-node was clearly working
in the browser. The reason: test_cmd::run unconditionally calls
fronter.relay() which is the apps_script-mode path; it doesn't go
through the tunnel mux at all. Result was a worst-of-both-worlds
silent fallback — looked like a successful Test, but the IP it
returned was nothing to do with the user's actual data plane.

Same shape of fix as the existing google_only branch: detect
Mode::Full and return a clear error explaining that Test isn't
wired for full mode, plus how to verify the tunnel-node manually
(whatismyipaddress.com via 127.0.0.1:8085) — at least until a real
full-mode test using the tunnel mux gets implemented.

Following up #160 to track the real fix as an enhancement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 10:51:22 +03:00
therealaleph fdc0405465 ci(release): heal root-owned target/ + always-chown after mipsel docker
Two related fixes for the self-hosted Linux jobs failing on
`actions/checkout@v4` with `EACCES: permission denied unlink
target/.rustc_info.json`:

1. Pre-checkout cleanup. The previous mipsel docker step left root-
   owned files in target/ when its `cargo +nightly build` failed —
   the post-step `sudo chown -R …` line was AFTER the docker run,
   and bash -e short-circuited on docker non-zero, so chown never
   executed. Once those root-owned files exist on the runner,
   every subsequent self-hosted job's `actions/checkout@v4` clean
   step fails because it can't unlink them. Add a guarded
   pre-checkout step that uses `sudo rm -rf` to clear root-owned
   leftovers from $GITHUB_WORKSPACE. Gated on
   `contains(matrix.os, 'self-hosted')` so GitHub-hosted runners
   (macOS, Windows) skip it — they get fresh VMs anyway.

2. Always-chown trap. The mipsel docker step now uses
   `trap '…chown…' EXIT` so the chown runs whether the inner
   `cargo build` succeeded or failed. A transient mipsel compile
   regression (tier-3 target, occasional std-build breakage) no
   longer poisons the runner's workspace for the next run.

Together: (1) recovers from the existing broken state on next run,
(2) prevents the same poisoned state from recurring.

Triggered for the v1.4.0 redeploy that's trying to publish the
missing mhrv-rs-openwrt-mipsel-softfloat artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:06:13 +03:00
therealaleph dc74a3fb55 ci(release): add workflow_dispatch with version input
The original `on: push: tags: 'v*'` trigger is the right primary path
— a new tag = a new release = a fresh workflow run. But it has a
failure mode: if one matrix job fails (e.g. mipsel-softfloat is
tier-3 and occasionally regresses) and we push a fix to main, we
can't move the immutable tag (tag protection rule), so the original
artifact stays missing from that release forever.

`workflow_dispatch` with a `version` input lets us re-run the workflow
from main against an existing release tag:

    gh workflow run release.yml --ref main -f version=1.4.0

The build matrix runs against the current main commit (which has
the fix), and the release-upload step uploads to the existing
v1.4.0 release page with the same filename pattern, alongside the
artifacts that succeeded on the original push.

Three call sites had to learn the new path: the macOS .app bundle
build, the Android APK rename step, and the Telegram caption builder.
All three previously did `VER="${GITHUB_REF#refs/tags/v}"`, which on
a workflow_dispatch from main produces "heads/main" — the new
two-line `VER="${{ inputs.version || github.ref_name }}"; VER="${VER#v}"`
pattern handles both: tag pushes get the bare version from
`github.ref_name` ("v1.4.0" → "1.4.0"), workflow_dispatch gets it
from the explicit input.

The release job's `softprops/action-gh-release@v2` step also needed an
explicit `tag_name` — without it the action defaults to `github.ref`,
which on workflow_dispatch is the dispatch ref (`refs/heads/main`)
and would try to create a release named "main". Now it's:

    tag_name: ${{ inputs.version && format('v{0}', inputs.version) || github.ref_name }}

Pair with `gh variable set TELEGRAM_NOTIFY_ENABLED --body false` before
dispatch when the re-run is for the same version (no new content for
the channel to see) — the Telegram job is already gated on that
variable, no per-run flag needed.

Used right now to publish the mhrv-rs-openwrt-mipsel-softfloat artifact
that failed on v1.4.0's original push, after commit febeeca fixed the
underlying compile error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 01:50:08 +03:00
therealaleph febeeca8a9 fix(tunnel-client): use portable_atomic::AtomicU64 for 32-bit MIPS
PR #153's connect_data instrumentation imported `AtomicU64` directly
from `std::sync::atomic`, which works on every release target except
the 32-bit MIPS OpenWRT target (`mipsel-unknown-linux-musl`) — that
platform has no hardware-backed 64-bit atomics, and Rust's std type
isn't even defined there. The v1.4.0 release workflow's mipsel build
failed with `error[E0432]: no AtomicU64 in sync::atomic`.

`domain_fronter.rs` already imports `portable_atomic::AtomicU64` for
the same reason — `portable-atomic` (already in Cargo.toml with the
"fallback" feature) provides a software-emulated 64-bit atomic on
targets that need it. Apply the same import here for the new
preread_* counter and connect_data_unsupported flag.

`AtomicBool` stays in std — it works on every target, no polyfill
needed.

Verified locally: cargo build + cargo test --lib (91/91 pass) clean
with the import change.

Same v1.4.0 — no version bump. Re-runs the release workflow to publish
the missing mhrv-rs-openwrt-mipsel-softfloat artifact alongside the
existing v1.4.0 release assets. Telegram notification suppressed for
this re-publish (same version, no point re-posting).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.0
2026-04-25 01:45:09 +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
Shin (Former Aleph) 00dfba354c Merge pull request #153 from dazzling-no-more/feature/connect_data
feat(tunnel): save one RTT per new HTTPS flow via connect_data op
2026-04-25 01:14:49 +03:00
Shin (Former Aleph) 4baefed850 Merge pull request #151 from freeinternet865/fix/range-parallel-total-cap
fix(relay): cap synthetic range stitching size
2026-04-25 01:14:16 +03:00
Shin (Former Aleph) 4890a3edb5 Merge pull request #150 from freeinternet865/fix/android-only-split-empty
fix(android): keep empty ONLY split lists self-excluded
2026-04-25 01:13:54 +03:00
dazzling-no-more 0a58943433 feat(tunnel): save one RTT per new HTTPS flow via connect_data op 2026-04-25 01:38:30 +04:00
freeinternet865 3f77790a9c fix(relay): cap synthetic range stitching size
Range-parallel relay trusted the origin's Content-Range total before planning remaining ranges and preallocating the stitched response buffer. A bad upstream could advertise an absurd total and force unbounded memory/CPU work. Bound synthetic stitching to 64 MiB and fall back to a normal single GET for larger totals, preserving correctness without returning a fake truncated 200 response.
2026-04-24 20:30:59 +00: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>
v1.3.0
2026-04-24 23:11:52 +03:00
freeinternet865 a148a7a1c6 fix(android): keep empty ONLY split lists self-excluded
When ONLY mode contains only this app or stale packages, no allowed application is successfully added. Android then treats the VPN as applying to every app, which can route more traffic than requested and can also include our own proxy traffic in the TUN path. Count successful addAllowedApplication calls and fall back to the existing ALL-mode self-exclusion behavior when none are usable.
2026-04-24 20:09:28 +00:00
therealaleph d73639a42c docs: acknowledge IP-exposure caveat for apps_script mode (fix #148)
@creep247 raised a fair concern: v1.2.9's forwarded-header stripping
handles the client-side leg (browser extensions / local proxies
inserting X-Forwarded-For before the request reaches Apps Script),
but it cannot cover whatever Google's infrastructure may add when
the Apps Script runtime's subsequent UrlFetchApp.fetch() hits the
target server — that leg is outside this client's control.

Added a paragraph to both the English and Persian "Security posture"
sections making the model honest:
  - what v1.2.9's stripping DOES cover (client-side added headers)
  - what it DOES NOT cover (Google's internal header chain on the
    fetch from Apps Script runtime → destination)
  - recommendation: users whose threat model requires the destination
    site cannot under any circumstances learn their IP should use
    Full Tunnel mode, which exits via the user's own VPS end-to-end

No code change — the privacy claim is narrower than a naive reading
of "v1.2.9 fixed the IP leak" might suggest, so the docs should say
so explicitly rather than let users over-trust the apps_script mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:59:04 +03:00
Shin (Former Aleph) 6d9bc2be6d Merge pull request #143 from dazzling-no-more/fix/split_tunneling_android
fix(android): app splitting ONLY mode don't mix allowed/disallowed apps
2026-04-24 22:57:48 +03:00
therealaleph cc04558437 android: publish per-ABI APKs in addition to universal (fix #136)
GitHub Releases is filtered from inside IR, and the 50 MB universal APK
is a bottleneck for users on slow/unstable censorship-tunnel paths that
can't reliably pull that much data. Per-ABI APKs are ~18–23 MB each —
small enough to succeed where the universal fails.

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:27:00 +03:00
dazzling-no-more 4ccd864e72 fix(android): app splitting ONLY mode — don't mix allowed/disallowed apps 2026-04-24 23:16:26 +04:00
Shin (Former Aleph) 7c102b4f63 Merge pull request #142 from vahidlazio/feat/per-deployment-concurrency
feat: per-deployment concurrency (30 req/account)
2026-04-24 22:14:40 +03:00
vahidlazio 2715991afa feat: per-deployment concurrency (30 req/account) instead of global pipeline depth
Apps Script enforces 30 concurrent executions per account. The old pipeline
used a single global semaphore sized to the number of deployments, meaning
1 in-flight batch per deployment. Now each deployment ID gets its own
semaphore with 30 permits — matching the actual per-account limit.

With N deployments the system can sustain 30×N concurrent batch requests
instead of N.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 21:10:06 +02:00
Shin (Former Aleph) 81d1883f45 Update README.md 2026-04-24 21:20:34 +03:00