Skip to content

feat: full tunnel mode — no certificate needed#86

Closed
vahidlazio wants to merge 1 commit intotherealaleph:mainfrom
vahidlazio:feat/full-tunnel-mode
Closed

feat: full tunnel mode — no certificate needed#86
vahidlazio wants to merge 1 commit intotherealaleph:mainfrom
vahidlazio:feat/full-tunnel-mode

Conversation

@vahidlazio
Copy link
Copy Markdown
Contributor

@vahidlazio vahidlazio commented Apr 23, 2026

Full Tunnel Mode — No Certificate Needed

New "mode": "full" that tunnels all traffic (TCP, TLS, HTTP) end-to-end through Apps Script to a remote tunnel node. The browser does TLS directly with the destination server — the proxy never sees plaintext. No CA certificate installation required on the client device.

Why Apps Script? Why not connect to the tunnel node directly?

If the user's ISP/network allows direct connections to a VPS, then upstream_socks5 pointing at that VPS is simpler and faster — no Apps Script hop, no 2s round-trip overhead. The Apps Script layer exists specifically for DPI bypass: when the network blocks direct connections to any non-whitelisted IP (VPS, xray, v2ray) but allows Google traffic through. Domain fronting through script.google.com makes the tunnel look like normal Google HTTPS to network inspection.

The full mode is for the scenario: "I can't reach my VPS directly, but I can reach Google."

Why a separate tunnel node? Why can't Apps Script do it alone?

Google Apps Script can only make HTTP requests (UrlFetchApp.fetch()). It cannot open raw TCP sockets, maintain persistent connections, or handle binary protocols like TLS handshakes. When we tunnel raw bytes (e.g. a TLS ClientHello), Apps Script has no way to forward those bytes to a destination server — it only speaks HTTP.

The tunnel node solves this: it's a lightweight server that holds real TCP connections on behalf of the client. Apps Script acts as a relay — it receives base64-encoded bytes from the client via domain-fronted HTTPS, POSTs them to the tunnel node over HTTP, and the tunnel node writes them to the actual TCP socket. Response bytes flow back the same way.

Trade-offs vs apps_script mode

apps_script (MITM) full (tunnel)
Certificate install Required Not needed
Extra infra None Tunnel node (Cloud Run / VPS)
Latency ~200ms per request ~2s per round trip
Protocol support HTTP/HTTPS only All TCP (TLS, HTTP, Telegram, etc.)
Privacy Proxy sees plaintext End-to-end encrypted
Operational burden Deploy Apps Script Deploy Apps Script + tunnel node

Full mode trades latency and operational complexity (standing up a tunnel node) for zero certificate requirements, full protocol support, and end-to-end encryption.

Architecture

Phone/Browser
    │
    ▼
mhrv-rs local proxy (no MITM, no cert)
    │  SOCKS5 or HTTP CONNECT
    ▼
Domain-fronted TLS to Google edge
    │  SNI = www.google.com, Host = script.google.com
    ▼
Google Apps Script (CodeFull.gs)
    │  UrlFetchApp.fetch() — HTTP only, no raw TCP
    │  (this is why we need the tunnel node)
    ▼
Tunnel Node on Google Cloud Run  ← same Google network, minimal latency
    │  Real TCP connection (TLS, HTTP, any protocol)
    ▼
Internet (example.com, google.com, Telegram, etc.)

Recommended: deploy the tunnel node on Google Cloud Run. Since Apps Script runs on Google's infrastructure, keeping the tunnel node on the same network minimizes the UrlFetchApp hop latency — this is the single largest contributor to round-trip time.


Components

1. Tunnel Node (tunnel-node/)

Standalone Rust HTTP server that bridges HTTP tunnel requests to real TCP connections. Each client TCP connection becomes a "session" with a UUID.

Protocol — POST /tunnel (single op):

{"k":"auth","op":"connect","host":"example.com","port":443}  → {"sid":"uuid","eof":false}
{"k":"auth","op":"data","sid":"uuid","data":"base64"}        → {"sid":"uuid","d":"base64","eof":false}
{"k":"auth","op":"close","sid":"uuid"}                       → {"sid":"uuid","eof":true}

Protocol — POST /tunnel/batch (multiple ops in one request):

{"k":"auth","ops":[
  {"op":"data","sid":"uuid1","d":"base64"},
  {"op":"data","sid":"uuid2","d":"base64"},
  {"op":"close","sid":"uuid3"}
]}
→ {"r":[{...},{...},{...}]}

Batch processes all active sessions in one HTTP round trip — critical since each Apps Script call takes ~2s.

Deployment — Google Cloud Run (recommended):

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

Cloud Run is recommended because Apps Script → Cloud Run stays on Google's internal network (lowest latency for the UrlFetchApp hop, auto HTTPS, scales to zero).

Also works on any VPS via Docker or direct binary — see tunnel-node/README.md.

2. Apps Script (assets/apps_script/CodeFull.gs)

Extends Code.gs with tunnel forwarding. All original HTTP relay functionality preserved. When request has t field → tunnel mode:

  • t: "connect"/"data"/"close" → single op to /tunnel
  • t: "batch" → batch to /tunnel/batch

3. Batch Multiplexer (src/tunnel_client.rs)

Central coordinator that collects data from all active sessions and sends one batch request per tick:

Without multiplexer:                    With multiplexer:
Session A → Apps Script → 2s            Session A ─┐
Session B → Apps Script → 2s            Session B ─┼→ ONE call → 2s → all responses
Session C → Apps Script → 2s            Session C ─┘
Total: 6s (serial)                      Total: 2s (batched)

Connects run in parallel as individual requests (each spawned on a different script ID via round-robin) because they're slow and can't block each other. Data/close ops are batched.

4. Rust Client Changes

  • src/config.rs: Mode::Full, same validation as apps_script
  • src/domain_fronter.rs: TunnelResponse, BatchOp, tunnel_request(), tunnel_batch_request() — reuse existing pool + SNI rotation
  • src/proxy_server.rs: TunnelMux init in run() (tokio runtime required), Full mode dispatch
  • src/main.rs: startup logging, cert-check skip

5. UI Changes (Android + Desktop)


Multiple Script Deployments

More deployments = more parallel connects + quota headroom:

  • Connects: each gets its own Apps Script call via round-robin. 12 scripts = 12 simultaneous connects
  • Data: batched into 1 request per tick regardless of script count
  • Quota: per Google account (not per deployment) — 20k UrlFetchApp calls/day. Use different Google accounts to multiply quota

Performance

~2s per round trip through Apps Script (irreducible Google infrastructure overhead).

Scenario Wall time Throughput vs serial
1 connection ~10s baseline
5 parallel ~16s 3x
10 parallel ~20s 5x

Bugs found and fixed during testing

  • TunnelMux::start() called outside tokio runtime: on Android, ProxyServer::new() runs on the JNI thread (no runtime). Moving TunnelMux init into run() fixes the SIGABRT crash.
  • Stale port binding: previous proxy instances hold ports after force-stop. Config allows alternate ports (8087/8088) as workaround; the upstream defensive-stop logic handles normal restarts.
  • SNI pool cert mismatch: upstream v1.2.6 scan-sni auto-discovery can produce SNI names that don't match Google's certificate. Workaround: set explicit sni_hosts in config. This is a pre-existing issue not introduced by this PR.

Rebased onto current main

Rebased onto d66f957 (v1.2.6). Resolved conflict in MhrvVpnService.kt — merged upstream's #73 startForeground crash fix with Full mode's credential check bypass.

Test plan

  • cargo test — 69 tests pass (includes upstream's new tests)
  • cargo build — clean for both main crate and tunnel-node
  • Desktop: SOCKS5 proxy → example.com HTTP 200 (8s), google.com HTTP 200 (12s)
  • Tunnel node: deployed to Cloud Run, health check ok, auth verified, batch endpoint tested
  • Android: APK builds, VPN connects, traffic flows through tunnel, data batching confirmed
  • Multiple script IDs: 12 deployments round-robin verified, parallel connects confirmed
  • Batch multiplexer: up to 5 sessions batched in single request
  • 10 parallel connections: 20s wall time (5x vs serial)
  • Long-running browsing session stability
  • Split into 3 PRs if preferred for review

🤖 Generated with Claude Code

@therealaleph
Copy link
Copy Markdown
Owner

Read through the draft — ambitious and interesting design. Leaving comments so you can factor in feedback before marking ready for review:

On the "no CA needed" claim: the tunnel node is effectively a remote proxy that the Apps Script layer can reach. If the client does TLS directly with the destination, the tunnel node acts as a plain TCP forwarder (since it sees encrypted bytes). That's a legitimate shape — same model as an HTTPS CONNECT proxy — and does remove the CA requirement for that flow. Worth being explicit in the PR body that this trades MITM for requiring the user to stand up a tunnel node (VPS with an always-on Rust service), which is a different operational burden.

Architecture question: why does traffic need to go through Apps Script at all in this mode? If the user already has a tunnel node on a VPS, upstream_socks5 pointing directly at it gives you the same "no CA, all traffic tunneled" property without the Apps Script hop — which is a measurable RTT saving and cuts out the 50 MB/fetch and 20k/day limits. Is there a specific DPI scenario where Apps Script is still needed as the outer layer? (E.g. if the user's ISP drops direct connections to their VPS IP but lets script.google.com through — the Apps Script hop acts as an additional front.)

Merge conflicts: the PR shows CONFLICTING against main. Since it touches config.rs, proxy_server.rs, domain_fronter.rs, MhrvVpnService.kt, and HomeScreen.kt — all of which I landed changes in recently (v1.2.0 google_only mode, v1.2.2 Android crash fix, v1.2.4/5 port-collision + range-parallel validation) — a rebase is going to require non-trivial reconciliation. Could you rebase onto current main?

Scope: 2586-line additions is a lot for one PR. If the design holds up, it'd be easier to review as three merges: (1) tunnel-node service + docker + README, (2) Rust-side Mode::Full + tunnel_client.rs, (3) Android UI surface for the new mode. Up to you.

Leaving this in draft until you address the above. Happy to review each chunk as it lands.


[reply via Anthropic Claude | reviewed by @therealaleph]

@vahidlazio
Copy link
Copy Markdown
Contributor Author

@therealaleph

Architecture question: why does traffic need to go through Apps Script at all in this mode? If the user already has a tunnel node on a VPS, upstream_socks5 pointing directly at it gives you the same "no CA, all traffic tunneled" property without the Apps Script hop — which is a measurable RTT saving and cuts out the 50 MB/fetch and 20k/day limits. Is there a specific DPI scenario where Apps Script is still needed as the outer layer? (E.g. if the user's ISP drops direct connections to their VPS IP but lets script.google.com through — the Apps Script hop acts as an additional front.)

1- the assumption is that google.com is the only thing that passes through dpi, if that's not the case then connecting directly to your VPS is the way to go, but if google.com is the only thing we can reach, then reaching app script with google.com fronting is the only thing we can see. (Google blocked Iranian IPs from reaching their services like Cloud Run etc and App script is the only thing that has not blocked the Iranian IPs).
2- connections to the app script is still domain fronted but since mhrv makes that conns we have something in manifest to not check the strict TLS stuff and it works, no cert needed.

the design is only suitable for scenarios when only google.com with google ips are passing through dpi.

@vahidlazio vahidlazio force-pushed the feat/full-tunnel-mode branch from 1b761c9 to 8d81e58 Compare April 23, 2026 20:05
New "full" mode that tunnels ALL traffic (TCP, TLS, HTTP) through
Apps Script to a remote tunnel node, eliminating the need for CA
certificate installation on the client device.

Architecture:
  Client → mhrv-rs (no MITM) → [domain-fronted TLS] → Apps Script
    → [HTTP] → Tunnel Node (Cloud Run / VPS) → [real TCP] → Internet

Components:
- tunnel-node/: standalone Rust HTTP server that bridges tunnel
  requests to real TCP connections. Docker-ready for Cloud Run or
  any VPS. Supports single-op and batch modes.
- assets/apps_script/CodeFull.gs: Apps Script with tunnel forwarding
  (single + batch) alongside existing HTTP relay functionality.
- src/tunnel_client.rs: batch multiplexer that collects data from
  all active sessions and sends ONE Apps Script request per tick,
  reducing N parallel calls to 1. Connects run in parallel across
  multiple script deployments via round-robin.
- src/domain_fronter.rs: TunnelResponse, BatchOp structs and
  tunnel_request() / tunnel_batch_request() methods reusing the
  existing domain-fronted connection pool.

Config: "mode": "full" with same script_id/auth_key as apps_script.
Multiple script_ids are round-robined for parallel connects and
quota distribution.

UI: Mode dropdown updated in both Android (Compose) and desktop
(egui) with "Full tunnel (no cert)" option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vahidlazio vahidlazio force-pushed the feat/full-tunnel-mode branch from 8d81e58 to 8337b58 Compare April 23, 2026 20:12
@vahidlazio vahidlazio closed this Apr 23, 2026
@vahidlazio
Copy link
Copy Markdown
Contributor Author

Split into 3 smaller PRs per review feedback:

  1. feat: tunnel-node service + CodeFull.gs (1/3) #93 — tunnel-node service + CodeFull.gs + Dockerfile + README (8 files)
  2. feat: Mode::Full + batch tunnel client (2/3) #94 — Rust client: Mode::Full + batch tunnel multiplexer + domain_fronter + proxy_server (7 files)
  3. feat: Android UI for full tunnel mode (3/3) #95 — Android UI: Mode.FULL enum + VPN service + HomeScreen dropdown (3 files)

Also created #92 as a standalone fix for the accounts.googl.com SNI pool cert validation bug discovered during testing.

Merge order: #92#93#94#95

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants