ItsGoin v0.3.2 — Decentralized social media network
No central server, user-owned data, reverse-chronological feed. Rust core + Tauri desktop + Android app + plain HTML/CSS/JS frontend. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
800388cda4
146 changed files with 53227 additions and 0 deletions
185
crates/core/src/stun.rs
Normal file
185
crates/core/src/stun.rs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
//! Minimal raw STUN client for NAT type detection.
|
||||
//! Sends STUN Binding Requests to two servers and compares mapped ports.
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::types::{NatMapping, NatType};
|
||||
|
||||
const STUN_SERVERS: &[&str] = &[
|
||||
"stun.l.google.com:19302",
|
||||
"stun.cloudflare.com:3478",
|
||||
];
|
||||
|
||||
const STUN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
/// STUN Binding Request (RFC 5389): 20 bytes
|
||||
/// Type: 0x0001 (Binding Request), Length: 0, Magic: 0x2112A442, Transaction ID: 12 random bytes
|
||||
fn build_binding_request() -> [u8; 20] {
|
||||
let mut buf = [0u8; 20];
|
||||
// Message type: Binding Request (0x0001)
|
||||
buf[0] = 0x00;
|
||||
buf[1] = 0x01;
|
||||
// Message length: 0
|
||||
buf[2] = 0x00;
|
||||
buf[3] = 0x00;
|
||||
// Magic cookie: 0x2112A442
|
||||
buf[4] = 0x21;
|
||||
buf[5] = 0x12;
|
||||
buf[6] = 0xA4;
|
||||
buf[7] = 0x42;
|
||||
// Transaction ID: 12 random bytes
|
||||
use rand::Rng;
|
||||
let mut rng = rand::rng();
|
||||
rng.fill(&mut buf[8..20]);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Parse XOR-MAPPED-ADDRESS from a STUN Binding Response.
|
||||
/// Returns the mapped SocketAddr or None if not found/parseable.
|
||||
fn parse_xor_mapped_address(resp: &[u8], txn_id: &[u8; 12]) -> Option<SocketAddr> {
|
||||
if resp.len() < 20 {
|
||||
return None;
|
||||
}
|
||||
// Verify it's a Binding Response (0x0101)
|
||||
if resp[0] != 0x01 || resp[1] != 0x01 {
|
||||
return None;
|
||||
}
|
||||
let magic: [u8; 4] = [0x21, 0x12, 0xA4, 0x42];
|
||||
|
||||
// Walk attributes
|
||||
let msg_len = u16::from_be_bytes([resp[2], resp[3]]) as usize;
|
||||
let end = std::cmp::min(20 + msg_len, resp.len());
|
||||
let mut pos = 20;
|
||||
while pos + 4 <= end {
|
||||
let attr_type = u16::from_be_bytes([resp[pos], resp[pos + 1]]);
|
||||
let attr_len = u16::from_be_bytes([resp[pos + 2], resp[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if pos + attr_len > end {
|
||||
break;
|
||||
}
|
||||
// XOR-MAPPED-ADDRESS = 0x0020, MAPPED-ADDRESS = 0x0001
|
||||
if attr_type == 0x0020 && attr_len >= 8 {
|
||||
// byte 0: reserved, byte 1: family (0x01=IPv4, 0x02=IPv6)
|
||||
let family = resp[pos + 1];
|
||||
if family == 0x01 {
|
||||
// IPv4
|
||||
let xport = u16::from_be_bytes([resp[pos + 2], resp[pos + 3]])
|
||||
^ u16::from_be_bytes([magic[0], magic[1]]);
|
||||
let xip = [
|
||||
resp[pos + 4] ^ magic[0],
|
||||
resp[pos + 5] ^ magic[1],
|
||||
resp[pos + 6] ^ magic[2],
|
||||
resp[pos + 7] ^ magic[3],
|
||||
];
|
||||
let addr = SocketAddr::new(
|
||||
std::net::IpAddr::V4(std::net::Ipv4Addr::new(xip[0], xip[1], xip[2], xip[3])),
|
||||
xport,
|
||||
);
|
||||
return Some(addr);
|
||||
} else if family == 0x02 && attr_len >= 20 {
|
||||
// IPv6: XOR with magic + txn_id
|
||||
let xport = u16::from_be_bytes([resp[pos + 2], resp[pos + 3]])
|
||||
^ u16::from_be_bytes([magic[0], magic[1]]);
|
||||
let mut ip6 = [0u8; 16];
|
||||
let xor_key: Vec<u8> = magic.iter().chain(txn_id.iter()).copied().collect();
|
||||
for i in 0..16 {
|
||||
ip6[i] = resp[pos + 4 + i] ^ xor_key[i];
|
||||
}
|
||||
let addr = SocketAddr::new(
|
||||
std::net::IpAddr::V6(std::net::Ipv6Addr::from(ip6)),
|
||||
xport,
|
||||
);
|
||||
return Some(addr);
|
||||
}
|
||||
}
|
||||
// Pad to 4-byte boundary
|
||||
pos += (attr_len + 3) & !3;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Query a single STUN server and return the mapped address.
|
||||
async fn stun_query(sock: &UdpSocket, server: &str) -> Option<SocketAddr> {
|
||||
use std::net::ToSocketAddrs;
|
||||
let server_addr = match server.to_socket_addrs() {
|
||||
Ok(mut addrs) => addrs.next()?,
|
||||
Err(e) => {
|
||||
debug!(server, error = %e, "STUN DNS resolution failed");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let request = build_binding_request();
|
||||
let txn_id: [u8; 12] = request[8..20].try_into().unwrap();
|
||||
|
||||
if let Err(e) = sock.send_to(&request, server_addr).await {
|
||||
debug!(server, error = %e, "STUN send failed");
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 256];
|
||||
match tokio::time::timeout(STUN_TIMEOUT, sock.recv_from(&mut buf)).await {
|
||||
Ok(Ok((len, _))) => parse_xor_mapped_address(&buf[..len], &txn_id),
|
||||
Ok(Err(e)) => {
|
||||
debug!(server, error = %e, "STUN recv failed");
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
debug!(server, "STUN query timed out (3s)");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect NAT type by comparing mapped addresses from two STUN servers.
|
||||
/// Must be called with the local port we're interested in (for Public detection).
|
||||
/// Also returns the NatMapping classification for the advanced NAT profile.
|
||||
pub async fn detect_nat_type(local_port: u16) -> (NatType, NatMapping) {
|
||||
let sock = match UdpSocket::bind("0.0.0.0:0").await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Failed to bind UDP socket for STUN");
|
||||
return (NatType::Unknown, NatMapping::Unknown);
|
||||
}
|
||||
};
|
||||
|
||||
let local_addr = sock.local_addr().ok();
|
||||
|
||||
// Query both servers from the same socket
|
||||
let result1 = stun_query(&sock, STUN_SERVERS[0]).await;
|
||||
let result2 = stun_query(&sock, STUN_SERVERS[1]).await;
|
||||
|
||||
match (result1, result2) {
|
||||
(Some(addr1), Some(addr2)) => {
|
||||
debug!(
|
||||
server1 = STUN_SERVERS[0], mapped1 = %addr1,
|
||||
server2 = STUN_SERVERS[1], mapped2 = %addr2,
|
||||
local_port,
|
||||
"STUN results"
|
||||
);
|
||||
// If mapped port matches our local port, we might be public/no-NAT
|
||||
if let Some(local) = local_addr {
|
||||
if addr1.port() == local.port() && addr2.port() == local.port() {
|
||||
return (NatType::Public, NatMapping::EndpointIndependent);
|
||||
}
|
||||
}
|
||||
// Same mapped port from both = cone NAT (Easy / EIM)
|
||||
// Different ports = symmetric NAT (Hard / EDM)
|
||||
if addr1.port() == addr2.port() {
|
||||
(NatType::Easy, NatMapping::EndpointIndependent)
|
||||
} else {
|
||||
(NatType::Hard, NatMapping::EndpointDependent)
|
||||
}
|
||||
}
|
||||
(Some(addr), None) | (None, Some(addr)) => {
|
||||
debug!(mapped = %addr, "Only one STUN server responded, assuming Easy");
|
||||
(NatType::Easy, NatMapping::EndpointIndependent)
|
||||
}
|
||||
(None, None) => {
|
||||
warn!("Both STUN servers unreachable, NAT type unknown");
|
||||
(NatType::Unknown, NatMapping::Unknown)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue