Skip to content

Commit cb3732f

Browse files
therealalephclaude
andcommitted
feat: v1.8.0 — DPI evasion, active-probing defense, full-mode usage counters
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>
1 parent f7da4f0 commit cb3732f

10 files changed

Lines changed: 379 additions & 51 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mhrv-rs"
3-
version = "1.7.11"
3+
version = "1.8.0"
44
edition = "2021"
55
description = "Rust port of MasterHttpRelayVPN -- DPI bypass via Google Apps Script relay with domain fronting"
66
license = "MIT"

android/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ android {
1414
applicationId = "com.therealaleph.mhrv"
1515
minSdk = 24 // Android 7.0 — covers 99%+ of live devices.
1616
targetSdk = 34
17-
versionCode = 156
18-
versionName = "1.7.11"
17+
versionCode = 157
18+
versionName = "1.8.0"
1919

2020
// Ship all four mainstream Android ABIs:
2121
// - arm64-v8a — 95%+ of real-world Android phones since 2019

assets/apps_script/Code.gs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@
1818

1919
const AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET";
2020

21+
// Active-probing defense. When false (production default), bad AUTH_KEY
22+
// requests get a decoy HTML page that looks like a placeholder Apps
23+
// Script web app instead of the JSON `{"e":"unauthorized"}` body. This
24+
// makes the deployment indistinguishable from a forgotten-but-public
25+
// Apps Script project to active scanners that POST malformed payloads
26+
// looking for proxy endpoints.
27+
//
28+
// Set to `true` during initial setup if a misconfigured client is
29+
// hitting "unauthorized" and you want the explicit JSON error to debug
30+
// — then flip back to false before the deployment is widely shared.
31+
// (Inspired by #365 Section 3, mhrv-rs v1.8.0+.)
32+
const DIAGNOSTIC_MODE = false;
33+
2134
// Keep browser capability headers (sec-ch-ua*, sec-fetch-*) intact.
2235
// Some modern apps, notably Google Meet, use them for browser gating.
2336
const SKIP_HEADERS = {
@@ -26,21 +39,49 @@ const SKIP_HEADERS = {
2639
"priority": 1, te: 1,
2740
};
2841

42+
// HTML body for the bad-auth decoy. Mimics a minimal Apps Script-style
43+
// placeholder page — no proxy-shaped JSON, nothing distinctive enough
44+
// for a probe to fingerprint as a tunnel endpoint.
45+
const DECOY_HTML =
46+
'<!DOCTYPE html><html><head><title>Web App</title></head>' +
47+
'<body><p>The script completed but did not return anything.</p>' +
48+
'</body></html>';
49+
50+
function _decoyOrError(jsonBody) {
51+
if (DIAGNOSTIC_MODE) return _json(jsonBody);
52+
return ContentService
53+
.createTextOutput(DECOY_HTML)
54+
.setMimeType(ContentService.MimeType.HTML);
55+
}
56+
2957
function doPost(e) {
3058
try {
3159
var req = JSON.parse(e.postData.contents);
32-
if (req.k !== AUTH_KEY) return _json({ e: "unauthorized" });
60+
if (req.k !== AUTH_KEY) return _decoyOrError({ e: "unauthorized" });
3361

3462
// Batch mode: { k, q: [...] }
3563
if (Array.isArray(req.q)) return _doBatch(req.q);
3664

3765
// Single mode
3866
return _doSingle(req);
3967
} catch (err) {
40-
return _json({ e: String(err) });
68+
// Parse failures of the request body are also probe-shaped — a real
69+
// mhrv-rs client never sends invalid JSON. Decoy for the same reason.
70+
return _decoyOrError({ e: String(err) });
4171
}
4272
}
4373

74+
// `doGet` is what active scanners hit first (HTTP GET probes are cheaper
75+
// than POSTs). Apps Script defaults to a "Script function not found" page
76+
// here which is a fine-enough decoy on its own, but explicitly returning
77+
// the same harmless placeholder makes the response identical to the
78+
// bad-auth POST decoy — one less fingerprint vector.
79+
function doGet(e) {
80+
return ContentService
81+
.createTextOutput(DECOY_HTML)
82+
.setMimeType(ContentService.MimeType.HTML);
83+
}
84+
4485
function _doSingle(req) {
4586
if (!req.u || typeof req.u !== "string" || !req.u.match(/^https?:\/\//i)) {
4687
return _json({ e: "bad url" });

assets/apps_script/CodeFull.gs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,46 @@ const AUTH_KEY = "CHANGE_ME_TO_A_STRONG_SECRET";
1616
const TUNNEL_SERVER_URL = "https://YOUR_TUNNEL_NODE_URL";
1717
const TUNNEL_AUTH_KEY = "YOUR_TUNNEL_AUTH_KEY";
1818

19+
// Active-probing defense. When false (production default), bad AUTH_KEY
20+
// requests get a decoy HTML page that looks like a placeholder Apps
21+
// Script web app instead of the JSON `{"e":"unauthorized"}` body. This
22+
// makes the deployment indistinguishable from a forgotten-but-public
23+
// Apps Script project to active scanners that POST malformed payloads
24+
// looking for proxy endpoints.
25+
//
26+
// Set to `true` during initial setup if a misconfigured client is
27+
// hitting "unauthorized" and you want the explicit JSON error to debug
28+
// — then flip back to false before the deployment is widely shared.
29+
// (Inspired by #365 Section 3, mhrv-rs v1.8.0+.)
30+
const DIAGNOSTIC_MODE = false;
31+
1932
const SKIP_HEADERS = {
2033
host: 1, connection: 1, "content-length": 1,
2134
"transfer-encoding": 1, "proxy-connection": 1, "proxy-authorization": 1,
2235
"priority": 1, te: 1,
2336
};
2437

38+
// HTML body for the bad-auth decoy. Mimics a minimal Apps Script-style
39+
// placeholder page — no proxy-shaped JSON, nothing distinctive enough
40+
// for a probe to fingerprint as a tunnel endpoint.
41+
const DECOY_HTML =
42+
'<!DOCTYPE html><html><head><title>Web App</title></head>' +
43+
'<body><p>The script completed but did not return anything.</p>' +
44+
'</body></html>';
45+
46+
function _decoyOrError(jsonBody) {
47+
if (DIAGNOSTIC_MODE) return _json(jsonBody);
48+
return ContentService
49+
.createTextOutput(DECOY_HTML)
50+
.setMimeType(ContentService.MimeType.HTML);
51+
}
52+
2553
// ========================== Entry point ==========================
2654

2755
function doPost(e) {
2856
try {
2957
var req = JSON.parse(e.postData.contents);
30-
if (req.k !== AUTH_KEY) return _json({ e: "unauthorized" });
58+
if (req.k !== AUTH_KEY) return _decoyOrError({ e: "unauthorized" });
3159

3260
// Tunnel mode
3361
if (req.t) return _doTunnel(req);
@@ -38,7 +66,9 @@ function doPost(e) {
3866
// Single relay mode
3967
return _doSingle(req);
4068
} catch (err) {
41-
return _json({ e: String(err) });
69+
// Parse failures of the request body are also probe-shaped — a real
70+
// mhrv-rs client never sends invalid JSON. Decoy for the same reason.
71+
return _decoyOrError({ e: String(err) });
4272
}
4373
}
4474

docs/changelog/v1.8.0.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
2+
• Padding random برای پایلود Apps Script ([#313](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/313)، [#365](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/365) Section 1): هر request به Apps Script حالا یک فیلد `_pad` با طول uniform-random بین ۰-۱۰۲۴ بایت اضافه می‌کنه — به‌صورت base64 encoded. بدون این، طول request body در هر mode تقریباً ثابت می‌مونه + DPI ایران می‌تونه بر اساس distribution طول fingerprint بزنه. حالا packet sizes uniformly distributed هستن + length-clustering match نمی‌کنه. تأثیر bandwidth: متوسط ۵۱۲ بایت اضافه به batch ~۲KB = +۲۵٪، negligible در برابر floor latency Apps Script. backward-compatible: Code.gs قدیم هم کار می‌کنه (unknown JSON fields ignore می‌شن).
3+
• Defense active probing: decoy 200 HTML در Code.gs / CodeFull.gs روی AUTH_KEY بد ([#365](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/365) Section 3): قبلاً request بدون auth `{"e":"unauthorized"}` JSON برمی‌گردوند — fingerprint مشخص "این یه API endpoint هست". حالا یه HTML benign placeholder برمی‌گردونه که شبیه یه Apps Script web app forgotten-but-public هست. scanner active که با AUTH_KEY ساختگی POST می‌کنه categorize می‌کنه به‌عنوان "non-tunnel، nothing interesting". flag `DIAGNOSTIC_MODE` برای setup که response قدیمی JSON رو برمی‌گردونه — default `false` (production-strong)
4+
• Defense active probing: decoy 404 nginx در tunnel-node روی auth بد: tunnel-node قبلاً `{"e":"unauthorized"}` JSON برمی‌گردوند. حالا response 404 با body HTML شبیه nginx default error می‌فرسته (active scanners "static web server هست، tunnel نیست" تشخیص می‌دن). env var `MHRV_DIAGNOSTIC=1` برای setup behavior قدیمی رو فعال می‌کنه
5+
• رفع باگ "Usage today (estimated) در Full mode همیشه ۰" ([#230](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/230)، [#362](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/362)): counter `today_calls` و `today_bytes` فقط روی apps_script-mode relay path در `domain_fronter::relay()` افزایش می‌یافت. Full mode از `tunnel_client::fire_batch` می‌گذره که کانتر رو زد. حالا fire_batch بعد از batch موفق `record_today(response_bytes)` رو صدا می‌زنه — bytes از sum طول `d` و `pkts` در BatchTunnelResponse تخمین زده می‌شه. Full mode users حالا "Usage today" واقعی می‌بینن
6+
• رفع باگ "quota reset countdown با time UTC به‌جای PT نشون داده می‌شه" ([#230](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/230)، [#362](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/362)): Apps Script's UrlFetchApp quota در 00:00 **Pacific Time** ریست می‌شه (PST/PDT با DST)، نه UTC. ما UTC midnight رو نشون می‌دادیم — ۷-۸ ساعت off. fix: helpers جدید `current_pt_day_key()` + `seconds_until_pacific_midnight()` با hand-rolled DST detection (بدون اضافه کردن chrono-tz / 3MB tzdb). UI label "UTC day" → "PT day" تغییر کرد. ۲ test جدید برای DST window boundaries (مارس ۲۰۲۴/۲۰۲۶/۲۰۲۷، نوامبر ۲۰۲۴/۲۰۲۶) + Sakamoto's day-of-week
7+
---
8+
• Random payload padding for Apps Script requests ([#313](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/313), [#365](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/365) Section 1): every outbound request to Apps Script now carries a `_pad` field of uniform-random length 0–1024 bytes (base64 encoded). Before this, request body sizes within each mode were tightly clustered, giving ISP DPI a clean length-distribution fingerprint to match against. Now packet sizes are spread uniformly across the range so length-clustering DPI heuristics can't match. Bandwidth cost: ~512 bytes added to a typical 2 KB tunnel batch = +25%, 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.
9+
• Active-probing defense: decoy 200 HTML on bad AUTH_KEY in `Code.gs` and `CodeFull.gs` ([#365](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/365) Section 3): previously a request with a missing/wrong AUTH_KEY got `{"e":"unauthorized"}` as a JSON body — a clear "this is some kind of API endpoint" signal that active scanners can fingerprint. Now bad-auth requests get a benign HTML placeholder page that looks like a forgotten-but-public Apps Script web app, indistinguishable from the millions of stale Apps Script projects on Google's infrastructure. New `DIAGNOSTIC_MODE` const (default `false`) restores the old JSON error response for setup/debugging — flip to `true` while configuring a misconfigured client, then back to `false` before sharing the deployment widely.
10+
• Active-probing defense: decoy 404 nginx-style HTML on bad auth in `tunnel-node` ([#365](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/365) Section 3): previously a bad-auth request got `{"e":"unauthorized"}`. Now it gets an HTTP 404 with an `nginx`-style error page body, looking like a vanilla static web server. Active scanners that POST malformed payloads to `/tunnel` to discover proxy endpoints categorize this host as "boring" and move on. New `MHRV_DIAGNOSTIC=1` env var restores the verbose JSON error during setup; default is the production decoy.
11+
• Fix "Usage today (estimated) is always 0 in Full mode" ([#230](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/230), [#362](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/362)): the daily-usage counters (`today_calls` / `today_bytes`) were incremented only on the `apps_script`-mode relay path inside `domain_fronter::relay()`. Full-mode traffic goes through `tunnel_client::fire_batch` which never wired the counter. Now `fire_batch` calls `record_today(response_bytes)` after each successful batch — bytes are estimated from the sum of per-session `d` (TCP payload) and `pkts` (UDP datagrams) lengths in the `BatchTunnelResponse`, which is a stable proxy for "how much did this batch move." Full mode users now see real usage numbers instead of stuck-at-zero.
12+
• Fix "quota reset countdown shown in UTC instead of Pacific Time" ([#230](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/230), [#362](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/362)): Apps Script's `UrlFetchApp` quota actually resets at midnight Pacific Time (PST/PDT — observes DST), not midnight UTC. We were displaying the countdown to UTC midnight, which is 7–8 hours off depending on DST. Fix: new `current_pt_day_key()` + `seconds_until_pacific_midnight()` helpers using a hand-rolled US DST detector (2nd Sunday of March → 1st Sunday of November = PDT, otherwise PST) so we don't pull `chrono-tz` and a ~3 MB IANA tzdb just for one helper. UI label updated from "UTC day" to "PT day". Two new tests pin down the DST window boundaries (March 2024 / 2026 / 2027, November 2024 / 2026) and Sakamoto's day-of-week formula.

src/bin/ui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,7 +1113,7 @@ impl eframe::App for App {
11131113
),
11141114
),
11151115
("bytes today", fmt_bytes(s.today_bytes)),
1116-
("UTC day", s.today_key.clone()),
1116+
("PT day", s.today_key.clone()),
11171117
("resets in", reset_str),
11181118
];
11191119
egui::Grid::new("usage_today")

0 commit comments

Comments
 (0)