Skip to content

Commit 124d0c3

Browse files
therealalephclaude
andcommitted
feat: add block_quic config option (#213)
w0l4i has been asking for client-side QUIC block since #213. Now implemented as a small config flag. When `block_quic = true`, the SOCKS5 UDP relay drops any datagram destined for port 443 — that's HTTP/3-over-UDP. The client's QUIC stack retries a couple of times and then falls back to TCP/HTTPS through the regular CONNECT path (which goes through the relay normally). Why client-side rather than server-side udpgw block: the udpgw block in #222 is bound to Full mode + Android tun2proxy. This covers everyone — apps_script users, desktop, Full mode, all the same path. Skipping at the SOCKS5 layer rather than the tunnel-node layer also avoids paying 200–500 ms tunnel-node round-trip per QUIC datagram drop, which compounds during browser retries. Silent drop is the contractually correct shape: SOCKS5 UDP wire has no `host unreachable` reply (RFC 1928 §6 only defines that for TCP CONNECT). Browsers' QUIC stacks have a "no response → fall back" timeout, so silent drop matches what the protocol expects. Default false (opt-in) — udpgw mitigates QUIC partly via persistent sockets, and a tiny minority of sites only support HTTP/3. Will ship in v1.7.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2a5946f commit 124d0c3

2 files changed

Lines changed: 56 additions & 0 deletions

File tree

src/config.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,33 @@ pub struct Config {
163163
/// Issues #39, #127.
164164
#[serde(default)]
165165
pub passthrough_hosts: Vec<String>,
166+
167+
/// Block outbound QUIC (UDP/443) at the SOCKS5 listener.
168+
///
169+
/// QUIC is HTTP/3-over-UDP. In `apps_script` mode it's hopeless —
170+
/// Apps Script is HTTP-only, so QUIC datagrams either get refused
171+
/// outright (UDP ASSOCIATE rejected) or silently fall through to
172+
/// `raw-tcp direct` and fail in interesting ways. In `full` mode
173+
/// the tunnel-node CAN carry UDP, but QUIC's congestion control
174+
/// stacked on top of TCP-encapsulated transport produces TCP
175+
/// meltdown for any non-trivial bandwidth — browsers see <1 Mbps
176+
/// where the same site over plain HTTPS would do >50.
177+
///
178+
/// With `block_quic = true`, the SOCKS5 UDP relay drops any
179+
/// datagram destined for port 443 (silent UDP — caller's stack
180+
/// retries a few times then falls back). Browsers then re-issue
181+
/// the same request as TCP/HTTPS through the regular CONNECT
182+
/// path, which goes through the relay normally.
183+
///
184+
/// Why this is opt-in rather than always-on: for users on Full
185+
/// mode + udpgw (a recent path; v1.7.0+) the QUIC TCP-meltdown
186+
/// is partially mitigated by udpgw's persistent-socket reuse,
187+
/// and a tiny minority of sites only support HTTP/3 (rare). The
188+
/// flag lets users who care about consistency over peak speed
189+
/// opt out of QUIC at the source rather than discovering its
190+
/// failure modes later. Issue #213.
191+
#[serde(default)]
192+
pub block_quic: bool,
166193
}
167194

168195
fn default_fetch_ips_from_api() -> bool { false }

src/proxy_server.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,10 @@ pub struct RewriteCtx {
195195
/// and pass through as plain TCP (optionally via upstream_socks5).
196196
/// See config.rs `passthrough_hosts` for matching rules. Issues #39, #127.
197197
pub passthrough_hosts: Vec<String>,
198+
/// If true, drop SOCKS5 UDP datagrams destined for port 443 so
199+
/// callers fall back to TCP/HTTPS. See config.rs `block_quic` for
200+
/// the trade-off. Issue #213.
201+
pub block_quic: bool,
198202
}
199203

200204
/// True if `host` matches any entry in the user's passthrough list.
@@ -263,6 +267,7 @@ impl ProxyServer {
263267
mode,
264268
youtube_via_relay: config.youtube_via_relay,
265269
passthrough_hosts: config.passthrough_hosts.clone(),
270+
block_quic: config.block_quic,
266271
});
267272

268273
let socks5_port = config.socks5_port.unwrap_or(config.listen_port + 1);
@@ -864,6 +869,30 @@ async fn handle_socks5_udp_associate(
864869
continue;
865870
};
866871

872+
// Issue #213: client-side QUIC block. UDP/443 is
873+
// HTTP/3 — drop the datagram silently so the client
874+
// stack retries a couple of times and then falls back
875+
// to TCP/HTTPS, which goes through the regular CONNECT
876+
// path. Skipping this at the SOCKS5 layer (rather than
877+
// letting it hit the tunnel-node) avoids paying the
878+
// 200–500 ms tunnel-node round-trip per dropped QUIC
879+
// datagram, which would otherwise compound during the
880+
// 1–3 retries before the browser falls back.
881+
//
882+
// Silent drop instead of an explicit error reply: the
883+
// SOCKS5 UDP wire has no "destination unreachable"
884+
// datagram — `0x04` only exists in TCP CONNECT replies
885+
// (RFC 1928 §6). The browser's QUIC stack already has
886+
// a "no response → fall back" timeout, so silent drop
887+
// is the contractually correct shape.
888+
if rewrite_ctx.block_quic && target.port == 443 {
889+
tracing::debug!(
890+
"udp dropped: block_quic=true, target {}:443",
891+
target.host
892+
);
893+
continue;
894+
}
895+
867896
// RFC 1928 §6: lock to the first VALID datagram's source
868897
// port. Subsequent datagrams must come from the same
869898
// (ip, port) pair.

0 commit comments

Comments
 (0)