From eb3adafe77f926c4f5d1d425dab88dd1f625a39f Mon Sep 17 00:00:00 2001 From: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com> Date: Tue, 19 May 2026 22:57:08 +0200 Subject: [PATCH 1/2] feat: end-to-end zstd compression for tunnel batches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-failure capability negotiation: - Batch 1: client sends ops + zc:1, tunnel responds with zr + zc:1 - Batch 2+: client sends zops (compressed), tunnel responds with zr - Apps Script passes zc/zops/zr through opaquely Backward compatible: old clients/tunnel-nodes/scripts ignore unknown fields — compression never activates, nothing breaks. Also: INFLIGHT_ACTIVE 4→6 for better pipeline throughput. Requires tunnel-node + CodeFull.gs redeployment for activation. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 31 +++++++++++- Cargo.toml | 1 + assets/apps_script/CodeFull.gs | 40 ++++++++++++--- src/domain_fronter.rs | 49 +++++++++++++++--- src/tunnel_client.rs | 2 +- tunnel-node/Cargo.lock | 91 +++++++++++++++++++++++++++++++++- tunnel-node/Cargo.toml | 1 + tunnel-node/src/main.rs | 63 +++++++++++++++++++++-- 8 files changed, 257 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e2ca756..0e629d39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2655,6 +2655,7 @@ dependencies = [ "url", "webpki-roots 0.26.11", "x509-parser", + "zstd", ] [[package]] @@ -5135,7 +5136,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -6030,6 +6031,34 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index 8d070e2c..6801176c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ rand = "0.8" h2 = "0.4" http = "1" flate2 = "1" +zstd = "0.13" directories = "5" futures-util = { version = "0.3", default-features = false, features = ["std"] } # 64-bit atomics on 32-bit MIPS/ARMv5 targets. Rust's std AtomicU64 is diff --git a/assets/apps_script/CodeFull.gs b/assets/apps_script/CodeFull.gs index 6b4200a6..904991dc 100644 --- a/assets/apps_script/CodeFull.gs +++ b/assets/apps_script/CodeFull.gs @@ -201,11 +201,17 @@ function _doTunnel(req) { // On a 5-DNS-query batch, this collapses 5 serial cache.get round trips // into one cache.getAll round trip. function _doTunnelBatch(req) { + // Compressed batch: forward opaquely, skip edge-DNS inspection. + if (req.zops) { + return _doTunnelBatchForwardCompressed(req.zops); + } + var ops = (req && req.ops) || []; + var zc = req && req.zc; // Feature off: byte-identical to the pre-feature behavior. if (!ENABLE_EDGE_DNS_CACHE) { - return _doTunnelBatchForward(ops); + return _doTunnelBatchForward(ops, zc); } var results = new Array(ops.length); // sparse: filled by edge-DNS hits @@ -272,10 +278,11 @@ function _doTunnelBatch(req) { // Nothing was served locally — forward verbatim, no splice needed. if (forwardOps.length === ops.length) { - return _doTunnelBatchForward(ops); + return _doTunnelBatchForward(ops, zc); } // Partial: forward the un-served ops and splice results back in place. + // Don't pass zc here — Apps Script needs to parse r[] for splicing. var resp = _doTunnelBatchFetch(forwardOps); if (resp.error) return _json({ e: resp.error }); if (resp.r.length !== forwardOps.length) { @@ -287,11 +294,30 @@ function _doTunnelBatch(req) { } // Verbatim forward: no splice, response passed through unchanged. -function _doTunnelBatchForward(ops) { +function _doTunnelBatchForward(ops, zc) { + var body = { k: TUNNEL_AUTH_KEY, ops: ops }; + if (zc) body.zc = zc; + var resp = UrlFetchApp.fetch(TUNNEL_SERVER_URL + "/tunnel/batch", { + method: "post", + contentType: "application/json", + payload: JSON.stringify(body), + muteHttpExceptions: true, + followRedirects: true, + }); + if (resp.getResponseCode() !== 200) { + return _json({ e: "tunnel batch HTTP " + resp.getResponseCode() }); + } + return ContentService.createTextOutput(resp.getContentText()) + .setMimeType(ContentService.MimeType.JSON); +} + +// Compressed forward: zops is an opaque blob, passed to tunnel-node as-is. +// Response is also opaque (may contain zr instead of r). +function _doTunnelBatchForwardCompressed(zops) { var resp = UrlFetchApp.fetch(TUNNEL_SERVER_URL + "/tunnel/batch", { method: "post", contentType: "application/json", - payload: JSON.stringify({ k: TUNNEL_AUTH_KEY, ops: ops }), + payload: JSON.stringify({ k: TUNNEL_AUTH_KEY, zops: zops }), muteHttpExceptions: true, followRedirects: true, }); @@ -304,11 +330,13 @@ function _doTunnelBatchForward(ops) { // Forward + parse for the splice path. Returns { r:[...] } on success or // { error: "..." } on any failure. -function _doTunnelBatchFetch(ops) { +function _doTunnelBatchFetch(ops, zc) { + var body = { k: TUNNEL_AUTH_KEY, ops: ops }; + if (zc) body.zc = zc; var resp = UrlFetchApp.fetch(TUNNEL_SERVER_URL + "/tunnel/batch", { method: "post", contentType: "application/json", - payload: JSON.stringify({ k: TUNNEL_AUTH_KEY, ops: ops }), + payload: JSON.stringify(body), muteHttpExceptions: true, followRedirects: true, }); diff --git a/src/domain_fronter.rs b/src/domain_fronter.rs index bfcebe50..b38e66fb 100644 --- a/src/domain_fronter.rs +++ b/src/domain_fronter.rs @@ -412,6 +412,7 @@ pub struct DomainFronter { /// payloads. Mirrors `Config::disable_padding` (#391). Default false /// (padding active = stronger DPI defense at +25% bandwidth cost). disable_padding: bool, + zstd_enabled: Arc, /// Per-instance auto-blacklist tuning. Mirrors `Config::auto_blacklist_*` /// (#391, #444). Cached here so the hot path in `record_timeout_strike` /// doesn't have to reach back through the Config (which we don't keep @@ -543,6 +544,10 @@ pub struct BatchTunnelResponse { pub r: Vec, #[serde(default)] pub e: Option, + #[serde(default)] + pub zr: Option, + #[serde(default)] + pub zc: Option, } impl DomainFronter { @@ -626,6 +631,7 @@ impl DomainFronter { today_bytes: AtomicU64::new(0), today_key: std::sync::Mutex::new(current_pt_day_key()), disable_padding: config.disable_padding, + zstd_enabled: Arc::new(AtomicBool::new(false)), auto_blacklist_strikes: config.auto_blacklist_strikes.max(1), auto_blacklist_window: Duration::from_secs( config.auto_blacklist_window_secs.clamp(1, 3600), @@ -3105,7 +3111,20 @@ impl DomainFronter { let mut map = serde_json::Map::new(); map.insert("k".into(), Value::String(self.auth_key.clone())); map.insert("t".into(), Value::String("batch".into())); - map.insert("ops".into(), serde_json::to_value(ops)?); + if self.zstd_enabled.load(Ordering::Relaxed) { + let ops_json = serde_json::to_vec(ops)?; + match zstd::encode_all(ops_json.as_slice(), 3) { + Ok(compressed) => { + map.insert("zops".into(), Value::String(B64.encode(&compressed))); + } + Err(_) => { + map.insert("ops".into(), serde_json::to_value(ops)?); + } + } + } else { + map.insert("ops".into(), serde_json::to_value(ops)?); + } + map.insert("zc".into(), Value::Number(1.into())); if !self.disable_padding { add_random_pad(&mut map); } @@ -3229,17 +3248,35 @@ impl DomainFronter { // credentials), and even at debug level a leaked log line ends // up in user-shared bug reports. Status + length are sufficient // for diagnosis; full body is available behind RUST_LOG=trace. - tracing::debug!( - "batch response: status={} body_len={}", + tracing::info!( + "batch response: status={} body={}", status, - json_str.len() + &json_str[..json_str.len().min(200)] ); tracing::trace!( "batch response body (trace only): {}", &json_str[..json_str.len().min(500)] ); - match serde_json::from_str(json_str) { - Ok(v) => Ok(v), + match serde_json::from_str::(json_str) { + Ok(mut resp) => { + if let Some(zr_b64) = resp.zr.take() { + match B64.decode(&zr_b64) { + Ok(compressed) => match zstd::decode_all(compressed.as_slice()) { + Ok(decompressed) => match serde_json::from_slice(&decompressed) { + Ok(r) => { resp.r = r; } + Err(e) => tracing::error!("zr json parse failed: {}", e), + }, + Err(e) => tracing::error!("zr zstd decompress failed: {}", e), + }, + Err(e) => tracing::error!("zr base64 decode failed: {}", e), + } + } + if resp.zc.is_some() && !self.zstd_enabled.load(Ordering::Relaxed) { + tracing::info!("tunnel-node supports zstd, enabling compressed batches"); + self.zstd_enabled.store(true, Ordering::Relaxed); + } + Ok(resp) + } Err(e) => { // Same redaction policy on the error path. Length and // the serde error message are enough to locate the diff --git a/src/tunnel_client.rs b/src/tunnel_client.rs index 54c21ffa..f539aa24 100644 --- a/src/tunnel_client.rs +++ b/src/tunnel_client.rs @@ -73,7 +73,7 @@ const INFLIGHT_OPTIMIST: usize = 2; /// Maximum pipeline depth when data is actively flowing. Ramps up on /// data-bearing replies, drops back to IDLE after consecutive empties. -const INFLIGHT_ACTIVE: usize = 4; +const INFLIGHT_ACTIVE: usize = 6; /// How many consecutive empty replies before dropping from active to idle depth. const INFLIGHT_COOLDOWN: u32 = 3; diff --git a/tunnel-node/Cargo.lock b/tunnel-node/Cargo.lock index 61069e86..ddb14303 100644 --- a/tunnel-node/Cargo.lock +++ b/tunnel-node/Cargo.lock @@ -119,6 +119,18 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" @@ -150,6 +162,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "flate2" version = "1.1.9" @@ -208,6 +226,18 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -216,7 +246,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -346,6 +376,16 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.95" @@ -414,6 +454,7 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "zstd", ] [[package]] @@ -470,6 +511,12 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + [[package]] name = "prettyplease" version = "0.2.37" @@ -498,6 +545,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -614,6 +667,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -813,7 +872,7 @@ version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ - "getrandom", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -1041,3 +1100,31 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/tunnel-node/Cargo.toml b/tunnel-node/Cargo.toml index acceb880..61710fda 100644 --- a/tunnel-node/Cargo.toml +++ b/tunnel-node/Cargo.toml @@ -18,6 +18,7 @@ uuid = { version = "1", features = ["v4"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } flate2 = "1" +zstd = "0.13" [profile.release] panic = "abort" diff --git a/tunnel-node/src/main.rs b/tunnel-node/src/main.rs index 33809086..28aa5932 100644 --- a/tunnel-node/src/main.rs +++ b/tunnel-node/src/main.rs @@ -676,7 +676,12 @@ impl TunnelResponse { #[derive(Deserialize)] struct BatchRequest { k: String, + #[serde(default)] ops: Vec, + #[serde(default)] + zops: Option, + #[serde(default)] + zc: Option, } #[derive(Deserialize)] @@ -772,6 +777,42 @@ async fn handle_batch( } }; + let had_zops = req.zops.is_some(); + let client_zstd = had_zops || req.zc.is_some(); + tracing::info!("batch: had_zops={} zc={:?} client_zstd={} ops_len={}", had_zops, req.zc, client_zstd, req.ops.len()); + let ops: Vec = if let Some(zops_b64) = req.zops { + tracing::info!("zops received: len={} first_bytes={}", zops_b64.len(), &zops_b64[..zops_b64.len().min(40)]); + match B64.decode(&zops_b64) { + Ok(compressed) => match zstd::decode_all(compressed.as_slice()) { + Ok(decompressed) => { + tracing::info!("zops decompressed: {} bytes, content={}", decompressed.len(), String::from_utf8_lossy(&decompressed[..decompressed.len().min(200)])); + match serde_json::from_slice(&decompressed) { + Ok(v) => v, + Err(e) => { + let resp = serde_json::to_vec(&BatchResponse { + r: vec![TunnelResponse::error(format!("zops json: {}", e))], + }).unwrap_or_default(); + return (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], resp); + } + }}, + Err(e) => { + let resp = serde_json::to_vec(&BatchResponse { + r: vec![TunnelResponse::error(format!("zstd decode: {}", e))], + }).unwrap_or_default(); + return (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], resp); + } + }, + Err(e) => { + let resp = serde_json::to_vec(&BatchResponse { + r: vec![TunnelResponse::error(format!("zops b64: {}", e))], + }).unwrap_or_default(); + return (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], resp); + } + } + } else { + req.ops + }; + if req.k != *state.auth_key { if state.diagnostic_mode { let resp = serde_json::to_vec(&BatchResponse { @@ -804,7 +845,7 @@ async fn handle_batch( // `connect_data` dominates in practice (new clients), but `connect` // still fires from server-speaks-first ports and from the preread // timeout fallback path. - let mut results: Vec<(usize, TunnelResponse)> = Vec::with_capacity(req.ops.len()); + let mut results: Vec<(usize, TunnelResponse)> = Vec::with_capacity(ops.len()); // Each drain entry carries the session's `Arc<…Inner>` alongside the // sid. Phase 2 drains through the Arc directly so the global sessions // map lock isn't held across the per-session read_buf / packets @@ -825,7 +866,7 @@ async fn handle_batch( } let mut new_conn_jobs: JoinSet<(usize, NewConn)> = JoinSet::new(); - for (i, op) in req.ops.iter().enumerate() { + for (i, op) in ops.iter().enumerate() { match op.op.as_str() { "connect" => { had_writes_or_connects = true; @@ -1213,11 +1254,23 @@ async fn handle_batch( // Sort results by original index and build response results.sort_by_key(|(i, _)| *i); - let batch_resp = BatchResponse { - r: results.into_iter().map(|(_, r)| r).collect(), + let r_vec: Vec = results.into_iter().map(|(_, r)| r).collect(); + + tracing::info!("batch response: r_count={} client_zstd={}", r_vec.len(), client_zstd); + let json = if client_zstd { + let r_json = serde_json::to_vec(&r_vec).unwrap_or_default(); + match zstd::encode_all(r_json.as_slice(), 3) { + Ok(compressed) => { + let zr_b64 = B64.encode(&compressed); + tracing::info!("batch response: sending zr ({} bytes compressed)", compressed.len()); + serde_json::to_vec(&serde_json::json!({"zr": zr_b64, "zc": 1})).unwrap_or_default() + } + Err(_) => serde_json::to_vec(&BatchResponse { r: r_vec }).unwrap_or_default(), + } + } else { + serde_json::to_vec(&BatchResponse { r: r_vec }).unwrap_or_default() }; - let json = serde_json::to_vec(&batch_resp).unwrap_or_default(); (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], json) } From 9b0978316de349732fa8f6b31166b7bed3b9328e Mon Sep 17 00:00:00 2001 From: therealaleph Date: Wed, 20 May 2026 13:32:09 +0300 Subject: [PATCH 2/2] fix: keep compressed tunnel logs redacted --- src/domain_fronter.rs | 6 +++--- tunnel-node/src/main.rs | 38 +++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/domain_fronter.rs b/src/domain_fronter.rs index b38e66fb..3fcfee5f 100644 --- a/src/domain_fronter.rs +++ b/src/domain_fronter.rs @@ -3248,10 +3248,10 @@ impl DomainFronter { // credentials), and even at debug level a leaked log line ends // up in user-shared bug reports. Status + length are sufficient // for diagnosis; full body is available behind RUST_LOG=trace. - tracing::info!( - "batch response: status={} body={}", + tracing::debug!( + "batch response: status={} body_len={}", status, - &json_str[..json_str.len().min(200)] + json_str.len() ); tracing::trace!( "batch response body (trace only): {}", diff --git a/tunnel-node/src/main.rs b/tunnel-node/src/main.rs index 28aa5932..a082f4b6 100644 --- a/tunnel-node/src/main.rs +++ b/tunnel-node/src/main.rs @@ -777,15 +777,32 @@ async fn handle_batch( } }; + if req.k != *state.auth_key { + 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 = "\r\n404 Not Found\r\n\ + \r\n

404 Not Found

\r\n\ +
nginx
\r\n\r\n\r\n" + .as_bytes() + .to_vec(); + return (StatusCode::NOT_FOUND, [(header::CONTENT_TYPE, "text/html")], body); + } + let had_zops = req.zops.is_some(); let client_zstd = had_zops || req.zc.is_some(); tracing::info!("batch: had_zops={} zc={:?} client_zstd={} ops_len={}", had_zops, req.zc, client_zstd, req.ops.len()); let ops: Vec = if let Some(zops_b64) = req.zops { - tracing::info!("zops received: len={} first_bytes={}", zops_b64.len(), &zops_b64[..zops_b64.len().min(40)]); + tracing::debug!("zops received: encoded_len={}", zops_b64.len()); match B64.decode(&zops_b64) { Ok(compressed) => match zstd::decode_all(compressed.as_slice()) { Ok(decompressed) => { - tracing::info!("zops decompressed: {} bytes, content={}", decompressed.len(), String::from_utf8_lossy(&decompressed[..decompressed.len().min(200)])); + tracing::debug!("zops decompressed: {} bytes", decompressed.len()); match serde_json::from_slice(&decompressed) { Ok(v) => v, Err(e) => { @@ -813,23 +830,6 @@ async fn handle_batch( req.ops }; - if req.k != *state.auth_key { - 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 = "\r\n404 Not Found\r\n\ - \r\n

404 Not Found

\r\n\ -
nginx
\r\n\r\n\r\n" - .as_bytes() - .to_vec(); - return (StatusCode::NOT_FOUND, [(header::CONTENT_TYPE, "text/html")], body); - } - // Process all ops in two phases. // // Phase 1: dispatch new connections concurrently and write outbound