From 100cb92ad146dac165f3de1923aa5083988fdf1e Mon Sep 17 00:00:00 2001 From: Vladislav Yaroslavlev Date: Fri, 20 Feb 2026 21:42:15 +0300 Subject: [PATCH] feat: add hostname support for SOCKS4/SOCKS5 upstream proxies Previously, SOCKS proxy addresses only accepted IP:port format. Now both IP:port and hostname:port formats are supported. Changes: - Try parsing as SocketAddr first (IP:port) for backward compatibility - Fall back to tokio::net::TcpStream::connect() for hostname resolution - Log warning if interface binding is specified with hostname (not supported) Example usage: [[upstreams]] type = "socks5" address = "proxy.example.com:1080" username = "user" password = "pass" --- src/transport/upstream.rs | 107 +++++++++++++++++++++++--------------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/src/transport/upstream.rs b/src/transport/upstream.rs index 660043f..7d8927d 100644 --- a/src/transport/upstream.rs +++ b/src/transport/upstream.rs @@ -383,32 +383,43 @@ impl UpstreamManager { Ok(stream) }, UpstreamType::Socks4 { address, interface, user_id } => { - let proxy_addr: SocketAddr = address.parse() - .map_err(|_| ProxyError::Config("Invalid SOCKS4 address".to_string()))?; + // Try to parse as SocketAddr first (IP:port), otherwise treat as hostname:port + let mut stream = if let Ok(proxy_addr) = address.parse::() { + // IP:port format - use socket with optional interface binding + let bind_ip = Self::resolve_bind_address( + interface, + &None, + proxy_addr, + bind_rr.as_deref(), + ); - let bind_ip = Self::resolve_bind_address( - interface, - &None, - proxy_addr, - bind_rr.as_deref(), - ); + let socket = create_outgoing_socket_bound(proxy_addr, bind_ip)?; - let socket = create_outgoing_socket_bound(proxy_addr, bind_ip)?; + socket.set_nonblocking(true)?; + match socket.connect(&proxy_addr.into()) { + Ok(()) => {}, + Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, + Err(err) => return Err(ProxyError::Io(err)), + } - socket.set_nonblocking(true)?; - match socket.connect(&proxy_addr.into()) { - Ok(()) => {}, - Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, - Err(err) => return Err(ProxyError::Io(err)), - } + let std_stream: std::net::TcpStream = socket.into(); + let stream = TcpStream::from_std(std_stream)?; - let std_stream: std::net::TcpStream = socket.into(); - let mut stream = TcpStream::from_std(std_stream)?; + stream.writable().await?; + if let Some(e) = stream.take_error()? { + return Err(ProxyError::Io(e)); + } + stream + } else { + // Hostname:port format - use tokio DNS resolution + // Note: interface binding is not supported for hostnames + if interface.is_some() { + warn!("SOCKS4 interface binding is not supported for hostname addresses, ignoring"); + } + TcpStream::connect(address).await + .map_err(ProxyError::Io)? + }; - stream.writable().await?; - if let Some(e) = stream.take_error()? { - return Err(ProxyError::Io(e)); - } // replace socks user_id with config.selected_scope, if set let scope: Option<&str> = Some(config.selected_scope.as_str()) .filter(|s| !s.is_empty()); @@ -418,32 +429,42 @@ impl UpstreamManager { Ok(stream) }, UpstreamType::Socks5 { address, interface, username, password } => { - let proxy_addr: SocketAddr = address.parse() - .map_err(|_| ProxyError::Config("Invalid SOCKS5 address".to_string()))?; + // Try to parse as SocketAddr first (IP:port), otherwise treat as hostname:port + let mut stream = if let Ok(proxy_addr) = address.parse::() { + // IP:port format - use socket with optional interface binding + let bind_ip = Self::resolve_bind_address( + interface, + &None, + proxy_addr, + bind_rr.as_deref(), + ); - let bind_ip = Self::resolve_bind_address( - interface, - &None, - proxy_addr, - bind_rr.as_deref(), - ); + let socket = create_outgoing_socket_bound(proxy_addr, bind_ip)?; - let socket = create_outgoing_socket_bound(proxy_addr, bind_ip)?; + socket.set_nonblocking(true)?; + match socket.connect(&proxy_addr.into()) { + Ok(()) => {}, + Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, + Err(err) => return Err(ProxyError::Io(err)), + } - socket.set_nonblocking(true)?; - match socket.connect(&proxy_addr.into()) { - Ok(()) => {}, - Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, - Err(err) => return Err(ProxyError::Io(err)), - } + let std_stream: std::net::TcpStream = socket.into(); + let stream = TcpStream::from_std(std_stream)?; - let std_stream: std::net::TcpStream = socket.into(); - let mut stream = TcpStream::from_std(std_stream)?; - - stream.writable().await?; - if let Some(e) = stream.take_error()? { - return Err(ProxyError::Io(e)); - } + stream.writable().await?; + if let Some(e) = stream.take_error()? { + return Err(ProxyError::Io(e)); + } + stream + } else { + // Hostname:port format - use tokio DNS resolution + // Note: interface binding is not supported for hostnames + if interface.is_some() { + warn!("SOCKS5 interface binding is not supported for hostname addresses, ignoring"); + } + TcpStream::connect(address).await + .map_err(ProxyError::Io)? + }; debug!(config = ?config, "Socks5 connection"); // replace socks user:pass with config.selected_scope, if set