Compare commits
No commits in common. "3ef27c0bed11975cd9957f7989ff7ac18d79aeba" and "84860feac470d0afdb694b6503cefdb8861656c8" have entirely different histories.
3ef27c0bed
...
84860feac4
337
Cargo.lock
generated
337
Cargo.lock
generated
@ -2,15 +2,6 @@
|
||||
# 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"
|
||||
@ -52,29 +43,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.8"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"once_cell",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backon"
|
||||
version = "1.5.0"
|
||||
name = "bitflags"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
]
|
||||
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@ -84,29 +72,19 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
version = "4.5.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap-verbosity-flag"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.38"
|
||||
version = "4.5.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@ -116,9 +94,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
version = "4.5.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@ -147,122 +125,41 @@ 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.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"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
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.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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"
|
||||
version = "0.2.169"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@ -280,67 +177,47 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
name = "once_cell"
|
||||
version = "1.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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",
|
||||
]
|
||||
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.1"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"zerocopy 0.8.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -355,91 +232,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.3"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||
checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[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",
|
||||
"zerocopy 0.8.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -450,9 +248,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
version = "2.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -463,30 +261,18 @@ dependencies = [
|
||||
name = "tailstun"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"backon",
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"crc32fast",
|
||||
"env_logger",
|
||||
"log",
|
||||
"nom",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
@ -496,9 +282,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
version = "0.13.3+wasi-0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
@ -578,27 +364,48 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
"byteorder",
|
||||
"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]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
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 = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -4,14 +4,8 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
backon = { version = "1.5.0", default-features = false, features = ["std", "std-blocking-sleep"] }
|
||||
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"
|
||||
|
@ -1,3 +1 @@
|
||||
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
551
src/lib.rs
@ -1,551 +0,0 @@
|
||||
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,
|
||||
},
|
||||
))
|
||||
}
|
620
src/main.rs
620
src/main.rs
@ -1,185 +1,503 @@
|
||||
use backon::BlockingRetryable;
|
||||
use backon::ExponentialBuilder;
|
||||
use clap::ValueEnum;
|
||||
use log::{debug, error, info};
|
||||
use std::{
|
||||
net::{IpAddr, SocketAddr, ToSocketAddrs, UdpSocket},
|
||||
time::Duration,
|
||||
};
|
||||
use tailstun::StunMessage;
|
||||
use clap;
|
||||
use std::fmt;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
use std::ops::Index;
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
enum OutputFormat {
|
||||
Text,
|
||||
Json,
|
||||
Yaml,
|
||||
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 as u16).to_be_bytes());
|
||||
buf.extend(&fp.to_be_bytes());
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
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, 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(),
|
||||
}
|
||||
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(clap::Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
#[command(about = "Test a Tailscale derp node's stun service")]
|
||||
struct Cli {
|
||||
host: String,
|
||||
#[clap(default_value = "3478")]
|
||||
port: u16,
|
||||
#[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,
|
||||
port: Option<u16>,
|
||||
#[clap(long, short, default_value_t = false)]
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
fn parse_duration(s: &str) -> Result<Duration, std::num::ParseFloatError> {
|
||||
let secs = s.parse()?;
|
||||
Ok(Duration::from_secs_f64(secs))
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StunClass {
|
||||
Request = 0,
|
||||
Indication = 1,
|
||||
SuccessResponse = 2,
|
||||
ErrorResponse = 3,
|
||||
}
|
||||
|
||||
fn stun_query(
|
||||
target: &SocketAddr,
|
||||
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, Copy)]
|
||||
enum StunMethod {
|
||||
Binding = 1,
|
||||
}
|
||||
|
||||
debug!("Building request packet");
|
||||
let req = tailstun::rand_request();
|
||||
let backoff = ExponentialBuilder::default().with_max_times(retries);
|
||||
#[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>)),
|
||||
}
|
||||
|
||||
debug!("request {:?}", &req);
|
||||
fn addr_family(addr: &IpAddr) -> &'static str {
|
||||
match addr {
|
||||
IpAddr::V4(_) => "IPv4",
|
||||
IpAddr::V6(_) => "IPv6",
|
||||
}
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 1500];
|
||||
let fetch = || {
|
||||
info!("Sending STUN request to {target} with timeout {timeout:?}");
|
||||
socket.send(&req)?;
|
||||
socket.recv(&mut buf)
|
||||
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 response = fetch
|
||||
.retry(backoff)
|
||||
.when(|r| r.kind() == std::io::ErrorKind::WouldBlock)
|
||||
.notify(|_err, dur| info!("retrying after {:?}", dur))
|
||||
.call();
|
||||
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 }))
|
||||
}
|
||||
|
||||
match response {
|
||||
Ok(received) => {
|
||||
let buf = &buf[..received];
|
||||
debug!("Received response: {:?}", buf);
|
||||
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))
|
||||
}
|
||||
|
||||
let msg = StunMessage::parse(buf).unwrap();
|
||||
info!("Parsed message from {}:", socket.peer_addr().unwrap());
|
||||
Ok(msg)
|
||||
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())
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
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() {
|
||||
let cli = <Cli as clap::Parser>::parse();
|
||||
env_logger::Builder::new()
|
||||
.filter_level(cli.verbose.log_level_filter())
|
||||
.init();
|
||||
|
||||
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
|
||||
let dest = cli.host + ":" + &cli.port.unwrap_or(3478).to_string();
|
||||
let socket = UdpSocket::bind("[::]:0").expect("Unable to bind a UDP socket");
|
||||
socket
|
||||
.connect(dest)
|
||||
.expect("Unable to connect to the destination");
|
||||
if cli.debug {
|
||||
println!(
|
||||
"Connected to {:?} from {:?}",
|
||||
socket.peer_addr(),
|
||||
socket.local_addr()
|
||||
);
|
||||
}
|
||||
})
|
||||
.expect("No address found for host");
|
||||
|
||||
let msg = match stun_query(&dest, cli.timeout, cli.retries) {
|
||||
Ok(msg) => msg,
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::WouldBlock {
|
||||
error!("Timed out after all retries used, giving up.");
|
||||
} else {
|
||||
error!("Error: {:?}", e);
|
||||
let req = TxId::new().make_request();
|
||||
if cli.debug {
|
||||
println!("Sending request {:?}", &req);
|
||||
}
|
||||
std::process::exit(1);
|
||||
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]);
|
||||
}
|
||||
};
|
||||
if cli.address_only {
|
||||
match msg.attributes.mapped_address() {
|
||||
Some(addr) => println!("{}", cli.format.format_address(addr)),
|
||||
None => {
|
||||
// No mapped address
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user