diff --git a/Cargo.lock b/Cargo.lock index 2e99516..8dfaf84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.18" @@ -80,6 +89,16 @@ dependencies = [ "clap_derive", ] +[[package]] +name = "clap-verbosity-flag" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +dependencies = [ + "clap", + "log", +] + [[package]] name = "clap_builder" version = "4.5.29" @@ -125,6 +144,35 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "getrandom" version = "0.3.1" @@ -137,18 +185,46 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + [[package]] name = "libc" version = "0.2.169" @@ -240,6 +316,86 @@ dependencies = [ "zerocopy 0.8.17", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "strsim" version = "0.11.1" @@ -262,10 +418,15 @@ name = "tailstun" version = "0.1.0" dependencies = [ "clap", + "clap-verbosity-flag", "crc32fast", + "env_logger", "log", "nom", "rand", + "serde", + "serde_json", + "serde_yaml", ] [[package]] @@ -274,6 +435,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf8parse" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 9fe70f6..329524b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,12 @@ edition = "2021" [dependencies] clap = { version = "4.5.29", features = ["derive"] } +clap-verbosity-flag = "3.0.2" crc32fast = "1.4.2" +env_logger = "0.11.6" log = "0.4.25" nom = "8.0.0" rand = "0.9.0" +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.138" +serde_yaml = "0.9.34" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..1936f4f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,500 @@ +use crc32fast::hash; +use log::{debug, info, warn}; +use nom::bytes::complete::{tag, take}; +use nom::error::ParseError; +use nom::multi::{many, many0}; +use nom::number::complete::{be_u128, be_u16, be_u32, be_u8}; +use nom::{AsBytes, IResult, Parser}; +use rand::seq::{IndexedRandom, SliceRandom}; +use rand::{self, RngCore}; +use serde::Serialize; +use std::fmt::{self, Debug}; +use std::net::IpAddr; + +// https://github.com/tailscale/tailscale/blob/main/net/stun/stun.go + +const ATTR_NUM_SOFTWARE: u16 = 0x8022; +const ATTR_NUM_FINGERPRINT: u16 = 0x8028; +const ATTR_MAPPED_ADDRESS: u16 = 0x0001; +const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020; +// This alternative attribute type is not +// mentioned in the RFC, but the shift into +// the "comprehension-optional" range seems +// like an easy mistake for a server to make. +// And servers appear to send it. +const ATTR_XOR_MAPPED_ADDRESS_ALT: u16 = 0x8020; +const ATTR_SOURCE_ADDRESS: u16 = 0x0004; +const ATTR_CHANGED_ADDRESS: u16 = 0x0005; +const ATTR_USERNAME: u16 = 0x0006; +const ATTR_MESSAGE_INTEGRITY: u16 = 0x0008; +const ATTR_ERROR_CODE: u16 = 0x0009; +const ATTR_UNKNOWN_ATTRIBUTES: u16 = 0x000a; +const ATTR_REALM: u16 = 0x0014; +const ATTR_NONCE: u16 = 0x0015; +const ATTR_ALTERNATE_SERVER: u16 = 0x8023; + +const SOFTWARE: [u8; 8] = *b"tailnode"; +const BINDING_REQUEST: [u8; 2] = [0x00, 0x01]; +const MAGIC_COOKIE: [u8; 4] = [0x21, 0x12, 0xa4, 0x42]; +const LEN_FINGERPRINT: u16 = 8; +const HEADER_LEN: u16 = 20; + +#[derive(Debug, Clone)] +pub struct TxId([u8; 12]); + +impl TxId { + pub fn new() -> Self { + let mut tx_id = [0; 12]; + rand::rng().fill_bytes(&mut tx_id); + Self(tx_id) + } + + pub fn from_bytes(bytes: &[u8]) -> Self { + let mut tx_id = [0; 12]; + tx_id.copy_from_slice(bytes); + Self(tx_id) + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn make_request(&self) -> Vec { + const LEN_ATTR_SOFTWARE: u16 = 4 + SOFTWARE.len() as u16; + let mut buf = + Vec::with_capacity((HEADER_LEN + LEN_ATTR_SOFTWARE + LEN_FINGERPRINT) as usize); + buf.extend(&BINDING_REQUEST); + buf.extend((LEN_ATTR_SOFTWARE + LEN_FINGERPRINT).to_be_bytes()); + buf.extend(&MAGIC_COOKIE); + buf.extend(self.as_bytes()); + buf.extend(ATTR_NUM_SOFTWARE.to_be_bytes()); + buf.extend((SOFTWARE.len() as u16).to_be_bytes()); + buf.extend(&SOFTWARE); + + let fp = fingerprint(&buf); + buf.extend(ATTR_NUM_FINGERPRINT.to_be_bytes()); + buf.extend(4_u16.to_be_bytes()); + buf.extend(&fp.to_be_bytes()); + + buf + } +} + +impl Serialize for TxId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u128(u128::from(self)) + } +} + +impl From<&TxId> for u128 { + fn from(value: &TxId) -> Self { + let mut padded = [0u8; 16]; + padded[4..].copy_from_slice(&value.0); + u128::from_be_bytes(padded) + } +} + +fn fingerprint(msg: &[u8]) -> u32 { + hash(msg) ^ 0x5354554e +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum StunClass { + Request = 0, + Indication = 1, + SuccessResponse = 2, + ErrorResponse = 3, +} + +#[derive(Debug, Clone, Copy, Serialize)] +pub enum StunMethod { + Binding = 1, +} + +#[derive(Debug, Clone, Serialize)] +pub enum StunAttribute { + MappedAddress((IpAddr, u16)), + XorMappedAddress((IpAddr, u16)), + SourceAddress((IpAddr, u16)), + ChangedAddress((IpAddr, u16)), + Username(String), + MessageIntegrity([u8; 20]), + Fingerprint(u32), + ErrorCode((u16, String)), + Realm(String), + Nonce(String), + UnknownAttributes(Vec), + Software(String), + AlternateServer((IpAddr, u16)), + Unknown((u16, Vec)), +} + +fn addr_family(addr: &IpAddr) -> &'static str { + match addr { + IpAddr::V4(_) => "IPv4", + IpAddr::V6(_) => "IPv6", + } +} + +impl fmt::Display for StunAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StunAttribute::MappedAddress((addr, port)) => { + write!( + f, + " MappedAddress ({}) {}:{}", + addr_family(addr), + addr, + port + ) + } + StunAttribute::SourceAddress((addr, port)) => { + write!( + f, + " SourceAddress ({}) {}:{}", + addr_family(addr), + addr, + port + ) + } + StunAttribute::ChangedAddress((addr, port)) => { + write!( + f, + " ChangedAddress ({}) {}:{}", + addr_family(addr), + addr, + port + ) + } + StunAttribute::XorMappedAddress((addr, port)) => { + write!( + f, + " XorMappedAddress ({}) {}:{}", + addr_family(addr), + addr, + port + ) + } + StunAttribute::Username(username) => writeln!(f, " Username {}", username), + StunAttribute::MessageIntegrity(msg_integrity) => { + write!(f, " MessageIntegrity {:?}", msg_integrity) + } + StunAttribute::Fingerprint(fingerprint) => { + write!(f, " Fingerprint 0x{:08x}", fingerprint) + } + StunAttribute::ErrorCode((err_num, error)) => { + write!(f, " ErrorCode {} ({})", err_num, error) + } + StunAttribute::Realm(realm) => writeln!(f, " Realm {}", realm), + StunAttribute::Nonce(nonce) => writeln!(f, " Nonce {}", nonce), + StunAttribute::UnknownAttributes(unknown_attrs) => { + write!(f, " UnknownAttributes {:?}", unknown_attrs) + } + StunAttribute::Software(software) => writeln!(f, " Software {}", software), + StunAttribute::AlternateServer((addr, port)) => { + write!( + f, + " AlternateServer ({}) {}:{}", + addr_family(addr), + addr, + port + ) + } + StunAttribute::Unknown((attr_type, data)) => { + write!(f, " Unknown ({}) {:?}", attr_type, data) + } + } + } +} + +#[derive(Debug, Serialize)] +pub struct StunMessageType { + pub class: StunClass, + pub method: StunMethod, +} + +#[derive(Debug, Serialize)] +pub struct StunHeader { + pub msg_type: StunMessageType, + pub msg_length: u16, + pub tx_id: TxId, +} + +impl fmt::Display for StunHeader { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, " Header")?; + writeln!( + f, + " MessageType class={:?} (0x{:02x}) method={:?} (0x{:04x})", + self.msg_type.class, + self.msg_type.class as usize, + self.msg_type.method, + self.msg_type.method as usize + ) + } +} + +#[derive(Debug, Serialize)] +pub struct StunAttributes(Vec); + + +impl fmt::Display for StunAttributes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, " Attributes")?; + for attr in &self.0 { + writeln!(f, " {}", attr)?; + } + Ok(()) + } +} + +impl StunAttributes { + pub fn mapped_address(&self) -> Option<&(IpAddr, u16)> { + self.0.iter().find_map(|attr| match attr { + StunAttribute::MappedAddress(addr) | StunAttribute::XorMappedAddress(addr) => Some(addr), + _ => None, + }) + } +} + +#[derive(Debug, Serialize)] +pub struct StunMessage { + pub header: StunHeader, + pub attributes: StunAttributes, +} + +impl fmt::Display for StunMessage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "StunMessage")?; + write!(f, "{}", self.header)?; + write!(f, "{}", self.attributes) + } +} + + +impl StunMessage { + pub fn parse(bytes: &[u8]) -> Result>> { + let (_, msg) = parse_stun_message(bytes)?; + Ok(msg) + } +} + +fn take_txid>(bytes: I) -> IResult +where + I: nom::Input + AsBytes, +{ + let (bytes, tx_id) = take(12usize)(bytes)?; + Ok((bytes, TxId::from_bytes(tx_id.as_bytes()))) +} + +fn parse_stun_message<'a, I, E: ParseError>(input: I) -> IResult +where + I: nom::Input + nom::Compare + nom::Compare<&'a [u8]> + AsBytes + Debug, +{ + let (bytes, h) = parse_stun_header(input)?; + let (residual, bytes) = take(h.msg_length)(bytes)?; + if residual.input_len() != 0 { + warn!("Trailing bytes in STUN message: {:?}", residual); + } + + let (bytes, attributes) = many0(parse_stun_attribute(&h.tx_id)).parse(bytes)?; + if !bytes.input_len() != 0 { + warn!("Trailing bytes in STUN message attributes: {:?}", bytes); + } + let attributes = StunAttributes(attributes.iter().filter_map(|i| i.clone()).collect()); + + Ok(( + residual, + StunMessage { + header: h, + attributes, + }, + )) +} + +fn parse_stun_message_type<'a, I: nom::Input, E: ParseError>( + input: I, +) -> IResult { + let (bytes, msg_type_raw) = be_u16(input)?; + if msg_type_raw & 0b11000000 != 0 { + panic!("Invalid STUN message type"); + } + let class_raw = ((msg_type_raw & (1 << 4)) >> 4) | ((msg_type_raw & (1 << 8)) >> 7); + let class = match class_raw { + 0 => StunClass::Request, + 1 => StunClass::Indication, + 2 => StunClass::SuccessResponse, + 3 => StunClass::ErrorResponse, + _ => panic!("Invalid STUN message class"), + }; + let method_raw = msg_type_raw & 0x0f | msg_type_raw & 0xe0 >> 1 | msg_type_raw & 0x3e >> 2; + let method = match method_raw { + 1 => StunMethod::Binding, + _ => panic!("Invalid STUN message method"), + }; + Ok((bytes, StunMessageType { class, method })) +} + +fn parse_stun_address, E: ParseError>( + bytes: I, +) -> IResult { + let (bytes, _) = take(1usize)(bytes)?; + let (bytes, family) = be_u8(bytes)?; + let (bytes, port) = be_u16(bytes)?; + let (bytes, addr) = match family { + 0x01 => { + let (bytes, val) = be_u32(bytes)?; + (bytes, (IpAddr::V4(val.into()), port)) + } + 0x02 => { + let (bytes, val) = be_u128(bytes)?; + (bytes, (IpAddr::V6(val.into()), port)) + } + _ => panic!("Invalid address family"), + }; + Ok((bytes, addr)) +} + +fn parse_stun_xor_address>( + bytes: I, + tx_id: &TxId, +) -> IResult +where + I: nom::Input, +{ + let (bytes, addr) = parse_stun_address(bytes)?; + let xor_port = addr.1 ^ 0x2112; + let xor_addr = match addr.0 { + IpAddr::V4(v4) => { + let v4 = u32::from(v4); + let xor_v4 = v4 ^ 0x2112a442; + IpAddr::V4(xor_v4.into()) + } + IpAddr::V6(v6) => { + let v6 = u128::from(v6); + let xor_v6: u128 = v6 ^ (0x2112a442 << 96 | u128::from(tx_id)); + IpAddr::V6(xor_v6.into()) + } + }; + Ok((bytes, (xor_addr, xor_port))) +} + +fn parse_stun_attribute>( + tx_id: &TxId, +) -> impl Fn(I) -> IResult, E> +where + I: nom::Input + nom::Compare + AsBytes, +{ + let tx_id = tx_id.clone(); + move |bytes| parse_stun_attribute_impl(bytes, &tx_id) +} + +fn parse_stun_attribute_impl>( + bytes: I, + tx_id: &TxId, +) -> IResult, E> +where + I: nom::Input + nom::Compare + AsBytes, +{ + let (bytes, attr_type) = be_u16(bytes)?; + let (bytes, attr_len) = be_u16(bytes)?; + let (bytes, attr_data) = take(attr_len)(bytes)?; + + if attr_len == 0 { + return Ok((bytes, None)); + } + + let attr = match attr_type { + ATTR_MAPPED_ADDRESS => { + let (_residual, addr) = parse_stun_address(attr_data)?; + StunAttribute::MappedAddress(addr) + } + ATTR_SOURCE_ADDRESS => { + let (_residual, addr) = parse_stun_address(attr_data)?; + StunAttribute::SourceAddress(addr) + } + ATTR_CHANGED_ADDRESS => { + let (_residual, addr) = parse_stun_address(attr_data)?; + StunAttribute::ChangedAddress(addr) + } + ATTR_XOR_MAPPED_ADDRESS | ATTR_XOR_MAPPED_ADDRESS_ALT => { + let (_residual, addr) = parse_stun_xor_address(attr_data, tx_id)?; + + StunAttribute::XorMappedAddress(addr) + } + ATTR_USERNAME => { + let username = String::from_iter(attr_data.iter_elements().map(|b| b as char)); + StunAttribute::Username(username) + } + ATTR_MESSAGE_INTEGRITY => { + let mut msg_integrity = [0u8; 20]; + msg_integrity.copy_from_slice(attr_data.as_bytes()); + StunAttribute::MessageIntegrity(msg_integrity) + } + ATTR_ERROR_CODE => { + let (attr_data, zeros) = take(2usize)(attr_data)?; + if zeros.iter_elements().any(|b| b != 0) { + panic!("Invalid STUN error code"); + } + let (attr_data, err_num) = be_u16(attr_data)?; + if err_num & 0b1111100000000000 != 0 { + panic!("Invalid STUN error code"); + } + let error = String::from_iter(attr_data.iter_elements().map(|b| b as char)); + StunAttribute::ErrorCode((err_num, error)) + } + ATTR_UNKNOWN_ATTRIBUTES => { + let (_residual, unknown_attrs) = + many((attr_len / 2) as usize, be_u16).parse(attr_data)?; + StunAttribute::UnknownAttributes(unknown_attrs) + } + ATTR_NUM_FINGERPRINT => { + let (_residual, fingerprint) = be_u32(attr_data)?; + StunAttribute::Fingerprint(fingerprint) + } + ATTR_REALM => { + let realm = String::from_iter(attr_data.iter_elements().map(|b| b as char)); + StunAttribute::Realm(realm) + } + ATTR_NONCE => { + let nonce = String::from_iter(attr_data.iter_elements().map(|b| b as char)); + StunAttribute::Nonce(nonce) + } + ATTR_NUM_SOFTWARE => { + let software = String::from_iter(attr_data.iter_elements().map(|b| b as char)); + StunAttribute::Software(software) + } + ATTR_ALTERNATE_SERVER => { + let (_residual, addr) = parse_stun_address(attr_data)?; + StunAttribute::AlternateServer(addr) + } + + t => { + warn!("Unknown STUN attribute type {t}"); + StunAttribute::Unknown((t, attr_data.iter_elements().collect())) + } + }; + + Ok((bytes, Some(attr))) +} + +fn parse_stun_header<'a, I, E: ParseError>(input: I) -> IResult +where + I: nom::Input + nom::Compare + nom::Compare<&'a [u8]> + AsBytes, +{ + let (bytes, msg_type) = parse_stun_message_type(input)?; + let (bytes, msg_length) = be_u16(bytes)?; + let (bytes, _) = tag(MAGIC_COOKIE.as_bytes())(bytes)?; + let (bytes, tx_id) = take_txid(bytes)?; + Ok(( + bytes, + StunHeader { + msg_type, + msg_length, + tx_id, + }, + )) +} diff --git a/src/main.rs b/src/main.rs index 642c80a..bc10bcf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,94 +1,31 @@ -use std::fmt; +use clap::ValueEnum; +use log::{debug, info, warn}; +use serde::Serialize; use std::net::{IpAddr, UdpSocket}; +use tailstun::{StunMessage, TxId}; -use crc32fast::hash; -use log::warn; -use nom::bytes::complete::{tag, take}; -use nom::error::ParseError; -use nom::multi::{many, many0}; -use nom::number::complete::{be_u128, be_u16, be_u32, be_u8}; -use nom::{AsBytes, IResult, Parser}; -use rand::RngCore; - -// https://github.com/tailscale/tailscale/blob/main/net/stun/stun.go - -const ATTR_NUM_SOFTWARE: u16 = 0x8022; -const ATTR_NUM_FINGERPRINT: u16 = 0x8028; -const ATTR_MAPPED_ADDRESS: u16 = 0x0001; -const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020; -// This alternative attribute type is not -// mentioned in the RFC, but the shift into -// the "comprehension-optional" range seems -// like an easy mistake for a server to make. -// And servers appear to send it. -const ATTR_XOR_MAPPED_ADDRESS_ALT: u16 = 0x8020; -const ATTR_SOURCE_ADDRESS: u16 = 0x0004; -const ATTR_CHANGED_ADDRESS: u16 = 0x0005; -const ATTR_USERNAME: u16 = 0x0006; -const ATTR_MESSAGE_INTEGRITY: u16 = 0x0008; -const ATTR_ERROR_CODE: u16 = 0x0009; -const ATTR_UNKNOWN_ATTRIBUTES: u16 = 0x000a; -const ATTR_REALM: u16 = 0x0014; -const ATTR_NONCE: u16 = 0x0015; -const ATTR_ALTERNATE_SERVER: u16 = 0x8023; - -const SOFTWARE: [u8; 8] = *b"tailnode"; -const BINDING_REQUEST: [u8; 2] = [0x00, 0x01]; -const MAGIC_COOKIE: [u8; 4] = [0x21, 0x12, 0xa4, 0x42]; -const LEN_FINGERPRINT: u16 = 8; -const HEADER_LEN: u16 = 20; - -#[derive(Debug, Clone)] -struct TxId([u8; 12]); - -impl TxId { - fn new() -> Self { - let mut tx_id = [0; 12]; - rand::rng().fill_bytes(&mut tx_id); - Self(tx_id) - } - - fn from_bytes(bytes: &[u8]) -> Self { - let mut tx_id = [0; 12]; - tx_id.copy_from_slice(bytes); - Self(tx_id) - } - - fn as_bytes(&self) -> &[u8] { - &self.0 - } - - fn make_request(&self) -> Vec { - const LEN_ATTR_SOFTWARE: u16 = 4 + SOFTWARE.len() as u16; - let mut buf = - Vec::with_capacity((HEADER_LEN + LEN_ATTR_SOFTWARE + LEN_FINGERPRINT) as usize); - buf.extend(&BINDING_REQUEST); - buf.extend((LEN_ATTR_SOFTWARE + LEN_FINGERPRINT).to_be_bytes()); - buf.extend(&MAGIC_COOKIE); - buf.extend(self.as_bytes()); - buf.extend(ATTR_NUM_SOFTWARE.to_be_bytes()); - buf.extend((SOFTWARE.len() as u16).to_be_bytes()); - buf.extend(&SOFTWARE); - - let fp = fingerprint(&buf); - buf.extend(ATTR_NUM_FINGERPRINT.to_be_bytes()); - buf.extend(4_u16.to_be_bytes()); - buf.extend(&fp.to_be_bytes()); - - buf - } +#[derive(Debug, Clone, ValueEnum)] +enum OutputFormat { + Text, + Json, + Yaml, } -impl From<&TxId> for u128 { - fn from(value: &TxId) -> Self { - let mut padded = [0u8; 16]; - padded[4..].copy_from_slice(&value.0); - u128::from_be_bytes(padded) +impl OutputFormat { + fn format_stun(&self, msg: &StunMessage) -> String { + match self { + OutputFormat::Text => format!("{}", msg), + OutputFormat::Json => serde_json::to_string_pretty(msg).unwrap(), + OutputFormat::Yaml => serde_yaml::to_string(msg).unwrap(), + } + } + fn format_address(&self, (addr, port): &(IpAddr, u16)) -> String { + match self { + OutputFormat::Text => format!("{}", addr), + OutputFormat::Json => serde_json::to_string_pretty(addr).unwrap(), + OutputFormat::Yaml => serde_yaml::to_string(addr).unwrap(), + } } -} - -fn fingerprint(msg: &[u8]) -> u32 { - hash(msg) ^ 0x5354554e } #[derive(clap::Parser)] @@ -96,405 +33,61 @@ fn fingerprint(msg: &[u8]) -> u32 { #[command(about = "Test a Tailscale derp node's stun service")] struct Cli { host: String, - port: Option, - #[clap(long, short, default_value_t = false)] - debug: bool, -} - -#[derive(Debug, Clone, Copy)] -enum StunClass { - Request = 0, - Indication = 1, - SuccessResponse = 2, - ErrorResponse = 3, -} - -#[derive(Debug, Clone, Copy)] -enum StunMethod { - Binding = 1, -} - -#[derive(Debug, Clone)] -enum StunAttribute { - MappedAddress((IpAddr, u16)), - XorMappedAddress((IpAddr, u16)), - SourceAddress((IpAddr, u16)), - ChangedAddress((IpAddr, u16)), - Username(String), - MessageIntegrity([u8; 20]), - Fingerprint(u32), - ErrorCode((u16, String)), - Realm(String), - Nonce(String), - UnknownAttributes(Vec), - Software(String), - AlternateServer((IpAddr, u16)), - Unknown((u16, Vec)), -} - -fn addr_family(addr: &IpAddr) -> &'static str { - match addr { - IpAddr::V4(_) => "IPv4", - IpAddr::V6(_) => "IPv6", - } -} - -impl fmt::Display for StunAttribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - StunAttribute::MappedAddress((addr, port)) => { - write!( - f, - " MappedAddress ({}) {}:{}", - addr_family(addr), - addr, - port - ) - } - StunAttribute::SourceAddress((addr, port)) => { - write!( - f, - " SourceAddress ({}) {}:{}", - addr_family(addr), - addr, - port - ) - } - StunAttribute::ChangedAddress((addr, port)) => { - write!( - f, - " ChangedAddress ({}) {}:{}", - addr_family(addr), - addr, - port - ) - } - StunAttribute::XorMappedAddress((addr, port)) => { - write!( - f, - " XorMappedAddress ({}) {}:{}", - addr_family(addr), - addr, - port - ) - } - StunAttribute::Username(username) => writeln!(f, " Username {}", username), - StunAttribute::MessageIntegrity(msg_integrity) => { - write!(f, " MessageIntegrity {:?}", msg_integrity) - } - StunAttribute::Fingerprint(fingerprint) => { - write!(f, " Fingerprint 0x{:08x}", fingerprint) - } - StunAttribute::ErrorCode((err_num, error)) => { - write!(f, " ErrorCode {} ({})", err_num, error) - } - StunAttribute::Realm(realm) => writeln!(f, " Realm {}", realm), - StunAttribute::Nonce(nonce) => writeln!(f, " Nonce {}", nonce), - StunAttribute::UnknownAttributes(unknown_attrs) => { - write!(f, " UnknownAttributes {:?}", unknown_attrs) - } - StunAttribute::Software(software) => writeln!(f, " Software {}", software), - StunAttribute::AlternateServer((addr, port)) => { - write!( - f, - " AlternateServer ({}) {}:{}", - addr_family(addr), - addr, - port - ) - } - StunAttribute::Unknown((attr_type, data)) => { - write!(f, " Unknown ({}) {:?}", attr_type, data) - } - } - } -} - -#[derive(Debug)] -struct StunMessageType { - class: StunClass, - method: StunMethod, -} - -#[derive(Debug)] -struct StunHeader { - msg_type: StunMessageType, - msg_length: u16, - tx_id: TxId, -} - -impl fmt::Display for StunHeader { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, " Header")?; - writeln!( - f, - " MessageType class={:?} (0x{:02x}) method={:?} (0x{:04x})", - self.msg_type.class, - self.msg_type.class as usize, - self.msg_type.method, - self.msg_type.method as usize - ) - } -} - -#[derive(Debug)] -struct StunAttributes(Vec); - -#[derive(Debug)] -struct StunMessage { - h: StunHeader, - attributes: StunAttributes, -} - -impl fmt::Display for StunMessage { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "StunMessage")?; - write!(f, "{}", self.h)?; - write!(f, "{}", self.attributes) - } -} - -impl fmt::Display for StunAttributes { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, " Attributes")?; - for attr in &self.0 { - writeln!(f, " {}", attr)?; - } - Ok(()) - } -} - -fn take_txid<'a, E: ParseError<&'a [u8]>>(bytes: &'a [u8]) -> IResult<&[u8], TxId, E> { - let (bytes, tx_id) = take(12usize)(bytes)?; - Ok((bytes, TxId::from_bytes(tx_id))) -} - -fn parse_stun_message<'a, E: ParseError<&'a [u8]>>( - bytes: &'a [u8], -) -> IResult<&'a [u8], StunMessage, E> { - let (bytes, h) = parse_stun_header(bytes)?; - - let (bytes, attributes) = many0(parse_stun_attribute::<&[u8], E>(&h.tx_id)).parse(bytes)?; - let attributes = StunAttributes(attributes.iter().filter_map(|i| i.clone()).collect()); - - Ok((bytes, StunMessage { h, attributes })) -} - -fn parse_stun_message_type<'a, E: ParseError<&'a [u8]>>( - bytes: &'a [u8], -) -> IResult<&'a [u8], StunMessageType, E> { - let (bytes, msg_type_raw) = be_u16(bytes)?; - if msg_type_raw & 0b11000000 != 0 { - panic!("Invalid STUN message type"); - } - let class_raw = ((msg_type_raw & (1 << 4)) >> 4) | ((msg_type_raw & (1 << 8)) >> 7); - let class = match class_raw { - 0 => StunClass::Request, - 1 => StunClass::Indication, - 2 => StunClass::SuccessResponse, - 3 => StunClass::ErrorResponse, - _ => panic!("Invalid STUN message class"), - }; - let method_raw = msg_type_raw & 0x0f | msg_type_raw & 0xe0 >> 1 | msg_type_raw & 0x3e >> 2; - let method = match method_raw { - 1 => StunMethod::Binding, - _ => panic!("Invalid STUN message method"), - }; - Ok((bytes, StunMessageType { class, method })) -} - -fn parse_stun_address>(bytes: I) -> IResult -where - I: nom::Input, -{ - let (bytes, _) = take(1usize)(bytes)?; - let (bytes, family) = be_u8(bytes)?; - let (bytes, port) = be_u16(bytes)?; - let (bytes, addr) = match family { - 0x01 => { - let (bytes, val) = be_u32(bytes)?; - (bytes, (IpAddr::V4(val.into()), port)) - } - 0x02 => { - let (bytes, val) = be_u128(bytes)?; - (bytes, (IpAddr::V6(val.into()), port)) - } - _ => panic!("Invalid address family"), - }; - Ok((bytes, addr)) -} - -fn parse_stun_xor_address>( - bytes: I, - tx_id: &TxId, -) -> IResult -where - I: nom::Input, -{ - let (bytes, addr) = parse_stun_address(bytes)?; - let xor_port = addr.1 ^ 0x2112; - let xor_addr = match addr.0 { - IpAddr::V4(v4) => { - let v4 = u32::from(v4); - let xor_v4 = v4 ^ 0x2112a442; - IpAddr::V4(xor_v4.into()) - } - IpAddr::V6(v6) => { - let v6 = u128::from(v6); - let xor_v6: u128 = v6 ^ (0x2112a442 << 96 | u128::from(tx_id)); - IpAddr::V6(xor_v6.into()) - } - }; - Ok((bytes, (xor_addr, xor_port))) -} - -fn parse_stun_attribute>( - tx_id: &TxId, -) -> impl Fn(I) -> IResult, E> -where - I: nom::Input + nom::Compare + AsBytes, -{ - let tx_id = tx_id.clone(); - move |bytes| parse_stun_attribute_impl(bytes, &tx_id) -} - -fn parse_stun_attribute_impl>( - bytes: I, - tx_id: &TxId, -) -> IResult, E> -where - I: nom::Input + nom::Compare + AsBytes, -{ - let (bytes, attr_type) = be_u16(bytes)?; - let (bytes, attr_len) = be_u16(bytes)?; - let (bytes, attr_data) = take(attr_len)(bytes)?; - - if attr_len == 0 { - return Ok((bytes, None)); - } - - let attr = match attr_type { - ATTR_MAPPED_ADDRESS => { - let (_residual, addr) = parse_stun_address(attr_data)?; - StunAttribute::MappedAddress(addr) - } - ATTR_SOURCE_ADDRESS => { - let (_residual, addr) = parse_stun_address(attr_data)?; - StunAttribute::SourceAddress(addr) - } - ATTR_CHANGED_ADDRESS => { - let (_residual, addr) = parse_stun_address(attr_data)?; - StunAttribute::ChangedAddress(addr) - } - ATTR_XOR_MAPPED_ADDRESS | ATTR_XOR_MAPPED_ADDRESS_ALT => { - let (_residual, addr) = parse_stun_xor_address(attr_data, tx_id)?; - - StunAttribute::XorMappedAddress(addr) - } - ATTR_USERNAME => { - let username = String::from_iter(attr_data.iter_elements().map(|b| b as char)); - StunAttribute::Username(username) - } - ATTR_MESSAGE_INTEGRITY => { - let mut msg_integrity = [0u8; 20]; - msg_integrity.copy_from_slice(attr_data.as_bytes()); - StunAttribute::MessageIntegrity(msg_integrity) - } - ATTR_ERROR_CODE => { - let (attr_data, zeros) = take(2usize)(attr_data)?; - if zeros.iter_elements().any(|b| b != 0) { - panic!("Invalid STUN error code"); - } - let (attr_data, err_num) = be_u16(attr_data)?; - if err_num & 0b1111100000000000 != 0 { - panic!("Invalid STUN error code"); - } - let error = String::from_iter(attr_data.iter_elements().map(|b| b as char)); - StunAttribute::ErrorCode((err_num, error)) - } - ATTR_UNKNOWN_ATTRIBUTES => { - let (_residual, unknown_attrs) = - many((attr_len / 2) as usize, be_u16).parse(attr_data)?; - StunAttribute::UnknownAttributes(unknown_attrs) - } - ATTR_NUM_FINGERPRINT => { - let (_residual, fingerprint) = be_u32(attr_data)?; - StunAttribute::Fingerprint(fingerprint) - } - ATTR_REALM => { - let realm = String::from_iter(attr_data.iter_elements().map(|b| b as char)); - StunAttribute::Realm(realm) - } - ATTR_NONCE => { - let nonce = String::from_iter(attr_data.iter_elements().map(|b| b as char)); - StunAttribute::Nonce(nonce) - } - ATTR_NUM_SOFTWARE => { - let software = String::from_iter(attr_data.iter_elements().map(|b| b as char)); - StunAttribute::Software(software) - } - ATTR_ALTERNATE_SERVER => { - let (_residual, addr) = parse_stun_address(attr_data)?; - StunAttribute::AlternateServer(addr) - } - - t => { - warn!("Unknown STUN attribute type {t}"); - StunAttribute::Unknown((t, attr_data.iter_elements().collect())) - } - }; - - Ok((bytes, Some(attr))) -} - -fn parse_stun_header<'a, E: ParseError<&'a [u8]>>( - bytes: &'a [u8], -) -> IResult<&'a [u8], StunHeader, E> { - let (bytes, msg_type) = parse_stun_message_type(bytes)?; - let (bytes, msg_length) = be_u16(bytes)?; - let (bytes, _) = tag(MAGIC_COOKIE.as_slice())(bytes)?; - let (bytes, tx_id) = take_txid(bytes)?; - Ok(( - bytes, - StunHeader { - msg_type, - msg_length, - tx_id, - }, - )) + #[clap(short, long, default_value = "3478")] + port: u16, + #[clap(short, long, default_value = "text")] + format: OutputFormat, + #[clap( + short, + long, + default_value = "false", + help = "Only output the first mapped address" + )] + address_only: bool, + #[command(flatten)] + verbose: clap_verbosity_flag::Verbosity, } fn main() { let cli = ::parse(); + env_logger::Builder::new() + .filter_level(cli.verbose.log_level_filter()) + .init(); - let dest = cli.host + ":" + &cli.port.unwrap_or(3478).to_string(); + let dest = cli.host + ":" + &cli.port.to_string(); let socket = UdpSocket::bind("[::]:0").expect("Unable to bind a UDP socket"); socket .connect(dest) .expect("Unable to connect to the destination"); - if cli.debug { - println!( - "Connected to {:?} from {:?}", - socket.peer_addr(), - socket.local_addr() - ); - } + + info!( + "Connected to {:?} from {:?}", + socket.peer_addr(), + socket.local_addr() + ); let req = TxId::new().make_request(); - if cli.debug { - println!("Sending request {:?}", &req); - } + + debug!("Sending request {:?}", &req); + socket.send(&req).expect("Unable to send request"); let mut buf = [0u8; 1500]; if let Ok(received) = socket.recv(&mut buf) { - if cli.debug { - println!("Received response: {:?}", &buf[..received]); + debug!("Received response: {:?}", &buf[..received]); + + let msg = StunMessage::parse(&buf).unwrap(); + info!("Parsed message from {}:", socket.peer_addr().unwrap()); + if cli.address_only { + if let Some(addr) = msg.attributes.mapped_address() { + println!("{}", cli.format.format_address(addr)); + } else { + std::process::exit(1); + } + } else { + println!("{}", cli.format.format_stun(&msg)); } - let (_residual, msg) = parse_stun_message::>(&buf).unwrap(); - println!("Parsed message from {}:", socket.peer_addr().unwrap()); - println!("{}", msg); } else if let Err(e) = socket.recv(&mut buf) { println!("recv function failed: {e:?}"); }