diff --git a/src/client/legacy/connect/proxy/socks/v4/mod.rs b/src/client/legacy/connect/proxy/socks/v4/mod.rs index 85988186..610d09be 100644 --- a/src/client/legacy/connect/proxy/socks/v4/mod.rs +++ b/src/client/legacy/connect/proxy/socks/v4/mod.rs @@ -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 diff --git a/src/client/legacy/connect/proxy/socks/v5/mod.rs b/src/client/legacy/connect/proxy/socks/v5/mod.rs index 5b3fc94a..cbf88de3 100644 --- a/src/client/legacy/connect/proxy/socks/v5/mod.rs +++ b/src/client/legacy/connect/proxy/socks/v5/mod.rs @@ -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 diff --git a/tests/proxy.rs b/tests/proxy.rs index 2c804e90..65e7e819 100644 --- a/tests/proxy.rs +++ b/tests/proxy.rs @@ -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"); +}