mirror of
https://github.com/telemt/telemt.git
synced 2026-07-15 16:30:20 +03:00
Hardened KDF-Tuple + NAT Probing + Paddings
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
+33
-7
@@ -12,7 +12,7 @@ use tracing::{debug, info, warn};
|
||||
use crate::config::{NetworkConfig, UpstreamConfig, UpstreamType};
|
||||
use crate::error::Result;
|
||||
use crate::network::stun::{
|
||||
DualStunResult, IpFamily, StunProbeResult, stun_probe_family_with_bind,
|
||||
DualStunResult, IpFamily, StunProbeResult, stun_probe_family_with_bind_and_tcp_fallback,
|
||||
};
|
||||
use crate::transport::UpstreamManager;
|
||||
|
||||
@@ -58,6 +58,7 @@ impl NetworkDecision {
|
||||
}
|
||||
|
||||
const STUN_BATCH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const STUN_BATCH_TCP_FALLBACK_TIMEOUT: Duration = Duration::from_secs(12);
|
||||
|
||||
pub async fn run_probe(
|
||||
config: &NetworkConfig,
|
||||
@@ -81,8 +82,14 @@ pub async fn run_probe(
|
||||
warn!("STUN probe is enabled but network.stun_servers is empty");
|
||||
DualStunResult::default()
|
||||
} else {
|
||||
probe_stun_servers_parallel(&servers, stun_nat_probe_concurrency.max(1), None, None)
|
||||
.await
|
||||
probe_stun_servers_parallel(
|
||||
&servers,
|
||||
stun_nat_probe_concurrency.max(1),
|
||||
None,
|
||||
None,
|
||||
config.stun_tcp_fallback,
|
||||
)
|
||||
.await
|
||||
}
|
||||
} else if nat_probe {
|
||||
info!("STUN probe is disabled by network.stun_use=false");
|
||||
@@ -163,6 +170,7 @@ pub async fn run_probe(
|
||||
stun_nat_probe_concurrency.max(1),
|
||||
bind_v4,
|
||||
bind_v6,
|
||||
config.stun_tcp_fallback,
|
||||
)
|
||||
.await;
|
||||
if let Some(reflected) = direct_stun_res.v4.map(|r| r.reflected_addr) {
|
||||
@@ -234,7 +242,7 @@ pub async fn run_probe(
|
||||
Ok(probe)
|
||||
}
|
||||
|
||||
async fn detect_public_ipv4_http(urls: &[String]) -> Option<Ipv4Addr> {
|
||||
pub(crate) async fn detect_public_ipv4_http(urls: &[String]) -> Option<Ipv4Addr> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()
|
||||
@@ -277,6 +285,7 @@ async fn probe_stun_servers_parallel(
|
||||
concurrency: usize,
|
||||
bind_v4: Option<IpAddr>,
|
||||
bind_v6: Option<IpAddr>,
|
||||
tcp_fallback: bool,
|
||||
) -> DualStunResult {
|
||||
let mut join_set = JoinSet::new();
|
||||
let mut next_idx = 0usize;
|
||||
@@ -288,9 +297,26 @@ async fn probe_stun_servers_parallel(
|
||||
let stun_addr = servers[next_idx].clone();
|
||||
next_idx += 1;
|
||||
join_set.spawn(async move {
|
||||
let res = timeout(STUN_BATCH_TIMEOUT, async {
|
||||
let v4 = stun_probe_family_with_bind(&stun_addr, IpFamily::V4, bind_v4).await?;
|
||||
let v6 = stun_probe_family_with_bind(&stun_addr, IpFamily::V6, bind_v6).await?;
|
||||
let batch_timeout = if tcp_fallback {
|
||||
STUN_BATCH_TCP_FALLBACK_TIMEOUT
|
||||
} else {
|
||||
STUN_BATCH_TIMEOUT
|
||||
};
|
||||
let res = timeout(batch_timeout, async {
|
||||
let v4 = stun_probe_family_with_bind_and_tcp_fallback(
|
||||
&stun_addr,
|
||||
IpFamily::V4,
|
||||
bind_v4,
|
||||
tcp_fallback,
|
||||
)
|
||||
.await?;
|
||||
let v6 = stun_probe_family_with_bind_and_tcp_fallback(
|
||||
&stun_addr,
|
||||
IpFamily::V6,
|
||||
bind_v6,
|
||||
tcp_fallback,
|
||||
)
|
||||
.await?;
|
||||
Ok::<DualStunResult, crate::error::ProxyError>(DualStunResult { v4, v6 })
|
||||
})
|
||||
.await;
|
||||
|
||||
+235
-41
@@ -4,7 +4,8 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use tokio::net::{UdpSocket, lookup_host};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::{TcpSocket, UdpSocket, lookup_host};
|
||||
use tokio::time::{Duration, sleep, timeout};
|
||||
|
||||
use crate::crypto::SecureRandom;
|
||||
@@ -36,9 +37,16 @@ pub struct DualStunResult {
|
||||
}
|
||||
|
||||
pub async fn stun_probe_dual(stun_addr: &str) -> Result<DualStunResult> {
|
||||
stun_probe_dual_with_tcp_fallback(stun_addr, false).await
|
||||
}
|
||||
|
||||
pub async fn stun_probe_dual_with_tcp_fallback(
|
||||
stun_addr: &str,
|
||||
tcp_fallback: bool,
|
||||
) -> Result<DualStunResult> {
|
||||
let (v4, v6) = tokio::join!(
|
||||
stun_probe_family(stun_addr, IpFamily::V4),
|
||||
stun_probe_family(stun_addr, IpFamily::V6),
|
||||
stun_probe_family_with_tcp_fallback(stun_addr, IpFamily::V4, tcp_fallback),
|
||||
stun_probe_family_with_tcp_fallback(stun_addr, IpFamily::V6, tcp_fallback),
|
||||
);
|
||||
|
||||
Ok(DualStunResult { v4: v4?, v6: v6? })
|
||||
@@ -48,13 +56,44 @@ pub async fn stun_probe_family(
|
||||
stun_addr: &str,
|
||||
family: IpFamily,
|
||||
) -> Result<Option<StunProbeResult>> {
|
||||
stun_probe_family_with_bind(stun_addr, family, None).await
|
||||
stun_probe_family_with_tcp_fallback(stun_addr, family, false).await
|
||||
}
|
||||
|
||||
pub async fn stun_probe_family_with_tcp_fallback(
|
||||
stun_addr: &str,
|
||||
family: IpFamily,
|
||||
tcp_fallback: bool,
|
||||
) -> Result<Option<StunProbeResult>> {
|
||||
stun_probe_family_with_bind_and_tcp_fallback(stun_addr, family, None, tcp_fallback).await
|
||||
}
|
||||
|
||||
pub async fn stun_probe_family_with_bind(
|
||||
stun_addr: &str,
|
||||
family: IpFamily,
|
||||
bind_ip: Option<IpAddr>,
|
||||
) -> Result<Option<StunProbeResult>> {
|
||||
stun_probe_family_with_bind_and_tcp_fallback(stun_addr, family, bind_ip, false).await
|
||||
}
|
||||
|
||||
pub async fn stun_probe_family_with_bind_and_tcp_fallback(
|
||||
stun_addr: &str,
|
||||
family: IpFamily,
|
||||
bind_ip: Option<IpAddr>,
|
||||
tcp_fallback: bool,
|
||||
) -> Result<Option<StunProbeResult>> {
|
||||
let udp_attempts = if tcp_fallback { 1 } else { 3 };
|
||||
let udp_result = stun_probe_family_udp(stun_addr, family, bind_ip, udp_attempts).await?;
|
||||
if udp_result.is_some() || !tcp_fallback {
|
||||
return Ok(udp_result);
|
||||
}
|
||||
stun_probe_family_tcp(stun_addr, family, bind_ip).await
|
||||
}
|
||||
|
||||
async fn stun_probe_family_udp(
|
||||
stun_addr: &str,
|
||||
family: IpFamily,
|
||||
bind_ip: Option<IpAddr>,
|
||||
max_attempts: u8,
|
||||
) -> Result<Option<StunProbeResult>> {
|
||||
let bind_addr = match (family, bind_ip) {
|
||||
(IpFamily::V4, Some(IpAddr::V4(ip))) => SocketAddr::new(IpAddr::V4(ip), 0),
|
||||
@@ -94,12 +133,7 @@ pub async fn stun_probe_family_with_bind(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut req = [0u8; 20];
|
||||
req[0..2].copy_from_slice(&0x0001u16.to_be_bytes()); // Binding Request
|
||||
req[2..4].copy_from_slice(&0u16.to_be_bytes()); // length
|
||||
req[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes()); // magic cookie
|
||||
stun_rng().fill(&mut req[8..20]); // transaction ID
|
||||
|
||||
let req = build_binding_request();
|
||||
let mut buf = [0u8; 256];
|
||||
let mut attempt = 0;
|
||||
let mut backoff = Duration::from_secs(1);
|
||||
@@ -115,7 +149,7 @@ pub async fn stun_probe_family_with_bind(
|
||||
Ok(Err(e)) => return Err(ProxyError::Proxy(format!("STUN recv failed: {e}"))),
|
||||
Err(_) => {
|
||||
attempt += 1;
|
||||
if attempt >= 3 {
|
||||
if attempt >= max_attempts {
|
||||
return Ok(None);
|
||||
}
|
||||
sleep(backoff).await;
|
||||
@@ -128,19 +162,139 @@ pub async fn stun_probe_family_with_bind(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let magic = 0x2112A442u32.to_be_bytes();
|
||||
let txid = &req[8..20];
|
||||
let mut idx = 20;
|
||||
while idx + 4 <= n {
|
||||
let atype = u16::from_be_bytes(buf[idx..idx + 2].try_into().unwrap());
|
||||
let alen = u16::from_be_bytes(buf[idx + 2..idx + 4].try_into().unwrap()) as usize;
|
||||
idx += 4;
|
||||
if idx + alen > n {
|
||||
break;
|
||||
}
|
||||
if let Some(reflected_addr) = parse_reflected_addr(&buf[..n], txid) {
|
||||
let local_addr = socket
|
||||
.local_addr()
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN local_addr failed: {e}")))?;
|
||||
return Ok(Some(StunProbeResult {
|
||||
local_addr,
|
||||
reflected_addr,
|
||||
family,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
match atype {
|
||||
0x0020 /* XOR-MAPPED-ADDRESS */ | 0x0001 /* MAPPED-ADDRESS */ => {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn stun_probe_family_tcp(
|
||||
stun_addr: &str,
|
||||
family: IpFamily,
|
||||
bind_ip: Option<IpAddr>,
|
||||
) -> Result<Option<StunProbeResult>> {
|
||||
let target_addr = match resolve_stun_addr(stun_addr, family).await? {
|
||||
Some(addr) => addr,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let socket = match family {
|
||||
IpFamily::V4 => TcpSocket::new_v4(),
|
||||
IpFamily::V6 => TcpSocket::new_v6(),
|
||||
}
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN TCP socket failed: {e}")))?;
|
||||
match (family, bind_ip) {
|
||||
(IpFamily::V4, Some(IpAddr::V4(ip))) => {
|
||||
if socket.bind(SocketAddr::new(IpAddr::V4(ip), 0)).is_err() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
(IpFamily::V6, Some(IpAddr::V6(ip))) => {
|
||||
if socket.bind(SocketAddr::new(IpAddr::V6(ip), 0)).is_err() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
(IpFamily::V4, Some(IpAddr::V6(_))) | (IpFamily::V6, Some(IpAddr::V4(_))) => {
|
||||
return Ok(None);
|
||||
}
|
||||
(_, None) => {}
|
||||
}
|
||||
|
||||
let connect_res = timeout(Duration::from_secs(3), socket.connect(target_addr)).await;
|
||||
let mut stream = match connect_res {
|
||||
Ok(Ok(stream)) => stream,
|
||||
Ok(Err(e))
|
||||
if family == IpFamily::V6
|
||||
&& matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::NetworkUnreachable
|
||||
| std::io::ErrorKind::HostUnreachable
|
||||
| std::io::ErrorKind::Unsupported
|
||||
| std::io::ErrorKind::NetworkDown
|
||||
) =>
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Err(e)) => return Err(ProxyError::Proxy(format!("STUN TCP connect failed: {e}"))),
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
let req = build_binding_request();
|
||||
timeout(Duration::from_secs(3), stream.write_all(&req))
|
||||
.await
|
||||
.map_err(|_| ProxyError::Proxy("STUN TCP send timeout".to_string()))?
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN TCP send failed: {e}")))?;
|
||||
|
||||
let mut header = [0u8; 20];
|
||||
timeout(Duration::from_secs(3), stream.read_exact(&mut header))
|
||||
.await
|
||||
.map_err(|_| ProxyError::Proxy("STUN TCP header timeout".to_string()))?
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN TCP header read failed: {e}")))?;
|
||||
let body_len = u16::from_be_bytes([header[2], header[3]]) as usize;
|
||||
if body_len > 236 {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut buf = [0u8; 256];
|
||||
buf[..20].copy_from_slice(&header);
|
||||
if body_len > 0 {
|
||||
timeout(
|
||||
Duration::from_secs(3),
|
||||
stream.read_exact(&mut buf[20..20 + body_len]),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ProxyError::Proxy("STUN TCP body timeout".to_string()))?
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN TCP body read failed: {e}")))?;
|
||||
}
|
||||
|
||||
let txid = &req[8..20];
|
||||
let Some(reflected_addr) = parse_reflected_addr(&buf[..20 + body_len], txid) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let local_addr = stream
|
||||
.local_addr()
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN TCP local_addr failed: {e}")))?;
|
||||
Ok(Some(StunProbeResult {
|
||||
local_addr,
|
||||
reflected_addr,
|
||||
family,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_binding_request() -> [u8; 20] {
|
||||
let mut req = [0u8; 20];
|
||||
req[0..2].copy_from_slice(&0x0001u16.to_be_bytes());
|
||||
req[2..4].copy_from_slice(&0u16.to_be_bytes());
|
||||
req[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes());
|
||||
stun_rng().fill(&mut req[8..20]);
|
||||
req
|
||||
}
|
||||
|
||||
fn parse_reflected_addr(buf: &[u8], txid: &[u8]) -> Option<SocketAddr> {
|
||||
if buf.len() < 20 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let magic = 0x2112A442u32.to_be_bytes();
|
||||
let mut idx = 20;
|
||||
while idx + 4 <= buf.len() {
|
||||
let atype = u16::from_be_bytes(buf[idx..idx + 2].try_into().ok()?);
|
||||
let alen = u16::from_be_bytes(buf[idx + 2..idx + 4].try_into().ok()?) as usize;
|
||||
idx += 4;
|
||||
if idx + alen > buf.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
match atype {
|
||||
0x0020 | 0x0001 => {
|
||||
if alen < 8 {
|
||||
break;
|
||||
}
|
||||
@@ -157,7 +311,6 @@ pub async fn stun_probe_family_with_bind(
|
||||
|
||||
let raw_ip = &buf[idx + 4..idx + 4 + len_check];
|
||||
let mut port = u16::from_be_bytes(port_bytes);
|
||||
|
||||
let reflected_ip = if atype == 0x0020 {
|
||||
port ^= ((magic[0] as u16) << 8) | magic[1] as u16;
|
||||
match family_byte {
|
||||
@@ -172,7 +325,9 @@ pub async fn stun_probe_family_with_bind(
|
||||
}
|
||||
0x02 => {
|
||||
let mut ip = [0u8; 16];
|
||||
let xor_key = [magic.as_slice(), txid].concat();
|
||||
let mut xor_key = [0u8; 16];
|
||||
xor_key[..4].copy_from_slice(&magic);
|
||||
xor_key[4..].copy_from_slice(txid.get(..12)?);
|
||||
for (i, b) in raw_ip.iter().enumerate().take(16) {
|
||||
ip[i] = *b ^ xor_key[i];
|
||||
}
|
||||
@@ -185,34 +340,24 @@ pub async fn stun_probe_family_with_bind(
|
||||
}
|
||||
} else {
|
||||
match family_byte {
|
||||
0x01 => IpAddr::V4(Ipv4Addr::new(raw_ip[0], raw_ip[1], raw_ip[2], raw_ip[3])),
|
||||
0x02 => IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(raw_ip).unwrap())),
|
||||
0x01 => {
|
||||
IpAddr::V4(Ipv4Addr::new(raw_ip[0], raw_ip[1], raw_ip[2], raw_ip[3]))
|
||||
}
|
||||
0x02 => IpAddr::V6(Ipv6Addr::from(<[u8; 16]>::try_from(raw_ip).ok()?)),
|
||||
_ => {
|
||||
idx += (alen + 3) & !3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let reflected_addr = SocketAddr::new(reflected_ip, port);
|
||||
let local_addr = socket
|
||||
.local_addr()
|
||||
.map_err(|e| ProxyError::Proxy(format!("STUN local_addr failed: {e}")))?;
|
||||
|
||||
return Ok(Some(StunProbeResult {
|
||||
local_addr,
|
||||
reflected_addr,
|
||||
family,
|
||||
}));
|
||||
return Some(SocketAddr::new(reflected_ip, port));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
idx += (alen + 3) & !3;
|
||||
}
|
||||
idx += (alen + 3) & !3;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
None
|
||||
}
|
||||
|
||||
async fn resolve_stun_addr(stun_addr: &str, family: IpFamily) -> Result<Option<SocketAddr>> {
|
||||
@@ -245,3 +390,52 @@ async fn resolve_stun_addr(stun_addr: &str, family: IpFamily) -> Result<Option<S
|
||||
});
|
||||
Ok(target)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_reflected_addr_reads_mapped_ipv4() {
|
||||
let txid = [0u8; 12];
|
||||
let mut response = [0u8; 32];
|
||||
response[0..2].copy_from_slice(&0x0101u16.to_be_bytes());
|
||||
response[2..4].copy_from_slice(&12u16.to_be_bytes());
|
||||
response[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes());
|
||||
response[20..22].copy_from_slice(&0x0001u16.to_be_bytes());
|
||||
response[22..24].copy_from_slice(&8u16.to_be_bytes());
|
||||
response[25] = 0x01;
|
||||
response[26..28].copy_from_slice(&443u16.to_be_bytes());
|
||||
response[28..32].copy_from_slice(&[203, 0, 113, 9]);
|
||||
|
||||
let reflected = parse_reflected_addr(&response, &txid).unwrap();
|
||||
assert_eq!(reflected, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 9)), 443));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_reflected_addr_reads_xor_mapped_ipv4() {
|
||||
let txid = [0u8; 12];
|
||||
let magic = 0x2112A442u32.to_be_bytes();
|
||||
let port = 443u16;
|
||||
let ip = [203u8, 0, 113, 9];
|
||||
let xport = port ^ (((magic[0] as u16) << 8) | magic[1] as u16);
|
||||
let xip = [
|
||||
ip[0] ^ magic[0],
|
||||
ip[1] ^ magic[1],
|
||||
ip[2] ^ magic[2],
|
||||
ip[3] ^ magic[3],
|
||||
];
|
||||
let mut response = [0u8; 32];
|
||||
response[0..2].copy_from_slice(&0x0101u16.to_be_bytes());
|
||||
response[2..4].copy_from_slice(&12u16.to_be_bytes());
|
||||
response[4..8].copy_from_slice(&0x2112A442u32.to_be_bytes());
|
||||
response[20..22].copy_from_slice(&0x0020u16.to_be_bytes());
|
||||
response[22..24].copy_from_slice(&8u16.to_be_bytes());
|
||||
response[25] = 0x01;
|
||||
response[26..28].copy_from_slice(&xport.to_be_bytes());
|
||||
response[28..32].copy_from_slice(&xip);
|
||||
|
||||
let reflected = parse_reflected_addr(&response, &txid).unwrap();
|
||||
assert_eq!(reflected, SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 9)), 443));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user