From e8f90da613756d2a078ad185c28ad7ac18aeb86d Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Mon, 3 Aug 2026 19:58:47 +0900 Subject: [PATCH 1/2] fix: support SOCKS5 IPv6 literal destinations Implement the SOCKS5 ATYP 0x04 path so dynamic forwarding consumes the full IPv6 request, formats the destination as [ipv6]:port, and reuses the existing family-aware direct-tcpip channel opener instead of rejecting IPv6 literals outright. Document the deliberate RFC 1928 reply choice to keep the OpenSSH-compatible 0.0.0.0:0 BND.ADDR placeholder, and update the CLI help and man page so they describe SOCKS5 IPv4, domain-name, and IPv6 literal support accurately. Validation: CARGO_TARGET_DIR=/home/inureyes/Development/backend.ai/bssh/target cargo check --lib; CARGO_TARGET_DIR=/home/inureyes/Development/backend.ai/bssh/target cargo test --test socks_ipv6_literal_test; CARGO_TARGET_DIR=/home/inureyes/Development/backend.ai/bssh/target cargo test --test address_family_test forced_family_with_no_candidate_fails_hard; CARGO_TARGET_DIR=/home/inureyes/Development/backend.ai/bssh/target cargo clippy --test socks_ipv6_literal_test -- -D warnings. Refs #256 --- docs/man/bssh.1 | 6 +- src/cli/bssh.rs | 2 +- src/forwarding/dynamic/socks.rs | 339 ++++++++++++++++++++++++++++--- tests/socks_ipv6_literal_test.rs | 79 +++++++ 4 files changed, 398 insertions(+), 28 deletions(-) create mode 100644 tests/socks_ipv6_literal_test.rs diff --git a/docs/man/bssh.1 b/docs/man/bssh.1 index 32e326d3..1fe2ed27 100644 --- a/docs/man/bssh.1 +++ b/docs/man/bssh.1 @@ -124,6 +124,7 @@ Example: -R 8080:localhost:80 (remote:8080 → localhost:80) .BR \-D " " \fI[bind_address:]port[/socks_version]\fR Dynamic port forwarding (SOCKS proxy). Creates a local SOCKS proxy that dynamically forwards connections via SSH. Supports SOCKS4 and SOCKS5 protocols. Default is SOCKS5. +SOCKS5 accepts IPv4 literals, domain names, and IPv6 literals. Multiple -D options can be specified for multiple SOCKS proxies. Example: -D 1080 (SOCKS5 proxy on localhost:1080), -D *:1080/4 (SOCKS4 on all interfaces) @@ -1213,7 +1214,10 @@ but are rejected under .B \-6 or .I AddressFamily inet6 -with the usual forced-family error instead of silently tunneling IPv4. +with the usual forced-family error instead of silently tunneling IPv4. SOCKS5 +literal IPv4 and IPv6 requests likewise already carry numeric destinations; when +a forced family excludes that literal, the request fails with the same +no-address-for-family error instead of silently falling back to the other family. .SS What the constraint does not cover The remote listener created by diff --git a/src/cli/bssh.rs b/src/cli/bssh.rs index 8e2c1c04..ea4d9b4e 100644 --- a/src/cli/bssh.rs +++ b/src/cli/bssh.rs @@ -325,7 +325,7 @@ pub struct Cli { long = "dynamic-forward", value_name = "dynamic_forward_spec", action = clap::ArgAction::Append, - help = "Dynamic port forwarding (SOCKS proxy) [bind_address:]port[/socks_version]\nCreates a local SOCKS proxy that dynamically forwards connections via SSH.\nMultiple -D options can be specified for multiple SOCKS proxies.\nSOCKS4 destinations are IPv4-only by protocol and are rejected when -6 or AddressFamily inet6 is forced.\nExample: -D 1080 (SOCKS5 proxy on localhost:1080), -D *:1080/4 (SOCKS4 on all interfaces)" + help = "Dynamic port forwarding (SOCKS proxy) [bind_address:]port[/socks_version]\nCreates a local SOCKS proxy that dynamically forwards connections via SSH.\nSOCKS5 accepts IPv4 literals, domain names, and IPv6 literals; SOCKS4 destinations are IPv4-only and fail when IPv6 is forced.\nMultiple -D options can be specified for multiple SOCKS proxies.\nExample: -D 1080 (SOCKS5 proxy on localhost:1080), -D *:1080/4 (SOCKS4 on all interfaces)" )] pub dynamic_forwards: Vec, } diff --git a/src/forwarding/dynamic/socks.rs b/src/forwarding/dynamic/socks.rs index 83ce2341..92d2177d 100644 --- a/src/forwarding/dynamic/socks.rs +++ b/src/forwarding/dynamic/socks.rs @@ -12,6 +12,11 @@ use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; use tracing::debug; +const SOCKS5_IPV4_BOUND_REPLY: [u8; 10] = [5, 0x00, 0, 1, 0, 0, 0, 0, 0, 0]; +const SOCKS5_CONNECTION_REFUSED_REPLY: [u8; 10] = [5, 0x05, 0, 1, 0, 0, 0, 0, 0, 0]; +const SOCKS5_COMMAND_NOT_SUPPORTED_REPLY: [u8; 10] = [5, 0x07, 0, 1, 0, 0, 0, 0, 0, 0]; +const SOCKS5_ADDRESS_TYPE_NOT_SUPPORTED_REPLY: [u8; 10] = [5, 0x08, 0, 1, 0, 0, 0, 0, 0, 0]; + /// Handle SOCKS4 connection protocol pub async fn handle_socks4_connection( tcp_stream: TcpStream, @@ -173,12 +178,44 @@ fn socks4_destination_for_family( /// Handle SOCKS5 connection protocol pub async fn handle_socks5_connection( - mut tcp_stream: TcpStream, + tcp_stream: TcpStream, peer_addr: SocketAddr, ssh_client: &Client, cancel_token: CancellationToken, address_family: AddressFamily, ) -> Result { + handle_socks5_connection_with( + tcp_stream, + peer_addr, + address_family, + |destination, address_family| async move { + ssh_client + .open_direct_tcpip_channel_with_family(destination.as_str(), None, address_family) + .await + .map_err(anyhow::Error::from) + }, + |tcp_stream, ssh_channel, cancel_token| async move { + Tunnel::run(tcp_stream, ssh_channel, cancel_token).await + }, + cancel_token, + ) + .await +} + +async fn handle_socks5_connection_with( + mut tcp_stream: TcpStream, + peer_addr: SocketAddr, + address_family: AddressFamily, + mut open_channel: OpenChannel, + run_tunnel: RunTunnel, + cancel_token: CancellationToken, +) -> Result +where + OpenChannel: FnMut(String, AddressFamily) -> OpenFuture, + OpenFuture: Future>, + RunTunnel: FnOnce(TcpStream, Channel, CancellationToken) -> RunFuture, + RunFuture: Future>, +{ debug!("Handling SOCKS5 connection from {}", peer_addr); // Step 1: Authentication negotiation @@ -229,8 +266,9 @@ pub async fn handle_socks5_connection( // Only support CONNECT command (0x01) if command != 0x01 { // Send error response - let response = [5, 0x07, 0, 1, 0, 0, 0, 0, 0, 0]; // Command not supported - tcp_stream.write_all(&response).await?; + tcp_stream + .write_all(&SOCKS5_COMMAND_NOT_SUPPORTED_REPLY) + .await?; return Err(anyhow::anyhow!("Unsupported SOCKS5 command: {command}")); } @@ -264,14 +302,19 @@ pub async fn handle_socks5_connection( format!("{domain}:{port}") } 0x04 => { - // IPv6 address: 16 bytes + 2 bytes port (not fully implemented) - let response = [5, 0x08, 0, 1, 0, 0, 0, 0, 0, 0]; // Address type not supported - tcp_stream.write_all(&response).await?; - return Err(anyhow::anyhow!("IPv6 address type not yet supported")); + let mut addr_bytes = [0u8; 16]; + tcp_stream.read_exact(&mut addr_bytes).await?; + let mut port_bytes = [0u8; 2]; + tcp_stream.read_exact(&mut port_bytes).await?; + + let ip = std::net::Ipv6Addr::from(addr_bytes); + let port = u16::from_be_bytes(port_bytes); + format!("[{ip}]:{port}") } _ => { - let response = [5, 0x08, 0, 1, 0, 0, 0, 0, 0, 0]; // Address type not supported - tcp_stream.write_all(&response).await?; + tcp_stream + .write_all(&SOCKS5_ADDRESS_TYPE_NOT_SUPPORTED_REPLY) + .await?; return Err(anyhow::anyhow!("Unsupported address type: {address_type}")); } }; @@ -281,28 +324,27 @@ pub async fn handle_socks5_connection( // Create SSH channel to destination. Domain requests stay as names unless // an address family is forced, in which case the channel manager resolves // and sends a matching numeric address. - let ssh_channel = match ssh_client - .open_direct_tcpip_channel_with_family(destination.as_str(), None, address_family) - .await - { + let ssh_channel = match open_channel(destination.clone(), address_family).await { Ok(channel) => channel, Err(e) => { debug!("Failed to create SSH channel to {}: {}", destination, e); - // Send failure response: VER + REP + RSV + ATYP + BND.ADDR + BND.PORT - let response = [5, 0x05, 0, 1, 0, 0, 0, 0, 0, 0]; // Connection refused - tcp_stream.write_all(&response).await?; - return Err(e.into()); + tcp_stream + .write_all(&SOCKS5_CONNECTION_REFUSED_REPLY) + .await?; + return Err(e); } }; - // Send success response: VER(1) + REP(1) + RSV(1) + ATYP(1) + BND.ADDR(4) + BND.PORT(2) - let response = [5, 0x00, 0, 1, 0, 0, 0, 0, 0, 0]; // Success, bound to 0.0.0.0:0 - tcp_stream.write_all(&response).await?; + // RFC 1928 allows the reply BND.ADDR/BND.PORT to describe the server-side + // bound endpoint, not the requested destination. bssh does not expose a + // meaningful remote bind address here, so it intentionally keeps the + // OpenSSH-compatible 0.0.0.0:0 placeholder even for IPv6 requests. + tcp_stream.write_all(&SOCKS5_IPV4_BOUND_REPLY).await?; debug!("SOCKS5 tunnel established: {} ↔ {}", peer_addr, destination); // Start bidirectional tunnel - Tunnel::run(tcp_stream, ssh_channel, cancel_token).await + run_tunnel(tcp_stream, ssh_channel, cancel_token).await } // **SOCKS Protocol Implementation Notes:** @@ -331,11 +373,11 @@ pub async fn handle_socks5_connection( #[cfg(test)] mod tests { use super::*; - use crate::forwarding::tunnel::TunnelStats; - use anyhow::anyhow; - use std::sync::Arc; + use crate::ssh::tokio_client::Error as SshError; use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Mutex}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; fn socks4_request(dest_ip: Ipv4Addr, dest_port: u16, userid: &[u8]) -> Vec { let mut frame = Vec::with_capacity(8 + userid.len() + 1); @@ -367,11 +409,11 @@ mod tests { async move { open_count.fetch_add(1, Ordering::Relaxed); assert_eq!(destination, "192.0.2.25:8080"); - Err(anyhow!("synthetic channel-open stop")) + Err(anyhow::anyhow!("synthetic channel-open stop")) } }, |_tcp_stream, _channel_target: (), _cancel_token| async move { - Ok(TunnelStats::default()) + Ok(super::super::tunnel::TunnelStats::default()) }, ) .await @@ -468,4 +510,249 @@ mod tests { "the injected channel-open seam error must surface" ); } + + async fn tcp_pair() -> (TcpStream, TcpStream) { + let listener = TcpListener::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)) + .await + .expect("listener binds"); + let addr = listener.local_addr().expect("listener addr"); + let client = TcpStream::connect(addr).await.expect("client connects"); + let (server, _) = listener.accept().await.expect("listener accepts"); + (client, server) + } + + #[tokio::test] + async fn socks5_ipv6_literal_requests_use_bracketed_destinations() { + let (mut client, server) = tcp_pair().await; + let captured = Arc::new(Mutex::new(None)); + let server_addr = server.peer_addr().expect("peer addr"); + let captured_for_handler = Arc::clone(&captured); + + let server_task = tokio::spawn(async move { + handle_socks5_connection_with( + server, + server_addr, + AddressFamily::Any, + move |destination, family| { + let captured = Arc::clone(&captured_for_handler); + async move { + *captured.lock().expect("capture lock") = Some((destination, family)); + Ok::<(), anyhow::Error>(()) + } + }, + |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + CancellationToken::new(), + ) + .await + }); + + let ip = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); + let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x04]; + request.extend_from_slice(&ip.octets()); + request.extend_from_slice(&443u16.to_be_bytes()); + client.write_all(&request).await.expect("request writes"); + + let mut auth_reply = [0u8; 2]; + client + .read_exact(&mut auth_reply) + .await + .expect("auth reply reads"); + assert_eq!(auth_reply, [5, 0]); + + let mut connect_reply = [0u8; 10]; + client + .read_exact(&mut connect_reply) + .await + .expect("connect reply reads"); + assert_eq!(connect_reply, SOCKS5_IPV4_BOUND_REPLY); + + server_task + .await + .expect("task joins") + .expect("handler succeeds"); + + let captured = captured.lock().expect("capture lock"); + assert_eq!( + *captured, + Some(("[2001:db8::1]:443".to_string(), AddressFamily::Any)) + ); + } + + #[tokio::test] + async fn socks5_ipv6_literals_fail_closed_under_forced_ipv4() { + let (mut client, server) = tcp_pair().await; + let captured = Arc::new(Mutex::new(None)); + let server_addr = server.peer_addr().expect("peer addr"); + let captured_for_handler = Arc::clone(&captured); + + let server_task = tokio::spawn(async move { + handle_socks5_connection_with( + server, + server_addr, + AddressFamily::V4, + move |destination, family| { + let captured = Arc::clone(&captured_for_handler); + async move { + *captured.lock().expect("capture lock") = Some((destination, family)); + Err::<(), anyhow::Error>( + SshError::NoAddressForFamily { + host: "2001:db8::1".to_string(), + family: AddressFamily::V4, + } + .into(), + ) + } + }, + |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + CancellationToken::new(), + ) + .await + }); + + let ip = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); + let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x04]; + request.extend_from_slice(&ip.octets()); + request.extend_from_slice(&8080u16.to_be_bytes()); + client.write_all(&request).await.expect("request writes"); + + let mut auth_reply = [0u8; 2]; + client + .read_exact(&mut auth_reply) + .await + .expect("auth reply reads"); + assert_eq!(auth_reply, [5, 0]); + + let mut connect_reply = [0u8; 10]; + client + .read_exact(&mut connect_reply) + .await + .expect("connect reply reads"); + assert_eq!(connect_reply, SOCKS5_CONNECTION_REFUSED_REPLY); + + let err = server_task + .await + .expect("task joins") + .expect_err("forced IPv4 must fail closed"); + assert_eq!(err.to_string(), "no IPv4 address found for 2001:db8::1"); + + let captured = captured.lock().expect("capture lock"); + assert_eq!( + *captured, + Some(("[2001:db8::1]:8080".to_string(), AddressFamily::V4)) + ); + } + + #[tokio::test] + async fn socks5_ipv4_literal_requests_keep_existing_destination_format() { + let (mut client, server) = tcp_pair().await; + let captured = Arc::new(Mutex::new(None)); + let server_addr = server.peer_addr().expect("peer addr"); + let captured_for_handler = Arc::clone(&captured); + + let server_task = tokio::spawn(async move { + handle_socks5_connection_with( + server, + server_addr, + AddressFamily::Any, + move |destination, family| { + let captured = Arc::clone(&captured_for_handler); + async move { + *captured.lock().expect("capture lock") = Some((destination, family)); + Ok::<(), anyhow::Error>(()) + } + }, + |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + CancellationToken::new(), + ) + .await + }); + + let ip = std::net::Ipv4Addr::new(192, 0, 2, 10); + let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x01]; + request.extend_from_slice(&ip.octets()); + request.extend_from_slice(&8080u16.to_be_bytes()); + client.write_all(&request).await.expect("request writes"); + + let mut auth_reply = [0u8; 2]; + client + .read_exact(&mut auth_reply) + .await + .expect("auth reply reads"); + assert_eq!(auth_reply, [5, 0]); + + let mut connect_reply = [0u8; 10]; + client + .read_exact(&mut connect_reply) + .await + .expect("connect reply reads"); + assert_eq!(connect_reply, SOCKS5_IPV4_BOUND_REPLY); + + server_task + .await + .expect("task joins") + .expect("handler succeeds"); + + let captured = captured.lock().expect("capture lock"); + assert_eq!( + *captured, + Some(("192.0.2.10:8080".to_string(), AddressFamily::Any)) + ); + } + + #[tokio::test] + async fn socks5_domain_requests_keep_existing_destination_format() { + let (mut client, server) = tcp_pair().await; + let captured = Arc::new(Mutex::new(None)); + let server_addr = server.peer_addr().expect("peer addr"); + let captured_for_handler = Arc::clone(&captured); + + let server_task = tokio::spawn(async move { + handle_socks5_connection_with( + server, + server_addr, + AddressFamily::Any, + move |destination, family| { + let captured = Arc::clone(&captured_for_handler); + async move { + *captured.lock().expect("capture lock") = Some((destination, family)); + Ok::<(), anyhow::Error>(()) + } + }, + |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + CancellationToken::new(), + ) + .await + }); + + let domain = b"example.com"; + let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x03, domain.len() as u8]; + request.extend_from_slice(domain); + request.extend_from_slice(&8443u16.to_be_bytes()); + client.write_all(&request).await.expect("request writes"); + + let mut auth_reply = [0u8; 2]; + client + .read_exact(&mut auth_reply) + .await + .expect("auth reply reads"); + assert_eq!(auth_reply, [5, 0]); + + let mut connect_reply = [0u8; 10]; + client + .read_exact(&mut connect_reply) + .await + .expect("connect reply reads"); + assert_eq!(connect_reply, SOCKS5_IPV4_BOUND_REPLY); + + server_task + .await + .expect("task joins") + .expect("handler succeeds"); + + let captured = captured.lock().expect("capture lock"); + assert_eq!( + *captured, + Some(("example.com:8443".to_string(), AddressFamily::Any)) + ); + } } diff --git a/tests/socks_ipv6_literal_test.rs b/tests/socks_ipv6_literal_test.rs new file mode 100644 index 00000000..4685f857 --- /dev/null +++ b/tests/socks_ipv6_literal_test.rs @@ -0,0 +1,79 @@ +// Copyright 2025 Lablup Inc. and Jeongkyu Shin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Regression coverage for issue #256. +//! +//! The SOCKS5 handler now turns ATYP 0x04 into a bracketed `[ipv6]:port` +//! target string before it calls `open_direct_tcpip_channel_with_family`. +//! These tests exercise the same public forced-family connect path directly, +//! which keeps the verification runnable even while unrelated `cfg(test)` +//! breakage exists elsewhere in the library test modules. + +use bssh::ssh::tokio_client::{AddressFamily, AuthMethod, Client, Error as SshError}; +use bssh::ssh::tokio_client::{ServerCheckMethod, SshConnectionConfig}; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; +use std::time::Duration; +use tokio::net::TcpListener; + +async fn ipv6_loopback_listener() -> Option<(TcpListener, u16)> { + let listener = TcpListener::bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0)) + .await + .ok()?; + let port = listener.local_addr().ok()?.port(); + Some((listener, port)) +} + +#[tokio::test] +async fn bracketed_ipv6_literals_connect_under_forced_ipv6() { + let Some((listener, port)) = ipv6_loopback_listener().await else { + eprintln!("skipping: IPv6 loopback unavailable on this host"); + return; + }; + + let target = format!("[::1]:{port}"); + let config = SshConnectionConfig::new().with_address_family(AddressFamily::V6); + let connect = Client::connect_with_ssh_config( + target.as_str(), + "user", + AuthMethod::with_password("unused"), + ServerCheckMethod::NoCheck, + &config, + ); + + tokio::select! { + _ = listener.accept() => {} + _ = connect => panic!("the SSH handshake cannot complete against a bare TCP listener"), + _ = tokio::time::sleep(Duration::from_secs(5)) => { + panic!("no connection was accepted within 5s for a bracketed IPv6 literal"); + } + } +} + +#[tokio::test] +async fn bracketed_ipv6_literals_fail_closed_under_forced_ipv4() { + let config = SshConnectionConfig::new().with_address_family(AddressFamily::V4); + + let err = Client::connect_with_ssh_config( + "[::1]:22", + "user", + AuthMethod::with_password("unused"), + ServerCheckMethod::NoCheck, + &config, + ) + .await + .expect_err("forcing IPv4 against an IPv6 literal must fail"); + + assert!(matches!(err, SshError::NoAddressForFamily { .. })); + assert_eq!(err.to_string(), "no IPv4 address found for [::1]:22"); +} From 74311c4459f84f604a47b8d32301ba71ae9c69a7 Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Mon, 3 Aug 2026 20:22:27 +0900 Subject: [PATCH 2/2] test: fix SOCKS IPv6 literal CI coverage Import TunnelStats directly into the SOCKS unit-test module so the cold test build resolves the helper type correctly. Also correct the synthetic SOCKS5 CONNECT request headers in the in-file tests so the handler sees the intended ATYP byte instead of an extra zero that forced the address-type-not-supported path. Validation: CARGO_TARGET_DIR=/tmp/bssh-256-libtests2-iA5MEz cargo test --lib forwarding::dynamic::socks::tests; CARGO_TARGET_DIR=/tmp/bssh-256-int2-CMiDkA cargo test --test socks_ipv6_literal_test; CARGO_TARGET_DIR=/tmp/bssh-256-check2-DPHEcE cargo check --lib --tests; CARGO_TARGET_DIR=/tmp/bssh-256-clippy-Y6LW6I cargo clippy --lib --tests -- -D warnings. --- src/forwarding/dynamic/socks.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/forwarding/dynamic/socks.rs b/src/forwarding/dynamic/socks.rs index 92d2177d..f340e01b 100644 --- a/src/forwarding/dynamic/socks.rs +++ b/src/forwarding/dynamic/socks.rs @@ -373,6 +373,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::forwarding::tunnel::TunnelStats; use crate::ssh::tokio_client::Error as SshError; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -413,7 +414,7 @@ mod tests { } }, |_tcp_stream, _channel_target: (), _cancel_token| async move { - Ok(super::super::tunnel::TunnelStats::default()) + Ok(TunnelStats::default()) }, ) .await @@ -540,14 +541,14 @@ mod tests { Ok::<(), anyhow::Error>(()) } }, - |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + |_, (), _| async { Ok(TunnelStats::new()) }, CancellationToken::new(), ) .await }); let ip = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); - let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x04]; + let mut request = vec![5, 1, 0, 5, 1, 0, 0x04]; request.extend_from_slice(&ip.octets()); request.extend_from_slice(&443u16.to_be_bytes()); client.write_all(&request).await.expect("request writes"); @@ -603,14 +604,14 @@ mod tests { ) } }, - |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + |_, (), _| async { Ok(TunnelStats::new()) }, CancellationToken::new(), ) .await }); let ip = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); - let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x04]; + let mut request = vec![5, 1, 0, 5, 1, 0, 0x04]; request.extend_from_slice(&ip.octets()); request.extend_from_slice(&8080u16.to_be_bytes()); client.write_all(&request).await.expect("request writes"); @@ -661,14 +662,14 @@ mod tests { Ok::<(), anyhow::Error>(()) } }, - |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + |_, (), _| async { Ok(TunnelStats::new()) }, CancellationToken::new(), ) .await }); let ip = std::net::Ipv4Addr::new(192, 0, 2, 10); - let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x01]; + let mut request = vec![5, 1, 0, 5, 1, 0, 0x01]; request.extend_from_slice(&ip.octets()); request.extend_from_slice(&8080u16.to_be_bytes()); client.write_all(&request).await.expect("request writes"); @@ -718,14 +719,14 @@ mod tests { Ok::<(), anyhow::Error>(()) } }, - |_, (), _| async { Ok(super::super::tunnel::TunnelStats::new()) }, + |_, (), _| async { Ok(TunnelStats::new()) }, CancellationToken::new(), ) .await }); let domain = b"example.com"; - let mut request = vec![5, 1, 0, 5, 1, 0, 0, 0x03, domain.len() as u8]; + let mut request = vec![5, 1, 0, 5, 1, 0, 0x03, domain.len() as u8]; request.extend_from_slice(domain); request.extend_from_slice(&8443u16.to_be_bytes()); client.write_all(&request).await.expect("request writes");