refactor address output, add -4, -6 AF flags

This commit is contained in:
Keenan Tims 2025-02-14 17:55:56 -08:00
parent a3e944d1d6
commit 903bf23646
No known key found for this signature in database
GPG Key ID: B8FDD4AD6B193F06
2 changed files with 127 additions and 83 deletions

View File

@ -1,11 +1,10 @@
use crc32fast::hash; use crc32fast::hash;
use log::{debug, info, warn}; use log::warn;
use nom::bytes::complete::{tag, take}; use nom::bytes::complete::{tag, take};
use nom::error::ParseError; use nom::error::ParseError;
use nom::multi::{many, many0}; use nom::multi::{many, many0};
use nom::number::complete::{be_u128, be_u16, be_u32, be_u8}; use nom::number::complete::{be_u128, be_u16, be_u32, be_u8};
use nom::{AsBytes, IResult, Parser}; use nom::{AsBytes, IResult, Parser};
use rand::seq::{IndexedRandom, SliceRandom};
use rand::{self, RngCore}; use rand::{self, RngCore};
use serde::Serialize; use serde::Serialize;
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
@ -42,6 +41,12 @@ const HEADER_LEN: u16 = 20;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TxId([u8; 12]); pub struct TxId([u8; 12]);
impl Default for TxId {
fn default() -> Self {
Self::new()
}
}
impl TxId { impl TxId {
pub fn new() -> Self { pub fn new() -> Self {
let mut tx_id = [0; 12]; let mut tx_id = [0; 12];
@ -101,6 +106,21 @@ fn fingerprint(msg: &[u8]) -> u32 {
hash(msg) ^ 0x5354554e hash(msg) ^ 0x5354554e
} }
#[derive(Debug, Clone, Serialize)]
pub struct AddrPort {
pub address: IpAddr,
pub port: u16,
}
impl From<(IpAddr, u16)> for AddrPort {
fn from(value: (IpAddr, u16)) -> Self {
Self {
address: value.0,
port: value.1,
}
}
}
#[derive(Debug, Clone, Copy, Serialize)] #[derive(Debug, Clone, Copy, Serialize)]
pub enum StunClass { pub enum StunClass {
Request = 0, Request = 0,
@ -116,10 +136,10 @@ pub enum StunMethod {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub enum StunAttribute { pub enum StunAttribute {
MappedAddress((IpAddr, u16)), MappedAddress(AddrPort),
XorMappedAddress((IpAddr, u16)), XorMappedAddress(AddrPort),
SourceAddress((IpAddr, u16)), SourceAddress(AddrPort),
ChangedAddress((IpAddr, u16)), ChangedAddress(AddrPort),
Username(String), Username(String),
MessageIntegrity([u8; 20]), MessageIntegrity([u8; 20]),
Fingerprint(u32), Fingerprint(u32),
@ -128,7 +148,7 @@ pub enum StunAttribute {
Nonce(String), Nonce(String),
UnknownAttributes(Vec<u16>), UnknownAttributes(Vec<u16>),
Software(String), Software(String),
AlternateServer((IpAddr, u16)), AlternateServer(AddrPort),
Unknown((u16, Vec<u8>)), Unknown((u16, Vec<u8>)),
} }
@ -142,40 +162,40 @@ fn addr_family(addr: &IpAddr) -> &'static str {
impl fmt::Display for StunAttribute { impl fmt::Display for StunAttribute {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
StunAttribute::MappedAddress((addr, port)) => { StunAttribute::MappedAddress(a) => {
write!( write!(
f, f,
" MappedAddress ({}) {}:{}", " MappedAddress ({}) {}:{}",
addr_family(addr), addr_family(&a.address),
addr, a.address,
port a.port
) )
} }
StunAttribute::SourceAddress((addr, port)) => { StunAttribute::SourceAddress(a) => {
write!( write!(
f, f,
" SourceAddress ({}) {}:{}", " SourceAddress ({}) {}:{}",
addr_family(addr), addr_family(&a.address),
addr, a.address,
port a.port
) )
} }
StunAttribute::ChangedAddress((addr, port)) => { StunAttribute::ChangedAddress(a) => {
write!( write!(
f, f,
" ChangedAddress ({}) {}:{}", " ChangedAddress ({}) {}:{}",
addr_family(addr), addr_family(&a.address),
addr, a.address,
port a.port
) )
} }
StunAttribute::XorMappedAddress((addr, port)) => { StunAttribute::XorMappedAddress(a) => {
write!( write!(
f, f,
" XorMappedAddress ({}) {}:{}", " XorMappedAddress ({}) {}:{}",
addr_family(addr), addr_family(&a.address),
addr, a.address,
port a.port
) )
} }
StunAttribute::Username(username) => writeln!(f, " Username {}", username), StunAttribute::Username(username) => writeln!(f, " Username {}", username),
@ -194,13 +214,13 @@ impl fmt::Display for StunAttribute {
write!(f, " UnknownAttributes {:?}", unknown_attrs) write!(f, " UnknownAttributes {:?}", unknown_attrs)
} }
StunAttribute::Software(software) => writeln!(f, " Software {}", software), StunAttribute::Software(software) => writeln!(f, " Software {}", software),
StunAttribute::AlternateServer((addr, port)) => { StunAttribute::AlternateServer(a) => {
write!( write!(
f, f,
" AlternateServer ({}) {}:{}", " AlternateServer ({}) {}:{}",
addr_family(addr), addr_family(&a.address),
addr, a.address,
port a.port
) )
} }
StunAttribute::Unknown((attr_type, data)) => { StunAttribute::Unknown((attr_type, data)) => {
@ -240,7 +260,6 @@ impl fmt::Display for StunHeader {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct StunAttributes(Vec<StunAttribute>); pub struct StunAttributes(Vec<StunAttribute>);
impl fmt::Display for StunAttributes { impl fmt::Display for StunAttributes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, " Attributes")?; writeln!(f, " Attributes")?;
@ -252,9 +271,11 @@ impl fmt::Display for StunAttributes {
} }
impl StunAttributes { impl StunAttributes {
pub fn mapped_address(&self) -> Option<&(IpAddr, u16)> { pub fn mapped_address(&self) -> Option<&AddrPort> {
self.0.iter().find_map(|attr| match attr { self.0.iter().find_map(|attr| match attr {
StunAttribute::MappedAddress(addr) | StunAttribute::XorMappedAddress(addr) => Some(addr), StunAttribute::MappedAddress(addr) | StunAttribute::XorMappedAddress(addr) => {
Some(addr)
}
_ => None, _ => None,
}) })
} }
@ -274,7 +295,6 @@ impl fmt::Display for StunMessage {
} }
} }
impl StunMessage { impl StunMessage {
pub fn parse(bytes: &[u8]) -> Result<Self, nom::Err<nom::error::Error<&[u8]>>> { pub fn parse(bytes: &[u8]) -> Result<Self, nom::Err<nom::error::Error<&[u8]>>> {
let (_, msg) = parse_stun_message(bytes)?; let (_, msg) = parse_stun_message(bytes)?;
@ -282,27 +302,27 @@ impl StunMessage {
} }
} }
fn take_txid<I, E: ParseError<I>>(bytes: I) -> IResult<I, TxId, E> fn take_txid<I, E: ParseError<I>>(input: I) -> IResult<I, TxId, E>
where where
I: nom::Input<Item = u8> + AsBytes, I: nom::Input<Item = u8> + AsBytes,
{ {
let (bytes, tx_id) = take(12usize)(bytes)?; let (input, tx_id) = take(12usize)(input)?;
Ok((bytes, TxId::from_bytes(tx_id.as_bytes()))) Ok((input, TxId::from_bytes(tx_id.as_bytes())))
} }
fn parse_stun_message<'a, I, E: ParseError<I>>(input: I) -> IResult<I, StunMessage, E> fn parse_stun_message<'a, I, E: ParseError<I>>(input: I) -> IResult<I, StunMessage, E>
where where
I: nom::Input<Item = u8> + nom::Compare<I> + nom::Compare<&'a [u8]> + AsBytes + Debug, I: nom::Input<Item = u8> + nom::Compare<I> + nom::Compare<&'a [u8]> + AsBytes + Debug,
{ {
let (bytes, h) = parse_stun_header(input)?; let (input, h) = parse_stun_header(input)?;
let (residual, bytes) = take(h.msg_length)(bytes)?; let (residual, input) = take(h.msg_length)(input)?;
if residual.input_len() != 0 { if residual.input_len() != 0 {
warn!("Trailing bytes in STUN message: {:?}", residual); warn!("Trailing bytes in STUN message: {:?}", residual);
} }
let (bytes, attributes) = many0(parse_stun_attribute(&h.tx_id)).parse(bytes)?; let (input, attributes) = many0(parse_stun_attribute(&h.tx_id)).parse(input)?;
if !bytes.input_len() != 0 { if !input.input_len() != 0 {
warn!("Trailing bytes in STUN message attributes: {:?}", bytes); warn!("Trailing bytes in STUN message attributes: {:?}", input);
} }
let attributes = StunAttributes(attributes.iter().filter_map(|i| i.clone()).collect()); let attributes = StunAttributes(attributes.iter().filter_map(|i| i.clone()).collect());
@ -315,10 +335,10 @@ where
)) ))
} }
fn parse_stun_message_type<'a, I: nom::Input<Item = u8>, E: ParseError<I>>( fn parse_stun_message_type<I: nom::Input<Item = u8>, E: ParseError<I>>(
input: I, input: I,
) -> IResult<I, StunMessageType, E> { ) -> IResult<I, StunMessageType, E> {
let (bytes, msg_type_raw) = be_u16(input)?; let (input, msg_type_raw) = be_u16(input)?;
if msg_type_raw & 0b11000000 != 0 { if msg_type_raw & 0b11000000 != 0 {
panic!("Invalid STUN message type"); panic!("Invalid STUN message type");
} }
@ -335,39 +355,36 @@ fn parse_stun_message_type<'a, I: nom::Input<Item = u8>, E: ParseError<I>>(
1 => StunMethod::Binding, 1 => StunMethod::Binding,
_ => panic!("Invalid STUN message method"), _ => panic!("Invalid STUN message method"),
}; };
Ok((bytes, StunMessageType { class, method })) Ok((input, StunMessageType { class, method }))
} }
fn parse_stun_address<I: nom::Input<Item = u8>, E: ParseError<I>>( fn parse_stun_address<I: nom::Input<Item = u8>, E: ParseError<I>>(
bytes: I, input: I,
) -> IResult<I, (IpAddr, u16), E> { ) -> IResult<I, AddrPort, E> {
let (bytes, _) = take(1usize)(bytes)?; let (input, _) = take(1usize)(input)?;
let (bytes, family) = be_u8(bytes)?; let (input, family) = be_u8(input)?;
let (bytes, port) = be_u16(bytes)?; let (input, port) = be_u16(input)?;
let (bytes, addr) = match family { let (input, addr) = match family {
0x01 => { 0x01 => {
let (bytes, val) = be_u32(bytes)?; let (input, val) = be_u32(input)?;
(bytes, (IpAddr::V4(val.into()), port)) (input, (IpAddr::V4(val.into()), port).into())
} }
0x02 => { 0x02 => {
let (bytes, val) = be_u128(bytes)?; let (input, val) = be_u128(input)?;
(bytes, (IpAddr::V6(val.into()), port)) (input, (IpAddr::V6(val.into()), port).into())
} }
_ => panic!("Invalid address family"), _ => panic!("Invalid address family"),
}; };
Ok((bytes, addr)) Ok((input, addr))
} }
fn parse_stun_xor_address<I, E: ParseError<I>>( fn parse_stun_xor_address<I, E: ParseError<I>>(input: I, tx_id: &TxId) -> IResult<I, AddrPort, E>
bytes: I,
tx_id: &TxId,
) -> IResult<I, (IpAddr, u16), E>
where where
I: nom::Input<Item = u8>, I: nom::Input<Item = u8>,
{ {
let (bytes, addr) = parse_stun_address(bytes)?; let (input, addr) = parse_stun_address(input)?;
let xor_port = addr.1 ^ 0x2112; let xor_port = addr.port ^ 0x2112;
let xor_addr = match addr.0 { let xor_addr = match addr.address {
IpAddr::V4(v4) => { IpAddr::V4(v4) => {
let v4 = u32::from(v4); let v4 = u32::from(v4);
let xor_v4 = v4 ^ 0x2112a442; let xor_v4 = v4 ^ 0x2112a442;
@ -379,7 +396,7 @@ where
IpAddr::V6(xor_v6.into()) IpAddr::V6(xor_v6.into())
} }
}; };
Ok((bytes, (xor_addr, xor_port))) Ok((input, (xor_addr, xor_port).into()))
} }
fn parse_stun_attribute<I, E: ParseError<I>>( fn parse_stun_attribute<I, E: ParseError<I>>(
@ -389,22 +406,22 @@ where
I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes, I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes,
{ {
let tx_id = tx_id.clone(); let tx_id = tx_id.clone();
move |bytes| parse_stun_attribute_impl(bytes, &tx_id) move |input| parse_stun_attribute_impl(input, &tx_id)
} }
fn parse_stun_attribute_impl<I, E: ParseError<I>>( fn parse_stun_attribute_impl<I, E: ParseError<I>>(
bytes: I, input: I,
tx_id: &TxId, tx_id: &TxId,
) -> IResult<I, Option<StunAttribute>, E> ) -> IResult<I, Option<StunAttribute>, E>
where where
I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes, I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes,
{ {
let (bytes, attr_type) = be_u16(bytes)?; let (input, attr_type) = be_u16(input)?;
let (bytes, attr_len) = be_u16(bytes)?; let (input, attr_len) = be_u16(input)?;
let (bytes, attr_data) = take(attr_len)(bytes)?; let (input, attr_data) = take(attr_len)(input)?;
if attr_len == 0 { if attr_len == 0 {
return Ok((bytes, None)); return Ok((input, None));
} }
let attr = match attr_type { let attr = match attr_type {
@ -478,19 +495,19 @@ where
} }
}; };
Ok((bytes, Some(attr))) Ok((input, Some(attr)))
} }
fn parse_stun_header<'a, I, E: ParseError<I>>(input: I) -> IResult<I, StunHeader, E> fn parse_stun_header<'a, I, E: ParseError<I>>(input: I) -> IResult<I, StunHeader, E>
where where
I: nom::Input<Item = u8> + nom::Compare<I> + nom::Compare<&'a [u8]> + AsBytes, I: nom::Input<Item = u8> + nom::Compare<I> + nom::Compare<&'a [u8]> + AsBytes,
{ {
let (bytes, msg_type) = parse_stun_message_type(input)?; let (input, msg_type) = parse_stun_message_type(input)?;
let (bytes, msg_length) = be_u16(bytes)?; let (input, msg_length) = be_u16(input)?;
let (bytes, _) = tag(MAGIC_COOKIE.as_bytes())(bytes)?; let (input, _) = tag(MAGIC_COOKIE.as_bytes())(input)?;
let (bytes, tx_id) = take_txid(bytes)?; let (input, tx_id) = take_txid(input)?;
Ok(( Ok((
bytes, input,
StunHeader { StunHeader {
msg_type, msg_type,
msg_length, msg_length,

View File

@ -1,8 +1,7 @@
use clap::ValueEnum; use clap::ValueEnum;
use log::{debug, info, warn}; use log::{debug, info};
use serde::Serialize; use std::net::{IpAddr, ToSocketAddrs, UdpSocket};
use std::net::{IpAddr, UdpSocket}; use tailstun::{AddrPort, StunMessage, TxId};
use tailstun::{StunMessage, TxId};
#[derive(Debug, Clone, ValueEnum)] #[derive(Debug, Clone, ValueEnum)]
enum OutputFormat { enum OutputFormat {
@ -19,11 +18,21 @@ impl OutputFormat {
OutputFormat::Yaml => serde_yaml::to_string(msg).unwrap(), OutputFormat::Yaml => serde_yaml::to_string(msg).unwrap(),
} }
} }
fn format_address(&self, (addr, port): &(IpAddr, u16)) -> String { fn format_address(&self, a: &AddrPort) -> String {
let a = match a.address {
IpAddr::V4(_) => a.address,
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
IpAddr::V4(v4)
} else {
a.address
}
}
};
match self { match self {
OutputFormat::Text => format!("{}", addr), OutputFormat::Text => format!("{}", a),
OutputFormat::Json => serde_json::to_string_pretty(addr).unwrap(), OutputFormat::Json => serde_json::to_string_pretty(&a).unwrap(),
OutputFormat::Yaml => serde_yaml::to_string(addr).unwrap(), OutputFormat::Yaml => serde_yaml::to_string(&a).unwrap(),
} }
} }
} }
@ -35,13 +44,17 @@ struct Cli {
host: String, host: String,
#[clap(short, long, default_value = "3478")] #[clap(short, long, default_value = "3478")]
port: u16, port: u16,
#[clap(short = '4', conflicts_with = "v6_only", default_value = "false")]
v4_only: bool,
#[clap(short = '6', conflicts_with = "v4_only", default_value = "false")]
v6_only: bool,
#[clap(short, long, default_value = "text")] #[clap(short, long, default_value = "text")]
format: OutputFormat, format: OutputFormat,
#[clap( #[clap(
short, short,
long, long,
default_value = "false", default_value = "false",
help = "Only output the first mapped address" help = "Only output the first mapped address & convert IPv6-mapped to IPv4"
)] )]
address_only: bool, address_only: bool,
#[command(flatten)] #[command(flatten)]
@ -54,7 +67,20 @@ fn main() {
.filter_level(cli.verbose.log_level_filter()) .filter_level(cli.verbose.log_level_filter())
.init(); .init();
let dest = cli.host + ":" + &cli.port.to_string(); let dest = (cli.host.as_str(), cli.port)
.to_socket_addrs()
.expect("Unable to resolve host")
.find(|a| {
if cli.v4_only {
a.is_ipv4()
} else if cli.v6_only {
a.is_ipv6()
} else {
true
}
})
.expect("No address found for host");
let socket = UdpSocket::bind("[::]:0").expect("Unable to bind a UDP socket"); let socket = UdpSocket::bind("[::]:0").expect("Unable to bind a UDP socket");
socket socket
.connect(dest) .connect(dest)
@ -83,6 +109,7 @@ fn main() {
if let Some(addr) = msg.attributes.mapped_address() { if let Some(addr) = msg.attributes.mapped_address() {
println!("{}", cli.format.format_address(addr)); println!("{}", cli.format.format_address(addr));
} else { } else {
// No mapped address
std::process::exit(1); std::process::exit(1);
} }
} else { } else {