diff --git a/README.md b/README.md index 6d609c4..4ca3c19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # tun2proxy -Tunnel TCP traffic through SOCKS5 or HTTP on Linux. +Tunnel TCP traffic through SOCKS or HTTP on Linux. **Error handling incomplete and too restrictive.** @@ -12,7 +12,7 @@ cargo build --release ## Setup A standard setup, which would route all traffic from your system through the tunnel interface, could look as follows: ```shell -# The proxy type can be either SOCKS5 or HTTP. +# The proxy type can be either SOCKS4, SOCKS5 or HTTP. PROXY_TYPE=SOCKS5 PROXY_IP=1.2.3.4 PROXY_PORT=1080 @@ -67,7 +67,7 @@ Options: -h, --help Print help -V, --version Print version ``` -Currently, tun2proxy supports two proxy protocols: HTTP and SOCKS5. A proxy is supplied to the `--proxy` argument in the +Currently, tun2proxy supports HTTP, SOCKS4/SOCKS4a and SOCKS5. A proxy is supplied to the `--proxy` argument in the URL format. For example, an HTTP proxy at `1.2.3.4:3128` with a username of `john.doe` and a password of `secret` is supplied as `--proxy http://john.doe:secret@1.2.3.4:3128`. This works analogously to curl's `--proxy` argument. @@ -96,5 +96,4 @@ requests for IPv6 addresses. - Improve handling of half-open connections - Increase error robustness (reduce `unwrap` and `expect` usage) - UDP support for SOCKS -- SOCKS4/SOCKS4a support - Native support for proxying DNS over TCP or TLS diff --git a/src/http.rs b/src/http.rs index 85a8f4b..33dfcc6 100644 --- a/src/http.rs +++ b/src/http.rs @@ -175,11 +175,11 @@ impl ConnectionManager for HttpManager { &self, connection: &Connection, manager: Rc, - ) -> Option> { + ) -> Result>, Error> { if connection.proto != IpProtocol::Tcp { - return None; + return Ok(None); } - Some(Box::new(HttpConnection::new(connection, manager))) + Ok(Some(Box::new(HttpConnection::new(connection, manager)))) } fn close_connection(&self, _: &Connection) {} diff --git a/src/lib.rs b/src/lib.rs index 0779247..bf26a58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ use crate::error::Error; +use crate::socks5::SocksVersion; use crate::tun2proxy::{Credentials, Options}; -use crate::{http::HttpManager, socks5::Socks5Manager, tun2proxy::TunToProxy}; +use crate::{http::HttpManager, socks5::SocksManager, tun2proxy::TunToProxy}; use std::net::{SocketAddr, ToSocketAddrs}; pub mod error; @@ -47,6 +48,7 @@ impl Proxy { let scheme = url.scheme(); let proxy_type = match url.scheme().to_ascii_lowercase().as_str() { + "socks4" => Some(ProxyType::Socks4), "socks5" => Some(ProxyType::Socks5), "http" => Some(ProxyType::Http), _ => None, @@ -63,6 +65,7 @@ impl Proxy { #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] pub enum ProxyType { + Socks4, Socks5, Http, } @@ -70,6 +73,7 @@ pub enum ProxyType { impl std::fmt::Display for ProxyType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + ProxyType::Socks4 => write!(f, "socks4"), ProxyType::Socks5 => write!(f, "socks5"), ProxyType::Http => write!(f, "http"), } @@ -79,8 +83,19 @@ impl std::fmt::Display for ProxyType { pub fn main_entry(tun: &str, proxy: Proxy, options: Options) -> Result<(), Error> { let mut ttp = TunToProxy::new(tun, options)?; match proxy.proxy_type { + ProxyType::Socks4 => { + ttp.add_connection_manager(SocksManager::new( + proxy.addr, + SocksVersion::V4, + proxy.credentials, + )); + } ProxyType::Socks5 => { - ttp.add_connection_manager(Socks5Manager::new(proxy.addr, proxy.credentials)); + ttp.add_connection_manager(SocksManager::new( + proxy.addr, + SocksVersion::V5, + proxy.credentials, + )); } ProxyType::Http => { ttp.add_connection_manager(HttpManager::new(proxy.addr, proxy.credentials)); diff --git a/src/socks5.rs b/src/socks5.rs index 60d7383..9dc5b74 100644 --- a/src/socks5.rs +++ b/src/socks5.rs @@ -28,6 +28,12 @@ enum SocksAddressType { Ipv6 = 4, } +#[derive(Copy, Clone)] +pub enum SocksVersion { + V4 = 4, + V5 = 5, +} + #[allow(dead_code)] enum SocksAuthentication { None = 0, @@ -63,10 +69,15 @@ pub(crate) struct SocksConnection { server_outbuf: VecDeque, data_buf: VecDeque, manager: Rc, + version: SocksVersion, } impl SocksConnection { - pub fn new(connection: &Connection, manager: Rc) -> Self { + pub fn new( + connection: &Connection, + manager: Rc, + version: SocksVersion, + ) -> Result { let mut result = Self { connection: connection.clone(), state: SocksState::ServerHello, @@ -76,35 +87,92 @@ impl SocksConnection { server_outbuf: Default::default(), data_buf: Default::default(), manager, + version, }; - result.send_client_hello(); - result + result.send_client_hello()?; + Ok(result) } - fn send_client_hello(&mut self) { + fn send_client_hello(&mut self) -> Result<(), Error> { let credentials = self.manager.get_credentials(); - if credentials.is_some() { - self.server_outbuf - .extend(&[5u8, 1, SocksAuthentication::Password as u8]); - } else { - self.server_outbuf - .extend(&[5u8, 1, SocksAuthentication::None as u8]); + match self.version { + SocksVersion::V4 => { + self.server_outbuf.extend(&[ + 4u8, + 1, + (self.connection.dst.port >> 8) as u8, + (self.connection.dst.port & 0xff) as u8, + ]); + let mut ip_vec = Vec::::new(); + let mut name_vec = Vec::::new(); + match &self.connection.dst.host { + DestinationHost::Address(dst_ip) => { + match dst_ip { + IpAddr::V4(ip) => ip_vec.extend(ip.octets().as_ref()), + IpAddr::V6(_) => return Err("SOCKS4 does not support IPv6".into()), + }; + } + DestinationHost::Hostname(host) => { + ip_vec.extend(&[0, 0, 0, host.len() as u8]); + name_vec.extend(host.as_bytes()); + name_vec.push(0); + } + } + self.server_outbuf.extend(ip_vec); + if let Some(credentials) = credentials { + self.server_outbuf.extend(&credentials.username); + if !credentials.password.is_empty() { + self.server_outbuf.push_back(b':'); + self.server_outbuf.extend(&credentials.password); + } + } + self.server_outbuf.push_back(0); + self.server_outbuf.extend(name_vec); + } + + SocksVersion::V5 => { + if credentials.is_some() { + self.server_outbuf + .extend(&[5u8, 1, SocksAuthentication::Password as u8]); + } else { + self.server_outbuf + .extend(&[5u8, 1, SocksAuthentication::None as u8]); + } + } } self.state = SocksState::ServerHello; + Ok(()) } - fn receive_server_hello(&mut self) -> Result<(), Error> { + fn receive_server_hello_socks4(&mut self) -> Result<(), Error> { + if self.server_inbuf.len() < 8 { + return Ok(()); + } + + if self.server_inbuf[1] != 0x5a { + return Err("SOCKS4 server replied with an unexpected reply code.".into()); + } + + self.server_inbuf.drain(0..8); + self.server_outbuf.append(&mut self.data_buf); + self.data_buf.clear(); + + self.state = SocksState::Established; + self.state_change() + } + + fn receive_server_hello_socks5(&mut self) -> Result<(), Error> { if self.server_inbuf.len() < 2 { return Ok(()); } if self.server_inbuf[0] != 5 { - return Err("SOCKS server replied with an unexpected version.".into()); + return Err("SOCKS5 server replied with an unexpected version.".into()); } if self.server_inbuf[1] != 0 && self.manager.get_credentials().is_none() || self.server_inbuf[1] != 2 && self.manager.get_credentials().is_some() { - return Err("SOCKS server requires an unsupported authentication method.".into()); + return Err("SOCKS5 server requires an unsupported authentication method.".into()); } self.server_inbuf.drain(0..2); @@ -117,6 +185,13 @@ impl SocksConnection { self.state_change() } + fn receive_server_hello(&mut self) -> Result<(), Error> { + match self.version { + SocksVersion::V4 => self.receive_server_hello_socks4(), + SocksVersion::V5 => self.receive_server_hello_socks5(), + } + } + fn send_auth_data(&mut self) -> Result<(), Error> { let tmp = Credentials::default(); let credentials = self.manager.get_credentials().as_ref().unwrap_or(&tmp); @@ -152,18 +227,18 @@ impl SocksConnection { let atyp = self.server_inbuf[3]; if ver != 5 { - return Err("SOCKS server replied with an unexpected version.".into()); + return Err("SOCKS5 server replied with an unexpected version.".into()); } if rep != 0 { - return Err("SOCKS connection unsuccessful.".into()); + return Err("SOCKS5 connection unsuccessful.".into()); } if atyp != SocksAddressType::Ipv4 as u8 && atyp != SocksAddressType::Ipv6 as u8 && atyp != SocksAddressType::DomainName as u8 { - return Err("SOCKS server replied with unrecognized address type.".into()); + return Err("SOCKS5 server replied with unrecognized address type.".into()); } if atyp == SocksAddressType::DomainName as u8 && self.server_inbuf.len() < 5 { @@ -221,6 +296,14 @@ impl SocksConnection { self.state_change() } + fn relay_traffic(&mut self) -> Result<(), Error> { + self.client_outbuf.extend(self.server_inbuf.iter()); + self.server_outbuf.extend(self.client_inbuf.iter()); + self.server_inbuf.clear(); + self.client_inbuf.clear(); + Ok(()) + } + pub fn state_change(&mut self) -> Result<(), Error> { match self.state { SocksState::ServerHello => self.receive_server_hello(), @@ -233,13 +316,7 @@ impl SocksConnection { SocksState::ReceiveResponse => self.receive_connection_status(), - SocksState::Established => { - self.client_outbuf.extend(self.server_inbuf.iter()); - self.server_outbuf.extend(self.client_inbuf.iter()); - self.server_inbuf.clear(); - self.client_inbuf.clear(); - Ok(()) - } + SocksState::Established => self.relay_traffic(), _ => Ok(()), } @@ -292,12 +369,13 @@ impl TcpProxy for SocksConnection { } } -pub struct Socks5Manager { +pub struct SocksManager { server: SocketAddr, credentials: Option, + version: SocksVersion, } -impl ConnectionManager for Socks5Manager { +impl ConnectionManager for SocksManager { fn handles_connection(&self, connection: &Connection) -> bool { connection.proto == IpProtocol::Tcp } @@ -306,11 +384,15 @@ impl ConnectionManager for Socks5Manager { &self, connection: &Connection, manager: Rc, - ) -> Option> { + ) -> Result>, Error> { if connection.proto != IpProtocol::Tcp { - return None; + return Ok(None); } - Some(Box::new(SocksConnection::new(connection, manager))) + Ok(Some(Box::new(SocksConnection::new( + connection, + manager, + self.version, + )?))) } fn close_connection(&self, _: &Connection) {} @@ -324,11 +406,16 @@ impl ConnectionManager for Socks5Manager { } } -impl Socks5Manager { - pub fn new(server: SocketAddr, credentials: Option) -> Rc { +impl SocksManager { + pub fn new( + server: SocketAddr, + version: SocksVersion, + credentials: Option, + ) -> Rc { Rc::new(Self { server, credentials, + version, }) } } diff --git a/src/tun2proxy.rs b/src/tun2proxy.rs index 61f4bcc..65b5284 100644 --- a/src/tun2proxy.rs +++ b/src/tun2proxy.rs @@ -247,7 +247,7 @@ pub(crate) trait ConnectionManager { &self, connection: &Connection, manager: Rc, - ) -> Option>; + ) -> Result>, Error>; fn close_connection(&self, connection: &Connection); fn get_server(&self) -> SocketAddr; fn get_credentials(&self) -> &Option; @@ -428,7 +428,7 @@ impl<'a> TunToProxy<'a> { if first_packet { for manager in self.connection_managers.iter_mut() { if let Some(handler) = - manager.new_connection(&resolved_conn, manager.clone()) + manager.new_connection(&resolved_conn, manager.clone())? { let mut socket = tcp::Socket::new( tcp::SocketBuffer::new(vec![0; 4096]), diff --git a/src/virtdns.rs b/src/virtdns.rs index a7553b4..e21533b 100644 --- a/src/virtdns.rs +++ b/src/virtdns.rs @@ -238,7 +238,7 @@ impl VirtualDns { } } - /// Parse a DNS qname at a specific offset and return the name along with its size. + /// Parse a non-root DNS qname at a specific offset and return the name along with its size. /// DNS packet parsing should be continued after the name. fn parse_qname(data: &[u8], mut offset: usize) -> Option<(String, usize)> { // Since we only parse qnames and qnames can't point anywhere, @@ -255,9 +255,15 @@ impl VirtualDns { } let label_len = data[offset]; if label_len == 0 { + if qname.is_empty() { + return None; + } offset += 1; break; } + if !qname.is_empty() { + qname.push('.'); + } for _ in 0..label_len { offset += 1; if offset >= data.len() { @@ -265,7 +271,6 @@ impl VirtualDns { } qname.push(data[offset] as char); } - qname.push('.'); offset += 1; } diff --git a/tests/proxy.rs b/tests/proxy.rs index add0648..a61bd32 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -35,8 +35,12 @@ mod tests { Ok(Test { proxy }) } - fn tests() -> [Result; 2] { - [test_from_env("SOCKS5_SERVER"), test_from_env("HTTP_SERVER")] + fn tests() -> [Result; 3] { + [ + test_from_env("SOCKS4_SERVER"), + test_from_env("SOCKS5_SERVER"), + test_from_env("HTTP_SERVER"), + ] } #[cfg(test)] @@ -166,6 +170,16 @@ mod tests { env::var(var).unwrap_or_else(|_| panic!("{}", "{var} environment variable required")); } + #[serial] + #[test_log::test] + fn test_socks4() { + require_var("SOCKS4_SERVER"); + run_test( + |test| test.proxy.proxy_type == ProxyType::Socks4, + request_ip_host_http, + ) + } + #[serial] #[test_log::test] fn test_socks5() { @@ -186,6 +200,16 @@ mod tests { ) } + #[serial] + #[test_log::test] + fn test_socks4_dns() { + require_var("SOCKS4_SERVER"); + run_test( + |test| test.proxy.proxy_type == ProxyType::Socks4, + request_example_https, + ) + } + #[serial] #[test_log::test] fn test_socks5_dns() {