Files
MasterHttpRelayVPN-RUST/tunnel-node
dazzling-no-more e70947ff0d fix(udpgw): move magic IP out of tun2proxy virtual-DNS range (#251, #1143)
Closes #251. In Android Full mode, Telegram worked but Google search and most other websites failed silently. `apps_script` mode on the same setup was unaffected.

**Root cause**: the udpgw magic destination (`198.18.0.1:7300`) was inside `198.18.0.0/15` — the exact range tun2proxy's `--dns virtual` allocator uses to synthesise fake IPs for hostname lookups. Whenever virtual DNS assigned `198.18.0.1` to a real hostname, that hostname's traffic was intercepted by tun2proxy *itself* as a udpgw connection and dropped. Telegram was immune because it uses hardcoded numeric IPs; `apps_script` mode was immune because it never sets `--udpgw-server`.

**Fix**: move `UDPGW_MAGIC_IP` to `192.0.2.1` (RFC 5737 TEST-NET-1) — outside any virtual-DNS allocation pool. Coordinated change across the tunnel-node constant and the Android `--udpgw-server` flag.

## Back-compat

v1.9.25 tunnel-nodes still recognise the legacy `198.18.0.1:7300` for one deprecation cycle (removal in v1.10.0).

| Android | Tunnel-node | Full-mode UDP |
|---|---|---|
| v1.9.25 | v1.9.25 |  fully fixed |
| ≤v1.9.24 | v1.9.25 | ⚠️ handshake works (legacy IP still recognised), but the old client still asks tun2proxy for `198.18.0.1`, so the #251 virtual-DNS collision is still live on-device |
| v1.9.25 | ≤v1.9.24 |  breaks silently (old node rejects `192.0.2.1`) |

The fix lives on the client side (which magic IP it asks tun2proxy to reserve). The back-compat is on the tunnel-node side (accepting both during the deprecation window).

## Verified locally

- `cargo test --lib --release`: 231/231 
- `cargo build --release --features ui --bin mhrv-rs-ui`: clean 
- `(cd tunnel-node && cargo test --release)`: 38/38  (+2 new tests for the IP change)

## Version bump

Cargo.toml already bumped to 1.9.25 in this PR; `docs/changelog/v1.9.25.md` pre-baked. Will combine with any other PRs landing into v1.9.25 before tagging.

Reviewed via Anthropic Claude.

Co-Authored-By: dazzling-no-more <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:45:23 +03:00
..

Tunnel Node

Persian / فارسی: README.fa.md

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 d is 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 /healthok

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 312 Apps Script instances across separate Google accounts and list all their deployment IDs in the client config.