Compare commits
10 Commits
84860feac4
...
3ef27c0bed
Author | SHA1 | Date | |
---|---|---|---|
|
3ef27c0bed | ||
|
b92c9f5503 | ||
|
1560535fbe | ||
|
286b21e940 | ||
|
1488dba92d | ||
|
a66a6a263e | ||
|
6985ed6614 | ||
|
903bf23646 | ||
|
a3e944d1d6 | ||
|
6dacc3a52f |
349
Cargo.lock
generated
349
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"
|
||||||
|
@ -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
551
src/lib.rs
Normal 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,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
620
src/main.rs
620
src/main.rs
@ -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 (bytes, port) = be_u16(bytes)?;
|
|
||||||
let (bytes, addr) = match family {
|
|
||||||
0x01 => {
|
|
||||||
let (bytes, val) = be_u32(bytes)?;
|
|
||||||
(bytes, (IpAddr::V4(val.into()), port))
|
|
||||||
}
|
|
||||||
0x02 => {
|
|
||||||
let (bytes, val) = be_u128(bytes)?;
|
|
||||||
(bytes, (IpAddr::V6(val.into()), port))
|
|
||||||
}
|
|
||||||
_ => panic!("Invalid address family"),
|
|
||||||
};
|
|
||||||
Ok((bytes, addr))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_stun_xor_address<I, E: ParseError<I>>(
|
let msg = StunMessage::parse(buf).unwrap();
|
||||||
bytes: I,
|
info!("Parsed message from {}:", socket.peer_addr().unwrap());
|
||||||
tx_id: &TxId,
|
Ok(msg)
|
||||||
) -> 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) => {
|
Err(e) => Err(e),
|
||||||
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 {
|
||||||
|
error!("Timed out after all retries used, giving up.");
|
||||||
|
} else {
|
||||||
|
error!("Error: {:?}", e);
|
||||||
}
|
}
|
||||||
socket.send(&req).expect("Unable to send request");
|
std::process::exit(1);
|
||||||
|
|
||||||
let mut buf = [0u8; 1500];
|
|
||||||
|
|
||||||
if let Ok(received) = socket.recv(&mut buf) {
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user