Skip to content

Commit ce3030f

Browse files
therealalephclaude
andcommitted
feat: v1.8.1 — decoy detection + script_id in error logs + disable_padding flag
Three small, ship-able-now changes from the past day's issue triage: 1. Client-side detection of the v1.8.0 bad-auth decoy HTML (#404 w0l4i, #310 sina-b4hrm) When mhrv-rs gets back the decoy HTML body that v1.8.0's Code.gs/ CodeFull.gs/tunnel-node return on bad AUTH_KEY, the client now string-matches the body's distinctive "The script completed but did not return anything" sentinel and emits an explicit ERROR line naming AUTH_KEY mismatch as the likely cause + walking the user through "redeploy as new version" + the DIAGNOSTIC_MODE escape hatch — instead of the previous cryptic "WARN batch failed: bad response: no json in batch response: <!DOCTYPE...". Saves users hours of debugging. Reported pattern hits everyone who edits Code.gs's AUTH_KEY without redeploying as a new version (Apps Script doesn't auto-pick-up that change). 2. script_id in every batch-failure log (#404 w0l4i) Previously WARN batch-failed lines didn't say which deployment failed. In multi-deployment setups (5–10 deployments where some have stale AUTH_KEY), users couldn't identify the culprit without the per-deployment curl probe loop. All four failure paths in tunnel_client::fire_batch — timeout, bad response, decoy detection, missing-response-in-batch — now include the script_id short prefix: `batch failed (script AKfycbz4): ...`. Combined with #1 above, this is the first reliable diagnostic for the "1 of 8 deployments has bad AUTH_KEY" pattern. 3. New disable_padding config flag (#391 EBRAHIM-AM) Default false (padding active = stronger DPI defense). For users on heavily-throttled ISPs where v1.8.0's ~25% bandwidth overhead from random padding compounds with the throttle and pushes borderline-working batches into timeouts, setting `"disable_padding": true` in config.json recovers headroom at the cost of losing length-distribution DPI defense. Don't flip on speculatively — only enable if you've measured actual throughput improvement on your specific ISP path. For users where Apps Script outbound flows freely, padding is free defense. Tested: - cargo build --release --bin mhrv-rs: clean - cargo build --release --bin mhrv-rs-ui --features ui: clean - cargo test --release --lib: 154 passed - UI FormState round-trips disable_padding through save/load Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0d54c5c commit ce3030f

8 files changed

Lines changed: 86 additions & 10 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.8.0"
3+
version = "1.8.1"
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 = 157
18-
versionName = "1.8.0"
17+
versionCode = 158
18+
versionName = "1.8.1"
1919

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

docs/changelog/v1.8.1.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!-- see docs/changelog/v1.1.0.md for the file format: Persian, then `---`, then English. -->
2+
• تشخیص خطای decoy v1.8.0 در سمت کلاینت — پیغام واضح به‌جای cryptic ([#404](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/404)، [#310](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/310)): قبلاً وقتی deployment auth fail می‌گرفت + decoy HTML برمی‌گردوند، client پیغام `WARN batch failed: bad response: no json in batch response: <!DOCTYPE html>...` می‌داد. کاربر باید خودش متن decoy رو می‌شناخت تا تشخیص بده. حالا client decoy رو با string-match تشخیص می‌ده + پیغام explicit می‌ده: "got the v1.8.0 bad-auth decoy — your AUTH_KEY in mhrv-rs config does NOT match the AUTH_KEY in this deployment's Code.gs. Either fix the mismatch + redeploy as a NEW VERSION, or set DIAGNOSTIC_MODE=true at the top of Code.gs + redeploy to see the explicit JSON `unauthorized` error during setup." — کاربر مستقیم می‌فهمه چی بکنه + ساعت‌ها debug ذخیره می‌شه
3+
• اضافه شدن `script_id` به همه log‌های batch-failure ([#404](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/404)): قبلاً log `WARN batch failed: ...` نام deployment که fail کرد رو نشون نمی‌داد. در multi-deployment scenarios (5-10 deployment که برخی AUTH_KEY اشتباه داره)، کاربر نمی‌تونست بدون سختی deployment معیوب رو identify کنه. حالا همه پیغام‌های failure (timeout، bad response، decoy، missing-response-in-batch) شامل short prefix script_id هستند: `batch failed (script AKfycbz4): ...`. این + flag تشخیص decoy، اولین diagnostic از سرنوشت توزیع کاربری به طور reliable
4+
• Flag config جدید `disable_padding: true` ([#391](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/391)): پیش‌فرض `false` (padding فعال = DPI defense). برای کاربران روی ISP‌های heavily-throttled که هزینه padding ~۲۵٪ bandwidth با throttle compounds + batchهای borderline-working رو into timeout می‌اندازه، گذاشتن `"disable_padding": true` در config.json در ازای محافظت length-distribution DPI، headroom برمی‌گردونه. توصیه نیست speculatively فعال بشه — فقط بعد از measurement throughput improvement.
5+
---
6+
• Client-side decoy detection — clear hint instead of cryptic error ([#404](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/404), [#310](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/310)): previously when a deployment had a stale/wrong AUTH_KEY, mhrv-rs returned the v1.8.0 bad-auth decoy HTML, and the client logged `WARN batch failed: bad response: no json in batch response: <!DOCTYPE html>...` — leaving the user to recognize the decoy body string and infer the cause. Now the client string-matches the decoy and emits an explicit error: "got the v1.8.0 bad-auth decoy — your AUTH_KEY in mhrv-rs config does NOT match the AUTH_KEY in this deployment's Code.gs. Either fix the mismatch + redeploy as a NEW VERSION (Apps Script doesn't auto-pick-up AUTH_KEY edits without an explicit redeploy), or set DIAGNOSTIC_MODE=true at the top of Code.gs + redeploy to see the explicit JSON `unauthorized` error during setup." Saves users hours of staring at "no json in batch response" trying to figure out what's wrong.
7+
• Add `script_id` to every batch-failure log line ([#404](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/404)): previously `WARN batch failed: ...` didn't identify which deployment failed. In multi-deployment setups (5-10 deployments where one or two have a stale AUTH_KEY), users couldn't identify the culprit without the per-deployment curl probe loop. Every failure log line now includes the short script_id prefix: `batch failed (script AKfycbz4): ...`, applied to all four failure paths (timeout, bad response, decoy, missing-response-in-batch). Together with the decoy detection above, the first reliable diagnostic for the multi-deployment-with-one-bad-AUTH_KEY user pattern.
8+
• New `disable_padding: true` config flag ([#391](https://github.com/therealaleph/MasterHttpRelayVPN-RUST/issues/391)): default `false` (padding active, full DPI defense). For users on heavily-throttled ISPs where the v1.8.0 random-padding cost (+~25% bandwidth per batch) compounds with the throttle to push borderline-working batches into timeouts, setting `"disable_padding": true` in `config.json` recovers headroom in exchange for losing length-distribution DPI defense. Don't flip on speculatively — for users where Apps Script outbound is uncongested, padding is free defense. Only enable if you've measured throughput improvement after the flip on your specific ISP path.

src/bin/ui.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,10 @@ struct FormState {
243243
/// drop the user's setting. Not currently exposed as a UI control;
244244
/// users edit `block_quic` directly in `config.json` (Issue #213).
245245
block_quic: bool,
246+
/// Round-tripped from config.json. Not exposed as a UI control —
247+
/// users edit `disable_padding` directly when needed (Issue #391).
248+
/// Default false (padding active).
249+
disable_padding: bool,
246250
}
247251

248252
#[derive(Clone, Debug)]
@@ -326,6 +330,7 @@ fn load_form() -> (FormState, Option<String>) {
326330
youtube_via_relay: c.youtube_via_relay,
327331
passthrough_hosts: c.passthrough_hosts.clone(),
328332
block_quic: c.block_quic,
333+
disable_padding: c.disable_padding,
329334
}
330335
} else {
331336
FormState {
@@ -354,6 +359,7 @@ fn load_form() -> (FormState, Option<String>) {
354359
youtube_via_relay: false,
355360
passthrough_hosts: Vec::new(),
356361
block_quic: false,
362+
disable_padding: false,
357363
}
358364
};
359365
(form, load_err)
@@ -500,6 +506,9 @@ impl FormState {
500506
// control yet). Round-trip through the file so save
501507
// doesn't drop a user-set true.
502508
block_quic: self.block_quic,
509+
// Issue #391: disable_padding is config-only for now.
510+
// Round-trip preserves the user's choice.
511+
disable_padding: self.disable_padding,
503512
})
504513
}
505514
}

src/config.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,21 @@ pub struct Config {
190190
/// failure modes later. Issue #213.
191191
#[serde(default)]
192192
pub block_quic: bool,
193+
/// When true, suppress the random `_pad` field that v1.8.0+ adds
194+
/// to outbound Apps Script requests for DPI evasion. Default off
195+
/// (padding active). Some users on heavily-throttled ISPs find
196+
/// the +25% bandwidth cost from padding compounds with the
197+
/// throttle to push borderline-working batches into timeouts;
198+
/// turning padding off recovers a bit of headroom at the cost of
199+
/// length-distribution defense against DPI fingerprinting. Issue
200+
/// #391 (EBRAHIM-AM).
201+
///
202+
/// Don't flip this on speculatively — for users where Apps Script
203+
/// outbound is uncongested, padding is free DPI defense. Only
204+
/// turn off if you've measured throughput improvement after the
205+
/// flip on your specific ISP path.
206+
#[serde(default)]
207+
pub disable_padding: bool,
193208
}
194209

195210
fn default_fetch_ips_from_api() -> bool { false }

src/domain_fronter.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ pub struct DomainFronter {
131131
today_calls: AtomicU64,
132132
today_bytes: AtomicU64,
133133
today_key: std::sync::Mutex<String>,
134+
/// Suppress the random `_pad` field that v1.8.0+ adds to outbound
135+
/// payloads. Mirrors `Config::disable_padding` (#391). Default false
136+
/// (padding active = stronger DPI defense at +25% bandwidth cost).
137+
disable_padding: bool,
134138
}
135139

136140
/// Aggregated stats for one remote host.
@@ -289,6 +293,7 @@ impl DomainFronter {
289293
today_calls: AtomicU64::new(0),
290294
today_bytes: AtomicU64::new(0),
291295
today_key: std::sync::Mutex::new(current_pt_day_key()),
296+
disable_padding: config.disable_padding,
292297
})
293298
}
294299

@@ -1160,7 +1165,9 @@ impl DomainFronter {
11601165
// discards.
11611166
let mut v = serde_json::to_value(&req)?;
11621167
if let Value::Object(map) = &mut v {
1163-
add_random_pad(map);
1168+
if !self.disable_padding {
1169+
add_random_pad(map);
1170+
}
11641171
}
11651172
Ok(serde_json::to_vec(&v)?)
11661173
}
@@ -1290,7 +1297,9 @@ impl DomainFronter {
12901297
if let Some(d) = data {
12911298
map.insert("d".into(), Value::String(d));
12921299
}
1293-
add_random_pad(&mut map);
1300+
if !self.disable_padding {
1301+
add_random_pad(&mut map);
1302+
}
12941303
Ok(serde_json::to_vec(&Value::Object(map))?)
12951304
}
12961305

@@ -1318,7 +1327,9 @@ impl DomainFronter {
13181327
map.insert("k".into(), Value::String(self.auth_key.clone()));
13191328
map.insert("t".into(), Value::String("batch".into()));
13201329
map.insert("ops".into(), serde_json::to_value(ops)?);
1321-
add_random_pad(&mut map);
1330+
if !self.disable_padding {
1331+
add_random_pad(&mut map);
1332+
}
13221333
let payload = serde_json::to_vec(&Value::Object(map))?;
13231334

13241335
let path = format!("/macros/s/{}/exec", script_id);

src/tunnel_client.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -857,11 +857,15 @@ async fn fire_batch(
857857
})
858858
.sum();
859859
f.record_today(response_bytes);
860+
let sid_short = &script_id[..script_id.len().min(8)];
860861
for (idx, reply) in data_replies {
861862
if let Some(resp) = batch_resp.r.get(idx) {
862863
let _ = reply.send(Ok((resp.clone(), script_id.clone())));
863864
} else {
864-
let _ = reply.send(Err("missing response in batch".into()));
865+
let _ = reply.send(Err(format!(
866+
"missing response in batch from script {}",
867+
sid_short
868+
)));
865869
}
866870
}
867871
}
@@ -876,7 +880,30 @@ async fn fire_batch(
876880
f.record_timeout_strike(&script_id);
877881
}
878882
let err_msg = format!("{}", e);
879-
tracing::warn!("batch failed: {}", err_msg);
883+
let sid_short = &script_id[..script_id.len().min(8)];
884+
// Detect the v1.8.0 bad-auth decoy HTML body. The relay layer
885+
// wraps any non-JSON response in `BadResponse("no json in
886+
// batch response: <body prefix>")`. The decoy body string
887+
// `"The script completed but did not return anything"` is
888+
// distinctive — Apps Script's stock pages never include it,
889+
// and our own `Code.gs` only returns it when AUTH_KEY check
890+
// fails. Surfacing this as an actionable hint saves users
891+
// (and #404 / #310 sina-b4hrm class issues) hours of
892+
// staring at "no json in batch response".
893+
if err_msg.contains("The script completed but did not return anything") {
894+
tracing::error!(
895+
"batch failed (script {}): got the v1.8.0 bad-auth decoy — \
896+
your AUTH_KEY in mhrv-rs config does NOT match the AUTH_KEY \
897+
in this deployment's Code.gs. Either fix the mismatch + \
898+
redeploy as a NEW VERSION (Apps Script doesn't auto-pick-up \
899+
AUTH_KEY edits without an explicit redeploy), or set \
900+
DIAGNOSTIC_MODE=true at the top of Code.gs + redeploy to \
901+
see the explicit JSON `unauthorized` error during setup.",
902+
sid_short
903+
);
904+
} else {
905+
tracing::warn!("batch failed (script {}): {}", sid_short, err_msg);
906+
}
880907
for (_, reply) in data_replies {
881908
let _ = reply.send(Err(err_msg.clone()));
882909
}
@@ -886,7 +913,13 @@ async fn fire_batch(
886913
// stronger signal than a per-read timeout — count it the same
887914
// way so a truly-stuck deployment exits round-robin fast.
888915
f.record_timeout_strike(&script_id);
889-
tracing::warn!("batch timed out after {:?} ({} ops)", BATCH_TIMEOUT, n_ops);
916+
let sid_short = &script_id[..script_id.len().min(8)];
917+
tracing::warn!(
918+
"batch timed out after {:?} (script {}, {} ops)",
919+
BATCH_TIMEOUT,
920+
sid_short,
921+
n_ops
922+
);
890923
for (_, reply) in data_replies {
891924
let _ = reply.send(Err("batch timed out".into()));
892925
}

0 commit comments

Comments
 (0)