diff --git a/src/transport/middle_proxy/handshake.rs b/src/transport/middle_proxy/handshake.rs index a46b0fb..6be70c7 100644 --- a/src/transport/middle_proxy/handshake.rs +++ b/src/transport/middle_proxy/handshake.rs @@ -290,11 +290,11 @@ impl MePool { None }; // BND describes the SOCKS egress to ME, whose family can differ from - // the local TCP connection to the SOCKS server. - let socks_family = if peer_addr.is_ipv4() { - IpFamily::V4 - } else { - IpFamily::V6 + // the local TCP connection to the SOCKS server. Match KDF classification + // so IPv4-mapped destinations require an IPv4 egress tuple. + let socks_family = match extract_ip_material(peer_addr) { + IpMaterial::V4(_) => IpFamily::V4, + IpMaterial::V6(_) => IpFamily::V6, }; let socks_bound_addr = Self::select_socks_bound_addr(socks_family, upstream_egress); let bnd_addr_status = if !is_socks_route { diff --git a/src/transport/middle_proxy/handshake/socks_tests.rs b/src/transport/middle_proxy/handshake/socks_tests.rs index e77cefa..0ea7a29 100644 --- a/src/transport/middle_proxy/handshake/socks_tests.rs +++ b/src/transport/middle_proxy/handshake/socks_tests.rs @@ -149,7 +149,8 @@ async fn accept_handshake( .write_all(&build_rpc_frame(-2, &nonce, RpcChecksumMode::Crc32)) .await .unwrap(); - let split = |ip: IpAddr| match ip { + // The ME peer derives mapped destinations as IPv4 independently of client selection. + let split = |ip: IpAddr| match ip.to_canonical() { IpAddr::V4(ip) => { let mut octets = ip.octets(); octets.reverse(); @@ -259,3 +260,71 @@ async fn strict_socks_rejects_wrong_me_family_and_incomplete_bnd() { check_socks_handshake(transport, target, bound, MeSocksKdfPolicy::Strict, false).await; } } + +#[tokio::test] +async fn b12_ipv4_mapped_me_strict_preserves_bnd_tuple() { + for transport in ["127.0.0.1:0", "[::1]:0"] { + check_socks_handshake( + transport, + "[::ffff:149.154.167.50]:8888", + V4_BND, + MeSocksKdfPolicy::Strict, + true, + ) + .await; + } +} + +#[tokio::test] +async fn b12_ipv4_mapped_me_compat_preserves_bnd_tuple() { + for transport in ["127.0.0.1:0", "[::1]:0"] { + check_socks_handshake( + transport, + "[::ffff:149.154.167.50]:8888", + V4_BND, + MeSocksKdfPolicy::Compat, + true, + ) + .await; + } +} + +#[tokio::test] +async fn b12_ipv4_mapped_me_strict_retains_bnd_validation() { + for transport in ["127.0.0.1:0", "[::1]:0"] { + for bound in [ + V6_BND, + "10.0.0.1:45678", + "0.0.0.0:45678", + "91.108.56.1:0", + "[::ffff:91.108.56.1]:45678", + ] { + check_socks_handshake( + transport, + "[::ffff:149.154.167.50]:8888", + bound, + MeSocksKdfPolicy::Strict, + false, + ) + .await; + } + } +} + +#[tokio::test] +async fn b12_ipv4_compatible_destination_remains_ipv6() { + // IPv4-compatible IPv6 is not IPv4-mapped and must keep native IPv6 KDF material. + for transport in ["127.0.0.1:0", "[::1]:0"] { + for policy in [MeSocksKdfPolicy::Strict, MeSocksKdfPolicy::Compat] { + check_socks_handshake(transport, "[::149.154.167.50]:8888", V6_BND, policy, true).await; + } + check_socks_handshake( + transport, + "[::149.154.167.50]:8888", + V4_BND, + MeSocksKdfPolicy::Strict, + false, + ) + .await; + } +}