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. | ||||
| 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" | ||||
| @@ -43,26 +52,29 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "anstyle-wincon" | ||||
| version = "3.0.7" | ||||
| version = "3.0.8" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" | ||||
| checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" | ||||
| dependencies = [ | ||||
|  "anstyle", | ||||
|  "once_cell", | ||||
|  "once_cell_polyfill", | ||||
|  "windows-sys", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "bitflags" | ||||
| version = "2.8.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" | ||||
|  | ||||
| [[package]] | ||||
| name = "byteorder" | ||||
| name = "backon" | ||||
| version = "1.5.0" | ||||
| 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]] | ||||
| name = "cfg-if" | ||||
| @@ -72,19 +84,29 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" | ||||
|  | ||||
| [[package]] | ||||
| name = "clap" | ||||
| version = "4.5.29" | ||||
| version = "4.5.38" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" | ||||
| checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" | ||||
| dependencies = [ | ||||
|  "clap_builder", | ||||
|  "clap_derive", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "clap_builder" | ||||
| version = "4.5.29" | ||||
| name = "clap-verbosity-flag" | ||||
| version = "3.0.3" | ||||
| 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 = [ | ||||
|  "anstream", | ||||
|  "anstyle", | ||||
| @@ -94,9 +116,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "clap_derive" | ||||
| version = "4.5.28" | ||||
| version = "4.5.32" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" | ||||
| checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" | ||||
| dependencies = [ | ||||
|  "heck", | ||||
|  "proc-macro2", | ||||
| @@ -126,23 +148,74 @@ dependencies = [ | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "getrandom" | ||||
| version = "0.3.1" | ||||
| name = "env_filter" | ||||
| version = "0.1.3" | ||||
| 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 = [ | ||||
|  "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" | ||||
| @@ -150,16 +223,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" | ||||
|  | ||||
| [[package]] | ||||
| name = "libc" | ||||
| version = "0.2.169" | ||||
| name = "itoa" | ||||
| version = "1.0.15" | ||||
| 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]] | ||||
| name = "log" | ||||
| version = "0.4.25" | ||||
| version = "0.4.27" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" | ||||
| checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" | ||||
|  | ||||
| [[package]] | ||||
| name = "memchr" | ||||
| @@ -177,47 +280,67 @@ dependencies = [ | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "once_cell" | ||||
| version = "1.20.3" | ||||
| name = "once_cell_polyfill" | ||||
| version = "1.70.1" | ||||
| 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]] | ||||
| name = "ppv-lite86" | ||||
| version = "0.2.20" | ||||
| version = "0.2.21" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" | ||||
| checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" | ||||
| dependencies = [ | ||||
|  "zerocopy 0.7.35", | ||||
|  "zerocopy", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "proc-macro2" | ||||
| version = "1.0.93" | ||||
| version = "1.0.95" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" | ||||
| checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" | ||||
| dependencies = [ | ||||
|  "unicode-ident", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "quote" | ||||
| version = "1.0.38" | ||||
| version = "1.0.40" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" | ||||
| checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rand" | ||||
| version = "0.9.0" | ||||
| name = "r-efi" | ||||
| version = "5.2.0" | ||||
| 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 = [ | ||||
|  "rand_chacha", | ||||
|  "rand_core", | ||||
|  "zerocopy 0.8.17", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -232,12 +355,91 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "rand_core" | ||||
| version = "0.9.0" | ||||
| version = "0.9.3" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" | ||||
| checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" | ||||
| dependencies = [ | ||||
|  "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]] | ||||
| @@ -248,9 +450,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | ||||
|  | ||||
| [[package]] | ||||
| name = "syn" | ||||
| version = "2.0.98" | ||||
| version = "2.0.101" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" | ||||
| checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
| @@ -261,18 +463,30 @@ 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.16" | ||||
| version = "1.0.18" | ||||
| 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]] | ||||
| name = "utf8parse" | ||||
| @@ -282,9 +496,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | ||||
|  | ||||
| [[package]] | ||||
| 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" | ||||
| checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" | ||||
| checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" | ||||
| dependencies = [ | ||||
|  "wit-bindgen-rt", | ||||
| ] | ||||
| @@ -364,48 +578,27 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" | ||||
|  | ||||
| [[package]] | ||||
| name = "wit-bindgen-rt" | ||||
| version = "0.33.0" | ||||
| version = "0.39.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" | ||||
| checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" | ||||
| dependencies = [ | ||||
|  "bitflags", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "zerocopy" | ||||
| version = "0.7.35" | ||||
| version = "0.8.25" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" | ||||
| checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" | ||||
| dependencies = [ | ||||
|  "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", | ||||
|  "zerocopy-derive", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "zerocopy-derive" | ||||
| version = "0.7.35" | ||||
| version = "0.8.25" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| 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" | ||||
| checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" | ||||
| dependencies = [ | ||||
|  "proc-macro2", | ||||
|  "quote", | ||||
|   | ||||
| @@ -4,8 +4,14 @@ 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 +1,3 @@ | ||||
| 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, | ||||
|         }, | ||||
|     )) | ||||
| } | ||||
							
								
								
									
										626
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										626
									
								
								src/main.rs
									
									
									
									
									
								
							| @@ -1,96 +1,45 @@ | ||||
| use clap; | ||||
| use std::fmt; | ||||
| use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; | ||||
| use std::ops::Index; | ||||
| 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 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 | ||||
|     } | ||||
| #[derive(Debug, Clone, ValueEnum)] | ||||
| enum OutputFormat { | ||||
|     Text, | ||||
|     Json, | ||||
|     Yaml, | ||||
| } | ||||
|  | ||||
| impl From<&TxId> for u128 { | ||||
|     fn from(value: &TxId) -> Self { | ||||
|         let mut padded = [0u8; 16]; | ||||
|         padded[4..].copy_from_slice(&value.0); | ||||
|         u128::from_be_bytes(padded) | ||||
| impl OutputFormat { | ||||
|     fn format_stun(&self, msg: &StunMessage) -> String { | ||||
|         match self { | ||||
|             OutputFormat::Text => format!("{}", msg), | ||||
|             OutputFormat::Json => serde_json::to_string_pretty(msg).unwrap(), | ||||
|             OutputFormat::Yaml => serde_yaml::to_string(msg).unwrap(), | ||||
|         } | ||||
|     } | ||||
|     fn format_address(&self, 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)] | ||||
| @@ -98,406 +47,139 @@ fn fingerprint(msg: &[u8]) -> u32 { | ||||
| #[command(about = "Test a Tailscale derp node's stun service")] | ||||
| struct Cli { | ||||
|     host: String, | ||||
|     port: Option<u16>, | ||||
|     #[clap(long, short, default_value_t = false)] | ||||
|     debug: bool, | ||||
|     #[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, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| enum StunClass { | ||||
|     Request = 0, | ||||
|     Indication = 1, | ||||
|     SuccessResponse = 2, | ||||
|     ErrorResponse = 3, | ||||
| 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 StunMethod { | ||||
|     Binding = 1, | ||||
| } | ||||
| 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)] | ||||
| 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!("Building request packet"); | ||||
|     let req = tailstun::rand_request(); | ||||
|     let backoff = ExponentialBuilder::default().with_max_times(retries); | ||||
|  | ||||
| fn addr_family(addr: &IpAddr) -> &'static str { | ||||
|     match addr { | ||||
|         IpAddr::V4(_) => "IPv4", | ||||
|         IpAddr::V6(_) => "IPv6", | ||||
|     } | ||||
| } | ||||
|     debug!("request {:?}", &req); | ||||
|  | ||||
| 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 mut buf = [0u8; 1500]; | ||||
|     let fetch = || { | ||||
|         info!("Sending STUN request to {target} with timeout {timeout:?}"); | ||||
|         socket.send(&req)?; | ||||
|         socket.recv(&mut buf) | ||||
|     }; | ||||
|     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 })) | ||||
| } | ||||
|     let response = fetch | ||||
|         .retry(backoff) | ||||
|         .when(|r| r.kind() == std::io::ErrorKind::WouldBlock) | ||||
|         .notify(|_err, dur| info!("retrying after {:?}", dur)) | ||||
|         .call(); | ||||
|  | ||||
| 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)) | ||||
|     match response { | ||||
|         Ok(received) => { | ||||
|             let buf = &buf[..received]; | ||||
|             debug!("Received response: {:?}", buf); | ||||
|  | ||||
|             let msg = StunMessage::parse(buf).unwrap(); | ||||
|             info!("Parsed message from {}:", socket.peer_addr().unwrap()); | ||||
|             Ok(msg) | ||||
|         } | ||||
|         0x02 => { | ||||
|             let (bytes, val) = be_u128(bytes)?; | ||||
|             (bytes, (IpAddr::V6(val.into()), port)) | ||||
|         } | ||||
|         _ => panic!("Invalid address family"), | ||||
|     }; | ||||
|     Ok((bytes, addr)) | ||||
| } | ||||
|  | ||||
| fn parse_stun_xor_address<I, E: ParseError<I>>( | ||||
|     bytes: I, | ||||
|     tx_id: &TxId, | ||||
| ) -> IResult<I, (IpAddr, u16), E> | ||||
| where | ||||
|     I: nom::Input<Item = u8>, | ||||
| { | ||||
|     let (bytes, addr) = parse_stun_address(bytes)?; | ||||
|     let xor_port = addr.1 ^ 0x2112; | ||||
|     let xor_addr = match addr.0 { | ||||
|         IpAddr::V4(v4) => { | ||||
|             let v4 = u32::from(v4); | ||||
|             let xor_v4 = v4 ^ 0x2112a442; | ||||
|             IpAddr::V4(xor_v4.into()) | ||||
|         } | ||||
|         IpAddr::V6(v6) => { | ||||
|             let v6 = u128::from(v6); | ||||
|             let xor_v6: u128 = v6 ^ (0x2112a442 << 96 | u128::from(tx_id)); | ||||
|             IpAddr::V6(xor_v6.into()) | ||||
|         } | ||||
|     }; | ||||
|     Ok((bytes, (xor_addr, xor_port))) | ||||
| } | ||||
|  | ||||
| fn parse_stun_attribute<I, E: ParseError<I>>( | ||||
|     tx_id: &TxId, | ||||
| ) -> impl Fn(I) -> IResult<I, Option<StunAttribute>, E> | ||||
| where | ||||
|     I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes, | ||||
| { | ||||
|     let tx_id = tx_id.clone(); | ||||
|     move |bytes| parse_stun_attribute_impl(bytes, &tx_id) | ||||
| } | ||||
|  | ||||
| fn parse_stun_attribute_impl<I, E: ParseError<I>>( | ||||
|     bytes: I, | ||||
|     tx_id: &TxId, | ||||
| ) -> IResult<I, Option<StunAttribute>, E> | ||||
| where | ||||
|     I: nom::Input<Item = u8> + nom::Compare<I> + AsBytes, | ||||
| { | ||||
|     let (bytes, attr_type) = be_u16(bytes)?; | ||||
|     let (bytes, attr_len) = be_u16(bytes)?; | ||||
|     let (bytes, attr_data) = take(attr_len)(bytes)?; | ||||
|  | ||||
|     if attr_len == 0 { | ||||
|         return Ok((bytes, None)); | ||||
|         Err(e) => Err(e), | ||||
|     } | ||||
|  | ||||
|     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 + ":" + &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() | ||||
|         ); | ||||
|     } | ||||
|     let dest = (cli.host.as_str(), cli.port) | ||||
|         .to_socket_addrs() | ||||
|         .expect("Unable to resolve host") | ||||
|         .find(|a| { | ||||
|             if cli.v4_only { | ||||
|                 a.is_ipv4() | ||||
|             } else if cli.v6_only { | ||||
|                 a.is_ipv6() | ||||
|             } else { | ||||
|                 true | ||||
|             } | ||||
|         }) | ||||
|         .expect("No address found for host"); | ||||
|  | ||||
|     let req = TxId::new().make_request(); | ||||
|     if cli.debug { | ||||
|         println!("Sending request {:?}", &req); | ||||
|     } | ||||
|     socket.send(&req).expect("Unable to send request"); | ||||
|  | ||||
|     let mut buf = [0u8; 1500]; | ||||
|  | ||||
|     if let Ok(received) = socket.recv(&mut buf) { | ||||
|         if cli.debug { | ||||
|             println!("Received response: {:?}", &buf[..received]); | ||||
|     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); | ||||
|             } | ||||
|             std::process::exit(1); | ||||
|         } | ||||
|         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:?}"); | ||||
|     }; | ||||
|     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)); | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user