Compare commits

...

10 Commits

Author SHA1 Message Date
Keenan Tims
3ef27c0bed
improve backoff/retry behaviour 2025-05-26 16:30:21 -07:00
Keenan Tims
b92c9f5503
add timeout & backoff, some refactoring 2025-05-26 15:49:35 -07:00
Keenan Tims
1560535fbe
update README 2025-05-26 15:19:33 -07:00
Keenan Tims
286b21e940
refactor for DRY and compartmentalization 2025-05-06 11:05:31 -07:00
Keenan Tims
1488dba92d
handle padding properly 2025-02-14 19:05:11 -08:00
Keenan Tims
a66a6a263e
support for checking fingerprint, misc 2025-02-14 18:55:57 -08:00
Keenan Tims
6985ed6614
add support for RESPONSE-ORIGIN and OTHER-ADDRESS attributes 2025-02-14 18:06:41 -08:00
Keenan Tims
903bf23646
refactor address output, add -4, -6 AF flags 2025-02-14 17:55:56 -08:00
Keenan Tims
a3e944d1d6
Add options and refactor to lib 2025-02-14 17:27:18 -08:00
Keenan Tims
6dacc3a52f
clippies 2025-02-13 18:35:55 -08:00
5 changed files with 984 additions and 550 deletions

349
Cargo.lock generated
View File

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@ -43,26 +52,29 @@ dependencies = [
[[package]] [[package]]
name = "anstyle-wincon" name = "anstyle-wincon"
version = "3.0.7" version = "3.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell", "once_cell_polyfill",
"windows-sys", "windows-sys",
] ]
[[package]] [[package]]
name = "bitflags" name = "backon"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "byteorder"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496"
dependencies = [
"fastrand",
]
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -72,19 +84,29 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.29" version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
] ]
[[package]] [[package]]
name = "clap_builder" name = "clap-verbosity-flag"
version = "4.5.29" version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1"
dependencies = [
"clap",
"log",
]
[[package]]
name = "clap_builder"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -94,9 +116,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.28" version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -126,23 +148,74 @@ dependencies = [
] ]
[[package]] [[package]]
name = "getrandom" name = "env_filter"
version = "0.3.1" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
dependencies = [
"log",
"regex",
]
[[package]]
name = "env_logger"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"jiff",
"log",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi",
"wasi", "wasi",
"windows-targets",
] ]
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.1" version = "1.70.1"
@ -150,16 +223,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "libc" name = "itoa"
version = "0.2.169" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jiff"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93"
dependencies = [
"jiff-static",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
]
[[package]]
name = "jiff-static"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "libc"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.25" version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]] [[package]]
name = "memchr" name = "memchr"
@ -177,47 +280,67 @@ dependencies = [
] ]
[[package]] [[package]]
name = "once_cell" name = "once_cell_polyfill"
version = "1.20.3" version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
version = "0.2.20" version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [ dependencies = [
"zerocopy 0.7.35", "zerocopy",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.93" version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]] [[package]]
name = "rand" name = "r-efi"
version = "0.9.0" version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [ dependencies = [
"rand_chacha", "rand_chacha",
"rand_core", "rand_core",
"zerocopy 0.8.17",
] ]
[[package]] [[package]]
@ -232,12 +355,91 @@ dependencies = [
[[package]] [[package]]
name = "rand_core" name = "rand_core"
version = "0.9.0" version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [ dependencies = [
"getrandom", "getrandom",
"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.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
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]] [[package]]
@ -248,9 +450,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.98" version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -261,18 +463,30 @@ dependencies = [
name = "tailstun" name = "tailstun"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"backon",
"clap", "clap",
"clap-verbosity-flag",
"crc32fast", "crc32fast",
"env_logger",
"log", "log",
"nom", "nom",
"rand", "rand",
"serde",
"serde_json",
"serde_yaml",
] ]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.16" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
@ -282,9 +496,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.13.3+wasi-0.2.2" version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [ dependencies = [
"wit-bindgen-rt", "wit-bindgen-rt",
] ]
@ -364,48 +578,27 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "wit-bindgen-rt" name = "wit-bindgen-rt"
version = "0.33.0" version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [ dependencies = [
"byteorder", "zerocopy-derive",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713"
dependencies = [
"zerocopy-derive 0.8.17",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.7.35" version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@ -4,8 +4,14 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
backon = { version = "1.5.0", default-features = false, features = ["std", "std-blocking-sleep"] }
clap = { version = "4.5.29", features = ["derive"] } clap = { version = "4.5.29", features = ["derive"] }
clap-verbosity-flag = "3.0.2"
crc32fast = "1.4.2" crc32fast = "1.4.2"
env_logger = "0.11.6"
log = "0.4.25" log = "0.4.25"
nom = "8.0.0" nom = "8.0.0"
rand = "0.9.0" rand = "0.9.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
serde_yaml = "0.9.34"

View File

@ -1 +1,3 @@
A trivial test client for Tailscale's derper STUN service, which doesn't respond to generic STUN requests. A trivial test client for Tailscale's derper STUN service, which doesn't respond to generic STUN requests.
It was originally designed specifically for the Tailscale derper, but it works fine to interrogate any other DERP server as well and get back the attributes in several formats (JSON, YAML, text)

551
src/lib.rs Normal file
View File

@ -0,0 +1,551 @@
use crc32fast::{hash, Hasher};
use log::warn;
use nom::bytes::complete::{tag, take};
use nom::error::ParseError;
use nom::multi::many;
use nom::number::complete::{be_u128, be_u16, be_u32, be_u8};
use nom::{AsBytes, IResult, Parser};
use rand::{self, RngCore};
use serde::Serialize;
use std::fmt::{self, Debug};
use std::net::{IpAddr, SocketAddr};
// 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 ATTR_RESPONSE_ORIGIN: u16 = 0x802b;
const ATTR_OTHER_ADDRESS: u16 = 0x802c;
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 Default for TxId {
fn default() -> Self {
Self::new([0; 12])
}
}
impl TxId {
pub fn new(tx_id: [u8; 12]) -> Self {
Self(tx_id)
}
pub fn random() -> Self {
let mut tx_id = [0; 12];
rand::rng().fill_bytes(&mut tx_id);
Self::new(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
}
}
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, 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)]
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(SocketAddr),
XorMappedAddress(SocketAddr),
SourceAddress(SocketAddr),
ChangedAddress(SocketAddr),
Username(String),
MessageIntegrity([u8; 20]),
Fingerprint((u32, bool)),
ErrorCode((u16, String)),
Realm(String),
Nonce(String),
UnknownAttributes(Vec<u16>),
Software(String),
AlternateServer(SocketAddr),
ResponseOrigin(SocketAddr),
OtherAddress(SocketAddr),
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 {
// Helper function for attributes with IP and port
fn format_ip_port(
f: &mut fmt::Formatter<'_>,
label: &str,
addr: &SocketAddr,
) -> fmt::Result {
write!(f, " {} ({}) {}", label, addr_family(&addr.ip()), addr)
}
match self {
StunAttribute::MappedAddress(a) => format_ip_port(f, "MappedAddress", a),
StunAttribute::SourceAddress(a) => format_ip_port(f, "SourceAddress", a),
StunAttribute::ChangedAddress(a) => format_ip_port(f, "ChangedAddress", a),
StunAttribute::XorMappedAddress(a) => format_ip_port(f, "XorMappedAddress", a),
StunAttribute::AlternateServer(a) => format_ip_port(f, "AlternateServer", a),
StunAttribute::ResponseOrigin(a) => format_ip_port(f, "ResponseOrigin", a),
StunAttribute::OtherAddress(a) => format_ip_port(f, "OtherAddress", a),
StunAttribute::Username(username) => write!(f, " Username {}", username),
StunAttribute::MessageIntegrity(msg_integrity) => {
write!(f, " MessageIntegrity {:?}", msg_integrity)
}
StunAttribute::Fingerprint((crc, ok)) => {
write!(
f,
" Fingerprint 0x{:08x} ({})",
crc,
if *ok { "OK" } else { "FAIL" }
)
}
StunAttribute::ErrorCode((err_num, error)) => {
write!(f, " ErrorCode {} ({})", err_num, error)
}
StunAttribute::Realm(realm) => write!(f, " Realm {}", realm),
StunAttribute::Nonce(nonce) => write!(f, " Nonce {}", nonce),
StunAttribute::UnknownAttributes(unknown_attrs) => {
write!(f, " UnknownAttributes {:?}", unknown_attrs)
}
StunAttribute::Software(software) => write!(f, " Software {}", software),
StunAttribute::Unknown((attr_type, data)) => {
write!(f, " Unknown ({:04x}) {:?}", 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<&SocketAddr> {
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)
}
}
pub fn rand_request() -> Vec<u8> {
const LEN_ATTR_SOFTWARE: u16 = 4 + SOFTWARE.len() as u16;
let tx_id = TxId::random();
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(tx_id.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
}
fn take_txid<I, E: ParseError<I>>(input: I) -> IResult<I, TxId, E>
where
I: nom::Input<Item = u8> + AsBytes,
{
let (input, tx_id) = take(12usize)(input)?;
Ok((input, 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]>
+ nom::Offset
+ AsBytes
+ Debug,
{
let mut hasher = Some(Hasher::new());
let input_start = input.clone();
let (input, h) = parse_stun_header(input)?;
hasher
.as_mut()
.unwrap()
.update(input_start.take(input_start.offset(&input)).as_bytes());
let (residual, input) = take(h.msg_length)(input)?;
if residual.input_len() != 0 {
warn!("Trailing bytes in STUN message: {:?}", residual);
}
let mut input = input;
let mut attributes = Vec::new();
while let Ok((new_input, attr)) = parse_stun_attribute::<I, E>(&h.tx_id)(input.clone()) {
let attr = if let Some(StunAttribute::Fingerprint(fingerprint)) = attr {
let crc = hasher.unwrap().finalize() ^ 0x5354554e;
hasher = None;
if crc == fingerprint.0 {
Some(StunAttribute::Fingerprint((crc, true)))
} else {
warn!(
"Fingerprint mismatch: expected 0x{:08x}, got 0x{:08x}",
crc, fingerprint.0
);
attr
}
} else {
if let Some(hasher) = hasher.as_mut() {
hasher.update(input.take(input.offset(&new_input)).as_bytes());
} else {
warn!("Received attributes after FINGERPRINT");
}
attr
};
attributes.push(attr);
input = new_input;
}
if input.input_len() != 0 {
warn!("Trailing bytes in STUN message attributes: {:?}", input);
}
let attributes = StunAttributes(attributes.iter().filter_map(|i| i.clone()).collect());
Ok((
residual,
StunMessage {
header: h,
attributes,
},
))
}
fn parse_stun_message_type<I: nom::Input<Item = u8>, E: ParseError<I>>(
input: I,
) -> IResult<I, StunMessageType, E> {
let (input, 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((input, StunMessageType { class, method }))
}
fn parse_stun_address<I: nom::Input<Item = u8>, E: ParseError<I>>(
input: I,
) -> IResult<I, SocketAddr, E> {
let (input, _) = take(1usize)(input)?;
let (input, family) = be_u8(input)?;
let (input, port) = be_u16(input)?;
let (input, addr) = match family {
0x01 => {
let (input, val) = be_u32(input)?;
(input, (IpAddr::V4(val.into()), port).into())
}
0x02 => {
let (input, val) = be_u128(input)?;
(input, (IpAddr::V6(val.into()), port).into())
}
_ => panic!("Invalid address family"),
};
Ok((input, addr))
}
fn parse_stun_xor_address<I, E: ParseError<I>>(input: I, tx_id: &TxId) -> IResult<I, SocketAddr, E>
where
I: nom::Input<Item = u8>,
{
let (input, addr) = parse_stun_address(input)?;
let xor_port = addr.port() ^ 0x2112;
let xor_addr = match addr {
SocketAddr::V4(v4) => {
let v4 = v4.ip().to_bits();
let xor_v4 = v4 ^ 0x2112a442;
IpAddr::V4(xor_v4.into())
}
SocketAddr::V6(v6) => {
let v6 = v6.ip().to_bits();
let xor_v6: u128 = v6 ^ (0x2112a442 << 96 | u128::from(tx_id));
IpAddr::V6(xor_v6.into())
}
};
Ok((input, (xor_addr, xor_port).into()))
}
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 |input| parse_stun_attribute_impl(input, &tx_id)
}
fn parse_stun_attribute_impl<I, E: ParseError<I>>(
input: I,
tx_id: &TxId,
) -> IResult<I, Option<StunAttribute>, E>
where
I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes,
{
let (input, attr_type) = be_u16(input)?;
let (input, attr_len) = be_u16(input)?;
let (input, attr_data) = take(attr_len)(input)?;
if attr_len == 0 {
return Ok((input, None));
}
let (input, _padding) = take(attr_len % 4)(input)?;
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, false))
}
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)
}
ATTR_RESPONSE_ORIGIN => {
let (_residual, addr) = parse_stun_address(attr_data)?;
StunAttribute::ResponseOrigin(addr)
}
ATTR_OTHER_ADDRESS => {
let (_residual, addr) = parse_stun_address(attr_data)?;
StunAttribute::OtherAddress(addr)
}
t => {
warn!("Unknown STUN attribute type {t}");
StunAttribute::Unknown((t, attr_data.iter_elements().collect()))
}
};
Ok((input, 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 (input, msg_type) = parse_stun_message_type(input)?;
let (input, msg_length) = be_u16(input)?;
let (input, _) = tag(MAGIC_COOKIE.as_bytes())(input)?;
let (input, tx_id) = take_txid(input)?;
Ok((
input,
StunHeader {
msg_type,
msg_length,
tx_id,
},
))
}

View File

@ -1,96 +1,45 @@
use clap; use backon::BlockingRetryable;
use std::fmt; use backon::ExponentialBuilder;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; use clap::ValueEnum;
use std::ops::Index; use log::{debug, error, info};
use std::{
net::{IpAddr, SocketAddr, ToSocketAddrs, UdpSocket},
time::Duration,
};
use tailstun::StunMessage;
use crc32fast::hash; #[derive(Debug, Clone, ValueEnum)]
use log::warn; enum OutputFormat {
use nom::bytes::complete::{tag, take}; Text,
use nom::error::ParseError; Json,
use nom::multi::{many, many0}; Yaml,
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 as u16).to_be_bytes());
buf.extend(&fp.to_be_bytes());
buf
}
} }
impl From<&TxId> for u128 { impl OutputFormat {
fn from(value: &TxId) -> Self { fn format_stun(&self, msg: &StunMessage) -> String {
let mut padded = [0u8; 16]; match self {
padded[4..].copy_from_slice(&value.0); OutputFormat::Text => format!("{}", msg),
u128::from_be_bytes(padded) OutputFormat::Json => serde_json::to_string_pretty(msg).unwrap(),
OutputFormat::Yaml => serde_yaml::to_string(msg).unwrap(),
}
}
fn format_address(&self, a: &SocketAddr) -> String {
let a = match a {
SocketAddr::V4(_) => a.ip(),
SocketAddr::V6(v6) => {
if let Some(v4) = v6.ip().to_ipv4_mapped() {
IpAddr::V4(v4)
} else {
a.ip()
}
}
};
match self {
OutputFormat::Text => format!("{}", a),
OutputFormat::Json => serde_json::to_string_pretty(&a).unwrap(),
OutputFormat::Yaml => serde_yaml::to_string(&a).unwrap(),
}
} }
}
fn fingerprint(msg: &[u8]) -> u32 {
hash(msg) ^ 0x5354554e
} }
#[derive(clap::Parser)] #[derive(clap::Parser)]
@ -98,406 +47,139 @@ fn fingerprint(msg: &[u8]) -> u32 {
#[command(about = "Test a Tailscale derp node's stun service")] #[command(about = "Test a Tailscale derp node's stun service")]
struct Cli { struct Cli {
host: String, host: String,
port: Option<u16>, #[clap(default_value = "3478")]
#[clap(long, short, default_value_t = false)] port: u16,
debug: bool, #[clap(
short = '4',
conflicts_with = "v6_only",
default_value = "false",
help = "Only use IPv4"
)]
v4_only: bool,
#[clap(
short = '6',
conflicts_with = "v4_only",
default_value = "false",
help = "Only use IPv6"
)]
v6_only: bool,
#[clap(short, long, default_value = "text")]
format: OutputFormat,
#[clap(
short,
long,
default_value = "false",
help = "Only output the first mapped address & convert IPv6-mapped to IPv4"
)]
address_only: bool,
#[clap(
short,
long,
default_value = "5.0",
value_parser = parse_duration,
help = "Timeout in seconds"
)]
timeout: Duration,
#[clap(short, long, default_value = "0", help = "Number of retries")]
retries: usize,
#[command(flatten)]
verbose: clap_verbosity_flag::Verbosity,
} }
#[derive(Debug, Clone, Copy)] fn parse_duration(s: &str) -> Result<Duration, std::num::ParseFloatError> {
enum StunClass { let secs = s.parse()?;
Request = 0, Ok(Duration::from_secs_f64(secs))
Indication = 1,
SuccessResponse = 2,
ErrorResponse = 3,
} }
#[derive(Debug, Clone, Copy)] fn stun_query(
enum StunMethod { target: &SocketAddr,
Binding = 1, timeout: Duration,
} retries: usize,
) -> Result<StunMessage, std::io::Error> {
let socket = UdpSocket::bind("[::]:0").expect("Unable to bind a UDP socket");
socket
.connect(target)
.expect("Unable to connect to the destination");
socket
.set_read_timeout(Some(timeout))
.expect("Unable to set read timeout");
debug!(
"Connected UDP socket to {:?} from {:?}",
socket.peer_addr(),
socket.local_addr()
);
#[derive(Debug, Clone)] debug!("Building request packet");
enum StunAttribute { let req = tailstun::rand_request();
MappedAddress((IpAddr, u16)), let backoff = ExponentialBuilder::default().with_max_times(retries);
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 { debug!("request {:?}", &req);
match addr {
IpAddr::V4(_) => "IPv4",
IpAddr::V6(_) => "IPv6",
}
}
impl fmt::Display for StunAttribute { let mut buf = [0u8; 1500];
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let fetch = || {
match self { info!("Sending STUN request to {target} with timeout {timeout:?}");
StunAttribute::MappedAddress((addr, port)) => { socket.send(&req)?;
write!( socket.recv(&mut buf)
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 response = fetch
let method = match method_raw { .retry(backoff)
1 => StunMethod::Binding, .when(|r| r.kind() == std::io::ErrorKind::WouldBlock)
_ => panic!("Invalid STUN message method"), .notify(|_err, dur| info!("retrying after {:?}", dur))
}; .call();
Ok((bytes, StunMessageType { class, method }))
}
fn parse_stun_address<I, E: ParseError<I>>(bytes: I) -> IResult<I, (IpAddr, u16), E> match response {
where Ok(received) => {
I: nom::Input<Item = u8>, let buf = &buf[..received];
{ debug!("Received response: {:?}", buf);
let (bytes, _) = take(1usize)(bytes)?;
let (bytes, family) = be_u8(bytes)?; let msg = StunMessage::parse(buf).unwrap();
let (bytes, port) = be_u16(bytes)?; info!("Parsed message from {}:", socket.peer_addr().unwrap());
let (bytes, addr) = match family { Ok(msg)
0x01 => {
let (bytes, val) = be_u32(bytes)?;
(bytes, (IpAddr::V4(val.into()), port))
} }
0x02 => { Err(e) => Err(e),
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,
},
))
} }
fn main() { fn main() {
let cli = <Cli as clap::Parser>::parse(); 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.as_str(), cli.port)
let socket = UdpSocket::bind("[::]:0").expect("Unable to bind a UDP socket"); .to_socket_addrs()
socket .expect("Unable to resolve host")
.connect(dest) .find(|a| {
.expect("Unable to connect to the destination"); if cli.v4_only {
if cli.debug { a.is_ipv4()
println!( } else if cli.v6_only {
"Connected to {:?} from {:?}", a.is_ipv6()
socket.peer_addr(), } else {
socket.local_addr() true
); }
} })
.expect("No address found for host");
let req = TxId::new().make_request(); let msg = match stun_query(&dest, cli.timeout, cli.retries) {
if cli.debug { Ok(msg) => msg,
println!("Sending request {:?}", &req); Err(e) => {
} if e.kind() == std::io::ErrorKind::WouldBlock {
socket.send(&req).expect("Unable to send request"); error!("Timed out after all retries used, giving up.");
} else {
let mut buf = [0u8; 1500]; error!("Error: {:?}", e);
}
if let Ok(received) = socket.recv(&mut buf) { std::process::exit(1);
if cli.debug {
println!("Received response: {:?}", &buf[..received]);
} }
let (_residual, msg) = parse_stun_message::<nom::error::Error<&[u8]>>(&buf).unwrap(); };
println!("Parsed message from {}:", socket.peer_addr().unwrap()); if cli.address_only {
println!("{}", msg); match msg.attributes.mapped_address() {
} else if let Err(e) = socket.recv(&mut buf) { Some(addr) => println!("{}", cli.format.format_address(addr)),
println!("recv function failed: {e:?}"); None => {
// No mapped address
std::process::exit(1);
}
}
} else {
println!("{}", cli.format.format_stun(&msg));
} }
} }