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
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

View file

@ -175,11 +175,11 @@ impl ConnectionManager for HttpManager {
&self,
connection: &Connection,
manager: Rc<dyn ConnectionManager>,
) -> Option<Box<dyn TcpProxy>> {
) -> Result<Option<Box<dyn TcpProxy>>, 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) {}

View file

@ -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));

View file

@ -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<u8>,
data_buf: VecDeque<u8>,
manager: Rc<dyn ConnectionManager>,
version: SocksVersion,
}
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 {
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::<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;
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<Credentials>,
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<dyn ConnectionManager>,
) -> Option<Box<dyn TcpProxy>> {
) -> Result<Option<Box<dyn TcpProxy>>, 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<Credentials>) -> Rc<Self> {
impl SocksManager {
pub fn new(
server: SocketAddr,
version: SocksVersion,
credentials: Option<Credentials>,
) -> Rc<Self> {
Rc::new(Self {
server,
credentials,
version,
})
}
}

View file

@ -247,7 +247,7 @@ pub(crate) trait ConnectionManager {
&self,
connection: &Connection,
manager: Rc<dyn ConnectionManager>,
) -> Option<Box<dyn TcpProxy>>;
) -> Result<Option<Box<dyn TcpProxy>>, Error>;
fn close_connection(&self, connection: &Connection);
fn get_server(&self) -> SocketAddr;
fn get_credentials(&self) -> &Option<Credentials>;
@ -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]),

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.
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;
}

View file

@ -35,8 +35,12 @@ mod tests {
Ok(Test { proxy })
}
fn tests() -> [Result<Test, String>; 2] {
[test_from_env("SOCKS5_SERVER"), test_from_env("HTTP_SERVER")]
fn tests() -> [Result<Test, String>; 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() {