mirror of
https://github.com/telemt/telemt.git
synced 2026-06-28 22:01:11 +03:00
Hardened KDF-Tuple + NAT Probing + Paddings
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,7 @@ use tokio::time::timeout;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::MeSocksKdfPolicy;
|
||||
use crate::crypto::{SecureRandom, build_middleproxy_prekey, derive_middleproxy_keys, sha256};
|
||||
use crate::crypto::{SecureRandom, derive_middleproxy_keys};
|
||||
use crate::error::{ProxyError, Result};
|
||||
use crate::network::IpFamily;
|
||||
use crate::network::probe::is_bogon;
|
||||
@@ -292,14 +292,15 @@ impl MePool {
|
||||
BndPortStatus::Error
|
||||
};
|
||||
record_bnd_status(bnd_addr_status, bnd_port_status, raw_socks_bound_addr);
|
||||
let reflected = if let Some(bound) = socks_bound_addr {
|
||||
let socks_bound_kdf_addr = socks_bound_addr.filter(|bound| bound.port() != 0);
|
||||
let reflected = if let Some(bound) = socks_bound_kdf_addr {
|
||||
Some(bound)
|
||||
} else if is_socks_route {
|
||||
match self.socks_kdf_policy() {
|
||||
MeSocksKdfPolicy::Strict => {
|
||||
self.stats.increment_me_socks_kdf_strict_reject();
|
||||
return Err(ProxyError::InvalidHandshake(
|
||||
"SOCKS route returned no valid BND.ADDR for ME KDF (strict policy)"
|
||||
"SOCKS route returned no valid BND tuple for ME KDF (strict policy)"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
@@ -323,16 +324,14 @@ impl MePool {
|
||||
let local_addr_nat = self.translate_our_addr_with_reflection(local_addr, reflected);
|
||||
let peer_addr_nat =
|
||||
SocketAddr::new(self.translate_ip_for_nat(peer_addr.ip()), peer_addr.port());
|
||||
let client_addr_for_kdf = socks_bound_kdf_addr.unwrap_or(local_addr_nat);
|
||||
if let Some(upstream_info) = upstream_egress {
|
||||
let client_ip_for_kdf = socks_bound_addr
|
||||
.map(|value| value.ip())
|
||||
.unwrap_or(local_addr_nat.ip());
|
||||
record_upstream_bnd_status(
|
||||
upstream_info.upstream_id,
|
||||
bnd_addr_status,
|
||||
bnd_port_status,
|
||||
raw_socks_bound_addr,
|
||||
Some(client_ip_for_kdf),
|
||||
Some(client_addr_for_kdf.ip()),
|
||||
);
|
||||
}
|
||||
let (mut rd, mut wr) = tokio::io::split(stream);
|
||||
@@ -409,6 +408,7 @@ impl MePool {
|
||||
info!(
|
||||
%local_addr,
|
||||
%local_addr_nat,
|
||||
%client_addr_for_kdf,
|
||||
reflected_ip = reflected.map(|r| r.ip()).as_ref().map(ToString::to_string),
|
||||
%peer_addr,
|
||||
%transport_peer_addr,
|
||||
@@ -422,16 +422,14 @@ impl MePool {
|
||||
|
||||
let ts_bytes = crypto_ts.to_le_bytes();
|
||||
let server_port_bytes = peer_addr_nat.port().to_le_bytes();
|
||||
let socks_bound_port = socks_bound_addr
|
||||
.map(|bound| bound.port())
|
||||
.filter(|port| *port != 0);
|
||||
let client_port_for_kdf = socks_bound_port.unwrap_or(local_addr_nat.port());
|
||||
let socks_bound_port = socks_bound_kdf_addr.map(|bound| bound.port());
|
||||
let client_port_for_kdf = client_addr_for_kdf.port();
|
||||
let client_port_source = KdfClientPortSource::from_socks_bound_port(socks_bound_port);
|
||||
let kdf_fingerprint = Self::kdf_material_fingerprint(
|
||||
local_addr_nat.ip(),
|
||||
client_addr_for_kdf.ip(),
|
||||
peer_addr_nat,
|
||||
reflected.map(|value| value.ip()),
|
||||
socks_bound_addr.map(|value| value.ip()),
|
||||
socks_bound_kdf_addr.map(|value| value.ip()),
|
||||
client_port_source,
|
||||
);
|
||||
let previous_kdf_fingerprint = {
|
||||
@@ -473,7 +471,7 @@ impl MePool {
|
||||
let client_port_bytes = client_port_for_kdf.to_le_bytes();
|
||||
|
||||
let server_ip = extract_ip_material(peer_addr_nat);
|
||||
let client_ip = extract_ip_material(local_addr_nat);
|
||||
let client_ip = extract_ip_material(client_addr_for_kdf);
|
||||
|
||||
let (srv_ip_opt, clt_ip_opt, clt_v6_opt, srv_v6_opt, hs_our_ip, hs_peer_ip) =
|
||||
match (server_ip, client_ip) {
|
||||
@@ -494,38 +492,6 @@ impl MePool {
|
||||
}
|
||||
};
|
||||
|
||||
let diag_level: u8 = std::env::var("ME_DIAG")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
let prekey_client = build_middleproxy_prekey(
|
||||
&srv_nonce,
|
||||
&my_nonce,
|
||||
&ts_bytes,
|
||||
srv_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&client_port_bytes,
|
||||
b"CLIENT",
|
||||
clt_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&server_port_bytes,
|
||||
&secret,
|
||||
clt_v6_opt.as_ref(),
|
||||
srv_v6_opt.as_ref(),
|
||||
);
|
||||
let prekey_server = build_middleproxy_prekey(
|
||||
&srv_nonce,
|
||||
&my_nonce,
|
||||
&ts_bytes,
|
||||
srv_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&client_port_bytes,
|
||||
b"SERVER",
|
||||
clt_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&server_port_bytes,
|
||||
&secret,
|
||||
clt_v6_opt.as_ref(),
|
||||
srv_v6_opt.as_ref(),
|
||||
);
|
||||
|
||||
let (wk, wi) = derive_middleproxy_keys(
|
||||
&srv_nonce,
|
||||
&my_nonce,
|
||||
@@ -556,47 +522,14 @@ impl MePool {
|
||||
let requested_crc_mode = RpcChecksumMode::Crc32c;
|
||||
let hs_payload = build_handshake_payload(
|
||||
hs_our_ip,
|
||||
local_addr.port(),
|
||||
client_port_for_kdf,
|
||||
hs_peer_ip,
|
||||
peer_addr.port(),
|
||||
peer_addr_nat.port(),
|
||||
requested_crc_mode.advertised_flags(),
|
||||
);
|
||||
let hs_frame = build_rpc_frame(-1, &hs_payload, RpcChecksumMode::Crc32);
|
||||
if diag_level >= 1 {
|
||||
info!(
|
||||
write_key = %hex_dump(&wk),
|
||||
write_iv = %hex_dump(&wi),
|
||||
read_key = %hex_dump(&rk),
|
||||
read_iv = %hex_dump(&ri),
|
||||
srv_ip = %srv_ip_opt.map(|ip| hex_dump(&ip)).unwrap_or_default(),
|
||||
clt_ip = %clt_ip_opt.map(|ip| hex_dump(&ip)).unwrap_or_default(),
|
||||
srv_port = %hex_dump(&server_port_bytes),
|
||||
clt_port = %hex_dump(&client_port_bytes),
|
||||
crypto_ts = %hex_dump(&ts_bytes),
|
||||
nonce_srv = %hex_dump(&srv_nonce),
|
||||
nonce_clt = %hex_dump(&my_nonce),
|
||||
prekey_sha256_client = %hex_dump(&sha256(&prekey_client)),
|
||||
prekey_sha256_server = %hex_dump(&sha256(&prekey_server)),
|
||||
hs_plain = %hex_dump(&hs_frame),
|
||||
proxy_secret_sha256 = %hex_dump(&sha256(&secret)),
|
||||
"ME diag: derived keys and handshake plaintext"
|
||||
);
|
||||
}
|
||||
if diag_level >= 2 {
|
||||
info!(
|
||||
prekey_client = %hex_dump(&prekey_client),
|
||||
prekey_server = %hex_dump(&prekey_server),
|
||||
"ME diag: full prekey buffers"
|
||||
);
|
||||
}
|
||||
|
||||
let (encrypted_hs, write_iv) = cbc_encrypt_padded(&wk, &wi, &hs_frame)?;
|
||||
if diag_level >= 1 {
|
||||
info!(
|
||||
hs_cipher = %hex_dump(&encrypted_hs),
|
||||
"ME diag: handshake ciphertext"
|
||||
);
|
||||
}
|
||||
wr.write_all(&encrypted_hs).await.map_err(ProxyError::Io)?;
|
||||
wr.flush().await.map_err(ProxyError::Io)?;
|
||||
|
||||
|
||||
@@ -1728,6 +1728,8 @@ mod tests {
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
@@ -336,6 +336,8 @@ pub(super) struct NatRuntimeCore {
|
||||
pub(super) nat_probe: bool,
|
||||
pub(super) nat_stun: Option<String>,
|
||||
pub(super) nat_stun_servers: Vec<String>,
|
||||
pub(super) stun_tcp_fallback: bool,
|
||||
pub(super) http_ip_detect_urls: Vec<String>,
|
||||
pub(super) nat_stun_live_servers: Arc<RwLock<Vec<String>>>,
|
||||
pub(super) nat_probe_concurrency: usize,
|
||||
pub(super) detected_ipv6: Option<Ipv6Addr>,
|
||||
@@ -484,6 +486,8 @@ impl MePool {
|
||||
nat_probe: bool,
|
||||
nat_stun: Option<String>,
|
||||
nat_stun_servers: Vec<String>,
|
||||
stun_tcp_fallback: bool,
|
||||
http_ip_detect_urls: Vec<String>,
|
||||
nat_probe_concurrency: usize,
|
||||
detected_ipv6: Option<Ipv6Addr>,
|
||||
me_one_retry: u8,
|
||||
@@ -706,6 +710,8 @@ impl MePool {
|
||||
nat_probe,
|
||||
nat_stun,
|
||||
nat_stun_servers,
|
||||
stun_tcp_fallback,
|
||||
http_ip_detect_urls,
|
||||
nat_stun_live_servers: Arc::new(RwLock::new(Vec::new())),
|
||||
nat_probe_concurrency: nat_probe_concurrency.max(1),
|
||||
detected_ipv6,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::task::JoinSet;
|
||||
use tokio::time::timeout;
|
||||
use tracing::{debug, info, warn};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::error::{ProxyError, Result};
|
||||
use crate::network::probe::is_bogon;
|
||||
use crate::network::stun::{IpFamily, stun_probe_dual, stun_probe_family_with_bind};
|
||||
use crate::network::probe::{detect_public_ipv4_http, is_bogon};
|
||||
use crate::network::stun::{
|
||||
IpFamily, stun_probe_dual_with_tcp_fallback, stun_probe_family_with_bind_and_tcp_fallback,
|
||||
};
|
||||
|
||||
use super::MePool;
|
||||
use std::time::Instant;
|
||||
|
||||
const STUN_BATCH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const STUN_BATCH_TCP_FALLBACK_TIMEOUT: Duration = Duration::from_secs(12);
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn stun_probe(stun_addr: Option<String>) -> Result<crate::network::stun::DualStunResult> {
|
||||
@@ -28,15 +31,14 @@ pub async fn stun_probe(stun_addr: Option<String>) -> Result<crate::network::stu
|
||||
"STUN server is not configured".to_string(),
|
||||
));
|
||||
}
|
||||
stun_probe_dual(&stun_addr).await
|
||||
stun_probe_dual_with_tcp_fallback(&stun_addr, false).await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn detect_public_ip() -> Option<IpAddr> {
|
||||
fetch_public_ipv4_with_retry()
|
||||
let urls = crate::config::defaults::default_http_ip_detect_urls();
|
||||
detect_public_ipv4_http(&urls)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(IpAddr::V4)
|
||||
}
|
||||
|
||||
@@ -65,15 +67,26 @@ impl MePool {
|
||||
let mut live_servers = Vec::new();
|
||||
let mut best_by_ip: HashMap<IpAddr, (usize, std::net::SocketAddr)> = HashMap::new();
|
||||
let concurrency = self.nat_runtime.nat_probe_concurrency.max(1);
|
||||
let tcp_fallback = self.nat_runtime.stun_tcp_fallback;
|
||||
|
||||
while next_idx < servers.len() || !join_set.is_empty() {
|
||||
while next_idx < servers.len() && join_set.len() < concurrency {
|
||||
let stun_addr = servers[next_idx].clone();
|
||||
next_idx += 1;
|
||||
join_set.spawn(async move {
|
||||
let batch_timeout = if tcp_fallback {
|
||||
STUN_BATCH_TCP_FALLBACK_TIMEOUT
|
||||
} else {
|
||||
STUN_BATCH_TIMEOUT
|
||||
};
|
||||
let res = timeout(
|
||||
STUN_BATCH_TIMEOUT,
|
||||
stun_probe_family_with_bind(&stun_addr, family, bind_ip),
|
||||
batch_timeout,
|
||||
stun_probe_family_with_bind_and_tcp_fallback(
|
||||
&stun_addr,
|
||||
family,
|
||||
bind_ip,
|
||||
tcp_fallback,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
(stun_addr, res)
|
||||
@@ -193,6 +206,10 @@ impl MePool {
|
||||
return self.nat_runtime.nat_ip_cfg;
|
||||
}
|
||||
|
||||
if !self.nat_runtime.nat_probe {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !(is_bogon(local_ip) || local_ip.is_loopback() || local_ip.is_unspecified()) {
|
||||
return None;
|
||||
}
|
||||
@@ -201,21 +218,15 @@ impl MePool {
|
||||
return Some(ip);
|
||||
}
|
||||
|
||||
match fetch_public_ipv4_with_retry().await {
|
||||
Ok(Some(ip)) => {
|
||||
{
|
||||
let mut guard = self.nat_runtime.nat_ip_detected.write().await;
|
||||
*guard = Some(IpAddr::V4(ip));
|
||||
}
|
||||
info!(public_ip = %ip, "Auto-detected public IP for NAT translation");
|
||||
Some(IpAddr::V4(ip))
|
||||
}
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Failed to auto-detect public IP");
|
||||
None
|
||||
}
|
||||
let Some(ip) = detect_public_ipv4_http(&self.nat_runtime.http_ip_detect_urls).await else {
|
||||
return None;
|
||||
};
|
||||
{
|
||||
let mut guard = self.nat_runtime.nat_ip_detected.write().await;
|
||||
*guard = Some(IpAddr::V4(ip));
|
||||
}
|
||||
info!(public_ip = %ip, "Auto-detected public IP for NAT translation");
|
||||
Some(IpAddr::V4(ip))
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_reflect_public_addr(
|
||||
@@ -365,31 +376,3 @@ impl MePool {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_public_ipv4_with_retry() -> Result<Option<Ipv4Addr>> {
|
||||
let providers = [
|
||||
"https://checkip.amazonaws.com",
|
||||
"http://v4.ident.me",
|
||||
"http://ipv4.icanhazip.com",
|
||||
];
|
||||
for url in providers {
|
||||
if let Ok(Some(ip)) = fetch_public_ipv4_once(url).await {
|
||||
return Ok(Some(ip));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn fetch_public_ipv4_once(url: &str) -> Result<Option<Ipv4Addr>> {
|
||||
let res = reqwest::get(url)
|
||||
.await
|
||||
.map_err(|e| ProxyError::Proxy(format!("public IP detection request failed: {e}")))?;
|
||||
|
||||
let text = res
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ProxyError::Proxy(format!("public IP detection read failed: {e}")))?;
|
||||
|
||||
let ip = text.trim().parse().ok();
|
||||
Ok(ip)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ async fn make_pool(
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
@@ -36,6 +36,8 @@ async fn make_pool(
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
@@ -31,6 +31,8 @@ async fn make_pool(me_pool_drain_threshold: u64) -> Arc<MePool> {
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
@@ -20,6 +20,8 @@ async fn make_pool() -> Arc<MePool> {
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
@@ -25,6 +25,8 @@ async fn make_pool() -> Arc<MePool> {
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
@@ -31,6 +31,8 @@ async fn make_pool() -> (Arc<MePool>, Arc<SecureRandom>) {
|
||||
false,
|
||||
None,
|
||||
Vec::new(),
|
||||
false,
|
||||
Vec::new(),
|
||||
1,
|
||||
None,
|
||||
12,
|
||||
|
||||
Reference in New Issue
Block a user