From 09202c4909eb8bf8523c87012e5664d7432364d5 Mon Sep 17 00:00:00 2001 From: Ale Paredes <1709578+ale7714@users.noreply.github.com> Date: Thu, 19 Mar 2026 12:19:12 -0400 Subject: [PATCH] Fix TURN CreatePermission panic caused by stun ErrorCodeAttribute Display impl The stun crate's ErrorCodeAttribute::Display implementation panics when the error reason contains non-UTF-8 bytes, because format!() panics when a Display impl returns Err(fmt::Error). This patch uses from_utf8_lossy() instead, which surfaces the actual TURN error gracefully rather than panicking. This fixes WebRTC connection failures where the TURN relay setup panic killed the connection attempt, resulting in "unknown service viam.robot.v1.RobotService" errors on fallback. Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 260 +++--- Cargo.toml | 3 + stun-patch/.gitignore | 11 + stun-patch/Cargo.toml | 108 +++ stun-patch/LICENSE-APACHE | 201 +++++ stun-patch/LICENSE-MIT | 21 + stun-patch/src/addr.rs | 128 +++ stun-patch/src/addr/addr_test.rs | 183 +++++ stun-patch/src/agent.rs | 283 +++++++ stun-patch/src/agent/agent_test.rs | 195 +++++ stun-patch/src/attributes.rs | 209 +++++ stun-patch/src/attributes/attributes_test.rs | 86 ++ stun-patch/src/checks.rs | 48 ++ stun-patch/src/client.rs | 473 +++++++++++ stun-patch/src/client/client_test.rs | 12 + stun-patch/src/error.rs | 98 +++ stun-patch/src/error_code.rs | 158 ++++ stun-patch/src/fingerprint.rs | 64 ++ .../src/fingerprint/fingerprint_test.rs | 73 ++ stun-patch/src/integrity.rs | 118 +++ stun-patch/src/integrity/integrity_test.rs | 93 +++ stun-patch/src/lib.rs | 26 + stun-patch/src/message.rs | 626 +++++++++++++++ stun-patch/src/message/message_test.rs | 744 ++++++++++++++++++ stun-patch/src/textattrs.rs | 95 +++ stun-patch/src/textattrs/textattrs_test.rs | 307 ++++++++ stun-patch/src/uattrs.rs | 62 ++ stun-patch/src/uattrs/uattrs_test.rs | 37 + stun-patch/src/uri.rs | 73 ++ stun-patch/src/uri/uri_test.rs | 68 ++ stun-patch/src/xoraddr.rs | 173 ++++ stun-patch/src/xoraddr/xoraddr_test.rs | 250 ++++++ 32 files changed, 5124 insertions(+), 162 deletions(-) create mode 100644 stun-patch/.gitignore create mode 100644 stun-patch/Cargo.toml create mode 100644 stun-patch/LICENSE-APACHE create mode 100644 stun-patch/LICENSE-MIT create mode 100644 stun-patch/src/addr.rs create mode 100644 stun-patch/src/addr/addr_test.rs create mode 100644 stun-patch/src/agent.rs create mode 100644 stun-patch/src/agent/agent_test.rs create mode 100644 stun-patch/src/attributes.rs create mode 100644 stun-patch/src/attributes/attributes_test.rs create mode 100644 stun-patch/src/checks.rs create mode 100644 stun-patch/src/client.rs create mode 100644 stun-patch/src/client/client_test.rs create mode 100644 stun-patch/src/error.rs create mode 100644 stun-patch/src/error_code.rs create mode 100644 stun-patch/src/fingerprint.rs create mode 100644 stun-patch/src/fingerprint/fingerprint_test.rs create mode 100644 stun-patch/src/integrity.rs create mode 100644 stun-patch/src/integrity/integrity_test.rs create mode 100644 stun-patch/src/lib.rs create mode 100644 stun-patch/src/message.rs create mode 100644 stun-patch/src/message/message_test.rs create mode 100644 stun-patch/src/textattrs.rs create mode 100644 stun-patch/src/textattrs/textattrs_test.rs create mode 100644 stun-patch/src/uattrs.rs create mode 100644 stun-patch/src/uattrs/uattrs_test.rs create mode 100644 stun-patch/src/uri.rs create mode 100644 stun-patch/src/uri/uri_test.rs create mode 100644 stun-patch/src/xoraddr.rs create mode 100644 stun-patch/src/xoraddr/xoraddr_test.rs diff --git a/Cargo.lock b/Cargo.lock index 4bf4a13..a418675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -78,15 +78,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -606,9 +606,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -663,9 +663,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -673,9 +673,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -685,9 +685,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -697,15 +697,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "concurrent-queue" @@ -1287,19 +1287,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if 1.0.4", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if 1.0.4", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -1731,9 +1731,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "is_terminal_polyfill" @@ -1758,9 +1758,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1789,9 +1789,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "linux-raw-sys" @@ -2131,9 +2131,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2258,18 +2258,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -2278,9 +2278,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2290,9 +2290,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2489,9 +2489,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2502,6 +2502,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.5" @@ -2615,9 +2621,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rfc6979" @@ -2779,9 +2785,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring 0.17.14", @@ -2875,9 +2881,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3137,12 +3143,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3176,8 +3182,6 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "stun" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea256fb46a13f9204e9dee9982997b2c3097db175a9fddaa8350310d03c4d5a3" dependencies = [ "base64 0.22.1", "crc", @@ -3260,12 +3264,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -3382,9 +3386,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3392,7 +3396,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -3409,9 +3413,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3475,7 +3479,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -3489,18 +3493,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tonic" @@ -3650,9 +3654,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3789,11 +3793,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -3924,9 +3928,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -3937,9 +3941,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.61" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if 1.0.4", "futures-util", @@ -3951,9 +3955,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3961,9 +3965,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3974,9 +3978,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -4017,9 +4021,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -4084,7 +4088,7 @@ dependencies = [ "ring 0.17.14", "rtcp 0.12.0", "rtp 0.12.0", - "rustls 0.23.36", + "rustls 0.23.37", "sdp", "serde", "serde_json", @@ -4146,7 +4150,7 @@ dependencies = [ "rand_core 0.6.4", "rcgen", "ring 0.17.14", - "rustls 0.23.36", + "rustls 0.23.37", "sec1", "serde", "sha1", @@ -4455,15 +4459,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -4497,30 +4492,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -4533,12 +4511,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -4551,12 +4523,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -4569,24 +4535,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -4599,12 +4553,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -4617,12 +4565,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -4635,12 +4577,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -4654,16 +4590,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "winnow" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" [[package]] name = "winnow" -version = "0.7.14" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" @@ -4823,18 +4759,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "5c5030500cb2d66bdfbb4ebc9563be6ce7005a4b5d0f26be0c523870fe372ca6" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "a5f86989a046a79640b9d8867c823349a139367bda96549794fcc3313ce91f4e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 4f50329..85fdf6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,3 +68,6 @@ env_logger = "0.9.0" [build-dependencies] tonic-build = {version = "0.9.2",features = ["prost"]} cbindgen = "0.29.2" + +[patch.crates-io] +stun = { path = "stun-patch" } diff --git a/stun-patch/.gitignore b/stun-patch/.gitignore new file mode 100644 index 0000000..81561ed --- /dev/null +++ b/stun-patch/.gitignore @@ -0,0 +1,11 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/.idea/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/stun-patch/Cargo.toml b/stun-patch/Cargo.toml new file mode 100644 index 0000000..50b7df3 --- /dev/null +++ b/stun-patch/Cargo.toml @@ -0,0 +1,108 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "stun" +version = "0.7.0" +authors = ["Rain Liu "] +build = false +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "A pure Rust implementation of STUN" +homepage = "https://webrtc.rs" +documentation = "https://docs.rs/stun" +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/webrtc-rs/webrtc/tree/master/stun" + +[lib] +name = "stun" +path = "src/lib.rs" + +[[example]] +name = "stun_client" +path = "examples/stun_client.rs" +bench = false + +[[example]] +name = "stun_decode" +path = "examples/stun_decode.rs" +bench = false + +[[bench]] +name = "bench" +path = "benches/bench.rs" +harness = false + +[dependencies.base64] +version = "0.22.1" + +[dependencies.crc] +version = "3" + +[dependencies.lazy_static] +version = "1" + +[dependencies.md-5] +version = "0.10" + +[dependencies.rand] +version = "0.8" + +[dependencies.ring] +version = "0.17" + +[dependencies.subtle] +version = "2.4" + +[dependencies.thiserror] +version = "1" + +[dependencies.tokio] +version = "1.32.0" +features = [ + "fs", + "io-util", + "io-std", + "macros", + "net", + "parking_lot", + "rt", + "rt-multi-thread", + "sync", + "time", +] + +[dependencies.url] +version = "2" + +[dependencies.util] +version = "0.10.0" +features = ["conn"] +default-features = false +package = "webrtc-util" + +[dev-dependencies.clap] +version = "3" + +[dev-dependencies.criterion] +version = "0.5" + +[dev-dependencies.tokio-test] +version = "0.4" + +[features] +bench = [] +default = [] diff --git a/stun-patch/LICENSE-APACHE b/stun-patch/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/stun-patch/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/stun-patch/LICENSE-MIT b/stun-patch/LICENSE-MIT new file mode 100644 index 0000000..e11d93b --- /dev/null +++ b/stun-patch/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 WebRTC.rs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/stun-patch/src/addr.rs b/stun-patch/src/addr.rs new file mode 100644 index 0000000..cafe609 --- /dev/null +++ b/stun-patch/src/addr.rs @@ -0,0 +1,128 @@ +#[cfg(test)] +mod addr_test; + +use std::fmt; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use crate::attributes::*; +use crate::error::*; +use crate::message::*; + +pub(crate) const FAMILY_IPV4: u16 = 0x01; +pub(crate) const FAMILY_IPV6: u16 = 0x02; +pub(crate) const IPV4LEN: usize = 4; +pub(crate) const IPV6LEN: usize = 16; + +/// MappedAddress represents MAPPED-ADDRESS attribute. +/// +/// This attribute is used only by servers for achieving backwards +/// compatibility with RFC 3489 clients. +/// +/// RFC 5389 Section 15.1 +pub struct MappedAddress { + pub ip: IpAddr, + pub port: u16, +} + +impl fmt::Display for MappedAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let family = match self.ip { + IpAddr::V4(_) => FAMILY_IPV4, + IpAddr::V6(_) => FAMILY_IPV6, + }; + if family == FAMILY_IPV4 { + write!(f, "{}:{}", self.ip, self.port) + } else { + write!(f, "[{}]:{}", self.ip, self.port) + } + } +} + +impl Default for MappedAddress { + fn default() -> Self { + MappedAddress { + ip: IpAddr::V4(Ipv4Addr::from(0)), + port: 0, + } + } +} + +impl Setter for MappedAddress { + /// add_to adds MAPPED-ADDRESS to message. + fn add_to(&self, m: &mut Message) -> Result<()> { + self.add_to_as(m, ATTR_MAPPED_ADDRESS) + } +} + +impl Getter for MappedAddress { + /// get_from decodes MAPPED-ADDRESS from message. + fn get_from(&mut self, m: &Message) -> Result<()> { + self.get_from_as(m, ATTR_MAPPED_ADDRESS) + } +} + +impl MappedAddress { + /// get_from_as decodes MAPPED-ADDRESS value in message m as an attribute of type t. + pub fn get_from_as(&mut self, m: &Message, t: AttrType) -> Result<()> { + let v = m.get(t)?; + if v.len() <= 4 { + return Err(Error::ErrUnexpectedEof); + } + + let family = u16::from_be_bytes([v[0], v[1]]); + if family != FAMILY_IPV6 && family != FAMILY_IPV4 { + return Err(Error::Other(format!("bad value {family}"))); + } + self.port = u16::from_be_bytes([v[2], v[3]]); + + if family == FAMILY_IPV6 { + let mut ip = [0; IPV6LEN]; + let l = std::cmp::min(ip.len(), v[4..].len()); + ip[..l].copy_from_slice(&v[4..4 + l]); + self.ip = IpAddr::V6(Ipv6Addr::from(ip)); + } else { + let mut ip = [0; IPV4LEN]; + let l = std::cmp::min(ip.len(), v[4..].len()); + ip[..l].copy_from_slice(&v[4..4 + l]); + self.ip = IpAddr::V4(Ipv4Addr::from(ip)); + }; + + Ok(()) + } + + /// add_to_as adds MAPPED-ADDRESS value to m as t attribute. + pub fn add_to_as(&self, m: &mut Message, t: AttrType) -> Result<()> { + let family = match self.ip { + IpAddr::V4(_) => FAMILY_IPV4, + IpAddr::V6(_) => FAMILY_IPV6, + }; + + let mut value = vec![0u8; 4]; + //value[0] = 0 // first 8 bits are zeroes + value[0..2].copy_from_slice(&family.to_be_bytes()); + value[2..4].copy_from_slice(&self.port.to_be_bytes()); + + match self.ip { + IpAddr::V4(ipv4) => value.extend_from_slice(&ipv4.octets()), + IpAddr::V6(ipv6) => value.extend_from_slice(&ipv6.octets()), + }; + + m.add(t, &value); + Ok(()) + } +} + +/// AlternateServer represents ALTERNATE-SERVER attribute. +/// +/// RFC 5389 Section 15.11 +pub type AlternateServer = MappedAddress; + +/// ResponseOrigin represents RESPONSE-ORIGIN attribute. +/// +/// RFC 5780 Section 7.3 +pub type ResponseOrigin = MappedAddress; + +/// OtherAddress represents OTHER-ADDRESS attribute. +/// +/// RFC 5780 Section 7.4 +pub type OtherAddress = MappedAddress; diff --git a/stun-patch/src/addr/addr_test.rs b/stun-patch/src/addr/addr_test.rs new file mode 100644 index 0000000..77f5ac6 --- /dev/null +++ b/stun-patch/src/addr/addr_test.rs @@ -0,0 +1,183 @@ +use super::*; +use crate::error::*; + +#[test] +fn test_mapped_address() -> Result<()> { + let mut m = Message::new(); + let addr = MappedAddress { + ip: "122.12.34.5".parse().unwrap(), + port: 5412, + }; + assert_eq!(addr.to_string(), "122.12.34.5:5412", "bad string {addr}"); + + //"add_to" + { + addr.add_to(&mut m)?; + + //"GetFrom" + { + let mut got = MappedAddress::default(); + got.get_from(&m)?; + assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); + + //"Not found" + { + let message = Message::new(); + let result = got.get_from(&message); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "should be not found: {err}" + ); + } else { + panic!("expected error, but got ok"); + } + } + //"Bad family" + { + let (mut v, _) = m.attributes.get(ATTR_MAPPED_ADDRESS); + v.value[0] = 32; + got.get_from(&m)? + } + //"Bad length" + { + let mut message = Message::new(); + message.add(ATTR_MAPPED_ADDRESS, &[1, 2, 3]); + let result = got.get_from(&message); + if let Err(err) = result { + assert_eq!( + Error::ErrUnexpectedEof, + err, + "<{}> should be <{}>", + err, + Error::ErrUnexpectedEof + ); + } else { + panic!("expected error, but got ok"); + } + } + } + } + + Ok(()) +} + +#[test] +fn test_mapped_address_v6() -> Result<()> { + let mut m = Message::new(); + let addr = MappedAddress { + ip: "::".parse().unwrap(), + port: 5412, + }; + + //"add_to" + { + addr.add_to(&mut m)?; + + //"GetFrom" + { + let mut got = MappedAddress::default(); + got.get_from(&m)?; + assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); + + //"Not found" + { + let message = Message::new(); + let result = got.get_from(&message); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "<{}> should be <{}>", + err, + Error::ErrAttributeNotFound, + ); + } else { + panic!("expected error, but got ok"); + } + } + } + } + Ok(()) +} + +#[test] +fn test_alternate_server() -> Result<()> { + let mut m = Message::new(); + let addr = MappedAddress { + ip: "122.12.34.5".parse().unwrap(), + port: 5412, + }; + + //"add_to" + { + addr.add_to(&mut m)?; + + //"GetFrom" + { + let mut got = AlternateServer::default(); + got.get_from(&m)?; + assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); + + //"Not found" + { + let message = Message::new(); + let result = got.get_from(&message); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "<{}> should be <{}>", + err, + Error::ErrAttributeNotFound, + ); + } else { + panic!("expected error, but got ok"); + } + } + } + } + + Ok(()) +} + +#[test] +fn test_other_address() -> Result<()> { + let mut m = Message::new(); + let addr = OtherAddress { + ip: "122.12.34.5".parse().unwrap(), + port: 5412, + }; + + //"add_to" + { + addr.add_to(&mut m)?; + + //"GetFrom" + { + let mut got = OtherAddress::default(); + got.get_from(&m)?; + assert_eq!(got.ip, addr.ip, "got bad IP: {}", got.ip); + + //"Not found" + { + let message = Message::new(); + let result = got.get_from(&message); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "<{}> should be <{}>", + err, + Error::ErrAttributeNotFound, + ); + } else { + panic!("expected error, but got ok"); + } + } + } + } + + Ok(()) +} diff --git a/stun-patch/src/agent.rs b/stun-patch/src/agent.rs new file mode 100644 index 0000000..4562df7 --- /dev/null +++ b/stun-patch/src/agent.rs @@ -0,0 +1,283 @@ +#[cfg(test)] +mod agent_test; + +use std::collections::HashMap; +use std::sync::Arc; + +use rand::Rng; +use tokio::sync::mpsc; +use tokio::time::Instant; + +use crate::client::ClientTransaction; +use crate::error::*; +use crate::message::*; + +/// Handler handles state changes of transaction. +/// Handler is called on transaction state change. +/// Usage of e is valid only during call, user must +/// copy needed fields explicitly. +pub type Handler = Option>>; + +/// noop_handler just discards any event. +pub fn noop_handler() -> Handler { + None +} + +/// Agent is low-level abstraction over transaction list that +/// handles concurrency (all calls are goroutine-safe) and +/// time outs (via Collect call). +pub struct Agent { + /// transactions is map of transactions that are currently + /// in progress. Event handling is done in such way when + /// transaction is unregistered before AgentTransaction access, + /// minimizing mux lock and protecting AgentTransaction from + /// data races via unexpected concurrent access. + transactions: HashMap, + /// all calls are invalid if true + closed: bool, + /// handles transactions + handler: Handler, +} + +#[derive(Debug, Clone)] +pub enum EventType { + Callback(TransactionId), + Insert(ClientTransaction), + Remove(TransactionId), + Close, +} + +impl Default for EventType { + fn default() -> Self { + EventType::Callback(TransactionId::default()) + } +} + +/// Event is passed to Handler describing the transaction event. +/// Do not reuse outside Handler. +#[derive(Debug)] //Clone +pub struct Event { + pub event_type: EventType, + pub event_body: Result, +} + +impl Default for Event { + fn default() -> Self { + Event { + event_type: EventType::default(), + event_body: Ok(Message::default()), + } + } +} + +/// AgentTransaction represents transaction in progress. +/// Concurrent access is invalid. +pub(crate) struct AgentTransaction { + id: TransactionId, + deadline: Instant, +} + +/// AGENT_COLLECT_CAP is initial capacity for Agent.Collect slices, +/// sufficient to make function zero-alloc in most cases. +const AGENT_COLLECT_CAP: usize = 100; + +#[derive(PartialEq, Eq, Hash, Copy, Clone, Default, Debug)] +pub struct TransactionId(pub [u8; TRANSACTION_ID_SIZE]); + +impl TransactionId { + /// new returns new random transaction ID using crypto/rand + /// as source. + pub fn new() -> Self { + let mut b = TransactionId([0u8; TRANSACTION_ID_SIZE]); + rand::thread_rng().fill(&mut b.0); + b + } +} + +impl Setter for TransactionId { + fn add_to(&self, m: &mut Message) -> Result<()> { + m.transaction_id = *self; + m.write_transaction_id(); + Ok(()) + } +} + +/// ClientAgent is Agent implementation that is used by Client to +/// process transactions. +#[derive(Debug)] +pub enum ClientAgent { + Process(Message), + Collect(Instant), + Start(TransactionId, Instant), + Stop(TransactionId), + Close, +} + +impl Agent { + /// new initializes and returns new Agent with provided handler. + pub fn new(handler: Handler) -> Self { + Agent { + transactions: HashMap::new(), + closed: false, + handler, + } + } + + /// stop_with_error removes transaction from list and calls handler with + /// provided error. Can return ErrTransactionNotExists and ErrAgentClosed. + pub fn stop_with_error(&mut self, id: TransactionId, error: Error) -> Result<()> { + if self.closed { + return Err(Error::ErrAgentClosed); + } + + let v = self.transactions.remove(&id); + if let Some(t) = v { + if let Some(handler) = &self.handler { + handler.send(Event { + event_type: EventType::Callback(t.id), + event_body: Err(error), + })?; + } + Ok(()) + } else { + Err(Error::ErrTransactionNotExists) + } + } + + /// process incoming message, synchronously passing it to handler. + pub fn process(&mut self, message: Message) -> Result<()> { + if self.closed { + return Err(Error::ErrAgentClosed); + } + + self.transactions.remove(&message.transaction_id); + + let e = Event { + event_type: EventType::Callback(message.transaction_id), + event_body: Ok(message), + }; + + if let Some(handler) = &self.handler { + handler.send(e)?; + } + + Ok(()) + } + + /// close terminates all transactions with ErrAgentClosed and renders Agent to + /// closed state. + pub fn close(&mut self) -> Result<()> { + if self.closed { + return Err(Error::ErrAgentClosed); + } + + for id in self.transactions.keys() { + let e = Event { + event_type: EventType::Callback(*id), + event_body: Err(Error::ErrAgentClosed), + }; + if let Some(handler) = &self.handler { + handler.send(e)?; + } + } + self.transactions = HashMap::new(); + self.closed = true; + self.handler = noop_handler(); + + Ok(()) + } + + /// start registers transaction with provided id and deadline. + /// Could return ErrAgentClosed, ErrTransactionExists. + /// + /// Agent handler is guaranteed to be eventually called. + pub fn start(&mut self, id: TransactionId, deadline: Instant) -> Result<()> { + if self.closed { + return Err(Error::ErrAgentClosed); + } + if self.transactions.contains_key(&id) { + return Err(Error::ErrTransactionExists); + } + + self.transactions + .insert(id, AgentTransaction { id, deadline }); + + Ok(()) + } + + /// stop stops transaction by id with ErrTransactionStopped, blocking + /// until handler returns. + pub fn stop(&mut self, id: TransactionId) -> Result<()> { + self.stop_with_error(id, Error::ErrTransactionStopped) + } + + /// collect terminates all transactions that have deadline before provided + /// time, blocking until all handlers will process ErrTransactionTimeOut. + /// Will return ErrAgentClosed if agent is already closed. + /// + /// It is safe to call Collect concurrently but makes no sense. + pub fn collect(&mut self, deadline: Instant) -> Result<()> { + if self.closed { + // Doing nothing if agent is closed. + // All transactions should be already closed + // during Close() call. + return Err(Error::ErrAgentClosed); + } + + let mut to_remove: Vec = Vec::with_capacity(AGENT_COLLECT_CAP); + + // Adding all transactions with deadline before gc_time + // to toCall and to_remove slices. + // No allocs if there are less than AGENT_COLLECT_CAP + // timed out transactions. + for (id, t) in &self.transactions { + if t.deadline < deadline { + to_remove.push(*id); + } + } + // Un-registering timed out transactions. + for id in &to_remove { + self.transactions.remove(id); + } + + for id in to_remove { + let event = Event { + event_type: EventType::Callback(id), + event_body: Err(Error::ErrTransactionTimeOut), + }; + if let Some(handler) = &self.handler { + handler.send(event)?; + } + } + + Ok(()) + } + + /// set_handler sets agent handler to h. + pub fn set_handler(&mut self, h: Handler) -> Result<()> { + if self.closed { + return Err(Error::ErrAgentClosed); + } + self.handler = h; + + Ok(()) + } + + pub(crate) async fn run(mut agent: Agent, mut rx: mpsc::Receiver) { + while let Some(client_agent) = rx.recv().await { + let result = match client_agent { + ClientAgent::Process(message) => agent.process(message), + ClientAgent::Collect(deadline) => agent.collect(deadline), + ClientAgent::Start(tid, deadline) => agent.start(tid, deadline), + ClientAgent::Stop(tid) => agent.stop(tid), + ClientAgent::Close => agent.close(), + }; + + if let Err(err) = result { + if Error::ErrAgentClosed == err { + break; + } + } + } + } +} diff --git a/stun-patch/src/agent/agent_test.rs b/stun-patch/src/agent/agent_test.rs new file mode 100644 index 0000000..8ca1afa --- /dev/null +++ b/stun-patch/src/agent/agent_test.rs @@ -0,0 +1,195 @@ +use std::ops::Add; + +use tokio::time::Duration; + +use super::*; +use crate::error::*; + +#[tokio::test] +async fn test_agent_process_in_transaction() -> Result<()> { + let mut m = Message::new(); + let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut a = Agent::new(Some(Arc::new(handler_tx))); + m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + a.start(m.transaction_id, Instant::now())?; + a.process(m)?; + a.close()?; + + while let Some(e) = handler_rx.recv().await { + assert!(e.event_body.is_ok(), "got error: {:?}", e.event_body); + + let tid = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + assert_eq!( + e.event_body.as_ref().unwrap().transaction_id, + tid, + "{:?} (got) != {:?} (expected)", + e.event_body.as_ref().unwrap().transaction_id, + tid + ); + } + + Ok(()) +} + +#[tokio::test] +async fn test_agent_process() -> Result<()> { + let mut m = Message::new(); + let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut a = Agent::new(Some(Arc::new(handler_tx))); + m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + a.process(m.clone())?; + a.close()?; + + while let Some(e) = handler_rx.recv().await { + assert!(e.event_body.is_ok(), "got error: {:?}", e.event_body); + + let tid = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + assert_eq!( + e.event_body.as_ref().unwrap().transaction_id, + tid, + "{:?} (got) != {:?} (expected)", + e.event_body.as_ref().unwrap().transaction_id, + tid + ); + } + + let result = a.process(m); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrAgentClosed, + "closed agent should return <{}>, but got <{}>", + Error::ErrAgentClosed, + err, + ); + } else { + panic!("expected error, but got ok"); + } + + Ok(()) +} + +#[test] +fn test_agent_start() -> Result<()> { + let mut a = Agent::new(noop_handler()); + let id = TransactionId::new(); + let deadline = Instant::now().add(Duration::from_secs(3600)); + a.start(id, deadline)?; + + let result = a.start(id, deadline); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrTransactionExists, + "duplicate start should return <{}>, got <{}>", + Error::ErrTransactionExists, + err, + ); + } else { + panic!("expected error, but got ok"); + } + a.close()?; + + let id = TransactionId::new(); + let result = a.start(id, deadline); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrAgentClosed, + "start on closed agent should return <{}>, got <{}>", + Error::ErrAgentClosed, + err, + ); + } else { + panic!("expected error, but got ok"); + } + + let result = a.set_handler(noop_handler()); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrAgentClosed, + "SetHandler on closed agent should return <{}>, got <{}>", + Error::ErrAgentClosed, + err, + ); + } else { + panic!("expected error, but got ok"); + } + + Ok(()) +} + +#[tokio::test] +async fn test_agent_stop() -> Result<()> { + let (handler_tx, mut handler_rx) = tokio::sync::mpsc::unbounded_channel(); + let mut a = Agent::new(Some(Arc::new(handler_tx))); + + let result = a.stop(TransactionId::default()); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrTransactionNotExists, + "unexpected error: {}, should be {}", + Error::ErrTransactionNotExists, + err, + ); + } else { + panic!("expected error, but got ok"); + } + + let id = TransactionId::new(); + let deadline = Instant::now().add(Duration::from_millis(200)); + a.start(id, deadline)?; + a.stop(id)?; + + let timeout = tokio::time::sleep(Duration::from_millis(400)); + tokio::pin!(timeout); + + tokio::select! { + evt = handler_rx.recv() => { + if let Err(err) = evt.unwrap().event_body{ + assert_eq!( + err, + Error::ErrTransactionStopped, + "unexpected error: {}, should be {}", + err, + Error::ErrTransactionStopped + ); + }else{ + panic!("expected error, got ok"); + } + } + _ = timeout.as_mut() => panic!("timed out"), + } + + a.close()?; + + let result = a.close(); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrAgentClosed, + "a.Close returned {} instead of {}", + Error::ErrAgentClosed, + err, + ); + } else { + panic!("expected error, but got ok"); + } + + let result = a.stop(TransactionId::default()); + if let Err(err) = result { + assert_eq!( + err, + Error::ErrAgentClosed, + "unexpected error: {}, should be {}", + Error::ErrAgentClosed, + err, + ); + } else { + panic!("expected error, but got ok"); + } + + Ok(()) +} diff --git a/stun-patch/src/attributes.rs b/stun-patch/src/attributes.rs new file mode 100644 index 0000000..f51a98e --- /dev/null +++ b/stun-patch/src/attributes.rs @@ -0,0 +1,209 @@ +#[cfg(test)] +mod attributes_test; + +use std::fmt; + +use crate::error::*; +use crate::message::*; + +/// Attributes is list of message attributes. +#[derive(Default, PartialEq, Eq, Debug, Clone)] +pub struct Attributes(pub Vec); + +impl Attributes { + /// get returns first attribute from list by the type. + /// If attribute is present the RawAttribute is returned and the + /// boolean is true. Otherwise the returned RawAttribute will be + /// empty and boolean will be false. + pub fn get(&self, t: AttrType) -> (RawAttribute, bool) { + for candidate in &self.0 { + if candidate.typ == t { + return (candidate.clone(), true); + } + } + + (RawAttribute::default(), false) + } +} + +/// AttrType is attribute type. +#[derive(PartialEq, Debug, Eq, Default, Copy, Clone)] +pub struct AttrType(pub u16); + +impl fmt::Display for AttrType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let other = format!("0x{:x}", self.0); + + let s = match *self { + ATTR_MAPPED_ADDRESS => "MAPPED-ADDRESS", + ATTR_USERNAME => "USERNAME", + ATTR_ERROR_CODE => "ERROR-CODE", + ATTR_MESSAGE_INTEGRITY => "MESSAGE-INTEGRITY", + ATTR_UNKNOWN_ATTRIBUTES => "UNKNOWN-ATTRIBUTES", + ATTR_REALM => "REALM", + ATTR_NONCE => "NONCE", + ATTR_XORMAPPED_ADDRESS => "XOR-MAPPED-ADDRESS", + ATTR_SOFTWARE => "SOFTWARE", + ATTR_ALTERNATE_SERVER => "ALTERNATE-SERVER", + ATTR_FINGERPRINT => "FINGERPRINT", + ATTR_PRIORITY => "PRIORITY", + ATTR_USE_CANDIDATE => "USE-CANDIDATE", + ATTR_ICE_CONTROLLED => "ICE-CONTROLLED", + ATTR_ICE_CONTROLLING => "ICE-CONTROLLING", + ATTR_CHANNEL_NUMBER => "CHANNEL-NUMBER", + ATTR_LIFETIME => "LIFETIME", + ATTR_XOR_PEER_ADDRESS => "XOR-PEER-ADDRESS", + ATTR_DATA => "DATA", + ATTR_XOR_RELAYED_ADDRESS => "XOR-RELAYED-ADDRESS", + ATTR_EVEN_PORT => "EVEN-PORT", + ATTR_REQUESTED_TRANSPORT => "REQUESTED-TRANSPORT", + ATTR_DONT_FRAGMENT => "DONT-FRAGMENT", + ATTR_RESERVATION_TOKEN => "RESERVATION-TOKEN", + ATTR_CONNECTION_ID => "CONNECTION-ID", + ATTR_REQUESTED_ADDRESS_FAMILY => "REQUESTED-ADDRESS-FAMILY", + ATTR_MESSAGE_INTEGRITY_SHA256 => "MESSAGE-INTEGRITY-SHA256", + ATTR_PASSWORD_ALGORITHM => "PASSWORD-ALGORITHM", + ATTR_USER_HASH => "USERHASH", + ATTR_PASSWORD_ALGORITHMS => "PASSWORD-ALGORITHMS", + ATTR_ALTERNATE_DOMAIN => "ALTERNATE-DOMAIN", + _ => other.as_str(), + }; + + write!(f, "{s}") + } +} + +impl AttrType { + /// required returns true if type is from comprehension-required range (0x0000-0x7FFF). + pub fn required(&self) -> bool { + self.0 <= 0x7FFF + } + + /// optional returns true if type is from comprehension-optional range (0x8000-0xFFFF). + pub fn optional(&self) -> bool { + self.0 >= 0x8000 + } + + /// value returns uint16 representation of attribute type. + pub fn value(&self) -> u16 { + self.0 + } +} + +/// Attributes from comprehension-required range (0x0000-0x7FFF). +pub const ATTR_MAPPED_ADDRESS: AttrType = AttrType(0x0001); // MAPPED-ADDRESS +pub const ATTR_USERNAME: AttrType = AttrType(0x0006); // USERNAME +pub const ATTR_MESSAGE_INTEGRITY: AttrType = AttrType(0x0008); // MESSAGE-INTEGRITY +pub const ATTR_ERROR_CODE: AttrType = AttrType(0x0009); // ERROR-CODE +pub const ATTR_UNKNOWN_ATTRIBUTES: AttrType = AttrType(0x000A); // UNKNOWN-ATTRIBUTES +pub const ATTR_REALM: AttrType = AttrType(0x0014); // REALM +pub const ATTR_NONCE: AttrType = AttrType(0x0015); // NONCE +pub const ATTR_XORMAPPED_ADDRESS: AttrType = AttrType(0x0020); // XOR-MAPPED-ADDRESS + +/// Attributes from comprehension-optional range (0x8000-0xFFFF). +pub const ATTR_SOFTWARE: AttrType = AttrType(0x8022); // SOFTWARE +pub const ATTR_ALTERNATE_SERVER: AttrType = AttrType(0x8023); // ALTERNATE-SERVER +pub const ATTR_FINGERPRINT: AttrType = AttrType(0x8028); // FINGERPRINT + +/// Attributes from RFC 5245 ICE. +pub const ATTR_PRIORITY: AttrType = AttrType(0x0024); // PRIORITY +pub const ATTR_USE_CANDIDATE: AttrType = AttrType(0x0025); // USE-CANDIDATE +pub const ATTR_ICE_CONTROLLED: AttrType = AttrType(0x8029); // ICE-CONTROLLED +pub const ATTR_ICE_CONTROLLING: AttrType = AttrType(0x802A); // ICE-CONTROLLING + +/// Attributes from RFC 5766 TURN. +pub const ATTR_CHANNEL_NUMBER: AttrType = AttrType(0x000C); // CHANNEL-NUMBER +pub const ATTR_LIFETIME: AttrType = AttrType(0x000D); // LIFETIME +pub const ATTR_XOR_PEER_ADDRESS: AttrType = AttrType(0x0012); // XOR-PEER-ADDRESS +pub const ATTR_DATA: AttrType = AttrType(0x0013); // DATA +pub const ATTR_XOR_RELAYED_ADDRESS: AttrType = AttrType(0x0016); // XOR-RELAYED-ADDRESS +pub const ATTR_EVEN_PORT: AttrType = AttrType(0x0018); // EVEN-PORT +pub const ATTR_REQUESTED_TRANSPORT: AttrType = AttrType(0x0019); // REQUESTED-TRANSPORT +pub const ATTR_DONT_FRAGMENT: AttrType = AttrType(0x001A); // DONT-FRAGMENT +pub const ATTR_RESERVATION_TOKEN: AttrType = AttrType(0x0022); // RESERVATION-TOKEN + +/// Attributes from RFC 5780 NAT Behavior Discovery +pub const ATTR_CHANGE_REQUEST: AttrType = AttrType(0x0003); // CHANGE-REQUEST +pub const ATTR_PADDING: AttrType = AttrType(0x0026); // PADDING +pub const ATTR_RESPONSE_PORT: AttrType = AttrType(0x0027); // RESPONSE-PORT +pub const ATTR_CACHE_TIMEOUT: AttrType = AttrType(0x8027); // CACHE-TIMEOUT +pub const ATTR_RESPONSE_ORIGIN: AttrType = AttrType(0x802b); // RESPONSE-ORIGIN +pub const ATTR_OTHER_ADDRESS: AttrType = AttrType(0x802C); // OTHER-ADDRESS + +/// Attributes from RFC 3489, removed by RFC 5389, +/// but still used by RFC5389-implementing software like Vovida.org, reTURNServer, etc. +pub const ATTR_SOURCE_ADDRESS: AttrType = AttrType(0x0004); // SOURCE-ADDRESS +pub const ATTR_CHANGED_ADDRESS: AttrType = AttrType(0x0005); // CHANGED-ADDRESS + +/// Attributes from RFC 6062 TURN Extensions for TCP Allocations. +pub const ATTR_CONNECTION_ID: AttrType = AttrType(0x002a); // CONNECTION-ID + +/// Attributes from RFC 6156 TURN IPv6. +pub const ATTR_REQUESTED_ADDRESS_FAMILY: AttrType = AttrType(0x0017); // REQUESTED-ADDRESS-FAMILY + +/// Attributes from An Origin Attribute for the STUN Protocol. +pub const ATTR_ORIGIN: AttrType = AttrType(0x802F); + +/// Attributes from RFC 8489 STUN. +pub const ATTR_MESSAGE_INTEGRITY_SHA256: AttrType = AttrType(0x001C); // MESSAGE-INTEGRITY-SHA256 +pub const ATTR_PASSWORD_ALGORITHM: AttrType = AttrType(0x001D); // PASSWORD-ALGORITHM +pub const ATTR_USER_HASH: AttrType = AttrType(0x001E); // USER-HASH +pub const ATTR_PASSWORD_ALGORITHMS: AttrType = AttrType(0x8002); // PASSWORD-ALGORITHMS +pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); // ALTERNATE-DOMAIN + +/// RawAttribute is a Type-Length-Value (TLV) object that +/// can be added to a STUN message. Attributes are divided into two +/// types: comprehension-required and comprehension-optional. STUN +/// agents can safely ignore comprehension-optional attributes they +/// don't understand, but cannot successfully process a message if it +/// contains comprehension-required attributes that are not +/// understood. +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct RawAttribute { + pub typ: AttrType, + pub length: u16, // ignored while encoding + pub value: Vec, +} + +impl fmt::Display for RawAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {:?}", self.typ, self.value) + } +} + +impl Setter for RawAttribute { + /// add_to implements Setter, adding attribute as a.Type with a.Value and ignoring + /// the Length field. + fn add_to(&self, m: &mut Message) -> Result<()> { + m.add(self.typ, &self.value); + Ok(()) + } +} + +pub(crate) const PADDING: usize = 4; + +/// STUN aligns attributes on 32-bit boundaries, attributes whose content +/// is not a multiple of 4 bytes are padded with 1, 2, or 3 bytes of +/// padding so that its value contains a multiple of 4 bytes. The +/// padding bits are ignored, and may be any value. +/// +/// https://tools.ietf.org/html/rfc5389#section-15 +pub(crate) fn nearest_padded_value_length(l: usize) -> usize { + let mut n = PADDING * (l / PADDING); + if n < l { + n += PADDING + } + n +} + +/// This method converts uint16 vlue to AttrType. If it finds an old attribute +/// type value, it also translates it to the new value to enable backward +/// compatibility. (See: https://github.com/pion/stun/issues/21) +pub(crate) fn compat_attr_type(val: u16) -> AttrType { + if val == 0x8020 { + // draft-ietf-behave-rfc3489bis-02, MS-TURN + ATTR_XORMAPPED_ADDRESS // new: 0x0020 (from draft-ietf-behave-rfc3489bis-03 on) + } else { + AttrType(val) + } +} diff --git a/stun-patch/src/attributes/attributes_test.rs b/stun-patch/src/attributes/attributes_test.rs new file mode 100644 index 0000000..3be540f --- /dev/null +++ b/stun-patch/src/attributes/attributes_test.rs @@ -0,0 +1,86 @@ +use super::*; +use crate::textattrs::TextAttribute; + +#[test] +fn test_raw_attribute_add_to() -> Result<()> { + let v = vec![1, 2, 3, 4]; + let mut m = Message::new(); + let ra = Box::new(RawAttribute { + typ: ATTR_DATA, + value: v.clone(), + ..Default::default() + }); + m.build(&[ra])?; + let got_v = m.get(ATTR_DATA)?; + assert_eq!(got_v, v, "value mismatch"); + + Ok(()) +} + +#[test] +fn test_message_get_no_allocs() -> Result<()> { + let mut m = Message::new(); + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "c".to_owned(), + }; + a.add_to(&mut m)?; + m.write_header(); + + //"Default" + { + m.get(ATTR_SOFTWARE)?; + } + //"Not found" + { + let result = m.get(ATTR_ORIGIN); + assert!(result.is_err(), "should error"); + } + + Ok(()) +} + +#[test] +fn test_padding() -> Result<()> { + let tt = vec![ + (4, 4), // 0 + (2, 4), // 1 + (5, 8), // 2 + (8, 8), // 3 + (11, 12), // 4 + (1, 4), // 5 + (3, 4), // 6 + (6, 8), // 7 + (7, 8), // 8 + (0, 0), // 9 + (40, 40), // 10 + ]; + + for (i, o) in tt { + let got = nearest_padded_value_length(i); + assert_eq!(got, o, "padded({i}) {got} (got) != {o} (expected)",); + } + + Ok(()) +} + +#[test] +fn test_attr_type_range() -> Result<()> { + let tests = vec![ + ATTR_PRIORITY, + ATTR_ERROR_CODE, + ATTR_USE_CANDIDATE, + ATTR_EVEN_PORT, + ATTR_REQUESTED_ADDRESS_FAMILY, + ]; + for a in tests { + assert!(!a.optional() && a.required(), "should be required"); + } + + let tests = vec![ATTR_SOFTWARE, ATTR_ICE_CONTROLLED, ATTR_ORIGIN]; + for a in tests { + assert!(!a.required() && a.optional(), "should be optional"); + } + + Ok(()) +} diff --git a/stun-patch/src/checks.rs b/stun-patch/src/checks.rs new file mode 100644 index 0000000..9f4c134 --- /dev/null +++ b/stun-patch/src/checks.rs @@ -0,0 +1,48 @@ +use subtle::ConstantTimeEq; + +use crate::attributes::*; +use crate::error::*; + +// check_size returns ErrAttrSizeInvalid if got is not equal to expected. +pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { + if got == expected { + Ok(()) + } else { + Err(Error::ErrAttributeSizeInvalid) + } +} + +// is_attr_size_invalid returns true if error means that attribute size is invalid. +pub fn is_attr_size_invalid(err: &Error) -> bool { + Error::ErrAttributeSizeInvalid == *err +} + +pub(crate) fn check_hmac(got: &[u8], expected: &[u8]) -> Result<()> { + if got.ct_eq(expected).unwrap_u8() != 1 { + Err(Error::ErrIntegrityMismatch) + } else { + Ok(()) + } +} + +pub(crate) fn check_fingerprint(got: u32, expected: u32) -> Result<()> { + if got == expected { + Ok(()) + } else { + Err(Error::ErrFingerprintMismatch) + } +} + +// check_overflow returns ErrAttributeSizeOverflow if got is bigger that max. +pub fn check_overflow(_at: AttrType, got: usize, max: usize) -> Result<()> { + if got <= max { + Ok(()) + } else { + Err(Error::ErrAttributeSizeOverflow) + } +} + +// is_attr_size_overflow returns true if error means that attribute size is too big. +pub fn is_attr_size_overflow(err: &Error) -> bool { + Error::ErrAttributeSizeOverflow == *err +} diff --git a/stun-patch/src/client.rs b/stun-patch/src/client.rs new file mode 100644 index 0000000..13d8bd6 --- /dev/null +++ b/stun-patch/src/client.rs @@ -0,0 +1,473 @@ +#[cfg(test)] +mod client_test; + +use std::collections::HashMap; +use std::io::BufReader; +use std::marker::{Send, Sync}; +use std::ops::Add; +use std::sync::Arc; + +use tokio::sync::mpsc; +use tokio::time::{self, Duration, Instant}; +use util::Conn; + +use crate::agent::*; +use crate::error::*; +use crate::message::*; + +const DEFAULT_TIMEOUT_RATE: Duration = Duration::from_millis(5); +const DEFAULT_RTO: Duration = Duration::from_millis(300); +const DEFAULT_MAX_ATTEMPTS: u32 = 7; +const DEFAULT_MAX_BUFFER_SIZE: usize = 8; + +/// Collector calls function f with constant rate. +/// +/// The simple Collector is ticker which calls function on each tick. +pub trait Collector { + fn start( + &mut self, + rate: Duration, + client_agent_tx: Arc>, + ) -> Result<()>; + fn close(&mut self) -> Result<()>; +} + +#[derive(Default)] +struct TickerCollector { + close_tx: Option>, +} + +impl Collector for TickerCollector { + fn start( + &mut self, + rate: Duration, + client_agent_tx: Arc>, + ) -> Result<()> { + let (close_tx, mut close_rx) = mpsc::channel(1); + self.close_tx = Some(close_tx); + + tokio::spawn(async move { + let mut interval = time::interval(rate); + + loop { + tokio::select! { + _ = close_rx.recv() => break, + _ = interval.tick() => { + if client_agent_tx.send(ClientAgent::Collect(Instant::now())).await.is_err() { + break; + } + } + } + } + }); + + Ok(()) + } + + fn close(&mut self) -> Result<()> { + if self.close_tx.is_none() { + return Err(Error::ErrCollectorClosed); + } + self.close_tx.take(); + Ok(()) + } +} + +/// ClientTransaction represents transaction in progress. +/// If transaction is succeed or failed, f will be called +/// provided by event. +/// Concurrent access is invalid. +#[derive(Debug, Clone)] +pub struct ClientTransaction { + id: TransactionId, + attempt: u32, + calls: u32, + handler: Handler, + start: Instant, + rto: Duration, + raw: Vec, +} + +impl ClientTransaction { + pub(crate) fn handle(&mut self, e: Event) -> Result<()> { + self.calls += 1; + if self.calls == 1 { + if let Some(handler) = &self.handler { + handler.send(e)?; + } + } + Ok(()) + } + + pub(crate) fn next_timeout(&self, now: Instant) -> Instant { + now.add((self.attempt + 1) * self.rto) + } +} + +struct ClientSettings { + buffer_size: usize, + rto: Duration, + rto_rate: Duration, + max_attempts: u32, + closed: bool, + //handler: Handler, + collector: Option>, + c: Option>, +} + +impl Default for ClientSettings { + fn default() -> Self { + ClientSettings { + buffer_size: DEFAULT_MAX_BUFFER_SIZE, + rto: DEFAULT_RTO, + rto_rate: DEFAULT_TIMEOUT_RATE, + max_attempts: DEFAULT_MAX_ATTEMPTS, + closed: false, + //handler: None, + collector: None, + c: None, + } + } +} + +#[derive(Default)] +pub struct ClientBuilder { + settings: ClientSettings, +} + +impl ClientBuilder { + // WithHandler sets client handler which is called if Agent emits the Event + // with TransactionID that is not currently registered by Client. + // Useful for handling Data indications from TURN server. + //pub fn with_handler(mut self, handler: Handler) -> Self { + // self.settings.handler = handler; + // self + //} + + /// with_rto sets client RTO as defined in STUN RFC. + pub fn with_rto(mut self, rto: Duration) -> Self { + self.settings.rto = rto; + self + } + + /// with_timeout_rate sets RTO timer minimum resolution. + pub fn with_timeout_rate(mut self, d: Duration) -> Self { + self.settings.rto_rate = d; + self + } + + /// with_buffer_size sets buffer size. + pub fn with_buffer_size(mut self, buffer_size: usize) -> Self { + self.settings.buffer_size = buffer_size; + self + } + + /// with_collector rests client timeout collector, the implementation + /// of ticker which calls function on each tick. + pub fn with_collector(mut self, coll: Box) -> Self { + self.settings.collector = Some(coll); + self + } + + /// with_conn sets transport connection + pub fn with_conn(mut self, conn: Arc) -> Self { + self.settings.c = Some(conn); + self + } + + /// with_no_retransmit disables retransmissions and sets RTO to + /// DEFAULT_MAX_ATTEMPTS * DEFAULT_RTO which will be effectively time out + /// if not set. + /// Useful for TCP connections where transport handles RTO. + pub fn with_no_retransmit(mut self) -> Self { + self.settings.max_attempts = 0; + if self.settings.rto == Duration::from_secs(0) { + self.settings.rto = DEFAULT_MAX_ATTEMPTS * DEFAULT_RTO; + } + self + } + + pub fn new() -> Self { + ClientBuilder { + settings: ClientSettings::default(), + } + } + + pub fn build(self) -> Result { + if self.settings.c.is_none() { + return Err(Error::ErrNoConnection); + } + + let client = Client { + settings: self.settings, + ..Default::default() + } + .run()?; + + Ok(client) + } +} + +/// Client simulates "connection" to STUN server. +#[derive(Default)] +pub struct Client { + settings: ClientSettings, + close_tx: Option>, + client_agent_tx: Option>>, + handler_tx: Option>>, +} + +impl Client { + async fn read_until_closed( + mut close_rx: mpsc::Receiver<()>, + c: Arc, + client_agent_tx: Arc>, + ) { + let mut msg = Message::new(); + let mut buf = vec![0; 1024]; + + loop { + tokio::select! { + _ = close_rx.recv() => return, + res = c.recv(&mut buf) => { + if let Ok(n) = res { + let mut reader = BufReader::new(&buf[..n]); + let result = msg.read_from(&mut reader); + if result.is_err() { + continue; + } + + if client_agent_tx.send(ClientAgent::Process(msg.clone())).await.is_err(){ + return; + } + } + } + } + } + } + + fn insert(&mut self, ct: ClientTransaction) -> Result<()> { + if self.settings.closed { + return Err(Error::ErrClientClosed); + } + + if let Some(handler_tx) = &mut self.handler_tx { + handler_tx.send(Event { + event_type: EventType::Insert(ct), + ..Default::default() + })?; + } + + Ok(()) + } + + fn remove(&mut self, id: TransactionId) -> Result<()> { + if self.settings.closed { + return Err(Error::ErrClientClosed); + } + + if let Some(handler_tx) = &mut self.handler_tx { + handler_tx.send(Event { + event_type: EventType::Remove(id), + ..Default::default() + })?; + } + + Ok(()) + } + + fn start( + conn: Option>, + mut handler_rx: mpsc::UnboundedReceiver, + client_agent_tx: Arc>, + mut t: HashMap, + max_attempts: u32, + ) { + tokio::spawn(async move { + while let Some(event) = handler_rx.recv().await { + match event.event_type { + EventType::Close => { + break; + } + EventType::Insert(ct) => { + if t.contains_key(&ct.id) { + continue; + } + t.insert(ct.id, ct); + } + EventType::Remove(id) => { + t.remove(&id); + } + EventType::Callback(id) => { + let mut ct = if t.contains_key(&id) { + t.remove(&id).unwrap() + } else { + /*if c.handler != nil && !errors.Is(e.Error, ErrTransactionStopped) { + c.handler(e) + }*/ + continue; + }; + + if ct.attempt >= max_attempts || event.event_body.is_ok() { + if let Some(handler) = ct.handler { + let _ = handler.send(event); + } + continue; + } + + // Doing re-transmission. + ct.attempt += 1; + + let raw = ct.raw.clone(); + let timeout = ct.next_timeout(Instant::now()); + let id = ct.id; + + // Starting client transaction. + t.insert(ct.id, ct); + + // Starting agent transaction. + if client_agent_tx + .send(ClientAgent::Start(id, timeout)) + .await + .is_err() + { + let ct = t.remove(&id).unwrap(); + if let Some(handler) = ct.handler { + let _ = handler.send(event); + } + continue; + } + + // Writing message to connection again. + if let Some(c) = &conn { + if c.send(&raw).await.is_err() { + let _ = client_agent_tx.send(ClientAgent::Stop(id)).await; + + let ct = t.remove(&id).unwrap(); + if let Some(handler) = ct.handler { + let _ = handler.send(event); + } + continue; + } + } + } + }; + } + }); + } + + /// close stops internal connection and agent, returning CloseErr on error. + pub async fn close(&mut self) -> Result<()> { + if self.settings.closed { + return Err(Error::ErrClientClosed); + } + + self.settings.closed = true; + + if let Some(collector) = &mut self.settings.collector { + let _ = collector.close(); + } + self.settings.collector.take(); + + self.close_tx.take(); //drop close channel + if let Some(client_agent_tx) = &mut self.client_agent_tx { + let _ = client_agent_tx.send(ClientAgent::Close).await; + } + self.client_agent_tx.take(); + + if let Some(c) = self.settings.c.take() { + c.close().await?; + } + + Ok(()) + } + + fn run(mut self) -> Result { + let (close_tx, close_rx) = mpsc::channel(1); + let (client_agent_tx, client_agent_rx) = mpsc::channel(self.settings.buffer_size); + let (handler_tx, handler_rx) = mpsc::unbounded_channel(); + let t: HashMap = HashMap::new(); + + let client_agent_tx = Arc::new(client_agent_tx); + let handler_tx = Arc::new(handler_tx); + self.client_agent_tx = Some(Arc::clone(&client_agent_tx)); + self.handler_tx = Some(Arc::clone(&handler_tx)); + self.close_tx = Some(close_tx); + + let conn = if let Some(conn) = &self.settings.c { + Arc::clone(conn) + } else { + return Err(Error::ErrNoConnection); + }; + + Client::start( + self.settings.c.clone(), + handler_rx, + Arc::clone(&client_agent_tx), + t, + self.settings.max_attempts, + ); + + let agent = Agent::new(Some(handler_tx)); + tokio::spawn(async move { Agent::run(agent, client_agent_rx).await }); + + if self.settings.collector.is_none() { + self.settings.collector = Some(Box::::default()); + } + if let Some(collector) = &mut self.settings.collector { + collector.start(self.settings.rto_rate, Arc::clone(&client_agent_tx))?; + } + + let conn_rx = Arc::clone(&conn); + tokio::spawn( + async move { Client::read_until_closed(close_rx, conn_rx, client_agent_tx).await }, + ); + + Ok(self) + } + + pub async fn send(&mut self, m: &Message, handler: Handler) -> Result<()> { + if self.settings.closed { + return Err(Error::ErrClientClosed); + } + + let has_handler = handler.is_some(); + + if handler.is_some() { + let t = ClientTransaction { + id: m.transaction_id, + attempt: 0, + calls: 0, + handler, + start: Instant::now(), + rto: self.settings.rto, + raw: m.raw.clone(), + }; + let d = t.next_timeout(t.start); + self.insert(t)?; + + if let Some(client_agent_tx) = &mut self.client_agent_tx { + client_agent_tx + .send(ClientAgent::Start(m.transaction_id, d)) + .await?; + } + } + + if let Some(c) = &self.settings.c { + let result = c.send(&m.raw).await; + if result.is_err() && has_handler { + self.remove(m.transaction_id)?; + + if let Some(client_agent_tx) = &mut self.client_agent_tx { + client_agent_tx + .send(ClientAgent::Stop(m.transaction_id)) + .await?; + } + } else if let Err(err) = result { + return Err(Error::Other(err.to_string())); + } + } + + Ok(()) + } +} diff --git a/stun-patch/src/client/client_test.rs b/stun-patch/src/client/client_test.rs new file mode 100644 index 0000000..c7bad84 --- /dev/null +++ b/stun-patch/src/client/client_test.rs @@ -0,0 +1,12 @@ +use super::*; + +#[test] +fn ensure_client_settings_is_send() { + let client = ClientSettings::default(); + + ensure_send(client); +} + +fn ensure_send(_: T) {} + +//TODO: add more client tests diff --git a/stun-patch/src/error.rs b/stun-patch/src/error.rs new file mode 100644 index 0000000..083b3ad --- /dev/null +++ b/stun-patch/src/error.rs @@ -0,0 +1,98 @@ +use std::io; +use std::string::FromUtf8Error; + +use thiserror::Error; +use tokio::sync::mpsc::error::SendError as MpscSendError; + +pub type Result = std::result::Result; + +#[derive(Debug, Error, PartialEq)] +#[non_exhaustive] +pub enum Error { + #[error("attribute not found")] + ErrAttributeNotFound, + #[error("transaction is stopped")] + ErrTransactionStopped, + #[error("transaction not exists")] + ErrTransactionNotExists, + #[error("transaction exists with same id")] + ErrTransactionExists, + #[error("agent is closed")] + ErrAgentClosed, + #[error("transaction is timed out")] + ErrTransactionTimeOut, + #[error("no default reason for ErrorCode")] + ErrNoDefaultReason, + #[error("unexpected EOF")] + ErrUnexpectedEof, + #[error("attribute size is invalid")] + ErrAttributeSizeInvalid, + #[error("attribute size overflow")] + ErrAttributeSizeOverflow, + #[error("attempt to decode to nil message")] + ErrDecodeToNil, + #[error("unexpected EOF: not enough bytes to read header")] + ErrUnexpectedHeaderEof, + #[error("integrity check failed")] + ErrIntegrityMismatch, + #[error("fingerprint check failed")] + ErrFingerprintMismatch, + #[error("FINGERPRINT before MESSAGE-INTEGRITY attribute")] + ErrFingerprintBeforeIntegrity, + #[error("bad UNKNOWN-ATTRIBUTES size")] + ErrBadUnknownAttrsSize, + #[error("invalid length of IP value")] + ErrBadIpLength, + #[error("no connection provided")] + ErrNoConnection, + #[error("client is closed")] + ErrClientClosed, + #[error("no agent is set")] + ErrNoAgent, + #[error("collector is closed")] + ErrCollectorClosed, + #[error("unsupported network")] + ErrUnsupportedNetwork, + #[error("invalid url")] + ErrInvalidUrl, + #[error("unknown scheme type")] + ErrSchemeType, + #[error("invalid hostname")] + ErrHost, + #[error("{0}")] + Other(String), + #[error("url parse: {0}")] + Url(#[from] url::ParseError), + #[error("utf8: {0}")] + Utf8(#[from] FromUtf8Error), + #[error("{0}")] + Io(#[source] IoError), + #[error("mpsc send: {0}")] + MpscSend(String), + #[error("{0}")] + Util(#[from] util::Error), +} + +#[derive(Debug, Error)] +#[error("io error: {0}")] +pub struct IoError(#[from] pub io::Error); + +// Workaround for wanting PartialEq for io::Error. +impl PartialEq for IoError { + fn eq(&self, other: &Self) -> bool { + self.0.kind() == other.0.kind() + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + Error::Io(IoError(e)) + } +} + +// Because Tokio SendError is parameterized, we sadly lose the backtrace. +impl From> for Error { + fn from(e: MpscSendError) -> Self { + Error::MpscSend(e.to_string()) + } +} diff --git a/stun-patch/src/error_code.rs b/stun-patch/src/error_code.rs new file mode 100644 index 0000000..3bf8cfc --- /dev/null +++ b/stun-patch/src/error_code.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; +use std::fmt; + +use crate::attributes::*; +use crate::checks::*; +use crate::error::*; +use crate::message::*; + +// ErrorCodeAttribute represents ERROR-CODE attribute. +// +// RFC 5389 Section 15.6 +#[derive(Default)] +pub struct ErrorCodeAttribute { + pub code: ErrorCode, + pub reason: Vec, +} + +impl fmt::Display for ErrorCodeAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let reason = String::from_utf8_lossy(&self.reason); + write!(f, "{}: {}", self.code.0, reason) + } +} + +// constants for ERROR-CODE encoding. +const ERROR_CODE_CLASS_BYTE: usize = 2; +const ERROR_CODE_NUMBER_BYTE: usize = 3; +const ERROR_CODE_REASON_START: usize = 4; +const ERROR_CODE_REASON_MAX_B: usize = 763; +const ERROR_CODE_MODULO: u16 = 100; + +impl Setter for ErrorCodeAttribute { + // add_to adds ERROR-CODE to m. + fn add_to(&self, m: &mut Message) -> Result<()> { + check_overflow( + ATTR_ERROR_CODE, + self.reason.len() + ERROR_CODE_REASON_START, + ERROR_CODE_REASON_MAX_B + ERROR_CODE_REASON_START, + )?; + + let mut value: Vec = Vec::with_capacity(ERROR_CODE_REASON_MAX_B); + + let number = (self.code.0 % ERROR_CODE_MODULO) as u8; // error code modulo 100 + let class = (self.code.0 / ERROR_CODE_MODULO) as u8; // hundred digit + value.extend_from_slice(&[0, 0]); + value.push(class); // [ERROR_CODE_CLASS_BYTE] + value.push(number); //[ERROR_CODE_NUMBER_BYTE] = + value.extend_from_slice(&self.reason); //[ERROR_CODE_REASON_START:] + + m.add(ATTR_ERROR_CODE, &value); + + Ok(()) + } +} + +impl Getter for ErrorCodeAttribute { + // GetFrom decodes ERROR-CODE from m. Reason is valid until m.Raw is valid. + fn get_from(&mut self, m: &Message) -> Result<()> { + let v = m.get(ATTR_ERROR_CODE)?; + + if v.len() < ERROR_CODE_REASON_START { + return Err(Error::ErrUnexpectedEof); + } + + let class = v[ERROR_CODE_CLASS_BYTE] as u16; + let number = v[ERROR_CODE_NUMBER_BYTE] as u16; + let code = class * ERROR_CODE_MODULO + number; + self.code = ErrorCode(code); + self.reason = v[ERROR_CODE_REASON_START..].to_vec(); + + Ok(()) + } +} + +// ErrorCode is code for ERROR-CODE attribute. +#[derive(PartialEq, Eq, Hash, Copy, Clone, Default)] +pub struct ErrorCode(pub u16); + +impl Setter for ErrorCode { + // add_to adds ERROR-CODE with default reason to m. If there + // is no default reason, returns ErrNoDefaultReason. + fn add_to(&self, m: &mut Message) -> Result<()> { + if let Some(reason) = ERROR_REASONS.get(self) { + let a = ErrorCodeAttribute { + code: *self, + reason: reason.clone(), + }; + a.add_to(m) + } else { + Err(Error::ErrNoDefaultReason) + } + } +} + +// Possible error codes. +pub const CODE_TRY_ALTERNATE: ErrorCode = ErrorCode(300); +pub const CODE_BAD_REQUEST: ErrorCode = ErrorCode(400); +pub const CODE_UNAUTHORIZED: ErrorCode = ErrorCode(401); +pub const CODE_UNKNOWN_ATTRIBUTE: ErrorCode = ErrorCode(420); +pub const CODE_STALE_NONCE: ErrorCode = ErrorCode(438); +pub const CODE_ROLE_CONFLICT: ErrorCode = ErrorCode(487); +pub const CODE_SERVER_ERROR: ErrorCode = ErrorCode(500); + +// DEPRECATED constants. +// DEPRECATED, use CODE_UNAUTHORIZED. +pub const CODE_UNAUTHORISED: ErrorCode = CODE_UNAUTHORIZED; + +// Error codes from RFC 5766. +// +// RFC 5766 Section 15 +pub const CODE_FORBIDDEN: ErrorCode = ErrorCode(403); // Forbidden +pub const CODE_ALLOC_MISMATCH: ErrorCode = ErrorCode(437); // Allocation Mismatch +pub const CODE_WRONG_CREDENTIALS: ErrorCode = ErrorCode(441); // Wrong Credentials +pub const CODE_UNSUPPORTED_TRANS_PROTO: ErrorCode = ErrorCode(442); // Unsupported Transport Protocol +pub const CODE_ALLOC_QUOTA_REACHED: ErrorCode = ErrorCode(486); // Allocation Quota Reached +pub const CODE_INSUFFICIENT_CAPACITY: ErrorCode = ErrorCode(508); // Insufficient Capacity + +// Error codes from RFC 6062. +// +// RFC 6062 Section 6.3 +pub const CODE_CONN_ALREADY_EXISTS: ErrorCode = ErrorCode(446); +pub const CODE_CONN_TIMEOUT_OR_FAILURE: ErrorCode = ErrorCode(447); + +// Error codes from RFC 6156. +// +// RFC 6156 Section 10.2 +pub const CODE_ADDR_FAMILY_NOT_SUPPORTED: ErrorCode = ErrorCode(440); // Address Family not Supported +pub const CODE_PEER_ADDR_FAMILY_MISMATCH: ErrorCode = ErrorCode(443); // Peer Address Family Mismatch + +lazy_static! { + pub static ref ERROR_REASONS:HashMap> = + [ + (CODE_TRY_ALTERNATE, b"Try Alternate".to_vec()), + (CODE_BAD_REQUEST, b"Bad Request".to_vec()), + (CODE_UNAUTHORIZED, b"Unauthorized".to_vec()), + (CODE_UNKNOWN_ATTRIBUTE, b"Unknown Attribute".to_vec()), + (CODE_STALE_NONCE, b"Stale Nonce".to_vec()), + (CODE_SERVER_ERROR, b"Server Error".to_vec()), + (CODE_ROLE_CONFLICT, b"Role Conflict".to_vec()), + + // RFC 5766. + (CODE_FORBIDDEN, b"Forbidden".to_vec()), + (CODE_ALLOC_MISMATCH, b"Allocation Mismatch".to_vec()), + (CODE_WRONG_CREDENTIALS, b"Wrong Credentials".to_vec()), + (CODE_UNSUPPORTED_TRANS_PROTO, b"Unsupported Transport Protocol".to_vec()), + (CODE_ALLOC_QUOTA_REACHED, b"Allocation Quota Reached".to_vec()), + (CODE_INSUFFICIENT_CAPACITY, b"Insufficient Capacity".to_vec()), + + // RFC 6062. + (CODE_CONN_ALREADY_EXISTS, b"Connection Already Exists".to_vec()), + (CODE_CONN_TIMEOUT_OR_FAILURE, b"Connection Timeout or Failure".to_vec()), + + // RFC 6156. + (CODE_ADDR_FAMILY_NOT_SUPPORTED, b"Address Family not Supported".to_vec()), + (CODE_PEER_ADDR_FAMILY_MISMATCH, b"Peer Address Family Mismatch".to_vec()), + ].iter().cloned().collect(); + +} diff --git a/stun-patch/src/fingerprint.rs b/stun-patch/src/fingerprint.rs new file mode 100644 index 0000000..648c288 --- /dev/null +++ b/stun-patch/src/fingerprint.rs @@ -0,0 +1,64 @@ +#[cfg(test)] +mod fingerprint_test; + +use crc::{Crc, CRC_32_ISO_HDLC}; + +use crate::attributes::ATTR_FINGERPRINT; +use crate::checks::*; +use crate::error::*; +use crate::message::*; + +// FingerprintAttr represents FINGERPRINT attribute. +// +// RFC 5389 Section 15.5 +pub struct FingerprintAttr; + +// FINGERPRINT is shorthand for FingerprintAttr. +// +// Example: +// +// m := New() +// FINGERPRINT.add_to(m) +pub const FINGERPRINT: FingerprintAttr = FingerprintAttr {}; + +pub const FINGERPRINT_XOR_VALUE: u32 = 0x5354554e; +pub const FINGERPRINT_SIZE: usize = 4; // 32 bit + +// FingerprintValue returns CRC-32 of b XOR-ed by 0x5354554e. +// +// The value of the attribute is computed as the CRC-32 of the STUN message +// up to (but excluding) the FINGERPRINT attribute itself, XOR'ed with +// the 32-bit value 0x5354554e (the XOR helps in cases where an +// application packet is also using CRC-32 in it). +pub fn fingerprint_value(b: &[u8]) -> u32 { + let checksum = Crc::::new(&CRC_32_ISO_HDLC).checksum(b); + checksum ^ FINGERPRINT_XOR_VALUE // XOR +} + +impl Setter for FingerprintAttr { + // add_to adds fingerprint to message. + fn add_to(&self, m: &mut Message) -> Result<()> { + let l = m.length; + // length in header should include size of fingerprint attribute + m.length += (FINGERPRINT_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; // increasing length + m.write_length(); // writing Length to Raw + let val = fingerprint_value(&m.raw); + let b = val.to_be_bytes(); + m.length = l; + m.add(ATTR_FINGERPRINT, &b); + Ok(()) + } +} + +impl FingerprintAttr { + // Check reads fingerprint value from m and checks it, returning error if any. + // Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. + pub fn check(&self, m: &Message) -> Result<()> { + let b = m.get(ATTR_FINGERPRINT)?; + check_size(ATTR_FINGERPRINT, b.len(), FINGERPRINT_SIZE)?; + let val = u32::from_be_bytes([b[0], b[1], b[2], b[3]]); + let attr_start = m.raw.len() - (FINGERPRINT_SIZE + ATTRIBUTE_HEADER_SIZE); + let expected = fingerprint_value(&m.raw[..attr_start]); + check_fingerprint(val, expected) + } +} diff --git a/stun-patch/src/fingerprint/fingerprint_test.rs b/stun-patch/src/fingerprint/fingerprint_test.rs new file mode 100644 index 0000000..1ac589d --- /dev/null +++ b/stun-patch/src/fingerprint/fingerprint_test.rs @@ -0,0 +1,73 @@ +use super::*; +use crate::attributes::ATTR_SOFTWARE; +use crate::textattrs::TextAttribute; + +#[test] +fn fingerprint_uses_crc_32_iso_hdlc() -> Result<()> { + let mut m = Message::new(); + + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "software".to_owned(), + }; + a.add_to(&mut m)?; + m.write_header(); + + FINGERPRINT.add_to(&mut m)?; + m.write_header(); + + assert_eq!(&m.raw[0..m.raw.len()-8], b"\x00\x00\x00\x14\x21\x12\xA4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\x22\x00\x08\x73\x6F\x66\x74\x77\x61\x72\x65"); + + assert_eq!(m.raw[m.raw.len() - 4..], [0xe4, 0x4c, 0x33, 0xd9]); + + Ok(()) +} + +#[test] +fn test_fingerprint_check() -> Result<()> { + let mut m = Message::new(); + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "software".to_owned(), + }; + a.add_to(&mut m)?; + m.write_header(); + + FINGERPRINT.add_to(&mut m)?; + m.write_header(); + FINGERPRINT.check(&m)?; + m.raw[3] += 1; + + let result = FINGERPRINT.check(&m); + assert!(result.is_err(), "should error"); + + Ok(()) +} + +#[test] +fn test_fingerprint_check_bad() -> Result<()> { + let mut m = Message::new(); + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "software".to_owned(), + }; + a.add_to(&mut m)?; + m.write_header(); + + let result = FINGERPRINT.check(&m); + assert!(result.is_err(), "should error"); + + m.add(ATTR_FINGERPRINT, &[1, 2, 3]); + + let result = FINGERPRINT.check(&m); + if let Err(err) = result { + assert!( + is_attr_size_invalid(&err), + "IsAttrSizeInvalid should be true" + ); + } else { + panic!("Expected error, but got ok"); + } + + Ok(()) +} diff --git a/stun-patch/src/integrity.rs b/stun-patch/src/integrity.rs new file mode 100644 index 0000000..cd692da --- /dev/null +++ b/stun-patch/src/integrity.rs @@ -0,0 +1,118 @@ +#[cfg(test)] +mod integrity_test; + +use std::fmt; + +use md5::{Digest, Md5}; +use ring::hmac; + +use crate::attributes::*; +use crate::checks::*; +use crate::error::*; +use crate::message::*; + +// separator for credentials. +pub(crate) const CREDENTIALS_SEP: &str = ":"; + +// MessageIntegrity represents MESSAGE-INTEGRITY attribute. +// +// add_to and Check methods are using zero-allocation version of hmac, see +// newHMAC function and internal/hmac/pool.go. +// +// RFC 5389 Section 15.4 +#[derive(Default, Clone)] +pub struct MessageIntegrity(pub Vec); + +fn new_hmac(key: &[u8], message: &[u8]) -> Vec { + let mac = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key); + hmac::sign(&mac, message).as_ref().to_vec() +} + +impl fmt::Display for MessageIntegrity { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "KEY: 0x{:x?}", self.0) + } +} + +impl Setter for MessageIntegrity { + // add_to adds MESSAGE-INTEGRITY attribute to message. + // + // CPU costly, see BenchmarkMessageIntegrity_AddTo. + fn add_to(&self, m: &mut Message) -> Result<()> { + for a in &m.attributes.0 { + // Message should not contain FINGERPRINT attribute + // before MESSAGE-INTEGRITY. + if a.typ == ATTR_FINGERPRINT { + return Err(Error::ErrFingerprintBeforeIntegrity); + } + } + // The text used as input to HMAC is the STUN message, + // including the header, up to and including the attribute preceding the + // MESSAGE-INTEGRITY attribute. + let length = m.length; + // Adjusting m.Length to contain MESSAGE-INTEGRITY TLV. + m.length += (MESSAGE_INTEGRITY_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; + m.write_length(); // writing length to m.Raw + let v = new_hmac(&self.0, &m.raw); // calculating HMAC for adjusted m.Raw + m.length = length; // changing m.Length back + + m.add(ATTR_MESSAGE_INTEGRITY, &v); + + Ok(()) + } +} + +pub(crate) const MESSAGE_INTEGRITY_SIZE: usize = 20; + +impl MessageIntegrity { + // new_long_term_integrity returns new MessageIntegrity with key for long-term + // credentials. Password, username, and realm must be SASL-prepared. + pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { + let s = [username, realm, password].join(CREDENTIALS_SEP); + + let mut h = Md5::new(); + h.update(s.as_bytes()); + + MessageIntegrity(h.finalize().as_slice().to_vec()) + } + + // new_short_term_integrity returns new MessageIntegrity with key for short-term + // credentials. Password must be SASL-prepared. + pub fn new_short_term_integrity(password: String) -> Self { + MessageIntegrity(password.as_bytes().to_vec()) + } + + // Check checks MESSAGE-INTEGRITY attribute. + // + // CPU costly, see BenchmarkMessageIntegrity_Check. + pub fn check(&self, m: &mut Message) -> Result<()> { + let v = m.get(ATTR_MESSAGE_INTEGRITY)?; + + // Adjusting length in header to match m.Raw that was + // used when computing HMAC. + + let length = m.length as usize; + let mut after_integrity = false; + let mut size_reduced = 0; + + for a in &m.attributes.0 { + if after_integrity { + size_reduced += nearest_padded_value_length(a.length as usize); + size_reduced += ATTRIBUTE_HEADER_SIZE; + } + if a.typ == ATTR_MESSAGE_INTEGRITY { + after_integrity = true; + } + } + m.length -= size_reduced as u32; + m.write_length(); + // start_of_hmac should be first byte of integrity attribute. + let start_of_hmac = MESSAGE_HEADER_SIZE + m.length as usize + - (ATTRIBUTE_HEADER_SIZE + MESSAGE_INTEGRITY_SIZE); + let b = &m.raw[..start_of_hmac]; // data before integrity attribute + let expected = new_hmac(&self.0, b); + m.length = length as u32; + m.write_length(); // writing length back + check_hmac(&v, &expected) + } +} diff --git a/stun-patch/src/integrity/integrity_test.rs b/stun-patch/src/integrity/integrity_test.rs new file mode 100644 index 0000000..e085cfa --- /dev/null +++ b/stun-patch/src/integrity/integrity_test.rs @@ -0,0 +1,93 @@ +use super::*; +use crate::agent::TransactionId; +use crate::attributes::ATTR_SOFTWARE; +use crate::fingerprint::FINGERPRINT; +use crate::textattrs::TextAttribute; + +#[test] +fn test_message_integrity_add_to_simple() -> Result<()> { + let i = MessageIntegrity::new_long_term_integrity( + "user".to_owned(), + "realm".to_owned(), + "pass".to_owned(), + ); + let expected = vec![ + 0x84, 0x93, 0xfb, 0xc5, 0x3b, 0xa5, 0x82, 0xfb, 0x4c, 0x04, 0x4c, 0x45, 0x6b, 0xdc, 0x40, + 0xeb, + ]; + assert_eq!(i.0, expected, "{}", Error::ErrIntegrityMismatch); + + //"Check" + { + let mut m = Message::new(); + m.write_header(); + i.add_to(&mut m)?; + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "software".to_owned(), + }; + a.add_to(&mut m)?; + m.write_header(); + + let mut d_m = Message::new(); + d_m.raw.clone_from(&m.raw); + d_m.decode()?; + i.check(&mut d_m)?; + + d_m.raw[24] += 12; // HMAC now invalid + d_m.decode()?; + let result = i.check(&mut d_m); + assert!(result.is_err(), "should be invalid"); + } + + Ok(()) +} + +#[test] +fn test_message_integrity_with_fingerprint() -> Result<()> { + let mut m = Message::new(); + m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0]); + m.write_header(); + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "software".to_owned(), + }; + a.add_to(&mut m)?; + + let i = MessageIntegrity::new_short_term_integrity("pwd".to_owned()); + assert_eq!(i.to_string(), "KEY: 0x[70, 77, 64]", "bad string {i}"); + let result = i.check(&mut m); + assert!(result.is_err(), "should error"); + + i.add_to(&mut m)?; + FINGERPRINT.add_to(&mut m)?; + i.check(&mut m)?; + m.raw[24] = 33; + m.decode()?; + let result = i.check(&mut m); + assert!(result.is_err(), "mismatch expected"); + + Ok(()) +} + +#[test] +fn test_message_integrity() -> Result<()> { + let mut m = Message::new(); + let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); + m.write_header(); + i.add_to(&mut m)?; + m.get(ATTR_MESSAGE_INTEGRITY)?; + Ok(()) +} + +#[test] +fn test_message_integrity_before_fingerprint() -> Result<()> { + let mut m = Message::new(); + m.write_header(); + FINGERPRINT.add_to(&mut m)?; + let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let result = i.add_to(&mut m); + assert!(result.is_err(), "should error"); + + Ok(()) +} diff --git a/stun-patch/src/lib.rs b/stun-patch/src/lib.rs new file mode 100644 index 0000000..f13ff34 --- /dev/null +++ b/stun-patch/src/lib.rs @@ -0,0 +1,26 @@ +#![warn(rust_2018_idioms)] +#![allow(dead_code)] + +#[macro_use] +extern crate lazy_static; + +pub mod addr; +pub mod agent; +pub mod attributes; +pub mod checks; +pub mod client; +mod error; +pub mod error_code; +pub mod fingerprint; +pub mod integrity; +pub mod message; +pub mod textattrs; +pub mod uattrs; +pub mod uri; +pub mod xoraddr; + +// IANA assigned ports for "stun" protocol. +pub const DEFAULT_PORT: u16 = 3478; +pub const DEFAULT_TLS_PORT: u16 = 5349; + +pub use error::Error; diff --git a/stun-patch/src/message.rs b/stun-patch/src/message.rs new file mode 100644 index 0000000..6ac245e --- /dev/null +++ b/stun-patch/src/message.rs @@ -0,0 +1,626 @@ +#[cfg(test)] +mod message_test; + +use std::fmt; +use std::io::{Read, Write}; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use rand::Rng; + +use crate::agent::*; +use crate::attributes::*; +use crate::error::*; + +// MAGIC_COOKIE is fixed value that aids in distinguishing STUN packets +// from packets of other protocols when STUN is multiplexed with those +// other protocols on the same Port. +// +// The magic cookie field MUST contain the fixed value 0x2112A442 in +// network byte order. +// +// Defined in "STUN Message Structure", section 6. +pub const MAGIC_COOKIE: u32 = 0x2112A442; +pub const ATTRIBUTE_HEADER_SIZE: usize = 4; +pub const MESSAGE_HEADER_SIZE: usize = 20; + +// TRANSACTION_ID_SIZE is length of transaction id array (in bytes). +pub const TRANSACTION_ID_SIZE: usize = 12; // 96 bit + +// Interfaces that are implemented by message attributes, shorthands for them, +// or helpers for message fields as type or transaction id. +pub trait Setter { + // Setter sets *Message attribute. + fn add_to(&self, m: &mut Message) -> Result<()>; +} + +// Getter parses attribute from *Message. +pub trait Getter { + fn get_from(&mut self, m: &Message) -> Result<()>; +} + +// Checker checks *Message attribute. +pub trait Checker { + fn check(&self, m: &Message) -> Result<()>; +} + +// is_message returns true if b looks like STUN message. +// Useful for multiplexing. is_message does not guarantee +// that decoding will be successful. +pub fn is_message(b: &[u8]) -> bool { + b.len() >= MESSAGE_HEADER_SIZE && u32::from_be_bytes([b[4], b[5], b[6], b[7]]) == MAGIC_COOKIE +} +// Message represents a single STUN packet. It uses aggressive internal +// buffering to enable zero-allocation encoding and decoding, +// so there are some usage constraints: +// +// Message, its fields, results of m.Get or any attribute a.GetFrom +// are valid only until Message.Raw is not modified. +#[derive(Default, Debug, Clone)] +pub struct Message { + pub typ: MessageType, + pub length: u32, // len(Raw) not including header + pub transaction_id: TransactionId, + pub attributes: Attributes, + pub raw: Vec, +} + +impl fmt::Display for Message { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let t_id = BASE64_STANDARD.encode(self.transaction_id.0); + write!( + f, + "{} l={} attrs={} id={}", + self.typ, + self.length, + self.attributes.0.len(), + t_id + ) + } +} + +// Equal returns true if Message b equals to m. +// Ignores m.Raw. +impl PartialEq for Message { + fn eq(&self, other: &Self) -> bool { + if self.typ != other.typ { + return false; + } + if self.transaction_id != other.transaction_id { + return false; + } + if self.length != other.length { + return false; + } + if self.attributes != other.attributes { + return false; + } + true + } +} + +const DEFAULT_RAW_CAPACITY: usize = 120; + +impl Setter for Message { + // add_to sets b.TransactionID to m.TransactionID. + // + // Implements Setter to aid in crafting responses. + fn add_to(&self, b: &mut Message) -> Result<()> { + b.transaction_id = self.transaction_id; + b.write_transaction_id(); + Ok(()) + } +} + +impl Message { + // New returns *Message with pre-allocated Raw. + pub fn new() -> Self { + Message { + raw: { + let mut raw = Vec::with_capacity(DEFAULT_RAW_CAPACITY); + raw.extend_from_slice(&[0; MESSAGE_HEADER_SIZE]); + raw + }, + ..Default::default() + } + } + + // marshal_binary implements the encoding.BinaryMarshaler interface. + pub fn marshal_binary(&self) -> Result> { + // We can't return m.Raw, allocation is expected by implicit interface + // contract induced by other implementations. + Ok(self.raw.clone()) + } + + // unmarshal_binary implements the encoding.BinaryUnmarshaler interface. + pub fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { + // We can't retain data, copy is expected by interface contract. + self.raw.clear(); + self.raw.extend_from_slice(data); + self.decode() + } + + // NewTransactionID sets m.TransactionID to random value from crypto/rand + // and returns error if any. + pub fn new_transaction_id(&mut self) -> Result<()> { + rand::thread_rng().fill(&mut self.transaction_id.0); + self.write_transaction_id(); + Ok(()) + } + + // Reset resets Message, attributes and underlying buffer length. + pub fn reset(&mut self) { + self.raw.clear(); + self.length = 0; + self.attributes.0.clear(); + } + + // grow ensures that internal buffer has n length. + fn grow(&mut self, n: usize, resize: bool) { + if self.raw.len() >= n { + if resize { + self.raw.resize(n, 0); + } + return; + } + self.raw.extend_from_slice(&vec![0; n - self.raw.len()]); + } + + // Add appends new attribute to message. Not goroutine-safe. + // + // Value of attribute is copied to internal buffer so + // it is safe to reuse v. + pub fn add(&mut self, t: AttrType, v: &[u8]) { + // Allocating buffer for TLV (type-length-value). + // T = t, L = len(v), V = v. + // m.Raw will look like: + // [0:20] <- message header + // [20:20+m.Length] <- existing message attributes + // [20+m.Length:20+m.Length+len(v) + 4] <- allocated buffer for new TLV + // [first:last] <- same as previous + // [0 1|2 3|4 4 + len(v)] <- mapping for allocated buffer + // T L V + let alloc_size = ATTRIBUTE_HEADER_SIZE + v.len(); // ~ len(TLV) = len(TL) + len(V) + let first = MESSAGE_HEADER_SIZE + self.length as usize; // first byte number + let mut last = first + alloc_size; // last byte number + self.grow(last, true); // growing cap(Raw) to fit TLV + self.length += alloc_size as u32; // rendering length change + + // Encoding attribute TLV to allocated buffer. + let buf = &mut self.raw[first..last]; + buf[0..2].copy_from_slice(&t.value().to_be_bytes()); // T + buf[2..4].copy_from_slice(&(v.len() as u16).to_be_bytes()); // L + + let value = &mut buf[ATTRIBUTE_HEADER_SIZE..]; + value.copy_from_slice(v); // V + + let attr = RawAttribute { + typ: t, // T + length: v.len() as u16, // L + value: value.to_vec(), // V + }; + + // Checking that attribute value needs padding. + if attr.length as usize % PADDING != 0 { + // Performing padding. + let bytes_to_add = nearest_padded_value_length(v.len()) - v.len(); + last += bytes_to_add; + self.grow(last, true); + // setting all padding bytes to zero + // to prevent data leak from previous + // data in next bytes_to_add bytes + let buf = &mut self.raw[last - bytes_to_add..last]; + for b in buf { + *b = 0; + } + self.length += bytes_to_add as u32; // rendering length change + } + self.attributes.0.push(attr); + self.write_length(); + } + + // WriteLength writes m.Length to m.Raw. + pub fn write_length(&mut self) { + self.grow(4, false); + self.raw[2..4].copy_from_slice(&(self.length as u16).to_be_bytes()); + } + + // WriteHeader writes header to underlying buffer. Not goroutine-safe. + pub fn write_header(&mut self) { + self.grow(MESSAGE_HEADER_SIZE, false); + + self.write_type(); + self.write_length(); + self.raw[4..8].copy_from_slice(&MAGIC_COOKIE.to_be_bytes()); // magic cookie + self.raw[8..MESSAGE_HEADER_SIZE].copy_from_slice(&self.transaction_id.0); + // transaction ID + } + + // WriteTransactionID writes m.TransactionID to m.Raw. + pub fn write_transaction_id(&mut self) { + self.raw[8..MESSAGE_HEADER_SIZE].copy_from_slice(&self.transaction_id.0); + // transaction ID + } + + // WriteAttributes encodes all m.Attributes to m. + pub fn write_attributes(&mut self) { + let attributes: Vec = self.attributes.0.drain(..).collect(); + for a in &attributes { + self.add(a.typ, &a.value); + } + self.attributes = Attributes(attributes); + } + + // WriteType writes m.Type to m.Raw. + pub fn write_type(&mut self) { + self.grow(2, false); + self.raw[..2].copy_from_slice(&self.typ.value().to_be_bytes()); // message type + } + + // SetType sets m.Type and writes it to m.Raw. + pub fn set_type(&mut self, t: MessageType) { + self.typ = t; + self.write_type(); + } + + // Encode re-encodes message into m.Raw. + pub fn encode(&mut self) { + self.raw.clear(); + self.write_header(); + self.length = 0; + self.write_attributes(); + } + + // Decode decodes m.Raw into m. + pub fn decode(&mut self) -> Result<()> { + // decoding message header + let buf = &self.raw; + if buf.len() < MESSAGE_HEADER_SIZE { + return Err(Error::ErrUnexpectedHeaderEof); + } + + let t = u16::from_be_bytes([buf[0], buf[1]]); // first 2 bytes + let size = u16::from_be_bytes([buf[2], buf[3]]) as usize; // second 2 bytes + let cookie = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); // last 4 bytes + let full_size = MESSAGE_HEADER_SIZE + size; // len(m.Raw) + + if cookie != MAGIC_COOKIE { + return Err(Error::Other(format!( + "{cookie:x} is invalid magic cookie (should be {MAGIC_COOKIE:x})" + ))); + } + if buf.len() < full_size { + return Err(Error::Other(format!( + "buffer length {} is less than {} (expected message size)", + buf.len(), + full_size + ))); + } + + // saving header data + self.typ.read_value(t); + self.length = size as u32; + self.transaction_id + .0 + .copy_from_slice(&buf[8..MESSAGE_HEADER_SIZE]); + + self.attributes.0.clear(); + let mut offset = 0; + let mut b = &buf[MESSAGE_HEADER_SIZE..full_size]; + + while offset < size { + // checking that we have enough bytes to read header + if b.len() < ATTRIBUTE_HEADER_SIZE { + return Err(Error::Other(format!( + "buffer length {} is less than {} (expected header size)", + b.len(), + ATTRIBUTE_HEADER_SIZE + ))); + } + + let mut a = RawAttribute { + typ: compat_attr_type(u16::from_be_bytes([b[0], b[1]])), // first 2 bytes + length: u16::from_be_bytes([b[2], b[3]]), // second 2 bytes + ..Default::default() + }; + let a_l = a.length as usize; // attribute length + let a_buff_l = nearest_padded_value_length(a_l); // expected buffer length (with padding) + + b = &b[ATTRIBUTE_HEADER_SIZE..]; // slicing again to simplify value read + offset += ATTRIBUTE_HEADER_SIZE; + if b.len() < a_buff_l { + // checking size + return Err(Error::Other(format!( + "buffer length {} is less than {} (expected value size for {})", + b.len(), + a_buff_l, + a.typ + ))); + } + a.value = b[..a_l].to_vec(); + offset += a_buff_l; + b = &b[a_buff_l..]; + + self.attributes.0.push(a); + } + + Ok(()) + } + + // WriteTo implements WriterTo via calling Write(m.Raw) on w and returning + // call result. + pub fn write_to(&self, writer: &mut W) -> Result { + let n = writer.write(&self.raw)?; + Ok(n) + } + + // ReadFrom implements ReaderFrom. Reads message from r into m.Raw, + // Decodes it and return error if any. If m.Raw is too small, will return + // ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. + // + // Can return *DecodeErr while decoding too. + pub fn read_from(&mut self, reader: &mut R) -> Result { + let mut t_buf = vec![0; DEFAULT_RAW_CAPACITY]; + let n = reader.read(&mut t_buf)?; + self.raw = t_buf[..n].to_vec(); + self.decode()?; + Ok(n) + } + + // Write decodes message and return error if any. + // + // Any error is unrecoverable, but message could be partially decoded. + pub fn write(&mut self, t_buf: &[u8]) -> Result { + self.raw.clear(); + self.raw.extend_from_slice(t_buf); + self.decode()?; + Ok(t_buf.len()) + } + + // CloneTo clones m to b securing any further m mutations. + pub fn clone_to(&self, b: &mut Message) -> Result<()> { + b.raw.clear(); + b.raw.extend_from_slice(&self.raw); + b.decode() + } + + // Contains return true if message contain t attribute. + pub fn contains(&self, t: AttrType) -> bool { + for a in &self.attributes.0 { + if a.typ == t { + return true; + } + } + false + } + + // get returns byte slice that represents attribute value, + // if there is no attribute with such type, + // ErrAttributeNotFound is returned. + pub fn get(&self, t: AttrType) -> Result> { + let (v, ok) = self.attributes.get(t); + if ok { + Ok(v.value) + } else { + Err(Error::ErrAttributeNotFound) + } + } + + // Build resets message and applies setters to it in batch, returning on + // first error. To prevent allocations, pass pointers to values. + // + // Example: + // var ( + // t = BindingRequest + // username = NewUsername("username") + // nonce = NewNonce("nonce") + // realm = NewRealm("example.org") + // ) + // m := new(Message) + // m.Build(t, username, nonce, realm) // 4 allocations + // m.Build(&t, &username, &nonce, &realm) // 0 allocations + // + // See BenchmarkBuildOverhead. + pub fn build(&mut self, setters: &[Box]) -> Result<()> { + self.reset(); + self.write_header(); + for s in setters { + s.add_to(self)?; + } + Ok(()) + } + + // Check applies checkers to message in batch, returning on first error. + pub fn check(&self, checkers: &[C]) -> Result<()> { + for c in checkers { + c.check(self)?; + } + Ok(()) + } + + // Parse applies getters to message in batch, returning on first error. + pub fn parse(&self, getters: &mut [G]) -> Result<()> { + for c in getters { + c.get_from(self)?; + } + Ok(()) + } +} + +// MessageClass is 8-bit representation of 2-bit class of STUN Message Class. +#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +pub struct MessageClass(u8); + +// Possible values for message class in STUN Message Type. +pub const CLASS_REQUEST: MessageClass = MessageClass(0x00); // 0b00 +pub const CLASS_INDICATION: MessageClass = MessageClass(0x01); // 0b01 +pub const CLASS_SUCCESS_RESPONSE: MessageClass = MessageClass(0x02); // 0b10 +pub const CLASS_ERROR_RESPONSE: MessageClass = MessageClass(0x03); // 0b11 + +impl fmt::Display for MessageClass { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match *self { + CLASS_REQUEST => "request", + CLASS_INDICATION => "indication", + CLASS_SUCCESS_RESPONSE => "success response", + CLASS_ERROR_RESPONSE => "error response", + _ => "unknown message class", + }; + + write!(f, "{s}") + } +} + +// Method is uint16 representation of 12-bit STUN method. +#[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +pub struct Method(u16); + +// Possible methods for STUN Message. +pub const METHOD_BINDING: Method = Method(0x001); +pub const METHOD_ALLOCATE: Method = Method(0x003); +pub const METHOD_REFRESH: Method = Method(0x004); +pub const METHOD_SEND: Method = Method(0x006); +pub const METHOD_DATA: Method = Method(0x007); +pub const METHOD_CREATE_PERMISSION: Method = Method(0x008); +pub const METHOD_CHANNEL_BIND: Method = Method(0x009); + +// Methods from RFC 6062. +pub const METHOD_CONNECT: Method = Method(0x000a); +pub const METHOD_CONNECTION_BIND: Method = Method(0x000b); +pub const METHOD_CONNECTION_ATTEMPT: Method = Method(0x000c); + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let unknown = format!("0x{:x}", self.0); + + let s = match *self { + METHOD_BINDING => "Binding", + METHOD_ALLOCATE => "Allocate", + METHOD_REFRESH => "Refresh", + METHOD_SEND => "Send", + METHOD_DATA => "Data", + METHOD_CREATE_PERMISSION => "CreatePermission", + METHOD_CHANNEL_BIND => "ChannelBind", + + // RFC 6062. + METHOD_CONNECT => "Connect", + METHOD_CONNECTION_BIND => "ConnectionBind", + METHOD_CONNECTION_ATTEMPT => "ConnectionAttempt", + _ => unknown.as_str(), + }; + + write!(f, "{s}") + } +} + +// MessageType is STUN Message Type Field. +#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +pub struct MessageType { + pub method: Method, // e.g. binding + pub class: MessageClass, // e.g. request +} + +// Common STUN message types. +// Binding request message type. +pub const BINDING_REQUEST: MessageType = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, +}; +// Binding success response message type +pub const BINDING_SUCCESS: MessageType = MessageType { + method: METHOD_BINDING, + class: CLASS_SUCCESS_RESPONSE, +}; +// Binding error response message type. +pub const BINDING_ERROR: MessageType = MessageType { + method: METHOD_BINDING, + class: CLASS_ERROR_RESPONSE, +}; + +impl fmt::Display for MessageType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.method, self.class) + } +} + +const METHOD_ABITS: u16 = 0xf; // 0b0000000000001111 +const METHOD_BBITS: u16 = 0x70; // 0b0000000001110000 +const METHOD_DBITS: u16 = 0xf80; // 0b0000111110000000 + +const METHOD_BSHIFT: u16 = 1; +const METHOD_DSHIFT: u16 = 2; + +const FIRST_BIT: u16 = 0x1; +const SECOND_BIT: u16 = 0x2; + +const C0BIT: u16 = FIRST_BIT; +const C1BIT: u16 = SECOND_BIT; + +const CLASS_C0SHIFT: u16 = 4; +const CLASS_C1SHIFT: u16 = 7; + +impl Setter for MessageType { + // add_to sets m type to t. + fn add_to(&self, m: &mut Message) -> Result<()> { + m.set_type(*self); + Ok(()) + } +} + +impl MessageType { + // NewType returns new message type with provided method and class. + pub fn new(method: Method, class: MessageClass) -> Self { + MessageType { method, class } + } + + // Value returns bit representation of messageType. + pub fn value(&self) -> u16 { + // 0 1 + // 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + // +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ + // |M |M |M|M|M|C|M|M|M|C|M|M|M|M| + // |11|10|9|8|7|1|6|5|4|0|3|2|1|0| + // +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ + // Figure 3: Format of STUN Message Type Field + + // Warning: Abandon all hope ye who enter here. + // Splitting M into A(M0-M3), B(M4-M6), D(M7-M11). + let method = self.method.0; + let a = method & METHOD_ABITS; // A = M * 0b0000000000001111 (right 4 bits) + let b = method & METHOD_BBITS; // B = M * 0b0000000001110000 (3 bits after A) + let d = method & METHOD_DBITS; // D = M * 0b0000111110000000 (5 bits after B) + + // Shifting to add "holes" for C0 (at 4 bit) and C1 (8 bit). + let method = a + (b << METHOD_BSHIFT) + (d << METHOD_DSHIFT); + + // C0 is zero bit of C, C1 is first bit. + // C0 = C * 0b01, C1 = (C * 0b10) >> 1 + // Ct = C0 << 4 + C1 << 8. + // Optimizations: "((C * 0b10) >> 1) << 8" as "(C * 0b10) << 7" + // We need C0 shifted by 4, and C1 by 8 to fit "11" and "7" positions + // (see figure 3). + let c = self.class.0 as u16; + let c0 = (c & C0BIT) << CLASS_C0SHIFT; + let c1 = (c & C1BIT) << CLASS_C1SHIFT; + let class = c0 + c1; + + method + class + } + + // ReadValue decodes uint16 into MessageType. + pub fn read_value(&mut self, value: u16) { + // Decoding class. + // We are taking first bit from v >> 4 and second from v >> 7. + let c0 = (value >> CLASS_C0SHIFT) & C0BIT; + let c1 = (value >> CLASS_C1SHIFT) & C1BIT; + let class = c0 + c1; + self.class = MessageClass(class as u8); + + // Decoding method. + let a = value & METHOD_ABITS; // A(M0-M3) + let b = (value >> METHOD_BSHIFT) & METHOD_BBITS; // B(M4-M6) + let d = (value >> METHOD_DSHIFT) & METHOD_DBITS; // D(M7-M11) + let m = a + b + d; + self.method = Method(m); + } +} diff --git a/stun-patch/src/message/message_test.rs b/stun-patch/src/message/message_test.rs new file mode 100644 index 0000000..afd388e --- /dev/null +++ b/stun-patch/src/message/message_test.rs @@ -0,0 +1,744 @@ +use std::io::{BufReader, BufWriter}; + +use super::*; +use crate::fingerprint::FINGERPRINT; +use crate::integrity::MessageIntegrity; +use crate::textattrs::TextAttribute; +use crate::xoraddr::*; + +#[test] +fn test_message_buffer() -> Result<()> { + let mut m = Message::new(); + m.typ = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }; + m.transaction_id = TransactionId::new(); + m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); + m.write_header(); + + let mut m_decoded = Message::new(); + let mut reader = BufReader::new(m.raw.as_slice()); + m_decoded.read_from(&mut reader)?; + + assert_eq!(m_decoded, m, "{m_decoded} != {m}"); + + Ok(()) +} + +#[test] +fn test_message_type_value() -> Result<()> { + let tests = vec![ + ( + MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }, + 0x0001, + ), + ( + MessageType { + method: METHOD_BINDING, + class: CLASS_SUCCESS_RESPONSE, + }, + 0x0101, + ), + ( + MessageType { + method: METHOD_BINDING, + class: CLASS_ERROR_RESPONSE, + }, + 0x0111, + ), + ( + MessageType { + method: Method(0xb6d), + class: MessageClass(0x3), + }, + 0x2ddd, + ), + ]; + + for (input, output) in tests { + let b = input.value(); + assert_eq!(b, output, "Value({input}) -> {b}, want {output}"); + } + + Ok(()) +} + +#[test] +fn test_message_type_read_value() -> Result<()> { + let tests = vec![ + ( + 0x0001, + MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }, + ), + ( + 0x0101, + MessageType { + method: METHOD_BINDING, + class: CLASS_SUCCESS_RESPONSE, + }, + ), + ( + 0x0111, + MessageType { + method: METHOD_BINDING, + class: CLASS_ERROR_RESPONSE, + }, + ), + ]; + + for (input, output) in tests { + let mut m = MessageType::default(); + m.read_value(input); + assert_eq!(m, output, "ReadValue({input}) -> {m}, want {output}"); + } + + Ok(()) +} + +#[test] +fn test_message_type_read_write_value() -> Result<()> { + let tests = vec![ + MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }, + MessageType { + method: METHOD_BINDING, + class: CLASS_SUCCESS_RESPONSE, + }, + MessageType { + method: METHOD_BINDING, + class: CLASS_ERROR_RESPONSE, + }, + MessageType { + method: Method(0x12), + class: CLASS_ERROR_RESPONSE, + }, + ]; + + for test in tests { + let mut m = MessageType::default(); + let v = test.value(); + m.read_value(v); + assert_eq!(m, test, "ReadValue({test} -> {v}) = {m}, should be {test}"); + } + + Ok(()) +} + +#[test] +fn test_message_write_to() -> Result<()> { + let mut m = Message::new(); + m.typ = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }; + m.transaction_id = TransactionId::new(); + m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); + m.write_header(); + let mut buf = vec![]; + { + let mut writer = BufWriter::<&mut Vec>::new(buf.as_mut()); + m.write_to(&mut writer)?; + } + + let mut m_decoded = Message::new(); + let mut reader = BufReader::new(buf.as_slice()); + m_decoded.read_from(&mut reader)?; + assert_eq!(m_decoded, m, "{m_decoded} != {m}"); + + Ok(()) +} + +#[test] +fn test_message_cookie() -> Result<()> { + let buf = vec![0; 20]; + let mut m_decoded = Message::new(); + let mut reader = BufReader::new(buf.as_slice()); + let result = m_decoded.read_from(&mut reader); + assert!(result.is_err(), "should error"); + + Ok(()) +} + +#[test] +fn test_message_length_less_header_size() -> Result<()> { + let buf = vec![0; 8]; + let mut m_decoded = Message::new(); + let mut reader = BufReader::new(buf.as_slice()); + let result = m_decoded.read_from(&mut reader); + assert!(result.is_err(), "should error"); + + Ok(()) +} + +#[test] +fn test_message_bad_length() -> Result<()> { + let m_type = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }; + let mut m = Message { + typ: m_type, + length: 4, + transaction_id: TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]), + ..Default::default() + }; + m.add(AttrType(0x1), &[1, 2]); + m.write_header(); + m.raw[20 + 3] = 10; // set attr length = 10 + + let mut m_decoded = Message::new(); + let result = m_decoded.write(&m.raw); + assert!(result.is_err(), "should error"); + + Ok(()) +} + +#[test] +fn test_message_attr_length_less_than_header() -> Result<()> { + let m_type = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }; + let message_attribute = RawAttribute { + length: 2, + value: vec![1, 2], + typ: AttrType(0x1), + }; + let message_attributes = Attributes(vec![message_attribute]); + let mut m = Message { + typ: m_type, + transaction_id: TransactionId::new(), + attributes: message_attributes, + ..Default::default() + }; + m.encode(); + + let mut m_decoded = Message::new(); + m.raw[3] = 2; // rewrite to bad length + + let mut reader = BufReader::new(&m.raw[..20 + 2]); + let result = m_decoded.read_from(&mut reader); + assert!(result.is_err(), "should be error"); + + Ok(()) +} + +#[test] +fn test_message_attr_size_less_than_length() -> Result<()> { + let m_type = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }; + let message_attribute = RawAttribute { + length: 4, + value: vec![1, 2, 3, 4], + typ: AttrType(0x1), + }; + let message_attributes = Attributes(vec![message_attribute]); + let mut m = Message { + typ: m_type, + transaction_id: TransactionId::new(), + attributes: message_attributes, + ..Default::default() + }; + m.write_attributes(); + m.write_header(); + m.raw[3] = 5; // rewrite to bad length + + let mut m_decoded = Message::new(); + let mut reader = BufReader::new(&m.raw[..20 + 5]); + let result = m_decoded.read_from(&mut reader); + assert!(result.is_err(), "should be error"); + + Ok(()) +} + +#[test] +fn test_message_read_from_error() -> Result<()> { + let mut m_decoded = Message::new(); + let buf = vec![]; + let mut reader = BufReader::new(buf.as_slice()); + let result = m_decoded.read_from(&mut reader); + assert!(result.is_err(), "should be error"); + + Ok(()) +} + +#[test] +fn test_message_class_string() -> Result<()> { + let v = vec![ + CLASS_REQUEST, + CLASS_ERROR_RESPONSE, + CLASS_SUCCESS_RESPONSE, + CLASS_INDICATION, + ]; + + for k in v { + if k.to_string() == *"unknown message class" { + panic!("bad stringer {k}"); + } + } + + // should panic + let p = MessageClass(0x05).to_string(); + assert_eq!(p, "unknown message class", "should be error {p}"); + + Ok(()) +} + +#[test] +fn test_attr_type_string() -> Result<()> { + let v = vec![ + ATTR_MAPPED_ADDRESS, + ATTR_USERNAME, + ATTR_ERROR_CODE, + ATTR_MESSAGE_INTEGRITY, + ATTR_UNKNOWN_ATTRIBUTES, + ATTR_REALM, + ATTR_NONCE, + ATTR_XORMAPPED_ADDRESS, + ATTR_SOFTWARE, + ATTR_ALTERNATE_SERVER, + ATTR_FINGERPRINT, + ]; + for k in v { + assert!(!k.to_string().starts_with("0x"), "bad stringer"); + } + + let v_non_standard = AttrType(0x512); + assert!( + v_non_standard.to_string().starts_with("0x512"), + "bad prefix" + ); + + Ok(()) +} + +#[test] +fn test_method_string() -> Result<()> { + assert_eq!( + METHOD_BINDING.to_string(), + "Binding".to_owned(), + "binding is not binding!" + ); + assert_eq!( + Method(0x616).to_string(), + "0x616".to_owned(), + "Bad stringer {}", + Method(0x616) + ); + + Ok(()) +} + +#[test] +fn test_attribute_equal() -> Result<()> { + let a = RawAttribute { + length: 2, + value: vec![0x1, 0x2], + ..Default::default() + }; + let b = RawAttribute { + length: 2, + value: vec![0x1, 0x2], + ..Default::default() + }; + assert_eq!(a, b, "should equal"); + + assert_ne!( + a, + RawAttribute { + typ: AttrType(0x2), + ..Default::default() + }, + "should not equal" + ); + assert_ne!( + a, + RawAttribute { + length: 0x2, + ..Default::default() + }, + "should not equal" + ); + assert_ne!( + a, + RawAttribute { + length: 0x3, + ..Default::default() + }, + "should not equal" + ); + assert_ne!( + a, + RawAttribute { + length: 0x2, + value: vec![0x1, 0x3], + ..Default::default() + }, + "should not equal" + ); + + Ok(()) +} + +#[test] +fn test_message_equal() -> Result<()> { + let attr = RawAttribute { + length: 2, + value: vec![0x1, 0x2], + typ: AttrType(0x1), + }; + let attrs = Attributes(vec![attr]); + let a = Message { + attributes: attrs.clone(), + length: 4 + 2, + ..Default::default() + }; + let b = Message { + attributes: attrs.clone(), + length: 4 + 2, + ..Default::default() + }; + assert_eq!(a, b, "should equal"); + assert_ne!( + a, + Message { + typ: MessageType { + class: MessageClass(128), + ..Default::default() + }, + ..Default::default() + }, + "should not equal" + ); + + let t_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + assert_ne!( + a, + Message { + transaction_id: t_id, + ..Default::default() + }, + "should not equal" + ); + assert_ne!( + a, + Message { + length: 3, + ..Default::default() + }, + "should not equal" + ); + + let t_attrs = Attributes(vec![RawAttribute { + length: 1, + value: vec![0x1], + typ: AttrType(0x1), + }]); + assert_ne!( + a, + Message { + attributes: t_attrs, + length: 4 + 2, + ..Default::default() + }, + "should not equal" + ); + + let t_attrs = Attributes(vec![RawAttribute { + length: 2, + value: vec![0x1, 0x1], + typ: AttrType(0x2), + }]); + assert_ne!( + a, + Message { + attributes: t_attrs, + length: 4 + 2, + ..Default::default() + }, + "should not equal" + ); + + //"Nil attributes" + { + let a = Message { + length: 4 + 2, + ..Default::default() + }; + let mut b = Message { + attributes: attrs, + length: 4 + 2, + ..Default::default() + }; + + assert_ne!(a, b, "should not equal"); + assert_ne!(b, a, "should not equal"); + b.attributes = Attributes::default(); + assert_eq!(a, b, "should equal"); + } + + //"Attributes length" + { + let attr = RawAttribute { + length: 2, + value: vec![0x1, 0x2], + typ: AttrType(0x1), + }; + let attr1 = RawAttribute { + length: 2, + value: vec![0x1, 0x2], + typ: AttrType(0x1), + }; + let a = Message { + attributes: Attributes(vec![attr.clone()]), + length: 4 + 2, + ..Default::default() + }; + let b = Message { + attributes: Attributes(vec![attr, attr1]), + length: 4 + 2, + ..Default::default() + }; + assert_ne!(a, b, "should not equal"); + } + + //"Attributes values" + { + let attr = RawAttribute { + length: 2, + value: vec![0x1, 0x2], + typ: AttrType(0x1), + }; + let attr1 = RawAttribute { + length: 2, + value: vec![0x1, 0x1], + typ: AttrType(0x1), + }; + let a = Message { + attributes: Attributes(vec![attr.clone(), attr.clone()]), + length: 4 + 2, + ..Default::default() + }; + let b = Message { + attributes: Attributes(vec![attr, attr1]), + length: 4 + 2, + ..Default::default() + }; + assert_ne!(a, b, "should not equal"); + } + + Ok(()) +} + +#[test] +fn test_message_grow() -> Result<()> { + let mut m = Message::new(); + m.grow(512, false); + assert_eq!(m.raw.len(), 512, "Bad length {}", m.raw.len()); + + Ok(()) +} + +#[test] +fn test_message_grow_smaller() -> Result<()> { + let mut m = Message::new(); + m.grow(2, false); + assert!(m.raw.capacity() >= 20, "Bad capacity {}", m.raw.capacity()); + + assert!(m.raw.len() >= 20, "Bad length {}", m.raw.len()); + + Ok(()) +} + +#[test] +fn test_message_string() -> Result<()> { + let m = Message::new(); + assert_ne!(m.to_string(), "", "bad string"); + + Ok(()) +} + +#[test] +fn test_is_message() -> Result<()> { + let mut m = Message::new(); + let a = TextAttribute { + attr: ATTR_SOFTWARE, + text: "software".to_owned(), + }; + a.add_to(&mut m)?; + m.write_header(); + + let tests = vec![ + (vec![], false), // 0 + (vec![1, 2, 3], false), // 1 + (vec![1, 2, 4], false), // 2 + (vec![1, 2, 4, 5, 6, 7, 8, 9, 20], false), // 3 + (m.raw.to_vec(), true), // 5 + ( + vec![ + 0, 0, 0, 0, 33, 18, 164, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + true, + ), // 6 + ]; + + for (input, output) in tests { + let got = is_message(&input); + assert_eq!(got, output, "IsMessage({input:?}) {got} != {output}"); + } + + Ok(()) +} + +#[test] +fn test_message_contains() -> Result<()> { + let mut m = Message::new(); + m.add(ATTR_SOFTWARE, "value".as_bytes()); + + assert!(m.contains(ATTR_SOFTWARE), "message should contain software"); + assert!(!m.contains(ATTR_NONCE), "message should not contain nonce"); + + Ok(()) +} + +#[test] +fn test_message_full_size() -> Result<()> { + let mut m = Message::new(); + m.build(&[ + Box::new(BINDING_REQUEST), + Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 0])), + Box::new(TextAttribute::new(ATTR_SOFTWARE, "pion/stun".to_owned())), + Box::new(MessageIntegrity::new_long_term_integrity( + "username".to_owned(), + "realm".to_owned(), + "password".to_owned(), + )), + Box::new(FINGERPRINT), + ])?; + let l = m.raw.len(); + m.raw = m.raw[..l - 10].to_vec(); + + let mut decoder = Message::new(); + let l = m.raw.len(); + decoder.raw = m.raw[..l - 10].to_vec(); + let result = decoder.decode(); + assert!(result.is_err(), "decode on truncated buffer should error"); + + Ok(()) +} + +#[test] +fn test_message_clone_to() -> Result<()> { + let mut m = Message::new(); + m.build(&[ + Box::new(BINDING_REQUEST), + Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 0])), + Box::new(TextAttribute::new(ATTR_SOFTWARE, "pion/stun".to_owned())), + Box::new(MessageIntegrity::new_long_term_integrity( + "username".to_owned(), + "realm".to_owned(), + "password".to_owned(), + )), + Box::new(FINGERPRINT), + ])?; + m.encode(); + + let mut b = Message::new(); + m.clone_to(&mut b)?; + assert_eq!(b, m, "not equal"); + + //TODO: Corrupting m and checking that b is not corrupted. + /*let (mut s, ok) = b.attributes.get(ATTR_SOFTWARE); + assert!(ok, "no software attribute"); + s.value[0] = b'k'; + s.add_to(&mut b)?; + assert_ne!(b, m, "should not be equal");*/ + + Ok(()) +} + +#[test] +fn test_message_add_to() -> Result<()> { + let mut m = Message::new(); + m.build(&[ + Box::new(BINDING_REQUEST), + Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 0])), + Box::new(FINGERPRINT), + ])?; + m.encode(); + + let mut b = Message::new(); + m.clone_to(&mut b)?; + + m.transaction_id = TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 2, 0]); + assert_ne!(b, m, "should not be equal"); + + m.add_to(&mut b)?; + assert_eq!(b, m, "should be equal"); + + Ok(()) +} + +#[test] +fn test_decode() -> Result<()> { + let mut m = Message::new(); + m.typ = MessageType { + method: METHOD_BINDING, + class: CLASS_REQUEST, + }; + m.transaction_id = TransactionId::new(); + m.add(ATTR_ERROR_CODE, &[0xff, 0xfe, 0xfa]); + m.write_header(); + + let mut m_decoded = Message::new(); + m_decoded.raw.clear(); + m_decoded.raw.extend_from_slice(&m.raw); + m_decoded.decode()?; + assert_eq!( + m_decoded, m, + "decoded result is not equal to encoded message" + ); + + Ok(()) +} + +#[test] +fn test_message_marshal_binary() -> Result<()> { + let mut m = Message::new(); + m.build(&[ + Box::new(TextAttribute::new(ATTR_SOFTWARE, "software".to_owned())), + Box::new(XorMappedAddress { + ip: "213.1.223.5".parse().unwrap(), + port: 0, + }), + ])?; + + let mut data = m.marshal_binary()?; + // Reset m.Raw to check retention. + for i in 0..m.raw.len() { + m.raw[i] = 0; + } + m.unmarshal_binary(&data)?; + + // Reset data to check retention. + #[allow(clippy::needless_range_loop)] + for i in 0..data.len() { + data[i] = 0; + } + + m.decode()?; + + Ok(()) +} diff --git a/stun-patch/src/textattrs.rs b/stun-patch/src/textattrs.rs new file mode 100644 index 0000000..54c5e77 --- /dev/null +++ b/stun-patch/src/textattrs.rs @@ -0,0 +1,95 @@ +#[cfg(test)] +mod textattrs_test; + +use std::fmt; + +use crate::attributes::*; +use crate::checks::*; +use crate::error::*; +use crate::message::*; + +const MAX_USERNAME_B: usize = 513; +const MAX_REALM_B: usize = 763; +const MAX_SOFTWARE_B: usize = 763; +const MAX_NONCE_B: usize = 763; + +// Username represents USERNAME attribute. +// +// RFC 5389 Section 15.3 +pub type Username = TextAttribute; + +// Realm represents REALM attribute. +// +// RFC 5389 Section 15.7 +pub type Realm = TextAttribute; + +// Nonce represents NONCE attribute. +// +// RFC 5389 Section 15.8 +pub type Nonce = TextAttribute; + +// Software is SOFTWARE attribute. +// +// RFC 5389 Section 15.10 +pub type Software = TextAttribute; + +// TextAttribute is helper for adding and getting text attributes. +#[derive(Clone, Default)] +pub struct TextAttribute { + pub attr: AttrType, + pub text: String, +} + +impl fmt::Display for TextAttribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.text) + } +} + +impl Setter for TextAttribute { + // add_to_as adds attribute with type t to m, checking maximum length. If max_len + // is less than 0, no check is performed. + fn add_to(&self, m: &mut Message) -> Result<()> { + let text = self.text.as_bytes(); + let max_len = match self.attr { + ATTR_USERNAME => MAX_USERNAME_B, + ATTR_REALM => MAX_REALM_B, + ATTR_SOFTWARE => MAX_SOFTWARE_B, + ATTR_NONCE => MAX_NONCE_B, + _ => return Err(Error::Other(format!("Unsupported AttrType {}", self.attr))), + }; + + check_overflow(self.attr, text.len(), max_len)?; + m.add(self.attr, text); + Ok(()) + } +} + +impl Getter for TextAttribute { + fn get_from(&mut self, m: &Message) -> Result<()> { + let attr = self.attr; + *self = TextAttribute::get_from_as(m, attr)?; + Ok(()) + } +} + +impl TextAttribute { + pub fn new(attr: AttrType, text: String) -> Self { + TextAttribute { attr, text } + } + + // get_from_as gets t attribute from m and appends its value to reset v. + pub fn get_from_as(m: &Message, attr: AttrType) -> Result { + match attr { + ATTR_USERNAME => {} + ATTR_REALM => {} + ATTR_SOFTWARE => {} + ATTR_NONCE => {} + _ => return Err(Error::Other(format!("Unsupported AttrType {attr}"))), + }; + + let a = m.get(attr)?; + let text = String::from_utf8(a)?; + Ok(TextAttribute { attr, text }) + } +} diff --git a/stun-patch/src/textattrs/textattrs_test.rs b/stun-patch/src/textattrs/textattrs_test.rs new file mode 100644 index 0000000..e0a01ab --- /dev/null +++ b/stun-patch/src/textattrs/textattrs_test.rs @@ -0,0 +1,307 @@ +use std::io::BufReader; + +use super::*; +use crate::checks::*; +use crate::error::*; + +#[test] +fn test_software_get_from() -> Result<()> { + let mut m = Message::new(); + let v = "Client v0.0.1".to_owned(); + m.add(ATTR_SOFTWARE, v.as_bytes()); + m.write_header(); + + let mut m2 = Message { + raw: Vec::with_capacity(256), + ..Default::default() + }; + + let mut reader = BufReader::new(m.raw.as_slice()); + m2.read_from(&mut reader)?; + let software = TextAttribute::get_from_as(&m, ATTR_SOFTWARE)?; + assert_eq!(software.to_string(), v, "Expected {v}, got {software}."); + + let (s_attr, ok) = m.attributes.get(ATTR_SOFTWARE); + assert!(ok, "sowfware attribute should be found"); + + let s = s_attr.to_string(); + assert!(s.starts_with("SOFTWARE:"), "bad string representation {s}"); + + Ok(()) +} + +#[test] +fn test_software_add_to_invalid() -> Result<()> { + let mut m = Message::new(); + let s = TextAttribute { + attr: ATTR_SOFTWARE, + text: String::from_utf8(vec![0; 1024]).unwrap(), + }; + let result = s.add_to(&mut m); + if let Err(err) = result { + assert!( + is_attr_size_overflow(&err), + "add_to should return AttrOverflowErr, got: {err}" + ); + } else { + panic!("expected error, but got ok"); + } + + let result = TextAttribute::get_from_as(&m, ATTR_SOFTWARE); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "GetFrom should return {}, got: {}", + Error::ErrAttributeNotFound, + err + ); + } else { + panic!("expected error, but got ok"); + } + + Ok(()) +} + +#[test] +fn test_software_add_to_regression() -> Result<()> { + // s.add_to checked len(m.Raw) instead of len(s.Raw). + let mut m = Message { + raw: vec![0u8; 2048], + ..Default::default() + }; + let s = TextAttribute { + attr: ATTR_SOFTWARE, + text: String::from_utf8(vec![0; 100]).unwrap(), + }; + s.add_to(&mut m)?; + + Ok(()) +} + +#[test] +fn test_username() -> Result<()> { + let username = "username".to_owned(); + let u = TextAttribute { + attr: ATTR_USERNAME, + text: username.clone(), + }; + let mut m = Message::new(); + m.write_header(); + //"Bad length" + { + let bad_u = TextAttribute { + attr: ATTR_USERNAME, + text: String::from_utf8(vec![0; 600]).unwrap(), + }; + let result = bad_u.add_to(&mut m); + if let Err(err) = result { + assert!( + is_attr_size_overflow(&err), + "add_to should return *AttrOverflowErr, got: {err}" + ); + } else { + panic!("expected error, but got ok"); + } + } + //"add_to" + { + u.add_to(&mut m)?; + + //"GetFrom" + { + let got = TextAttribute::get_from_as(&m, ATTR_USERNAME)?; + assert_eq!( + got.to_string(), + username, + "expedted: {username}, got: {got}" + ); + //"Not found" + { + let m = Message::new(); + let result = TextAttribute::get_from_as(&m, ATTR_USERNAME); + if let Err(err) = result { + assert_eq!(Error::ErrAttributeNotFound, err, "Should error"); + } else { + panic!("expected error, but got ok"); + } + } + } + } + + //"No allocations" + { + let mut m = Message::new(); + m.write_header(); + let u = TextAttribute { + attr: ATTR_USERNAME, + text: "username".to_owned(), + }; + + u.add_to(&mut m)?; + m.reset(); + } + + Ok(()) +} + +#[test] +fn test_realm_get_from() -> Result<()> { + let mut m = Message::new(); + let v = "realm".to_owned(); + m.add(ATTR_REALM, v.as_bytes()); + m.write_header(); + + let mut m2 = Message { + raw: Vec::with_capacity(256), + ..Default::default() + }; + + let result = TextAttribute::get_from_as(&m2, ATTR_REALM); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "GetFrom should return {}, got: {}", + Error::ErrAttributeNotFound, + err + ); + } else { + panic!("Expected error, but got ok"); + } + + let mut reader = BufReader::new(m.raw.as_slice()); + m2.read_from(&mut reader)?; + + let r = TextAttribute::get_from_as(&m, ATTR_REALM)?; + assert_eq!(r.to_string(), v, "Expected {v}, got {r}."); + + let (r_attr, ok) = m.attributes.get(ATTR_REALM); + assert!(ok, "realm attribute should be found"); + + let s = r_attr.to_string(); + assert!(s.starts_with("REALM:"), "bad string representation {s}"); + + Ok(()) +} + +#[test] +fn test_realm_add_to_invalid() -> Result<()> { + let mut m = Message::new(); + let s = TextAttribute { + attr: ATTR_REALM, + text: String::from_utf8(vec![0; 1024]).unwrap(), + }; + let result = s.add_to(&mut m); + if let Err(err) = result { + assert!( + is_attr_size_overflow(&err), + "add_to should return AttrOverflowErr, got: {err}" + ); + } else { + panic!("expected error, but got ok"); + } + + let result = TextAttribute::get_from_as(&m, ATTR_REALM); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "GetFrom should return {}, got: {}", + Error::ErrAttributeNotFound, + err + ); + } else { + panic!("expected error, but got ok"); + } + + Ok(()) +} + +#[test] +fn test_nonce_get_from() -> Result<()> { + let mut m = Message::new(); + let v = "example.org".to_owned(); + m.add(ATTR_NONCE, v.as_bytes()); + m.write_header(); + + let mut m2 = Message { + raw: Vec::with_capacity(256), + ..Default::default() + }; + + let result = TextAttribute::get_from_as(&m2, ATTR_NONCE); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "GetFrom should return {}, got: {}", + Error::ErrAttributeNotFound, + err + ); + } else { + panic!("Expected error, but got ok"); + } + + let mut reader = BufReader::new(m.raw.as_slice()); + m2.read_from(&mut reader)?; + + let r = TextAttribute::get_from_as(&m, ATTR_NONCE)?; + assert_eq!(r.to_string(), v, "Expected {v}, got {r}."); + + let (r_attr, ok) = m.attributes.get(ATTR_NONCE); + assert!(ok, "realm attribute should be found"); + + let s = r_attr.to_string(); + assert!(s.starts_with("NONCE:"), "bad string representation {s}"); + + Ok(()) +} + +#[test] +fn test_nonce_add_to_invalid() -> Result<()> { + let mut m = Message::new(); + let s = TextAttribute { + attr: ATTR_NONCE, + text: String::from_utf8(vec![0; 1024]).unwrap(), + }; + let result = s.add_to(&mut m); + if let Err(err) = result { + assert!( + is_attr_size_overflow(&err), + "add_to should return AttrOverflowErr, got: {err}" + ); + } else { + panic!("expected error, but got ok"); + } + + let result = TextAttribute::get_from_as(&m, ATTR_NONCE); + if let Err(err) = result { + assert_eq!( + Error::ErrAttributeNotFound, + err, + "GetFrom should return {}, got: {}", + Error::ErrAttributeNotFound, + err + ); + } else { + panic!("expected error, but got ok"); + } + + Ok(()) +} + +#[test] +fn test_nonce_add_to() -> Result<()> { + let mut m = Message::new(); + let n = TextAttribute { + attr: ATTR_NONCE, + text: "example.org".to_owned(), + }; + n.add_to(&mut m)?; + + let v = m.get(ATTR_NONCE)?; + assert_eq!(v.as_slice(), b"example.org", "bad nonce {v:?}"); + + Ok(()) +} diff --git a/stun-patch/src/uattrs.rs b/stun-patch/src/uattrs.rs new file mode 100644 index 0000000..087d809 --- /dev/null +++ b/stun-patch/src/uattrs.rs @@ -0,0 +1,62 @@ +#[cfg(test)] +mod uattrs_test; + +use std::fmt; + +use crate::attributes::*; +use crate::error::*; +use crate::message::*; + +// UnknownAttributes represents UNKNOWN-ATTRIBUTES attribute. +// +// RFC 5389 Section 15.9 +pub struct UnknownAttributes(pub Vec); + +impl fmt::Display for UnknownAttributes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + write!(f, "") + } else { + let mut s = vec![]; + for t in &self.0 { + s.push(t.to_string()); + } + write!(f, "{}", s.join(", ")) + } + } +} + +// type size is 16 bit. +const ATTR_TYPE_SIZE: usize = 2; + +impl Setter for UnknownAttributes { + // add_to adds UNKNOWN-ATTRIBUTES attribute to message. + fn add_to(&self, m: &mut Message) -> Result<()> { + let mut v = Vec::with_capacity(ATTR_TYPE_SIZE * 20); // 20 should be enough + // If len(a.Types) > 20, there will be allocations. + for t in &self.0 { + v.extend_from_slice(&t.value().to_be_bytes()); + } + m.add(ATTR_UNKNOWN_ATTRIBUTES, &v); + Ok(()) + } +} + +impl Getter for UnknownAttributes { + // GetFrom parses UNKNOWN-ATTRIBUTES from message. + fn get_from(&mut self, m: &Message) -> Result<()> { + let v = m.get(ATTR_UNKNOWN_ATTRIBUTES)?; + if v.len() % ATTR_TYPE_SIZE != 0 { + return Err(Error::ErrBadUnknownAttrsSize); + } + self.0.clear(); + let mut first = 0usize; + while first < v.len() { + let last = first + ATTR_TYPE_SIZE; + self.0 + .push(AttrType(u16::from_be_bytes([v[first], v[first + 1]]))); + first = last; + } + Ok(()) + } +} diff --git a/stun-patch/src/uattrs/uattrs_test.rs b/stun-patch/src/uattrs/uattrs_test.rs new file mode 100644 index 0000000..2351d55 --- /dev/null +++ b/stun-patch/src/uattrs/uattrs_test.rs @@ -0,0 +1,37 @@ +use super::*; + +#[test] +fn test_unknown_attributes() -> Result<()> { + let mut m = Message::new(); + let a = UnknownAttributes(vec![ATTR_DONT_FRAGMENT, ATTR_CHANNEL_NUMBER]); + assert_eq!( + a.to_string(), + "DONT-FRAGMENT, CHANNEL-NUMBER", + "bad String:{a}" + ); + assert_eq!( + UnknownAttributes(vec![]).to_string(), + "", + "bad blank string" + ); + + a.add_to(&mut m)?; + + //"GetFrom" + { + let mut attrs = UnknownAttributes(Vec::with_capacity(10)); + attrs.get_from(&m)?; + for i in 0..a.0.len() { + assert_eq!(a.0[i], attrs.0[i], "expected {} != {}", a.0[i], attrs.0[i]); + } + let mut m_blank = Message::new(); + let result = attrs.get_from(&m_blank); + assert!(result.is_err(), "should error"); + + m_blank.add(ATTR_UNKNOWN_ATTRIBUTES, &[1, 2, 3]); + let result = attrs.get_from(&m_blank); + assert!(result.is_err(), "should error"); + } + + Ok(()) +} diff --git a/stun-patch/src/uri.rs b/stun-patch/src/uri.rs new file mode 100644 index 0000000..5dce476 --- /dev/null +++ b/stun-patch/src/uri.rs @@ -0,0 +1,73 @@ +#[cfg(test)] +mod uri_test; + +use std::fmt; + +use crate::error::*; + +// SCHEME definitions from RFC 7064 Section 3.2. + +pub const SCHEME: &str = "stun"; +pub const SCHEME_SECURE: &str = "stuns"; + +// URI as defined in RFC 7064. +#[derive(PartialEq, Eq, Debug)] +pub struct Uri { + pub scheme: String, + pub host: String, + pub port: Option, +} + +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let host = if self.host.contains("::") { + "[".to_owned() + self.host.as_str() + "]" + } else { + self.host.clone() + }; + + if let Some(port) = self.port { + write!(f, "{}:{}:{}", self.scheme, host, port) + } else { + write!(f, "{}:{}", self.scheme, host) + } + } +} + +impl Uri { + // parse_uri parses URI from string. + pub fn parse_uri(raw: &str) -> Result { + // work around for url crate + if raw.contains("//") { + return Err(Error::ErrInvalidUrl); + } + + let mut s = raw.to_string(); + let pos = raw.find(':'); + if let Some(p) = pos { + s.replace_range(p..p + 1, "://"); + } else { + return Err(Error::ErrSchemeType); + } + + let raw_parts = url::Url::parse(&s)?; + + let scheme = raw_parts.scheme().into(); + if scheme != SCHEME && scheme != SCHEME_SECURE { + return Err(Error::ErrSchemeType); + } + + let host = if let Some(host) = raw_parts.host_str() { + host.trim() + .trim_start_matches('[') + .trim_end_matches(']') + .to_owned() + } else { + return Err(Error::ErrHost); + }; + + let port = raw_parts.port(); + + Ok(Uri { scheme, host, port }) + } +} diff --git a/stun-patch/src/uri/uri_test.rs b/stun-patch/src/uri/uri_test.rs new file mode 100644 index 0000000..20f13d1 --- /dev/null +++ b/stun-patch/src/uri/uri_test.rs @@ -0,0 +1,68 @@ +use super::*; + +#[test] +fn test_parse_uri() -> Result<()> { + let tests = vec![ + ( + "default", + "stun:example.org", + Uri { + host: "example.org".to_owned(), + scheme: SCHEME.to_owned(), + port: None, + }, + "stun:example.org", + ), + ( + "secure", + "stuns:example.org", + Uri { + host: "example.org".to_owned(), + scheme: SCHEME_SECURE.to_owned(), + port: None, + }, + "stuns:example.org", + ), + ( + "with port", + "stun:example.org:8000", + Uri { + host: "example.org".to_owned(), + scheme: SCHEME.to_owned(), + port: Some(8000), + }, + "stun:example.org:8000", + ), + ( + "ipv6 address", + "stun:[::1]:123", + Uri { + host: "::1".to_owned(), + scheme: SCHEME.to_owned(), + port: Some(123), + }, + "stun:[::1]:123", + ), + ]; + + for (name, input, output, expected_str) in tests { + let out = Uri::parse_uri(input)?; + assert_eq!(out, output, "{name}: {out} != {output}"); + assert_eq!(out.to_string(), expected_str, "{name}"); + } + + //"MustFail" + { + let tests = vec![ + ("hierarchical", "stun://example.org"), + ("bad scheme", "tcp:example.org"), + ("invalid uri scheme", "stun_s:test"), + ]; + for (name, input) in tests { + let result = Uri::parse_uri(input); + assert!(result.is_err(), "{name} should fail, but did not"); + } + } + + Ok(()) +} diff --git a/stun-patch/src/xoraddr.rs b/stun-patch/src/xoraddr.rs new file mode 100644 index 0000000..0a86bb3 --- /dev/null +++ b/stun-patch/src/xoraddr.rs @@ -0,0 +1,173 @@ +#[cfg(test)] +mod xoraddr_test; + +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::{fmt, mem}; + +use crate::addr::*; +use crate::attributes::*; +use crate::checks::*; +use crate::error::*; +use crate::message::*; + +const WORD_SIZE: usize = mem::size_of::(); + +//var supportsUnaligned = runtime.GOARCH == "386" || runtime.GOARCH == "amd64" // nolint:gochecknoglobals + +// fast_xor_bytes xors in bulk. It only works on architectures that +// support unaligned read/writes. +/*TODO: fn fast_xor_bytes(dst:&[u8], a:&[u8], b:&[u8]) ->usize { + let mut n = a.len(); + if b.len() < n { + n = b.len(); + } + + let w = n / WORD_SIZE; + if w > 0 { + let dw = *(*[]uintptr)(unsafe.Pointer(&dst)) + let aw = *(*[]uintptr)(unsafe.Pointer(&a)) + let bw = *(*[]uintptr)(unsafe.Pointer(&b)) + for i := 0; i < w; i++ { + dw[i] = aw[i] ^ bw[i] + } + } + + for i := n - n%WORD_SIZE; i < n; i++ { + dst[i] = a[i] ^ b[i] + } + + return n +}*/ + +fn safe_xor_bytes(dst: &mut [u8], a: &[u8], b: &[u8]) -> usize { + let mut n = a.len(); + if b.len() < n { + n = b.len(); + } + if dst.len() < n { + n = dst.len(); + } + for i in 0..n { + dst[i] = a[i] ^ b[i]; + } + n +} + +/// xor_bytes xors the bytes in a and b. The destination is assumed to have enough +/// space. Returns the number of bytes xor'd. +pub fn xor_bytes(dst: &mut [u8], a: &[u8], b: &[u8]) -> usize { + //TODO: if supportsUnaligned { + // return fastXORBytes(dst, a, b) + //} + safe_xor_bytes(dst, a, b) +} + +/// XORMappedAddress implements XOR-MAPPED-ADDRESS attribute. +/// +/// RFC 5389 Section 15.2 +pub struct XorMappedAddress { + pub ip: IpAddr, + pub port: u16, +} + +impl Default for XorMappedAddress { + fn default() -> Self { + XorMappedAddress { + ip: IpAddr::V4(Ipv4Addr::from(0)), + port: 0, + } + } +} + +impl fmt::Display for XorMappedAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let family = match self.ip { + IpAddr::V4(_) => FAMILY_IPV4, + IpAddr::V6(_) => FAMILY_IPV6, + }; + if family == FAMILY_IPV4 { + write!(f, "{}:{}", self.ip, self.port) + } else { + write!(f, "[{}]:{}", self.ip, self.port) + } + } +} + +impl Setter for XorMappedAddress { + /// add_to adds XOR-MAPPED-ADDRESS to m. Can return ErrBadIPLength + /// if len(a.IP) is invalid. + fn add_to(&self, m: &mut Message) -> Result<()> { + self.add_to_as(m, ATTR_XORMAPPED_ADDRESS) + } +} + +impl Getter for XorMappedAddress { + /// get_from decodes XOR-MAPPED-ADDRESS attribute in message and returns + /// error if any. While decoding, a.IP is reused if possible and can be + /// rendered to invalid state (e.g. if a.IP was set to IPv6 and then + /// IPv4 value were decoded into it), be careful. + fn get_from(&mut self, m: &Message) -> Result<()> { + self.get_from_as(m, ATTR_XORMAPPED_ADDRESS) + } +} + +impl XorMappedAddress { + /// add_to_as adds XOR-MAPPED-ADDRESS value to m as t attribute. + pub fn add_to_as(&self, m: &mut Message, t: AttrType) -> Result<()> { + let (family, ip_len, ip) = match self.ip { + IpAddr::V4(ipv4) => (FAMILY_IPV4, IPV4LEN, ipv4.octets().to_vec()), + IpAddr::V6(ipv6) => (FAMILY_IPV6, IPV6LEN, ipv6.octets().to_vec()), + }; + + let mut value = [0; 32 + 128]; + //value[0] = 0 // first 8 bits are zeroes + let mut xor_value = vec![0; IPV6LEN]; + xor_value[4..].copy_from_slice(&m.transaction_id.0); + xor_value[0..4].copy_from_slice(&MAGIC_COOKIE.to_be_bytes()); + value[0..2].copy_from_slice(&family.to_be_bytes()); + value[2..4].copy_from_slice(&(self.port ^ (MAGIC_COOKIE >> 16) as u16).to_be_bytes()); + xor_bytes(&mut value[4..4 + ip_len], &ip, &xor_value); + m.add(t, &value[..4 + ip_len]); + Ok(()) + } + + /// get_from_as decodes XOR-MAPPED-ADDRESS attribute value in message + /// getting it as for t type. + pub fn get_from_as(&mut self, m: &Message, t: AttrType) -> Result<()> { + let v = m.get(t)?; + if v.len() <= 4 { + return Err(Error::ErrUnexpectedEof); + } + + let family = u16::from_be_bytes([v[0], v[1]]); + if family != FAMILY_IPV6 && family != FAMILY_IPV4 { + return Err(Error::Other(format!("bad value {family}"))); + } + + check_overflow( + t, + v[4..].len(), + if family == FAMILY_IPV4 { + IPV4LEN + } else { + IPV6LEN + }, + )?; + self.port = u16::from_be_bytes([v[2], v[3]]) ^ (MAGIC_COOKIE >> 16) as u16; + let mut xor_value = vec![0; 4 + TRANSACTION_ID_SIZE]; + xor_value[0..4].copy_from_slice(&MAGIC_COOKIE.to_be_bytes()); + xor_value[4..].copy_from_slice(&m.transaction_id.0); + + if family == FAMILY_IPV6 { + let mut ip = [0; IPV6LEN]; + xor_bytes(&mut ip, &v[4..], &xor_value); + self.ip = IpAddr::V6(Ipv6Addr::from(ip)); + } else { + let mut ip = [0; IPV4LEN]; + xor_bytes(&mut ip, &v[4..], &xor_value); + self.ip = IpAddr::V4(Ipv4Addr::from(ip)); + }; + + Ok(()) + } +} diff --git a/stun-patch/src/xoraddr/xoraddr_test.rs b/stun-patch/src/xoraddr/xoraddr_test.rs new file mode 100644 index 0000000..2d5544a --- /dev/null +++ b/stun-patch/src/xoraddr/xoraddr_test.rs @@ -0,0 +1,250 @@ +use std::io::BufReader; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; + +use super::*; +use crate::checks::*; + +#[test] +fn test_xor_safe() -> Result<()> { + let mut dst = vec![0; 8]; + let a = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let b = vec![8, 7, 7, 6, 6, 3, 4, 1]; + safe_xor_bytes(&mut dst, &a, &b); + let c = dst.clone(); + safe_xor_bytes(&mut dst, &c, &a); + for i in 0..dst.len() { + assert_eq!(b[i], dst[i], "{} != {}", b[i], dst[i]); + } + + Ok(()) +} + +#[test] +fn test_xor_safe_bsmaller() -> Result<()> { + let mut dst = vec![0; 5]; + let a = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let b = vec![8, 7, 7, 6, 6]; + safe_xor_bytes(&mut dst, &a, &b); + let c = dst.clone(); + safe_xor_bytes(&mut dst, &c, &a); + for i in 0..dst.len() { + assert_eq!(b[i], dst[i], "{} != {}", b[i], dst[i]); + } + + Ok(()) +} + +#[test] +fn test_xormapped_address_get_from() -> Result<()> { + let mut m = Message::new(); + let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); + m.transaction_id.0.copy_from_slice(&transaction_id); + let addr_value = vec![0x00, 0x01, 0x9c, 0xd5, 0xf4, 0x9f, 0x38, 0xae]; + m.add(ATTR_XORMAPPED_ADDRESS, &addr_value); + let mut addr = XorMappedAddress { + ip: "0.0.0.0".parse().unwrap(), + port: 0, + }; + addr.get_from(&m)?; + assert_eq!( + addr.ip.to_string(), + "213.141.156.236", + "bad IP {} != 213.141.156.236", + addr.ip + ); + assert_eq!(addr.port, 48583, "bad Port {} != 48583", addr.port); + + //"UnexpectedEOF" + { + let mut m = Message::new(); + // {0, 1} is correct addr family. + m.add(ATTR_XORMAPPED_ADDRESS, &[0, 1, 3, 4]); + let mut addr = XorMappedAddress { + ip: "0.0.0.0".parse().unwrap(), + port: 0, + }; + let result = addr.get_from(&m); + if let Err(err) = result { + assert_eq!( + Error::ErrUnexpectedEof, + err, + "len(v) = 4 should render <{}> error, got <{}>", + Error::ErrUnexpectedEof, + err + ); + } else { + panic!("expected error, got ok"); + } + } + //"AttrOverflowErr" + { + let mut m = Message::new(); + // {0, 1} is correct addr family. + m.add( + ATTR_XORMAPPED_ADDRESS, + &[0, 1, 3, 4, 5, 6, 7, 8, 9, 1, 1, 1, 1, 1, 2, 3, 4], + ); + let mut addr = XorMappedAddress { + ip: "0.0.0.0".parse().unwrap(), + port: 0, + }; + let result = addr.get_from(&m); + if let Err(err) = result { + assert!( + is_attr_size_overflow(&err), + "AddTo should return AttrOverflowErr, got: {err}" + ); + } else { + panic!("expected error, got ok"); + } + } + + Ok(()) +} + +#[test] +fn test_xormapped_address_get_from_invalid() -> Result<()> { + let mut m = Message::new(); + let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); + m.transaction_id.0.copy_from_slice(&transaction_id); + let expected_ip: IpAddr = "213.141.156.236".parse().unwrap(); + let expected_port = 21254u16; + let mut addr = XorMappedAddress { + ip: "0.0.0.0".parse().unwrap(), + port: 0, + }; + let result = addr.get_from(&m); + assert!(result.is_err(), "should be error"); + + addr.ip = expected_ip; + addr.port = expected_port; + addr.add_to(&mut m)?; + m.write_header(); + + let mut m_res = Message::new(); + m.raw[20 + 4 + 1] = 0x21; + m.decode()?; + let mut reader = BufReader::new(m.raw.as_slice()); + m_res.read_from(&mut reader)?; + let result = addr.get_from(&m); + assert!(result.is_err(), "should be error"); + + Ok(()) +} + +#[test] +fn test_xormapped_address_add_to() -> Result<()> { + let mut m = Message::new(); + let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); + m.transaction_id.0.copy_from_slice(&transaction_id); + let expected_ip: IpAddr = "213.141.156.236".parse().unwrap(); + let expected_port = 21254u16; + let mut addr = XorMappedAddress { + ip: "213.141.156.236".parse().unwrap(), + port: expected_port, + }; + addr.add_to(&mut m)?; + m.write_header(); + + let mut m_res = Message::new(); + m_res.write(&m.raw)?; + addr.get_from(&m_res)?; + assert_eq!( + addr.ip, expected_ip, + "{} (got) != {} (expected)", + addr.ip, expected_ip + ); + + assert_eq!( + addr.port, expected_port, + "bad Port {} != {}", + addr.port, expected_port + ); + + Ok(()) +} + +#[test] +fn test_xormapped_address_add_to_ipv6() -> Result<()> { + let mut m = Message::new(); + let transaction_id = BASE64_STANDARD.decode("jxhBARZwX+rsC6er").unwrap(); + m.transaction_id.0.copy_from_slice(&transaction_id); + let expected_ip: IpAddr = "fe80::dc2b:44ff:fe20:6009".parse().unwrap(); + let expected_port = 21254u16; + let addr = XorMappedAddress { + ip: "fe80::dc2b:44ff:fe20:6009".parse().unwrap(), + port: 21254, + }; + addr.add_to(&mut m)?; + m.write_header(); + + let mut m_res = Message::new(); + let mut reader = BufReader::new(m.raw.as_slice()); + m_res.read_from(&mut reader)?; + + let mut got_addr = XorMappedAddress { + ip: "0.0.0.0".parse().unwrap(), + port: 0, + }; + got_addr.get_from(&m)?; + + assert_eq!( + got_addr.ip, expected_ip, + "bad IP {} != {}", + got_addr.ip, expected_ip + ); + assert_eq!( + got_addr.port, expected_port, + "bad Port {} != {}", + got_addr.port, expected_port + ); + + Ok(()) +} + +/* +#[test] +fn TestXORMappedAddress_AddTo_Invalid() -> Result<()> { + let mut m = Message::new(); + let mut addr = XORMappedAddress{ + ip: 1, 2, 3, 4, 5, 6, 7, 8}, + port: 21254, + } + if err := addr.AddTo(m); !errors.Is(err, ErrBadIPLength) { + t.Errorf("AddTo should return %q, got: %v", ErrBadIPLength, err) + } +}*/ + +#[test] +fn test_xormapped_address_string() -> Result<()> { + let tests = vec![ + ( + // 0 + XorMappedAddress { + ip: "fe80::dc2b:44ff:fe20:6009".parse().unwrap(), + port: 124, + }, + "[fe80::dc2b:44ff:fe20:6009]:124", + ), + ( + // 1 + XorMappedAddress { + ip: "213.141.156.236".parse().unwrap(), + port: 8147, + }, + "213.141.156.236:8147", + ), + ]; + + for (addr, ip) in tests { + assert_eq!( + addr.to_string(), + ip, + " XORMappesAddress.String() {addr} (got) != {ip} (expected)", + ); + } + + Ok(()) +}