Five user-visible changes shipping together. Each is independently useful + bounded; bundled because they're all "small architectural hardening" that benefits from one release announcement. 1. Random payload padding (#313, #365 §1) Every outbound Apps Script JSON request now carries a `_pad` field of uniform-random length 0..1024 bytes (base64). Defeats DPI that fingerprints on the tight length distribution of mhrv-rs's previous per-mode-bound packet sizes. ~25% bandwidth on a typical 2 KB batch, negligible against Apps Script's per-call latency floor. Backward- compatible — old `Code.gs` deployments ignore the unknown field. Applied at all three payload-build sites: single relay, single tunnel op, batch tunnel. 2. Active-probing decoy: GAS bad-auth → 200 HTML (#365 §3) `Code.gs` and `CodeFull.gs` now return a benign Apps-Script-style placeholder HTML page on bad/missing AUTH_KEY instead of the JSON `{"e":"unauthorized"}`. To an active scanner the deployment looks like one of the millions of forgotten public Apps Script projects rather than an obvious API endpoint. New `DIAGNOSTIC_MODE` const restores JSON errors during setup; default false (production-strong). 3. Active-probing decoy: tunnel-node bad-auth → 404 nginx (#365 §3) `tunnel-node` returns an HTTP 404 with an nginx-style HTML body on bad auth instead of `{"e":"unauthorized"}`. Active scanners cataloging the host see "static web server, nothing tunnel-shaped here." New `MHRV_DIAGNOSTIC=1` env var restores verbose JSON during setup. 4. Fix: Full-mode usage counter stuck at zero (#230, #362) `today_calls` / `today_bytes` were only being incremented on the apps_script-mode relay path. Full-mode batches go through `tunnel_client::fire_batch` which never wired into the counter. Now `fire_batch` calls `record_today(response_bytes)` after each successful batch — bytes estimated from the `d` (TCP payload) and `pkts` (UDP datagrams) sizes in the BatchTunnelResponse. Full-mode users now see real usage numbers. 5. Fix: quota reset countdown was UTC, should be PT (#230, #362) Apps Script's UrlFetchApp daily quota resets at midnight Pacific Time, not UTC. We were displaying the countdown to UTC midnight, off by 7-8h depending on DST. New `current_pt_day_key()` and `seconds_until_pacific_midnight()` helpers with hand-rolled US DST detection (2nd Sunday March → 1st Sunday November = PDT, else PST) so we don't pull `chrono-tz` and a ~3 MB IANA tzdb just for one helper. UI label "UTC day" → "PT day". Tests pin DST window boundaries against March/November of 2024, 2026, 2027 to catch regressions in the day-of-week math. Tested: - cargo test --lib: 154 passed (was 152, +2 for DST window + day-of-week) - cargo build --release: clean - cargo build --release --bin mhrv-rs-ui --features ui: clean (macOS arm64) - tunnel-node cargo test: 30 passed - Android: ./gradlew assembleDebug succeeds; APK installs + launches on mhrv_test emulator (arm64-v8a), no UnsatisfiedLink, no crash Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tunnel Node
HTTP tunnel bridge server for MasterHttpRelayVPN "full" mode. Bridges HTTP tunnel requests (from Apps Script) to real TCP connections.
Architecture
Phone → mhrv-rs → [domain-fronted TLS] → Apps Script → [HTTP] → Tunnel Node → [real TCP] → Internet
The tunnel node manages persistent TCP and UDP sessions. TCP sessions are real TCP connections to a destination server; UDP sessions are connected UDP sockets to one destination host:port. Data flows through a JSON protocol:
- connect — open TCP to host:port, return session ID
- data — write client data, return server response
- udp_open — open UDP to host:port, optionally send the first datagram
- udp_data — send one UDP datagram, or poll for returned datagrams when
dis omitted - close — tear down session
- batch — process multiple ops in one HTTP request (reduces round trips)
Deployment
Cloud Run
cd tunnel-node
gcloud run deploy tunnel-node \
--source . \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars TUNNEL_AUTH_KEY=$(openssl rand -hex 24) \
--memory 256Mi \
--cpu 1 \
--max-instances 1
Docker — prebuilt image (any VPS)
The fastest path. Pull a prebuilt image and run it; no Rust toolchain needed on the VPS.
# Generate a strong secret. Save it — you'll paste the same value into CodeFull.gs.
SECRET=$(openssl rand -hex 24)
echo "Your TUNNEL_AUTH_KEY: $SECRET"
# Pull + run.
docker run -d \
--name mhrv-tunnel \
--restart unless-stopped \
-p 8080:8080 \
-e TUNNEL_AUTH_KEY="$SECRET" \
ghcr.io/therealaleph/mhrv-tunnel-node:latest
The :latest tag tracks the most recent release. To pin a specific version (recommended for production), use ghcr.io/therealaleph/mhrv-tunnel-node:v1.5.0 (or whatever release you're on). Image is available for linux/amd64 and linux/arm64.
docker-compose.yml if you prefer:
services:
tunnel:
image: ghcr.io/therealaleph/mhrv-tunnel-node:latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
TUNNEL_AUTH_KEY: ${TUNNEL_AUTH_KEY}
Then TUNNEL_AUTH_KEY=your-secret docker compose up -d.
Docker — build from source
If you'd rather build the image yourself (or add custom changes):
cd tunnel-node
docker build -t tunnel-node .
docker run -p 8080:8080 -e TUNNEL_AUTH_KEY=your-secret tunnel-node
Direct binary
cd tunnel-node
cargo build --release
TUNNEL_AUTH_KEY=your-secret PORT=8080 ./target/release/tunnel-node
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
TUNNEL_AUTH_KEY |
Yes | changeme |
Shared secret — must match TUNNEL_AUTH_KEY in CodeFull.gs |
PORT |
No | 8080 |
Listen port (Cloud Run sets this automatically) |
Protocol
Single op: POST /tunnel
{"k":"auth","op":"connect","host":"example.com","port":443}
{"k":"auth","op":"data","sid":"uuid","data":"base64"}
{"k":"auth","op":"close","sid":"uuid"}
Batch: POST /tunnel/batch
{
"k": "auth",
"ops": [
{"op":"data","sid":"uuid1","d":"base64"},
{"op":"udp_data","sid":"uuid2","d":"base64"},
{"op":"close","sid":"uuid3"}
]
}
→ {"r": [{...}, {...}, {...}]}
Health check: GET /health → ok
Performance: deployment count and pipeline depth
The mhrv-rs client runs a pipelined batch multiplexer in full mode. Each Apps Script round-trip takes ~2s, so the client fires multiple batch requests concurrently — the pipeline depth equals the number of configured script deployment IDs (minimum 2, no upper cap).
More deployments = more concurrent batches hitting the tunnel-node = lower per-session latency. With 6 deployments, a new batch arrives every ~0.3s instead of every 2s.
The tunnel-node itself is stateless per-request (sessions are keyed by UUID), so it handles concurrent batches naturally. For best results, deploy 3–12 Apps Script instances across separate Google accounts and list all their deployment IDs in the client config.