fix: resolve VPN SOCKS5 domain CONNECT requests through the tunnel

This commit is contained in:
zhom
2026-07-07 22:43:32 +04:00
parent 2c4163383d
commit 5a46d0e266
2 changed files with 129 additions and 17 deletions
+1 -1
View File
@@ -107,7 +107,7 @@ quick-xml = { version = "0.40", features = ["serialize"] }
# VPN support
boringtun = "0.7"
smoltcp = { version = "0.13", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-udp"] }
smoltcp = { version = "0.13", default-features = false, features = ["std", "medium-ip", "proto-ipv4", "proto-ipv6", "socket-tcp", "socket-udp", "socket-dns"] }
# Tray icon decoding (main-process system tray)
image = "0.25"
+128 -16
View File
@@ -3,6 +3,7 @@ use boringtun::noise::{Tunn, TunnResult};
use boringtun::x25519::{PublicKey, StaticSecret};
use smoltcp::iface::{Config as IfaceConfig, Interface, SocketHandle, SocketSet};
use smoltcp::phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken};
use smoltcp::socket::dns;
use smoltcp::socket::tcp::{Socket as TcpSocket, SocketBuffer};
use smoltcp::socket::udp;
use smoltcp::time::Instant as SmolInstant;
@@ -483,8 +484,31 @@ impl WireGuardSocks5Server {
actual_port
);
// DNS resolution for domain-name CONNECT requests must go THROUGH the tunnel, never
// the host resolver (which would leak the query to the local network — the
// whole point of the VPN). Resolve via the WireGuard config's DNS server
// (default 1.1.1.1, still routed through the tunnel). `dns_servers` must
// outlive `sockets` since the socket borrows it, so declare it first.
let dns_servers = [self
.config
.dns
.as_deref()
.and_then(|s| s.trim().parse::<std::net::Ipv4Addr>().ok())
.map(|v4| {
let o = v4.octets();
IpAddress::Ipv4(Ipv4Address::new(o[0], o[1], o[2], o[3]))
})
.unwrap_or_else(|| IpAddress::Ipv4(Ipv4Address::new(1, 1, 1, 1)))];
let mut sockets = SocketSet::new(vec![]);
// Single DNS socket shared by all connections; queries are driven through
// the tunnel by iface.poll like every other socket. Build the query storage
// without vec![None; N] since DnsQuery isn't Clone.
let dns_queries: Vec<Option<dns::DnsQuery>> = (0..64).map(|_| None).collect();
let dns_socket = dns::Socket::new(&dns_servers, dns_queries);
let dns_handle = sockets.add(dns_socket);
// A live SOCKS5 UDP ASSOCIATE: the loopback relay socket the browser sends
// datagrams to, and the browser's learned source address. The tunnel-side
// smoltcp UDP socket lives in `sockets`, keyed by the connection's
@@ -494,6 +518,12 @@ impl WireGuardSocks5Server {
client_addr: Option<SocketAddr>,
}
// An in-flight through-tunnel DNS resolution for a domain-name CONNECT.
struct PendingDns {
query: dns::QueryHandle,
port: u16,
}
struct Connection {
smol_handle: SocketHandle,
tcp_stream: TcpStream,
@@ -503,6 +533,7 @@ impl WireGuardSocks5Server {
read_buf: Vec<u8>,
dest_addr: Option<SocketAddr>,
udp: Option<UdpAssoc>,
pending_dns: Option<PendingDns>,
}
let mut connections: Vec<Connection> = Vec::new();
@@ -527,6 +558,7 @@ impl WireGuardSocks5Server {
read_buf: Vec::new(),
dest_addr: None,
udp: None,
pending_dns: None,
});
}
@@ -543,7 +575,67 @@ impl WireGuardSocks5Server {
// Process each connection
let mut completed = Vec::new();
for (idx, conn) in connections.iter_mut().enumerate() {
if conn.connecting {
if let Some(pending) = conn.pending_dns.take() {
// A through-tunnel DNS resolution is in flight for this connection.
let result = {
let dns_sock = sockets.get_mut::<dns::Socket>(dns_handle);
dns_sock.get_query_result(pending.query)
};
match result {
Ok(addrs) => {
// Tunnel routing is IPv4-only, so take the first A record.
let resolved = addrs.iter().copied().find_map(|a| match a {
IpAddress::Ipv4(v4) => Some(v4),
_ => None,
});
match resolved {
Some(v4) => {
let o = v4.octets();
conn.dest_addr = Some(SocketAddr::new(
std::net::IpAddr::V4(std::net::Ipv4Addr::new(o[0], o[1], o[2], o[3])),
pending.port,
));
let socket = sockets.get_mut::<TcpSocket>(conn.smol_handle);
let local_port = 10000 + (rand::random::<u16>() % 50000);
if socket
.connect(
iface.context(),
(IpAddress::Ipv4(v4), pending.port),
local_port,
)
.is_err()
{
let _ = conn
.tcp_stream
.try_write(&[0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
completed.push(idx);
continue;
}
conn.connecting = true;
}
None => {
// Resolved, but no usable A record — host unreachable.
let _ = conn
.tcp_stream
.try_write(&[0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
completed.push(idx);
continue;
}
}
}
Err(dns::GetQueryResultError::Pending) => {
// Still resolving; put it back and check again next poll.
conn.pending_dns = Some(pending);
}
Err(_) => {
let _ = conn
.tcp_stream
.try_write(&[0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
completed.push(idx);
continue;
}
}
} else if conn.connecting {
let socket = sockets.get_mut::<TcpSocket>(conn.smol_handle);
if socket.may_send() {
let _ = conn.tcp_stream.try_write(&[
@@ -645,27 +737,42 @@ impl WireGuardSocks5Server {
let port_start = 5 + domain_len;
let port =
u16::from_be_bytes([conn.read_buf[port_start], conn.read_buf[port_start + 1]]);
// Resolve domain
match format!("{}:{}", domain, port).to_socket_addrs() {
Ok(mut addrs) => {
if let Some(addr) = addrs.next() {
(addr, needed)
} else {
// Send SOCKS5 error: host unreachable
if cmd == 0x03 {
// UDP ASSOCIATE ignores the DST address (the relay learns the
// client's source from its first datagram), so no resolution
// is needed — never resolve it on the host.
(
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), port),
needed,
)
} else {
// TCP CONNECT: resolve the domain THROUGH THE TUNNEL via the
// config DNS server, never the host resolver (which would leak
// the query onto the local network). Start an async query and
// defer the connection until it resolves.
conn.read_buf.drain(..needed);
let dns_sock = sockets.get_mut::<dns::Socket>(dns_handle);
match dns_sock.start_query(
iface.context(),
&domain,
smoltcp::wire::DnsQueryType::A,
) {
Ok(query) => {
conn.pending_dns = Some(PendingDns { query, port });
}
Err(e) => {
log::warn!(
"[vpn-worker] Failed to start DNS query for {}: {:?}",
domain,
e
);
let _ = conn
.tcp_stream
.try_write(&[0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
completed.push(idx);
continue;
}
}
Err(_) => {
let _ = conn
.tcp_stream
.try_write(&[0x05, 0x04, 0x00, 0x01, 0, 0, 0, 0, 0, 0]);
completed.push(idx);
continue;
}
continue;
}
}
0x04 => {
@@ -888,6 +995,11 @@ impl WireGuardSocks5Server {
completed.dedup();
for idx in completed.into_iter().rev() {
let conn = connections.remove(idx);
if let Some(pending) = conn.pending_dns {
sockets
.get_mut::<dns::Socket>(dns_handle)
.cancel_query(pending.query);
}
sockets.remove(conn.smol_handle);
}