Add options and refactor to lib

This commit is contained in:
Keenan Tims 2025-02-14 17:27:18 -08:00
parent 6dacc3a52f
commit a3e944d1d6
No known key found for this signature in database
GPG Key ID: B8FDD4AD6B193F06
4 changed files with 733 additions and 468 deletions

167
Cargo.lock generated
View File

@ -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"

View File

@ -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"

500
src/lib.rs Normal file
View File

@ -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<u8> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<u16>),
Software(String),
AlternateServer((IpAddr, u16)),
Unknown((u16, Vec<u8>)),
}
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<StunAttribute>);
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<Self, nom::Err<nom::error::Error<&[u8]>>> {
let (_, msg) = parse_stun_message(bytes)?;
Ok(msg)
}
}
fn take_txid<I, E: ParseError<I>>(bytes: I) -> IResult<I, TxId, E>
where
I: nom::Input<Item = u8> + 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<I>>(input: I) -> IResult<I, StunMessage, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + 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<Item = u8>, E: ParseError<I>>(
input: I,
) -> IResult<I, StunMessageType, E> {
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<I: nom::Input<Item = u8>, E: ParseError<I>>(
bytes: I,
) -> IResult<I, (IpAddr, u16), E> {
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<I, E: ParseError<I>>(
bytes: I,
tx_id: &TxId,
) -> IResult<I, (IpAddr, u16), E>
where
I: nom::Input<Item = u8>,
{
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<I, E: ParseError<I>>(
tx_id: &TxId,
) -> impl Fn(I) -> IResult<I, Option<StunAttribute>, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes,
{
let tx_id = tx_id.clone();
move |bytes| parse_stun_attribute_impl(bytes, &tx_id)
}
fn parse_stun_attribute_impl<I, E: ParseError<I>>(
bytes: I,
tx_id: &TxId,
) -> IResult<I, Option<StunAttribute>, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + 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<I>>(input: I) -> IResult<I, StunHeader, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + 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,
},
))
}

View File

@ -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<u8> {
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<u16>,
#[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<u16>),
Software(String),
AlternateServer((IpAddr, u16)),
Unknown((u16, Vec<u8>)),
}
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<StunAttribute>);
#[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<I, E: ParseError<I>>(bytes: I) -> IResult<I, (IpAddr, u16), E>
where
I: nom::Input<Item = u8>,
{
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<I, E: ParseError<I>>(
bytes: I,
tx_id: &TxId,
) -> IResult<I, (IpAddr, u16), E>
where
I: nom::Input<Item = u8>,
{
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<I, E: ParseError<I>>(
tx_id: &TxId,
) -> impl Fn(I) -> IResult<I, Option<StunAttribute>, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes,
{
let tx_id = tx_id.clone();
move |bytes| parse_stun_attribute_impl(bytes, &tx_id)
}
fn parse_stun_attribute_impl<I, E: ParseError<I>>(
bytes: I,
tx_id: &TxId,
) -> IResult<I, Option<StunAttribute>, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + 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 = <Cli as clap::Parser>::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!(
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::<nom::error::Error<&[u8]>>(&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:?}");
}