Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/client/legacy/connect/proxy/socks/v4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,19 @@ where

let fut = async move {
let port = dst.port().map(|p| p.as_u16()).unwrap_or(443);
let host = dst.host().ok_or(SocksError::MissingHost)?.to_string();
let host_from_uri = dst.host().ok_or(SocksError::MissingHost)?;

// Note: host() returns the host part of the URI which could be a square bracketed IPv6
// address. We need to remove any brackets to make it a valid IP address.
//
// Whilst SOCKSv4 does not support IPv6, this bracket stripping will enable an IPv6
// destination to be correctly rejected down the line. Without it, we would end up with
// a DNS resolution failure which would be confusing.
let host = host_from_uri
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or(host_from_uri)
.to_string();

let conn = connecting.await.map_err(SocksError::Inner)?;
config.execute(conn, host, port).await
Expand Down
10 changes: 9 additions & 1 deletion src/client/legacy/connect/proxy/socks/v5/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,15 @@ where

let fut = async move {
let port = dst.port().map(|p| p.as_u16()).unwrap_or(443);
let host = dst.host().ok_or(SocksError::MissingHost)?.to_string();
let host_from_uri = dst.host().ok_or(SocksError::MissingHost)?;

// Note: host() returns the host part of the URI which could be a square bracketed IPv6
// address. We need to remove any brackets to make it a valid IP address.
let host = host_from_uri
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or(host_from_uri)
.to_string();

let conn = connecting.await.map_err(SocksError::Inner)?;
config.execute(conn, host, port).await
Expand Down
136 changes: 136 additions & 0 deletions tests/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,139 @@ async fn test_socks_v5_optimistic_works() {
t1.await.expect("task - client");
t2.await.expect("task - proxy");
}

#[cfg(not(miri))]
#[tokio::test]
async fn test_socks_v5_with_ipv6_target_works() {
let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let proxy_addr = proxy_tcp.local_addr().expect("local_addr");
let proxy_dst = format!("http://{proxy_addr}").parse().expect("uri");

let target_tcp = TcpListener::bind("[::1]:0").await.expect("bind");
let target_addr = target_tcp.local_addr().expect("local_addr");
let target_dst = format!("http://{target_addr}").parse().expect("uri");

let mut connector = SocksV5::new(proxy_dst, HttpConnector::new());

// Client
//
// Will use `SocksV5` to establish proxy tunnel to an IPv6 target.
// The `host()` of the target URI is a bracketed IPv6 address (e.g. "[::1]"),
// which must be stripped of its brackets before being parsed as an IP.
// Will send "Hello World!" to the target and receive "Goodbye!" back.
let t1 = tokio::spawn(async move {
let conn = connector.call(target_dst).await.expect("tunnel");
let mut tcp = conn.into_inner();

tcp.write_all(b"Hello World!").await.expect("write 1");

let mut buf = [0u8; 64];
let n = tcp.read(&mut buf).await.expect("read 1");
assert_eq!(&buf[..n], b"Goodbye!");
});

// Proxy
//
// Will receive CONNECT command from client with an IPv6 address (ATYP 0x04).
// Will connect to target and success code back to client.
// Will blindly tunnel between client and target.
let t2 = tokio::spawn(async move {
let (mut to_client, _) = proxy_tcp.accept().await.expect("accept");
let mut buf = [0u8; 513];

// negotiation req/res
let n = to_client.read(&mut buf).await.expect("read 1");
assert_eq!(&buf[..n], [0x05, 0x01, 0x00]);

to_client.write_all(&[0x05, 0x00]).await.expect("write 1");

// command req/res
let n = to_client.read(&mut buf).await.expect("read 2");
let [p1, p2] = target_addr.port().to_be_bytes();
let mut message = vec![0x05, 0x01, 0x00, 0x04];
match target_addr.ip() {
std::net::IpAddr::V6(v6) => message.extend_from_slice(&v6.octets()),
_ => panic!("expected IPv6 target"),
}
message.extend_from_slice(&[p1, p2]);
assert_eq!(&buf[..n], message);

let mut to_target = TcpStream::connect(target_addr).await.expect("connect");

let mut reply = vec![0x05, 0x00, 0x00, 0x04];
match target_addr.ip() {
std::net::IpAddr::V6(v6) => reply.extend_from_slice(&v6.octets()),
_ => panic!("expected IPv6 target"),
}
reply.extend_from_slice(&[p1, p2]);
to_client.write_all(&reply).await.expect("write 2");

let (from_client, from_target) =
tokio::io::copy_bidirectional(&mut to_client, &mut to_target)
.await
.expect("proxy");

assert_eq!(from_client, 12);
assert_eq!(from_target, 8)
});

// Target server
//
// Will accept connection from proxy server
// Will receive "Hello World!" from the client and return "Goodbye!"
let t3 = tokio::spawn(async move {
let (mut io, _) = target_tcp.accept().await.expect("accept");
let mut buf = [0u8; 64];

let n = io.read(&mut buf).await.expect("read 1");
assert_eq!(&buf[..n], b"Hello World!");

io.write_all(b"Goodbye!").await.expect("write 1");
});

t1.await.expect("task - client");
t2.await.expect("task - proxy");
t3.await.expect("task - target");
}

#[cfg(not(miri))]
#[tokio::test]
async fn test_socks_v4_with_ipv6_target_fails() {
let proxy_tcp = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let proxy_addr = proxy_tcp.local_addr().expect("local_addr");
let proxy_dst = format!("http://{proxy_addr}").parse().expect("uri");

// SOCKSv4 does not support IPv6. A bracketed IPv6 target URI must be
// rejected with `SocksV4Error::IpV6` after the brackets are stripped and
// the remaining host is parsed as an IPv6 address.
let target_addr = std::net::SocketAddr::new(std::net::Ipv6Addr::LOCALHOST.into(), 1234);
let target_dst = format!("http://{target_addr}").parse().expect("uri");

let mut connector = SocksV4::new(proxy_dst, HttpConnector::new());

// Client
//
// Will use `SocksV4` to attempt a proxy tunnel to an IPv6 target.
// SOCKSv4 does not support IPv6, so this should fail.
let t1 = tokio::spawn(async move {
let result = connector.call(target_dst).await;
assert!(result.is_err(), "expected SOCKSv4 to reject IPv6 target");
let err = result.unwrap_err().to_string();
assert!(
err.contains("IPV6 is not supported"),
"unexpected error: {err}"
);
});

// Proxy
//
// Will accept the underlying connection from the client. No SOCKSv4
// handshake is sent because the client rejects the IPv6 target before
// serializing the request.
let t2 = tokio::spawn(async move {
let (_to_client, _) = proxy_tcp.accept().await.expect("accept");
});

t1.await.expect("task - client");
t2.await.expect("task - proxy");
}