mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-18 05:44:35 +03:00
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>
This commit is contained in:
+72
-13
@@ -535,6 +535,16 @@ struct AppState {
|
||||
sessions: Arc<Mutex<HashMap<String, ManagedSession>>>,
|
||||
udp_sessions: Arc<Mutex<HashMap<String, ManagedUdpSession>>>,
|
||||
auth_key: String,
|
||||
/// Active probing defense: when false (default, production), bad
|
||||
/// AUTH_KEY responses are a generic-looking 404 with no JSON-shaped
|
||||
/// "unauthorized" body — same as a static nginx 404. Active scanners
|
||||
/// that POST malformed payloads to `/tunnel` to discover proxy
|
||||
/// endpoints categorize this as a non-tunnel host and move on.
|
||||
/// Enable via `MHRV_DIAGNOSTIC=1` for setup/debugging — restores the
|
||||
/// previous JSON `{"e":"unauthorized"}` body so it's clear *which*
|
||||
/// of "wrong key", "wrong URL path", or "wrong tunnel-node" you've
|
||||
/// hit. (Inspired by #365 Section 3.)
|
||||
diagnostic_mode: bool,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -608,19 +618,41 @@ struct BatchResponse {
|
||||
async fn handle_tunnel(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<TunnelRequest>,
|
||||
) -> Json<TunnelResponse> {
|
||||
) -> axum::response::Response {
|
||||
if req.k != state.auth_key {
|
||||
return Json(TunnelResponse::error("unauthorized"));
|
||||
return decoy_or_unauthorized(state.diagnostic_mode);
|
||||
}
|
||||
match req.op.as_str() {
|
||||
"connect" => Json(handle_connect(&state, req.host, req.port).await),
|
||||
let resp: TunnelResponse = match req.op.as_str() {
|
||||
"connect" => handle_connect(&state, req.host, req.port).await,
|
||||
"connect_data" => {
|
||||
Json(handle_connect_data_single(&state, req.host, req.port, req.data).await)
|
||||
handle_connect_data_single(&state, req.host, req.port, req.data).await
|
||||
}
|
||||
"data" => Json(handle_data_single(&state, req.sid, req.data).await),
|
||||
"close" => Json(handle_close(&state, req.sid).await),
|
||||
other => Json(TunnelResponse::unsupported_op(other)),
|
||||
"data" => handle_data_single(&state, req.sid, req.data).await,
|
||||
"close" => handle_close(&state, req.sid).await,
|
||||
other => TunnelResponse::unsupported_op(other),
|
||||
};
|
||||
Json(resp).into_response()
|
||||
}
|
||||
|
||||
/// Active-probing defense for the bad-auth path. Production default is
|
||||
/// a 404 with a generic "Not Found" HTML body that mimics a vanilla
|
||||
/// nginx/apache static error page — active scanners categorize this
|
||||
/// as a regular web server with nothing interesting and move on.
|
||||
/// `MHRV_DIAGNOSTIC=1` restores the previous JSON `{"e":"unauthorized"}`
|
||||
/// body so misconfigured clients get a clear error during setup.
|
||||
fn decoy_or_unauthorized(diagnostic_mode: bool) -> axum::response::Response {
|
||||
if diagnostic_mode {
|
||||
return Json(TunnelResponse::error("unauthorized")).into_response();
|
||||
}
|
||||
let body = "<html>\r\n<head><title>404 Not Found</title></head>\r\n\
|
||||
<body>\r\n<center><h1>404 Not Found</h1></center>\r\n\
|
||||
<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n";
|
||||
(
|
||||
StatusCode::NOT_FOUND,
|
||||
[(header::CONTENT_TYPE, "text/html")],
|
||||
body,
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -657,10 +689,20 @@ async fn handle_batch(
|
||||
};
|
||||
|
||||
if req.k != state.auth_key {
|
||||
let resp = serde_json::to_vec(&BatchResponse {
|
||||
r: vec![TunnelResponse::error("unauthorized")],
|
||||
}).unwrap_or_default();
|
||||
return (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], resp);
|
||||
if state.diagnostic_mode {
|
||||
let resp = serde_json::to_vec(&BatchResponse {
|
||||
r: vec![TunnelResponse::error("unauthorized")],
|
||||
}).unwrap_or_default();
|
||||
return (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], resp);
|
||||
}
|
||||
// Production: same nginx-404 decoy as the single-op path. See
|
||||
// `decoy_or_unauthorized` for rationale.
|
||||
let body = "<html>\r\n<head><title>404 Not Found</title></head>\r\n\
|
||||
<body>\r\n<center><h1>404 Not Found</h1></center>\r\n\
|
||||
<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n"
|
||||
.as_bytes()
|
||||
.to_vec();
|
||||
return (StatusCode::NOT_FOUND, [(header::CONTENT_TYPE, "text/html")], body);
|
||||
}
|
||||
|
||||
// Process all ops in two phases.
|
||||
@@ -1311,7 +1353,20 @@ async fn main() {
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
tokio::spawn(cleanup_task(sessions.clone(), udp_sessions.clone()));
|
||||
|
||||
let state = AppState { sessions, udp_sessions, auth_key };
|
||||
// MHRV_DIAGNOSTIC=1 in env restores verbose JSON error responses on
|
||||
// bad auth (instead of the nginx-404 decoy). Use during setup so
|
||||
// misconfigured clients see "unauthorized"; flip back off in prod.
|
||||
let diagnostic_mode = std::env::var("MHRV_DIAGNOSTIC")
|
||||
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
.unwrap_or(false);
|
||||
if diagnostic_mode {
|
||||
tracing::warn!(
|
||||
"MHRV_DIAGNOSTIC=1 — bad-auth responses are verbose JSON \
|
||||
errors instead of the production nginx-404 decoy. Disable \
|
||||
before exposing this tunnel-node to the public internet."
|
||||
);
|
||||
}
|
||||
let state = AppState { sessions, udp_sessions, auth_key, diagnostic_mode };
|
||||
|
||||
let app = Router::new()
|
||||
.route("/tunnel", post(handle_tunnel))
|
||||
@@ -1346,6 +1401,10 @@ mod tests {
|
||||
sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
udp_sessions: Arc::new(Mutex::new(HashMap::new())),
|
||||
auth_key: "test-key".into(),
|
||||
// Tests assert against the JSON `unauthorized` body shape
|
||||
// (see e.g. `bad_auth_returns_unauthorized`), so they need
|
||||
// diagnostic_mode enabled. Production default is false.
|
||||
diagnostic_mode: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user