diff --git a/docs/guide.fa.md b/docs/guide.fa.md index d0247453..0176373d 100644 --- a/docs/guide.fa.md +++ b/docs/guide.fa.md @@ -206,6 +206,8 @@ upstream_socks5 = "127.0.0.1:50529" HTTP / HTTPS مثل قبل از Apps Script می‌رود (تغییری نمی‌کند)، تونل بازنویسی SNI برای `google.com` / `youtube.com` همچنان از هر دو دور می‌زند — یوتیوب به سرعت قبل می‌ماند و تلگرام هم تونل واقعی پیدا می‌کند. +هدف‌های دامنه‌ای SOCKS5 معنای remote-DNS را حفظ می‌کنند: اگر پروکسی برای TCP خام مجبور شود hostname را محلی resolve کند و نه Full Tunnel و نه `upstream_socks5` در دسترس باشد، به‌جای نشت DNS plaintext، اتصال را fail-closed می‌کند. + ## حالت تونل کامل `"mode": "full"` **تمام** ترافیک را end-to-end از Apps Script و یک [tunnel-node](../tunnel-node/) راه دور رد می‌کند — بدون نیاز به نصب گواهی MITM. TCP به‌صورت سشن‌های پایدار تونل، و UDP از کلاینت‌های اندروید / TUN از طریق SOCKS5 `UDP ASSOCIATE` به tunnel-node که UDP واقعی را از سمت سرور منتشر می‌کند. مبادله: تأخیر بیشتر هر درخواست (هر بایت Apps Script → tunnel-node → مقصد می‌رود)، اما برای هر پروتکل و هر برنامه‌ای بدون نصب CA کار می‌کند. diff --git a/docs/guide.md b/docs/guide.md index 679a35d0..6e505d79 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -206,6 +206,8 @@ upstream_socks5 = "127.0.0.1:50529" HTTP / HTTPS keeps going through Apps Script (no change), and the SNI-rewrite tunnel for `google.com` / `youtube.com` keeps bypassing both — YouTube stays as fast as before while Telegram gets a real tunnel. +SOCKS5 domain targets preserve remote-DNS semantics: if the proxy would have to resolve a hostname locally for raw TCP and neither Full Tunnel nor `upstream_socks5` is available, it fails closed instead of leaking a plaintext DNS lookup. + ## Full Tunnel mode `"mode": "full"` routes **all** traffic end-to-end through Apps Script and a remote [tunnel-node](../tunnel-node/) — no MITM certificate needed. TCP carried as persistent tunnel sessions, UDP from Android / TUN clients via SOCKS5 `UDP ASSOCIATE` to the tunnel-node which emits real UDP server-side. Trade-off: higher per-request latency (every byte goes Apps Script → tunnel-node → destination), but works for any protocol and any app, no CA install required. diff --git a/src/proxy_server.rs b/src/proxy_server.rs index 209bbc58..6d1e8ab6 100644 --- a/src/proxy_server.rs +++ b/src/proxy_server.rs @@ -832,7 +832,17 @@ async fn handle_http_client( sock.write_all(b"HTTP/1.1 200 Connection Established\r\n\r\n") .await?; sock.flush().await?; - dispatch_tunnel(sock, host, port, fronter, mitm, rewrite_ctx, tunnel_mux).await + dispatch_tunnel( + sock, + host, + port, + fronter, + mitm, + rewrite_ctx, + tunnel_mux, + false, + ) + .await } else { // Plain HTTP proxy request (e.g. `GET http://…`). // @@ -960,7 +970,18 @@ async fn handle_socks5_client( .await?; sock.flush().await?; - dispatch_tunnel(sock, host, port, fronter, mitm, rewrite_ctx, tunnel_mux).await + let require_remote_dns = atyp == 0x03; + dispatch_tunnel( + sock, + host, + port, + fronter, + mitm, + rewrite_ctx, + tunnel_mux, + require_remote_dns, + ) + .await } #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -1626,6 +1647,7 @@ async fn dispatch_tunnel( mitm: Arc>, rewrite_ctx: Arc, tunnel_mux: Option>, + require_remote_dns: bool, ) -> std::io::Result<()> { // 0. User-configured passthrough list wins over every other path. // If the host matches `passthrough_hosts`, we raw-TCP it (through @@ -1641,7 +1663,7 @@ async fn dispatch_tunnel( port, via.unwrap_or("direct") ); - plain_tcp_passthrough(sock, &host, port, via).await; + plain_tcp_passthrough(sock, &host, port, via, require_remote_dns).await; return Ok(()); } @@ -1675,7 +1697,7 @@ async fn dispatch_tunnel( port, via.unwrap_or("direct") ); - plain_tcp_passthrough(sock, &host, port, via).await; + plain_tcp_passthrough(sock, &host, port, via, require_remote_dns).await; return Ok(()); } @@ -1761,7 +1783,7 @@ async fn dispatch_tunnel( port, via.unwrap_or("direct") ); - plain_tcp_passthrough(sock, &host, port, via).await; + plain_tcp_passthrough(sock, &host, port, via, require_remote_dns).await; return Ok(()); } @@ -1776,7 +1798,14 @@ async fn dispatch_tunnel( host, port ); - plain_tcp_passthrough(sock, &host, port, rewrite_ctx.upstream_socks5.as_deref()).await; + plain_tcp_passthrough( + sock, + &host, + port, + rewrite_ctx.upstream_socks5.as_deref(), + require_remote_dns, + ) + .await; return Ok(()); } }; @@ -1802,7 +1831,7 @@ async fn dispatch_tunnel( port, via.unwrap_or("direct") ); - plain_tcp_passthrough(sock, &host, port, via).await; + plain_tcp_passthrough(sock, &host, port, via, require_remote_dns).await; return Ok(()); } }; @@ -1843,7 +1872,7 @@ async fn dispatch_tunnel( port, via.unwrap_or("direct") ); - plain_tcp_passthrough(sock, &host, port, via).await; + plain_tcp_passthrough(sock, &host, port, via, require_remote_dns).await; Ok(()) } @@ -1854,8 +1883,18 @@ async fn plain_tcp_passthrough( host: &str, port: u16, upstream_socks5: Option<&str>, + require_remote_dns: bool, ) { let target_host = host.trim_start_matches('[').trim_end_matches(']'); + let target_is_ip = looks_like_ip(target_host); + if should_refuse_local_dns_fallback(require_remote_dns, target_host, upstream_socks5) { + tracing::warn!( + "refusing raw-tcp direct fallback for SOCKS5 domain target {}:{} to avoid local DNS resolution", + host, + port + ); + return; + } // Shorter connect timeout for IP literals (4s vs 10s for hostnames). // Ported from upstream Python 7b1812c: when the target is an IP (i.e. // a raw Telegram DC, or an IP someone hardcoded), and that route is @@ -1867,7 +1906,7 @@ async fn plain_tcp_passthrough( // Hostnames still get 10s because DNS + first-hop TCP genuinely can // take that long on flaky links, and the resolver fallbacks already // trim the worst case. - let connect_timeout = if looks_like_ip(target_host) { + let connect_timeout = if target_is_ip { std::time::Duration::from_secs(4) } else { std::time::Duration::from_secs(10) @@ -1879,6 +1918,16 @@ async fn plain_tcp_passthrough( s } Err(e) => { + if require_remote_dns && !target_is_ip { + tracing::warn!( + "upstream-socks5 {} -> {}:{} failed: {}; refusing local DNS fallback", + proxy, + host, + port, + e + ); + return; + } tracing::warn!( "upstream-socks5 {} -> {}:{} failed: {} (falling back to direct)", proxy, @@ -1928,6 +1977,14 @@ async fn plain_tcp_passthrough( } } +fn should_refuse_local_dns_fallback( + require_remote_dns: bool, + target_host: &str, + upstream_socks5: Option<&str>, +) -> bool { + require_remote_dns && upstream_socks5.is_none() && !looks_like_ip(target_host) +} + /// Open a TCP stream to `(host, port)` through an upstream SOCKS5 proxy /// (no-auth only). Returns the connected stream after SOCKS5 negotiation. async fn socks5_connect_via(proxy: &str, host: &str, port: u16) -> std::io::Result { @@ -3228,6 +3285,40 @@ mod tests { assert_eq!(build_socks5_udp_packet(&target, payload), raw); } + #[test] + fn socks5_remote_dns_refuses_direct_hostname_fallback() { + assert!(should_refuse_local_dns_fallback( + true, + "example.com", + None + )); + assert!(should_refuse_local_dns_fallback( + true, + "sub.example.com", + None + )); + } + + #[test] + fn socks5_remote_dns_allows_paths_that_do_not_need_local_dns() { + assert!(!should_refuse_local_dns_fallback(true, "1.2.3.4", None)); + assert!(!should_refuse_local_dns_fallback( + true, + "2001:db8::1", + None + )); + assert!(!should_refuse_local_dns_fallback( + true, + "example.com", + Some("127.0.0.1:9050") + )); + assert!(!should_refuse_local_dns_fallback( + false, + "example.com", + None + )); + } + #[tokio::test(flavor = "current_thread")] async fn read_body_decodes_chunked_request() { let (mut client, mut server) = duplex(1024);