Merge SOCKS4 support

The SOCKS4 tests in the CI pipeline will currently fail, as no SOCKS4
test server for automated testing has been set up yet.
This commit is contained in:
B. Blechschmidt 2023-03-25 01:53:30 +01:00
commit a5aac8c0a4
7 changed files with 175 additions and 45 deletions

View file

@ -1,5 +1,5 @@
# tun2proxy # 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.** **Error handling incomplete and too restrictive.**
@ -12,7 +12,7 @@ cargo build --release
## Setup ## Setup
A standard setup, which would route all traffic from your system through the tunnel interface, could look as follows: A standard setup, which would route all traffic from your system through the tunnel interface, could look as follows:
```shell ```shell
# The proxy type can be either SOCKS5 or HTTP. # The proxy type can be either SOCKS4, SOCKS5 or HTTP.
PROXY_TYPE=SOCKS5 PROXY_TYPE=SOCKS5
PROXY_IP=1.2.3.4 PROXY_IP=1.2.3.4
PROXY_PORT=1080 PROXY_PORT=1080
@ -67,7 +67,7 @@ Options:
-h, --help Print help -h, --help Print help
-V, --version Print version -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 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. 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 - Improve handling of half-open connections
- Increase error robustness (reduce `unwrap` and `expect` usage) - Increase error robustness (reduce `unwrap` and `expect` usage)
- UDP support for SOCKS - UDP support for SOCKS
- SOCKS4/SOCKS4a support
- Native support for proxying DNS over TCP or TLS - Native support for proxying DNS over TCP or TLS

View file

@ -175,11 +175,11 @@ impl ConnectionManager for HttpManager {
&self, &self,
connection: &Connection, connection: &Connection,
manager: Rc<dyn ConnectionManager>, manager: Rc<dyn ConnectionManager>,
) -> Option<Box<dyn TcpProxy>> { ) -> Result<Option<Box<dyn TcpProxy>>, Error> {
if connection.proto != IpProtocol::Tcp { 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) {} fn close_connection(&self, _: &Connection) {}

View file

@ -1,6 +1,7 @@
use crate::error::Error; use crate::error::Error;
use crate::socks5::SocksVersion;
use crate::tun2proxy::{Credentials, Options}; 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}; use std::net::{SocketAddr, ToSocketAddrs};
pub mod error; pub mod error;
@ -47,6 +48,7 @@ impl Proxy {
let scheme = url.scheme(); let scheme = url.scheme();
let proxy_type = match url.scheme().to_ascii_lowercase().as_str() { let proxy_type = match url.scheme().to_ascii_lowercase().as_str() {
"socks4" => Some(ProxyType::Socks4),
"socks5" => Some(ProxyType::Socks5), "socks5" => Some(ProxyType::Socks5),
"http" => Some(ProxyType::Http), "http" => Some(ProxyType::Http),
_ => None, _ => None,
@ -63,6 +65,7 @@ impl Proxy {
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum ProxyType { pub enum ProxyType {
Socks4,
Socks5, Socks5,
Http, Http,
} }
@ -70,6 +73,7 @@ pub enum ProxyType {
impl std::fmt::Display for ProxyType { impl std::fmt::Display for ProxyType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
ProxyType::Socks4 => write!(f, "socks4"),
ProxyType::Socks5 => write!(f, "socks5"), ProxyType::Socks5 => write!(f, "socks5"),
ProxyType::Http => write!(f, "http"), 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> { pub fn main_entry(tun: &str, proxy: Proxy, options: Options) -> Result<(), Error> {
let mut ttp = TunToProxy::new(tun, options)?; let mut ttp = TunToProxy::new(tun, options)?;
match proxy.proxy_type { match proxy.proxy_type {
ProxyType::Socks4 => {
ttp.add_connection_manager(SocksManager::new(
proxy.addr,
SocksVersion::V4,
proxy.credentials,
));
}
ProxyType::Socks5 => { 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 => { ProxyType::Http => {
ttp.add_connection_manager(HttpManager::new(proxy.addr, proxy.credentials)); ttp.add_connection_manager(HttpManager::new(proxy.addr, proxy.credentials));

View file

@ -28,6 +28,12 @@ enum SocksAddressType {
Ipv6 = 4, Ipv6 = 4,
} }
#[derive(Copy, Clone)]
pub enum SocksVersion {
V4 = 4,
V5 = 5,
}
#[allow(dead_code)] #[allow(dead_code)]
enum SocksAuthentication { enum SocksAuthentication {
None = 0, None = 0,
@ -63,10 +69,15 @@ pub(crate) struct SocksConnection {
server_outbuf: VecDeque<u8>, server_outbuf: VecDeque<u8>,
data_buf: VecDeque<u8>, data_buf: VecDeque<u8>,
manager: Rc<dyn ConnectionManager>, manager: Rc<dyn ConnectionManager>,
version: SocksVersion,
} }
impl SocksConnection { impl SocksConnection {
pub fn new(connection: &Connection, manager: Rc<dyn ConnectionManager>) -> Self { pub fn new(
connection: &Connection,
manager: Rc<dyn ConnectionManager>,
version: SocksVersion,
) -> Result<Self, Error> {
let mut result = Self { let mut result = Self {
connection: connection.clone(), connection: connection.clone(),
state: SocksState::ServerHello, state: SocksState::ServerHello,
@ -76,35 +87,92 @@ impl SocksConnection {
server_outbuf: Default::default(), server_outbuf: Default::default(),
data_buf: Default::default(), data_buf: Default::default(),
manager, manager,
version,
}; };
result.send_client_hello(); result.send_client_hello()?;
result Ok(result)
} }
fn send_client_hello(&mut self) { fn send_client_hello(&mut self) -> Result<(), Error> {
let credentials = self.manager.get_credentials(); let credentials = self.manager.get_credentials();
if credentials.is_some() { match self.version {
self.server_outbuf SocksVersion::V4 => {
.extend(&[5u8, 1, SocksAuthentication::Password as u8]); self.server_outbuf.extend(&[
} else { 4u8,
self.server_outbuf 1,
.extend(&[5u8, 1, SocksAuthentication::None as u8]); (self.connection.dst.port >> 8) as u8,
(self.connection.dst.port & 0xff) as u8,
]);
let mut ip_vec = Vec::<u8>::new();
let mut name_vec = Vec::<u8>::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; 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 { if self.server_inbuf.len() < 2 {
return Ok(()); return Ok(());
} }
if self.server_inbuf[0] != 5 { 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() if self.server_inbuf[1] != 0 && self.manager.get_credentials().is_none()
|| self.server_inbuf[1] != 2 && self.manager.get_credentials().is_some() || 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); self.server_inbuf.drain(0..2);
@ -117,6 +185,13 @@ impl SocksConnection {
self.state_change() 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> { fn send_auth_data(&mut self) -> Result<(), Error> {
let tmp = Credentials::default(); let tmp = Credentials::default();
let credentials = self.manager.get_credentials().as_ref().unwrap_or(&tmp); let credentials = self.manager.get_credentials().as_ref().unwrap_or(&tmp);
@ -152,18 +227,18 @@ impl SocksConnection {
let atyp = self.server_inbuf[3]; let atyp = self.server_inbuf[3];
if ver != 5 { 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 { if rep != 0 {
return Err("SOCKS connection unsuccessful.".into()); return Err("SOCKS5 connection unsuccessful.".into());
} }
if atyp != SocksAddressType::Ipv4 as u8 if atyp != SocksAddressType::Ipv4 as u8
&& atyp != SocksAddressType::Ipv6 as u8 && atyp != SocksAddressType::Ipv6 as u8
&& atyp != SocksAddressType::DomainName 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 { if atyp == SocksAddressType::DomainName as u8 && self.server_inbuf.len() < 5 {
@ -221,6 +296,14 @@ impl SocksConnection {
self.state_change() 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> { pub fn state_change(&mut self) -> Result<(), Error> {
match self.state { match self.state {
SocksState::ServerHello => self.receive_server_hello(), SocksState::ServerHello => self.receive_server_hello(),
@ -233,13 +316,7 @@ impl SocksConnection {
SocksState::ReceiveResponse => self.receive_connection_status(), SocksState::ReceiveResponse => self.receive_connection_status(),
SocksState::Established => { SocksState::Established => self.relay_traffic(),
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(())
}
_ => Ok(()), _ => Ok(()),
} }
@ -292,12 +369,13 @@ impl TcpProxy for SocksConnection {
} }
} }
pub struct Socks5Manager { pub struct SocksManager {
server: SocketAddr, server: SocketAddr,
credentials: Option<Credentials>, credentials: Option<Credentials>,
version: SocksVersion,
} }
impl ConnectionManager for Socks5Manager { impl ConnectionManager for SocksManager {
fn handles_connection(&self, connection: &Connection) -> bool { fn handles_connection(&self, connection: &Connection) -> bool {
connection.proto == IpProtocol::Tcp connection.proto == IpProtocol::Tcp
} }
@ -306,11 +384,15 @@ impl ConnectionManager for Socks5Manager {
&self, &self,
connection: &Connection, connection: &Connection,
manager: Rc<dyn ConnectionManager>, manager: Rc<dyn ConnectionManager>,
) -> Option<Box<dyn TcpProxy>> { ) -> Result<Option<Box<dyn TcpProxy>>, Error> {
if connection.proto != IpProtocol::Tcp { 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) {} fn close_connection(&self, _: &Connection) {}
@ -324,11 +406,16 @@ impl ConnectionManager for Socks5Manager {
} }
} }
impl Socks5Manager { impl SocksManager {
pub fn new(server: SocketAddr, credentials: Option<Credentials>) -> Rc<Self> { pub fn new(
server: SocketAddr,
version: SocksVersion,
credentials: Option<Credentials>,
) -> Rc<Self> {
Rc::new(Self { Rc::new(Self {
server, server,
credentials, credentials,
version,
}) })
} }
} }

View file

@ -247,7 +247,7 @@ pub(crate) trait ConnectionManager {
&self, &self,
connection: &Connection, connection: &Connection,
manager: Rc<dyn ConnectionManager>, manager: Rc<dyn ConnectionManager>,
) -> Option<Box<dyn TcpProxy>>; ) -> Result<Option<Box<dyn TcpProxy>>, Error>;
fn close_connection(&self, connection: &Connection); fn close_connection(&self, connection: &Connection);
fn get_server(&self) -> SocketAddr; fn get_server(&self) -> SocketAddr;
fn get_credentials(&self) -> &Option<Credentials>; fn get_credentials(&self) -> &Option<Credentials>;
@ -428,7 +428,7 @@ impl<'a> TunToProxy<'a> {
if first_packet { if first_packet {
for manager in self.connection_managers.iter_mut() { for manager in self.connection_managers.iter_mut() {
if let Some(handler) = if let Some(handler) =
manager.new_connection(&resolved_conn, manager.clone()) manager.new_connection(&resolved_conn, manager.clone())?
{ {
let mut socket = tcp::Socket::new( let mut socket = tcp::Socket::new(
tcp::SocketBuffer::new(vec![0; 4096]), tcp::SocketBuffer::new(vec![0; 4096]),

View file

@ -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. /// DNS packet parsing should be continued after the name.
fn parse_qname(data: &[u8], mut offset: usize) -> Option<(String, usize)> { fn parse_qname(data: &[u8], mut offset: usize) -> Option<(String, usize)> {
// Since we only parse qnames and qnames can't point anywhere, // Since we only parse qnames and qnames can't point anywhere,
@ -255,9 +255,15 @@ impl VirtualDns {
} }
let label_len = data[offset]; let label_len = data[offset];
if label_len == 0 { if label_len == 0 {
if qname.is_empty() {
return None;
}
offset += 1; offset += 1;
break; break;
} }
if !qname.is_empty() {
qname.push('.');
}
for _ in 0..label_len { for _ in 0..label_len {
offset += 1; offset += 1;
if offset >= data.len() { if offset >= data.len() {
@ -265,7 +271,6 @@ impl VirtualDns {
} }
qname.push(data[offset] as char); qname.push(data[offset] as char);
} }
qname.push('.');
offset += 1; offset += 1;
} }

View file

@ -35,8 +35,12 @@ mod tests {
Ok(Test { proxy }) Ok(Test { proxy })
} }
fn tests() -> [Result<Test, String>; 2] { fn tests() -> [Result<Test, String>; 3] {
[test_from_env("SOCKS5_SERVER"), test_from_env("HTTP_SERVER")] [
test_from_env("SOCKS4_SERVER"),
test_from_env("SOCKS5_SERVER"),
test_from_env("HTTP_SERVER"),
]
} }
#[cfg(test)] #[cfg(test)]
@ -166,6 +170,16 @@ mod tests {
env::var(var).unwrap_or_else(|_| panic!("{}", "{var} environment variable required")); 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] #[serial]
#[test_log::test] #[test_log::test]
fn test_socks5() { 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] #[serial]
#[test_log::test] #[test_log::test]
fn test_socks5_dns() { fn test_socks5_dns() {