use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::time::Duration;
use dig_nat::method::natpmp::{
encode_map_request, NatPmpMethod, OP_EXTERNAL_ADDRESS, OP_MAP_UDP, RESPONSE_FLAG,
};
use dig_nat::method::pcp::{MapNonce, PcpMethod, OP_MAP, PCP_VERSION, PROTO_UDP, RESPONSE_BIT};
use dig_nat::method::{TraversalKind, TraversalMethod};
use dig_nat::stun::{query_reflexive_address, StunError, BINDING_SUCCESS, MAGIC_COOKIE};
use dig_nat::{PeerId, PeerTarget};
use tokio::net::UdpSocket;
fn peer(addr: &str) -> PeerTarget {
PeerTarget::with_addr(
PeerId::from_bytes([1u8; 32]),
addr.parse().unwrap(),
"DIG_MAINNET",
)
}
#[tokio::test]
async fn natpmp_attempt_succeeds_against_loopback_gateway() {
let gw = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let gw_addr = gw.local_addr().unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 32];
let (_, from) = gw.recv_from(&mut buf).await.unwrap();
let mut ext = vec![0u8; 12];
ext[1] = OP_EXTERNAL_ADDRESS + RESPONSE_FLAG;
ext[8..12].copy_from_slice(&[203, 0, 113, 9]);
gw.send_to(&ext, from).await.unwrap();
let (n, from) = gw.recv_from(&mut buf).await.unwrap();
assert_eq!(buf[1], OP_MAP_UDP, "second request is a map");
let _ = n;
let mut map = vec![0u8; 16];
map[1] = OP_MAP_UDP + RESPONSE_FLAG;
map[8..10].copy_from_slice(&4444u16.to_be_bytes());
map[10..12].copy_from_slice(&4444u16.to_be_bytes());
map[12..16].copy_from_slice(&7200u32.to_be_bytes());
gw.send_to(&map, from).await.unwrap();
});
let mut m = NatPmpMethod::new(*ipv4(gw_addr), 4444);
m.gateway = to_v4(gw_addr);
m.timeout = Duration::from_secs(2);
let out = m.attempt(&peer("198.51.100.7:4444")).await.unwrap();
assert_eq!(out.kind, TraversalKind::NatPmp);
assert_eq!(out.dial_addr(), Some("198.51.100.7:4444".parse().unwrap()));
}
#[tokio::test]
async fn natpmp_times_out_when_no_gateway() {
let mut m = NatPmpMethod::new(Ipv4Addr::LOCALHOST, 4444);
m.gateway = "127.0.0.1:9".parse().unwrap();
m.timeout = Duration::from_millis(150);
let err = m.attempt(&peer("198.51.100.7:4444")).await.unwrap_err();
assert_eq!(err.kind, TraversalKind::NatPmp);
assert!(
err.timeout || err.reason.contains("io"),
"graceful failure, got: {}",
err.reason
);
}
#[tokio::test]
async fn pcp_attempt_succeeds_against_loopback_gateway() {
let gw = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let gw_addr = gw.local_addr().unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 128];
let (n, from) = gw.recv_from(&mut buf).await.unwrap();
assert!(n >= 60, "a PCP MAP request is 60 bytes");
let nonce: MapNonce = buf[24..36].try_into().unwrap();
let mut resp = vec![0u8; 60];
resp[0] = PCP_VERSION;
resp[1] = OP_MAP | RESPONSE_BIT;
resp[3] = 0; resp[4..8].copy_from_slice(&7200u32.to_be_bytes());
resp[24..36].copy_from_slice(&nonce);
resp[36] = PROTO_UDP;
resp[42..44].copy_from_slice(&5555u16.to_be_bytes());
let mapped = Ipv4Addr::new(203, 0, 113, 9).to_ipv6_mapped().octets();
resp[44..60].copy_from_slice(&mapped);
gw.send_to(&resp, from).await.unwrap();
});
let mut m = PcpMethod::new(
*ipv4(gw_addr),
4444,
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)),
);
m.gateway = to_v4(gw_addr);
m.timeout = Duration::from_secs(2);
let out = m.attempt(&peer("198.51.100.7:4444")).await.unwrap();
assert_eq!(out.kind, TraversalKind::Pcp);
assert_eq!(out.dial_addr(), Some("198.51.100.7:4444".parse().unwrap()));
}
#[tokio::test]
async fn pcp_ignores_response_from_non_gateway_source() {
let gw = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let gw_addr = gw.local_addr().unwrap();
let attacker = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 128];
let (n, from) = gw.recv_from(&mut buf).await.unwrap();
assert!(n >= 60, "a PCP MAP request is 60 bytes");
let nonce: MapNonce = buf[24..36].try_into().unwrap();
let mut spoofed = vec![0u8; 60];
spoofed[0] = PCP_VERSION;
spoofed[1] = OP_MAP | RESPONSE_BIT;
spoofed[3] = 0;
spoofed[4..8].copy_from_slice(&7200u32.to_be_bytes());
spoofed[24..36].copy_from_slice(&nonce);
spoofed[36] = PROTO_UDP;
spoofed[42..44].copy_from_slice(&6666u16.to_be_bytes());
let poisoned = Ipv4Addr::new(198, 51, 100, 66).to_ipv6_mapped().octets();
spoofed[44..60].copy_from_slice(&poisoned);
attacker.send_to(&spoofed, from).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
let mut resp = vec![0u8; 60];
resp[0] = PCP_VERSION;
resp[1] = OP_MAP | RESPONSE_BIT;
resp[3] = 0;
resp[4..8].copy_from_slice(&7200u32.to_be_bytes());
resp[24..36].copy_from_slice(&nonce);
resp[36] = PROTO_UDP;
resp[42..44].copy_from_slice(&5555u16.to_be_bytes());
let mapped = Ipv4Addr::new(203, 0, 113, 9).to_ipv6_mapped().octets();
resp[44..60].copy_from_slice(&mapped);
gw.send_to(&resp, from).await.unwrap();
});
let mut m = PcpMethod::new(
*ipv4(gw_addr),
4444,
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)),
);
m.gateway = to_v4(gw_addr);
m.timeout = Duration::from_secs(2);
let out = m
.attempt(&peer("198.51.100.7:4444"))
.await
.expect("keeps waiting past the spoofed datagram and accepts the genuine reply");
assert_eq!(out.kind, TraversalKind::Pcp);
assert_eq!(out.dial_addr(), Some("198.51.100.7:4444".parse().unwrap()));
}
#[tokio::test]
async fn pcp_times_out_when_no_gateway() {
let mut m = PcpMethod::new(Ipv4Addr::LOCALHOST, 4444, IpAddr::V4(Ipv4Addr::LOCALHOST));
m.gateway = "127.0.0.1:9".parse().unwrap();
m.timeout = Duration::from_millis(150);
let err = m.attempt(&peer("198.51.100.7:4444")).await.unwrap_err();
assert_eq!(err.kind, TraversalKind::Pcp);
assert!(
err.timeout || err.reason.contains("io"),
"graceful failure, got: {}",
err.reason
);
}
#[tokio::test]
async fn stun_query_reflexive_address_against_loopback_server() {
let server = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let server_addr = server.local_addr().unwrap();
tokio::spawn(async move {
let mut buf = [0u8; 512];
let (_, from) = server.recv_from(&mut buf).await.unwrap();
let txid: [u8; 12] = buf[8..20].try_into().unwrap();
let reflexive = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 55)), 41234);
let resp = build_xor_mapped_response(&txid, reflexive);
server.send_to(&resp, from).await.unwrap();
});
let client = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let got = query_reflexive_address(&client, server_addr, Duration::from_secs(2))
.await
.unwrap();
assert_eq!(got.port(), 41234);
assert_eq!(got.ip(), IpAddr::V4(Ipv4Addr::new(203, 0, 113, 55)));
}
#[tokio::test]
async fn stun_ignores_response_from_non_queried_source() {
let server = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let server_addr = server.local_addr().unwrap();
let attacker = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let genuine_reflexive = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 55)), 41234);
let poisoned_reflexive = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 66)), 6666);
tokio::spawn(async move {
let mut buf = [0u8; 512];
let (_, from) = server.recv_from(&mut buf).await.unwrap();
let txid: [u8; 12] = buf[8..20].try_into().unwrap();
let spoofed = build_xor_mapped_response(&txid, poisoned_reflexive);
attacker.send_to(&spoofed, from).await.unwrap();
tokio::time::sleep(Duration::from_millis(50)).await;
let genuine = build_xor_mapped_response(&txid, genuine_reflexive);
server.send_to(&genuine, from).await.unwrap();
});
let client = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let got = query_reflexive_address(&client, server_addr, Duration::from_secs(2))
.await
.expect("keeps waiting past the spoofed datagram and accepts the genuine reply");
assert_eq!(
got, genuine_reflexive,
"must return the server's genuine reflexive address, not the attacker's poisoned one"
);
}
#[tokio::test]
async fn stun_query_times_out_when_no_server() {
let client = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let err = query_reflexive_address(
&client,
"127.0.0.1:9".parse().unwrap(),
Duration::from_millis(150),
)
.await
.unwrap_err();
assert!(
matches!(err, StunError::Timeout | StunError::Io(_)),
"graceful failure, got: {err:?}"
);
}
#[test]
fn natpmp_map_encoder_public() {
let req = encode_map_request(true, 1, 2, 3);
assert_eq!(req.len(), 12);
}
fn ipv4(addr: SocketAddr) -> &'static Ipv4Addr {
match addr {
SocketAddr::V4(v4) => Box::leak(Box::new(*v4.ip())),
SocketAddr::V6(_) => Box::leak(Box::new(Ipv4Addr::LOCALHOST)),
}
}
fn to_v4(addr: SocketAddr) -> std::net::SocketAddrV4 {
match addr {
SocketAddr::V4(v4) => v4,
SocketAddr::V6(_) => std::net::SocketAddrV4::new(Ipv4Addr::LOCALHOST, addr.port()),
}
}
fn build_xor_mapped_response(txid: &[u8; 12], addr: SocketAddr) -> Vec<u8> {
let cookie_be = MAGIC_COOKIE.to_be_bytes();
let mut value = vec![0u8]; value.push(0x01); let port = addr.port() ^ ((MAGIC_COOKIE >> 16) as u16);
value.extend_from_slice(&port.to_be_bytes());
let IpAddr::V4(v4) = addr.ip() else {
unreachable!()
};
let mut octets = v4.octets();
for (i, o) in octets.iter_mut().enumerate() {
*o ^= cookie_be[i];
}
value.extend_from_slice(&octets);
let mut attr = Vec::new();
attr.extend_from_slice(&0x0020u16.to_be_bytes()); attr.extend_from_slice(&(value.len() as u16).to_be_bytes());
attr.extend_from_slice(&value);
let mut msg = Vec::new();
msg.extend_from_slice(&BINDING_SUCCESS.to_be_bytes());
msg.extend_from_slice(&(attr.len() as u16).to_be_bytes());
msg.extend_from_slice(&cookie_be);
msg.extend_from_slice(txid);
msg.extend_from_slice(&attr);
msg
}