mirror of
https://github.com/telemt/telemt.git
synced 2026-10-31 21:02:01 +03:00
Fix SOCKS BND family validation against the ME destination
This commit is contained in:
@@ -42,6 +42,10 @@ use super::wire::{IpMaterial, extract_ip_material};
|
|||||||
|
|
||||||
const ME_KDF_DRIFT_STRICT: bool = false;
|
const ME_KDF_DRIFT_STRICT: bool = false;
|
||||||
|
|
||||||
|
// Exercises SOCKS negotiation and encrypted ME handshakes across address families.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod socks_tests;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
enum KdfClientPortSource {
|
enum KdfClientPortSource {
|
||||||
LocalSocket = 0,
|
LocalSocket = 0,
|
||||||
@@ -60,14 +64,23 @@ impl KdfClientPortSource {
|
|||||||
|
|
||||||
/// Result of a successful ME handshake with timings.
|
/// Result of a successful ME handshake with timings.
|
||||||
pub(crate) struct HandshakeOutput {
|
pub(crate) struct HandshakeOutput {
|
||||||
|
/// Read half positioned after the encrypted handshake response.
|
||||||
pub rd: ReadHalf<TcpStream>,
|
pub rd: ReadHalf<TcpStream>,
|
||||||
|
/// Write half positioned after the encrypted handshake request.
|
||||||
pub wr: WriteHalf<TcpStream>,
|
pub wr: WriteHalf<TcpStream>,
|
||||||
|
/// Translated local IP advertised in subsequent proxy requests.
|
||||||
pub source_ip: IpAddr,
|
pub source_ip: IpAddr,
|
||||||
|
/// Key for decrypting subsequent ME frames.
|
||||||
pub read_key: [u8; 32],
|
pub read_key: [u8; 32],
|
||||||
|
/// CBC continuation IV after reading the handshake response.
|
||||||
pub read_iv: [u8; 16],
|
pub read_iv: [u8; 16],
|
||||||
|
/// Key for encrypting subsequent ME frames.
|
||||||
pub write_key: [u8; 32],
|
pub write_key: [u8; 32],
|
||||||
|
/// CBC continuation IV after writing the handshake request.
|
||||||
pub write_iv: [u8; 16],
|
pub write_iv: [u8; 16],
|
||||||
|
/// Checksum mode negotiated with the ME peer.
|
||||||
pub crc_mode: RpcChecksumMode,
|
pub crc_mode: RpcChecksumMode,
|
||||||
|
/// Elapsed handshake time in milliseconds, excluding TCP connection setup.
|
||||||
pub handshake_ms: f64,
|
pub handshake_ms: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +289,14 @@ impl MePool {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
let socks_bound_addr = Self::select_socks_bound_addr(family, upstream_egress);
|
// 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
|
||||||
|
};
|
||||||
|
let socks_bound_addr = Self::select_socks_bound_addr(socks_family, upstream_egress);
|
||||||
let bnd_addr_status = if !is_socks_route {
|
let bnd_addr_status = if !is_socks_route {
|
||||||
BndAddrStatus::Error
|
BndAddrStatus::Error
|
||||||
} else if raw_socks_bound_addr.is_some() && socks_bound_addr.is_none() {
|
} else if raw_socks_bound_addr.is_some() && socks_bound_addr.is_none() {
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
//! SOCKS transport family must not determine the ME KDF endpoint family.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::network::probe::NetworkDecision;
|
||||||
|
use crate::transport::middle_proxy::pool_writer_security_tests::make_pool_with_decision;
|
||||||
|
use crate::transport::socks::connect_socks5;
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
|
// These public tuples are simulated by the local SOCKS/ME peer; no external traffic is sent.
|
||||||
|
const V4_ME: &str = "149.154.167.50:8888";
|
||||||
|
const V4_BND: &str = "91.108.56.1:45678";
|
||||||
|
const V6_ME: &str = "[2001:67c:4e8:f002::a]:8888";
|
||||||
|
const V6_BND: &str = "[2606:4700:4700::1111]:45678";
|
||||||
|
|
||||||
|
fn socks_address(addr: SocketAddr) -> Vec<u8> {
|
||||||
|
let mut wire = Vec::new();
|
||||||
|
match addr.ip() {
|
||||||
|
IpAddr::V4(ip) => {
|
||||||
|
wire.push(1);
|
||||||
|
wire.extend_from_slice(&ip.octets());
|
||||||
|
}
|
||||||
|
IpAddr::V6(ip) => {
|
||||||
|
wire.push(4);
|
||||||
|
wire.extend_from_slice(&ip.octets());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wire.extend_from_slice(&addr.port().to_be_bytes());
|
||||||
|
wire
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn socks_reply(stream: &mut TcpStream, target: SocketAddr, bound: SocketAddr) {
|
||||||
|
let mut greeting = [0u8; 3];
|
||||||
|
stream.read_exact(&mut greeting).await.unwrap();
|
||||||
|
assert_eq!(greeting, [5, 1, 0]);
|
||||||
|
stream.write_all(&[5, 0]).await.unwrap();
|
||||||
|
let mut expected = vec![5, 1, 0];
|
||||||
|
expected.extend_from_slice(&socks_address(target));
|
||||||
|
let mut request = vec![0u8; expected.len()];
|
||||||
|
stream.read_exact(&mut request).await.unwrap();
|
||||||
|
assert_eq!(request, expected);
|
||||||
|
let mut response = vec![5, 0, 0];
|
||||||
|
response.extend_from_slice(&socks_address(bound));
|
||||||
|
stream.write_all(&response).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn check_socks_handshake(
|
||||||
|
transport: &str,
|
||||||
|
target: &str,
|
||||||
|
bound: &str,
|
||||||
|
policy: MeSocksKdfPolicy,
|
||||||
|
valid: bool,
|
||||||
|
) {
|
||||||
|
let target: SocketAddr = target.parse().unwrap();
|
||||||
|
let mut bound: SocketAddr = bound.parse().unwrap();
|
||||||
|
let mut pool = make_pool_with_decision(NetworkDecision {
|
||||||
|
ipv4_me: true,
|
||||||
|
ipv6_me: true,
|
||||||
|
..NetworkDecision::default()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
// Deliberately disagree with BND: an IP-only override must not replace its egress tuple.
|
||||||
|
let nat = Arc::get_mut(&mut Arc::get_mut(&mut pool).unwrap().nat_runtime).unwrap();
|
||||||
|
nat.nat_ip_cfg = Some("93.184.216.34".parse().unwrap());
|
||||||
|
pool.transport_policy
|
||||||
|
.me_socks_kdf_policy
|
||||||
|
.store(policy.as_u8(), Ordering::Relaxed);
|
||||||
|
let listener = TcpListener::bind(transport).await.unwrap();
|
||||||
|
let proxy_addr = listener.local_addr().unwrap();
|
||||||
|
let mut stream = TcpStream::connect(proxy_addr).await.unwrap();
|
||||||
|
let local = stream.local_addr().unwrap();
|
||||||
|
// Avoid accidental equality with a kernel-selected ephemeral port.
|
||||||
|
while bound.port() == local.port() || bound.port() == proxy_addr.port() {
|
||||||
|
bound.set_port(bound.port().checked_add(1).unwrap_or(1));
|
||||||
|
}
|
||||||
|
let client = async {
|
||||||
|
assert_ne!(local.port(), bound.port());
|
||||||
|
assert_ne!(proxy_addr.port(), bound.port());
|
||||||
|
let reply = connect_socks5(&mut stream, target, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(reply.addr, bound);
|
||||||
|
let egress = UpstreamEgressInfo {
|
||||||
|
upstream_id: 7,
|
||||||
|
route_kind: UpstreamRouteKind::Socks5,
|
||||||
|
local_addr: Some(local),
|
||||||
|
direct_bind_ip: None,
|
||||||
|
socks_bound_addr: Some(reply.addr),
|
||||||
|
socks_proxy_addr: Some(proxy_addr),
|
||||||
|
};
|
||||||
|
pool.handshake_only(stream, target, Some(egress), &pool.rng)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
let server = async {
|
||||||
|
let (mut stream, _) = listener.accept().await.unwrap();
|
||||||
|
socks_reply(&mut stream, target, bound).await;
|
||||||
|
if valid {
|
||||||
|
accept_handshake(stream, target, bound).await.is_some()
|
||||||
|
} else {
|
||||||
|
let mut byte = [0u8; 1];
|
||||||
|
assert_eq!(
|
||||||
|
stream.read(&mut byte).await.unwrap(),
|
||||||
|
0,
|
||||||
|
"strict rejection must precede nonce"
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (result, accepted) = timeout(Duration::from_secs(3), async {
|
||||||
|
tokio::join!(client, server)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("local SOCKS and ME handshake must finish");
|
||||||
|
if valid {
|
||||||
|
match result {
|
||||||
|
Ok(_) => assert!(accepted, "ME must validate the encrypted IP and port tuple"),
|
||||||
|
Err(error) => panic!("valid SOCKS BND must complete ME handshake: {error}"),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert!(
|
||||||
|
matches!(result, Err(ProxyError::InvalidHandshake(ref message))
|
||||||
|
if message.contains("no valid BND tuple")),
|
||||||
|
"strict policy must reject invalid BND"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn accept_handshake(
|
||||||
|
mut stream: TcpStream,
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
client_addr: SocketAddr,
|
||||||
|
) -> Option<TcpStream> {
|
||||||
|
let client_port = client_addr.port();
|
||||||
|
let server_port = server_addr.port();
|
||||||
|
let client_ip = client_addr.ip();
|
||||||
|
let server_ip = server_addr.ip();
|
||||||
|
let (seq, payload) = match read_rpc_frame_plaintext(&mut stream).await {
|
||||||
|
Ok(frame) => frame,
|
||||||
|
// Strict policy can reject the tuple before sending any nonce.
|
||||||
|
Err(ProxyError::Io(error)) if error.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Err(error) => panic!("invalid client nonce: {error}"),
|
||||||
|
};
|
||||||
|
assert_eq!(seq, -2);
|
||||||
|
let (selector, _, timestamp, client_nonce) = parse_nonce_payload(&payload).unwrap();
|
||||||
|
let server_nonce = [0x42u8; 16];
|
||||||
|
let nonce = build_nonce_payload(selector, timestamp, &server_nonce);
|
||||||
|
stream
|
||||||
|
.write_all(&build_rpc_frame(-2, &nonce, RpcChecksumMode::Crc32))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let split = |ip: IpAddr| match ip {
|
||||||
|
IpAddr::V4(ip) => {
|
||||||
|
let mut octets = ip.octets();
|
||||||
|
octets.reverse();
|
||||||
|
(Some(octets), None)
|
||||||
|
}
|
||||||
|
IpAddr::V6(ip) => (None, Some(ip.octets())),
|
||||||
|
};
|
||||||
|
let (client_v4, client_v6) = split(client_ip);
|
||||||
|
let (server_v4, server_v6) = split(server_ip);
|
||||||
|
let client_ip = client_v4.unwrap_or([0; 4]);
|
||||||
|
let server_ip = server_v4.unwrap_or([0; 4]);
|
||||||
|
let (read_key, read_iv) = derive_middleproxy_keys(
|
||||||
|
&server_nonce,
|
||||||
|
&client_nonce,
|
||||||
|
×tamp.to_le_bytes(),
|
||||||
|
server_v4.as_ref().map(|ip| ip.as_slice()),
|
||||||
|
&client_port.to_le_bytes(),
|
||||||
|
b"CLIENT",
|
||||||
|
client_v4.as_ref().map(|ip| ip.as_slice()),
|
||||||
|
&server_port.to_le_bytes(),
|
||||||
|
&[1u8; 32],
|
||||||
|
client_v6.as_ref(),
|
||||||
|
server_v6.as_ref(),
|
||||||
|
);
|
||||||
|
let (write_key, write_iv) = derive_middleproxy_keys(
|
||||||
|
&server_nonce,
|
||||||
|
&client_nonce,
|
||||||
|
×tamp.to_le_bytes(),
|
||||||
|
server_v4.as_ref().map(|ip| ip.as_slice()),
|
||||||
|
&client_port.to_le_bytes(),
|
||||||
|
b"SERVER",
|
||||||
|
client_v4.as_ref().map(|ip| ip.as_slice()),
|
||||||
|
&server_port.to_le_bytes(),
|
||||||
|
&[1u8; 32],
|
||||||
|
client_v6.as_ref(),
|
||||||
|
server_v6.as_ref(),
|
||||||
|
);
|
||||||
|
let mut encrypted = [0u8; 48];
|
||||||
|
stream.read_exact(&mut encrypted).await.unwrap();
|
||||||
|
cbc_decrypt_inplace(&read_key, &read_iv, &mut encrypted).unwrap();
|
||||||
|
assert_eq!(u32::from_le_bytes(encrypted[..4].try_into().unwrap()), 44);
|
||||||
|
assert_eq!(i32::from_le_bytes(encrypted[4..8].try_into().unwrap()), -1);
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_le_bytes(encrypted[40..44].try_into().unwrap()),
|
||||||
|
rpc_crc(RpcChecksumMode::Crc32, &encrypted[..40])
|
||||||
|
);
|
||||||
|
parse_handshake_flags(&encrypted[8..40]).unwrap();
|
||||||
|
assert_eq!(&encrypted[16..20], &client_ip);
|
||||||
|
assert_eq!(
|
||||||
|
u16::from_le_bytes(encrypted[20..22].try_into().unwrap()),
|
||||||
|
client_port
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
u16::from_le_bytes(encrypted[32..34].try_into().unwrap()),
|
||||||
|
server_port
|
||||||
|
);
|
||||||
|
let reply = build_handshake_payload(server_ip, server_port, client_ip, client_port, 0);
|
||||||
|
let (encrypted, _) = cbc_encrypt_padded(
|
||||||
|
&write_key,
|
||||||
|
&write_iv,
|
||||||
|
&build_rpc_frame(-1, &reply, RpcChecksumMode::Crc32),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
stream.write_all(&encrypted).await.unwrap();
|
||||||
|
Some(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ipv6_socks_transport_to_ipv4_me_strict() {
|
||||||
|
check_socks_handshake("[::1]:0", V4_ME, V4_BND, MeSocksKdfPolicy::Strict, true).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ipv6_socks_transport_to_ipv4_me_compat() {
|
||||||
|
check_socks_handshake("[::1]:0", V4_ME, V4_BND, MeSocksKdfPolicy::Compat, true).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ipv4_socks_transport_to_ipv6_me_strict() {
|
||||||
|
check_socks_handshake("127.0.0.1:0", V6_ME, V6_BND, MeSocksKdfPolicy::Strict, true).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ipv4_socks_transport_to_ipv6_me_compat() {
|
||||||
|
check_socks_handshake("127.0.0.1:0", V6_ME, V6_BND, MeSocksKdfPolicy::Compat, true).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn same_family_socks_keeps_bnd_ip_and_port_over_configured_nat() {
|
||||||
|
for policy in [MeSocksKdfPolicy::Strict, MeSocksKdfPolicy::Compat] {
|
||||||
|
check_socks_handshake("127.0.0.1:0", V4_ME, V4_BND, policy, true).await;
|
||||||
|
check_socks_handshake("[::1]:0", V6_ME, V6_BND, policy, true).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn strict_socks_rejects_wrong_me_family_and_incomplete_bnd() {
|
||||||
|
for (transport, target, bound) in [
|
||||||
|
("[::1]:0", V4_ME, V6_BND),
|
||||||
|
("127.0.0.1:0", V6_ME, V4_BND),
|
||||||
|
("[::1]:0", V4_ME, "10.0.0.1:45678"),
|
||||||
|
("[::1]:0", V4_ME, "0.0.0.0:45678"),
|
||||||
|
("[::1]:0", V4_ME, "91.108.56.1:0"),
|
||||||
|
("127.0.0.1:0", V6_ME, "[::]:45678"),
|
||||||
|
("127.0.0.1:0", V6_ME, "[2606:4700:4700::1111]:0"),
|
||||||
|
] {
|
||||||
|
check_socks_handshake(transport, target, bound, MeSocksKdfPolicy::Strict, false).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user