Patch release for the Win7 i686 binary fix shipped via #323.
No code changes; CI workflow change only — Cargo, Gradle, and
changelog bumps in lockstep so the release produces a fresh
i686 Windows binary built against Rust 1.77.2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(release): pin i686-pc-windows-msvc to Rust 1.77.2 for Win7 compat
Fixes#318. Rust 1.78 (May 2024) raised the std MSRV for Windows from
Win7 to Win10 by switching std::time to GetSystemTimePreciseAsFileTime,
a kernel32 export that doesn't exist on Win7 SP1. Building the i686
binary with stable Rust (currently 1.86+) produces an exe that fails
to load on Win7 with "the procedure entry point
GetSystemTimePreciseAsFile could not be located in the dynamic link
library kernel32.dll" — making the whole reason we ship i686 (legacy
Win7 32-bit boxes per #272) moot.
Add a per-matrix `rust_toolchain` knob; only i686-pc-windows-msvc uses
it, pinning to 1.77.2 (last stable that supports Win7). Other targets
remain on @stable and pick up regular Rust updates.
dtolnay/rust-toolchain switches from `@stable` to `@master` because
the per-tag aliases (`@stable`, `@1.77.2`) can't be selected via a
matrix variable — `@master` accepts the toolchain string as input.
Cache key gains a toolchain suffix so the 1.77.2 cache doesn't collide
with the stable cache for the same target, and a future toolchain bump
invalidates only the affected slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(release): make i686-pc-windows-msvc continue-on-error
Companion to the Rust 1.77.2 pin: if the deps' MSRV ever moves above
1.77, the i686 target will fail to build, but we don't want it to
block the rest of the release. Mirror the mipsel-softfloat approach.
If/when this triggers, options are dropping i686 entirely or moving
to the tier-3 i686-win7-windows-msvc target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
Patch release for the auto-blacklist of timeout-saturated deployments
shipped via #319. No new features; bugfix only — cargo, gradle, and
changelog bumps in lockstep so the release workflow can ship matching
artifacts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
The v1.7.7 tag commit (6885800) only updated the changelog; the
version field edits failed earlier due to file-state-changed-mid-edit
race. Fixing forward — Cargo.toml + build.gradle.kts now show 1.7.7
properly.
Workflow will build from main HEAD on workflow_dispatch, so the
v1.7.7 release-page artifacts will have the correct internal version
even though the tag commit itself doesn't include the version bump.
- #288 (@amiralishoja): adds i686-pc-windows-msvc to the release
matrix. 32-bit Windows users get mhrv-rs-windows-i686.zip on
every release.
- #290 (@dazzling-no-more): per-deployment longpoll fallback state
with TTL-based auto-recovery. Replaces a global AtomicBool that
one degraded deployment could permanently flip. Now the aggregate
legacy gate only fires when every configured deployment is marked,
and self-corrects on TTL expiry — upgraded tunnel-nodes rejoin
the fast path automatically. 4 new tokio::test virtual-time tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-committed by release workflow so users behind GitHub-Releases-page filtering can download via the in-repo releases/ folder. The GitHub Release page itself still has the canonical versioned artifacts; this folder is the fallback path for users who can only reach the static source tree (Code → Download ZIP).
The commit-releases job's `actions/download-artifact@v4` step has
failed twice in a row (v1.7.5 retrigger, v1.7.6) with the same
shape: ~10 artifacts download successfully, then "Unable to
download artifact(s): Artifact download failed after 5 retries" on
the 11th-13th. The 10 that complete print their SHA256 digests
cleanly; the failure is unambiguously inside actions/download-
artifact, not on our side.
Workaround: pull from `gh release download` instead. The `release`
job populated the GitHub Release page a few seconds earlier with
the same artifacts; pulling from there reads from a different
CDN (Release-page blob store) with different retry / rate-limit
characteristics. Empirically more reliable for our 13-artifact
release size.
Filtered to *.tar.gz / *.zip / *.apk so we only fetch the user-
facing artifacts (skipping anything like checksum sidecars that
softprops/action-gh-release@v2 might add later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.7.4 added googlevideo.com to SNI_REWRITE_SUFFIXES on the theory
that video chunks should bypass the Apps Script relay. Multiple
users (#275 amirabbas117, #281 mrerf) reported total YouTube
breakage on v1.7.4: SNI-rewriting googlevideo.com:443 to a GFE IP
returned TLS handshake failure / wrong-cert error.
Root cause: googlevideo.com is served by Google's separate "EVA"
edge IPs, not the regular GFE IPs that the user's `google_ip`
typically points at. The SNI-rewrite tunnel TLS handshake against
a GFE IP for googlevideo.com SNI fails because the GFE IP doesn't
hold a googlevideo.com cert.
Pre-v1.7.4 behaviour restored: video chunks fall through to the
Apps Script relay path. Slower but reliable on every GFE IP.
The other v1.7.4 youtube_via_relay carve-out fixes (ytimg.com
correctly stays on SNI rewrite, youtubei.googleapis.com correctly
goes through relay) remain intact — those were a separate
improvement and still correct.
Future: if we want direct googlevideo.com routing, it needs a
separate `eva_edge_ip` config knob — users can populate from their
own EVA scan, defaulting to "use relay" if not configured.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.7.5's block_quic config field broke the UI binary build because
src/bin/ui.rs constructs Config{} explicitly and I forgot to add the
new field there. CLI binary loads from JSON via serde so it didn't
trip — only the 4 UI-building targets failed (linux-amd64-gnu,
windows-amd64, macos-amd64, macos-arm64).
block_quic is round-tripped through the form (config-only for now,
no UI control) so save doesn't drop a user-set true.
- Adds `block_quic = true` config flag for client-side QUIC drop.
SOCKS5 UDP relay refuses UDP/443 datagrams; browsers fall back to
TCP/HTTPS through the relay. Opt-in. Thanks @w0l4i
- Workflow now auto-refreshes the in-repo releases/ folder on each
release tag, so Iranian users behind GitHub-Releases-page filtering
can download via Code → Download ZIP. Practice was started before
v1.1.0 then dropped; resumed at user request.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resume the practice (dropped after v1.1.0) of committing prebuilt
binaries to the repo's releases/ folder. Iranian users behind state
network filtering frequently can't reach the GitHub Releases page
(/releases/tag/...) but CAN reach the static source tree via
Code → Download ZIP — that pulls the in-repo releases/ folder along
with the source. Telegram channel feedback explicitly requested
this be resumed.
The new commit-releases job:
1. Runs after release+build+android succeed.
2. Wipes existing binary artifacts from releases/ (.apk, .tar.gz,
.zip) but preserves README.md and .gitattributes.
3. Copies all desktop archives (which already have stable
platform-suffixed names like mhrv-rs-linux-amd64.tar.gz).
4. Copies all per-ABI Android APKs (so users on slow connections
can grab the ~37 MB arm64-v8a APK instead of the ~110 MB
universal).
5. sed-updates the "Current version" line and APK filename refs
in releases/README.md (both English and Persian copies).
6. Commits as github-actions[bot] and pushes to main.
The GitHub Release page itself keeps the canonical versioned
artifacts as before — this in-repo folder is the fallback for
users who can't reach that URL.
Tag protection rules don't apply to refs/heads/main so the push
isn't gated. release-drafter.yml triggers on push-to-main but only
updates the next-release draft, no cycle risk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
w0l4i has been asking for client-side QUIC block since #213. Now
implemented as a small config flag.
When `block_quic = true`, the SOCKS5 UDP relay drops any datagram
destined for port 443 — that's HTTP/3-over-UDP. The client's QUIC
stack retries a couple of times and then falls back to TCP/HTTPS
through the regular CONNECT path (which goes through the relay
normally).
Why client-side rather than server-side udpgw block: the udpgw
block in #222 is bound to Full mode + Android tun2proxy. This
covers everyone — apps_script users, desktop, Full mode, all the
same path. Skipping at the SOCKS5 layer rather than the tunnel-node
layer also avoids paying 200–500 ms tunnel-node round-trip per
QUIC datagram drop, which compounds during browser retries.
Silent drop is the contractually correct shape: SOCKS5 UDP wire
has no `host unreachable` reply (RFC 1928 §6 only defines that for
TCP CONNECT). Browsers' QUIC stacks have a "no response → fall
back" timeout, so silent drop matches what the protocol expects.
Default false (opt-in) — udpgw mitigates QUIC partly via persistent
sockets, and a tiny minority of sites only support HTTP/3.
Will ship in v1.7.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#275: youtube_via_relay no longer routes video/image CDNs through
Apps Script. The flag now correctly carves out only the API/HTML
hosts where Restricted Mode is enforced; video chunks come direct
from googlevideo.com (which was missing from the SNI rewrite list
entirely — fixed). Long videos no longer hit Apps Script's 6-min
execution cap, and single-chunk timeouts no longer abort playback.
#280: TunnelMux now caches "destination unreachable" responses from
the tunnel-node (Network is unreachable / No route to host) for 30
seconds, short-circuiting subsequent CONNECTs to that destination
with 502 (HTTP) or 0x04 (SOCKS5). Saves ~5 batches/second on
IPv6-only host probes. Startup pre-warm pool grew 12→24.
143/143 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The tunnel-docker job in v1.7.3 release failed with:
error: failed to unpack package `serde_json v1.0.149`
Caused by: failed to open `/usr/local/cargo/registry/src/.../serde_json-1.0.149/.cargo-ok`
Caused by: File exists (os error 17)
Root cause: BuildKit's default cache-mount sharing is "shared" — both
linux/amd64 and linux/arm64 build stages mount the SAME on-disk cache
dir. Cargo's registry source extraction is non-atomic; both arches
race on `tar -xzf serde_json-1.0.149.crate` into the same destination,
and the loser hits EEXIST mid-unpack.
Fix: scope each cache mount with `id=cargo-registry-${TARGETPLATFORM}`
(and matching for cargo-git + target). BuildKit then keeps separate
on-disk caches per architecture — no race. Per-arch warm-build speedup
is preserved (each cache fills with that arch's pre-built deps); the
only loss is one cache miss per arch on the first build after this
change, which we already paid in v1.7.3.
The target/ mount is also platform-scoped since target/ holds compiled
object files for a single ABI; sharing across arches would either miss
or, worse, link wrong-ABI objects together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-#275, youtube_via_relay=true routed every YouTube-related host
through Apps Script — including ytimg.com (thumbnails) and any
googlevideo.com chunk request the player issued. Two problems:
1. ytimg.com via Apps Script is wasted quota — image CDN, no
Restricted Mode logic to bypass.
2. googlevideo.com wasn't even in SNI_REWRITE_SUFFIXES, so video
chunks hit the relay regardless of the flag. A single chunk
timeout aborted the whole video on Firefox; long videos risked
the Apps Script 6-min execution cap mid-playback.
Fix: split YouTube into "API/HTML hosts" (where Restricted Mode
lives, gated by the flag) and "asset CDNs" (always direct). The
new YOUTUBE_RELAY_HOSTS list is youtube.com, youtu.be,
youtube-nocookie.com, youtubei.googleapis.com — those go through
relay when the flag is on. ytimg.com, googlevideo.com (added),
ggpht.com all stay on SNI rewrite.
The matches_sni_rewrite logic was also restructured: the carve-out
now runs FIRST before the SNI suffix match, so the broad
googleapis.com entry can't override the narrower
youtubei.googleapis.com decision.
Reported with detailed analysis by @amirabbas117. Will ship in v1.7.4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move from yyoyoian-pixel/tun2proxy fork (with patched JNI signature)
to canonical tun2proxy 0.7.21 from crates.io with feature flag
"udpgw". Cargo.toml [patch.crates-io] section removed entirely.
The Android side now resolves tun2proxy_run_with_cli_args at runtime
via dlsym from libtun2proxy.so, which is the upstream maintainer's
recommended path for callers that need full CLI flexibility.
mhrv-rs builds the CLI string in MhrvVpnService and passes it through
Native.runTun2proxy → src/android_jni.rs → dlsym → tun2proxy.
Future tun2proxy upgrades are now a single Cargo version bump.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uses tun2proxy_run_with_cli_args (the C API) via dlsym instead of
modifying the JNI run() signature. The upstream tun2proxy maintainer
recommended this path — the CLI API accepts --udpgw-server natively.
- Cargo.toml: enable udpgw feature, remove [patch.crates-io]
- MhrvVpnService.kt: build CLI args with --udpgw-server in full mode
- Native.kt + android_jni.rs: dlsym wrapper for the C API
- Tun2proxy.kt: reverted to upstream signature
No fork, no patch, no submodule.
Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
* 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>
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>
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>
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>
* 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>
- #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>
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>
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>
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>
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>
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>
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.