Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/guide.fa.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 کار می‌کند.
Expand Down
2 changes: 2 additions & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
109 changes: 100 additions & 9 deletions src/proxy_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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://…`).
//
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -1626,6 +1647,7 @@ async fn dispatch_tunnel(
mitm: Arc<Mutex<MitmCertManager>>,
rewrite_ctx: Arc<RewriteCtx>,
tunnel_mux: Option<Arc<TunnelMux>>,
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
Expand All @@ -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(());
}

Expand Down Expand Up @@ -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(());
}

Expand Down Expand Up @@ -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(());
}

Expand All @@ -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(());
}
};
Expand All @@ -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(());
}
};
Expand Down Expand Up @@ -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(())
}

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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<TcpStream> {
Expand Down Expand Up @@ -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);
Expand Down
Loading