diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 56acbd02..a0574b16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,17 +77,17 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ - key: "${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}" + key: "${{ matrix.os }}-${{ matrix.target }}" # Install dependencies - name: Install dependencies (Linux) if: runner.os == 'Linux' run: | sudo apt update - sudo apt install -y gcc-aarch64-linux-gnu + sudo apt install -y gcc-aarch64-linux-gnu protobuf-compiler rustup update rustup target add ${{ matrix.target }} - cargo install cargo-zigbuild + cargo install cargo-zigbuild --force sudo snap install zig --classic --beta - name: Install dependencies (Macos) @@ -95,7 +95,7 @@ jobs: run: | rustup update rustup target add ${{ matrix.target }} - cargo install cargo-zigbuild + cargo install cargo-zigbuild --force brew install zig # Build release diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4dba6bf4..83230b52 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,14 +3,6 @@ on: push: branches: - main - paths: - - drivers/** - - tests/** - - stun/** - - turn/** - - turn-server/** - - Cargo.toml - - Cargo.lock pull_request: branches: - main @@ -27,6 +19,13 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ - key: "${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}" - - name: Run tests - run: cargo test + key: "test-ubuntu-latest" + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y protobuf-compiler + cargo install cargo-hack --force + - name: Run tests for all features + run: cargo test --workspace + - name: Run check all possible feature combinations + run: cargo hack check --feature-powerset diff --git a/.gitignore b/.gitignore index 8ed024bc..9649ec26 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ /workflow build/ .DS_Store -turn_server.dev.toml +*.dev.toml +/proptest-regressions diff --git a/Cargo.lock b/Cargo.lock index 8791c702..2ea7ddff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,18 +13,18 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "ahash" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", - "getrandom 0.2.15", + "getrandom 0.3.3", "once_cell", "version_check", "zerocopy", @@ -47,9 +47,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -62,49 +62,91 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-rs" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b8ff6c09cd57b16da53641caa860168b88c172a5ee163b0288d3d6eea12786" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "0e44d16778acaf6a9ec9899b92cebd65580b83f685446bf2e1f5d3d732f99dcd" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] name = "axum" @@ -114,13 +156,10 @@ checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", "bytes", - "form_urlencoded", "futures-util", "http", "http-body", "http-body-util", - "hyper", - "hyper-util", "itoa", "matchit", "memchr", @@ -129,15 +168,10 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", "sync_wrapper", - "tokio", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -157,14 +191,13 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -181,11 +214,31 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools 0.10.5", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" [[package]] name = "block-buffer" @@ -198,21 +251,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" - -[[package]] -name = "byteorder" -version = "1.5.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cast" @@ -222,18 +269,29 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.5" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "ciborium" @@ -262,11 +320,22 @@ dependencies = [ "half", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" -version = "4.5.23" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -274,9 +343,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -286,9 +355,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" dependencies = [ "heck", "proc-macro2", @@ -298,15 +367,24 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" @@ -319,29 +397,14 @@ dependencies = [ ] [[package]] -name = "cpufeatures" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.2.1" +name = "crc32fast" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "crc-catalog", + "cfg-if", ] -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "criterion" version = "0.7.0" @@ -402,9 +465,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" @@ -418,9 +481,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -433,20 +496,68 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] [[package]] name = "fnv" @@ -456,13 +567,19 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -514,13 +631,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -532,7 +649,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.4+wasi-0.2.4", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -541,11 +658,36 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -553,9 +695,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -564,19 +706,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "hmac" -version = "0.12.1" +name = "hermit-abi" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "http" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -595,12 +734,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", + "futures-core", "http", "http-body", "pin-project-lite", @@ -608,9 +747,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -620,44 +759,173 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", + "h2", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "bytes", + "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "libc", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] name = "indexmap" -version = "2.7.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", @@ -665,9 +933,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.10" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ "bitflags", "cfg-if", @@ -682,33 +950,43 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.13.0" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itertools" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.3", + "libc", +] [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -722,25 +1000,47 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.175" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] [[package]] name = "libmimalloc-sys" -version = "0.1.39" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23aa6811d3bd4deb8a84dde645f943476d13b248d818edcf8ce0b2f37f036b44" +checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" dependencies = [ "cc", "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -748,9 +1048,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchit" @@ -770,15 +1070,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mimalloc" -version = "0.1.43" +version = "0.1.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68914350ae34959d83f732418d51e2427a794055d0b9529f48259ac07af65633" +checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" dependencies = [ "libmimalloc-sys", ] @@ -789,24 +1089,46 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] @@ -824,20 +1146,31 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -856,30 +1189,36 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "oorandom" -version = "11.1.4" +version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -887,9 +1226,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -900,15 +1239,45 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -944,6 +1313,21 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -952,71 +1336,120 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] -name = "prometheus" -version = "0.14.0" +name = "prost" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "protobuf", - "thiserror 2.0.8", + "bytes", + "prost-derive", ] [[package]] -name = "protobuf" -version = "3.7.2" +name = "prost-build" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" dependencies = [ + "heck", + "itertools 0.10.5", + "log", + "multimap", "once_cell", - "protobuf-support", - "thiserror 1.0.69", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "protobuf-support" -version = "3.7.2" +name = "prost-types" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" dependencies = [ - "thiserror 1.0.69", + "prost", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +dependencies = [ + "pulldown-cmark", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -1058,9 +1491,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1068,9 +1501,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1078,18 +1511,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -1099,9 +1532,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -1110,27 +1543,96 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -1149,18 +1651,28 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "fd6c24dee235d0da097043389623fb913daddf92c76e9f5a1db88607a0bcbd1d" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.225" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659356f9a0cb1e529b24c01e43ad2bdf520ec4ceaf83047b83ddcc2251f96383" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.225" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "0ea936adf78b1f766949a4977b91d2f5595825bd6ec079aa9543ad2685fc4516" dependencies = [ "proc-macro2", "quote", @@ -1169,56 +1681,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", + "serde_core", ] [[package]] name = "serde_spanned" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "sha-1" -version = "0.10.1" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +checksum = "2789234a13a53fc4be1b51ea1bab45a3c338bdb884862a257d10e5a74ae009e6" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "serde_core", ] [[package]] @@ -1229,9 +1709,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -1256,9 +1736,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -1270,6 +1750,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.11.1" @@ -1284,9 +1770,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -1300,28 +1786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] -name = "thiserror" -version = "1.0.69" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f5383f3e0071702bf93ab5ee99b52d26936be9dedd9413067cbdcddcb6141a" -dependencies = [ - "thiserror-impl 2.0.8", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -1329,21 +1797,23 @@ dependencies = [ ] [[package]] -name = "thiserror-impl" -version = "2.0.8" +name = "tempfile" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f357fcec90b3caef6623a099691be676d033b40a058ac95d2a6ade6fa0c943" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ - "proc-macro2", - "quote", - "syn", + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.60.2", ] [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -1358,20 +1828,30 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1413,6 +1893,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -1427,9 +1917,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1440,43 +1930,43 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" dependencies = [ "indexmap", - "serde", + "serde_core", "serde_spanned", - "toml_datetime 0.7.0", + "toml_datetime 0.7.1", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "a197c0ec7d131bfc6f7e82c8442ba1595aeab35da7adbf05b6b73cd06a16b6be" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", - "toml_datetime 0.6.8", - "winnow 0.6.20", + "toml_datetime 0.6.11", + "winnow", ] [[package]] @@ -1485,7 +1975,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ - "winnow 0.7.13", + "winnow", ] [[package]] @@ -1494,6 +1984,76 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "flate2", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", + "tempfile", + "tonic-build", +] + [[package]] name = "tower" version = "0.5.2" @@ -1502,9 +2062,12 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -1528,62 +2091,150 @@ version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ - "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "turn-server" -version = "3.4.0" +version = "4.0.0" dependencies = [ "ahash", "anyhow", - "axum", - "base64", "bytes", "clap", - "crc", - "criterion", - "hmac", - "itertools 0.14.0", "log", - "md-5", "mimalloc", - "num_enum", + "num_cpus", "parking_lot", - "prometheus", - "rand", + "prost", "serde", - "serde_json", - "sha-1", "simple_logger", - "thiserror 2.0.8", "tokio", + "tokio-rustls", "tokio-stream", "toml", + "tonic", + "tonic-prost", + "tonic-prost-build", + "turn-server-codec", + "turn-server-service", + "url", +] + +[[package]] +name = "turn-server-codec" +version = "0.1.0" +dependencies = [ + "anyhow", + "aws-lc-rs", + "base64", + "bytes", + "crc32fast", + "criterion", + "md-5", + "num_enum", +] + +[[package]] +name = "turn-server-sdk" +version = "0.1.0" +dependencies = [ + "prost", + "tonic", + "tonic-prost", + "tonic-prost-build", + "turn-server-codec", +] + +[[package]] +name = "turn-server-service" +version = "0.1.0" +dependencies = [ + "ahash", + "anyhow", + "bytes", + "parking_lot", + "pollster", + "rand", + "serde", + "turn-server-codec", ] [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "utf8parse" @@ -1607,37 +2258,48 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.4+wasi-0.2.4" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "wit-bindgen", + "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -1649,9 +2311,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1659,9 +2321,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -1672,15 +2334,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -1688,13 +2353,19 @@ dependencies = [ [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-sys" version = "0.48.0" @@ -1722,6 +2393,15 @@ 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.3", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1746,13 +2426,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1765,6 +2462,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -1777,6 +2480,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -1789,12 +2498,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -1807,6 +2528,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -1819,6 +2546,12 @@ 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1831,6 +2564,12 @@ 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -1843,42 +2582,134 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] [[package]] -name = "winnow" -version = "0.7.13" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] [[package]] -name = "wit-bindgen" -version = "0.45.1" +name = "writeable" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ac2d2f1d..1c98961c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "turn-server" -version = "3.4.0" +version = "4.0.0" edition = "2024" authors = ["mycrl "] description = "A pure rust-implemented turn server." @@ -14,46 +14,37 @@ categories = ["parsing", "network-programming"] [dependencies] ahash = "0.8" anyhow = "1.0" -axum = "0.8" bytes = "1" -base64 = "0.22" clap = { version = "4", features = ["derive"] } +parking_lot = "0.12" log = "0.4" mimalloc = { version = "0.1", default-features = false } -parking_lot = "0.12" serde = { version = "1", features = ["derive"] } -serde_json = "1.0" +toml = "0.9.6" simple_logger = "5" tokio = { version = "1", features = ["full"] } -toml = "0.9" -rand = "0.9" -itertools = "0.14" -prometheus = "0.14" -num_enum = "0.7" -md-5 = "0.10" -hmac = "0.12" -sha-1 = "0.10" -crc = "3" -thiserror = "2.0.4" -tokio-stream = { version = "0.1", features = ["sync"] } +tokio-rustls = { version = "0.26", features = ["aws-lc-rs"], optional = true } +tokio-stream = { version = "0.1", features = ["sync"], optional = true } +num_cpus = "1.17.0" +tonic = { version = "0.14", features = ["deflate"], optional = true } +prost = { version = "0.14", optional = true } +tonic-prost = { version = "0.14", optional = true } +url = { version = "2.5.7", optional = true } +codec = { workspace = true } +service = { workspace = true, features = ["serde"] } -[dev-dependencies] -criterion = "0.7" +[build-dependencies] anyhow = "1.0" -base64 = "0.22.1" -tokio = { version = "1", features = ["full"] } -bytes = "1.4.0" - -[[bench]] -name = "benchmark" -harness = false +tonic-prost-build = { version = "0.14", optional = true } [features] -default = ["udp"] -udp = [] +default = ["all"] +all = ["udp", "tcp", "ssl", "rpc"] tcp = [] -api = [] -prometheus = ["api"] +udp = [] +rpc-build = ["dep:tonic-prost-build", "dep:tonic-prost", "dep:prost"] +rpc = ["dep:tonic", "rpc-build", "dep:url"] +ssl = ["dep:tokio-rustls", "tonic/tls-aws-lc"] [profile.release] debug-assertions = false @@ -63,3 +54,11 @@ panic = "abort" debug = false lto = true codegen-units = 1 + +[workspace] +resolver = "2" +members = ["./", "crates/codec", "crates/service", "sdk"] + +[workspace.dependencies] +codec = { path = "crates/codec", version = "0.1.0", package = "turn-server-codec" } +service = { path = "crates/service", version = "0.1.0", package = "turn-server-service" } diff --git a/Dockerfile b/Dockerfile index b890f2c8..38ea7787 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,12 @@ -FROM clux/muslrust:stable AS builder +FROM rust:bookworm AS builder WORKDIR /usr/src/ COPY . . -RUN cargo build --release --target x86_64-unknown-linux-musl --all-features +RUN cargo build --release --all-features -FROM debian:buster-slim +FROM debian:bookworm-slim WORKDIR /app RUN apt update && \ apt-get install pkg-config libssl-dev -y -COPY --from=builder /usr/src/target/x86_64-unknown-linux-musl/release/turn-server /usr/local/bin/turn-server -COPY --from=builder /usr/src/turn-server.toml /etc/turn-server/config.toml -CMD turn-server --config=/etc/turn-server/config.toml +COPY --from=builder /usr/src/target/release/turn-server /usr/local/bin/turn-server +COPY --from=builder /usr/src/turn-server.json /etc/turn-server/config.json +CMD turn-server --config=/etc/turn-server/config.json diff --git a/README.md b/README.md index d7ed718e..55b08a0d 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,16 @@
TURN Server implemented by ❤️ Rust
+
+ Deutsch | + Español | + français | + 日本語 | + 한국어 | + Português | + Русский | + 中文 +
--- @@ -29,22 +39,6 @@ First of all, I remain in awe and respect for coturn, which is a much more matur However, turn-rs is not a simple duplicate implementation, and this project is not a blind “RIIR”. Because turn server is currently the largest use of the scene or WebRTC, for WebRTC business, many features are not too much necessary, so keep it simple and fast is the best choice. -##### "Better performance" - -Because turn-rs only focuses on the core business, it removes a lot of features that are almost less commonly used in WebRTC scenarios, resulting in better performance, both in terms of throughput and memory performance. - -##### "Database storage is not supported" - -I don't think turn servers should be concerned about user information, just do their essential work, it's better to leave the hosting and storing of user information to other services, and interacting with databases adds complexity. - -##### "No transport layer encryption" - -Unlike coturn, which provides various transport layer encryption, turn-rs does not provide any transport layer encryption. Currently turn clients that support encryption are relatively rare, and there is minimal benefit to the turn server in providing transport layer encryption, since for WebRTC the transport data is already encrypted. - -##### "Only allow turn-rs as transit address" - -Some clients currently use local addresses for the turn server to create bindings and permissions under certain NAT types, coturn supports this behaviour. However, turn-rs does not allow this behaviour, any client must use the turn server's transit address to communicate, which provides help for clients to hide their IP addresses. - ## Table of contents - [features](#features) diff --git a/tests/integration_test.html b/WEBRTC_DEMO.html similarity index 99% rename from tests/integration_test.html rename to WEBRTC_DEMO.html index 577c1f73..47c753ba 100644 --- a/tests/integration_test.html +++ b/WEBRTC_DEMO.html @@ -2,7 +2,7 @@ - turn server integration test + WEBRTC DEMO diff --git a/benches/benchmark.rs b/benches/benchmark.rs deleted file mode 100644 index 99221519..00000000 --- a/benches/benchmark.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::{net::SocketAddr, sync::LazyLock}; - -use bytes::BytesMut; -use criterion::*; -use rand::seq::SliceRandom; -use turn_server::{ - stun::{ - ChannelData, Decoder, MessageEncoder, Transport, - attribute::{ChannelNumber, Nonce, Realm, ReqeestedTransport, UserName, XorPeerAddress}, - method::{ALLOCATE_REQUEST, BINDING_REQUEST, CHANNEL_BIND_REQUEST, CREATE_PERMISSION_REQUEST}, - }, - turn::{Observer, Service, SessionAddr}, -}; - -#[derive(Clone)] -struct SimpleObserver; - -impl Observer for SimpleObserver { - fn get_password(&self, _: &str) -> Option { - Some("test".to_string()) - } -} - -static TOKEN: LazyLock<[u8; 12]> = LazyLock::new(|| { - let mut rng = rand::rng(); - let mut token = [0u8; 12]; - token.shuffle(&mut rng); - token -}); - -fn criterion_benchmark(c: &mut Criterion) { - let a_session_addr = SessionAddr { - address: "127.0.0.1:1000".parse().unwrap(), - interface: "127.0.0.1:3478".parse().unwrap(), - }; - - let b_session_addr = SessionAddr { - address: "127.0.0.1:1001".parse().unwrap(), - interface: "127.0.0.1:3478".parse().unwrap(), - }; - - let service = Service::new( - "test".to_string(), - "test".to_string(), - vec![a_session_addr.interface], - SimpleObserver, - ); - - let sessions = service.get_sessions(); - let a_nonce = sessions - .get_nonce(&a_session_addr) - .get_ref() - .map(|it| it.0.clone()) - .unwrap(); - let a_integrity = sessions.get_integrity(&a_session_addr, "test", "test").unwrap(); - - { - let _ = sessions.get_integrity(&b_session_addr, "test", "test").unwrap(); - } - - let a_port = sessions.allocate(&a_session_addr).unwrap(); - let b_port = sessions.allocate(&b_session_addr).unwrap(); - - { - sessions.create_permission(&b_session_addr, &b_session_addr.address, &[a_port]); - sessions.bind_channel(&b_session_addr, &b_session_addr.address, a_port, 0x4000); - } - - let mut a_operationer = service.get_operationer(a_session_addr.address, a_session_addr.interface); - - let bind_request = { - let mut bytes = BytesMut::zeroed(1500); - MessageEncoder::new(BINDING_REQUEST, &TOKEN, &mut bytes) - .flush(None) - .unwrap(); - bytes - }; - - let allocate_request = { - let mut bytes = BytesMut::zeroed(1500); - let mut message = MessageEncoder::new(ALLOCATE_REQUEST, &TOKEN, &mut bytes); - message.append::(Transport::UDP); - message.append::("test"); - message.append::("test"); - message.append::(&a_nonce); - message.flush(Some(&a_integrity)).unwrap(); - bytes - }; - - let create_permission_request = { - let mut bytes = BytesMut::zeroed(1500); - let mut message = MessageEncoder::new(CREATE_PERMISSION_REQUEST, &TOKEN, &mut bytes); - message.append::(SocketAddr::new("127.0.0.1".parse().unwrap(), b_port)); - message.append::("test"); - message.append::("test"); - message.append::(&a_nonce); - message.flush(Some(&a_integrity)).unwrap(); - bytes - }; - - let channel_bind_request = { - let mut bytes = BytesMut::zeroed(1500); - let mut message = MessageEncoder::new(CHANNEL_BIND_REQUEST, &TOKEN, &mut bytes); - message.append::(0x4000); - message.append::(SocketAddr::new("127.0.0.1".parse().unwrap(), b_port)); - message.append::("test"); - message.append::("test"); - message.append::(&a_nonce); - message.flush(Some(&a_integrity)).unwrap(); - bytes - }; - - let channel_data = { - let mut bytes = BytesMut::zeroed(1500); - ChannelData { - number: 0x4000, - bytes: TOKEN.as_slice(), - } - .encode(&mut bytes); - bytes - }; - - { - let mut stun = c.benchmark_group("stun"); - let mut codec = Decoder::default(); - - stun.throughput(Throughput::Elements(1)); - - stun.bench_function("decode_binding_request", |b| { - b.iter(|| { - codec.decode(&bind_request).unwrap(); - }) - }); - - stun.bench_function("decode_allocate_request", |b| { - b.iter(|| { - codec.decode(&allocate_request).unwrap(); - }) - }); - - stun.bench_function("decode_create_permission_request", |b| { - b.iter(|| { - codec.decode(&create_permission_request).unwrap(); - }) - }); - - stun.bench_function("decode_channel_bind_request", |b| { - b.iter(|| { - codec.decode(&channel_bind_request).unwrap(); - }) - }); - - stun.bench_function("decode_channel_data", |b| { - b.iter(|| { - codec.decode(&channel_data).unwrap(); - }) - }); - - stun.finish(); - } - - { - let mut turn = c.benchmark_group("turn"); - - turn.throughput(Throughput::Elements(1)); - - turn.bench_function("bind_request", |b| { - b.iter(|| { - a_operationer.route(&bind_request, a_session_addr.address).unwrap(); - }) - }); - - turn.bench_function("allocate_request", |b| { - b.iter(|| { - a_operationer.route(&allocate_request, a_session_addr.address).unwrap(); - }) - }); - - turn.bench_function("create_permission_request", |b| { - b.iter(|| { - a_operationer - .route(&create_permission_request, a_session_addr.address) - .unwrap(); - }) - }); - - turn.bench_function("channel_bind_request", |b| { - b.iter(|| { - a_operationer - .route(&channel_bind_request, a_session_addr.address) - .unwrap(); - }) - }); - - turn.bench_function("channel_data", |b| { - b.iter(|| { - a_operationer.route(&channel_data, a_session_addr.address).unwrap(); - }) - }); - } -} - -criterion_group!(benches, criterion_benchmark); -criterion_main!(benches); diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..7e6f9aae --- /dev/null +++ b/build.rs @@ -0,0 +1,11 @@ +fn main() -> anyhow::Result<()> { + #[cfg(feature = "rpc")] + { + tonic_prost_build::configure() + .build_server(true) + .build_client(true) + .compile_protos(&["protos/server.proto"], &["protos"])?; + } + + Ok(()) +} diff --git a/crates/codec/Cargo.toml b/crates/codec/Cargo.toml new file mode 100644 index 00000000..6b07b847 --- /dev/null +++ b/crates/codec/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "turn-server-codec" +version = "0.1.0" +edition = "2024" + +[dependencies] +bytes = "1" +base64 = "0.22" +num_enum = "0.7" +aws-lc-rs = { version = "1.14", features = ["prebuilt-nasm"] } +md-5 = "0.10" +crc32fast = "1.5.0" + +[dev-dependencies] +anyhow = "1.0" +criterion = "0.7" + +[[bench]] +name = "stun" +harness = false diff --git a/crates/codec/benches/stun.rs b/crates/codec/benches/stun.rs new file mode 100644 index 00000000..6c8e1209 --- /dev/null +++ b/crates/codec/benches/stun.rs @@ -0,0 +1,40 @@ +use criterion::{Criterion, Throughput, criterion_group, criterion_main}; +use turn_server_codec::Decoder; + +fn criterion_benchmark(c: &mut Criterion) { + let mut decoder = Decoder::default(); + + #[rustfmt::skip] + let mut samples = [ + include_bytes!("../../../tests/samples/BindingRequest.bin").as_slice(), + include_bytes!("../../../tests/samples/BindingResponse.bin").as_slice(), + include_bytes!("../../../tests/samples/UnauthorizedAllocateRequest.bin").as_slice(), + include_bytes!("../../../tests/samples/UnauthorizedAllocateResponse.bin").as_slice(), + include_bytes!("../../../tests/samples/AllocateRequest.bin").as_slice(), + include_bytes!("../../../tests/samples/AllocateResponse.bin").as_slice(), + include_bytes!("../../../tests/samples/CreatePermissionRequest.bin").as_slice(), + include_bytes!("../../../tests/samples/CreatePermissionResponse.bin").as_slice(), + include_bytes!("../../../tests/samples/ChannelBindRequest.bin").as_slice(), + include_bytes!("../../../tests/samples/ChannelBindResponse.bin").as_slice(), + include_bytes!("../../../tests/samples/DataIndication.bin").as_slice(), + include_bytes!("../../../tests/samples/SendIndication.bin").as_slice(), + include_bytes!("../../../tests/samples/RefreshRequest.bin").as_slice(), + include_bytes!("../../../tests/samples/RefreshResponse.bin").as_slice(), + ] + .into_iter() + .cycle(); + + let mut stun_criterion = c.benchmark_group("stun"); + + stun_criterion.throughput(Throughput::Elements(1)); + stun_criterion.bench_function("decode_all_simples", |bencher| { + bencher.iter(|| { + decoder.decode(samples.next().unwrap()).unwrap(); + }) + }); + + stun_criterion.finish(); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/stun/channel.rs b/crates/codec/src/channel_data.rs similarity index 61% rename from src/stun/channel.rs rename to crates/codec/src/channel_data.rs index b0e49c9e..dd814efa 100644 --- a/src/stun/channel.rs +++ b/crates/codec/src/channel_data.rs @@ -1,31 +1,11 @@ use bytes::{BufMut, BytesMut}; -use super::StunError; - -use std::convert::TryFrom; +use super::Error; /// The ChannelData Message /// /// The ChannelData message is used to carry application data between the -/// client and the server. -/// It has the following format: -/// -/// ```text -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | Channel Number | Length | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// / Application Data / -/// / / -/// | | -/// | +-------------------------------+ -/// | | -/// +-------------------------------+ -/// -/// Figure 5 -/// ``` +/// client and the server. /// /// The Channel Number field specifies the number of the channel on which /// the data is traveling, and thus, the address of the peer that is @@ -37,21 +17,25 @@ use std::convert::TryFrom; /// /// The Application Data field carries the data the client is trying to /// send to the peer, or that the peer is sending to the client. -#[derive(Debug)] pub struct ChannelData<'a> { - /// channnel data bytes. pub bytes: &'a [u8], - /// channel number. pub number: u16, } -impl ChannelData<'_> { +impl<'a> ChannelData<'a> { + pub fn number(&self) -> u16 { + self.number + } + + pub fn as_bytes(&self) -> &[u8] { + self.bytes + } + /// # Test /// /// ``` /// use bytes::{BufMut, BytesMut}; - /// use std::convert::TryFrom; - /// use turn_server::stun::*; + /// use turn_server_codec::channel_data::ChannelData; /// /// let data: [u8; 4] = [0x40, 0x00, 0x00, 0x40]; /// let mut bytes = BytesMut::with_capacity(1500); @@ -63,15 +47,16 @@ impl ChannelData<'_> { /// .encode(&mut bytes); /// /// let size = ChannelData::message_size(&bytes[..], false).unwrap(); + /// /// assert_eq!(size, 8); /// ``` - pub fn message_size(bytes: &[u8], is_tcp: bool) -> Result { + pub fn message_size(bytes: &[u8], is_tcp: bool) -> Result { if bytes.len() < 4 { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } if !(1..3).contains(&(bytes[0] >> 6)) { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } let mut size = (u16::from_be_bytes(bytes[2..4].try_into()?) + 4) as usize; @@ -86,8 +71,7 @@ impl ChannelData<'_> { /// /// ``` /// use bytes::{BufMut, BytesMut}; - /// use std::convert::TryFrom; - /// use turn_server::stun::*; + /// use turn_server_codec::channel_data::ChannelData; /// /// let data: [u8; 4] = [0x40, 0x00, 0x00, 0x40]; /// let mut bytes = BytesMut::with_capacity(1500); @@ -98,27 +82,23 @@ impl ChannelData<'_> { /// } /// .encode(&mut bytes); /// - /// let ret = ChannelData::try_from(&bytes[..]).unwrap(); + /// let ret = ChannelData::decode(&bytes[..]).unwrap(); + /// /// assert_eq!(ret.number, 16384); /// assert_eq!(ret.bytes, &data[..]); /// ``` pub fn encode(self, bytes: &mut BytesMut) { - unsafe { bytes.set_len(0) } + bytes.clear(); bytes.put_u16(self.number); bytes.put_u16(self.bytes.len() as u16); bytes.extend_from_slice(self.bytes); } -} - -impl<'a> TryFrom<&'a [u8]> for ChannelData<'a> { - type Error = StunError; /// # Test /// /// ``` /// use bytes::{BufMut, BytesMut}; - /// use std::convert::TryFrom; - /// use turn_server::stun::*; + /// use turn_server_codec::channel_data::ChannelData; /// /// let data: [u8; 4] = [0x40, 0x00, 0x00, 0x40]; /// let mut bytes = BytesMut::with_capacity(1500); @@ -129,23 +109,24 @@ impl<'a> TryFrom<&'a [u8]> for ChannelData<'a> { /// } /// .encode(&mut bytes); /// - /// let ret = ChannelData::try_from(&bytes[..]).unwrap(); + /// let ret = ChannelData::decode(&bytes[..]).unwrap(); + /// /// assert_eq!(ret.number, 16384); /// assert_eq!(ret.bytes, &data[..]); /// ``` - fn try_from(bytes: &'a [u8]) -> Result { + pub fn decode(bytes: &'a [u8]) -> Result { if bytes.len() < 4 { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } let number = u16::from_be_bytes(bytes[..2].try_into()?); if !(0x4000..0xFFFF).contains(&number) { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } let size = u16::from_be_bytes(bytes[2..4].try_into()?) as usize; if size > bytes.len() - 4 { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } Ok(Self { diff --git a/crates/codec/src/crypto.rs b/crates/codec/src/crypto.rs new file mode 100644 index 00000000..8bd5593f --- /dev/null +++ b/crates/codec/src/crypto.rs @@ -0,0 +1,157 @@ +use std::ops::Deref; + +use aws_lc_rs::{digest, hmac}; +use base64::{Engine, prelude::BASE64_STANDARD}; +use md5::{Digest, Md5}; + +use crate::message::attributes::PasswordAlgorithm; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Password { + Md5([u8; 16]), + Sha256([u8; 32]), +} + +impl Deref for Password { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + Password::Md5(it) => it, + Password::Sha256(it) => it, + } + } +} + +/// HMAC SHA1 digest. +/// +/// # Test +/// +/// ``` +/// use turn_server_codec::crypto::hmac_sha1; +/// +/// let buffer = [ +/// 0x00u8, 0x03, 0x00, 0x50, 0x21, 0x12, 0xa4, 0x42, 0x64, 0x4f, 0x5a, +/// 0x78, 0x6a, 0x56, 0x33, 0x62, 0x4b, 0x52, 0x33, 0x31, 0x00, 0x19, 0x00, +/// 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x05, 0x70, 0x61, 0x6e, +/// 0x64, 0x61, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x09, 0x72, 0x61, 0x73, +/// 0x70, 0x62, 0x65, 0x72, 0x72, 0x79, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, +/// 0x10, 0x31, 0x63, 0x31, 0x33, 0x64, 0x32, 0x62, 0x32, 0x34, 0x35, 0x62, +/// 0x33, 0x61, 0x37, 0x33, 0x34, +/// ]; +/// +/// let key = [ +/// 0x3eu8, 0x2f, 0x79, 0x1e, 0x1f, 0x14, 0xd1, 0x73, 0xfc, 0x91, 0xff, +/// 0x2f, 0x59, 0xb5, 0x0f, 0xd1, +/// ]; +/// +/// let sign = [ +/// 0xd6u8, 0x78, 0x26, 0x99, 0x0e, 0x15, 0x56, 0x15, 0xe5, 0xf4, 0x24, +/// 0x74, 0xe2, 0x3c, 0x26, 0xc5, 0xb1, 0x03, 0xb2, 0x6d, +/// ]; +/// +/// let hmac_output = hmac_sha1(&key, &[&buffer]); +/// +/// assert_eq!(&hmac_output, &sign); +/// ``` +pub fn hmac_sha1(key: &[u8], source: &[&[u8]]) -> [u8; 20] { + let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key); + let mut ctx = hmac::Context::with_key(&key); + + for buf in source { + ctx.update(buf); + } + + let mut result = [0u8; 20]; + result.copy_from_slice(ctx.sign().as_ref()); + result +} + +/// CRC32 Fingerprint. +/// +/// # Test +/// +/// ``` +/// use turn_server_codec::crypto::fingerprint; +/// +/// assert_eq!(fingerprint(b"1"), 3498621689); +/// ``` +pub fn fingerprint(bytes: &[u8]) -> u32 { + crc32fast::hash(bytes) ^ 0x5354_554e +} + +/// generate create long term credential. +/// +/// > key = MD5(username ":" OpaqueString(realm) ":" OpaqueString(password)) +/// +/// # Test +/// +/// ``` +/// use turn_server_codec::crypto::{generate_password, Password}; +/// use turn_server_codec::message::attributes::PasswordAlgorithm; +/// +/// let buffer = [ +/// 0x3eu8, 0x2f, 0x79, 0x1e, 0x1f, 0x14, 0xd1, 0x73, 0xfc, 0x91, 0xff, +/// 0x2f, 0x59, 0xb5, 0x0f, 0xd1, +/// ]; +/// +/// let password = generate_password( +/// "panda", +/// "panda", +/// "raspberry", +/// PasswordAlgorithm::Md5, +/// ); +/// +/// match password { +/// Password::Md5(it) => { +/// assert_eq!(it, buffer); +/// } +/// Password::Sha256(it) => { +/// unreachable!(); +/// } +/// } +/// ``` +pub fn generate_password( + username: &str, + password: &str, + realm: &str, + algorithm: PasswordAlgorithm, +) -> Password { + match algorithm { + PasswordAlgorithm::Md5 => { + let mut hasher = Md5::new(); + + hasher.update([username, realm, password].join(":")); + + Password::Md5(hasher.finalize().into()) + } + PasswordAlgorithm::Sha256 => { + let mut ctx = digest::Context::new(&digest::SHA256); + + ctx.update([username, realm, password].join(":").as_bytes()); + + let mut result = [0u8; 32]; + result.copy_from_slice(ctx.finish().as_ref()); + Password::Sha256(result) + } + } +} + +// Because (TURN REST api) this RFC does not mandate the format of the username, +// only suggested values. In principle, the RFC also indicates that the +// timestamp part of username can be set at will, so the timestamp is not +// verified here, and the external web service guarantees its security by +// itself. +// +// https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00#section-2.2 +pub fn static_auth_secret( + username: &str, + secret: &str, + realm: &str, + algorithm: PasswordAlgorithm, +) -> Password { + let password = + BASE64_STANDARD.encode(hmac_sha1(secret.as_bytes(), &[username.as_bytes()]).as_slice()); + + generate_password(username, &password, realm, algorithm) +} diff --git a/crates/codec/src/lib.rs b/crates/codec/src/lib.rs new file mode 100644 index 00000000..87de1277 --- /dev/null +++ b/crates/codec/src/lib.rs @@ -0,0 +1,193 @@ +//! ## Session Traversal Utilities for NAT (STUN) +//! +//! [RFC8445]: https://tools.ietf.org/html/rfc8445 +//! [RFC5626]: https://tools.ietf.org/html/rfc5626 +//! [Section 13]: https://tools.ietf.org/html/rfc8489#section-13 +//! +//! STUN is intended to be used in the context of one or more NAT +//! traversal solutions. These solutions are known as "STUN Usages". +//! Each usage describes how STUN is utilized to achieve the NAT +//! traversal solution. Typically, a usage indicates when STUN messages +//! get sent, which optional attributes to include, what server is used, +//! and what authentication mechanism is to be used. Interactive +//! Connectivity Establishment (ICE) [RFC8445] is one usage of STUN. +//! SIP Outbound [RFC5626] is another usage of STUN. In some cases, +//! a usage will require extensions to STUN. A STUN extension can be +//! in the form of new methods, attributes, or error response codes. +//! More information on STUN Usages can be found in [Section 13]. + +pub mod channel_data; +pub mod crypto; +pub mod message; + +use self::{ + channel_data::ChannelData, + message::{Message, attributes::AttributeType}, +}; + +use std::{array::TryFromSliceError, ops::Range, str::Utf8Error}; + +#[derive(Debug)] +pub enum Error { + InvalidInput, + SummaryFailed, + NotFoundIntegrity, + IntegrityFailed, + NotFoundMagicNumber, + UnknownMethod, + FatalError, + Utf8Error(Utf8Error), + TryFromSliceError(TryFromSliceError), +} + +impl std::error::Error for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From for Error { + fn from(value: Utf8Error) -> Self { + Self::Utf8Error(value) + } +} + +impl From for Error { + fn from(value: TryFromSliceError) -> Self { + Self::TryFromSliceError(value) + } +} + +pub enum DecodeResult<'a> { + Message(Message<'a>), + ChannelData(ChannelData<'a>), +} + +/// A cache of the list of attributes, this is for internal use only. +#[derive(Debug, Clone)] +pub struct Attributes(Vec<(AttributeType, Range)>); + +impl Default for Attributes { + fn default() -> Self { + Self(Vec::with_capacity(20)) + } +} + +impl Attributes { + /// Adds an attribute to the list. + pub fn append(&mut self, kind: AttributeType, range: Range) { + self.0.push((kind, range)); + } + + /// Gets an attribute from the list. + /// + /// Note: This function will only look for the first matching property in + /// the list and return it. + pub fn get(&self, kind: &AttributeType) -> Option> { + self.0 + .iter() + .find(|(k, _)| k == kind) + .map(|(_, v)| v.clone()) + } + + /// Gets all the values of an attribute from a list. + /// + /// Normally a stun message can have multiple attributes with the same name, + /// and this function will all the values of the current attribute. + pub fn get_all<'a>( + &'a self, + kind: &'a AttributeType, + ) -> impl Iterator> { + self.0 + .iter() + .filter(move |(k, _)| k == kind) + .map(|(_, v)| v) + } + + pub fn clear(&mut self) { + if !self.0.is_empty() { + self.0.clear(); + } + } +} + +#[derive(Default)] +pub struct Decoder(Attributes); + +impl Decoder { + /// # Test + /// + /// ``` + /// use turn_server_codec::message::attributes::UserName; + /// use turn_server_codec::{Decoder, DecodeResult}; + /// + /// let buffer = [ + /// 0x00, 0x01, 0x00, 0x4c, 0x21, 0x12, 0xa4, 0x42, 0x71, 0x66, 0x46, 0x31, + /// 0x2b, 0x59, 0x79, 0x65, 0x56, 0x69, 0x32, 0x72, 0x00, 0x06, 0x00, 0x09, + /// 0x55, 0x43, 0x74, 0x39, 0x3a, 0x56, 0x2f, 0x2b, 0x2f, 0x00, 0x00, 0x00, + /// 0xc0, 0x57, 0x00, 0x04, 0x00, 0x00, 0x03, 0xe7, 0x80, 0x29, 0x00, 0x08, + /// 0x22, 0x49, 0xda, 0x28, 0x2c, 0x6f, 0x2e, 0xdb, 0x00, 0x24, 0x00, 0x04, + /// 0x6e, 0x00, 0x28, 0xff, 0x00, 0x08, 0x00, 0x14, 0x19, 0x58, 0xda, 0x38, + /// 0xed, 0x1e, 0xdd, 0xc8, 0x6b, 0x8e, 0x22, 0x63, 0x3a, 0x22, 0x63, 0x97, + /// 0xcf, 0xf5, 0xde, 0x82, 0x80, 0x28, 0x00, 0x04, 0x56, 0xf7, 0xa3, 0xed, + /// ]; + /// + /// let mut decoder = Decoder::default(); + /// let payload = decoder.decode(&buffer).unwrap(); + /// + /// if let DecodeResult::Message(reader) = payload { + /// assert!(reader.get::().is_some()) + /// } + /// ``` + pub fn decode<'a>(&'a mut self, bytes: &'a [u8]) -> Result, Error> { + assert!(bytes.len() >= 4); + + let flag = bytes[0] >> 6; + if flag > 3 { + return Err(Error::InvalidInput); + } + + Ok(if flag == 0 { + self.0.clear(); + + DecodeResult::Message(Message::decode(bytes, &mut self.0)?) + } else { + DecodeResult::ChannelData(ChannelData::decode(bytes)?) + }) + } + + /// # Test + /// + /// ``` + /// use turn_server_codec::Decoder; + /// + /// let buffer = [ + /// 0x00, 0x01, 0x00, 0x4c, 0x21, 0x12, 0xa4, 0x42, 0x71, 0x66, 0x46, 0x31, + /// 0x2b, 0x59, 0x79, 0x65, 0x56, 0x69, 0x32, 0x72, 0x00, 0x06, 0x00, 0x09, + /// 0x55, 0x43, 0x74, 0x39, 0x3a, 0x56, 0x2f, 0x2b, 0x2f, 0x00, 0x00, 0x00, + /// 0xc0, 0x57, 0x00, 0x04, 0x00, 0x00, 0x03, 0xe7, 0x80, 0x29, 0x00, 0x08, + /// 0x22, 0x49, 0xda, 0x28, 0x2c, 0x6f, 0x2e, 0xdb, 0x00, 0x24, 0x00, 0x04, + /// 0x6e, 0x00, 0x28, 0xff, 0x00, 0x08, 0x00, 0x14, 0x19, 0x58, 0xda, 0x38, + /// 0xed, 0x1e, 0xdd, 0xc8, 0x6b, 0x8e, 0x22, 0x63, 0x3a, 0x22, 0x63, 0x97, + /// 0xcf, 0xf5, 0xde, 0x82, 0x80, 0x28, 0x00, 0x04, 0x56, 0xf7, 0xa3, 0xed, + /// ]; + /// + /// let size = Decoder::message_size(&buffer, false).unwrap(); + /// + /// assert_eq!(size, 96); + /// ``` + pub fn message_size(bytes: &[u8], is_tcp: bool) -> Result { + let flag = bytes[0] >> 6; + if flag > 3 { + return Err(Error::InvalidInput); + } + + Ok(if flag == 0 { + Message::message_size(bytes)? + } else { + ChannelData::message_size(bytes, is_tcp)? + }) + } +} diff --git a/crates/codec/src/message/attributes/address.rs b/crates/codec/src/message/attributes/address.rs new file mode 100644 index 00000000..b735c296 --- /dev/null +++ b/crates/codec/src/message/attributes/address.rs @@ -0,0 +1,304 @@ +use std::net::{IpAddr, SocketAddr}; + +use bytes::{Buf, BufMut}; +use num_enum::TryFromPrimitive; + +use crate::Error; + +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +pub enum IpFamily { + V4 = 0x01, + V6 = 0x02, +} + +/// [RFC3489]: https://datatracker.ietf.org/doc/html/rfc3489 +/// +/// The Address attribute indicates a reflexive transport address +/// of the client. It consists of an 8-bit address family and a 16-bit +/// port, followed by a fixed-length value representing the IP address. +/// If the address family is IPv4, the address MUST be 32 bits. If the +/// address family is IPv6, the address MUST be 128 bits. All fields +/// must be in network byte order. +/// +/// The format of the MAPPED-ADDRESS attribute is: +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |0 0 0 0 0 0 0 0| Family | Port | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | | +/// | Address (32 bits or 128 bits) | +/// | | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// ``` +/// +/// Figure 5: Format of MAPPED-ADDRESS Attribute +/// +/// The address family can take on the following values: +/// +/// * 0x01:IPv4 +/// * 0x02:IPv6 +/// +/// The first 8 bits of the MAPPED-ADDRESS MUST be set to 0 and MUST be +/// ignored by receivers. These bits are present for aligning parameters +/// on natural 32-bit boundaries. +/// +/// This attribute is used only by servers for achieving backwards +/// compatibility with [RFC3489] clients. +/// +/// The XOR-MAPPED-ADDRESS attribute is identical to the MAPPED-ADDRESS +/// attribute, except that the reflexive transport address is obfuscated +/// through the XOR function. +/// +/// The format of the XOR-MAPPED-ADDRESS is: +/// +/// ```text +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// |0 0 0 0 0 0 0 0| Family | X-Port | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | X-Address (Variable) +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// +/// Figure 6: Format of XOR-MAPPED-ADDRESS Attribute +/// ``` +/// +/// The Family field represents the IP address family and is encoded +/// identically to the Family field in MAPPED-ADDRESS. +/// +/// X-Port is computed by XOR'ing the mapped port with the most +/// significant 16 bits of the magic cookie. If the IP address family is +/// IPv4, X-Address is computed by XOR'ing the mapped IP address with the +/// magic cookie. If the IP address family is IPv6, X-Address is +/// computed by XOR'ing the mapped IP address with the concatenation of +/// the magic cookie and the 96-bit transaction ID. In all cases, the +/// XOR operation works on its inputs in network byte order (that is, the +/// order they will be encoded in the message). +/// +/// The rules for encoding and processing the first 8 bits of the +/// attribute's value, the rules for handling multiple occurrences of the +/// attribute, and the rules for processing address families are the same +/// as for MAPPED-ADDRESS. +/// +/// Note: XOR-MAPPED-ADDRESS and MAPPED-ADDRESS differ only in their +/// encoding of the transport address. The former encodes the transport +/// address by XOR'ing it with the magic cookie. The latter encodes it +/// directly in binary. [RFC3489] originally specified only MAPPED- +/// ADDRESS. However, deployment experience found that some NATs rewrite +/// the 32-bit binary payloads containing the NAT's public IP address, +/// such as STUN's MAPPED-ADDRESS attribute, in the well-meaning but +/// misguided attempt to provide a generic Application Layer Gateway +/// (ALG) function. Such behavior interferes with the operation of STUN +/// and also causes failure of STUN's message-integrity checking. +#[derive(Debug, Clone, Copy)] +pub struct XAddress; + +impl XAddress { + /// encoder SocketAddr as Bytes. + /// + /// # Test + /// + /// ``` + /// use bytes::BytesMut; + /// use turn_server_codec::message::attributes::address::XAddress; + /// + /// let xor_addr_bytes: [u8; 8] = + /// [0x00, 0x01, 0xfc, 0xbe, 0xe1, 0xba, 0xa4, 0x29]; + /// + /// let addr_bytes: [u8; 8] = [0x00, 0x01, 0xdd, 0xac, 0xc0, 0xa8, 0x00, 0x6b]; + /// + /// let transaction_id: [u8; 12] = [ + /// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, + /// ]; + /// + /// let source = "192.168.0.107:56748".parse().unwrap(); + /// + /// let mut buffer = BytesMut::with_capacity(1280); + /// XAddress::serialize(&source, &transaction_id, &mut buffer, true); + /// assert_eq!(&xor_addr_bytes, &buffer[..]); + /// + /// let mut buffer = BytesMut::with_capacity(1280); + /// XAddress::serialize(&source, &transaction_id, &mut buffer, false); + /// assert_eq!(&addr_bytes, &buffer[..]); + /// ``` + pub fn serialize( + addr: &SocketAddr, + transaction_id: &[u8], + bytes: &mut B, + is_xor: bool, + ) { + bytes.put_u8(0); + + let xor_addr = if is_xor { + xor(addr, transaction_id) + } else { + *addr + }; + + bytes.put_u8(if xor_addr.is_ipv4() { + IpFamily::V4 + } else { + IpFamily::V6 + } as u8); + + bytes.put_u16(xor_addr.port()); + + if let IpAddr::V4(ip) = xor_addr.ip() { + bytes.put(&ip.octets()[..]); + } + + if let IpAddr::V6(ip) = xor_addr.ip() { + bytes.put(&ip.octets()[..]); + } + } + + /// decoder Bytes as SocketAddr. + /// + /// # Test + /// + /// ``` + /// use turn_server_codec::message::attributes::address::XAddress; + /// + /// let xor_addr_bytes: [u8; 8] = + /// [0x00, 0x01, 0xfc, 0xbe, 0xe1, 0xba, 0xa4, 0x29]; + /// + /// let addr_bytes: [u8; 8] = [0x00, 0x01, 0xdd, 0xac, 0xc0, 0xa8, 0x00, 0x6b]; + /// + /// let transaction_id: [u8; 12] = [ + /// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, + /// ]; + /// + /// let source = "192.168.0.107:56748".parse().unwrap(); + /// + /// let addr = XAddress::deserialize(&xor_addr_bytes, &transaction_id, true).unwrap(); + /// assert_eq!(addr, source); + /// + /// let addr = XAddress::deserialize(&addr_bytes, &transaction_id, false).unwrap(); + /// assert_eq!(addr, source); + /// ``` + pub fn deserialize( + mut bytes: &[u8], + transaction_id: &[u8], + is_xor: bool, + ) -> Result { + if bytes.len() < 4 { + return Err(Error::InvalidInput); + } + + // skip the first 8 bits + bytes.advance(1); + + let family = IpFamily::try_from(bytes.get_u8()).map_err(|_| Error::InvalidInput)?; + let port = bytes.get_u16(); + + let addr = SocketAddr::new( + match family { + IpFamily::V4 => ipv4_from_bytes(bytes)?, + IpFamily::V6 => ipv6_from_bytes(bytes)?, + }, + port, + ); + + Ok(if is_xor { + xor(&addr, transaction_id) + } else { + addr + }) + } +} + +/// # Test +/// +/// ``` +/// use std::net::IpAddr; +/// use turn_server_codec::message::attributes::address::ipv4_from_bytes; +/// +/// let bytes: [u8; 4] = [0xc0, 0xa8, 0x00, 0x6b]; +/// +/// let source: IpAddr = "192.168.0.107".parse().unwrap(); +/// +/// let addr = ipv4_from_bytes(&bytes).unwrap(); +/// assert_eq!(addr, source); +/// ``` +pub fn ipv4_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 4 { + return Err(Error::InvalidInput); + } + + let bytes: [u8; 4] = bytes[..4].try_into()?; + Ok(IpAddr::V4(bytes.into())) +} + +/// # Test +/// +/// ``` +/// use std::net::IpAddr; +/// use turn_server_codec::message::attributes::address::ipv6_from_bytes; +/// +/// let bytes: [u8; 16] = [ +/// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +/// 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x0A, 0x2F, 0x0F, +/// ]; +/// +/// let source: IpAddr = "::ffff:192.10.47.15".parse().unwrap(); +/// +/// let addr = ipv6_from_bytes(&bytes).unwrap(); +/// assert_eq!(addr, source); +/// ``` +pub fn ipv6_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != 16 { + return Err(Error::InvalidInput); + } + + let bytes: [u8; 16] = bytes[..16].try_into()?; + Ok(IpAddr::V6(bytes.into())) +} + +/// # Test +/// +/// ``` +/// use std::net::SocketAddr; +/// use turn_server_codec::message::attributes::address::xor; +/// +/// let source: SocketAddr = "192.168.0.107:1".parse().unwrap(); +/// +/// let res: SocketAddr = "225.186.164.41:8467".parse().unwrap(); +/// +/// let transaction_id: [u8; 12] = [ +/// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, +/// ]; +/// +/// let addr = xor(&source, &transaction_id); +/// assert_eq!(addr, res); +/// ``` +pub fn xor(addr: &SocketAddr, transaction_id: &[u8]) -> SocketAddr { + SocketAddr::new( + match addr.ip() { + IpAddr::V4(it) => { + let mut octets = it.octets(); + for (i, b) in octets.iter_mut().enumerate() { + *b ^= (0x2112A442 >> (24 - i * 8)) as u8; + } + + IpAddr::V4(From::from(octets)) + } + IpAddr::V6(it) => { + let mut octets = it.octets(); + for (i, b) in octets.iter_mut().enumerate().take(4) { + *b ^= (0x2112A442 >> (24 - i * 8)) as u8; + } + + for (i, b) in octets.iter_mut().enumerate().take(16).skip(4) { + *b ^= transaction_id[i - 4]; + } + + IpAddr::V6(From::from(octets)) + } + }, + addr.port() ^ (0x2112A442 >> 16) as u16, + ) +} diff --git a/crates/codec/src/message/attributes/error.rs b/crates/codec/src/message/attributes/error.rs new file mode 100644 index 00000000..0bee2144 --- /dev/null +++ b/crates/codec/src/message/attributes/error.rs @@ -0,0 +1,96 @@ +use num_enum::TryFromPrimitive; + +/// The following error codes, along with their recommended reason +/// phrases, are defined: +/// +/// 300 Try Alternate: The client should contact an alternate server for +/// this request. This error response MUST only be sent if the +/// request included either a USERNAME or USERHASH attribute and a +/// valid MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 attribute; +/// otherwise, it MUST NOT be sent and error code 400 (Bad Request) +/// is suggested. This error response MUST be protected with the +/// MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 attribute, and +/// receivers MUST validate the MESSAGE-INTEGRITY or MESSAGE- +/// INTEGRITY-SHA256 of this response before redirecting themselves +/// to an alternate server. +/// Note: Failure to generate and validate message integrity for a +/// 300 response allows an on-path attacker to falsify a 300 +/// response thus causing subsequent STUN messages to be sent to a +/// victim. +/// +/// 400 Bad Request: The request was malformed. The client SHOULD NOT +/// retry the request without modification from the previous +/// attempt. The server may not be able to generate a valid +/// MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 for this error, so +/// the client MUST NOT expect a valid MESSAGE-INTEGRITY or MESSAGE- +/// INTEGRITY-SHA256 attribute on this response. +/// +/// 401 Unauthenticated: The request did not contain the correct +/// credentials to proceed. The client should retry the request +/// with proper credentials. +/// +/// 420 Unknown Attribute: The server received a STUN packet containing +/// a comprehension-required attribute that it did not understand. +/// The server MUST put this unknown attribute in the UNKNOWN- +/// ATTRIBUTE attribute of its error response. +/// +/// 438 Stale Nonce: The NONCE used by the client was no longer valid. +/// The client should retry, using the NONCE provided in the +/// response. +/// +/// 500 Server Error: The server has suffered a temporary error. The +/// client should try again. +const fn errno(code: u16) -> u16 { + ((code / 100) << 8) | (code % 100) +} + +#[repr(u16)] +#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash, TryFromPrimitive)] +pub enum ErrorType { + TryAlternate = errno(300), + BadRequest = errno(400), + Unauthorized = errno(401), + Forbidden = errno(403), + UnknownAttribute = errno(420), + AllocationMismatch = errno(437), + StaleNonce = errno(438), + AddressFamilyNotSupported = errno(440), + WrongCredentials = errno(441), + UnsupportedTransportAddress = errno(442), + PeerAddressFamilyMismatch = errno(443), + AllocationQuotaReached = errno(486), + ServerError = errno(500), + InsufficientCapacity = errno(508), +} + +impl From for &'static str { + /// # Test + /// + /// ``` + /// use std::convert::Into; + /// use turn_server_codec::message::attributes::error::ErrorType; + /// use turn_server_codec::Error; + /// + /// let err: &'static str = ErrorType::TryAlternate.into(); + /// assert_eq!(err, "Try Alternate"); + /// ``` + #[rustfmt::skip] + fn from(val: ErrorType) -> Self { + match val { + ErrorType::TryAlternate => "Try Alternate", + ErrorType::BadRequest => "Bad Request", + ErrorType::Unauthorized => "Unauthorized", + ErrorType::Forbidden => "Forbidden", + ErrorType::UnknownAttribute => "Unknown Attribute", + ErrorType::AllocationMismatch => "Allocation Mismatch", + ErrorType::StaleNonce => "Stale Nonce", + ErrorType::AddressFamilyNotSupported => "Address Family not Supported", + ErrorType::WrongCredentials => "Wrong Credentials", + ErrorType::UnsupportedTransportAddress => "Unsupported Transport Address", + ErrorType::AllocationQuotaReached => "Allocation Quota Reached", + ErrorType::ServerError => "Server Error", + ErrorType::InsufficientCapacity => "Insufficient Capacity", + ErrorType::PeerAddressFamilyMismatch => "Peer Address Family Mismatch", + } + } +} diff --git a/src/stun/attribute.rs b/crates/codec/src/message/attributes/mod.rs similarity index 52% rename from src/stun/attribute.rs rename to crates/codec/src/message/attributes/mod.rs index 1ce71de6..99552bf8 100644 --- a/src/stun/attribute.rs +++ b/crates/codec/src/message/attributes/mod.rs @@ -1,333 +1,18 @@ -use super::StunError; +pub mod address; +pub mod error; -use std::{ - fmt::Debug, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, -}; +use std::{fmt::Debug, net::SocketAddr}; -use bytes::{Buf, BufMut, BytesMut}; +use bytes::{Buf, BufMut}; use num_enum::TryFromPrimitive; -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] -pub enum Transport { - TCP = 0x06000000, - UDP = 0x11000000, -} - -#[repr(u8)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum IpFamily { - V4 = 0x01, - V6 = 0x02, -} - -impl TryFrom for IpFamily { - type Error = StunError; - - fn try_from(value: u8) -> Result { - Ok(match value { - 0x01 => Self::V4, - 0x02 => Self::V6, - _ => return Err(StunError::InvalidInput), - }) - } -} - -/// [RFC3489]: https://datatracker.ietf.org/doc/html/rfc3489 -/// -/// The Address attribute indicates a reflexive transport address -/// of the client. It consists of an 8-bit address family and a 16-bit -/// port, followed by a fixed-length value representing the IP address. -/// If the address family is IPv4, the address MUST be 32 bits. If the -/// address family is IPv6, the address MUST be 128 bits. All fields -/// must be in network byte order. -/// -/// The format of the MAPPED-ADDRESS attribute is: -/// -/// ```text -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |0 0 0 0 0 0 0 0| Family | Port | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | | -/// | Address (32 bits or 128 bits) | -/// | | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// ``` -/// -/// Figure 5: Format of MAPPED-ADDRESS Attribute -/// -/// The address family can take on the following values: -/// -/// * 0x01:IPv4 -/// * 0x02:IPv6 -/// -/// The first 8 bits of the MAPPED-ADDRESS MUST be set to 0 and MUST be -/// ignored by receivers. These bits are present for aligning parameters -/// on natural 32-bit boundaries. -/// -/// This attribute is used only by servers for achieving backwards -/// compatibility with [RFC3489] clients. -/// -/// The XOR-MAPPED-ADDRESS attribute is identical to the MAPPED-ADDRESS -/// attribute, except that the reflexive transport address is obfuscated -/// through the XOR function. -/// -/// The format of the XOR-MAPPED-ADDRESS is: -/// -/// ```text -/// 0 1 2 3 -/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// |0 0 0 0 0 0 0 0| Family | X-Port | -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// | X-Address (Variable) -/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -/// -/// Figure 6: Format of XOR-MAPPED-ADDRESS Attribute -/// ``` -/// -/// The Family field represents the IP address family and is encoded -/// identically to the Family field in MAPPED-ADDRESS. -/// -/// X-Port is computed by XOR'ing the mapped port with the most -/// significant 16 bits of the magic cookie. If the IP address family is -/// IPv4, X-Address is computed by XOR'ing the mapped IP address with the -/// magic cookie. If the IP address family is IPv6, X-Address is -/// computed by XOR'ing the mapped IP address with the concatenation of -/// the magic cookie and the 96-bit transaction ID. In all cases, the -/// XOR operation works on its inputs in network byte order (that is, the -/// order they will be encoded in the message). -/// -/// The rules for encoding and processing the first 8 bits of the -/// attribute's value, the rules for handling multiple occurrences of the -/// attribute, and the rules for processing address families are the same -/// as for MAPPED-ADDRESS. -/// -/// Note: XOR-MAPPED-ADDRESS and MAPPED-ADDRESS differ only in their -/// encoding of the transport address. The former encodes the transport -/// address by XOR'ing it with the magic cookie. The latter encodes it -/// directly in binary. [RFC3489] originally specified only MAPPED- -/// ADDRESS. However, deployment experience found that some NATs rewrite -/// the 32-bit binary payloads containing the NAT's public IP address, -/// such as STUN's MAPPED-ADDRESS attribute, in the well-meaning but -/// misguided attempt to provide a generic Application Layer Gateway -/// (ALG) function. Such behavior interferes with the operation of STUN -/// and also causes failure of STUN's message-integrity checking. -pub struct Addr; - -impl Addr { - /// encoder SocketAddr as Bytes. - /// - /// # Test - /// - /// ``` - /// use bytes::BytesMut; - /// use turn_server::stun::attribute::*; - /// - /// let xor_addr_bytes: [u8; 8] = - /// [0x00, 0x01, 0xfc, 0xbe, 0xe1, 0xba, 0xa4, 0x29]; - /// - /// let addr_bytes: [u8; 8] = [0x00, 0x01, 0xdd, 0xac, 0xc0, 0xa8, 0x00, 0x6b]; - /// - /// let token: [u8; 12] = [ - /// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, - /// ]; - /// - /// let source = "192.168.0.107:56748".parse().unwrap(); - /// - /// let mut buffer = BytesMut::with_capacity(1280); - /// Addr::encode(&source, &token, &mut buffer, true); - /// assert_eq!(&xor_addr_bytes, &buffer[..]); - /// - /// let mut buffer = BytesMut::with_capacity(1280); - /// Addr::encode(&source, &token, &mut buffer, false); - /// assert_eq!(&addr_bytes, &buffer[..]); - /// ``` - pub fn encode(addr: &SocketAddr, token: &[u8], bytes: &mut BytesMut, is_xor: bool) { - bytes.put_u8(0); - let xor_addr = if is_xor { xor(addr, token) } else { *addr }; - - bytes.put_u8(if xor_addr.is_ipv4() { IpFamily::V4 } else { IpFamily::V6 } as u8); - - bytes.put_u16(xor_addr.port()); - if let IpAddr::V4(ip) = xor_addr.ip() { - bytes.put(&ip.octets()[..]); - } - - if let IpAddr::V6(ip) = xor_addr.ip() { - bytes.put(&ip.octets()[..]); - } - } - - /// decoder Bytes as SocketAddr. - /// - /// # Test - /// - /// ``` - /// use turn_server::stun::attribute::*; - /// - /// let xor_addr_bytes: [u8; 8] = - /// [0x00, 0x01, 0xfc, 0xbe, 0xe1, 0xba, 0xa4, 0x29]; - /// - /// let addr_bytes: [u8; 8] = [0x00, 0x01, 0xdd, 0xac, 0xc0, 0xa8, 0x00, 0x6b]; - /// - /// let token: [u8; 12] = [ - /// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, - /// ]; - /// - /// let source = "192.168.0.107:56748".parse().unwrap(); - /// - /// let addr = Addr::decode(&xor_addr_bytes, &token, true).unwrap(); - /// assert_eq!(addr, source); - /// - /// let addr = Addr::decode(&addr_bytes, &token, false).unwrap(); - /// assert_eq!(addr, source); - /// ``` - pub fn decode(packet: &[u8], token: &[u8], is_xor: bool) -> Result { - if packet.len() < 4 { - return Err(StunError::InvalidInput); - } - - let port = u16::from_be_bytes([packet[2], packet[3]]); - let ip_addr = match IpFamily::try_from(packet[1])? { - IpFamily::V4 => from_bytes_v4(packet)?, - IpFamily::V6 => from_bytes_v6(packet)?, - }; - - let dyn_addr = SocketAddr::new(ip_addr, port); - Ok(if is_xor { xor(&dyn_addr, token) } else { dyn_addr }) - } -} - -/// # Test -/// -/// ``` -/// use std::net::IpAddr; -/// use turn_server::stun::attribute::*; -/// -/// let bytes: [u8; 8] = [0x00, 0x01, 0xdd, 0xac, 0xc0, 0xa8, 0x00, 0x6b]; -/// -/// let source: IpAddr = "192.168.0.107".parse().unwrap(); -/// -/// let addr = from_bytes_v4(&bytes).unwrap(); -/// assert_eq!(addr, source); -/// ``` -pub fn from_bytes_v4(packet: &[u8]) -> Result { - if packet.len() != 8 { - return Err(StunError::InvalidInput); - } - - let bytes: [u8; 4] = packet[4..8].try_into()?; - Ok(IpAddr::V4(bytes.into())) -} - -/// # Test -/// -/// ``` -/// use std::net::IpAddr; -/// use turn_server::stun::attribute::*; -/// -/// let bytes: [u8; 20] = [ -/// 0x00, 0x01, 0xdd, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -/// 0x00, 0x00, 0xFF, 0xFF, 0xC0, 0x0A, 0x2F, 0x0F, -/// ]; -/// -/// let source: IpAddr = "::ffff:192.10.47.15".parse().unwrap(); -/// -/// let addr = from_bytes_v6(&bytes).unwrap(); -/// assert_eq!(addr, source); -/// ``` -pub fn from_bytes_v6(packet: &[u8]) -> Result { - if packet.len() != 20 { - return Err(StunError::InvalidInput); - } - - let bytes: [u8; 16] = packet[4..20].try_into()?; - Ok(IpAddr::V6(bytes.into())) -} - -/// # Test -/// -/// ``` -/// use std::net::SocketAddr; -/// use turn_server::stun::attribute::*; -/// -/// let source: SocketAddr = "192.168.0.107:1".parse().unwrap(); -/// -/// let res: SocketAddr = "225.186.164.41:8467".parse().unwrap(); -/// -/// let token: [u8; 12] = [ -/// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, -/// ]; -/// -/// let addr = xor(&source, &token); -/// assert_eq!(addr, res); -/// ``` -pub fn xor(addr: &SocketAddr, token: &[u8]) -> SocketAddr { - let port = addr.port() ^ (0x2112A442 >> 16) as u16; - let ip_addr = match addr.ip() { - IpAddr::V4(x) => xor_v4(x), - IpAddr::V6(x) => xor_v6(x, token), - }; - - SocketAddr::new(ip_addr, port) -} - -/// # Test -/// -/// ``` -/// use std::net::{IpAddr, Ipv4Addr}; -/// use turn_server::stun::attribute::*; -/// -/// let source: Ipv4Addr = "192.168.0.107".parse().unwrap(); -/// -/// let xor: IpAddr = "225.186.164.41".parse().unwrap(); -/// -/// let addr = xor_v4(source); -/// assert_eq!(addr, xor); -/// ``` -pub fn xor_v4(addr: Ipv4Addr) -> IpAddr { - let mut octets = addr.octets(); - for (i, b) in octets.iter_mut().enumerate() { - *b ^= (0x2112A442 >> (24 - i * 8)) as u8; - } - - IpAddr::V4(From::from(octets)) -} - -/// # Test -/// -/// ``` -/// use std::net::{IpAddr, Ipv6Addr}; -/// use turn_server::stun::attribute::*; -/// -/// let source: Ipv6Addr = "::ffff:192.10.47.15".parse().unwrap(); -/// -/// let xor: IpAddr = -/// "2112:a442:6c46:6254:754b:bbae:8642:637e".parse().unwrap(); -/// -/// let token: [u8; 12] = [ -/// 0x6c, 0x46, 0x62, 0x54, 0x75, 0x4b, 0x44, 0x51, 0x46, 0x48, 0x4c, 0x71, -/// ]; -/// -/// let addr = xor_v6(source, &token); -/// assert_eq!(addr, xor); -/// ``` -pub fn xor_v6(addr: Ipv6Addr, token: &[u8]) -> IpAddr { - let mut octets = addr.octets(); - for (i, b) in octets.iter_mut().enumerate().take(4) { - *b ^= (0x2112A442 >> (24 - i * 8)) as u8; - } - - for (i, b) in octets.iter_mut().enumerate().take(16).skip(4) { - *b ^= token[i - 4]; - } - - IpAddr::V6(From::from(octets)) -} +use crate::{ + Error, + message::attributes::{ + address::{IpFamily, XAddress}, + error::ErrorType, + }, +}; /// STUN Attributes Registry /// @@ -399,7 +84,7 @@ pub fn xor_v6(addr: Ipv6Addr, token: &[u8]) -> IpAddr { /// 0x8003: ALTERNATE-DOMAIN #[repr(u16)] #[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug, TryFromPrimitive)] -pub enum AttrKind { +pub enum AttributeType { #[default] Unknown = 0x0000, MappedAddress = 0x0001, @@ -455,14 +140,14 @@ pub trait Attribute<'a> { type Item; /// current attribute type. - const KIND: AttrKind; + const TYPE: AttributeType; /// write the current attribute to the bytesfer. #[allow(unused_variables)] - fn encode(value: Self::Item, bytes: &mut BytesMut, token: &'a [u8]) {} + fn serialize(value: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) {} /// convert bytesfer to current attribute. - fn decode(bytes: &'a [u8], token: &'a [u8]) -> Result; + fn deserialize(bytes: &'a [u8], transaction_id: &'a [u8]) -> Result; } /// [RFC8265]: https://datatracker.ietf.org/doc/html/rfc8265 @@ -479,42 +164,72 @@ pub trait Attribute<'a> { /// the OpaqueString profile [RFC8265]. A compliant implementation MUST /// be able to parse a UTF-8-encoded sequence of 763 or fewer octets to /// be compatible with [RFC5389]. +#[derive(Debug, Clone, Copy)] pub struct UserName; impl<'a> Attribute<'a> for UserName { - type Error = StunError; + type Error = Error; type Item = &'a str; - const KIND: AttrKind = AttrKind::UserName; + const TYPE: AttributeType = AttributeType::UserName; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value.as_bytes()); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(std::str::from_utf8(bytes)?) } } +/// The USERHASH attribute is used as a replacement for the USERNAME attribute +/// when username anonymity is supported. +/// +/// The value of USERHASH has a fixed length of 32 bytes. The username MUST have +/// been processed using the OpaqueString profile [RFC8265], and the realm MUST +/// have been processed using the OpaqueString profile [RFC8265] before hashing. +/// +/// The following is the operation that the client will perform to hash the username: +/// +/// userhash = SHA-256(OpaqueString(username) ":" OpaqueString(realm)) +#[derive(Debug, Clone, Copy)] +pub struct UserHash; + +impl<'a> Attribute<'a> for UserHash { + type Error = Error; + type Item = &'a [u8]; + + const TYPE: AttributeType = AttributeType::UserHash; + + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { + bytes.put(value); + } + + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + Ok(bytes) + } +} + /// The DATA attribute is present in all Send and Data indications. The /// value portion of this attribute is variable length and consists of /// the application data (that is, the data that would immediately follow /// the UDP header if the data was been sent directly between the client /// and the peer). If the length of this attribute is not a multiple of /// 4, then padding must be added after this attribute. +#[derive(Debug, Clone, Copy)] pub struct Data; impl<'a> Attribute<'a> for Data { - type Error = StunError; + type Error = Error; type Item = &'a [u8]; - const KIND: AttrKind = AttrKind::Data; + const TYPE: AttributeType = AttributeType::Data; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(bytes) } } @@ -537,19 +252,20 @@ impl<'a> Attribute<'a> for Data { /// credentials are being used for authentication. Presence in certain /// error responses indicates that the server wishes the client to use a /// long-term credential in that realm for authentication. +#[derive(Debug, Clone, Copy)] pub struct Realm; impl<'a> Attribute<'a> for Realm { - type Error = StunError; + type Error = Error; type Item = &'a str; - const KIND: AttrKind = AttrKind::Realm; + const TYPE: AttributeType = AttributeType::Realm; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value.as_bytes()); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(std::str::from_utf8(bytes)?) } } @@ -565,19 +281,20 @@ impl<'a> Attribute<'a> for Realm { /// when encoding them and a long as 763 bytes when decoding them). See /// Section 5.4 of [RFC7616] for guidance on selection of nonce values in /// a server. +#[derive(Debug, Clone, Copy)] pub struct Nonce; impl<'a> Attribute<'a> for Nonce { - type Error = StunError; + type Error = Error; type Item = &'a str; - const KIND: AttrKind = AttrKind::Nonce; + const TYPE: AttributeType = AttributeType::Nonce; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value.as_bytes()); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(std::str::from_utf8(bytes)?) } } @@ -593,19 +310,20 @@ impl<'a> Attribute<'a> for Nonce { /// [RFC3629] sequence of fewer than 128 characters (which can be as long /// as 509 when encoding them and as long as 763 bytes when decoding /// them). +#[derive(Debug, Clone, Copy)] pub struct Software; impl<'a> Attribute<'a> for Software { - type Error = StunError; + type Error = Error; type Item = &'a str; - const KIND: AttrKind = AttrKind::Software; + const TYPE: AttributeType = AttributeType::Software; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value.as_bytes()); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(std::str::from_utf8(bytes)?) } } @@ -641,43 +359,209 @@ impl<'a> Attribute<'a> for Software { /// attributes, such as FINGERPRINT and MESSAGE-INTEGRITY-SHA256, appear /// after MESSAGE-INTEGRITY. See also [RFC5769] for examples of such /// calculations. +#[derive(Debug, Clone, Copy)] pub struct MessageIntegrity; impl<'a> Attribute<'a> for MessageIntegrity { - type Error = StunError; + type Error = Error; type Item = &'a [u8]; - const KIND: AttrKind = AttrKind::MessageIntegrity; + const TYPE: AttributeType = AttributeType::MessageIntegrity; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(bytes) } } +/// The MESSAGE-INTEGRITY-SHA256 attribute contains an HMAC-SHA256 +/// [RFC2104] of the STUN message. The MESSAGE-INTEGRITY-SHA256 +/// attribute can be present in any STUN message type. The MESSAGE- +/// INTEGRITY-SHA256 attribute contains an initial portion of the HMAC- +/// SHA-256 [RFC2104] of the STUN message. The value will be at most 32 +/// bytes, but it MUST be at least 16 bytes and MUST be a multiple of 4 +/// bytes. The value must be the full 32 bytes unless the STUN Usage +/// explicitly specifies that truncation is allowed. STUN Usages may +/// specify a minimum length longer than 16 bytes. +/// +/// The key for the HMAC depends on which credential mechanism is in use. +/// Section 9.1.1 defines the key for the short-term credential +/// mechanism, and Section 9.2.2 defines the key for the long-term +/// credential mechanism. Other credential mechanism MUST define the key +/// that is used for the HMAC. +/// +/// The text used as input to HMAC is the STUN message, up to and +/// including the attribute preceding the MESSAGE-INTEGRITY-SHA256 +/// attribute. The Length field of the STUN message header is adjusted +/// to point to the end of the MESSAGE-INTEGRITY-SHA256 attribute. The +/// value of the MESSAGE-INTEGRITY-SHA256 attribute is set to a dummy +/// value. +/// +/// Once the computation is performed, the value of the MESSAGE- +/// INTEGRITY-SHA256 attribute is filled in, and the value of the length +/// in the STUN header is set to its correct value -- the length of the +/// entire message. Similarly, when validating the MESSAGE-INTEGRITY- +/// SHA256, the Length field in the STUN header must be adjusted to point +/// to the end of the MESSAGE-INTEGRITY-SHA256 attribute prior to +/// calculating the HMAC over the STUN message, up to and including the +/// attribute preceding the MESSAGE-INTEGRITY-SHA256 attribute. Such +/// adjustment is necessary when attributes, such as FINGERPRINT, appear +/// after MESSAGE-INTEGRITY-SHA256. See also Appendix B.1 for examples +/// of such calculations. +#[derive(Debug, Clone, Copy)] +pub struct MessageIntegritySha256; + +impl<'a> Attribute<'a> for MessageIntegritySha256 { + type Error = Error; + type Item = &'a [u8]; + + const TYPE: AttributeType = AttributeType::MessageIntegritySha256; + + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { + bytes.put(value); + } + + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + Ok(bytes) + } +} + +/// The PASSWORD-ALGORITHM attribute is present only in requests. It +/// contains the algorithm that the server must use to derive a key from +/// the long-term password. +/// +/// The set of known algorithms is maintained by IANA. The initial set +/// defined by this specification is found in Section 18.5. +/// +/// The attribute contains an algorithm number and variable length +/// parameters. The algorithm number is a 16-bit value as defined in +/// Section 18.5. The parameters starts with the length (prior to +/// padding) of the parameters as a 16-bit value, followed by the +/// parameters that are specific to the algorithm. The parameters are +/// padded to a 32-bit boundary, in the same manner as an attribute. +/// Similarly, the padding bits MUST be set to zero on sending and MUST +/// be ignored by the receiver. +/// +/// 0 1 2 3 +/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Algorithm | Algorithm Parameters Length | +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +/// | Algorithm Parameters (variable) +/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +#[repr(u16)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum PasswordAlgorithm { + Md5 = 0x0001, + Sha256 = 0x0002, +} + +impl<'a> Attribute<'a> for PasswordAlgorithm { + type Error = Error; + type Item = Self; + + const TYPE: AttributeType = AttributeType::PasswordAlgorithm; + + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { + bytes.put_u16(value as u16); + bytes.put_u16(0); + } + + fn deserialize(mut bytes: &'a [u8], _: &'a [u8]) -> Result { + if bytes.len() < 4 { + return Err(Error::InvalidInput); + } + + let ty = match bytes.get_u16() { + 0x0001 => Self::Md5, + 0x0002 => Self::Sha256, + _ => return Err(Error::InvalidInput), + }; + + // Ignore attribute value, as it does not exist currently + let size = bytes.get_u16(); + bytes.advance(super::alignment_32(size as usize)); + + Ok(ty) + } +} + +/// The PASSWORD-ALGORITHMS attribute may be present in requests and +/// responses. It contains the list of algorithms that the server can +/// use to derive the long-term password. +/// +/// The set of known algorithms is maintained by IANA. The initial set +/// defined by this specification is found in Section 18.5. +/// +/// The attribute contains a list of algorithm numbers and variable +/// length parameters. The algorithm number is a 16-bit value as defined +/// in Section 18.5. The parameters start with the length (prior to +/// padding) of the parameters as a 16-bit value, followed by the +/// parameters that are specific to each algorithm. The parameters are +/// padded to a 32-bit boundary, in the same manner as an attribute. +pub struct PasswordAlgorithms; + +impl<'a> Attribute<'a> for PasswordAlgorithms { + type Error = Error; + type Item = Vec; + + const TYPE: AttributeType = AttributeType::PasswordAlgorithms; + + fn serialize(algorithms: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) { + for algorithm in algorithms { + PasswordAlgorithm::serialize(algorithm, bytes, transaction_id); + } + } + + fn deserialize(mut bytes: &'a [u8], _: &'a [u8]) -> Result { + let mut algorithms = Vec::new(); + + loop { + if bytes.len() < 4 { + break; + } + + let ty = match bytes.get_u16() { + 0x0001 => PasswordAlgorithm::Md5, + 0x0002 => PasswordAlgorithm::Sha256, + _ => break, + }; + + // Ignore attribute value, as it does not exist currently + let size = bytes.get_u16(); + bytes.advance(super::alignment_32(size as usize)); + + algorithms.push(ty); + } + + Ok(algorithms) + } +} + /// [RFC5389]: https://datatracker.ietf.org/doc/html/rfc5389 /// /// The XOR-PEER-ADDRESS specifies the address and port of the peer as /// seen from the TURN server. (For example, the peer's server-reflexive /// transport address if the peer is behind a NAT.) It is encoded in the /// same way as XOR-MAPPED-ADDRESS [RFC5389]. +#[derive(Debug, Clone, Copy)] pub struct XorPeerAddress; impl<'a> Attribute<'a> for XorPeerAddress { - type Error = StunError; + type Error = Error; type Item = SocketAddr; - const KIND: AttrKind = AttrKind::XorPeerAddress; + const TYPE: AttributeType = AttributeType::XorPeerAddress; - fn encode(value: Self::Item, bytes: &mut BytesMut, token: &'a [u8]) { - Addr::encode(&value, token, bytes, true) + fn serialize(value: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) { + XAddress::serialize(&value, transaction_id, bytes, true) } - fn decode(bytes: &'a [u8], token: &'a [u8]) -> Result { - Addr::decode(bytes, token, true) + fn deserialize(bytes: &'a [u8], transaction_id: &'a [u8]) -> Result { + XAddress::deserialize(bytes, transaction_id, true) } } @@ -687,20 +571,21 @@ impl<'a> Attribute<'a> for XorPeerAddress { /// specifies the address and port that the server allocated to the /// client. It is encoded in the same way as XOR-MAPPED-ADDRESS /// [RFC5389]. +#[derive(Debug, Clone, Copy)] pub struct XorRelayedAddress; impl<'a> Attribute<'a> for XorRelayedAddress { - type Error = StunError; + type Error = Error; type Item = SocketAddr; - const KIND: AttrKind = AttrKind::XorRelayedAddress; + const TYPE: AttributeType = AttributeType::XorRelayedAddress; - fn encode(value: Self::Item, bytes: &mut BytesMut, token: &'a [u8]) { - Addr::encode(&value, token, bytes, true) + fn serialize(value: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) { + XAddress::serialize(&value, transaction_id, bytes, true) } - fn decode(bytes: &'a [u8], token: &'a [u8]) -> Result { - Addr::decode(bytes, token, true) + fn deserialize(bytes: &'a [u8], transaction_id: &'a [u8]) -> Result { + XAddress::deserialize(bytes, transaction_id, true) } } @@ -737,20 +622,21 @@ impl<'a> Attribute<'a> for XorRelayedAddress { /// misguided attempt to provide a generic Application Layer Gateway /// (ALG) function. Such behavior interferes with the operation of STUN /// and also causes failure of STUN's message-integrity checking. +#[derive(Debug, Clone, Copy)] pub struct XorMappedAddress; impl<'a> Attribute<'a> for XorMappedAddress { - type Error = StunError; + type Error = Error; type Item = SocketAddr; - const KIND: AttrKind = AttrKind::XorMappedAddress; + const TYPE: AttributeType = AttributeType::XorMappedAddress; - fn encode(value: Self::Item, bytes: &mut BytesMut, token: &'a [u8]) { - Addr::encode(&value, token, bytes, true) + fn serialize(value: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) { + XAddress::serialize(&value, transaction_id, bytes, true) } - fn decode(bytes: &'a [u8], token: &'a [u8]) -> Result { - Addr::decode(bytes, token, true) + fn deserialize(bytes: &'a [u8], transaction_id: &'a [u8]) -> Result { + XAddress::deserialize(bytes, transaction_id, true) } } @@ -774,20 +660,21 @@ impl<'a> Attribute<'a> for XorMappedAddress { /// /// This attribute is used only by servers for achieving backwards /// compatibility with [RFC3489] clients. +#[derive(Debug, Clone, Copy)] pub struct MappedAddress; impl<'a> Attribute<'a> for MappedAddress { - type Error = StunError; + type Error = Error; type Item = SocketAddr; - const KIND: AttrKind = AttrKind::MappedAddress; + const TYPE: AttributeType = AttributeType::MappedAddress; - fn encode(value: Self::Item, bytes: &mut BytesMut, token: &'a [u8]) { - Addr::encode(&value, token, bytes, false) + fn serialize(value: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) { + XAddress::serialize(&value, transaction_id, bytes, false) } - fn decode(bytes: &'a [u8], token: &'a [u8]) -> Result { - Addr::decode(bytes, token, false) + fn deserialize(bytes: &'a [u8], transaction_id: &'a [u8]) -> Result { + XAddress::deserialize(bytes, transaction_id, false) } } @@ -795,86 +682,24 @@ impl<'a> Attribute<'a> for MappedAddress { /// the source IP address and port the response was sent from. It is /// useful for detecting double NAT configurations. It is only present /// in Binding Responses. +#[derive(Debug, Clone, Copy)] pub struct ResponseOrigin; impl<'a> Attribute<'a> for ResponseOrigin { - type Error = StunError; + type Error = Error; type Item = SocketAddr; - const KIND: AttrKind = AttrKind::ResponseOrigin; + const TYPE: AttributeType = AttributeType::ResponseOrigin; - fn encode(value: Self::Item, bytes: &mut BytesMut, token: &'a [u8]) { - Addr::encode(&value, token, bytes, false) + fn serialize(value: Self::Item, bytes: &mut B, transaction_id: &'a [u8]) { + XAddress::serialize(&value, transaction_id, bytes, false) } - fn decode(bytes: &'a [u8], token: &'a [u8]) -> Result { - Addr::decode(bytes, token, false) + fn deserialize(bytes: &'a [u8], transaction_id: &'a [u8]) -> Result { + XAddress::deserialize(bytes, transaction_id, false) } } -/// The following error codes, along with their recommended reason -/// phrases, are defined: -/// -/// 300 Try Alternate: The client should contact an alternate server for -/// this request. This error response MUST only be sent if the -/// request included either a USERNAME or USERHASH attribute and a -/// valid MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 attribute; -/// otherwise, it MUST NOT be sent and error code 400 (Bad Request) -/// is suggested. This error response MUST be protected with the -/// MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 attribute, and -/// receivers MUST validate the MESSAGE-INTEGRITY or MESSAGE- -/// INTEGRITY-SHA256 of this response before redirecting themselves -/// to an alternate server. -/// Note: Failure to generate and validate message integrity for a -/// 300 response allows an on-path attacker to falsify a 300 -/// response thus causing subsequent STUN messages to be sent to a -/// victim. -/// -/// 400 Bad Request: The request was malformed. The client SHOULD NOT -/// retry the request without modification from the previous -/// attempt. The server may not be able to generate a valid -/// MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 for this error, so -/// the client MUST NOT expect a valid MESSAGE-INTEGRITY or MESSAGE- -/// INTEGRITY-SHA256 attribute on this response. -/// -/// 401 Unauthenticated: The request did not contain the correct -/// credentials to proceed. The client should retry the request -/// with proper credentials. -/// -/// 420 Unknown Attribute: The server received a STUN packet containing -/// a comprehension-required attribute that it did not understand. -/// The server MUST put this unknown attribute in the UNKNOWN- -/// ATTRIBUTE attribute of its error response. -/// -/// 438 Stale Nonce: The NONCE used by the client was no longer valid. -/// The client should retry, using the NONCE provided in the -/// response. -/// -/// 500 Server Error: The server has suffered a temporary error. The -/// client should try again. -const fn errno(code: u16) -> u16 { - ((code / 100) << 8) | (code % 100) -} - -#[repr(u16)] -#[derive(PartialEq, Eq, Copy, Clone, Debug, Hash, TryFromPrimitive)] -pub enum ErrorKind { - TryAlternate = errno(300), - BadRequest = errno(400), - Unauthorized = errno(401), - Forbidden = errno(403), - UnknownAttribute = errno(420), - AllocationMismatch = errno(437), - StaleNonce = errno(438), - AddressFamilyNotSupported = errno(440), - WrongCredentials = errno(441), - UnsupportedTransportAddress = errno(442), - PeerAddressFamilyMismatch = errno(443), - AllocationQuotaReached = errno(486), - ServerError = errno(500), - InsufficientCapacity = errno(508), -} - /// [RFC3629]: https://datatracker.ietf.org/doc/html/rfc3629 /// [RFC7231]: https://datatracker.ietf.org/doc/html/rfc7231 /// [RFC3261]: https://datatracker.ietf.org/doc/html/rfc3261 @@ -913,23 +738,38 @@ pub enum ErrorKind { /// the hundreds digit of the error code. The value MUST be between 3 /// and 6. The Number represents the binary encoding of the error code /// modulo 100, and its value MUST be between 0 and 99. -#[derive(Clone, Debug)] -pub struct Error<'a> { +#[derive(Debug, Clone, Copy)] +pub struct ErrorCode<'a> { pub code: u16, pub message: &'a str, } -impl From for Error<'_> { +impl<'a> Attribute<'a> for ErrorCode<'a> { + type Error = Error; + type Item = Self; + + const TYPE: AttributeType = AttributeType::ErrorCode; + + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { + value.serialize(bytes); + } + + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + Self::try_from(bytes) + } +} + +impl From for ErrorCode<'_> { /// create error from error type. /// /// # Example /// /// ```no_run - /// use turn_server::stun::attribute::*; + /// use turn_server_codec::message::attributes::error::ErrorType; /// - /// Error::from(ErrorKind::TryAlternate); + /// // Error::from(ErrorType::TryAlternate); /// ``` - fn from(value: ErrorKind) -> Self { + fn from(value: ErrorType) -> Self { Self { code: value as u16, message: value.into(), @@ -937,14 +777,15 @@ impl From for Error<'_> { } } -impl Error<'_> { +impl ErrorCode<'_> { /// encode the error type as bytes. /// /// # Test /// /// ``` /// use bytes::BytesMut; - /// use turn_server::stun::attribute::*; + /// use turn_server_codec::message::attributes::error::ErrorType; + /// use turn_server_codec::Error; /// /// let buffer = [ /// 0x00u8, 0x00, 0x03, 0x00, 0x54, 0x72, 0x79, 0x20, 0x41, 0x6c, 0x74, @@ -952,42 +793,44 @@ impl Error<'_> { /// ]; /// /// let mut buf = BytesMut::with_capacity(1280); - /// let error = Error::from(ErrorKind::TryAlternate); - /// error.encode(&mut buf); + /// let error = turn_server_codec::message::attributes::ErrorCode::from(ErrorType::TryAlternate); + /// + /// error.serialize(&mut buf); /// assert_eq!(&buf[..], &buffer); /// ``` - pub fn encode(self, bytes: &mut BytesMut) { + pub fn serialize(self, bytes: &mut B) { bytes.put_u16(0x0000); bytes.put_u16(self.code); bytes.put(self.message.as_bytes()); } } -impl<'a> TryFrom<&'a [u8]> for Error<'a> { - type Error = StunError; +impl<'a> TryFrom<&'a [u8]> for ErrorCode<'a> { + type Error = Error; /// # Test /// /// ``` /// use std::convert::TryFrom; - /// use turn_server::stun::attribute::*; + /// use turn_server_codec::message::attributes::error::ErrorType; + /// use turn_server_codec::Error; /// /// let buffer = [ /// 0x00u8, 0x00, 0x03, 0x00, 0x54, 0x72, 0x79, 0x20, 0x41, 0x6c, 0x74, /// 0x65, 0x72, 0x6e, 0x61, 0x74, 0x65, /// ]; /// - /// let error = Error::try_from(&buffer[..]).unwrap(); - /// assert_eq!(error.code, ErrorKind::TryAlternate as u16); + /// let error = turn_server_codec::message::attributes::ErrorCode::try_from(&buffer[..]).unwrap(); + /// assert_eq!(error.code, ErrorType::TryAlternate as u16); /// assert_eq!(error.message, "Try Alternate"); /// ``` fn try_from(packet: &'a [u8]) -> Result { if packet.len() < 4 { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } if u16::from_be_bytes(packet[..2].try_into()?) != 0x0000 { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } Ok(Self { @@ -997,94 +840,32 @@ impl<'a> TryFrom<&'a [u8]> for Error<'a> { } } -impl From for &'static str { - /// # Test - /// - /// ``` - /// use std::convert::Into; - /// use turn_server::stun::attribute::*; - /// - /// let err: &'static str = ErrorKind::TryAlternate.into(); - /// assert_eq!(err, "Try Alternate"); - /// ``` - #[rustfmt::skip] - fn from(val: ErrorKind) -> Self { - match val { - ErrorKind::TryAlternate => "Try Alternate", - ErrorKind::BadRequest => "Bad Request", - ErrorKind::Unauthorized => "Unauthorized", - ErrorKind::Forbidden => "Forbidden", - ErrorKind::UnknownAttribute => "Unknown Attribute", - ErrorKind::AllocationMismatch => "Allocation Mismatch", - ErrorKind::StaleNonce => "Stale Nonce", - ErrorKind::AddressFamilyNotSupported => "Address Family not Supported", - ErrorKind::WrongCredentials => "Wrong Credentials", - ErrorKind::UnsupportedTransportAddress => "Unsupported Transport Address", - ErrorKind::AllocationQuotaReached => "Allocation Quota Reached", - ErrorKind::ServerError => "Server Error", - ErrorKind::InsufficientCapacity => "Insufficient Capacity", - ErrorKind::PeerAddressFamilyMismatch => "Peer Address Family Mismatch", - } - } -} - -impl Eq for Error<'_> {} -impl PartialEq for Error<'_> { +impl Eq for ErrorCode<'_> {} +impl PartialEq for ErrorCode<'_> { fn eq(&self, other: &Self) -> bool { self.code == other.code } } -/// [RFC7231]: https://datatracker.ietf.org/doc/html/rfc7231 -/// [RFC3261]: https://datatracker.ietf.org/doc/html/rfc3261 -/// [RFC3629]: https://datatracker.ietf.org/doc/html/rfc3629 -/// -/// The ERROR-CODE attribute is used in error response messages. It -/// contains a numeric error code value in the range of 300 to 699 plus a -/// textual reason phrase encoded in UTF-8 [RFC3629]; it is also -/// consistent in its code assignments and semantics with SIP [RFC3261] -/// and HTTP [RFC7231]. The reason phrase is meant for diagnostic -/// purposes and can be anything appropriate for the error code. -/// Recommended reason phrases for the defined error codes are included -/// in the IANA registry for error codes. The reason phrase MUST be a -/// UTF-8-encoded [RFC3629] sequence of fewer than 128 characters (which -/// can be as long as 509 bytes when encoding them or 763 bytes when -/// decoding them). -pub struct ErrorCode; - -impl<'a> Attribute<'a> for ErrorCode { - type Error = StunError; - type Item = Error<'a>; - - const KIND: AttrKind = AttrKind::ErrorCode; - - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { - value.encode(bytes) - } - - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { - Error::try_from(bytes) - } -} - /// The LIFETIME attribute represents the duration for which the server /// will maintain an allocation in the absence of a refresh. The value /// portion of this attribute is 4-bytes long and consists of a 32-bit /// unsigned integral value representing the number of seconds remaining /// until expiration. +#[derive(Debug, Clone, Copy)] pub struct Lifetime; impl<'a> Attribute<'a> for Lifetime { - type Error = StunError; + type Error = Error; type Item = u32; - const KIND: AttrKind = AttrKind::Lifetime; + const TYPE: AttributeType = AttributeType::Lifetime; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u32(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u32::from_be_bytes(bytes.try_into()?)) } } @@ -1100,21 +881,25 @@ impl<'a> Attribute<'a> for Lifetime { /// /// The RFFU field MUST be set to zero on transmission and MUST be /// ignored on reception. It is reserved for future uses. -pub struct ReqeestedTransport; +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)] +pub enum ReqeestedTransport { + Tcp = 0x06000000, + Udp = 0x11000000, +} impl<'a> Attribute<'a> for ReqeestedTransport { - type Error = StunError; - type Item = Transport; + type Error = Error; + type Item = Self; - const KIND: AttrKind = AttrKind::ReqeestedTransport; + const TYPE: AttributeType = AttributeType::ReqeestedTransport; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u32(value as u32) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { - let value = u32::from_be_bytes(bytes.try_into()?); - Transport::try_from(value).map_err(|_| StunError::InvalidInput) + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + Self::try_from(u32::from_be_bytes(bytes.try_into()?)).map_err(|_| Error::InvalidInput) } } @@ -1152,19 +937,20 @@ impl<'a> Attribute<'a> for ReqeestedTransport { /// integrity value before the CRC is computed, since the CRC is done /// over the value of the MESSAGE-INTEGRITY and MESSAGE-INTEGRITY-SHA256 /// attributes as well. +#[derive(Debug, Clone, Copy)] pub struct Fingerprint; impl<'a> Attribute<'a> for Fingerprint { - type Error = StunError; + type Error = Error; type Item = u32; - const KIND: AttrKind = AttrKind::Fingerprint; + const TYPE: AttributeType = AttributeType::Fingerprint; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u32(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u32::from_be_bytes(bytes.try_into()?)) } } @@ -1174,19 +960,20 @@ impl<'a> Attribute<'a> for Fingerprint { /// 16-bit unsigned integer followed by a two-octet RFFU (Reserved For /// Future Use) field, which MUST be set to 0 on transmission and MUST be /// ignored on reception. +#[derive(Debug, Clone, Copy)] pub struct ChannelNumber; impl<'a> Attribute<'a> for ChannelNumber { - type Error = StunError; + type Error = Error; type Item = u16; - const KIND: AttrKind = AttrKind::ChannelNumber; + const TYPE: AttributeType = AttributeType::ChannelNumber; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u16(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u16::from_be_bytes(bytes[..2].try_into()?)) } } @@ -1200,19 +987,20 @@ impl<'a> Attribute<'a> for ChannelNumber { /// requests, for all streams, within an ICE session, unless it has /// received a 487 response, in which case it MUST change the number. /// The agent MAY change the number when an ICE restart occurs. +#[derive(Debug, Clone, Copy)] pub struct IceControlling; impl<'a> Attribute<'a> for IceControlling { - type Error = StunError; + type Error = Error; type Item = u64; - const KIND: AttrKind = AttrKind::IceControlling; + const TYPE: AttributeType = AttributeType::IceControlling; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u64(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u64::from_be_bytes(bytes.try_into()?)) } } @@ -1220,18 +1008,19 @@ impl<'a> Attribute<'a> for IceControlling { /// The USE-CANDIDATE attribute indicates that the candidate pair /// resulting from this check will be used for transmission of data. The /// attribute has no content (the Length field of the attribute is zero); -/// it serves as a flag. It has an attribute value of 0x0025.. +/// it serves as a flag. It has an attribute value of 0x0025. +#[derive(Debug, Clone, Copy)] pub struct UseCandidate; impl<'a> Attribute<'a> for UseCandidate { - type Error = StunError; + type Error = Error; type Item = (); - const KIND: AttrKind = AttrKind::UseCandidate; + const TYPE: AttributeType = AttributeType::UseCandidate; - fn encode(_: Self::Item, _: &mut BytesMut, _: &'a [u8]) {} + fn serialize(_: Self::Item, _: &mut B, _: &'a [u8]) {} - fn decode(_: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(_: &'a [u8], _: &'a [u8]) -> Result { Ok(()) } } @@ -1245,19 +1034,20 @@ impl<'a> Attribute<'a> for UseCandidate { /// all Binding requests, for all streams, within an ICE session, unless /// it has received a 487 response, in which case it MUST change the /// number. The agent MAY change the number when an ICE restart occurs. +#[derive(Debug, Clone, Copy)] pub struct IceControlled; impl<'a> Attribute<'a> for IceControlled { - type Error = StunError; + type Error = Error; type Item = u64; - const KIND: AttrKind = AttrKind::IceControlled; + const TYPE: AttributeType = AttributeType::IceControlled; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u64(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u64::from_be_bytes(bytes.try_into()?)) } } @@ -1266,44 +1056,46 @@ impl<'a> Attribute<'a> for IceControlled { /// associated with a peer-reflexive candidate, if one will be discovered /// by this check. It is a 32-bit unsigned integer and has an attribute /// value of 0x0024. +#[derive(Debug, Clone, Copy)] pub struct Priority; impl<'a> Attribute<'a> for Priority { - type Error = StunError; + type Error = Error; type Item = u32; - const KIND: AttrKind = AttrKind::Priority; + const TYPE: AttributeType = AttributeType::Priority; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u32(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u32::from_be_bytes(bytes.try_into()?)) } } -/// The RESERVATION-TOKEN attribute contains a token that uniquely identifies a +/// The RESERVATION-TOKEN attribute contains a transaction_id that uniquely identifies a /// relayed transport address being held in reserve by the server. The server /// includes this attribute in a success response to tell the client about the -/// token, and the client includes this attribute in a subsequent Allocate +/// transaction_id, and the client includes this attribute in a subsequent Allocate /// request to request the server use that relayed transport address for the /// allocation. /// -/// The attribute value is 8 bytes and contains the token value. +/// The attribute value is 8 bytes and contains the transaction_id value. +#[derive(Debug, Clone, Copy)] pub struct ReservationToken; impl<'a> Attribute<'a> for ReservationToken { - type Error = StunError; + type Error = Error; type Item = u64; - const KIND: AttrKind = AttrKind::ReservationToken; + const TYPE: AttributeType = AttributeType::ReservationToken; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u64(value) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(u64::from_be_bytes(bytes.try_into()?)) } } @@ -1312,19 +1104,24 @@ impl<'a> Attribute<'a> for ReservationToken { /// transport address be even, and (optionally) that the server reserve the /// next-higher port number. The value portion of this attribute is 1 byte /// long. +#[derive(Debug, Clone, Copy)] pub struct EvenPort; impl<'a> Attribute<'a> for EvenPort { - type Error = StunError; + type Error = Error; type Item = bool; - const KIND: AttrKind = AttrKind::EvenPort; + const TYPE: AttributeType = AttributeType::EvenPort; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u8(if value { 0b10000000 } else { 0b00000000 }) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + if bytes.is_empty() { + return Err(Error::InvalidInput); + } + Ok(bytes[0] == 0b10000000) } } @@ -1334,20 +1131,25 @@ impl<'a> Attribute<'a> for EvenPort { /// format of the REQUESTED-ADDRESS-FAMILY attribute. Note that TURN attributes /// are TLV (Type-Length-Value) encoded, with a 16-bit type, a 16-bit length, /// and a variable-length value. +#[derive(Debug, Clone, Copy)] pub struct RequestedAddressFamily; impl<'a> Attribute<'a> for RequestedAddressFamily { - type Error = StunError; + type Error = Error; type Item = IpFamily; - const KIND: AttrKind = AttrKind::RequestedAddressFamily; + const TYPE: AttributeType = AttributeType::RequestedAddressFamily; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u8(value as u8) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { - IpFamily::try_from(bytes[0]) + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + if bytes.is_empty() { + return Err(Error::InvalidInput); + } + + IpFamily::try_from(bytes[0]).map_err(|_| Error::InvalidInput) } } @@ -1356,20 +1158,25 @@ impl<'a> Attribute<'a> for RequestedAddressFamily { /// REQUESTED-ADDRESS-FAMILY attribute; The ADDITIONAL-ADDRESS-FAMILY attribute /// MAY be present in the Allocate request. The attribute value of 0x02 (IPv6 /// address) is the only valid value in Allocate request. +#[derive(Debug, Clone, Copy)] pub struct AdditionalAddressFamily; impl<'a> Attribute<'a> for AdditionalAddressFamily { - type Error = StunError; + type Error = Error; type Item = IpFamily; - const KIND: AttrKind = AttrKind::AdditionalAddressFamily; + const TYPE: AttributeType = AttributeType::AdditionalAddressFamily; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put_u8(value as u8) } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { - IpFamily::try_from(bytes[0]) + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { + if bytes.is_empty() { + return Err(Error::InvalidInput); + } + + IpFamily::try_from(bytes[0]).map_err(|_| Error::InvalidInput) } } @@ -1378,98 +1185,26 @@ impl<'a> Attribute<'a> for AdditionalAddressFamily { /// onward to the peer and for determining the server capability in Allocate /// requests. This attribute has no value part, and thus, the attribute length /// field is 0. +#[derive(Debug, Clone, Copy)] pub struct DontFragment; impl<'a> Attribute<'a> for DontFragment { - type Error = StunError; + type Error = Error; type Item = (); - const KIND: AttrKind = AttrKind::DontFragment; + const TYPE: AttributeType = AttributeType::DontFragment; - fn decode(_: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(_: &'a [u8], _: &'a [u8]) -> Result { Ok(()) } } -/// The access token is issued by the authorization server. OAuth 2.0 does not -/// impose any limitation on the length of the access token but if path MTU is -/// unknown, then STUN messages over IPv4 would need to be less than 548 bytes -/// (Section 7.1 of [RFC5389]). The access token length needs to be restricted -/// to fit within the maximum STUN message size. Note that the self-contained -/// token is opaque to the client, and the client MUST NOT examine the token. -/// The ACCESS-TOKEN attribute is a comprehension-required attribute (see -/// Section 15 from [RFC5389]). -pub struct AccessToken<'a> { - pub nonce: &'a str, - pub mac_key: &'a str, - pub timestamp: u64, - pub lifetime: u32, -} - -impl<'a> Attribute<'a> for AccessToken<'a> { - type Error = StunError; - type Item = AccessToken<'a>; - - const KIND: AttrKind = AttrKind::AccessToken; - - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { - bytes.put_u16(value.nonce.len() as u16); - bytes.extend_from_slice(value.nonce.as_bytes()); - bytes.put_u16(value.mac_key.len() as u16); - bytes.extend_from_slice(value.mac_key.as_bytes()); - bytes.put_u64(value.timestamp); - bytes.put_u32(value.lifetime); - } - - fn decode(mut bytes: &'a [u8], _: &'a [u8]) -> Result { - // nonce_length: Length of the nonce field. The length of nonce for AEAD - // algorithms is explained in [RFC5116]. - let nonce_length = bytes.get_u16() as usize; - if nonce_length >= bytes.len() { - return Err(StunError::InvalidInput); - } - - // Nonce: Nonce (N) formation is explained in Section 3.2 of [RFC5116]. - let nonce = std::str::from_utf8(&bytes[..nonce_length])?; - - // key_length: Length of the session key in octets. The key length of 160 bits - // MUST be supported (i.e., only the 160-bit key is used by HMAC-SHA-1 for - // message integrity of STUN messages). The key length facilitates the hash - // agility plan discussed in Section 16.3 of [RFC5389]. - let key_length = bytes.get_u16() as usize; - if key_length >= bytes.len() { - return Err(StunError::InvalidInput); - } - - // mac_key: The session key generated by the authorization server. - let mac_key = std::str::from_utf8(&bytes[..key_length])?; - if bytes.len() < 12 { - return Err(StunError::InvalidInput); - } - - Ok(Self { - // timestamp: 64-bit unsigned integer field containing a timestamp. The value indicates the time since - // January 1, 1970, 00:00 UTC, by using a fixed-point format. In this format, the integer number of seconds - // is contained in the first 48 bits of the field, and the remaining 16 bits indicate the number of 1/64000 - // fractions of a second (Native format - Unix). - timestamp: bytes.get_u64(), - // lifetime: The lifetime of the access token, in seconds. For example, the value 3600 indicates one hour. - // The lifetime value MUST be greater than or equal to the 'expires_in' parameter defined in Section 4.2.2 - // of [RFC6749], otherwise the resource server could revoke the token, but the client would assume that the - // token has not expired and would not refresh the token. - lifetime: bytes.get_u32(), - mac_key, - nonce, - }) - } -} - /// This attribute is used by the STUN server to inform the client that /// it supports third-party authorization. This attribute value contains /// the STUN server name. The authorization server may have tie ups with /// multiple STUN servers and vice versa, so the client MUST provide the /// STUN server name to the authorization server so that it can select -/// the appropriate keying material to generate the self-contained token. +/// the appropriate keying material to generate the self-contained transaction_id. /// If the authorization server does not have tie up with the STUN /// server, then it returns an error to the client. If the client does /// not support or is not capable of doing third-party authorization, @@ -1479,19 +1214,20 @@ impl<'a> Attribute<'a> for AccessToken<'a> { /// comprehend THIRD-PARTY-AUTHORIZATION, it MUST ensure that third-party /// authorization takes precedence over first-party authentication (as /// explained in Section 10 of [RFC5389]). +#[derive(Debug, Clone, Copy)] pub struct ThirdPartyAuathorization; impl<'a> Attribute<'a> for ThirdPartyAuathorization { - type Error = StunError; + type Error = Error; type Item = &'a str; - const KIND: AttrKind = AttrKind::ThirdPartyAuathorization; + const TYPE: AttributeType = AttributeType::ThirdPartyAuathorization; - fn encode(value: Self::Item, bytes: &mut BytesMut, _: &'a [u8]) { + fn serialize(value: Self::Item, bytes: &mut B, _: &'a [u8]) { bytes.put(value.as_bytes()); } - fn decode(bytes: &'a [u8], _: &'a [u8]) -> Result { + fn deserialize(bytes: &'a [u8], _: &'a [u8]) -> Result { Ok(std::str::from_utf8(bytes)?) } } diff --git a/crates/codec/src/message/methods.rs b/crates/codec/src/message/methods.rs new file mode 100644 index 00000000..3da7bc7f --- /dev/null +++ b/crates/codec/src/message/methods.rs @@ -0,0 +1,191 @@ +use crate::Error; + +/// STUN Methods Registry +/// +/// [RFC5389]: https://datatracker.ietf.org/doc/html/rfc5389 +/// [RFC8489]: https://datatracker.ietf.org/doc/html/rfc8489 +/// [RFC8126]: https://datatracker.ietf.org/doc/html/rfc8126 +/// [Section 5]: https://datatracker.ietf.org/doc/html/rfc8489#section-5 +/// +/// A STUN method is a hex number in the range 0x000-0x0FF. The encoding +/// of a STUN method into a STUN message is described in [Section 5]. +/// +/// STUN methods in the range 0x000-0x07F are assigned by IETF Review +/// [RFC8126]. STUN methods in the range 0x080-0x0FF are assigned by +/// Expert Review [RFC8126]. The responsibility of the expert is to +/// verify that the selected codepoint(s) is not in use and that the +/// request is not for an abnormally large number of codepoints. +/// Technical review of the extension itself is outside the scope of the +/// designated expert responsibility. +/// +/// IANA has updated the name for method 0x002 as described below as well +/// as updated the reference from [RFC5389] to [RFC8489] for the following +/// STUN methods: +/// +/// 0x000: Reserved +/// 0x001: Binding +/// 0x002: Reserved; was SharedSecret prior to [RFC5389] +/// 0x003: Allocate +/// 0x004: Refresh +/// 0x006: Send +/// 0x007: Data +/// 0x008: CreatePermission +/// 0x009: ChannelBind +#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] +pub enum MethodType { + Request, + Response, + Error, +} + +#[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] +pub enum Method { + Binding(MethodType), + Allocate(MethodType), + CreatePermission(MethodType), + ChannelBind(MethodType), + Refresh(MethodType), + SendIndication, + DataIndication, +} + +pub const BINDING_REQUEST: Method = Method::Binding(MethodType::Request); +pub const BINDING_RESPONSE: Method = Method::Binding(MethodType::Response); +pub const BINDING_ERROR: Method = Method::Binding(MethodType::Error); +pub const ALLOCATE_REQUEST: Method = Method::Allocate(MethodType::Request); +pub const ALLOCATE_RESPONSE: Method = Method::Allocate(MethodType::Response); +pub const ALLOCATE_ERROR: Method = Method::Allocate(MethodType::Error); +pub const CREATE_PERMISSION_REQUEST: Method = Method::CreatePermission(MethodType::Request); +pub const CREATE_PERMISSION_RESPONSE: Method = Method::CreatePermission(MethodType::Response); +pub const CREATE_PERMISSION_ERROR: Method = Method::CreatePermission(MethodType::Error); +pub const CHANNEL_BIND_REQUEST: Method = Method::ChannelBind(MethodType::Request); +pub const CHANNEL_BIND_RESPONSE: Method = Method::ChannelBind(MethodType::Response); +pub const CHANNEL_BIND_ERROR: Method = Method::ChannelBind(MethodType::Error); +pub const REFRESH_REQUEST: Method = Method::Refresh(MethodType::Request); +pub const REFRESH_RESPONSE: Method = Method::Refresh(MethodType::Response); +pub const REFRESH_ERROR: Method = Method::Refresh(MethodType::Error); +pub const SEND_INDICATION: Method = Method::SendIndication; +pub const DATA_INDICATION: Method = Method::DataIndication; + +impl Method { + pub fn is_error(&self) -> bool { + matches!( + self, + Method::Binding(MethodType::Error) + | Method::Refresh(MethodType::Error) + | Method::Allocate(MethodType::Error) + | Method::CreatePermission(MethodType::Error) + | Method::ChannelBind(MethodType::Error) + ) + } + + pub fn error(&self) -> Option { + match self { + Method::Binding(_) => Some(BINDING_ERROR), + Method::Allocate(_) => Some(ALLOCATE_ERROR), + Method::CreatePermission(_) => Some(CREATE_PERMISSION_ERROR), + Method::ChannelBind(_) => Some(CHANNEL_BIND_ERROR), + Method::Refresh(_) => Some(REFRESH_ERROR), + _ => None, + } + } +} + +impl TryFrom for Method { + type Error = Error; + + /// # Test + /// + /// ``` + /// use turn_server_codec::message::methods::*; + /// use std::convert::TryFrom; + /// + /// assert_eq!(Method::try_from(0x0001).unwrap(), BINDING_REQUEST); + /// assert_eq!(Method::try_from(0x0101).unwrap(), BINDING_RESPONSE); + /// assert_eq!(Method::try_from(0x0111).unwrap(), BINDING_ERROR); + /// assert_eq!(Method::try_from(0x0003).unwrap(), ALLOCATE_REQUEST); + /// assert_eq!(Method::try_from(0x0103).unwrap(), ALLOCATE_RESPONSE); + /// assert_eq!(Method::try_from(0x0113).unwrap(), ALLOCATE_ERROR); + /// assert_eq!(Method::try_from(0x0008).unwrap(), CREATE_PERMISSION_REQUEST); + /// assert_eq!(Method::try_from(0x0108).unwrap(), CREATE_PERMISSION_RESPONSE); + /// assert_eq!(Method::try_from(0x0118).unwrap(), CREATE_PERMISSION_ERROR); + /// assert_eq!(Method::try_from(0x0009).unwrap(), CHANNEL_BIND_REQUEST); + /// assert_eq!(Method::try_from(0x0109).unwrap(), CHANNEL_BIND_RESPONSE); + /// assert_eq!(Method::try_from(0x0119).unwrap(), CHANNEL_BIND_ERROR); + /// assert_eq!(Method::try_from(0x0004).unwrap(), REFRESH_REQUEST); + /// assert_eq!(Method::try_from(0x0104).unwrap(), REFRESH_RESPONSE); + /// assert_eq!(Method::try_from(0x0114).unwrap(), REFRESH_ERROR); + /// assert_eq!(Method::try_from(0x0016).unwrap(), SEND_INDICATION); + /// assert_eq!(Method::try_from(0x0017).unwrap(), DATA_INDICATION); + /// ``` + fn try_from(value: u16) -> Result { + Ok(match value { + 0x0001 => Self::Binding(MethodType::Request), + 0x0101 => Self::Binding(MethodType::Response), + 0x0111 => Self::Binding(MethodType::Error), + 0x0003 => Self::Allocate(MethodType::Request), + 0x0103 => Self::Allocate(MethodType::Response), + 0x0113 => Self::Allocate(MethodType::Error), + 0x0008 => Self::CreatePermission(MethodType::Request), + 0x0108 => Self::CreatePermission(MethodType::Response), + 0x0118 => Self::CreatePermission(MethodType::Error), + 0x0009 => Self::ChannelBind(MethodType::Request), + 0x0109 => Self::ChannelBind(MethodType::Response), + 0x0119 => Self::ChannelBind(MethodType::Error), + 0x0004 => Self::Refresh(MethodType::Request), + 0x0104 => Self::Refresh(MethodType::Response), + 0x0114 => Self::Refresh(MethodType::Error), + 0x0016 => Self::SendIndication, + 0x0017 => Self::DataIndication, + _ => return Err(Error::UnknownMethod), + }) + } +} + +impl From for u16 { + /// # Test + /// + /// ``` + /// use turn_server_codec::message::methods::*; + /// use std::convert::From; + /// + /// assert_eq!(0x0001u16, u16::from(BINDING_REQUEST)); + /// assert_eq!(0x0101u16, u16::from(BINDING_RESPONSE)); + /// assert_eq!(0x0111u16, u16::from(BINDING_ERROR)); + /// assert_eq!(0x0003u16, u16::from(ALLOCATE_REQUEST)); + /// assert_eq!(0x0103u16, u16::from(ALLOCATE_RESPONSE)); + /// assert_eq!(0x0113u16, u16::from(ALLOCATE_ERROR)); + /// assert_eq!(0x0008u16, u16::from(CREATE_PERMISSION_REQUEST)); + /// assert_eq!(0x0108u16, u16::from(CREATE_PERMISSION_RESPONSE)); + /// assert_eq!(0x0118u16, u16::from(CREATE_PERMISSION_ERROR)); + /// assert_eq!(0x0009u16, u16::from(CHANNEL_BIND_REQUEST)); + /// assert_eq!(0x0109u16, u16::from(CHANNEL_BIND_RESPONSE)); + /// assert_eq!(0x0119u16, u16::from(CHANNEL_BIND_ERROR)); + /// assert_eq!(0x0004u16, u16::from(REFRESH_REQUEST)); + /// assert_eq!(0x0104u16, u16::from(REFRESH_RESPONSE)); + /// assert_eq!(0x0114u16, u16::from(REFRESH_ERROR)); + /// assert_eq!(0x0016u16, u16::from(SEND_INDICATION)); + /// assert_eq!(0x0017u16, u16::from(DATA_INDICATION)); + /// ``` + fn from(val: Method) -> Self { + match val { + Method::Binding(MethodType::Request) => 0x0001, + Method::Binding(MethodType::Response) => 0x0101, + Method::Binding(MethodType::Error) => 0x0111, + Method::Allocate(MethodType::Request) => 0x0003, + Method::Allocate(MethodType::Response) => 0x0103, + Method::Allocate(MethodType::Error) => 0x0113, + Method::CreatePermission(MethodType::Request) => 0x0008, + Method::CreatePermission(MethodType::Response) => 0x0108, + Method::CreatePermission(MethodType::Error) => 0x0118, + Method::ChannelBind(MethodType::Request) => 0x0009, + Method::ChannelBind(MethodType::Response) => 0x0109, + Method::ChannelBind(MethodType::Error) => 0x0119, + Method::Refresh(MethodType::Request) => 0x0004, + Method::Refresh(MethodType::Response) => 0x0104, + Method::Refresh(MethodType::Error) => 0x0114, + Method::SendIndication => 0x0016, + Method::DataIndication => 0x0017, + } + } +} diff --git a/src/stun/message.rs b/crates/codec/src/message/mod.rs similarity index 53% rename from src/stun/message.rs rename to crates/codec/src/message/mod.rs index bf3c3a98..e4fdbd56 100644 --- a/src/stun/message.rs +++ b/crates/codec/src/message/mod.rs @@ -1,33 +1,36 @@ -use bytes::{BufMut, BytesMut}; - -use std::convert::TryFrom; - -use super::{ - Attributes, StunError, - attribute::{AttrKind, Attribute, MessageIntegrity}, - method::StunMethod, - util, +pub mod attributes; +pub mod methods; + +use crate::{ + Attributes, Error, + crypto::{Password, fingerprint, hmac_sha1}, + message::{ + attributes::{Attribute, AttributeType, MessageIntegrity, MessageIntegritySha256}, + methods::Method, + }, }; -const ZOER_BUF: [u8; 10] = [0u8; 10]; -const COOKIE: [u8; 4] = 0x2112A442u32.to_be_bytes(); +use bytes::{BufMut, BytesMut}; -/// (username, password, realm) -type Digest = [u8; 16]; +static MAGIC_NUMBER: u32 = 0x2112A442; pub struct MessageEncoder<'a> { - pub token: &'a [u8], - pub bytes: &'a mut BytesMut, + transaction_id: &'a [u8], + bytes: &'a mut BytesMut, } -impl<'a, 'b> MessageEncoder<'a> { - pub fn new(method: StunMethod, token: &'a [u8; 12], bytes: &'a mut BytesMut) -> Self { - unsafe { bytes.set_len(0) } +impl<'a> MessageEncoder<'a> { + pub fn new(method: Method, transaction_id: &'a [u8; 12], bytes: &'a mut BytesMut) -> Self { + bytes.clear(); bytes.put_u16(method.into()); bytes.put_u16(0); - bytes.put(&COOKIE[..]); - bytes.put(token.as_slice()); - Self { bytes, token } + bytes.put_u32(MAGIC_NUMBER); + bytes.put(transaction_id.as_slice()); + + Self { + bytes, + transaction_id, + } } /// rely on old message to create new message. @@ -37,10 +40,9 @@ impl<'a, 'b> MessageEncoder<'a> { /// ``` /// use bytes::BytesMut; /// use std::convert::TryFrom; - /// use turn_server::stun::method::{ - /// StunMethod as Method, StunMethodKind as Kind, - /// }; - /// use turn_server::stun::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, @@ -49,19 +51,23 @@ impl<'a, 'b> MessageEncoder<'a> { /// /// let mut attributes = Attributes::default(); /// let mut buf = BytesMut::new(); - /// let old = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); - /// MessageEncoder::extend(Method::Binding(Kind::Request), &old, &mut buf); + /// let old = Message::decode(&buffer[..], &mut attributes).unwrap(); + /// MessageEncoder::extend(Method::Binding(MethodType::Request), &old, &mut buf); + /// /// assert_eq!(&buf[..], &buffer[..]); /// ``` - pub fn extend(method: StunMethod, reader: &MessageRef<'a>, bytes: &'a mut BytesMut) -> Self { - let token = reader.token(); + pub fn extend(method: Method, reader: &Message<'a>, bytes: &'a mut BytesMut) -> Self { + let transaction_id = reader.transaction_id(); - unsafe { bytes.set_len(0) } + bytes.clear(); bytes.put_u16(method.into()); bytes.put_u16(0); - bytes.put(&COOKIE[..]); - bytes.put(token); - Self { bytes, token } + bytes.put_u32(MAGIC_NUMBER); + bytes.put(transaction_id); + Self { + bytes, + transaction_id, + } } /// append attribute. @@ -73,11 +79,10 @@ impl<'a, 'b> MessageEncoder<'a> { /// ``` /// use bytes::BytesMut; /// use std::convert::TryFrom; - /// use turn_server::stun::attribute::UserName; - /// use turn_server::stun::method::{ - /// StunMethod as Method, StunMethodKind as Kind, - /// }; - /// use turn_server::stun::*; + /// use turn_server_codec::message::attributes::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, @@ -92,22 +97,23 @@ impl<'a, 'b> MessageEncoder<'a> { /// /// let mut buf = BytesMut::new(); /// let mut attributes = Attributes::default(); - /// let old = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); + /// let old = Message::decode(&buffer[..], &mut attributes).unwrap(); /// let mut message = - /// MessageEncoder::extend(Method::Binding(Kind::Request), &old, &mut buf); + /// MessageEncoder::extend(Method::Binding(MethodType::Request), &old, &mut buf); /// /// message.append::("panda"); + /// /// assert_eq!(&new_buf[..], &buf[..]); /// ``` pub fn append<'c, T: Attribute<'c>>(&'c mut self, value: T::Item) { - self.bytes.put_u16(T::KIND as u16); + self.bytes.put_u16(T::TYPE as u16); // record the current position, // and then advance the internal cursor 2 bytes, // here is to reserve the position. let os = self.bytes.len(); unsafe { self.bytes.advance_mut(2) } - T::encode(value, self.bytes, self.token); + T::serialize(value, self.bytes, self.transaction_id); // compute write index, // back to source index write size. @@ -118,9 +124,9 @@ impl<'a, 'b> MessageEncoder<'a> { // if you need to padding, // padding in the zero bytes. - let psize = util::pad_size(size); + let psize = alignment_32(size); if psize > 0 { - self.bytes.put(&ZOER_BUF[0..psize]); + self.bytes.put(&[0u8; 10][0..psize]); } } @@ -131,10 +137,9 @@ impl<'a, 'b> MessageEncoder<'a> { /// ``` /// use bytes::BytesMut; /// use std::convert::TryFrom; - /// use turn_server::stun::method::{ - /// StunMethod as Method, StunMethodKind as Kind, - /// }; - /// use turn_server::stun::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, @@ -150,26 +155,28 @@ impl<'a, 'b> MessageEncoder<'a> { /// /// let mut attributes = Attributes::default(); /// let mut buf = BytesMut::with_capacity(1280); - /// let old = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); + /// let old = Message::decode(&buffer[..], &mut attributes).unwrap(); /// let mut message = - /// MessageEncoder::extend(Method::Binding(Kind::Request), &old, &mut buf); + /// MessageEncoder::extend(Method::Binding(MethodType::Request), &old, &mut buf); /// /// message - /// .flush(Some(&util::long_term_credential_digest( + /// .flush(Some(&turn_server_codec::crypto::generate_password( /// "panda", /// "panda", /// "raspberry", + /// turn_server_codec::message::attributes::PasswordAlgorithm::Md5, /// ))) /// .unwrap(); + /// /// assert_eq!(&buf[..], &result); /// ``` - pub fn flush(&mut self, digest: Option<&Digest>) -> Result<(), StunError> { + pub fn flush(&mut self, password: Option<&Password>) -> Result<(), Error> { // write attribute list size. self.set_len(self.bytes.len() - 20); // if need message integrity? - if let Some(a) = digest { - self.integrity(a)?; + if let Some(it) = password { + self.verify(it)?; } Ok(()) @@ -185,10 +192,9 @@ impl<'a, 'b> MessageEncoder<'a> { /// ``` /// use bytes::BytesMut; /// use std::convert::TryFrom; - /// use turn_server::stun::method::{ - /// StunMethod as Method, StunMethodKind as Kind, - /// }; - /// use turn_server::stun::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, @@ -204,20 +210,22 @@ impl<'a, 'b> MessageEncoder<'a> { /// /// let mut attributes = Attributes::default(); /// let mut buf = BytesMut::from(&buffer[..]); - /// let old = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); + /// let old = Message::decode(&buffer[..], &mut attributes).unwrap(); /// let mut message = - /// MessageEncoder::extend(Method::Binding(Kind::Request), &old, &mut buf); + /// MessageEncoder::extend(Method::Binding(MethodType::Request), &old, &mut buf); /// /// message - /// .flush(Some(&util::long_term_credential_digest( + /// .flush(Some(&turn_server_codec::crypto::generate_password( /// "panda", /// "panda", /// "raspberry", + /// turn_server_codec::message::attributes::PasswordAlgorithm::Md5, /// ))) /// .unwrap(); + /// /// assert_eq!(&buf[..], &result); /// ``` - fn integrity(&mut self, digest: &Digest) -> Result<(), StunError> { + fn verify(&mut self, passwrd: &Password) -> Result<(), Error> { assert!(self.bytes.len() >= 20); let len = self.bytes.len(); @@ -226,18 +234,24 @@ impl<'a, 'b> MessageEncoder<'a> { self.set_len(len + 4); // write MessageIntegrity attribute. - let hmac_output = util::hmac_sha1(digest, &[self.bytes])?.into_bytes(); - self.bytes.put_u16(AttrKind::MessageIntegrity as u16); - self.bytes.put_u16(20); - self.bytes.put(hmac_output.as_slice()); + { + let hmac = hmac_sha1(passwrd, &[self.bytes]); + self.bytes.put_u16(match passwrd { + Password::Md5(_) => AttributeType::MessageIntegrity as u16, + Password::Sha256(_) => AttributeType::MessageIntegritySha256 as u16, + }); + + self.bytes.put_u16(20); + self.bytes.put(hmac.as_slice()); + } // compute new size, // new size include the Fingerprint attribute size. self.set_len(len + 4 + 8); // CRC Fingerprint - let fingerprint = util::fingerprint(self.bytes); - self.bytes.put_u16(AttrKind::Fingerprint as u16); + let fingerprint = fingerprint(self.bytes); + self.bytes.put_u16(AttributeType::Fingerprint as u16); self.bytes.put_u16(4); self.bytes.put_u32(fingerprint); @@ -250,140 +264,9 @@ impl<'a, 'b> MessageEncoder<'a> { } } -pub struct MessageDecoder; - -impl MessageDecoder { - /// # Test - /// - /// ``` - /// use std::convert::TryFrom; - /// use turn_server::stun::attribute::*; - /// use turn_server::stun::method::*; - /// use turn_server::stun::*; - /// - /// let buffer: [u8; 20] = [ - /// 0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, 0x42, - /// 0x72, 0x52, 0x64, 0x48, 0x57, 0x62, 0x4b, 0x2b, - /// ]; - /// - /// let mut attributes = Attributes::default(); - /// let message = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); - /// assert_eq!( - /// message.method(), - /// StunMethod::Binding(StunMethodKind::Request) - /// ); - /// assert!(message.get::().is_none()); - /// ``` - pub fn decode<'a>(bytes: &'a [u8], attributes: &'a mut Attributes) -> Result, StunError> { - if bytes.len() < 20 { - return Err(StunError::InvalidInput); - } - - let count_size = bytes.len(); - let mut find_integrity = false; - let mut payload_size = 0; - - // message type - // message size - // check fixed magic cookie - // check if the message size is overflow - let method = StunMethod::try_from(u16::from_be_bytes(bytes[..2].try_into()?))?; - let size = u16::from_be_bytes(bytes[2..4].try_into()?) as usize + 20; - if bytes[4..8] != COOKIE[..] { - return Err(StunError::NotFoundCookie); - } - - if count_size < size { - return Err(StunError::InvalidInput); - } - - let mut offset = 20; - loop { - // if the buf length is not long enough to continue, - // jump out of the loop. - if count_size - offset < 4 { - break; - } - - // get attribute type - let key = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]); - - // whether the MessageIntegrity attribute has been found, - // if found, record the current offset position. - if !find_integrity { - payload_size = offset as u16; - } - - // check whether the current attribute is MessageIntegrity, - // if it is, mark this attribute has been found. - if key == AttrKind::MessageIntegrity as u16 { - find_integrity = true; - } - - // get attribute size - let size = u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]]) as usize; - - // check if the attribute length has overflowed. - offset += 4; - if count_size - offset < size { - break; - } - - // body range. - let range = offset..(offset + size); - - // if there are padding bytes, - // skip padding size. - if size > 0 { - offset += size; - offset += util::pad_size(size); - } - - // skip the attributes that are not supported. - let attrkind = match AttrKind::try_from(key) { - Err(_) => continue, - Ok(a) => a, - }; - - // get attribute body - // insert attribute to attributes list. - attributes.append(attrkind, range); - } - - Ok(MessageRef { - size: payload_size, - attributes, - method, - bytes, - }) - } - - /// # Test - /// - /// ``` - /// use turn_server::stun::*; - /// - /// let buffer: [u8; 20] = [ - /// 0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, 0x42, - /// 0x72, 0x52, 0x64, 0x48, 0x57, 0x62, 0x4b, 0x2b, - /// ]; - /// - /// let size = MessageDecoder::message_size(&buffer[..]).unwrap(); - /// assert_eq!(size, 20); - /// ``` - pub fn message_size(buf: &[u8]) -> Result { - if buf[0] >> 6 != 0 || buf.len() < 20 { - return Err(StunError::InvalidInput); - } - - Ok((u16::from_be_bytes(buf[2..4].try_into()?) + 20) as usize) - } -} - -#[derive(Debug)] -pub struct MessageRef<'a> { +pub struct Message<'a> { /// message method. - method: StunMethod, + method: Method, /// message source bytes. bytes: &'a [u8], /// message payload size. @@ -392,16 +275,16 @@ pub struct MessageRef<'a> { attributes: &'a Attributes, } -impl<'a> MessageRef<'a> { +impl<'a> Message<'a> { /// message method. #[inline] - pub fn method(&self) -> StunMethod { + pub fn method(&self) -> Method { self.method } /// message transaction id. #[inline] - pub fn token(&self) -> &'a [u8] { + pub fn transaction_id(&self) -> &'a [u8] { &self.bytes[8..20] } @@ -413,8 +296,10 @@ impl<'a> MessageRef<'a> { /// /// ``` /// use std::convert::TryFrom; - /// use turn_server::stun::attribute::*; - /// use turn_server::stun::*; + /// use turn_server_codec::message::attributes::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, @@ -422,12 +307,41 @@ impl<'a> MessageRef<'a> { /// ]; /// /// let mut attributes = Attributes::default(); - /// let message = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); + /// let message = Message::decode(&buffer[..], &mut attributes).unwrap(); + /// /// assert!(message.get::().is_none()); /// ``` pub fn get>(&self) -> Option { - let range = self.attributes.get(&T::KIND)?; - T::decode(&self.bytes[range.clone()], self.token()).ok() + let range = self.attributes.get(&T::TYPE)?; + T::deserialize(&self.bytes[range], self.transaction_id()).ok() + } + + /// get attribute for type. + /// + /// get attribute from message attribute list. + /// + /// # Test + /// + /// ``` + /// use std::convert::TryFrom; + /// use turn_server_codec::message::attributes::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; + /// + /// let buffer = [ + /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, + /// 0x42, 0x72, 0x52, 0x64, 0x48, 0x57, 0x62, 0x4b, 0x2b, + /// ]; + /// + /// let mut attributes = Attributes::default(); + /// let message = Message::decode(&buffer[..], &mut attributes).unwrap(); + /// + /// assert!(message.get_for_type(AttributeType::UserName).is_none()); + /// ``` + pub fn get_for_type(&self, attr_type: AttributeType) -> Option<&'a [u8]> { + let range = self.attributes.get(&attr_type)?; + Some(&self.bytes[range]) } /// Gets all the values of an attribute from a list. @@ -439,8 +353,10 @@ impl<'a> MessageRef<'a> { /// /// ``` /// use std::convert::TryFrom; - /// use turn_server::stun::attribute::*; - /// use turn_server::stun::*; + /// use turn_server_codec::message::attributes::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, @@ -448,28 +364,31 @@ impl<'a> MessageRef<'a> { /// ]; /// /// let mut attributes = Attributes::default(); - /// let message = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); + /// let message = Message::decode(&buffer[..], &mut attributes).unwrap(); /// /// assert_eq!(message.get_all::().next(), None); /// ``` pub fn get_all>(&self) -> impl Iterator { self.attributes - .get_all(&T::KIND) - .map(|it| T::decode(&self.bytes[it.clone()], self.token())) + .get_all(&T::TYPE) + .map(|it| T::deserialize(&self.bytes[it.clone()], self.transaction_id())) .filter(|it| it.is_ok()) - .map(|it| it.unwrap()) + .flatten() } /// check MessageRefIntegrity attribute. /// - /// return whether the `MessageRefIntegrity` attribute - /// contained in the message can pass the check. + /// return whether the `MessageRefIntegrity` attribute contained in the message + /// can pass the check. + /// /// /// # Test /// /// ``` /// use std::convert::TryFrom; - /// use turn_server::stun::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; /// /// let buffer = [ /// 0x00u8, 0x03, 0x00, 0x50, 0x21, 0x12, 0xa4, 0x42, 0x64, 0x4f, 0x5a, @@ -484,38 +403,205 @@ impl<'a> MessageRef<'a> { /// ]; /// /// let mut attributes = Attributes::default(); - /// let message = MessageDecoder::decode(&buffer[..], &mut attributes).unwrap(); + /// let message = Message::decode(&buffer[..], &mut attributes).unwrap(); /// let result = message - /// .integrity(&util::long_term_credential_digest( + /// .verify(&turn_server_codec::crypto::generate_password( /// "panda", /// "panda", /// "raspberry", + /// turn_server_codec::message::attributes::PasswordAlgorithm::Md5, /// )) /// .is_ok(); + /// /// assert!(result); /// ``` - pub fn integrity(&self, digest: &Digest) -> Result<(), StunError> { + pub fn verify(&self, password: &Password) -> Result<(), Error> { if self.bytes.is_empty() || self.size < 20 { - return Err(StunError::InvalidInput); + return Err(Error::InvalidInput); } // unwrap MessageIntegrity attribute, // an error occurs if not found. - let integrity = self.get::().ok_or(StunError::NotFoundIntegrity)?; + let integrity = match password { + Password::Md5(_) => self.get::(), + Password::Sha256(_) => self.get::(), + } + .ok_or(Error::NotFoundIntegrity)?; // create multiple submit. let size_buf = (self.size + 4).to_be_bytes(); - let body = [&self.bytes[0..2], &size_buf, &self.bytes[4..self.size as usize]]; + let body = [ + &self.bytes[0..2], + &size_buf, + &self.bytes[4..self.size as usize], + ]; // digest the message buffer. - let hmac_output = util::hmac_sha1(digest, &body)?.into_bytes(); - let hmac_buf = hmac_output.as_slice(); - - // Compare local and original attribute. - if integrity != hmac_buf { - return Err(StunError::IntegrityFailed); + { + // Compare local and original attribute. + if integrity != hmac_sha1(password, &body).as_slice() { + return Err(Error::IntegrityFailed); + } } Ok(()) } + + /// # Test + /// + /// ``` + /// use std::convert::TryFrom; + /// use turn_server_codec::message::attributes::*; + /// use turn_server_codec::message::methods::*; + /// use turn_server_codec::message::*; + /// use turn_server_codec::*; + /// + /// let buffer: [u8; 20] = [ + /// 0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, 0x42, + /// 0x72, 0x52, 0x64, 0x48, 0x57, 0x62, 0x4b, 0x2b, + /// ]; + /// + /// let mut attributes = Attributes::default(); + /// let message = Message::decode(&buffer[..], &mut attributes).unwrap(); + /// + /// assert_eq!( + /// message.method(), + /// Method::Binding(MethodType::Request) + /// ); + /// + /// assert!(message.get::().is_none()); + /// ``` + pub fn decode(bytes: &'a [u8], attributes: &'a mut Attributes) -> Result { + let len = bytes.len(); + + // There must be at least a complete header. + if len < 20 { + return Err(Error::InvalidInput); + } + + let method = Method::try_from(u16::from_be_bytes(bytes[..2].try_into()?))?; + + // First check whether the message length is valid. Here, the length needs + // to add the 20 bytes of the header, because the length field here does + // not include the header length. + { + let size = u16::from_be_bytes(bytes[2..4].try_into()?) as usize + 20; + if len < size { + return Err(Error::InvalidInput); + } + } + + // Check whether the magic number is the same. + if bytes[4..8] != MAGIC_NUMBER.to_be_bytes() { + return Err(Error::NotFoundMagicNumber); + } + + let mut find_integrity = false; + let mut content_len = 0; + let mut offset = 20; + + loop { + // if the buf length is not long enough to continue, + // jump out of the loop. + if len - offset < 4 { + break; + } + + // get attribute type + let key = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]); + + // whether the MessageIntegrity attribute has been found, + // if found, record the current offset position. + if !find_integrity { + content_len = offset as u16; + } + + // get attribute size + let size = u16::from_be_bytes([bytes[offset + 2], bytes[offset + 3]]) as usize; + + // check if the attribute length has overflowed. + offset += 4; + if len - offset < size { + break; + } + + // body range. + let range = offset..(offset + size); + + // if there are padding bytes, + // skip padding size. + if size > 0 { + offset += size + alignment_32(size); + } + + // skip the attributes that are not supported. + let attrkind = if let Ok(kind) = AttributeType::try_from(key) { + // check whether the current attribute is MessageIntegrity, + // if it is, mark this attribute has been found. + if kind == AttributeType::MessageIntegrity { + find_integrity = true; + } + + kind + } else { + continue; + }; + + // get attribute body + // insert attribute to attributes list. + attributes.append(attrkind, range); + } + + Ok(Self { + size: content_len, + attributes, + method, + bytes, + }) + } + + /// # Test + /// + /// ``` + /// use turn_server_codec::message::*; + /// + /// let buffer: [u8; 20] = [ + /// 0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42, 0x72, 0x6d, 0x49, 0x42, + /// 0x72, 0x52, 0x64, 0x48, 0x57, 0x62, 0x4b, 0x2b, + /// ]; + /// + /// let size = Message::message_size(&buffer[..]).unwrap(); + /// + /// assert_eq!(size, 20); + /// ``` + pub fn message_size(buffer: &[u8]) -> Result { + if buffer[0] >> 6 != 0 || buffer.len() < 20 { + return Err(Error::InvalidInput); + } + + Ok((u16::from_be_bytes(buffer[2..4].try_into()?) + 20) as usize) + } +} + +/// compute padding size. +/// +/// RFC5766 stipulates that the attribute content is a multiple of 4. +/// +/// # Test +/// +/// ``` +/// use turn_server_codec::message::alignment_32; +/// +/// assert_eq!(alignment_32(4), 0); +/// assert_eq!(alignment_32(0), 0); +/// assert_eq!(alignment_32(5), 3); +/// ``` +#[inline(always)] +pub fn alignment_32(size: usize) -> usize { + let range = size % 4; + if size == 0 || range == 0 { + return 0; + } + + 4 - range } diff --git a/crates/codec/tests/stun.rs b/crates/codec/tests/stun.rs new file mode 100644 index 00000000..1194b7c2 --- /dev/null +++ b/crates/codec/tests/stun.rs @@ -0,0 +1,195 @@ +use anyhow::Result; +use turn_server_codec::{ + DecodeResult, Decoder, + crypto::generate_password, + message::{ + attributes::{error::ErrorType, *}, + methods::*, + }, +}; + +#[test] +#[rustfmt::skip] +fn test_turn_server_codec() -> Result<()> { + let mut decoder = Decoder::default(); + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/BindingRequest.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), BINDING_REQUEST); + assert_eq!(message.transaction_id(), &[0x45, 0x58, 0x65, 0x61, 0x57, 0x53, 0x5a, 0x6e, 0x57, 0x35, 0x76, 0x46]); + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/BindingResponse.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), BINDING_RESPONSE); + assert_eq!(message.get::(), Some("127.0.0.1:51678".parse()?)); + assert_eq!(message.get::(), Some("127.0.0.1:51678".parse()?)); + assert_eq!(message.get::(), Some("127.0.0.1:3478".parse()?)); + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/UnauthorizedAllocateRequest.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), ALLOCATE_REQUEST); + assert_eq!(message.get::(), Some(ReqeestedTransport::Udp)); + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/UnauthorizedAllocateResponse.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), ALLOCATE_ERROR); + assert_eq!(message.get::(), Some(ErrorCode::from(ErrorType::Unauthorized))); + assert_eq!(message.get::(), Some("localhost")); + assert_eq!(message.get::(), Some("UHm1hiE0jm9r9rGS")); + assert_eq!(message.get::(), Some(vec![PasswordAlgorithm::Md5, PasswordAlgorithm::Sha256])); + } + + let password = generate_password("user1", "test", "localhost", PasswordAlgorithm::Md5); + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/AllocateRequest.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), ALLOCATE_REQUEST); + assert_eq!(message.get::(), Some(ReqeestedTransport::Udp)); + assert_eq!(message.get::(), Some("user1")); + assert_eq!(message.get::(), Some("localhost")); + assert_eq!(message.get::(), Some("UHm1hiE0jm9r9rGS")); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/AllocateResponse.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), ALLOCATE_RESPONSE); + assert_eq!(message.get::(), Some("127.0.0.1:55616".parse()?)); + assert_eq!(message.get::(), Some("127.0.0.1:51678".parse()?)); + assert_eq!(message.get::(), Some(600)); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/CreatePermissionRequest.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), CREATE_PERMISSION_REQUEST); + assert_eq!(message.get::(), Some("127.0.0.1:55616".parse()?)); + assert_eq!(message.get::(), Some("user1")); + assert_eq!(message.get::(), Some("localhost")); + assert_eq!(message.get::(), Some("9jLBcjff3xrKRAES")); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/CreatePermissionResponse.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), CREATE_PERMISSION_RESPONSE); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/ChannelBindRequest.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), CHANNEL_BIND_REQUEST); + assert_eq!(message.get::(), Some(0x4000)); + assert_eq!(message.get::(), Some("127.0.0.1:55616".parse()?)); + assert_eq!(message.get::(), Some("user1")); + assert_eq!(message.get::(), Some("localhost")); + assert_eq!(message.get::(), Some("9jLBcjff3xrKRAES")); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/ChannelBindResponse.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), CHANNEL_BIND_RESPONSE); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/DataIndication.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), DATA_INDICATION); + assert_eq!(message.get::(), Some("127.0.0.1:55616".parse()?)); + assert_eq!(message.get::().map(|it| it.len()), Some(100)); + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/SendIndication.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), SEND_INDICATION); + assert_eq!(message.get::(), Some("127.0.0.1:55616".parse()?)); + assert_eq!(message.get::().map(|it| it.len()), Some(96)); + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/RefreshRequest.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), REFRESH_REQUEST); + assert_eq!(message.get::(), Some(0)); + assert_eq!(message.get::(), Some("user1")); + assert_eq!(message.get::(), Some("localhost")); + assert_eq!(message.get::(), Some("UHm1hiE0jm9r9rGS")); + + message.verify(&password)?; + } + + { + let message = decoder.decode(include_bytes!("../../../tests/samples/RefreshResponse.bin"))?; + let DecodeResult::Message(message) = message else { + return Err(anyhow::anyhow!("Expected Message")); + }; + + assert_eq!(message.method(), REFRESH_RESPONSE); + assert_eq!(message.get::(), Some(0)); + + message.verify(&password)?; + } + + Ok(()) +} diff --git a/crates/service/Cargo.toml b/crates/service/Cargo.toml new file mode 100644 index 00000000..5fcddb66 --- /dev/null +++ b/crates/service/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "turn-server-service" +version = "0.1.0" +edition = "2024" + +[dependencies] +ahash = "0.8" +bytes = "1" +parking_lot = "0.12" +rand = "0.9" +codec = { workspace = true } +serde = { version = "1", optional = true } + +[dev-dependencies] +pollster = "0.4.0" +anyhow = "1.0" + +[features] +serde = ["dep:serde"] diff --git a/src/turn/mod.rs b/crates/service/src/lib.rs similarity index 72% rename from src/turn/mod.rs rename to crates/service/src/lib.rs index 68a31e77..b2a2792f 100644 --- a/src/turn/mod.rs +++ b/crates/service/src/lib.rs @@ -1,20 +1,21 @@ -pub mod operations; -pub mod sessions; +pub mod routing; +pub mod session; -use self::operations::ServiceContext; +use std::{net::SocketAddr, sync::Arc}; -pub use self::{ - operations::{Operationer, ResponseMethod}, - sessions::{PortAllocatePools, Session, SessionAddr, Sessions}, -}; +use codec::{crypto::Password, message::attributes::PasswordAlgorithm}; -use std::{net::SocketAddr, sync::Arc}; +use crate::{ + routing::Router, + session::{Identifier, SessionManager, SessionManagerOptions, ports::PortRange}, +}; -#[allow(unused)] -pub trait Observer: Send + Sync { - fn get_password(&self, username: &str) -> Option { - None - } +pub trait ServiceHandler: Send + Sync + 'static { + fn get_password( + &self, + username: &str, + algorithm: PasswordAlgorithm, + ) -> impl Future> + Send; /// allocate request /// @@ -32,7 +33,8 @@ pub trait Observer: Send + Sync { /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well- /// Known Port range) to discourage clients from using TURN to run /// standard services. - fn allocated(&self, addr: &SessionAddr, username: &str, port: u16) {} + #[allow(unused_variables)] + fn on_allocated(&self, id: &Identifier, username: &str, port: u16) {} /// channel binding request /// @@ -64,7 +66,8 @@ pub trait Observer: Send + Sync { /// different channel, eliminating the possibility that the /// transaction would initially fail but succeed on a /// retransmission. - fn channel_bind(&self, addr: &SessionAddr, username: &str, channel: u16) {} + #[allow(unused_variables)] + fn on_channel_bind(&self, id: &Identifier, username: &str, channel: u16) {} /// create permission request /// @@ -101,11 +104,12 @@ pub trait Observer: Send + Sync { /// The server then responds with a CreatePermission success response. /// There are no mandatory attributes in the success response. /// - /// > NOTE: A server need not do anything special to implement + /// NOTE: A server need not do anything special to implement /// idempotency of CreatePermission requests over UDP using the /// "stateless stack approach". Retransmitted CreatePermission /// requests will simply refresh the permissions. - fn create_permission(&self, addr: &SessionAddr, username: &str, ports: &[u16]) {} + #[allow(unused_variables)] + fn on_create_permission(&self, id: &Identifier, username: &str, ports: &[u16]) {} /// refresh request /// @@ -126,17 +130,17 @@ pub trait Observer: Send + Sync { /// Subsequent processing depends on the "desired lifetime" value: /// /// * If the "desired lifetime" is zero, then the request succeeds and - /// the allocation is deleted. + /// the allocation is deleted. /// /// * If the "desired lifetime" is non-zero, then the request succeeds - /// and the allocation's time-to-expiry is set to the "desired - /// lifetime". + /// and the allocation's time-to-expiry is set to the "desired + /// lifetime". /// /// If the request succeeds, then the server sends a success response /// containing: /// /// * A LIFETIME attribute containing the current value of the time-to- - /// expiry timer. + /// expiry timer. /// /// NOTE: A server need not do anything special to implement /// idempotency of Refresh requests over UDP using the "stateless @@ -146,91 +150,60 @@ pub trait Observer: Send + Sync { /// will cause a 437 (Allocation Mismatch) response if the /// allocation has already been deleted, but the client will treat /// this as equivalent to a success response (see below). - fn refresh(&self, addr: &SessionAddr, username: &str, lifetime: u32) {} + #[allow(unused_variables)] + fn on_refresh(&self, id: &Identifier, username: &str, lifetime: u32) {} - /// session closed + /// session destroy /// /// Triggered when the session leaves from the turn. Possible reasons: the /// session life cycle has expired, external active deletion, or active /// exit of the session. - fn closed(&self, addr: &SessionAddr, username: &str) {} + #[allow(unused_variables)] + fn on_destroy(&self, id: &Identifier, username: &str) {} +} + +pub struct ServiceOptions { + pub port_range: PortRange, + pub software: String, + pub realm: String, + pub interfaces: Vec, + pub handler: T, } /// Turn service. #[derive(Clone)] pub struct Service { interfaces: Arc>, - sessions: Arc>, + manager: Arc>, software: String, realm: String, - observer: T, + handler: T, } impl Service where - T: Clone + Observer + 'static, + T: ServiceHandler + Clone, { - pub fn get_sessions(&self) -> Arc> { - self.sessions.clone() - } - /// Create turn service. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest {} - /// - /// Service::new("test".to_string(), "test".to_string(), vec![], ObserverTest); - /// ``` - pub fn new(software: String, realm: String, interfaces: Vec, observer: T) -> Self { + pub fn new(options: ServiceOptions) -> Self { Self { - sessions: Sessions::new(observer.clone()), - interfaces: Arc::new(interfaces), - observer, - software, - realm, + manager: SessionManager::new(SessionManagerOptions { + port_range: options.port_range, + handler: options.handler.clone(), + }), + interfaces: Arc::new(options.interfaces), + software: options.software, + handler: options.handler, + realm: options.realm, } } - /// Get operationer. - /// - /// # Test - /// - /// ``` - /// use std::net::SocketAddr; - /// use turn_server::stun::attribute::Transport; - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest {} - /// - /// let addr = "127.0.0.1:8080".parse::().unwrap(); - /// let service = Service::new( - /// "test".to_string(), - /// "test".to_string(), - /// vec![], - /// ObserverTest, - /// ); - /// - /// service.get_operationer(addr, addr); - /// ``` - pub fn get_operationer(&self, endpoint: SocketAddr, interface: SocketAddr) -> Operationer { - Operationer::new(ServiceContext { - interfaces: self.interfaces.clone(), - observer: self.observer.clone(), - sessions: self.sessions.clone(), - software: self.software.clone(), - realm: self.realm.clone(), - interface, - endpoint, - }) + /// create a router. + pub fn make_router(&self, endpoint: SocketAddr, interface: SocketAddr) -> Router { + Router::new(self, endpoint, interface) + } + + pub fn get_session_manager(&self) -> &SessionManager { + &self.manager } } diff --git a/crates/service/src/routing.rs b/crates/service/src/routing.rs new file mode 100644 index 00000000..a3733831 --- /dev/null +++ b/crates/service/src/routing.rs @@ -0,0 +1,697 @@ +use crate::{ + Service, ServiceHandler, + session::{Identifier, Session, SessionManager}, +}; + +use std::{net::SocketAddr, sync::Arc}; + +use bytes::BytesMut; +use codec::{ + DecodeResult, Decoder, + channel_data::ChannelData, + crypto::Password, + message::{ + Message, MessageEncoder, + attributes::{error::ErrorType, *}, + methods::*, + }, +}; + +struct Request<'a, 'b, T, M> +where + T: ServiceHandler, +{ + id: &'a Identifier, + encode_buffer: &'b mut BytesMut, + state: &'a State, + payload: &'a M, +} + +impl<'a, 'b, T> Request<'a, 'b, T, Message<'a>> +where + T: ServiceHandler, +{ + // Verify the IP address specified by the client in the request, such as the + // peer address used when creating permissions and binding channels. Currently, + // only peer addresses that are local addresses of the TURN server are allowed; + // arbitrary addresses are not permitted. + // + // Allowing arbitrary addresses would pose security risks, such as enabling + // the TURN server to forward data to any target. + #[inline(always)] + fn verify_ip(&self, address: &SocketAddr) -> bool { + self.state + .interfaces + .iter() + .any(|item| item.ip() == address.ip()) + } + + // The key for the HMAC depends on whether long-term or short-term + // credentials are in use. For long-term credentials, the key is 16 + // bytes: + // + // key = MD5(username ":" realm ":" SASLprep(password)) + // + // That is, the 16-byte key is formed by taking the MD5 hash of the + // result of concatenating the following five fields: (1) the username, + // with any quotes and trailing nulls removed, as taken from the + // USERNAME attribute (in which case SASLprep has already been applied); + // (2) a single colon; (3) the realm, with any quotes and trailing nulls + // removed; (4) a single colon; and (5) the password, with any trailing + // nulls removed and after processing using SASLprep. For example, if + // the username was 'user', the realm was 'realm', and the password was + // 'pass', then the 16-byte HMAC key would be the result of performing + // an MD5 hash on the string 'user:realm:pass', the resulting hash being + // 0x8493fbc53ba582fb4c044c456bdc40eb. + // + // For short-term credentials: + // + // key = SASLprep(password) + // + // where MD5 is defined in RFC 1321 [RFC1321] and SASLprep() is defined + // in RFC 4013 [RFC4013]. + // + // The structure of the key when used with long-term credentials + // facilitates deployment in systems that also utilize SIP. Typically, + // SIP systems utilizing SIP's digest authentication mechanism do not + // actually store the password in the database. Rather, they store a + // value called H(A1), which is equal to the key defined above. + // + // Based on the rules above, the hash used to construct MESSAGE- + // INTEGRITY includes the length field from the STUN message header. + // Prior to performing the hash, the MESSAGE-INTEGRITY attribute MUST be + // inserted into the message (with dummy content). The length MUST then + // be set to point to the length of the message up to, and including, + // the MESSAGE-INTEGRITY attribute itself, but excluding any attributes + // after it. Once the computation is performed, the value of the + // MESSAGE-INTEGRITY attribute can be filled in, and the value of the + // length in the STUN header can be set to its correct value -- the + // length of the entire message. Similarly, when validating the + // MESSAGE-INTEGRITY, the length field should be adjusted to point to + // the end of the MESSAGE-INTEGRITY attribute prior to calculating the + // HMAC. Such adjustment is necessary when attributes, such as + // FINGERPRINT, appear after MESSAGE-INTEGRITY. + #[inline(always)] + async fn verify(&self) -> Option<(&str, Password)> { + let username = self.payload.get::()?; + let algorithm = self + .payload + .get::() + .unwrap_or(PasswordAlgorithm::Md5); + + let password = self + .state + .manager + .get_password(self.id, username, algorithm) + .await?; + + if self.payload.verify(&password).is_err() { + return None; + } + + Some((username, password)) + } +} + +// The target of the response. +#[derive(Debug, Clone, Copy, Default)] +pub struct Target { + pub endpoint: Option, + pub relay: Option, +} + +// The response. +#[derive(Debug)] +pub enum Response<'a> { + Message { + method: Method, + bytes: &'a [u8], + target: Target, + }, + ChannelData { + bytes: &'a [u8], + target: Target, + }, +} + +pub(crate) struct State +where + T: ServiceHandler, +{ + pub realm: String, + pub software: String, + pub manager: Arc>, + pub endpoint: SocketAddr, + pub interface: SocketAddr, + pub interfaces: Arc>, + pub handler: T, +} + +pub struct Router +where + T: ServiceHandler, +{ + current_id: Identifier, + state: State, + decoder: Decoder, + bytes: BytesMut, +} + +impl Router +where + T: ServiceHandler + Clone, +{ + pub fn new(service: &Service, endpoint: SocketAddr, interface: SocketAddr) -> Self { + Self { + bytes: BytesMut::with_capacity(4096), + decoder: Decoder::default(), + current_id: Identifier { + source: "0.0.0.0:0".parse().unwrap(), + interface, + }, + state: State { + interfaces: service.interfaces.clone(), + software: service.software.clone(), + handler: service.handler.clone(), + manager: service.manager.clone(), + realm: service.realm.clone(), + interface, + endpoint, + }, + } + } + + pub async fn route<'a, 'b: 'a>( + &'b mut self, + bytes: &'b [u8], + address: SocketAddr, + ) -> Result>, codec::Error> { + { + self.current_id.source = address; + } + + Ok(match self.decoder.decode(bytes)? { + DecodeResult::ChannelData(channel) => channel_data( + bytes, + Request { + id: &self.current_id, + state: &self.state, + encode_buffer: &mut self.bytes, + payload: &channel, + }, + ), + DecodeResult::Message(message) => { + let req = Request { + id: &self.current_id, + state: &self.state, + encode_buffer: &mut self.bytes, + payload: &message, + }; + + match req.payload.method() { + BINDING_REQUEST => binding(req), + ALLOCATE_REQUEST => allocate(req).await, + CREATE_PERMISSION_REQUEST => create_permission(req).await, + CHANNEL_BIND_REQUEST => channel_bind(req).await, + REFRESH_REQUEST => refresh(req).await, + SEND_INDICATION => indication(req), + _ => None, + } + } + }) + } +} + +fn reject<'a, T>(req: Request<'_, 'a, T, Message<'_>>, error: ErrorType) -> Option> +where + T: ServiceHandler, +{ + let method = req.payload.method().error()?; + + { + let mut message = MessageEncoder::extend(method, req.payload, req.encode_buffer); + message.append::(ErrorCode::from(error)); + + if error == ErrorType::Unauthorized { + message.append::(&req.state.realm); + message.append::( + req.state + .manager + .get_session_or_default(req.id) + .get_ref()? + .nonce(), + ); + + message.append::(vec![ + PasswordAlgorithm::Md5, + PasswordAlgorithm::Sha256, + ]); + } + + message.flush(None).ok()?; + } + + Some(Response::Message { + target: Target::default(), + bytes: req.encode_buffer, + method, + }) +} + +/// [rfc8489](https://tools.ietf.org/html/rfc8489) +/// +/// In the Binding request/response transaction, a Binding request is +/// sent from a STUN client to a STUN server. When the Binding request +/// arrives at the STUN server, it may have passed through one or more +/// NATs between the STUN client and the STUN server (in Figure 1, there +/// are two such NATs). As the Binding request message passes through a +/// NAT, the NAT will modify the source transport address (that is, the +/// source IP address and the source port) of the packet. As a result, +/// the source transport address of the request received by the server +/// will be the public IP address and port created by the NAT closest to +/// the server. This is called a "reflexive transport address". The +/// STUN server copies that source transport address into an XOR-MAPPED- +/// ADDRESS attribute in the STUN Binding response and sends the Binding +/// response back to the STUN client. As this packet passes back through +/// a NAT, the NAT will modify the destination transport address in the +/// IP header, but the transport address in the XOR-MAPPED-ADDRESS +/// attribute within the body of the STUN response will remain untouched. +/// In this way, the client can learn its reflexive transport address +/// allocated by the outermost NAT with respect to the STUN server. +fn binding<'a, T>(req: Request<'_, 'a, T, Message<'_>>) -> Option> +where + T: ServiceHandler, +{ + { + let mut message = MessageEncoder::extend(BINDING_RESPONSE, req.payload, req.encode_buffer); + message.append::(req.id.source); + message.append::(req.id.source); + message.append::(req.state.interface); + message.append::(&req.state.software); + message.flush(None).ok()?; + } + + Some(Response::Message { + target: Target::default(), + method: BINDING_RESPONSE, + bytes: req.encode_buffer, + }) +} + +/// [rfc8489](https://tools.ietf.org/html/rfc8489) +/// +/// In all cases, the server SHOULD only allocate ports from the range +/// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]), +/// unless the TURN server application knows, through some means not +/// specified here, that other applications running on the same host as +/// the TURN server application will not be impacted by allocating ports +/// outside this range. This condition can often be satisfied by running +/// the TURN server application on a dedicated machine and/or by +/// arranging that any other applications on the machine allocate ports +/// before the TURN server application starts. In any case, the TURN +/// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well- +/// Known Port range) to discourage clients from using TURN to run +/// standard contexts. +async fn allocate<'a, T>(req: Request<'_, 'a, T, Message<'_>>) -> Option> +where + T: ServiceHandler, +{ + if req.payload.get::().is_none() { + return reject(req, ErrorType::ServerError); + } + + let Some((username, password)) = req.verify().await else { + return reject(req, ErrorType::Unauthorized); + }; + + let lifetime = req.payload.get::(); + + let Some(port) = req.state.manager.allocate(req.id, lifetime) else { + return reject(req, ErrorType::AllocationQuotaReached); + }; + + req.state.handler.on_allocated(req.id, username, port); + + { + let mut message = MessageEncoder::extend(ALLOCATE_RESPONSE, req.payload, req.encode_buffer); + message.append::(SocketAddr::new(req.state.interface.ip(), port)); + message.append::(req.id.source); + message.append::(lifetime.unwrap_or(600)); + message.append::(&req.state.software); + message.flush(Some(&password)).ok()?; + } + + Some(Response::Message { + target: Target::default(), + method: ALLOCATE_RESPONSE, + bytes: req.encode_buffer, + }) +} + +/// The server MAY impose restrictions on the IP address and port values +/// allowed in the XOR-PEER-ADDRESS attribute; if a value is not allowed, +/// the server rejects the request with a 403 (Forbidden) error. +/// +/// If the request is valid, but the server is unable to fulfill the +/// request due to some capacity limit or similar, the server replies +/// with a 508 (Insufficient Capacity) error. +/// +/// Otherwise, the server replies with a ChannelBind success response. +/// There are no required attributes in a successful ChannelBind +/// response. +/// +/// If the server can satisfy the request, then the server creates or +/// refreshes the channel binding using the channel number in the +/// CHANNEL-NUMBER attribute and the transport address in the XOR-PEER- +/// ADDRESS attribute. The server also installs or refreshes a +/// permission for the IP address in the XOR-PEER-ADDRESS attribute as +/// described in Section 9. +/// +/// NOTE: A server need not do anything special to implement +/// idempotency of ChannelBind requests over UDP using the +/// "stateless stack approach". Retransmitted ChannelBind requests +/// will simply refresh the channel binding and the corresponding +/// permission. Furthermore, the client must wait 5 minutes before +/// binding a previously bound channel number or peer address to a +/// different channel, eliminating the possibility that the +/// transaction would initially fail but succeed on a +/// retransmission. +async fn channel_bind<'a, T>(req: Request<'_, 'a, T, Message<'_>>) -> Option> +where + T: ServiceHandler, +{ + let Some(peer) = req.payload.get::() else { + return reject(req, ErrorType::BadRequest); + }; + + if !req.verify_ip(&peer) { + return reject(req, ErrorType::PeerAddressFamilyMismatch); + } + + let Some(number) = req.payload.get::() else { + return reject(req, ErrorType::BadRequest); + }; + + if !(0x4000..=0x7FFF).contains(&number) { + return reject(req, ErrorType::BadRequest); + } + + let Some((username, password)) = req.verify().await else { + return reject(req, ErrorType::Unauthorized); + }; + + if !req + .state + .manager + .bind_channel(req.id, &req.state.endpoint, peer.port(), number) + { + return reject(req, ErrorType::Forbidden); + } + + req.state.handler.on_channel_bind(req.id, username, number); + + { + MessageEncoder::extend(CHANNEL_BIND_RESPONSE, req.payload, req.encode_buffer) + .flush(Some(&password)) + .ok()?; + } + + Some(Response::Message { + target: Target::default(), + method: CHANNEL_BIND_RESPONSE, + bytes: req.encode_buffer, + }) +} + +/// [rfc8489](https://tools.ietf.org/html/rfc8489) +/// +/// When the server receives the CreatePermission request, it processes +/// as per [Section 5](https://tools.ietf.org/html/rfc8656#section-5) +/// plus the specific rules mentioned here. +/// +/// The message is checked for validity. The CreatePermission request +/// MUST contain at least one XOR-PEER-ADDRESS attribute and MAY contain +/// multiple such attributes. If no such attribute exists, or if any of +/// these attributes are invalid, then a 400 (Bad Request) error is +/// returned. If the request is valid, but the server is unable to +/// satisfy the request due to some capacity limit or similar, then a 508 +/// (Insufficient Capacity) error is returned. +/// +/// If an XOR-PEER-ADDRESS attribute contains an address of an address +/// family that is not the same as that of a relayed transport address +/// for the allocation, the server MUST generate an error response with +/// the 443 (Peer Address Family Mismatch) response code. +/// +/// The server MAY impose restrictions on the IP address allowed in the +/// XOR-PEER-ADDRESS attribute; if a value is not allowed, the server +/// rejects the request with a 403 (Forbidden) error. +/// +/// If the message is valid and the server is capable of carrying out the +/// request, then the server installs or refreshes a permission for the +/// IP address contained in each XOR-PEER-ADDRESS attribute as described +/// in [Section 9](https://tools.ietf.org/html/rfc8656#section-9). +/// The port portion of each attribute is ignored and may be any arbitrary +/// value. +/// +/// The server then responds with a CreatePermission success response. +/// There are no mandatory attributes in the success response. +/// +/// NOTE: A server need not do anything special to implement idempotency of +/// CreatePermission requests over UDP using the "stateless stack approach". +/// Retransmitted CreatePermission requests will simply refresh the +/// permissions. +async fn create_permission<'a, T>(req: Request<'_, 'a, T, Message<'_>>) -> Option> +where + T: ServiceHandler, +{ + let Some((username, password)) = req.verify().await else { + return reject(req, ErrorType::Unauthorized); + }; + + let mut ports = Vec::with_capacity(15); + for it in req.payload.get_all::() { + if !req.verify_ip(&it) { + return reject(req, ErrorType::PeerAddressFamilyMismatch); + } + + ports.push(it.port()); + } + + if !req + .state + .manager + .create_permission(req.id, &req.state.endpoint, &ports) + { + return reject(req, ErrorType::Forbidden); + } + + req.state + .handler + .on_create_permission(req.id, username, &ports); + + { + MessageEncoder::extend(CREATE_PERMISSION_RESPONSE, req.payload, req.encode_buffer) + .flush(Some(&password)) + .ok()?; + } + + Some(Response::Message { + method: CREATE_PERMISSION_RESPONSE, + target: Target::default(), + bytes: req.encode_buffer, + }) +} + +/// When the server receives a Send indication, it processes as per +/// [Section 5](https://tools.ietf.org/html/rfc8656#section-5) plus +/// the specific rules mentioned here. +/// +/// The message is first checked for validity. The Send indication MUST +/// contain both an XOR-PEER-ADDRESS attribute and a DATA attribute. If +/// one of these attributes is missing or invalid, then the message is +/// discarded. Note that the DATA attribute is allowed to contain zero +/// bytes of data. +/// +/// The Send indication may also contain the DONT-FRAGMENT attribute. If +/// the server is unable to set the DF bit on outgoing UDP datagrams when +/// this attribute is present, then the server acts as if the DONT- +/// FRAGMENT attribute is an unknown comprehension-required attribute +/// (and thus the Send indication is discarded). +/// +/// The server also checks that there is a permission installed for the +/// IP address contained in the XOR-PEER-ADDRESS attribute. If no such +/// permission exists, the message is discarded. Note that a Send +/// indication never causes the server to refresh the permission. +/// +/// The server MAY impose restrictions on the IP address and port values +/// allowed in the XOR-PEER-ADDRESS attribute; if a value is not allowed, +/// the server silently discards the Send indication. +/// +/// If everything is OK, then the server forms a UDP datagram as follows: +/// +/// * the source transport address is the relayed transport address of the +/// allocation, where the allocation is determined by the 5-tuple on which the +/// Send indication arrived; +/// +/// * the destination transport address is taken from the XOR-PEER-ADDRESS +/// attribute; +/// +/// * the data following the UDP header is the contents of the value field of +/// the DATA attribute. +/// +/// The handling of the DONT-FRAGMENT attribute (if present), is +/// described in Sections [14](https://tools.ietf.org/html/rfc8656#section-14) +/// and [15](https://tools.ietf.org/html/rfc8656#section-15). +/// +/// The resulting UDP datagram is then sent to the peer. +#[rustfmt::skip] +fn indication<'a, T>(req: Request<'_, 'a, T, Message<'_>>) -> Option> +where + T: ServiceHandler, +{ + let peer = req.payload.get::()?; + let data = req.payload.get::()?; + + if let Some(Session::Authenticated { allocate_port, .. }) = + req.state.manager.get_session(req.id).get_ref() && let Some(local_port) = *allocate_port + { + let relay = req.state.manager.get_relay_address(req.id, peer.port())?; + + { + let mut message = MessageEncoder::extend(DATA_INDICATION, req.payload, req.encode_buffer); + message.append::(SocketAddr::new(req.state.interface.ip(), local_port)); + message.append::(data); + message.flush(None).ok()?; + } + + return Some(Response::Message { + method: DATA_INDICATION, + bytes: req.encode_buffer, + target: Target { + relay: Some(relay.source), + endpoint: if req.state.endpoint != relay.endpoint { + Some(relay.endpoint) + } else { + None + }, + }, + }); + } + + None +} + +/// If the server receives a Refresh Request with a REQUESTED-ADDRESS- +/// FAMILY attribute and the attribute value does not match the address +/// family of the allocation, the server MUST reply with a 443 (Peer +/// Address Family Mismatch) Refresh error response. +/// +/// The server computes a value called the "desired lifetime" as follows: +/// if the request contains a LIFETIME attribute and the attribute value +/// is zero, then the "desired lifetime" is zero. Otherwise, if the +/// request contains a LIFETIME attribute, then the server computes the +/// minimum of the client's requested lifetime and the server's maximum +/// allowed lifetime. If this computed value is greater than the default +/// lifetime, then the "desired lifetime" is the computed value. +/// Otherwise, the "desired lifetime" is the default lifetime. +/// +/// Subsequent processing depends on the "desired lifetime" value: +/// +/// * If the "desired lifetime" is zero, then the request succeeds and the +/// allocation is deleted. +/// +/// * If the "desired lifetime" is non-zero, then the request succeeds and the +/// allocation's time-to-expiry is set to the "desired lifetime". +/// +/// If the request succeeds, then the server sends a success response +/// containing: +/// +/// * A LIFETIME attribute containing the current value of the time-to-expiry +/// timer. +/// +/// NOTE: A server need not do anything special to implement +/// idempotency of Refresh requests over UDP using the "stateless +/// stack approach". Retransmitted Refresh requests with a non- +/// zero "desired lifetime" will simply refresh the allocation. A +/// retransmitted Refresh request with a zero "desired lifetime" +/// will cause a 437 (Allocation Mismatch) response if the +/// allocation has already been deleted, but the client will treat +/// this as equivalent to a success response (see below). +async fn refresh<'a, T>(req: Request<'_, 'a, T, Message<'_>>) -> Option> +where + T: ServiceHandler, +{ + let Some((username, password)) = req.verify().await else { + return reject(req, ErrorType::Unauthorized); + }; + + let lifetime = req.payload.get::().unwrap_or(600); + if !req.state.manager.refresh(req.id, lifetime) { + return reject(req, ErrorType::AllocationMismatch); + } + + req.state.handler.on_refresh(req.id, username, lifetime); + + { + let mut message = MessageEncoder::extend(REFRESH_RESPONSE, req.payload, req.encode_buffer); + message.append::(lifetime); + message.flush(Some(&password)).ok()?; + } + + Some(Response::Message { + target: Target::default(), + method: REFRESH_RESPONSE, + bytes: req.encode_buffer, + }) +} + +/// If the ChannelData message is received on a channel that is not bound +/// to any peer, then the message is silently discarded. +/// +/// On the client, it is RECOMMENDED that the client discard the +/// ChannelData message if the client believes there is no active +/// permission towards the peer. On the server, the receipt of a +/// ChannelData message MUST NOT refresh either the channel binding or +/// the permission towards the peer. +/// +/// On the server, if no errors are detected, the server relays the +/// application data to the peer by forming a UDP datagram as follows: +/// +/// * the source transport address is the relayed transport address of the +/// allocation, where the allocation is determined by the 5-tuple on which the +/// ChannelData message arrived; +/// +/// * the destination transport address is the transport address to which the +/// channel is bound; +/// +/// * the data following the UDP header is the contents of the data field of the +/// ChannelData message. +/// +/// The resulting UDP datagram is then sent to the peer. Note that if +/// the Length field in the ChannelData message is 0, then there will be +/// no data in the UDP datagram, but the UDP datagram is still formed and +/// sent [(Section 4.1 of [RFC6263])](https://tools.ietf.org/html/rfc6263#section-4.1). +fn channel_data<'a, T>( + bytes: &'a [u8], + req: Request<'_, 'a, T, ChannelData<'_>>, +) -> Option> +where + T: ServiceHandler, +{ + let relay = req + .state + .manager + .get_channel_relay_address(req.id, req.payload.number())?; + + Some(Response::ChannelData { + bytes, + target: Target { + relay: Some(relay.source), + endpoint: if req.state.endpoint != relay.endpoint { + Some(relay.endpoint) + } else { + None + }, + }, + }) +} diff --git a/crates/service/src/session/mod.rs b/crates/service/src/session/mod.rs new file mode 100644 index 00000000..348c416f --- /dev/null +++ b/crates/service/src/session/mod.rs @@ -0,0 +1,1092 @@ +pub mod ports; + +use crate::{ + ServiceHandler, + session::ports::{PortAllocator, PortRange}, +}; + +use std::{ + hash::Hash, + net::SocketAddr, + ops::{Deref, DerefMut}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, + thread::{self, sleep}, + time::Duration, +}; + +use ahash::{HashMap, HashMapExt}; +use codec::{crypto::Password, message::attributes::PasswordAlgorithm}; +use parking_lot::{Mutex, RwLock, RwLockReadGuard}; +use rand::{Rng, distr::Alphanumeric}; + +/// The identifier of the session or addr. +/// +/// Each session needs to be identified by a combination of three pieces of +/// information: the addr address, and the transport protocol. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Identifier { + pub source: SocketAddr, + pub interface: SocketAddr, +} + +/// The addr used to record the current session. +/// +/// This is used when forwarding data. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Endpoint { + pub source: SocketAddr, + pub endpoint: SocketAddr, +} + +/// The default HashMap is created without allocating capacity. To improve +/// performance, the turn server needs to pre-allocate the available capacity. +/// +/// So here the HashMap is rewrapped to allocate a large capacity (number of +/// ports that can be allocated) at the default creation time as well. +pub struct Table(HashMap); + +impl Default for Table { + fn default() -> Self { + Self(HashMap::with_capacity(PortRange::default().size())) + } +} + +impl AsRef> for Table { + fn as_ref(&self) -> &HashMap { + &self.0 + } +} + +impl Deref for Table { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Table { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Used to lengthen the timing of the release of a readable lock guard and to +/// provide a more convenient way for external access to the lock's internal +/// data. +pub struct ReadLock<'a, 'b, K, R> { + pub key: &'a K, + pub lock: RwLockReadGuard<'b, R>, +} + +impl<'a, 'b, K, V> ReadLock<'a, 'b, K, Table> +where + K: Eq + Hash, +{ + pub fn get_ref(&self) -> Option<&V> { + self.lock.get(self.key) + } +} + +/// A specially optimised timer. +/// +/// This timer does not stack automatically and needs to be stacked externally +/// and manually. +/// +/// ``` +/// use turn_server_service::session::Timer; +/// +/// let timer = Timer::default(); +/// +/// assert_eq!(timer.get(), 0); +/// assert_eq!(timer.add(), 1); +/// assert_eq!(timer.get(), 1); +/// ``` +#[derive(Default)] +pub struct Timer(AtomicU64); + +impl Timer { + pub fn get(&self) -> u64 { + self.0.load(Ordering::Relaxed) + } + + pub fn add(&self) -> u64 { + self.0.fetch_add(1, Ordering::Relaxed) + 1 + } +} + +/// turn session information. +/// +/// A user can have many sessions. +/// +/// The default survival time for a session is 600 seconds. +#[derive(Debug, Clone)] +pub enum Session { + New { + nonce: String, + expires: u64, + }, + Authenticated { + nonce: String, + /// Authentication information for the session. + /// + /// Digest data is data that summarises usernames and passwords by means of + /// long-term authentication. + username: String, + password: Password, + /// Assignment information for the session. + /// + /// SessionManager are all bound to only one port and one channel. + allocate_port: Option, + allocate_channels: Vec, + permissions: Vec, + expires: u64, + }, +} + +impl Session { + /// Get the nonce of the session. + pub fn nonce(&self) -> &str { + match self { + Session::New { nonce, .. } | Session::Authenticated { nonce, .. } => nonce, + } + } + + /// Check if the session is a new session. + pub fn is_new(&self) -> bool { + matches!(self, Session::New { .. }) + } + + /// Check if the session is an authenticated session. + pub fn is_authenticated(&self) -> bool { + matches!(self, Session::Authenticated { .. }) + } +} + +pub struct SessionManagerOptions { + pub port_range: PortRange, + pub handler: T, +} + +pub struct SessionManager { + sessions: RwLock>, + port_allocator: Mutex, + // Records the sessions corresponding to each assigned port, which will be needed when looking + // up sessions assigned to this port based on the port number. + port_mapping_table: RwLock>, + // Stores the address to which the session should be forwarded when it sends indication to a + // port. This is written when permissions are created to allow a certain address to be + // forwarded to the current session. + port_relay_table: RwLock>>, + // Indicates to which session the data sent by a session to a channel should be forwarded. + channel_relay_table: RwLock>>, + timer: Timer, + handler: T, +} + +impl SessionManager +where + T: ServiceHandler, +{ + pub fn new(options: SessionManagerOptions) -> Arc { + let this = Arc::new(Self { + port_allocator: Mutex::new(PortAllocator::new(options.port_range)), + channel_relay_table: RwLock::new(Table::default()), + port_mapping_table: RwLock::new(Table::default()), + port_relay_table: RwLock::new(Table::default()), + sessions: RwLock::new(Table::default()), + timer: Timer::default(), + handler: options.handler, + }); + + // This is a background thread that silently handles expiring sessions and + // cleans up session information when it expires. + let this_ = Arc::downgrade(&this); + thread::spawn(move || { + let mut address = Vec::with_capacity(255); + + while let Some(this) = this_.upgrade() { + // The timer advances one second and gets the current time offset. + let now = this.timer.add(); + + // This is the part that deletes the session information. + { + // Finds sessions that have expired. + { + this.sessions + .read() + .iter() + .filter(|(_, v)| match v { + Session::New { expires, .. } + | Session::Authenticated { expires, .. } => *expires <= now, + }) + .for_each(|(k, _)| address.push(*k)); + } + + // Delete the expired sessions. + if !address.is_empty() { + this.remove_session(&address); + address.clear(); + } + } + + // Fixing a second tick. + sleep(Duration::from_secs(1)); + } + }); + + this + } + + fn remove_session(&self, addrs: &[Identifier]) { + let mut sessions = self.sessions.write(); + let mut port_allocator = self.port_allocator.lock(); + let mut port_mapping_table = self.port_mapping_table.write(); + let mut port_relay_table = self.port_relay_table.write(); + let mut channel_relay_table = self.channel_relay_table.write(); + + addrs.iter().for_each(|k| { + port_relay_table.remove(k); + channel_relay_table.remove(k); + + if let Some(Session::Authenticated { + allocate_port, + username, + .. + }) = sessions.remove(k) + { + // Removes the session-bound port from the port binding table and + // releases the port back into the allocation pool. + if let Some(port) = allocate_port { + port_mapping_table.remove(&port); + port_allocator.restore(port); + } + + // Notifies that the external session has been closed. + self.handler.on_destroy(k, &username); + } + }); + } + + /// Get session for addr. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// // get_session always creates a new session if it doesn't exist + /// { + /// assert!(sessions.get_session(&addr).get_ref().is_none()); + /// } + /// + /// // get_session always creates a new session if it doesn't exist + /// { + /// let lock = sessions.get_session_or_default(&addr); + /// let session = lock.get_ref().unwrap(); + /// match session { + /// Session::New { .. } => {}, + /// _ => panic!("Expected new session"), + /// } + /// } + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// { + /// let lock = sessions.get_session(&addr); + /// let session = lock.get_ref().unwrap(); + /// match session { + /// Session::Authenticated { username, allocate_port, allocate_channels, .. } => { + /// assert_eq!(username, "test"); + /// assert_eq!(allocate_port, &None); + /// assert_eq!(allocate_channels.len(), 0); + /// } + /// _ => panic!("Expected authenticated session"), + /// } + /// } + /// ``` + pub fn get_session_or_default<'a, 'b>( + &'a self, + key: &'b Identifier, + ) -> ReadLock<'b, 'a, Identifier, Table> { + { + let lock = self.sessions.read(); + if lock.contains_key(key) { + return ReadLock { + lock: self.sessions.read(), + key, + }; + } + } + + { + self.sessions.write().insert( + *key, + Session::New { + // A random string of length 16. + nonce: make_nonce(), + // Current time stacks for 600 seconds. + expires: self.timer.get() + 600, + }, + ); + } + + ReadLock { + lock: self.sessions.read(), + key, + } + } + + pub fn get_session<'a, 'b>( + &'a self, + key: &'b Identifier, + ) -> ReadLock<'b, 'a, Identifier, Table> { + ReadLock { + lock: self.sessions.read(), + key, + } + } + + /// Get digest for addr. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// // First call get_session to create a new session + /// { + /// sessions.get_session(&addr); + /// } + /// assert_eq!(pollster::block_on(sessions.get_password(&addr, "test1", PasswordAlgorithm::Md5)), None); + /// + /// // Create a new session for the next test + /// { + /// sessions.get_session(&addr); + /// } + /// assert_eq!(sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(), Some(digest)); + /// + /// // The third call should return cached digest + /// assert_eq!(sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(), Some(digest)); + /// ``` + pub async fn get_password( + &self, + addr: &Identifier, + username: &str, + algorithm: PasswordAlgorithm, + ) -> Option { + // Already authenticated, get the cached digest directly. + { + if let Some(Session::Authenticated { password, .. }) = self.sessions.read().get(addr) { + return Some(*password); + } + } + + // Get the current user's password from an external handler and create a + // digest. + let password = self.handler.get_password(username, algorithm).await?; + + // Record a new session. + { + let mut lock = self.sessions.write(); + let nonce = if let Some(Session::New { nonce, .. }) = lock.remove(addr) { + nonce + } else { + make_nonce() + }; + + lock.insert( + *addr, + Session::Authenticated { + allocate_channels: Vec::with_capacity(10), + permissions: Vec::with_capacity(10), + expires: self.timer.get() + 600, + username: username.to_string(), + allocate_port: None, + password, + nonce, + }, + ); + } + + Some(password) + } + + pub fn allocated(&self) -> usize { + self.port_allocator.lock().len() + } + + /// Assign a port number to the session. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// { + /// let lock = sessions.get_session(&addr); + /// let session = lock.get_ref().unwrap(); + /// match session { + /// Session::Authenticated { username, allocate_port, allocate_channels, .. } => { + /// assert_eq!(username, "test"); + /// assert_eq!(allocate_port, &None); + /// assert_eq!(allocate_channels.len(), 0); + /// } + /// _ => panic!("Expected authenticated session"), + /// } + /// } + /// + /// let port = sessions.allocate(&addr, None).unwrap(); + /// { + /// let lock = sessions.get_session(&addr); + /// let session = lock.get_ref().unwrap(); + /// match session { + /// Session::Authenticated { username, allocate_port, allocate_channels, .. } => { + /// assert_eq!(username, "test"); + /// assert_eq!(allocate_port, &Some(port)); + /// assert_eq!(allocate_channels.len(), 0); + /// } + /// _ => panic!("Expected authenticated session"), + /// } + /// } + /// + /// assert_eq!(sessions.allocate(&addr, None), Some(port)); + /// ``` + pub fn allocate(&self, addr: &Identifier, lifetime: Option) -> Option { + let mut lock = self.sessions.write(); + + if let Some(Session::Authenticated { + allocate_port, + expires, + .. + }) = lock.get_mut(addr) + { + // If the port has already been allocated, re-allocation is not allowed. + if let Some(port) = allocate_port { + return Some(*port); + } + + // Records the port assigned to the current session and resets the alive time. + let port = self.port_allocator.lock().alloc(None)?; + *expires = self.timer.get() + (lifetime.unwrap_or(600) as u64); + *allocate_port = Some(port); + + // Write the allocation port binding table. + self.port_mapping_table.write().insert(port, *addr); + Some(port) + } else { + None + } + } + + /// Create permission for session. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let endpoint = "127.0.0.1:3478".parse().unwrap(); + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let peer_addr = Identifier { + /// source: "127.0.0.1:8081".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// sessions.get_password(&peer_addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// let port = sessions.allocate(&addr, None).unwrap(); + /// let peer_port = sessions.allocate(&peer_addr, None).unwrap(); + /// + /// assert!(!sessions.create_permission(&addr, &endpoint, &[port])); + /// assert!(sessions.create_permission(&addr, &endpoint, &[peer_port])); + /// + /// assert!(!sessions.create_permission(&peer_addr, &endpoint, &[peer_port])); + /// assert!(sessions.create_permission(&peer_addr, &endpoint, &[port])); + /// ``` + pub fn create_permission( + &self, + addr: &Identifier, + endpoint: &SocketAddr, + ports: &[u16], + ) -> bool { + let mut sessions = self.sessions.write(); + let mut port_relay_table = self.port_relay_table.write(); + let port_mapping_table = self.port_mapping_table.read(); + + // Finds information about the current session. + if let Some(Session::Authenticated { + allocate_port, + permissions, + .. + }) = sessions.get_mut(addr) + { + // The port number assigned to the current session. + let local_port = if let Some(it) = allocate_port { + *it + } else { + return false; + }; + + // You cannot create permissions for yourself. + if ports.contains(&local_port) { + return false; + } + + // Each peer port must be present. + let mut peers = Vec::with_capacity(15); + for port in ports { + if let Some(it) = port_mapping_table.get(port) { + peers.push((it, *port)); + } else { + return false; + } + } + + // Create a port forwarding mapping relationship for each peer session. + for (peer, port) in peers { + port_relay_table + .entry(*peer) + .or_insert_with(|| HashMap::with_capacity(20)) + .insert( + local_port, + Endpoint { + source: addr.source, + endpoint: *endpoint, + }, + ); + + // Do not store the same peer ports to the permission list over and over again. + if !permissions.contains(&port) { + permissions.push(port); + } + } + + true + } else { + false + } + } + + /// Binding a channel to the session. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let endpoint = "127.0.0.1:3478".parse().unwrap(); + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let peer_addr = Identifier { + /// source: "127.0.0.1:8081".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// sessions.get_password(&peer_addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// let port = sessions.allocate(&addr, None).unwrap(); + /// let peer_port = sessions.allocate(&peer_addr, None).unwrap(); + /// { + /// assert_eq!( + /// match sessions.get_session(&addr).get_ref().unwrap() { + /// Session::Authenticated { allocate_channels, .. } => allocate_channels.len(), + /// _ => panic!("Expected authenticated session"), + /// }, + /// 0 + /// ); + /// } + /// + /// { + /// assert_eq!( + /// match sessions.get_session(&peer_addr).get_ref().unwrap() { + /// Session::Authenticated { allocate_channels, .. } => allocate_channels.len(), + /// _ => panic!("Expected authenticated session"), + /// }, + /// 0 + /// ); + /// } + /// + /// assert!(sessions.bind_channel(&addr, &endpoint, peer_port, 0x4000)); + /// assert!(sessions.bind_channel(&peer_addr, &endpoint, port, 0x4000)); + /// + /// { + /// assert_eq!( + /// match sessions.get_session(&addr).get_ref().unwrap() { + /// Session::Authenticated { allocate_channels, .. } => allocate_channels.clone(), + /// _ => panic!("Expected authenticated session"), + /// }, + /// vec![0x4000] + /// ); + /// } + /// + /// { + /// assert_eq!( + /// match sessions.get_session(&peer_addr).get_ref().unwrap() { + /// Session::Authenticated { allocate_channels, .. } => allocate_channels.clone(), + /// _ => panic!("Expected authenticated session"), + /// }, + /// vec![0x4000] + /// ); + /// } + /// ``` + pub fn bind_channel( + &self, + addr: &Identifier, + endpoint: &SocketAddr, + port: u16, + channel: u16, + ) -> bool { + // Finds the address of the bound opposing port. + let peer = if let Some(it) = self.port_mapping_table.read().get(&port) { + *it + } else { + return false; + }; + + // Records the channel used for the current session. + { + let mut lock = self.sessions.write(); + if let Some(Session::Authenticated { + allocate_channels, .. + }) = lock.get_mut(addr) + { + if !allocate_channels.contains(&channel) { + allocate_channels.push(channel); + } + } else { + return false; + }; + } + + // Binding ports also creates permissions. + if !self.create_permission(addr, endpoint, &[port]) { + return false; + } + + // Create channel forwarding mapping relationships for peers. + self.channel_relay_table + .write() + .entry(peer) + .or_insert_with(|| HashMap::with_capacity(10)) + .insert( + channel, + Endpoint { + source: addr.source, + endpoint: *endpoint, + }, + ); + + true + } + + /// Gets the peer of the current session bound channel. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let endpoint = "127.0.0.1:3478".parse().unwrap(); + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let peer_addr = Identifier { + /// source: "127.0.0.1:8081".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// sessions.get_password(&peer_addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// let port = sessions.allocate(&addr, None).unwrap(); + /// let peer_port = sessions.allocate(&peer_addr, None).unwrap(); + /// + /// assert!(sessions.bind_channel(&addr, &endpoint, peer_port, 0x4000)); + /// assert!(sessions.bind_channel(&peer_addr, &endpoint, port, 0x4000)); + /// assert_eq!( + /// sessions + /// .get_channel_relay_address(&addr, 0x4000) + /// .unwrap() + /// .endpoint, + /// endpoint + /// ); + /// + /// assert_eq!( + /// sessions + /// .get_channel_relay_address(&peer_addr, 0x4000) + /// .unwrap() + /// .endpoint, + /// endpoint + /// ); + /// ``` + pub fn get_channel_relay_address(&self, addr: &Identifier, channel: u16) -> Option { + self.channel_relay_table + .read() + .get(addr)? + .get(&channel) + .copied() + } + + /// Get the address of the port binding. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let endpoint = "127.0.0.1:3478".parse().unwrap(); + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let peer_addr = Identifier { + /// source: "127.0.0.1:8081".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// sessions.get_password(&peer_addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// let port = sessions.allocate(&addr, None).unwrap(); + /// let peer_port = sessions.allocate(&peer_addr, None).unwrap(); + /// + /// assert!(sessions.create_permission(&addr, &endpoint, &[peer_port])); + /// assert!(sessions.create_permission(&peer_addr, &endpoint, &[port])); + /// + /// assert_eq!( + /// sessions + /// .get_relay_address(&addr, peer_port) + /// .unwrap() + /// .endpoint, + /// endpoint + /// ); + /// + /// assert_eq!( + /// sessions + /// .get_relay_address(&peer_addr, port) + /// .unwrap() + /// .endpoint, + /// endpoint + /// ); + /// ``` + pub fn get_relay_address(&self, addr: &Identifier, port: u16) -> Option { + self.port_relay_table.read().get(addr)?.get(&port).copied() + } + + /// Refresh the session for addr. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::*; + /// use turn_server_service::*; + /// use codec::message::attributes::PasswordAlgorithm; + /// use codec::crypto::Password; + /// use pollster::FutureExt; + /// + /// #[derive(Clone)] + /// struct ServiceHandlerTest; + /// + /// impl ServiceHandler for ServiceHandlerTest { + /// async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + /// if username == "test" { + /// Some(codec::crypto::generate_password(username, "test", "test", algorithm)) + /// } else { + /// None + /// } + /// } + /// } + /// + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), + /// interface: "127.0.0.1:3478".parse().unwrap(), + /// }; + /// + /// let digest = Password::Md5([ + /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, + /// 239, + /// ]); + /// + /// let sessions = SessionManager::new(SessionManagerOptions { + /// port_range: (49152..65535).into(), + /// handler: ServiceHandlerTest, + /// }); + /// + /// // get_session always creates a new session if it doesn't exist + /// { + /// let lock = sessions.get_session_or_default(&addr); + /// let session = lock.get_ref().unwrap(); + /// match session { + /// Session::New { .. } => {}, + /// _ => panic!("Expected new session"), + /// } + /// } + /// + /// sessions.get_password(&addr, "test", PasswordAlgorithm::Md5).block_on(); + /// + /// { + /// let lock = sessions.get_session(&addr); + /// let expires = match lock.get_ref().unwrap() { + /// Session::Authenticated { expires, .. } => *expires, + /// _ => panic!("Expected authenticated session"), + /// }; + /// + /// assert!(expires == 600 || expires == 601 || expires == 602); + /// } + /// + /// assert!(sessions.refresh(&addr, 0)); + /// + /// // After refresh with lifetime 0, session should be removed + /// { + /// assert!(sessions.get_session(&addr).get_ref().is_none()); + /// } + /// ``` + pub fn refresh(&self, addr: &Identifier, lifetime: u32) -> bool { + if lifetime > 3600 { + return false; + } + + if lifetime == 0 { + self.remove_session(&[*addr]); + } else if let Some(Session::Authenticated { expires, .. }) = + self.sessions.write().get_mut(addr) + { + *expires = self.timer.get() + lifetime as u64; + } else { + return false; + } + + true + } +} + +/// Generate a random nonce. +fn make_nonce() -> String { + rand::rng() + .sample_iter(&Alphanumeric) + .take(16) + .map(char::from) + .collect() +} diff --git a/crates/service/src/session/ports.rs b/crates/service/src/session/ports.rs new file mode 100644 index 00000000..28edfadb --- /dev/null +++ b/crates/service/src/session/ports.rs @@ -0,0 +1,399 @@ +use std::{fmt::Display, str::FromStr}; + +use rand::Rng; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PortRange { + start: u16, + end: u16, +} + +impl PortRange { + pub fn size(&self) -> usize { + (self.end - self.start) as usize + } + + pub fn contains(&self, port: u16) -> bool { + port >= self.start && port <= self.end + } + + pub fn start(&self) -> u16 { + self.start + } + + pub fn end(&self) -> u16 { + self.end + } +} + +impl Default for PortRange { + fn default() -> Self { + Self { + start: 49152, + end: 65535, + } + } +} + +impl From> for PortRange { + fn from(range: std::ops::Range) -> Self { + assert!(range.start <= range.end); + + Self { + start: range.start, + end: range.end, + } + } +} + +impl Display for PortRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}..{}", self.start, self.end) + } +} + +#[derive(Debug)] +pub struct PortRangeParseError(String); + +impl std::error::Error for PortRangeParseError {} + +impl std::fmt::Display for PortRangeParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for PortRangeParseError { + fn from(error: std::num::ParseIntError) -> Self { + PortRangeParseError(error.to_string()) + } +} + +impl FromStr for PortRange { + type Err = PortRangeParseError; + + fn from_str(s: &str) -> Result { + let (start, end) = s + .split_once("..") + .ok_or(PortRangeParseError(s.to_string()))?; + + Ok(Self { + start: start.parse()?, + end: end.parse()?, + }) + } +} + +#[cfg(feature = "serde")] +impl Serialize for PortRange { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for PortRange { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(|e| serde::de::Error::custom(e.0)) + } +} + +/// Bit Flag +#[derive(PartialEq, Eq)] +pub enum Bit { + Low, + High, +} + +/// Random Port +/// +/// Recently, awareness has been raised about a number of "blind" attacks +/// (i.e., attacks that can be performed without the need to sniff the +/// packets that correspond to the transport protocol instance to be +/// attacked) that can be performed against the Transmission Control +/// Protocol (TCP) [RFC0793] and similar protocols. The consequences of +/// these attacks range from throughput reduction to broken connections +/// or data corruption [RFC5927] [RFC4953] [Watson]. +/// +/// All these attacks rely on the attacker's ability to guess or know the +/// five-tuple (Protocol, Source Address, Source port, Destination +/// Address, Destination Port) that identifies the transport protocol +/// instance to be attacked. +/// +/// Services are usually located at fixed, "well-known" ports [IANA] at +/// the host supplying the service (the server). Client applications +/// connecting to any such service will contact the server by specifying +/// the server IP address and service port number. The IP address and +/// port number of the client are normally left unspecified by the client +/// application and thus are chosen automatically by the client +/// networking stack. Ports chosen automatically by the networking stack +/// are known as ephemeral ports [Stevens]. +/// +/// While the server IP address, the well-known port, and the client IP +/// address may be known by an attacker, the ephemeral port of the client +/// is usually unknown and must be guessed. +/// +/// # Test +/// +/// ``` +/// use std::collections::HashSet; +/// use turn_server_service::session::ports::*; +/// +/// let mut pool = PortAllocator::default(); +/// let mut ports = HashSet::with_capacity(PortAllocator::default().capacity()); +/// +/// while let Some(port) = pool.alloc(None) { +/// ports.insert(port); +/// } +/// +/// assert_eq!(PortAllocator::default().capacity() + 1, ports.len()); +/// ``` +pub struct PortAllocator { + port_range: PortRange, + buckets: Vec, + allocated: usize, + bit_len: u32, + max_offset: usize, +} + +impl Default for PortAllocator { + fn default() -> Self { + Self::new(PortRange::default()) + } +} + +impl PortAllocator { + pub fn new(port_range: PortRange) -> Self { + let capacity = port_range.size(); + let bucket_size = (capacity as f32 / 64.0).ceil() as usize; + + Self { + bit_len: (capacity as f32 % 64.0).ceil() as u32, + buckets: vec![0; bucket_size], + max_offset: bucket_size - 1, + allocated: 0, + port_range, + } + } + + /// get pools capacity. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// assert_eq!(PortAllocator::default().capacity(), 65535 - 49152); + /// ``` + pub fn capacity(&self) -> usize { + self.port_range.size() + } + + /// get port range. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// let pool = PortAllocator::default(); + /// + /// assert_eq!(pool.port_range().start(), 49152); + /// assert_eq!(pool.port_range().end(), 65535); + /// + /// let pool = PortAllocator::new((50000..60000).into()); + /// + /// assert_eq!(pool.port_range().start(), 50000); + /// assert_eq!(pool.port_range().end(), 60000); + /// ``` + pub fn port_range(&self) -> &PortRange { + &self.port_range + } + + /// get pools allocated size. + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// let mut pools = PortAllocator::default(); + /// assert_eq!(pools.len(), 0); + /// + /// pools.alloc(None).unwrap(); + /// assert_eq!(pools.len(), 1); + /// ``` + pub fn len(&self) -> usize { + self.allocated + } + + /// get pools allocated size is empty. + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// let mut pools = PortAllocator::default(); + /// assert_eq!(pools.len(), 0); + /// assert_eq!(pools.is_empty(), true); + /// ``` + pub fn is_empty(&self) -> bool { + self.allocated == 0 + } + + /// random assign a port. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// let mut pool = PortAllocator::default(); + /// + /// assert_eq!(pool.alloc(Some(0)), Some(49152)); + /// assert_eq!(pool.alloc(Some(0)), Some(49153)); + /// + /// assert!(pool.alloc(None).is_some()); + /// ``` + pub fn alloc(&mut self, start: Option) -> Option { + let mut index = None; + let mut offset = start.unwrap_or_else(|| rand::rng().random_range(0..self.max_offset)); + + // When the partition lookup has gone through the entire partition list, the + // lookup should be stopped, and the location where it should be stopped is + // recorded here. + let previous = if offset == 0 { + self.max_offset + } else { + offset - 1 + }; + + loop { + // Finds the first high position in the partition. + if let Some(i) = { + let bucket = self.buckets[offset]; + if bucket < u64::MAX { + let idx = bucket.leading_ones(); + + // Check to see if the jump is beyond the partition list or the lookup exceeds + // the maximum length of the allocation table. + if offset == self.max_offset && idx > self.bit_len { + None + } else { + Some(idx) + } + } else { + None + } + } { + index = Some(i as usize); + break; + } + + // As long as it doesn't find it, it continues to re-find it from the next + // partition. + if offset == self.max_offset { + offset = 0; + } else { + offset += 1; + } + + // Already gone through all partitions, lookup failed. + if offset == previous { + break; + } + } + + // Writes to the partition, marking the current location as already allocated. + let index = index?; + self.set_bit(offset, index, Bit::High); + self.allocated += 1; + + // The actual port number is calculated from the partition offset position. + let num = (offset * 64 + index) as u16; + let port = self.port_range.start + num; + Some(port) + } + + /// write bit flag in the bucket. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// let mut pool = PortAllocator::default(); + /// + /// assert_eq!(pool.alloc(Some(0)), Some(49152)); + /// assert_eq!(pool.alloc(Some(0)), Some(49153)); + /// + /// pool.set_bit(0, 0, Bit::High); + /// pool.set_bit(0, 1, Bit::High); + /// + /// assert_eq!(pool.alloc(Some(0)), Some(49154)); + /// assert_eq!(pool.alloc(Some(0)), Some(49155)); + /// ``` + pub fn set_bit(&mut self, bucket: usize, index: usize, bit: Bit) { + let high_mask = 1 << (63 - index); + let mask = match bit { + Bit::Low => u64::MAX ^ high_mask, + Bit::High => high_mask, + }; + + let value = self.buckets[bucket]; + self.buckets[bucket] = match bit { + Bit::High => value | mask, + Bit::Low => value & mask, + }; + } + + /// restore port in the buckets. + /// + /// # Test + /// + /// ``` + /// use turn_server_service::session::ports::*; + /// + /// let mut pool = PortAllocator::default(); + /// + /// assert_eq!(pool.alloc(Some(0)), Some(49152)); + /// assert_eq!(pool.alloc(Some(0)), Some(49153)); + /// + /// pool.restore(49152); + /// pool.restore(49153); + /// + /// assert_eq!(pool.alloc(Some(0)), Some(49152)); + /// assert_eq!(pool.alloc(Some(0)), Some(49153)); + /// ``` + pub fn restore(&mut self, port: u16) { + assert!(self.port_range.contains(port)); + + // Calculate the location in the partition from the port number. + let offset = (port - self.port_range.start) as usize; + let bucket = offset / 64; + let index = offset - (bucket * 64); + + // Gets the bit value in the port position in the partition, if it is low, no + // processing is required. + if { + match (self.buckets[bucket] & (1 << (63 - index))) >> (63 - index) { + 0 => Bit::Low, + 1 => Bit::High, + _ => panic!(), + } + } == Bit::Low + { + return; + } + + self.set_bit(bucket, index, Bit::Low); + self.allocated -= 1; + } +} diff --git a/crates/service/tests/turn.rs b/crates/service/tests/turn.rs new file mode 100644 index 00000000..b91c3a63 --- /dev/null +++ b/crates/service/tests/turn.rs @@ -0,0 +1,49 @@ +use std::net::{Ipv4Addr, SocketAddr}; + +use anyhow::Result; +use codec::{ + crypto::{Password, generate_password}, + message::attributes::PasswordAlgorithm, +}; +use turn_server_service::{routing::Router, session::ports::PortRange, Service, ServiceHandler, ServiceOptions}; + +#[derive(Default, Clone)] +struct Handler; + +impl ServiceHandler for Handler { + async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { + if username != "USERNAME" { + return None; + } + + Some(generate_password( + "USERNAME", "PASSWORD", "REALM", algorithm, + )) + } +} + +#[test] +fn test_turn_server_service() -> Result<()> { + let service = Service::new(ServiceOptions { + port_range: PortRange::default(), + software: "SOFTWARE".to_string(), + realm: "REALM".to_string(), + interfaces: vec![SocketAddr::from((Ipv4Addr::LOCALHOST, 3478))], + handler: Handler::default(), + }); + + let router = service.make_router( + SocketAddr::from((Ipv4Addr::LOCALHOST, 3478)), + SocketAddr::from((Ipv4Addr::LOCALHOST, 3478)), + ); + + Ok(()) +} + +async fn test_peer(router: Router) -> Result<()> { + { + router.route(bytes, address).await?; + } + + Ok(()) +} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 3109dec1..00000000 --- a/docs/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Table of contents - -- [Install](install.md) -- [Build](build.md) -- [Start the server](start-the-server.md) -- [Configure](configure.md) -- [API](api.md) diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index f047626b..00000000 --- a/docs/api.md +++ /dev/null @@ -1,95 +0,0 @@ -# REST API - ---- - -### GET - `/info` - Info - -Info: - -- `software` - string - Software information of turn server -- `uptime` - uint64 - Turn the server's running time in seconds -- `port_allocated` - uint16 - The number of allocated ports -- `port_capacity` - uint16 - The total number of ports available for allocation -- `interfaces` - Interface[] - Turn all interfaces bound to the server - -Interface: - -- `transport` - int - 0 = UDP, 1 = TCP -- `bind` - string - turn server listen address -- `external` - string - specify the node external address and port - -Get the information of the turn server, including version information, listening interface, startup time, etc. - ---- - -### GET `/session?address=&interface=` - Session[] - -Session: - -- `address` - string - The IP address and port number currently used by the session -- `username` - string - Username used in session authentication -- `channels` - uint16[] - Channel numbers that have been assigned to the session -- `port` - uint16 - Port numbers that have been assigned to the session -- `expires` - uint32 - The validity period of the current session application, in seconds -- `permissions` - uint16[] - What ports have forwarding privileges for the session. - -Get session information. A session corresponds to each UDP socket. It should be noted that a user can have multiple sessions at the same time. - ---- - -### GET - `/session/statistics?address=&interface=` - Statistics - -Statistics: - -- `received_bytes` - uint64 - Number of bytes received in the current session -- `send_bytes` - uint64 - The number of bytes sent by the current session -- `received_pkts` - uint64 - Number of packets received in the current session -- `send_pkts` - uint64 - The number of packets sent by the current session - -Get session statistics, which is mainly the traffic statistics of the current session. - ---- - -### DELETE - `/session?address=&interface=` - -Delete the session. Deleting the session will cause the turn server to delete all routing information of the current session. If there is a peer, the peer will also be disconnected. - ---- - -### GET - `/events` (EventSource) - -> This is an sse push event interface through which clients can subscribe to server events. - -[Session]: - -- `address` - string - The IP address and port number of the UDP or TCP connection used by the client. -- `interface` - string - The network interface used by the current session. - -allocate: - -- `session` - Session -- `username` - string - The username used for the turn session. -- `port` - uint16 - The port to which the request is assigned. - -channel binding: - -- `session` - Session -- `username` - string - The username used for the turn session. -- `channel` - uint16 - The channel to which the request is binding. - -create permission: - -- `session` - Session -- `username` - string - The username used for the turn session. -- `ports` - uint16[] - The port number of the other side specified when the privilege was created. - -refresh: - -- `session` - Session -- `username` - string - The username used for the turn session. -- `lifetime` - uint32 - Time to expiration in seconds. - -closed: - -- `session` - Session -- `username` - string - The username used for the turn session. diff --git a/docs/build.md b/docs/build.md deleted file mode 100644 index 545f8a90..00000000 --- a/docs/build.md +++ /dev/null @@ -1,39 +0,0 @@ -# Build - -### Prerequisites - -You need to install the Rust toolchain, if you have already installed it, you can skip it, Install Rust, then get the source code: - -```bash -git clone https://github.com/mycrl/turn-rs -``` - -### Build Workspace - -Compile the entire workspace in release mode: - -```bash -cd turn-rs -cargo build --release -``` - -You can enable target CPU optimizations, which will enable optimizations based on your current CPU. This can be easily enabled by adding an environment variable before compiling: - -```bash -export RUSTFLAGS='-C target-cpu=native' -``` - -### Features - -- `udp` - (enabled by default) Enables UDP transport layer support. -- `tcp` - Enables TCP transport layer support. -- `api` - Enable the HTTP REST API server feature. -- `prometheus` - Enable prometheus indicator support. - -No features are enabled by default and need to be turned on by manual specification. - -```bash -cargo build --release --features udp,tcp,api,prometheus -``` - -After the compilation is complete, you can find the binary file in the `target/release` directory. diff --git a/docs/configure.md b/docs/configure.md deleted file mode 100644 index 6e616200..00000000 --- a/docs/configure.md +++ /dev/null @@ -1,162 +0,0 @@ -# Configure - -Sample configuration file. However, please note that the sample is only used to show all configuration items. You need to adjust the corresponding configuration according to the actual situation. The configuration file is written in TOML format. - -```toml -[turn] -# turn server realm -# -# specify the domain where the server is located. -# for a single node, this configuration is fixed, -# but each node can be configured as a different domain. -# this is a good idea to divide the nodes by namespace. -realm = "localhost" - -# turn server listen interfaces -# -# The address and port to which the UDP Server is bound. Multiple -# addresses can be bound at the same time. The binding address supports -# ipv4 and ipv6. -[[turn.interfaces]] -transport = "udp" -bind = "127.0.0.1:3478" -# external address -# -# specify the node external address and port. -# for the case of exposing the service to the outside, -# you need to manually specify the server external IP -# address and service listening port. -external = "127.0.0.1:3478" - -[[turn.interfaces]] -transport = "tcp" -bind = "127.0.0.1:3478" -external = "127.0.0.1:3478" - -[api] -# controller bind -# -# This option specifies the http server binding address used to control -# the turn server. -# -# Warn: This http server does not contain any means of authentication, -# and sensitive information and dangerous operations can be obtained -# through this service, please do not expose it directly to an unsafe -# environment. -bind = "127.0.0.1:3000" - -[log] -# log level -# -# An enum representing the available verbosity levels of the logger. -level = "info" - -[auth] -# Static authentication key value (string) that applies only to the TURN -# REST API. -# -# If set, the turn server will not request external services via the HTTP -# Hooks API to obtain the key. -# -# static_auth_secret = "" - -# static user password -# -# This option can be used to specify the -# static identity authentication information used by the turn server for -# verification. Note: this is a high-priority authentication method, turn -# The server will try to use static authentication first, and then use -# external control service authentication. -[auth.static_credentials] -# user1 = "test" -# user2 = "test" -``` - -## Configuration keys - ---- - -### `turn.realm` - -- Type: string -- Default: "localhost" - -This option describes the realm of the turn service. For the definition of realm, please refer to [RFC](https://datatracker.ietf.org/doc/html/rfc5766#section-3). - ---- - -### `[turn.interfaces]` - -- Type: array of interface -- Default: [] - -This option describes the interface to which the turn service is bound. A turn service can be bound to multiple interfaces at the same time. - ---- - -### `[turn.interfaces.transport]` - -- Type: enum of string - -Describes the transport protocol used by the interface. The value can be `udp` or `tcp`, which correspond to udp turn and tcp turn respectively, and choose whether to bind the turn service to a udp socket or a tcp socket. - ---- - -### `[turn.interfaces.bind]` - -- Type: string - -The IP address and port number bound to the interface. This is the address to which the internal socket is bound. - ---- - -### `[turn.interfaces.external]` - -- Type: string - -bind is used to bind to the address of your local NIC, for example, you have two NICs A and B on your server, the IP address of NIC A is 192.168.1.2, and the address of NIC B is 192.168.1.3, if you bind to NIC A, you should bind to the address of 192.168.1.2, and bind to 0.0.0.0 means that it listens to all of them at the same time. - -external is that your network card for the client can "see" the ip address, continue the above example, your A network card in communication with the external, if it is in the local area network, then other clients see is your LAN address, that is, 192.168.1.2, but in reality, generally However, in reality, the network topology where the server is deployed, there will be another public ip, such as 1.1.1.1, which is your ip address seen by other clients. - -As for why bind and external are needed, this is because for the stun protocol, the situation is more complicated, the stun server needs to inform its own external ip address, which allows the stun client to connect to the specified address through the ip address informed by the server. - ---- - -### `api.bind` - -- Type: string -- Default: "127.0.0.1:3000" - -Describes the address to which the turn api server is bound. - -The turn service provides an external REST API. External parties can control the turn service through HTTP or allow the turn service to perform dynamic authentication and push events to the outside through HTTP. - -> Warning: The REST API does not provide any authentication or encryption measures. You need to run the turn service in a trusted network environment or add a proxy to increase authentication and encryption measures. - ---- - -### `log.level` - -- Type: enum of string -- Default: "info" - -Describes the log level of the turn service. Possible values ​​are `"error"`, `"warn"`, `"info"`, `"debug"`, `"trace"`. - ---- - -### `auth.static_credentials` - -- Type: key values - -Describes static authentication information, with username and password as key pair. Static identity authentication is authentication information provided to the turn service in advance. The turn service will first look for this table when it needs to authenticate the turn session. If it cannot find it, it will use Web Hooks for external authentication. - ---- - -### `auth.static_auth_secret` - -- Type: string -- Default: None - -Static authentication key value (string) that applies only to the TURN REST API. - -If set, the turn server will not request external services via the HTTP Hooks API to obtain the key. diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index df3a3c02..00000000 --- a/docs/install.md +++ /dev/null @@ -1,15 +0,0 @@ -# Install - -turn-rs is cross-platform, supports Linux, Windows, macOS, and provides a Docker image. - -First, download the corresponding binary file for your platform from [github release](https://github.com/mycrl/turn-rs/releases). If your current platform and operating system are not provided in the release, you can jump to [build](./build.md) to compile it yourself. Don't panic, because compiling turn-rs is very simple. - -### Docker - -The docker image is published to [github packages](https://github.com/mycrl/turn-rs/pkgs/container/turn-server), and no docker.io image is provided. Using this image is very simple: - -```bash -docker pull ghcr.io/mycrl/turn-server:latest -``` - -It should be noted that using this image requires a custom configuration file. You can use the `-v` option to override the default configuration file path inside the image. The default configuration file path is `/etc/turn-server/config.toml` diff --git a/docs/start-the-server.md b/docs/start-the-server.md deleted file mode 100644 index 1a79252d..00000000 --- a/docs/start-the-server.md +++ /dev/null @@ -1,45 +0,0 @@ -# Start the server - -turn-server has only one command line parameter `--config`, which is used to run the server by specifying a configuration file. The detailed information of the configuration file can be found in [configure](./configure.md). - -```bash -turn-server --config ./turn-server.toml -``` - -Starting the service is that simple. - -### Linux service - -If you need to run turn-rs as a systemd service, first, create a service description file: - -```bash -vim /etc/systemd/system/turn-server.service -``` - -Enter the following content in the service description file: - -```ini -[Unit] -Description=A pure rust-implemented turn server. -After=network.target - -[Service] -Type=simple -Restart=always -ExecStart=/usr/local/bin/turn-server --config=/etc/turn-server/config.toml - -[Install] -WantedBy=multi-user.target -``` - -`ExecStart` can be adjusted according to your actual situation, but it is recommended to place the corresponding file in the location of the above example. - -Next, set the service to start automatically by default and start the service: - -```bash -systemctl daemon-reload -systemctl enable turn-server -systemctl start turn-server -``` - -You can use `systemctl status turn-server` to view the startup status of the service. diff --git a/install-service.sh b/install-service.sh index 6d693580..e2c2ba79 100644 --- a/install-service.sh +++ b/install-service.sh @@ -11,8 +11,8 @@ if [ ! -d "/etc/turn-server" ]; then mkdir /etc/turn-server fi -if [ ! -f "/etc/turn-server/config.toml" ]; then - cp ./turn-server.toml /etc/turn-server/config.toml +if [ ! -f "/etc/turn-server/config.json" ]; then + cp ./turn-server.json /etc/turn-server/config.json fi if [ ! -f "/etc/systemd/system/turn-server.service" ]; then diff --git a/protos/server.proto b/protos/server.proto new file mode 100644 index 00000000..ffa53587 --- /dev/null +++ b/protos/server.proto @@ -0,0 +1,128 @@ +syntax = "proto3"; + +package turn.server; + +import "google/protobuf/empty.proto"; + +// turn server info +message TurnServerInfo { + // software info + string software = 1; + // uptime seconds + uint64 uptime = 2; + // interfaces + repeated string interfaces = 3; + // port capacity size + uint32 port_capacity = 4; + // port allocated size + uint32 port_allocated = 5; +} + +// session query params +message SessionQueryParams { + string id = 1; +} + +// turn session +message TurnSession { + string username = 1; + // permissions port + repeated int32 permissions = 2; + // channels number + repeated int32 channels = 3; + // allocated port number + optional int32 port = 4; + // expires seconds + int64 expires = 6; +} + +// turn session statistics +message TurnSessionStatistics { + // received bytes + uint64 received_bytes = 1; + // send bytes + uint64 send_bytes = 2; + // received packets + uint64 received_pkts = 3; + // send packets + uint64 send_pkts = 4; + // error packets + uint64 error_pkts = 5; +} + +// turn service +service TurnService { + // get server info + rpc GetInfo(google.protobuf.Empty) returns (TurnServerInfo); + // get session + rpc GetSession(SessionQueryParams) returns (TurnSession); + // get session statistics + rpc GetSessionStatistics(SessionQueryParams) returns (TurnSessionStatistics); + // destroy session + rpc DestroySession(SessionQueryParams) returns (google.protobuf.Empty); +} + +// turn allocated event +message TurnAllocatedEvent { + string id = 1; + string username = 2; + int32 port = 3; +} + +// turn channel bind event +message TurnChannelBindEvent { + string id = 1; + string username = 2; + int32 channel = 3; +} + +// turn create permission event +message TurnCreatePermissionEvent { + string id = 1; + string username = 2; + repeated int32 ports = 3; +} + +// turn refresh event +message TurnRefreshEvent { + string id = 1; + string username = 2; + int32 lifetime = 3; +} + +// turn destroy event +message TurnDestroyEvent { + string id = 1; + string username = 2; +} + +enum PasswordAlgorithm { + UNSPECIFIED = 0; + MD5 = 1; + SHA256 = 2; +} + +// get turn message integrity request +message GetTurnPasswordRequest { + PasswordAlgorithm algorithm = 1; + string username = 2; +} + +// get turn message integrity response +message GetTurnPasswordResponse { + bytes password = 2; +} + +// turn hooks service +service TurnHooksService { + + // hooks + rpc GetPassword(GetTurnPasswordRequest) returns (GetTurnPasswordResponse); + + // events + rpc OnAllocatedEvent(TurnAllocatedEvent) returns (google.protobuf.Empty); + rpc OnChannelBindEvent(TurnChannelBindEvent) returns (google.protobuf.Empty); + rpc OnCreatePermissionEvent(TurnCreatePermissionEvent) returns (google.protobuf.Empty); + rpc OnRefreshEvent(TurnRefreshEvent) returns (google.protobuf.Empty); + rpc OnDestroyEvent(TurnDestroyEvent) returns (google.protobuf.Empty); +} diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 22e3af63..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1,4 +0,0 @@ -max_width = 120 -wrap_comments = true -format_code_in_doc_comments = true -doc_comment_code_block_width = 80 diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml new file mode 100644 index 00000000..9db19ede --- /dev/null +++ b/sdk/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "turn-server-sdk" +version = "0.1.0" +edition = "2024" + +[dependencies] +tonic = { version = "0.14", features = ["deflate"] } +prost = "0.14" +tonic-prost = "0.14" +codec = { workspace = true } + +[build-dependencies] +tonic-prost-build = "0.14" diff --git a/sdk/build.rs b/sdk/build.rs new file mode 100644 index 00000000..43a4536e --- /dev/null +++ b/sdk/build.rs @@ -0,0 +1,7 @@ +fn main() { + tonic_prost_build::configure() + .build_server(true) + .build_client(true) + .compile_protos(&["../protos/server.proto"], &["../protos"]) + .unwrap(); +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs new file mode 100644 index 00000000..4984cba6 --- /dev/null +++ b/sdk/src/lib.rs @@ -0,0 +1,282 @@ +pub use tonic; + +pub mod proto { + tonic::include_proto!("turn.server"); +} + +use std::net::SocketAddr; + +pub use codec::message::attributes::PasswordAlgorithm; + +use tonic::{ + Request, Response, Status, + transport::{Channel, Server}, +}; + +use crate::proto::{ + GetTurnPasswordRequest, GetTurnPasswordResponse, SessionQueryParams, TurnAllocatedEvent, + TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent, TurnRefreshEvent, + TurnServerInfo, TurnSession, TurnSessionStatistics, + turn_hooks_service_server::{TurnHooksService, TurnHooksServiceServer}, + turn_service_client::TurnServiceClient, +}; + +impl TryInto for proto::PasswordAlgorithm { + type Error = Status; + + fn try_into(self) -> Result { + Ok(match self { + proto::PasswordAlgorithm::Md5 => PasswordAlgorithm::Md5, + proto::PasswordAlgorithm::Sha256 => PasswordAlgorithm::Sha256, + proto::PasswordAlgorithm::Unspecified => { + return Err(Status::invalid_argument("Invalid password algorithm")); + } + }) + } +} + +/// turn service client +/// +/// This struct is used to interact with the turn service. +pub struct TurnService(TurnServiceClient); + +impl TurnService { + /// create a new turn service client + pub fn new(channel: Channel) -> Self { + Self(TurnServiceClient::new(channel)) + } + + /// get the server info + pub async fn get_info(&mut self) -> Result { + Ok(self.0.get_info(Request::new(())).await?.into_inner()) + } + + /// get the session + pub async fn get_session(&mut self, id: String) -> Result { + Ok(self + .0 + .get_session(Request::new(SessionQueryParams { id })) + .await? + .into_inner()) + } + + /// get the session statistics + pub async fn get_session_statistics( + &mut self, + id: String, + ) -> Result { + Ok(self + .0 + .get_session_statistics(Request::new(SessionQueryParams { id })) + .await? + .into_inner()) + } + + /// destroy the session + pub async fn destroy_session(&mut self, id: String) -> Result<(), Status> { + Ok(self + .0 + .destroy_session(Request::new(SessionQueryParams { id })) + .await? + .into_inner()) + } +} + +/// credential +/// +/// This struct is used to store the credential for the turn hooks server. +pub struct Credential<'a> { + pub password: &'a str, + pub realm: &'a str, +} + +struct TurnHooksServerInner(T); + +#[tonic::async_trait] +impl TurnHooksService for TurnHooksServerInner { + async fn get_password( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + let algorithm = request.algorithm().try_into()?; + + if let Ok(credential) = self.0.get_password(&request.username, algorithm).await { + Ok(Response::new(GetTurnPasswordResponse { + password: codec::crypto::generate_password( + &request.username, + credential.password, + credential.realm, + algorithm, + ) + .to_vec(), + })) + } else { + Err(Status::not_found("Message integrity not found")) + } + } + + async fn on_allocated_event( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + self.0 + .on_allocated(request.id, request.username, request.port as u16) + .await; + + Ok(Response::new(())) + } + + async fn on_channel_bind_event( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + self.0 + .on_channel_bind(request.id, request.username, request.channel as u16) + .await; + + Ok(Response::new(())) + } + + async fn on_create_permission_event( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + self.0 + .on_create_permission( + request.id, + request.username, + request.ports.iter().map(|p| *p as u16).collect(), + ) + .await; + + Ok(Response::new(())) + } + + async fn on_refresh_event( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + self.0 + .on_refresh(request.id, request.username, request.lifetime as u32) + .await; + + Ok(Response::new(())) + } + + async fn on_destroy_event( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + self.0.on_destroy(request.id, request.username).await; + + Ok(Response::new(())) + } +} + +#[tonic::async_trait] +pub trait TurnHooksServer: Send + Sync { + #[allow(unused_variables)] + async fn get_password( + &self, + username: &str, + algorithm: PasswordAlgorithm, + ) -> Result { + Err(Status::unimplemented("get_password is not implemented")) + } + + /// allocate request + /// + /// [rfc8489](https://tools.ietf.org/html/rfc8489) + /// + /// In all cases, the server SHOULD only allocate ports from the range + /// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]), + /// unless the TURN server application knows, through some means not + /// specified here, that other applications running on the same host as + /// the TURN server application will not be impacted by allocating ports + /// outside this range. This condition can often be satisfied by running + /// the TURN server application on a dedicated machine and/or by + /// arranging that any other applications on the machine allocate ports + /// before the TURN server application starts. In any case, the TURN + /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well- + /// Known Port range) to discourage clients from using TURN to run + /// standard services. + #[allow(unused_variables)] + async fn on_allocated(&self, id: String, username: String, port: u16) {} + + /// channel bind request + /// + /// [rfc8489](https://tools.ietf.org/html/rfc8489) + /// + /// If the request is valid, but the server is unable to fulfill the + /// request due to some capacity limit or similar, the server replies + /// with a 508 (Insufficient Capacity) error. + /// + /// Otherwise, the server replies with a ChannelBind success response. + /// There are no required attributes in a successful ChannelBind + /// response. + #[allow(unused_variables)] + async fn on_channel_bind(&self, id: String, username: String, channel: u16) {} + + /// create permission request + /// + /// [rfc8489](https://tools.ietf.org/html/rfc8489) + /// + /// If the request is valid, but the server is unable to fulfill the + /// request due to some capacity limit or similar, the server replies + /// with a 508 (Insufficient Capacity) error. + /// + /// Otherwise, the server replies with a ChannelBind success response. + /// There are no required attributes in a successful ChannelBind + /// response. + #[allow(unused_variables)] + async fn on_create_permission(&self, id: String, username: String, ports: Vec) {} + + /// refresh request + /// + /// [rfc8489](https://tools.ietf.org/html/rfc8489) + /// + /// If the request is valid, but the server is unable to fulfill the + /// request due to some capacity limit or similar, the server replies + /// with a 508 (Insufficient Capacity) error. + /// + /// Otherwise, the server replies with a ChannelBind success response. + /// There are no required attributes in a successful ChannelBind + /// response. + #[allow(unused_variables)] + async fn on_refresh(&self, id: String, username: String, lifetime: u32) {} + + /// session closed + /// + /// Triggered when the session leaves from the turn. Possible reasons: the + /// session life cycle has expired, external active deletion, or active + /// exit of the session. + #[allow(unused_variables)] + async fn on_destroy(&self, id: String, username: String) {} + + /// start the turn hooks server + /// + /// This function will start the turn hooks server on the given server and listen address. + async fn start_with_server( + self, + server: &mut Server, + listen: SocketAddr, + ) -> Result<(), tonic::transport::Error> + where + Self: Sized + 'static, + { + server + .add_service(TurnHooksServiceServer::>::new( + TurnHooksServerInner(self), + )) + .serve(listen) + .await?; + + Ok(()) + } +} diff --git a/src/api.rs b/src/api.rs deleted file mode 100644 index ee683008..00000000 --- a/src/api.rs +++ /dev/null @@ -1,188 +0,0 @@ -use std::{net::SocketAddr, sync::Arc, time::Instant}; - -use axum::{ - Json, Router, - extract::{Query, State}, - http::StatusCode, - response::{IntoResponse, Sse, sse::KeepAlive}, - routing::{delete, get}, -}; - -use serde::Deserialize; -use serde_json::json; -use tokio::net::TcpListener; - -use crate::{ - config::Config, - observer::Observer, - statistics::Statistics, - turn::{PortAllocatePools, Service, SessionAddr}, -}; - -struct ApiState { - config: Arc, - service: Service, - statistics: Statistics, - uptime: Instant, -} - -#[derive(Deserialize)] -struct QueryParams { - address: SocketAddr, - interface: SocketAddr, -} - -impl Into for QueryParams { - fn into(self) -> SessionAddr { - SessionAddr { - address: self.address, - interface: self.interface, - } - } -} - -pub mod events { - use std::sync::LazyLock; - - use axum::response::sse::Event; - use serde::Serialize; - use tokio::sync::broadcast::{Sender, channel}; - use tokio_stream::wrappers::BroadcastStream; - - static CHANNEL: LazyLock> = LazyLock::new(|| channel(10).0); - - pub fn get_event_stream() -> BroadcastStream { - BroadcastStream::new(CHANNEL.subscribe()) - } - - pub fn send_with_stream(event: &str, handle: F) - where - F: FnOnce() -> T, - T: Serialize, - { - if CHANNEL.receiver_count() > 0 { - let _ = CHANNEL.send(Event::default().event(event).json_data(handle()).unwrap()); - } - } -} - -/// start http server -/// -/// Create an http server and start it, and you can access the controller -/// instance through the http interface. -/// -/// Warn: This http server does not contain -/// any means of authentication, and sensitive information and dangerous -/// operations can be obtained through this service, please do not expose it -/// directly to an unsafe environment. -pub async fn start_server( - config: Arc, - service: Service, - statistics: Statistics, -) -> anyhow::Result<()> { - let state = Arc::new(ApiState { - config: config.clone(), - uptime: Instant::now(), - service, - statistics, - }); - - #[allow(unused_mut)] - let mut app = Router::new() - .route( - "/info", - get(|State(app_state): State>| async move { - let sessions = app_state.service.get_sessions(); - Json(json!({ - "software": crate::SOFTWARE, - "uptime": app_state.uptime.elapsed().as_secs(), - "interfaces": app_state.config.turn.interfaces, - "port_capacity": PortAllocatePools::capacity(), - "port_allocated": sessions.allocated(), - })) - }), - ) - .route( - "/session", - get( - |Query(query): Query, State(state): State>| async move { - if let Some(session) = state.service.get_sessions().get_session(&query.into()).get_ref() { - Json(json!({ - "username": session.auth.username, - "permissions": session.permissions, - "channels": session.allocate.channels, - "port": session.allocate.port, - "expires": session.expires, - })) - .into_response() - } else { - StatusCode::NOT_FOUND.into_response() - } - }, - ), - ) - .route( - "/session/statistics", - get( - |Query(query): Query, State(state): State>| async move { - let addr: SessionAddr = query.into(); - if let Some(counts) = state.statistics.get(&addr) { - Json(json!({ - "received_bytes": counts.received_bytes, - "send_bytes": counts.send_bytes, - "received_pkts": counts.received_pkts, - "send_pkts": counts.send_pkts, - "error_pkts": counts.error_pkts, - })) - .into_response() - } else { - StatusCode::NOT_FOUND.into_response() - } - }, - ), - ) - .route( - "/session", - delete( - |Query(query): Query, State(state): State>| async move { - if state.service.get_sessions().refresh(&query.into(), 0) { - StatusCode::OK - } else { - StatusCode::EXPECTATION_FAILED - } - }, - ), - ) - .route( - "/events", - get(|| async move { Sse::new(events::get_event_stream()).keep_alive(KeepAlive::default()) }), - ); - - #[cfg(feature = "prometheus")] - { - use crate::statistics::prometheus::generate_metrics; - use axum::http::header::CONTENT_TYPE; - - let mut metrics_bytes = Vec::with_capacity(4096); - - app = app.route( - "/metrics", - get(|| async move { - metrics_bytes.clear(); - - if generate_metrics(&mut metrics_bytes).is_err() { - StatusCode::EXPECTATION_FAILED.into_response() - } else { - ([(CONTENT_TYPE, "text/plain")], metrics_bytes).into_response() - } - }), - ); - } - - let listener = TcpListener::bind(config.api.bind).await?; - - log::info!("api server listening={:?}", &config.api.bind); - - axum::serve(listener, app.with_state(state)).await?; - Ok(()) -} diff --git a/src/config.rs b/src/config.rs index db70aea0..315b8bfb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,135 +1,216 @@ use std::{collections::HashMap, fs::read_to_string, net::SocketAddr, str::FromStr}; -use anyhow::anyhow; +use anyhow::Result; use clap::Parser; -use itertools::Itertools; use serde::{Deserialize, Serialize}; +use service::session::ports::PortRange; -#[repr(C)] -#[derive(Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum Transport { - TCP = 0, - UDP = 1, -} - -impl FromStr for Transport { - type Err = anyhow::Error; - - fn from_str(value: &str) -> Result { - Ok(match value { - "udp" => Self::UDP, - "tcp" => Self::TCP, - _ => return Err(anyhow!("unknown transport: {value}")), - }) - } +/// SSL configuration +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Ssl { + /// + /// SSL private key file + /// + pub private_key: String, + /// + /// SSL certificate chain file + /// + pub certificate_chain: String, } #[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Interface { - pub transport: Transport, - /// turn server listen address - pub bind: SocketAddr, - /// external address - /// - /// specify the node external address and port. - /// for the case of exposing the service to the outside, - /// you need to manually specify the server external IP - /// address and service listening port. - pub external: SocketAddr, +#[serde(tag = "transport", rename_all = "kebab-case")] +pub enum Interface { + Tcp { + listen: SocketAddr, + /// + /// external address + /// + /// specify the node external address and port. + /// for the case of exposing the service to the outside, + /// you need to manually specify the server external IP + /// address and service listening port. + /// + external: SocketAddr, + /// + /// Idle timeout + /// + /// If no packet is received within the specified number of seconds, the + /// connection will be closed to prevent resources from being occupied + /// for a long time. + #[serde(default = "Interface::idle_timeout")] + idle_timeout: u32, + /// + /// SSL configuration + /// + #[serde(default)] + ssl: Option, + }, + Udp { + listen: SocketAddr, + /// + /// external address + /// + /// specify the node external address and port. + /// for the case of exposing the service to the outside, + /// you need to manually specify the server external IP + /// address and service listening port. + /// + external: SocketAddr, + /// + /// Idle timeout + /// + /// If no packet is received within the specified number of seconds, the + /// connection will be closed to prevent resources from being occupied + /// for a long time. + #[serde(default = "Interface::idle_timeout")] + idle_timeout: u32, + /// + /// Maximum Transmission Unit (MTU) size for network packets. + /// + #[serde(default = "Interface::mtu")] + mtu: usize, + }, } -impl FromStr for Interface { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let (transport, addrs) = s - .split('@') - .collect_tuple() - .ok_or_else(|| anyhow!("invalid interface transport: {}", s))?; - - let (bind, external) = addrs - .split('/') - .collect_tuple() - .ok_or_else(|| anyhow!("invalid interface address: {}", s))?; +impl Interface { + fn mtu() -> usize { + 1500 + } - Ok(Interface { - external: external.parse::()?, - bind: bind.parse::()?, - transport: transport.parse()?, - }) + fn idle_timeout() -> u32 { + 20 } } -#[derive(Deserialize, Debug)] -pub struct Turn { +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Server { + /// + /// Port range, the maximum range is 65535 - 49152. + /// + #[serde(default = "Server::port_range")] + pub port_range: PortRange, + /// + /// Maximum number of threads the TURN server can use. + /// + #[serde(default = "Server::max_threads")] + pub max_threads: usize, + /// /// turn server realm /// /// specify the domain where the server is located. /// for a single node, this configuration is fixed, /// but each node can be configured as a different domain. /// this is a good idea to divide the nodes by namespace. - #[serde(default = "Turn::realm")] + /// + #[serde(default = "Server::realm")] pub realm: String, - + /// /// turn server listen interfaces /// /// The address and port to which the UDP Server is bound. Multiple /// addresses can be bound at the same time. The binding address supports /// ipv4 and ipv6. - #[serde(default = "Turn::interfaces")] + /// + #[serde(default)] pub interfaces: Vec, } -impl Turn { +impl Server { pub fn get_externals(&self) -> Vec { - self.interfaces.iter().map(|item| item.external).collect() + self.interfaces + .iter() + .map(|item| match item { + Interface::Tcp { external, .. } => *external, + Interface::Udp { external, .. } => *external, + }) + .collect() } } -impl Turn { +impl Server { fn realm() -> String { "localhost".to_string() } - fn interfaces() -> Vec { - vec![] + fn port_range() -> PortRange { + PortRange::default() + } + + fn max_threads() -> usize { + num_cpus::get() } } -impl Default for Turn { +impl Default for Server { fn default() -> Self { Self { realm: Self::realm(), - interfaces: Self::interfaces(), + interfaces: Default::default(), + port_range: Self::port_range(), + max_threads: Self::max_threads(), } } } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct Hooks { + #[serde(default = "Hooks::max_channel_size")] + pub max_channel_size: usize, + pub endpoint: String, + #[serde(default)] + pub ssl: Option, + #[serde(default = "Hooks::timeout")] + pub timeout: u32, +} + +impl Hooks { + fn max_channel_size() -> usize { + 1024 + } + + fn timeout() -> u32 { + 5 + } +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "kebab-case")] pub struct Api { - /// api bind /// - /// This option specifies the http server binding address used to control + /// rpc server listen + /// + /// This option specifies the rpc server binding address used to control /// the turn server. /// - /// Warn: This http server does not contain any means of authentication, - /// and sensitive information and dangerous operations can be obtained - /// through this service, please do not expose it directly to an unsafe - /// environment. #[serde(default = "Api::bind")] - pub bind: SocketAddr, + pub listen: SocketAddr, + #[serde(default)] + pub ssl: Option, + #[serde(default = "Api::timeout")] + pub timeout: u32, } impl Api { fn bind() -> SocketAddr { "127.0.0.1:3000".parse().unwrap() } + + fn timeout() -> u32 { + 5 + } } impl Default for Api { fn default() -> Self { - Self { bind: Self::bind() } + Self { + timeout: Self::timeout(), + listen: Self::bind(), + ssl: None, + } } } @@ -176,40 +257,54 @@ impl LogLevel { } } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case")] pub struct Log { + /// /// log level /// /// An enum representing the available verbosity levels of the logger. + /// #[serde(default)] pub level: LogLevel, } -#[derive(Deserialize, Debug, Default)] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case")] pub struct Auth { + /// /// static user password /// - /// This option can be used to specify the - /// static identity authentication information used by the turn server for - /// verification. Note: this is a high-priority authentication method, turn - /// The server will try to use static authentication first, and then use - /// external control service authentication. + /// This option can be used to specify the static identity authentication + /// information used by the turn server for verification. + /// + /// Note: this is a high-priority authentication method, turn The server will + /// try to use static authentication first, and then use external control + /// service authentication. + /// #[serde(default)] pub static_credentials: HashMap, + /// /// Static authentication key value (string) that applies only to the TURN /// REST API. /// /// If set, the turn server will not request external services via the HTTP /// Hooks API to obtain the key. + /// pub static_auth_secret: Option, + #[serde(default)] + pub enable_hooks_auth: bool, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default, Clone)] +#[serde(rename_all = "kebab-case")] pub struct Config { #[serde(default)] - pub turn: Turn, + pub server: Server, + #[serde(default)] + pub api: Option, #[serde(default)] - pub api: Api, + pub hooks: Option, #[serde(default)] pub log: Log, #[serde(default)] @@ -223,119 +318,26 @@ pub struct Config { author = env!("CARGO_PKG_AUTHORS"), )] struct Cli { + /// /// Specify the configuration file path /// - /// Example: --config /etc/turn-rs/config.toml - #[arg(long, short)] - config: Option, - /// Static user password + /// Example: turn-server --config /etc/turn-rs/config.toml /// - /// Example: --auth-static-credentials test=test - #[arg(long, value_parser = Cli::parse_credential)] - auth_static_credentials: Option>, - /// Static authentication key value (string) that applies only to the TURN - /// REST API - #[arg(long)] - auth_static_auth_secret: Option, - /// An enum representing the available verbosity levels of the logger - #[arg( - long, - value_parser = clap::value_parser!(LogLevel), - )] - log_level: Option, - /// This option specifies the http server binding address used to control - /// the turn server - #[arg(long)] - api_bind: Option, - /// TURN server realm - #[arg(long)] - turn_realm: Option, - /// TURN server listen interfaces - /// - /// Example: --turn-interfaces udp@127.0.0.1:3478/127.0.0.1:3478 - #[arg(long)] - turn_interfaces: Option>, -} - -impl Cli { - // [username]:[password] - fn parse_credential(s: &str) -> Result<(String, String), anyhow::Error> { - let (username, password) = s - .split('=') - .collect_tuple() - .ok_or_else(|| anyhow!("invalid credential str: {}", s))?; - Ok((username.to_string(), password.to_string())) - } + #[arg(long, short)] + config: String, } impl Config { + /// /// Load configure from config file and command line parameters. /// - /// Load command line parameters, if the configuration file path is - /// specified, the configuration is read from the configuration file, - /// otherwise the default configuration is used. - pub fn load() -> anyhow::Result { - let cli = Cli::parse(); - let mut config = toml::from_str::( - &cli.config - .and_then(|path| read_to_string(path).ok()) - .unwrap_or("".to_string()), - )?; - - // Command line arguments have a high priority and override configuration file - // options; here they are used to replace the configuration parsed out of the - // configuration file. - { - if let Some(credentials) = cli.auth_static_credentials { - for (k, v) in credentials { - config.auth.static_credentials.insert(k, v); - } - } - - if let Some(secret) = cli.auth_static_auth_secret { - config.auth.static_auth_secret.replace(secret); - } - - if let Some(level) = cli.log_level { - config.log.level = level; - } - - if let Some(bind) = cli.api_bind { - config.api.bind = bind; - } - - if let Some(realm) = cli.turn_realm { - config.turn.realm = realm; - } - - if let Some(interfaces) = cli.turn_interfaces { - for interface in interfaces { - config.turn.interfaces.push(interface); - } - } - } - - // Filters out transport protocols that are not enabled. - { - let mut interfaces = Vec::with_capacity(config.turn.interfaces.len()); - - { - for it in &config.turn.interfaces { - #[cfg(feature = "udp")] - if it.transport == Transport::UDP { - interfaces.push(it.clone()); - } - - #[cfg(feature = "tcp")] - if it.transport == Transport::TCP { - interfaces.push(it.clone()); - } - } - } - - config.turn.interfaces = interfaces; - } - - Ok(config) + /// Load command line parameters, if the configuration file path is specified, + /// the configuration is read from the configuration file, otherwise the + /// default configuration is used. + /// + pub fn load() -> Result { + Ok(toml::from_str::(&read_to_string( + &Cli::parse().config, + )?)?) } } diff --git a/src/observer.rs b/src/handler.rs similarity index 64% rename from src/observer.rs rename to src/handler.rs index de82de6b..67d36883 100644 --- a/src/observer.rs +++ b/src/handler.rs @@ -1,55 +1,68 @@ +#[cfg(feature = "rpc")] use std::sync::Arc; -use crate::{config::Config, statistics::Statistics, turn::SessionAddr}; +use crate::{config::Config, statistics::Statistics}; -use anyhow::Result; -use base64::{Engine, prelude::BASE64_STANDARD}; +#[cfg(feature = "rpc")] +use crate::rpc::{ + HooksEvent, IdString, RpcHooksService, + proto::{ + TurnAllocatedEvent, TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent, + TurnRefreshEvent, + }, +}; -#[cfg(feature = "api")] -use serde_json::json; +use anyhow::Result; +use codec::{crypto::Password, message::attributes::PasswordAlgorithm}; +use service::{ServiceHandler, session::Identifier}; #[derive(Clone)] -pub struct Observer { - config: Arc, - #[cfg(feature = "api")] +pub struct Handler { + config: Config, + #[cfg(feature = "rpc")] statistics: Statistics, + #[cfg(feature = "rpc")] + rpc: Arc, } -impl Observer { +impl Handler { #[allow(unused_variables)] - pub async fn new(config: Arc, statistics: Statistics) -> Result { + pub async fn new(config: Config, statistics: Statistics) -> Result { Ok(Self { - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] + rpc: RpcHooksService::new(&config).await?.into(), + #[cfg(feature = "rpc")] statistics, config, }) } } -impl crate::turn::Observer for Observer { - fn get_password(&self, username: &str) -> Option { +impl ServiceHandler for Handler { + async fn get_password(&self, username: &str, algorithm: PasswordAlgorithm) -> Option { // Match the static authentication information first. - if let Some(it) = self.config.auth.static_credentials.get(username) { - return Some(it.clone()); + if let Some(password) = self.config.auth.static_credentials.get(username) { + return Some(codec::crypto::generate_password( + username, + password, + &self.config.server.realm, + algorithm, + )); } // Try again to match the static authentication key. - if let Some(it) = &self.config.auth.static_auth_secret { - // Because (TURN REST api) this RFC does not mandate the format of the username, - // only suggested values. In principle, the RFC also indicates that the - // timestamp part of username can be set at will, so the timestamp is not - // verified here, and the external web service guarantees its security by - // itself. - // - // https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00#section-2.2 - return Some( - BASE64_STANDARD.encode( - crate::stun::util::hmac_sha1(it.as_bytes(), &[username.as_bytes()]) - .ok()? - .into_bytes() - .as_slice(), - ), - ); + if let Some(secret) = &self.config.auth.static_auth_secret { + return Some(codec::crypto::static_auth_secret( + username, + secret, + &self.config.server.realm, + algorithm, + )); + } + + #[cfg(feature = "rpc")] + if self.config.auth.enable_hooks_auth { + return self.rpc.get_password(username, algorithm).await; } None @@ -71,30 +84,25 @@ impl crate::turn::Observer for Observer { /// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well- /// Known Port range) to discourage clients from using TURN to run /// standard services. - #[allow(clippy::let_underscore_future)] - fn allocated(&self, addr: &SessionAddr, name: &str, port: u16) { + fn on_allocated(&self, id: &Identifier, name: &str, port: u16) { log::info!( "allocate: address={:?}, interface={:?}, username={:?}, port={}", - addr.address, - addr.interface, + id.source, + id.interface, name, port ); - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] { - self.statistics.register(*addr); + self.statistics.register(*id); - crate::api::events::send_with_stream("allocated", || { - json!({ - "session": { - "address": addr.address, - "interface": addr.interface, - }, - "username": name, - "port": port, - }) - }); + self.rpc + .send_event(HooksEvent::Allocated(TurnAllocatedEvent { + id: id.to_string(), + username: name.to_string(), + port: port as i32, + })); } } @@ -128,28 +136,23 @@ impl crate::turn::Observer for Observer { /// different channel, eliminating the possibility that the /// transaction would initially fail but succeed on a /// retransmission. - #[allow(clippy::let_underscore_future)] - fn channel_bind(&self, addr: &SessionAddr, name: &str, channel: u16) { + fn on_channel_bind(&self, id: &Identifier, name: &str, channel: u16) { log::info!( "channel bind: address={:?}, interface={:?}, username={:?}, channel={}", - addr.address, - addr.interface, + id.source, + id.interface, name, channel ); - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] { - crate::api::events::send_with_stream("channel_bind", || { - json!({ - "session": { - "address": addr.address, - "interface": addr.interface, - }, - "username": name, - "channel": channel, - }) - }); + self.rpc + .send_event(HooksEvent::ChannelBind(TurnChannelBindEvent { + id: id.to_string(), + username: name.to_string(), + channel: channel as i32, + })); } } @@ -188,32 +191,27 @@ impl crate::turn::Observer for Observer { /// The server then responds with a CreatePermission success response. /// There are no mandatory attributes in the success response. /// - /// > NOTE: A server need not do anything special to implement + /// NOTE: A server need not do anything special to implement /// idempotency of CreatePermission requests over UDP using the /// "stateless stack approach". Retransmitted CreatePermission /// requests will simply refresh the permissions. - #[allow(clippy::let_underscore_future)] - fn create_permission(&self, addr: &SessionAddr, name: &str, ports: &[u16]) { + fn on_create_permission(&self, id: &Identifier, name: &str, ports: &[u16]) { log::info!( "create permission: address={:?}, interface={:?}, username={:?}, ports={:?}", - addr.address, - addr.interface, + id.source, + id.interface, name, ports ); - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] { - crate::api::events::send_with_stream("create_permission", || { - json!({ - "session": { - "address": addr.address, - "interface": addr.interface, - }, - "username": name, - "ports": ports, - }) - }); + self.rpc + .send_event(HooksEvent::CreatePermission(TurnCreatePermissionEvent { + id: id.to_string(), + username: name.to_string(), + ports: ports.iter().map(|p| *p as i32).collect(), + })); } } @@ -236,17 +234,17 @@ impl crate::turn::Observer for Observer { /// Subsequent processing depends on the "desired lifetime" value: /// /// * If the "desired lifetime" is zero, then the request succeeds and - /// the allocation is deleted. + /// the allocation is deleted. /// /// * If the "desired lifetime" is non-zero, then the request succeeds - /// and the allocation's time-to-expiry is set to the "desired - /// lifetime". + /// and the allocation's time-to-expiry is set to the "desired + /// lifetime". /// /// If the request succeeds, then the server sends a success response /// containing: /// /// * A LIFETIME attribute containing the current value of the time-to- - /// expiry timer. + /// expiry timer. /// /// NOTE: A server need not do anything special to implement /// idempotency of Refresh requests over UDP using the "stateless @@ -256,28 +254,22 @@ impl crate::turn::Observer for Observer { /// will cause a 437 (Allocation Mismatch) response if the /// allocation has already been deleted, but the client will treat /// this as equivalent to a success response (see below). - #[allow(clippy::let_underscore_future)] - fn refresh(&self, addr: &SessionAddr, name: &str, lifetime: u32) { + fn on_refresh(&self, id: &Identifier, name: &str, lifetime: u32) { log::info!( "refresh: address={:?}, interface={:?}, username={:?}, lifetime={}", - addr.address, - addr.interface, + id.source, + id.interface, name, lifetime ); - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] { - crate::api::events::send_with_stream("refresh", || { - json!({ - "session": { - "address": addr.address, - "interface": addr.interface, - }, - "username": name, - "lifetime": lifetime, - }) - }); + self.rpc.send_event(HooksEvent::Refresh(TurnRefreshEvent { + id: id.to_string(), + username: name.to_string(), + lifetime: lifetime as i32, + })); } } @@ -286,28 +278,22 @@ impl crate::turn::Observer for Observer { /// Triggered when the session leaves from the turn. Possible reasons: the /// session life cycle has expired, external active deletion, or active /// exit of the session. - #[allow(clippy::let_underscore_future)] - fn closed(&self, addr: &SessionAddr, name: &str) { + fn on_destroy(&self, id: &Identifier, name: &str) { log::info!( "closed: address={:?}, interface={:?}, username={:?}", - addr.address, - addr.interface, + id.source, + id.interface, name ); - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] { - self.statistics.unregister(&addr); + self.statistics.unregister(id); - crate::api::events::send_with_stream("closed", || { - json!({ - "session": { - "address": addr.address, - "interface": addr.interface, - }, - "username": name, - }) - }); + self.rpc.send_event(HooksEvent::Destroy(TurnDestroyEvent { + id: id.to_string(), + username: name.to_string(), + })); } } } diff --git a/src/lib.rs b/src/lib.rs index fbd92fbf..b4e83cc5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,15 @@ -pub mod api; +#[cfg(feature = "rpc")] +pub mod rpc; + pub mod config; -pub mod observer; -pub mod router; +pub mod handler; pub mod server; pub mod statistics; -pub mod stun; -pub mod turn; -use std::sync::Arc; +use self::{config::Config, handler::Handler, statistics::Statistics}; -use self::{config::Config, observer::Observer, statistics::Statistics, turn::Service}; +use service::ServiceOptions; +use tokio::task::JoinSet; #[rustfmt::skip] static SOFTWARE: &str = concat!( @@ -17,30 +17,38 @@ static SOFTWARE: &str = concat!( env!("CARGO_PKG_VERSION") ); +pub(crate) type Service = service::Service; + /// In order to let the integration test directly use the turn-server crate and /// start the server, a function is opened to replace the main function to /// directly start the server. -pub async fn startup(config: Arc) -> anyhow::Result<()> { +pub async fn start_server(config: Config) -> anyhow::Result<()> { let statistics = Statistics::default(); - let service = Service::new( - SOFTWARE.to_string(), - config.turn.realm.clone(), - config.turn.get_externals(), - Observer::new(config.clone(), statistics.clone()).await?, - ); + let service = service::Service::new(ServiceOptions { + software: SOFTWARE.to_string(), + realm: config.server.realm.clone(), + port_range: config.server.port_range, + interfaces: config.server.get_externals(), + handler: Handler::new(config.clone(), statistics.clone()).await?, + }); - server::start(&config, &statistics, &service).await?; - - #[cfg(feature = "api")] { - api::start_server(config, service, statistics).await?; - } + let mut workers = JoinSet::new(); - // The turn server is non-blocking after it runs and needs to be kept from - // exiting immediately if the api server is not enabled. - #[cfg(not(feature = "api"))] - { - std::future::pending::<()>().await; + workers.spawn(server::start_server( + config.clone(), + service.clone(), + statistics.clone(), + )); + + #[cfg(feature = "rpc")] + workers.spawn(rpc::start_server(config, service, statistics)); + + if let Some(res) = workers.join_next().await { + workers.abort_all(); + + return res?; + } } Ok(()) diff --git a/src/main.rs b/src/main.rs index 50dce071..9adf72ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,13 @@ #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; -use std::sync::Arc; - use turn_server::config::Config; -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let config = Arc::new(Config::load()?); +fn main() -> anyhow::Result<()> { + let config = Config::load()?; simple_logger::init_with_level(config.log.level.as_level())?; - if config.turn.interfaces.is_empty() { + if config.server.interfaces.is_empty() { log::warn!( "No interfaces are bound, no features are enabled, it's just a program without any functionality :-)" ); @@ -18,5 +15,9 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } - turn_server::startup(config).await + tokio::runtime::Builder::new_multi_thread() + .worker_threads(config.server.max_threads) + .enable_all() + .build()? + .block_on(turn_server::start_server(config)) } diff --git a/src/router.rs b/src/router.rs deleted file mode 100644 index 4e9794eb..00000000 --- a/src/router.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{net::SocketAddr, sync::Arc}; - -use ahash::AHashMap; -use parking_lot::RwLock; -use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}; - -use crate::turn::ResponseMethod; - -type Receiver = UnboundedSender<(Vec, ResponseMethod, SocketAddr)>; - -/// Handles packet forwarding between transport protocols. -#[derive(Clone)] -pub struct Router(Arc>>); - -impl Default for Router { - fn default() -> Self { - Self(Arc::new(RwLock::new(AHashMap::with_capacity(1024)))) - } -} - -impl Router { - /// Get the socket reader for the route. - /// - /// Each transport protocol is layered according to its own socket, and - /// the data forwarded to this socket can be obtained by routing. - /// - /// # Example - /// - /// ``` - /// use std::net::SocketAddr; - /// use turn_server::router::*; - /// use turn_server::turn::ResponseMethod; - /// - /// #[tokio::main] - /// async fn main() { - /// let addr = "127.0.0.1:8080".parse::().unwrap(); - /// let router = Router::default(); - /// let mut receiver = router.get_receiver(addr); - /// - /// router.send(&addr, ResponseMethod::ChannelData, &addr, &[1, 2, 3]); - /// let ret = receiver.recv().await.unwrap(); - /// assert_eq!(ret.0, vec![1, 2, 3]); - /// assert_eq!(ret.1, ResponseMethod::ChannelData); - /// assert_eq!(ret.2, addr); - /// } - /// ``` - pub fn get_receiver(&self, interface: SocketAddr) -> UnboundedReceiver<(Vec, ResponseMethod, SocketAddr)> { - let (sender, receiver) = unbounded_channel(); - self.0.write().insert(interface, sender); - receiver - } - - /// Send data to router. - /// - /// By specifying the socket identifier and destination address, the route - /// is forwarded to the corresponding socket. However, it should be noted - /// that calling this function will not notify whether the socket exists. - /// If it does not exist, the data will be discarded by default. - /// - /// # Example - /// - /// ``` - /// use std::net::SocketAddr; - /// use turn_server::router::*; - /// use turn_server::turn::ResponseMethod; - /// - /// #[tokio::main] - /// async fn main() { - /// let addr = "127.0.0.1:8080".parse::().unwrap(); - /// let router = Router::default(); - /// let mut receiver = router.get_receiver(addr); - /// - /// router.send(&addr, ResponseMethod::ChannelData, &addr, &[1, 2, 3]); - /// let ret = receiver.recv().await.unwrap(); - /// assert_eq!(ret.0, vec![1, 2, 3]); - /// assert_eq!(ret.1, ResponseMethod::ChannelData); - /// assert_eq!(ret.2, addr); - /// } - /// ``` - pub fn send(&self, interface: &SocketAddr, method: ResponseMethod, addr: &SocketAddr, data: &[u8]) { - let mut is_destroy = false; - - { - if let Some(sender) = self.0.read().get(interface) { - if sender.send((data.to_vec(), method, *addr)).is_err() { - is_destroy = true; - } - } - } - - if is_destroy { - self.remove(interface); - } - } - - /// delete socket. - /// - /// # Example - /// - /// ``` - /// use std::net::SocketAddr; - /// use turn_server::router::*; - /// use turn_server::turn::ResponseMethod; - /// - /// #[tokio::main] - /// async fn main() { - /// let addr = "127.0.0.1:8080".parse::().unwrap(); - /// let router = Router::default(); - /// let mut receiver = router.get_receiver(addr); - /// - /// router.send(&addr, ResponseMethod::ChannelData, &addr, &[1, 2, 3]); - /// let ret = receiver.recv().await.unwrap(); - /// assert_eq!(ret.0, vec![1, 2, 3]); - /// assert_eq!(ret.1, ResponseMethod::ChannelData); - /// assert_eq!(ret.2, addr); - /// - /// router.remove(&addr); - /// assert!(receiver.recv().await.is_none()); - /// } - /// ``` - pub fn remove(&self, interface: &SocketAddr) { - drop(self.0.write().remove(interface)) - } -} diff --git a/src/rpc.rs b/src/rpc.rs new file mode 100644 index 00000000..d4e0d74d --- /dev/null +++ b/src/rpc.rs @@ -0,0 +1,320 @@ +pub mod proto { + tonic::include_proto!("turn.server"); +} + +use std::time::{Duration, Instant}; + +use anyhow::{Result, anyhow}; +use codec::{crypto::Password, message::attributes::PasswordAlgorithm}; +use service::session::{Identifier, Session}; +use tokio::sync::{ + Mutex, + mpsc::{Sender, channel}, +}; + +use tonic::{ + Request, Response, Status, + transport::{Channel, Server}, +}; + +#[cfg(feature = "ssl")] +use tonic::transport::{Certificate, ClientTlsConfig, Identity, ServerTlsConfig}; + +use proto::{ + PasswordAlgorithm as ProtoPasswordAlgorithm, SessionQueryParams, TurnAllocatedEvent, + TurnChannelBindEvent, TurnCreatePermissionEvent, TurnDestroyEvent, TurnRefreshEvent, + TurnServerInfo, TurnSession, TurnSessionStatistics, + turn_hooks_service_client::TurnHooksServiceClient, + turn_service_server::{TurnService, TurnServiceServer}, +}; + +use crate::{Service, config::Config, rpc::proto::GetTurnPasswordRequest, statistics::Statistics}; + +impl From for ProtoPasswordAlgorithm { + fn from(val: PasswordAlgorithm) -> Self { + match val { + PasswordAlgorithm::Md5 => Self::Md5, + PasswordAlgorithm::Sha256 => Self::Sha256, + } + } +} + +pub trait IdString { + type Error; + + fn to_string(&self) -> String; + fn from_string(s: String) -> Result + where + Self: Sized; +} + +impl IdString for Identifier { + type Error = anyhow::Error; + + fn to_string(&self) -> String { + format!("{}/{}", self.source, self.interface) + } + + fn from_string(s: String) -> Result { + let (source, interface) = s.split_once('/').ok_or(anyhow!("Invalid identifier"))?; + + Ok(Self { + source: source.parse()?, + interface: interface.parse()?, + }) + } +} + +struct RpcService { + config: Config, + service: Service, + statistics: Statistics, + uptime: Instant, +} + +#[tonic::async_trait] +impl TurnService for RpcService { + async fn get_info(&self, _: Request<()>) -> Result, Status> { + Ok(Response::new(TurnServerInfo { + software: crate::SOFTWARE.to_string(), + uptime: self.uptime.elapsed().as_secs(), + interfaces: self + .config + .server + .get_externals() + .iter() + .map(|addr| addr.to_string()) + .collect(), + port_capacity: self.config.server.port_range.size() as u32, + port_allocated: self.service.get_session_manager().allocated() as u32, + })) + } + + async fn get_session( + &self, + request: Request, + ) -> Result, Status> { + if let Some(Session::Authenticated { + username, + allocate_port, + allocate_channels, + permissions, + expires, + .. + }) = self + .service + .get_session_manager() + .get_session( + &Identifier::from_string(request.into_inner().id) + .map_err(|_| Status::invalid_argument("Invalid identifier"))?, + ) + .get_ref() + { + Ok(Response::new(TurnSession { + username: username.to_string(), + permissions: permissions.iter().map(|p| *p as i32).collect(), + channels: allocate_channels.iter().map(|p| *p as i32).collect(), + port: allocate_port.map(|p| p as i32), + expires: *expires as i64, + })) + } else { + Err(Status::not_found("Session not found")) + } + } + + async fn get_session_statistics( + &self, + request: Request, + ) -> Result, Status> { + if let Some(counts) = self.statistics.get( + &Identifier::from_string(request.into_inner().id) + .map_err(|_| Status::invalid_argument("Invalid identifier"))?, + ) { + Ok(Response::new(TurnSessionStatistics { + received_bytes: counts.received_bytes as u64, + send_bytes: counts.send_bytes as u64, + received_pkts: counts.received_pkts as u64, + send_pkts: counts.send_pkts as u64, + error_pkts: counts.error_pkts as u64, + })) + } else { + Err(Status::not_found("Session not found")) + } + } + + async fn destroy_session( + &self, + request: Request, + ) -> Result, Status> { + if self.service.get_session_manager().refresh( + &Identifier::from_string(request.into_inner().id) + .map_err(|_| Status::invalid_argument("Invalid identifier"))?, + 0, + ) { + Ok(Response::new(())) + } else { + Err(Status::failed_precondition("Session not found")) + } + } +} + +pub async fn start_server(config: Config, service: Service, statistics: Statistics) -> Result<()> { + if let Some(api) = &config.api { + let mut builder = Server::builder(); + + builder = builder + .timeout(Duration::from_secs(api.timeout as u64)) + .accept_http1(false); + + #[cfg(feature = "ssl")] + if let Some(ssl) = &api.ssl { + builder = builder.tls_config(ServerTlsConfig::new().identity(Identity::from_pem( + ssl.certificate_chain.clone(), + ssl.private_key.clone(), + )))?; + } + + log::info!("api server listening: listen={}", api.listen); + + builder + .add_service(TurnServiceServer::new(RpcService { + config: config.clone(), + uptime: Instant::now(), + statistics, + service, + })) + .serve(api.listen) + .await?; + } else { + std::future::pending().await + } + + Ok(()) +} + +pub enum HooksEvent { + Allocated(TurnAllocatedEvent), + ChannelBind(TurnChannelBindEvent), + CreatePermission(TurnCreatePermissionEvent), + Refresh(TurnRefreshEvent), + Destroy(TurnDestroyEvent), +} + +struct RpcHooksServiceInner { + event_channel: Sender, + client: Mutex>, +} + +pub struct RpcHooksService(Option); + +impl RpcHooksService { + pub async fn new(config: &Config) -> Result { + if let Some(hooks) = &config.hooks { + let (event_channel, mut rx) = channel(hooks.max_channel_size); + let client = { + let mut builder = Channel::builder(hooks.endpoint.as_str().try_into()?); + + builder = builder.timeout(Duration::from_secs(hooks.timeout as u64)); + + #[cfg(feature = "ssl")] + if let Some(ssl) = &hooks.ssl { + builder = builder.tls_config( + ClientTlsConfig::new() + .ca_certificate(Certificate::from_pem(ssl.certificate_chain.clone())) + .domain_name( + url::Url::parse(&hooks.endpoint)? + .domain() + .ok_or_else(|| anyhow!("Invalid hooks server domain"))?, + ), + )?; + } + + TurnHooksServiceClient::new( + builder + .connect_timeout(Duration::from_secs(5)) + .timeout(Duration::from_secs(1)) + .connect() + .await?, + ) + }; + + { + let mut client = client.clone(); + + tokio::spawn(async move { + while let Some(event) = rx.recv().await { + if match event { + HooksEvent::Allocated(event) => { + client.on_allocated_event(Request::new(event)).await + } + HooksEvent::ChannelBind(event) => { + client.on_channel_bind_event(Request::new(event)).await + } + HooksEvent::CreatePermission(event) => { + client.on_create_permission_event(Request::new(event)).await + } + HooksEvent::Refresh(event) => { + client.on_refresh_event(Request::new(event)).await + } + HooksEvent::Destroy(event) => { + client.on_destroy_event(Request::new(event)).await + } + } + .is_err() + { + break; + } + } + }); + } + + log::info!("create hooks client, endpoint={}", hooks.endpoint); + + Ok(Self(Some(RpcHooksServiceInner { + client: Mutex::new(client), + event_channel, + }))) + } else { + Ok(Self(None)) + } + } + + pub fn send_event(&self, event: HooksEvent) { + if let Some(inner) = &self.0 + && !inner.event_channel.is_closed() + && let Err(e) = inner.event_channel.try_send(event) + { + log::error!("Failed to send event to hooks server: {}", e); + } + } + + pub async fn get_password( + &self, + username: &str, + algorithm: PasswordAlgorithm, + ) -> Option { + if let Some(inner) = &self.0 { + let algorithm: ProtoPasswordAlgorithm = algorithm.into(); + let password = inner + .client + .lock() + .await + .get_password(Request::new(GetTurnPasswordRequest { + username: username.to_string(), + algorithm: algorithm as i32, + })) + .await + .ok()? + .into_inner() + .password; + + return Some(match algorithm { + ProtoPasswordAlgorithm::Md5 => Password::Md5(password.try_into().ok()?), + ProtoPasswordAlgorithm::Sha256 => Password::Sha256(password.try_into().ok()?), + ProtoPasswordAlgorithm::Unspecified => unreachable!(), + }); + } + + None + } +} diff --git a/src/server.rs b/src/server.rs deleted file mode 100644 index 224cb708..00000000 --- a/src/server.rs +++ /dev/null @@ -1,489 +0,0 @@ -use crate::{ - config::{Config, Interface}, - router::Router, - statistics::Statistics, - turn::{Observer, Service}, -}; - -use std::net::SocketAddr; - -#[allow(unused)] -struct ServerStartOptions { - bind: SocketAddr, - external: SocketAddr, - service: Service, - router: Router, - statistics: Statistics, -} - -#[allow(unused)] -trait Server { - async fn start(options: ServerStartOptions) -> Result<(), anyhow::Error> - where - T: Clone + Observer + 'static; -} - -#[cfg(feature = "udp")] -mod udp { - use super::{Server as ServerExt, ServerStartOptions}; - use crate::{ - statistics::Stats, - stun::Transport, - turn::{Observer, ResponseMethod, SessionAddr}, - }; - - use std::{io::ErrorKind::ConnectionReset, sync::Arc}; - - use tokio::net::UdpSocket; - - /// udp socket process thread. - /// - /// read the data packet from the UDP socket and hand - /// it to the proto for processing, and send the processed - /// data packet to the specified address. - pub struct Server; - - impl ServerExt for Server { - async fn start( - ServerStartOptions { - bind, - external, - service, - router, - statistics, - }: ServerStartOptions, - ) -> Result<(), anyhow::Error> - where - T: Clone + Observer + 'static, - { - let socket = Arc::new(UdpSocket::bind(bind).await?); - let local_addr = socket.local_addr()?; - - { - let socket = socket.clone(); - let router = router.clone(); - let reporter = statistics.get_reporter(Transport::UDP); - let mut operationer = service.get_operationer(external, external); - - let mut session_addr = SessionAddr { - address: external, - interface: external, - }; - - tokio::spawn(async move { - let mut buf = vec![0u8; 2048]; - - loop { - // Note: An error will also be reported when the remote host is - // shut down, which is not processed yet, but a - // warning will be issued. - let (size, addr) = match socket.recv_from(&mut buf).await { - Err(e) if e.kind() != ConnectionReset => break, - Ok(s) => s, - _ => continue, - }; - - session_addr.address = addr; - - reporter.send(&session_addr, &[Stats::ReceivedBytes(size), Stats::ReceivedPkts(1)]); - - // The stun message requires at least 4 bytes. (currently the - // smallest stun message is channel data, - // excluding content) - if size >= 4 { - if let Ok(Some(res)) = operationer.route(&buf[..size], addr) { - let target = res.relay.as_ref().unwrap_or(&addr); - if let Some(ref endpoint) = res.endpoint { - router.send(endpoint, res.method, target, res.bytes); - } else { - if let Err(e) = socket.send_to(res.bytes, target).await { - if e.kind() != ConnectionReset { - break; - } - } - - reporter - .send(&session_addr, &[Stats::SendBytes(res.bytes.len()), Stats::SendPkts(1)]); - - if let ResponseMethod::Stun(method) = res.method { - if method.is_error() { - reporter.send(&session_addr, &[Stats::ErrorPkts(1)]); - } - } - } - } - } - } - }); - } - - tokio::spawn(async move { - let mut session_addr = SessionAddr { - address: external, - interface: external, - }; - - let reporter = statistics.get_reporter(Transport::UDP); - let mut receiver = router.get_receiver(external); - while let Some((bytes, _, addr)) = receiver.recv().await { - session_addr.address = addr; - - if let Err(e) = socket.send_to(&bytes, addr).await { - if e.kind() != ConnectionReset { - break; - } - } else { - reporter.send(&session_addr, &[Stats::SendBytes(bytes.len()), Stats::SendPkts(1)]); - } - } - - router.remove(&external); - - log::error!("udp server close: interface={:?}", local_addr); - }); - - log::info!( - "turn server listening: bind={}, external={}, transport=UDP", - bind, - external, - ); - - Ok(()) - } - } -} - -#[cfg(feature = "tcp")] -mod tcp { - use super::{Server as ServerExt, ServerStartOptions}; - use crate::{ - statistics::Stats, - stun::{Decoder, Transport}, - turn::{Observer, ResponseMethod, SessionAddr}, - }; - - use std::{ - ops::{Deref, DerefMut}, - sync::Arc, - }; - - use tokio::{io::AsyncReadExt, io::AsyncWriteExt, net::TcpListener, sync::Mutex}; - - static ZERO_BYTES: [u8; 8] = [0u8; 8]; - - /// An emulated double buffer queue, this is used when reading data over - /// TCP. - /// - /// When reading data over TCP, you need to keep adding to the buffer until - /// you find the delimited position. But this double buffer queue solves - /// this problem well, in the queue, the separation is treated as the first - /// read operation and after the separation the buffer is reversed and - /// another free buffer is used for writing the data. - /// - /// If the current buffer in the separation after the existence of - /// unconsumed data, this time the unconsumed data will be copied to another - /// free buffer, and fill the length of the free buffer data, this time to - /// write data again when you can continue to fill to the end of the - /// unconsumed data. - /// - /// This queue only needs to copy the unconsumed data without duplicating - /// the memory allocation, which will reduce a lot of overhead. - struct ExchangeBuffer { - buffers: [(Vec, usize /* len */); 2], - index: usize, - } - - impl Default for ExchangeBuffer { - #[rustfmt::skip] - fn default() -> Self { - Self { - index: 0, - buffers: [ - (vec![0u8; 2048], 0), - (vec![0u8; 2048], 0), - ], - } - } - } - - impl Deref for ExchangeBuffer { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.buffers[self.index].0[..] - } - } - - impl DerefMut for ExchangeBuffer { - // Writes need to take into account overwriting written data, so fetching the - // writable buffer starts with the internal cursor. - fn deref_mut(&mut self) -> &mut Self::Target { - let len = self.buffers[self.index].1; - &mut self.buffers[self.index].0[len..] - } - } - - impl ExchangeBuffer { - fn len(&self) -> usize { - self.buffers[self.index].1 - } - - /// The buffer does not automatically advance the cursor as BytesMut - /// does, and you need to manually advance the length of the data - /// written. - fn advance(&mut self, len: usize) { - self.buffers[self.index].1 += len; - } - - fn split(&mut self, len: usize) -> &[u8] { - let (ref current_bytes, current_len) = self.buffers[self.index]; - - // The length of the separation cannot be greater than the length of the data. - assert!(len <= current_len); - - // Length of unconsumed data - let remaining = current_len - len; - - { - // The current buffer is no longer in use, resetting the content length. - self.buffers[self.index].1 = 0; - - // Invert the buffer. - self.index = if self.index == 0 { 1 } else { 0 }; - - // The length of unconsumed data needs to be updated into the reversed - // completion buffer. - self.buffers[self.index].1 = remaining; - } - - // Unconsumed data exists and is copied to the free buffer. - #[allow(mutable_transmutes)] - if remaining > 0 { - unsafe { std::mem::transmute::<&[u8], &mut [u8]>(&self.buffers[self.index].0[..remaining]) } - .copy_from_slice(¤t_bytes[len..current_len]); - } - - ¤t_bytes[..len] - } - } - - /// tcp socket process thread. - /// - /// This function is used to handle all connections coming from the tcp - /// listener, and handle the receiving, sending and forwarding of messages. - pub struct Server; - - impl ServerExt for Server { - async fn start( - ServerStartOptions { - bind, - external, - service, - router, - statistics, - }: ServerStartOptions, - ) -> Result<(), anyhow::Error> - where - T: Clone + Observer + 'static, - { - let listener = TcpListener::bind(bind).await?; - let local_addr = listener.local_addr()?; - - tokio::spawn(async move { - // Accept all connections on the current listener, but exit the entire - // process when an error occurs. - while let Ok((socket, address)) = listener.accept().await { - let router = router.clone(); - let reporter = statistics.get_reporter(Transport::TCP); - let mut receiver = router.get_receiver(address); - let mut operationer = service.get_operationer(address, external); - - log::info!("tcp socket accept: addr={:?}, interface={:?}", address, local_addr,); - - // Disable the Nagle algorithm. - // because to maintain real-time, any received data should be processed - // as soon as possible. - if let Err(e) = socket.set_nodelay(true) { - log::error!("tcp socket set nodelay failed!: addr={}, err={}", address, e); - } - - let session_addr = SessionAddr { - interface: external, - address, - }; - - let (mut reader, writer) = socket.into_split(); - let writer = Arc::new(Mutex::new(writer)); - - // Use a separate task to handle messages forwarded to this socket. - let writer_ = writer.clone(); - let reporter_ = reporter.clone(); - tokio::spawn(async move { - while let Some((bytes, method, _)) = receiver.recv().await { - let mut writer = writer_.lock().await; - if writer.write_all(bytes.as_slice()).await.is_err() { - break; - } else { - reporter_.send(&session_addr, &[Stats::SendBytes(bytes.len()), Stats::SendPkts(1)]); - } - - // The channel data needs to be aligned in multiples of 4 in - // tcp. If the channel data is forwarded to tcp, the alignment - // bit needs to be filled, because if the channel data comes - // from udp, it is not guaranteed to be aligned and needs to be - // checked. - if method == ResponseMethod::ChannelData { - let pad = bytes.len() % 4; - if pad > 0 && writer.write_all(&ZERO_BYTES[..(4 - pad)]).await.is_err() { - break; - } - } - } - }); - - let sessions = service.get_sessions(); - tokio::spawn(async move { - let mut buffer = ExchangeBuffer::default(); - - 'a: while let Ok(size) = reader.read(&mut buffer).await { - // When the received message is 0, it means that the socket - // has been closed. - if size == 0 { - break; - } else { - reporter.send(&session_addr, &[Stats::ReceivedBytes(size)]); - buffer.advance(size); - } - - // The minimum length of a stun message will not be less - // than 4. - if buffer.len() < 4 { - continue; - } - - loop { - if buffer.len() <= 4 { - break; - } - - // Try to get the message length, if the currently - // received data is less than the message length, jump - // out of the current loop and continue to receive more - // data. - let size = match Decoder::message_size(&buffer, true) { - Err(_) => break, - Ok(s) => { - // Limit the maximum length of messages to 2048, this is to prevent buffer - // overflow attacks. - if s > 2048 { - break 'a; - } - - if s > buffer.len() { - break; - } - - reporter.send(&session_addr, &[Stats::ReceivedPkts(1)]); - - s - } - }; - - let chunk = buffer.split(size); - if let Ok(ret) = operationer.route(chunk, address) { - if let Some(res) = ret { - if let Some(ref inerface) = res.endpoint { - router.send( - inerface, - res.method, - res.relay.as_ref().unwrap_or(&address), - res.bytes, - ); - } else { - if writer.lock().await.write_all(res.bytes).await.is_err() { - break 'a; - } - - reporter.send( - &session_addr, - &[Stats::SendBytes(res.bytes.len()), Stats::SendPkts(1)], - ); - - if let ResponseMethod::Stun(method) = res.method { - if method.is_error() { - reporter.send(&session_addr, &[Stats::ErrorPkts(1)]); - } - } - } - } - } else { - break 'a; - } - } - } - - // When the tcp connection is closed, the procedure to close the session is - // process directly once, avoiding the connection being disconnected - // directly without going through the closing - // process. - sessions.refresh(&session_addr, 0); - - router.remove(&address); - - log::info!("tcp socket disconnect: addr={:?}, interface={:?}", address, local_addr); - }); - } - - log::error!("tcp server close: interface={:?}", local_addr); - }); - - log::info!( - "turn server listening: bind={}, external={}, transport=TCP", - bind, - external, - ); - - Ok(()) - } - } -} - -/// start turn server. -/// -/// create a specified number of threads, -/// each thread processes udp data separately. -pub async fn start(config: &Config, statistics: &Statistics, service: &Service) -> anyhow::Result<()> -where - T: Clone + Observer + 'static, -{ - #[allow(unused)] - use crate::config::Transport; - - let router = Router::default(); - for Interface { - transport, - external, - bind, - } in config.turn.interfaces.iter().cloned() - { - #[allow(unused)] - let options = ServerStartOptions { - statistics: statistics.clone(), - service: service.clone(), - router: router.clone(), - external, - bind, - }; - - match transport { - #[cfg(feature = "udp")] - Transport::UDP => udp::Server::start(options).await?, - #[cfg(feature = "tcp")] - Transport::TCP => tcp::Server::start(options).await?, - #[allow(unreachable_patterns)] - _ => (), - }; - } - - Ok(()) -} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 00000000..cefcf773 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,138 @@ +pub mod transport; + +use std::{net::SocketAddr, sync::Arc}; + +use ahash::{HashMap, HashMapExt}; +use anyhow::Result; +use bytes::Bytes; +use codec::message::methods::Method; +use parking_lot::RwLock; + +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, + task::JoinSet, +}; + +use crate::{ + Service, + config::{Config, Interface}, + server::transport::{ListenOptions, Listener, Transport, tcp::TcpServer, udp::UdpServer}, + statistics::Statistics, +}; + +pub async fn start_server(config: Config, service: Service, statistics: Statistics) -> Result<()> { + let exchanger = Exchanger::default(); + + let mut servers = JoinSet::new(); + + for interface in config.server.interfaces { + match interface { + Interface::Udp { + listen, + external, + idle_timeout, + mtu, + } => { + servers.spawn(UdpServer::start( + ListenOptions { + transport: Transport::Udp, + idle_timeout, + ssl: None, + external, + listen, + mtu, + }, + service.clone(), + statistics.clone(), + exchanger.clone(), + )); + } + Interface::Tcp { + listen, + external, + idle_timeout, + ssl, + } => { + servers.spawn(TcpServer::start( + ListenOptions { + transport: Transport::Tcp, + idle_timeout, + external, + listen, + mtu: 0, + ssl, + }, + service.clone(), + statistics.clone(), + exchanger.clone(), + )); + } + }; + } + + // As soon as one server exits, all servers will be exited to ensure the + // availability of all servers. + if let Some(res) = servers.join_next().await { + servers.abort_all(); + + return res?; + } + + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PayloadType { + Message(Method), + ChannelData, +} + +/// Handles packet forwarding between transport protocols. +#[derive(Clone)] +pub struct Exchanger(Arc>>>); + +impl Default for Exchanger { + fn default() -> Self { + Self(Arc::new(RwLock::new(HashMap::with_capacity(1024)))) + } +} + +impl Exchanger { + /// Get the socket reader for the route. + /// + /// Each transport protocol is layered according to its own socket, and + /// the data forwarded to this socket can be obtained by routing. + fn get_receiver(&self, interface: SocketAddr) -> UnboundedReceiver<(Bytes, PayloadType)> { + let (sender, receiver) = unbounded_channel(); + self.0.write().insert(interface, sender); + + receiver + } + + /// Send data to dispatcher. + /// + /// By specifying the socket identifier and destination address, the route + /// is forwarded to the corresponding socket. However, it should be noted + /// that calling this function will not notify whether the socket exists. + /// If it does not exist, the data will be discarded by default. + fn send(&self, interface: &SocketAddr, ty: PayloadType, data: Bytes) { + let mut is_destroy = false; + + { + if let Some(sender) = self.0.read().get(interface) + && sender.send((data, ty)).is_err() + { + is_destroy = true; + } + } + + if is_destroy { + self.remove(interface); + } + } + + /// delete socket. + pub fn remove(&self, interface: &SocketAddr) { + drop(self.0.write().remove(interface)) + } +} diff --git a/src/server/transport/mod.rs b/src/server/transport/mod.rs new file mode 100644 index 00000000..46bb8233 --- /dev/null +++ b/src/server/transport/mod.rs @@ -0,0 +1,183 @@ +pub mod tcp; +pub mod udp; + +use std::{net::SocketAddr, time::Duration}; + +use anyhow::Result; +use bytes::Bytes; +use service::{ + routing::Response, + session::Identifier, +}; + +use tokio::time::interval; + +use crate::{ + Service, + config::Ssl, + server::{Exchanger, PayloadType}, + statistics::{Statistics, Stats}, +}; + +pub const MAX_MESSAGE_SIZE: usize = 4096; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Transport { + Udp, + Tcp, +} + +pub trait Socket: Send + 'static { + fn read(&mut self) -> impl Future> + Send; + fn write(&mut self, buffer: &[u8]) -> impl Future> + Send; + fn close(&mut self) -> impl Future + Send; +} + +#[allow(unused)] +pub struct ListenOptions { + pub transport: Transport, + pub idle_timeout: u32, + pub listen: SocketAddr, + pub external: SocketAddr, + pub ssl: Option, + pub mtu: usize, +} + +pub trait Listener: Sized + Send { + type Socket: Socket; + + fn bind(options: &ListenOptions) -> impl Future> + Send; + fn accept(&mut self) -> impl Future> + Send; + fn local_addr(&self) -> Result; + + fn start( + options: ListenOptions, + service: Service, + statistics: Statistics, + exchanger: Exchanger, + ) -> impl Future> + Send { + let transport = options.transport; + let idle_timeout = options.idle_timeout as u64; + + async move { + let mut listener = Self::bind(&options).await?; + let local_addr = listener.local_addr()?; + + log::info!( + "server listening: listen={}, external={}, local addr={local_addr}, transport={transport:?}", + options.listen, + options.external, + ); + + while let Some((mut socket, address)) = listener.accept().await { + let id = Identifier { + interface: options.external, + source: address, + }; + + let mut receiver = exchanger.get_receiver(address); + let mut router = service.make_router(address, options.external); + let reporter = statistics.get_reporter(); + + let service = service.clone(); + let exchanger = exchanger.clone(); + + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(1)); + let mut read_delay = 0; + + loop { + tokio::select! { + Some(buffer) = socket.read() => { + read_delay = 0; + + if let Ok(res) = router.route(&buffer, address).await + { + let Some(res) = res else { + continue; + }; + + let (ty, bytes, target) = match res { + Response::Message { + method, + bytes, + target, + } => (PayloadType::Message(method), bytes, target), + Response::ChannelData { bytes, target } => { + (PayloadType::ChannelData, bytes, target) + } + }; + + if let Some(endpoint) = target.endpoint { + exchanger.send(&endpoint, ty, Bytes::copy_from_slice(bytes)); + } else { + if socket.write(bytes).await.is_err() { + break; + } + + reporter.send( + &id, + &[Stats::SendBytes(bytes.len()), Stats::SendPkts(1)], + ); + + if let PayloadType::Message(method) = ty && method.is_error() { + reporter.send(&id, &[Stats::ErrorPkts(1)]); + } + } + } + } + Some((bytes, method)) = receiver.recv() => { + if socket.write(&bytes).await.is_err() { + break; + } else { + reporter.send(&id, &[Stats::SendBytes(bytes.len()), Stats::SendPkts(1)]); + } + + // The channel data needs to be aligned in multiples of 4 in + // tcp. If the channel data is forwarded to tcp, the alignment + // bit needs to be filled, because if the channel data comes + // from udp, it is not guaranteed to be aligned and needs to be + // checked. + if transport == Transport::Tcp && method == PayloadType::ChannelData { + let pad = bytes.len() % 4; + if pad > 0 && socket.write(&[0u8; 8][..(4 - pad)]).await.is_err() { + break; + } + } + } + _ = interval.tick() => { + read_delay += 1; + + if read_delay >= idle_timeout { + break; + } + } + else => { + break; + } + } + } + + // close the socket + socket.close().await; + + // When the socket connection is closed, the procedure to close the session is + // process directly once, avoiding the connection being disconnected + // directly without going through the closing + // process. + service.get_session_manager().refresh(&id, 0); + + exchanger.remove(&address); + + log::info!( + "socket disconnect: addr={address:?}, interface={local_addr:?}, transport={transport:?}" + ); + }); + } + + log::error!("server shutdown: interface={local_addr:?}, transport={transport:?}"); + + Ok(()) + } + } +} diff --git a/src/server/transport/tcp.rs b/src/server/transport/tcp.rs new file mode 100644 index 00000000..50c646ea --- /dev/null +++ b/src/server/transport/tcp.rs @@ -0,0 +1,264 @@ +use std::{io::Error, net::SocketAddr}; + +#[cfg(feature = "ssl")] +use std::sync::Arc; + +use anyhow::Result; +use bytes::{Bytes, BytesMut}; +use codec::Decoder; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf}, + net::{TcpListener as TokioTcpListener, TcpStream}, + sync::mpsc::{Sender, UnboundedReceiver, channel, unbounded_channel}, +}; + +#[cfg(feature = "ssl")] +use tokio_rustls::{ + TlsAcceptor, + rustls::{ + ServerConfig, + pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject}, + }, + server::TlsStream, +}; + +use crate::server::transport::{ListenOptions, Listener, MAX_MESSAGE_SIZE, Socket}; + +enum MaybeSslStream { + #[cfg(feature = "ssl")] + Ssl(Box>), + Base(TcpStream), +} + +impl MaybeSslStream { + fn split(self) -> (Reader, Writer) { + use tokio::io::split; + + match self { + Self::Base(it) => { + let (rx, tx) = split(it); + + (Reader::Base(rx), Writer::Base(tx)) + } + #[cfg(feature = "ssl")] + Self::Ssl(it) => { + let (rx, tx) = split(it); + + (Reader::Ssl(rx), Writer::Ssl(tx)) + } + } + } +} + +enum Reader { + #[cfg(feature = "ssl")] + Ssl(ReadHalf>>), + Base(ReadHalf), +} + +impl Reader { + async fn read_buf(&mut self, buffer: &mut BytesMut) -> Result { + match self { + Self::Base(it) => it.read_buf(buffer).await, + #[cfg(feature = "ssl")] + Self::Ssl(it) => it.read_buf(buffer).await, + } + } +} + +enum Writer { + #[cfg(feature = "ssl")] + Ssl(WriteHalf>>), + Base(WriteHalf), +} + +impl Writer { + async fn write_all(&mut self, buffer: &[u8]) -> Result<(), Error> { + match self { + Self::Base(it) => it.write_all(buffer).await, + #[cfg(feature = "ssl")] + Self::Ssl(it) => it.write_all(buffer).await, + } + } +} + +pub struct TcpSocket { + writer: Writer, + receiver: UnboundedReceiver, + close_signal_sender: Sender<()>, +} + +impl TcpSocket { + fn new(stream: MaybeSslStream, addr: SocketAddr) -> Self { + let (close_signal_sender, mut close_signal_receiver) = channel::<()>(1); + let (tx, receiver) = unbounded_channel::(); + let (mut reader, writer) = stream.split(); + + tokio::spawn(async move { + let mut buffer = BytesMut::new(); + + 'a: loop { + tokio::select! { + Ok(size) = reader.read_buf(&mut buffer) => { + if size == 0 { + break; + } + + // The minimum length of a stun message will not be less + // than 4. + if buffer.len() < 4 { + continue; + } + + // Limit the maximum length of messages to 2048, this is to prevent buffer + // overflow attacks. + if buffer.len() > MAX_MESSAGE_SIZE * 3 { + break; + } + + loop { + if buffer.len() <= 4 { + break; + } + + // Try to get the message length, if the currently + // received data is less than the message length, jump + // out of the current loop and continue to receive more + // data. + let size = match Decoder::message_size(&buffer, true) { + Err(_) => break, + Ok(size) => { + if size > MAX_MESSAGE_SIZE { + log::warn!( + "tcp message size too large: \ + size={size}, \ + max={MAX_MESSAGE_SIZE}, \ + addr={addr:?}" + ); + + break 'a; + } + + if size > buffer.len() { + break; + } + + size + } + }; + + if tx.send(buffer.split_to(size).freeze()).is_err() { + break 'a; + } + } + } + _ = close_signal_receiver.recv() => { + break; + } + else => { + break; + } + } + } + }); + + Self { + close_signal_sender, + writer, + receiver, + } + } +} + +impl Socket for TcpSocket { + async fn read(&mut self) -> Option { + self.receiver.recv().await + } + + async fn write(&mut self, buffer: &[u8]) -> Result<()> { + Ok(self.writer.write_all(buffer).await?) + } + + async fn close(&mut self) { + self.receiver.close(); + + let _ = self.close_signal_sender.send(()).await; + } +} + +pub struct TcpServer { + socket_receiver: UnboundedReceiver<(TcpSocket, SocketAddr)>, + local_addr: SocketAddr, +} + +impl Listener for TcpServer { + type Socket = TcpSocket; + + async fn bind(options: &ListenOptions) -> Result { + #[cfg(feature = "ssl")] + let acceptor = if let Some(ssl) = &options.ssl { + Some(TlsAcceptor::from(Arc::new( + ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + CertificateDer::pem_file_iter(ssl.certificate_chain.clone())? + .collect::, _>>()?, + PrivateKeyDer::from_pem_file(ssl.private_key.clone())?, + )?, + ))) + } else { + None + }; + + let listener = TokioTcpListener::bind(options.listen).await?; + let local_addr = listener.local_addr()?; + + let (tx, socket_receiver) = unbounded_channel::<(TcpSocket, SocketAddr)>(); + tokio::spawn(async move { + while let Ok((socket, addr)) = listener.accept().await { + // Disable the Nagle algorithm. + // because to maintain real-time, any received data should be processed + // as soon as possible. + if let Err(e) = socket.set_nodelay(true) { + log::warn!("tls socket set nodelay failed!: addr={addr}, err={e}"); + } + + #[cfg(feature = "ssl")] + if let Some(acceptor) = acceptor.clone() { + let tx = tx.clone(); + + tokio::spawn(async move { + if let Ok(socket) = acceptor.accept(socket).await { + let _ = tx.send(( + TcpSocket::new(MaybeSslStream::Ssl(socket.into()), addr), + addr, + )); + }; + }); + + continue; + } + + if tx + .send((TcpSocket::new(MaybeSslStream::Base(socket), addr), addr)) + .is_err() + { + break; + } + } + }); + + Ok(Self { + socket_receiver, + local_addr, + }) + } + + async fn accept(&mut self) -> Option<(Self::Socket, SocketAddr)> { + self.socket_receiver.recv().await + } + + fn local_addr(&self) -> Result { + Ok(self.local_addr) + } +} diff --git a/src/server/transport/udp.rs b/src/server/transport/udp.rs new file mode 100644 index 00000000..30b1ee68 --- /dev/null +++ b/src/server/transport/udp.rs @@ -0,0 +1,136 @@ +use std::{io::ErrorKind, net::SocketAddr, sync::Arc}; + +use ahash::{HashMap, HashMapExt}; +use anyhow::Result; +use bytes::{Bytes, BytesMut}; +use tokio::{ + net::UdpSocket as TokioUdpSocket, + sync::mpsc::{ + Receiver, Sender, UnboundedReceiver, UnboundedSender, channel, unbounded_channel, + }, +}; + +use crate::server::transport::{ListenOptions, Listener, Socket}; + +pub struct UdpSocket { + close_signal_sender: UnboundedSender, + bytes_receiver: Receiver, + socket: Arc, + addr: SocketAddr, +} + +impl Socket for UdpSocket { + async fn read(&mut self) -> Option { + self.bytes_receiver.recv().await + } + + async fn write(&mut self, buffer: &[u8]) -> Result<()> { + if let Err(e) = self.socket.send_to(buffer, self.addr).await { + // Note: An error will also be reported when the remote host is + // shut down, which is not processed yet, but a + // warning will be issued. + if e.kind() != ErrorKind::ConnectionReset { + return Err(e.into()); + } + } + + Ok(()) + } + + async fn close(&mut self) { + self.bytes_receiver.close(); + + let _ = self.close_signal_sender.send(self.addr); + } +} + +pub struct UdpServer { + receiver: UnboundedReceiver<(UdpSocket, SocketAddr)>, + socket: Arc, +} + +impl Listener for UdpServer { + type Socket = UdpSocket; + + async fn bind(options: &ListenOptions) -> Result { + let socket = Arc::new(TokioUdpSocket::bind(options.listen).await?); + let (socket_sender, socket_receiver) = unbounded_channel::<(UdpSocket, SocketAddr)>(); + let (close_signal_sender, mut close_signal_receiver) = unbounded_channel::(); + + { + let socket = socket.clone(); + + let mut buffer = BytesMut::zeroed(options.mtu); + + tokio::spawn(async move { + let mut sockets = HashMap::>::with_capacity(1024); + + loop { + tokio::select! { + ret = socket.recv_from(&mut buffer) => { + let (size, addr) = match ret { + Ok(it) => it, + // Note: An error will also be reported when the remote host is + // shut down, which is not processed yet, but a + // warning will be issued. + Err(e) => { + if e.kind() != ErrorKind::ConnectionReset { + log::error!("udp server recv_from error={e}"); + + break; + } else { + continue; + } + } + }; + + if let Some(stream) = sockets.get(&addr) { + if stream.try_send(Bytes::copy_from_slice(&buffer[..size])).is_err() + { + sockets.remove(&addr); + } + } else { + let (tx, bytes_receiver) = channel::(100); + sockets.insert(addr, tx); + + if socket_sender + .send(( + UdpSocket { + close_signal_sender: close_signal_sender.clone(), + socket: socket.clone(), + bytes_receiver, + addr, + }, + addr, + )) + .is_err() + { + break; + } + } + } + Some(addr) = close_signal_receiver.recv() => { + let _ = sockets.remove(&addr); + } + else => { + break; + } + } + } + }); + } + + Ok(Self { + receiver: socket_receiver, + socket, + }) + } + + async fn accept(&mut self) -> Option<(UdpSocket, SocketAddr)> { + self.receiver.recv().await + } + + fn local_addr(&self) -> Result { + Ok(self.socket.local_addr()?) + } +} diff --git a/src/statistics.rs b/src/statistics.rs index 8d4bdeee..34b544b9 100644 --- a/src/statistics.rs +++ b/src/statistics.rs @@ -3,126 +3,9 @@ use std::sync::{ atomic::{AtomicUsize, Ordering}, }; -use ahash::AHashMap; +use ahash::HashMap; use parking_lot::RwLock; - -use crate::{stun::Transport, turn::SessionAddr}; - -/// [issue](https://github.com/mycrl/turn-rs/issues/101) -/// -/// Integrated Prometheus Metrics Exporter -pub mod prometheus { - use std::sync::LazyLock; - - use anyhow::Result; - use prometheus::{Encoder, IntCounter, IntGauge, TextEncoder, register_int_counter, register_int_gauge}; - - use super::{Counts, Number, Stats}; - use crate::stun::Transport; - - // The `register_int_counter` macro would be too long if written out in full, - // with too many line breaks after formatting, and this is wrapped directly into - // a macro again. - macro_rules! counter { - ($prefix:expr, $operation:expr, $dst:expr) => { - register_int_counter!( - format!("{}_{}_{}", $prefix, $operation, $dst), - format!("The {} amount of {} {}", $prefix, $dst, $operation) - ) - }; - } - - pub static METRICS: LazyLock = LazyLock::new(|| Metrics::default()); - - /// # Example - /// - /// ``` - /// use prometheus::register_int_counter; - /// use turn_server::statistics::{Number, prometheus::*}; - /// - /// let count = register_int_counter!("test", "test").unwrap(); - /// - /// count.add(1); - /// assert_eq!(count.get(), 1); - /// - /// count.add(1); - /// assert_eq!(count.get(), 2); - /// ``` - impl Number for IntCounter { - fn add(&self, value: usize) { - self.inc_by(value as u64); - } - - fn get(&self) -> usize { - self.get() as usize - } - } - - impl Counts { - fn new(prefix: &str) -> Result { - Ok(Self { - received_bytes: counter!(prefix, "received", "bytes")?, - send_bytes: counter!(prefix, "sent", "bytes")?, - received_pkts: counter!(prefix, "received", "packets")?, - send_pkts: counter!(prefix, "sent", "packets")?, - error_pkts: counter!(prefix, "error", "packets")?, - }) - } - } - - /// Summarized metrics data for Global/TCP/UDP. - pub struct Metrics { - pub allocated: IntGauge, - pub total: Counts, - pub tcp: Counts, - pub udp: Counts, - } - - impl Default for Metrics { - fn default() -> Self { - Self::new().expect("Unable to initialize Prometheus metrics data!") - } - } - - impl Metrics { - pub fn new() -> Result { - Ok(Self { - total: Counts::new("total")?, - tcp: Counts::new("tcp")?, - udp: Counts::new("udp")?, - allocated: register_int_gauge!("allocated", "The number of allocated ports, count = 16383")?, - }) - } - - /// # Example - /// - /// ``` - /// use turn_server::statistics::{prometheus::*, *}; - /// use turn_server::stun::Transport; - /// - /// METRICS.add(Transport::TCP, &Stats::ReceivedBytes(1)); - /// assert_eq!(METRICS.tcp.received_bytes.get(), 1); - /// assert_eq!(METRICS.total.received_bytes.get(), 1); - /// assert_eq!(METRICS.udp.received_bytes.get(), 0); - /// ``` - pub fn add(&self, transport: Transport, payload: &Stats) { - self.total.add(payload); - - if transport == Transport::TCP { - self.tcp.add(payload); - } else { - self.udp.add(payload); - } - } - } - - /// Generate prometheus metrics data that externally needs to be exposed to - /// the `/metrics` route. - pub fn generate_metrics(buf: &mut Vec) -> Result<()> { - TextEncoder::new().encode(&prometheus::gather(), buf)?; - Ok(()) - } -} +use service::session::Identifier; /// The type of information passed in the statisticsing channel #[derive(Debug, Clone, Copy)] @@ -200,16 +83,18 @@ impl Counts { /// worker cluster statistics #[derive(Clone)] -pub struct Statistics(Arc>>>); +pub struct Statistics(Arc>>>); impl Default for Statistics { - #[cfg(feature = "api")] + #[cfg(feature = "rpc")] fn default() -> Self { - Self(Arc::new(RwLock::new(AHashMap::with_capacity(1024)))) + use ahash::HashMapExt; + + Self(Arc::new(RwLock::new(HashMap::with_capacity(1024)))) } // There's no need to take up so much memory when you don't have stats enabled. - #[cfg(not(feature = "api"))] + #[cfg(not(feature = "rpc"))] fn default() -> Self { Self(Default::default()) } @@ -224,25 +109,22 @@ impl Statistics { /// # Example /// /// ``` - /// use std::net::SocketAddr; /// use turn_server::statistics::*; - /// use turn_server::stun::Transport; - /// use turn_server::turn::*; + /// use service::session::Identifier; /// /// let statistics = Statistics::default(); - /// let sender = statistics.get_reporter(Transport::UDP); + /// let sender = statistics.get_reporter(); /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), /// interface: "127.0.0.1:3478".parse().unwrap(), /// }; /// /// sender.send(&addr, &[Stats::ReceivedBytes(100)]); /// ``` - pub fn get_reporter(&self, transport: Transport) -> StatisticsReporter { + pub fn get_reporter(&self) -> StatisticsReporter { StatisticsReporter { table: self.0.clone(), - transport, } } @@ -251,26 +133,20 @@ impl Statistics { /// # Example /// /// ``` - /// use std::net::SocketAddr; /// use turn_server::statistics::*; - /// use turn_server::turn::*; + /// use service::session::Identifier; /// /// let statistics = Statistics::default(); /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), /// interface: "127.0.0.1:3478".parse().unwrap(), /// }; /// /// statistics.register(addr.clone()); /// assert_eq!(statistics.get(&addr).is_some(), true); /// ``` - pub fn register(&self, addr: SessionAddr) { - #[cfg(feature = "prometheus")] - { - self::prometheus::METRICS.allocated.inc(); - } - + pub fn register(&self, addr: Identifier) { self.0.write().insert( addr, Counts { @@ -288,14 +164,13 @@ impl Statistics { /// # Example /// /// ``` - /// use std::net::SocketAddr; /// use turn_server::statistics::*; - /// use turn_server::turn::*; + /// use service::session::Identifier; /// /// let statistics = Statistics::default(); /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), /// interface: "127.0.0.1:3478".parse().unwrap(), /// }; /// @@ -305,12 +180,7 @@ impl Statistics { /// statistics.unregister(&addr); /// assert_eq!(statistics.get(&addr).is_some(), false); /// ``` - pub fn unregister(&self, addr: &SessionAddr) { - #[cfg(feature = "prometheus")] - { - self::prometheus::METRICS.allocated.dec(); - } - + pub fn unregister(&self, addr: &Identifier) { self.0.write().remove(addr); } @@ -321,21 +191,20 @@ impl Statistics { /// # Example /// /// ``` - /// use std::net::SocketAddr; /// use turn_server::statistics::*; - /// use turn_server::turn::*; + /// use service::session::Identifier; /// /// let statistics = Statistics::default(); /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), + /// let addr = Identifier { + /// source: "127.0.0.1:8080".parse().unwrap(), /// interface: "127.0.0.1:3478".parse().unwrap(), /// }; /// /// statistics.register(addr.clone()); /// assert_eq!(statistics.get(&addr).is_some(), true); /// ``` - pub fn get(&self, addr: &SessionAddr) -> Option> { + pub fn get(&self, addr: &Identifier) -> Option> { self.0.read().get(addr).map(|counts| Counts { received_bytes: counts.received_bytes.get(), received_pkts: counts.received_pkts.get(), @@ -354,22 +223,14 @@ impl Statistics { #[derive(Clone)] #[allow(unused)] pub struct StatisticsReporter { - table: Arc>>>, - transport: Transport, + table: Arc>>>, } impl StatisticsReporter { #[allow(unused_variables)] - pub fn send(&self, addr: &SessionAddr, reports: &[Stats]) { - #[cfg(feature = "api")] + pub fn send(&self, addr: &Identifier, reports: &[Stats]) { + #[cfg(feature = "rpc")] { - #[cfg(feature = "prometheus")] - { - for report in reports { - self::prometheus::METRICS.add(self.transport, report); - } - } - if let Some(counts) = self.table.read().get(addr) { for item in reports { counts.add(item); diff --git a/src/stun/mod.rs b/src/stun/mod.rs deleted file mode 100644 index 2e6910ea..00000000 --- a/src/stun/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -//! ## Session Traversal Utilities for NAT (STUN) -//! -//! [RFC8445]: https://tools.ietf.org/html/rfc8445 -//! [RFC5626]: https://tools.ietf.org/html/rfc5626 -//! [Section 13]: https://tools.ietf.org/html/rfc8489#section-13 -//! -//! ### STUN Message Structure -//! -//! ```text -//! 0 1 2 3 -//! 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! |0 0| STUN Message Type | Message Length | -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! | Magic Cookie | -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! | | -//! | Transaction ID (96 bits) | -//! | | -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! ``` -//! -//! ### STUN Attributes -//! -//! ```text -//! 0 1 2 3 -//! 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! | Type | Length | -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! | Value (variable) .... -//! +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -//! ``` -//! -//! STUN is intended to be used in the context of one or more NAT -//! traversal solutions. These solutions are known as "STUN Usages". -//! Each usage describes how STUN is utilized to achieve the NAT -//! traversal solution. Typically, a usage indicates when STUN messages -//! get sent, which optional attributes to include, what server is used, -//! and what authentication mechanism is to be used. Interactive -//! Connectivity Establishment (ICE) [RFC8445] is one usage of STUN. -//! SIP Outbound [RFC5626] is another usage of STUN. In some cases, -//! a usage will require extensions to STUN. A STUN extension can be -//! in the form of new methods, attributes, or error response codes. -//! More information on STUN Usages can be found in [Section 13]. - -pub mod attribute; -pub mod channel; -pub mod message; -pub mod util; - -pub use self::{ - attribute::{AttrKind, Transport}, - channel::ChannelData, - message::*, -}; - -use std::ops::Range; - -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum StunError { - #[error("InvalidInput")] - InvalidInput, - #[error("SummaryFailed")] - SummaryFailed, - #[error("NotFoundIntegrity")] - NotFoundIntegrity, - #[error("IntegrityFailed")] - IntegrityFailed, - #[error("NotFoundCookie")] - NotFoundCookie, - #[error("UnknownStunMethod")] - UnknownStunMethod, - #[error("FatalError")] - FatalError, - #[error("Utf8Error: {0}")] - Utf8Error(#[from] std::str::Utf8Error), - #[error("TryFromSliceError: {0}")] - TryFromSliceError(#[from] std::array::TryFromSliceError), -} - -#[rustfmt::skip] -pub mod method { - use super::StunError; - - pub const BINDING_REQUEST: StunMethod = StunMethod::Binding(StunMethodKind::Request); - pub const BINDING_RESPONSE: StunMethod = StunMethod::Binding(StunMethodKind::Response); - pub const BINDING_ERROR: StunMethod = StunMethod::Binding(StunMethodKind::Error); - pub const ALLOCATE_REQUEST: StunMethod = StunMethod::Allocate(StunMethodKind::Request); - pub const ALLOCATE_RESPONSE: StunMethod = StunMethod::Allocate(StunMethodKind::Response); - pub const ALLOCATE_ERROR: StunMethod = StunMethod::Allocate(StunMethodKind::Error); - pub const CREATE_PERMISSION_REQUEST: StunMethod = StunMethod::CreatePermission(StunMethodKind::Request); - pub const CREATE_PERMISSION_RESPONSE: StunMethod = StunMethod::CreatePermission(StunMethodKind::Response); - pub const CREATE_PERMISSION_ERROR: StunMethod = StunMethod::CreatePermission(StunMethodKind::Error); - pub const CHANNEL_BIND_REQUEST: StunMethod = StunMethod::ChannelBind(StunMethodKind::Request); - pub const CHANNEL_BIND_RESPONSE: StunMethod = StunMethod::ChannelBind(StunMethodKind::Response); - pub const CHANNEL_BIND_ERROR: StunMethod = StunMethod::ChannelBind(StunMethodKind::Error); - pub const REFRESH_REQUEST: StunMethod = StunMethod::Refresh(StunMethodKind::Request); - pub const REFRESH_RESPONSE: StunMethod = StunMethod::Refresh(StunMethodKind::Response); - pub const REFRESH_ERROR: StunMethod = StunMethod::Refresh(StunMethodKind::Error); - pub const SEND_INDICATION: StunMethod = StunMethod::SendIndication; - pub const DATA_INDICATION: StunMethod = StunMethod::DataIndication; - - /// STUN StunMethods Registry - /// - /// [RFC5389]: https://datatracker.ietf.org/doc/html/rfc5389 - /// [RFC8489]: https://datatracker.ietf.org/doc/html/rfc8489 - /// [RFC8126]: https://datatracker.ietf.org/doc/html/rfc8126 - /// [Section 5]: https://datatracker.ietf.org/doc/html/rfc8489#section-5 - /// - /// A STUN method is a hex number in the range 0x000-0x0FF. The encoding - /// of a STUN method into a STUN message is described in [Section 5]. - /// - /// STUN methods in the range 0x000-0x07F are assigned by IETF Review - /// [RFC8126]. STUN methods in the range 0x080-0x0FF are assigned by - /// Expert Review [RFC8126]. The responsibility of the expert is to - /// verify that the selected codepoint(s) is not in use and that the - /// request is not for an abnormally large number of codepoints. - /// Technical review of the extension itself is outside the scope of the - /// designated expert responsibility. - /// - /// IANA has updated the name for method 0x002 as described below as well - /// as updated the reference from [RFC5389] to [RFC8489] for the following - /// STUN methods: - /// - /// 0x000: Reserved - /// 0x001: Binding - /// 0x002: Reserved; was SharedSecret prior to [RFC5389] - /// 0x003: Allocate - /// 0x004: Refresh - /// 0x006: Send - /// 0x007: Data - /// 0x008: CreatePermission - /// 0x009: ChannelBind - #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] - pub enum StunMethodKind { - Request, - Response, - Error, - } - - #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)] - pub enum StunMethod { - Binding(StunMethodKind), - Allocate(StunMethodKind), - CreatePermission(StunMethodKind), - ChannelBind(StunMethodKind), - Refresh(StunMethodKind), - SendIndication, - DataIndication, - } - - impl StunMethod { - pub fn is_error(&self) -> bool { - match self { - StunMethod::Binding(StunMethodKind::Error) - | StunMethod::Refresh(StunMethodKind::Error) - | StunMethod::Allocate(StunMethodKind::Error) - | StunMethod::CreatePermission(StunMethodKind::Error) - | StunMethod::ChannelBind(StunMethodKind::Error) => true, - _ => false, - } - } - } - - impl TryFrom for StunMethod { - type Error = StunError; - - /// # Test - /// - /// ``` - /// use turn_server::stun::method::*; - /// use std::convert::TryFrom; - /// - /// assert_eq!(StunMethod::try_from(0x0001).unwrap(), BINDING_REQUEST); - /// assert_eq!(StunMethod::try_from(0x0101).unwrap(), BINDING_RESPONSE); - /// assert_eq!(StunMethod::try_from(0x0111).unwrap(), BINDING_ERROR); - /// assert_eq!(StunMethod::try_from(0x0003).unwrap(), ALLOCATE_REQUEST); - /// assert_eq!(StunMethod::try_from(0x0103).unwrap(), ALLOCATE_RESPONSE); - /// assert_eq!(StunMethod::try_from(0x0113).unwrap(), ALLOCATE_ERROR); - /// assert_eq!(StunMethod::try_from(0x0008).unwrap(), CREATE_PERMISSION_REQUEST); - /// assert_eq!(StunMethod::try_from(0x0108).unwrap(), CREATE_PERMISSION_RESPONSE); - /// assert_eq!(StunMethod::try_from(0x0118).unwrap(), CREATE_PERMISSION_ERROR); - /// assert_eq!(StunMethod::try_from(0x0009).unwrap(), CHANNEL_BIND_REQUEST); - /// assert_eq!(StunMethod::try_from(0x0109).unwrap(), CHANNEL_BIND_RESPONSE); - /// assert_eq!(StunMethod::try_from(0x0119).unwrap(), CHANNEL_BIND_ERROR); - /// assert_eq!(StunMethod::try_from(0x0004).unwrap(), REFRESH_REQUEST); - /// assert_eq!(StunMethod::try_from(0x0104).unwrap(), REFRESH_RESPONSE); - /// assert_eq!(StunMethod::try_from(0x0114).unwrap(), REFRESH_ERROR); - /// assert_eq!(StunMethod::try_from(0x0016).unwrap(), SEND_INDICATION); - /// assert_eq!(StunMethod::try_from(0x0017).unwrap(), DATA_INDICATION); - /// ``` - fn try_from(value: u16) -> Result { - Ok(match value { - 0x0001 => Self::Binding(StunMethodKind::Request), - 0x0101 => Self::Binding(StunMethodKind::Response), - 0x0111 => Self::Binding(StunMethodKind::Error), - 0x0003 => Self::Allocate(StunMethodKind::Request), - 0x0103 => Self::Allocate(StunMethodKind::Response), - 0x0113 => Self::Allocate(StunMethodKind::Error), - 0x0008 => Self::CreatePermission(StunMethodKind::Request), - 0x0108 => Self::CreatePermission(StunMethodKind::Response), - 0x0118 => Self::CreatePermission(StunMethodKind::Error), - 0x0009 => Self::ChannelBind(StunMethodKind::Request), - 0x0109 => Self::ChannelBind(StunMethodKind::Response), - 0x0119 => Self::ChannelBind(StunMethodKind::Error), - 0x0004 => Self::Refresh(StunMethodKind::Request), - 0x0104 => Self::Refresh(StunMethodKind::Response), - 0x0114 => Self::Refresh(StunMethodKind::Error), - 0x0016 => Self::SendIndication, - 0x0017 => Self::DataIndication, - _ => return Err(StunError::UnknownStunMethod), - }) - } - } - - impl Into for StunMethod { - /// # Test - /// - /// ``` - /// use turn_server::stun::method::*; - /// use std::convert::Into; - /// - /// assert_eq!(0x0001u16, >::into(BINDING_REQUEST)); - /// assert_eq!(0x0101u16, >::into(BINDING_RESPONSE)); - /// assert_eq!(0x0111u16, >::into(BINDING_ERROR)); - /// assert_eq!(0x0003u16, >::into(ALLOCATE_REQUEST)); - /// assert_eq!(0x0103u16, >::into(ALLOCATE_RESPONSE)); - /// assert_eq!(0x0113u16, >::into(ALLOCATE_ERROR)); - /// assert_eq!(0x0008u16, >::into(CREATE_PERMISSION_REQUEST)); - /// assert_eq!(0x0108u16, >::into(CREATE_PERMISSION_RESPONSE)); - /// assert_eq!(0x0118u16, >::into(CREATE_PERMISSION_ERROR)); - /// assert_eq!(0x0009u16, >::into(CHANNEL_BIND_REQUEST)); - /// assert_eq!(0x0109u16, >::into(CHANNEL_BIND_RESPONSE)); - /// assert_eq!(0x0119u16, >::into(CHANNEL_BIND_ERROR)); - /// assert_eq!(0x0004u16, >::into(REFRESH_REQUEST)); - /// assert_eq!(0x0104u16, >::into(REFRESH_RESPONSE)); - /// assert_eq!(0x0114u16, >::into(REFRESH_ERROR)); - /// assert_eq!(0x0016u16, >::into(SEND_INDICATION)); - /// assert_eq!(0x0017u16, >::into(DATA_INDICATION)); - /// ``` - fn into(self) -> u16 { - match self { - Self::Binding(StunMethodKind::Request) => 0x0001, - Self::Binding(StunMethodKind::Response) => 0x0101, - Self::Binding(StunMethodKind::Error) => 0x0111, - Self::Allocate(StunMethodKind::Request) => 0x0003, - Self::Allocate(StunMethodKind::Response) => 0x0103, - Self::Allocate(StunMethodKind::Error) => 0x0113, - Self::CreatePermission(StunMethodKind::Request) => 0x0008, - Self::CreatePermission(StunMethodKind::Response) => 0x0108, - Self::CreatePermission(StunMethodKind::Error) => 0x0118, - Self::ChannelBind(StunMethodKind::Request) => 0x0009, - Self::ChannelBind(StunMethodKind::Response) => 0x0109, - Self::ChannelBind(StunMethodKind::Error) => 0x0119, - Self::Refresh(StunMethodKind::Request) => 0x0004, - Self::Refresh(StunMethodKind::Response) => 0x0104, - Self::Refresh(StunMethodKind::Error) => 0x0114, - Self::SendIndication => 0x0016, - Self::DataIndication => 0x0017, - } - } - } -} - -#[derive(Debug)] -pub enum Payload<'a> { - Message(MessageRef<'a>), - ChannelData(ChannelData<'a>), -} - -/// A cache of the list of attributes, this is for internal use only. -#[derive(Debug, Clone)] -pub struct Attributes(Vec<(AttrKind, Range)>); - -impl Default for Attributes { - fn default() -> Self { - Self(Vec::with_capacity(20)) - } -} - -impl Attributes { - /// Adds an attribute to the list. - pub fn append(&mut self, kind: AttrKind, range: Range) { - self.0.push((kind, range)); - } - - /// Gets an attribute from the list. - /// - /// Note: This function will only look for the first matching property in - /// the list and return it. - pub fn get(&self, kind: &AttrKind) -> Option> { - self.0.iter().find(|(k, _)| k == kind).map(|(_, v)| v.clone()) - } - - /// Gets all the values of an attribute from a list. - /// - /// Normally a stun message can have multiple attributes with the same name, - /// and this function will all the values of the current attribute. - pub fn get_all<'a>(&'a self, kind: &'a AttrKind) -> impl Iterator> { - self.0 - .iter() - .filter(move |(k, _)| k == kind) - .map(|(_, v)| v) - .into_iter() - } - - pub fn clear(&mut self) { - if !self.0.is_empty() { - self.0.clear(); - } - } -} - -#[derive(Default)] -pub struct Decoder(Attributes); - -impl Decoder { - /// # Test - /// - /// ``` - /// use turn_server::stun::attribute::*; - /// use turn_server::stun::*; - /// - /// let buffer = [ - /// 0x00, 0x01, 0x00, 0x4c, 0x21, 0x12, 0xa4, 0x42, 0x71, 0x66, 0x46, 0x31, - /// 0x2b, 0x59, 0x79, 0x65, 0x56, 0x69, 0x32, 0x72, 0x00, 0x06, 0x00, 0x09, - /// 0x55, 0x43, 0x74, 0x39, 0x3a, 0x56, 0x2f, 0x2b, 0x2f, 0x00, 0x00, 0x00, - /// 0xc0, 0x57, 0x00, 0x04, 0x00, 0x00, 0x03, 0xe7, 0x80, 0x29, 0x00, 0x08, - /// 0x22, 0x49, 0xda, 0x28, 0x2c, 0x6f, 0x2e, 0xdb, 0x00, 0x24, 0x00, 0x04, - /// 0x6e, 0x00, 0x28, 0xff, 0x00, 0x08, 0x00, 0x14, 0x19, 0x58, 0xda, 0x38, - /// 0xed, 0x1e, 0xdd, 0xc8, 0x6b, 0x8e, 0x22, 0x63, 0x3a, 0x22, 0x63, 0x97, - /// 0xcf, 0xf5, 0xde, 0x82, 0x80, 0x28, 0x00, 0x04, 0x56, 0xf7, 0xa3, 0xed, - /// ]; - /// - /// let mut decoder = Decoder::default(); - /// let payload = decoder.decode(&buffer).unwrap(); - /// if let Payload::Message(reader) = payload { - /// assert!(reader.get::().is_some()) - /// } - /// ``` - pub fn decode<'a>(&'a mut self, bytes: &'a [u8]) -> Result, StunError> { - assert!(bytes.len() >= 4); - - let flag = bytes[0] >> 6; - if flag > 3 { - return Err(StunError::InvalidInput); - } - - Ok(if flag == 0 { - self.0.clear(); - - Payload::Message(MessageDecoder::decode(bytes, &mut self.0)?) - } else { - Payload::ChannelData(ChannelData::try_from(bytes)?) - }) - } - - /// # Test - /// - /// ``` - /// use turn_server::stun::attribute::*; - /// use turn_server::stun::*; - /// - /// let buffer = [ - /// 0x00, 0x01, 0x00, 0x4c, 0x21, 0x12, 0xa4, 0x42, 0x71, 0x66, 0x46, 0x31, - /// 0x2b, 0x59, 0x79, 0x65, 0x56, 0x69, 0x32, 0x72, 0x00, 0x06, 0x00, 0x09, - /// 0x55, 0x43, 0x74, 0x39, 0x3a, 0x56, 0x2f, 0x2b, 0x2f, 0x00, 0x00, 0x00, - /// 0xc0, 0x57, 0x00, 0x04, 0x00, 0x00, 0x03, 0xe7, 0x80, 0x29, 0x00, 0x08, - /// 0x22, 0x49, 0xda, 0x28, 0x2c, 0x6f, 0x2e, 0xdb, 0x00, 0x24, 0x00, 0x04, - /// 0x6e, 0x00, 0x28, 0xff, 0x00, 0x08, 0x00, 0x14, 0x19, 0x58, 0xda, 0x38, - /// 0xed, 0x1e, 0xdd, 0xc8, 0x6b, 0x8e, 0x22, 0x63, 0x3a, 0x22, 0x63, 0x97, - /// 0xcf, 0xf5, 0xde, 0x82, 0x80, 0x28, 0x00, 0x04, 0x56, 0xf7, 0xa3, 0xed, - /// ]; - /// - /// let size = Decoder::message_size(&buffer, false).unwrap(); - /// assert_eq!(size, 96); - /// ``` - pub fn message_size(bytes: &[u8], is_tcp: bool) -> Result { - let flag = bytes[0] >> 6; - if flag > 3 { - return Err(StunError::InvalidInput); - } - - Ok(if flag == 0 { - MessageDecoder::message_size(bytes)? - } else { - ChannelData::message_size(bytes, is_tcp)? - }) - } -} diff --git a/src/stun/util.rs b/src/stun/util.rs deleted file mode 100644 index 35d2ca2c..00000000 --- a/src/stun/util.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crc::{CRC_32_ISO_HDLC, Crc}; -use hmac::{Hmac, Mac, digest::CtOutput}; -use md5::{Digest, Md5}; - -use super::StunError; - -/// compute padding size. -/// -/// RFC5766 stipulates that the attribute -/// content is a multiple of 4. -/// -/// # Test -/// -/// ``` -/// assert_eq!(turn_server::stun::util::pad_size(4), 0); -/// assert_eq!(turn_server::stun::util::pad_size(0), 0); -/// assert_eq!(turn_server::stun::util::pad_size(5), 3); -/// ``` -#[inline(always)] -pub fn pad_size(size: usize) -> usize { - let range = size % 4; - if size == 0 || range == 0 { - return 0; - } - - 4 - range -} - -/// create long term credential. -/// -/// > key = MD5(username ":" OpaqueString(realm) ":" OpaqueString(password)) -/// -/// ``` -/// let buffer = [ -/// 0x3eu8, 0x2f, 0x79, 0x1e, 0x1f, 0x14, 0xd1, 0x73, 0xfc, 0x91, 0xff, -/// 0x2f, 0x59, 0xb5, 0x0f, 0xd1, -/// ]; -/// -/// let key = turn_server::stun::util::long_term_credential_digest( -/// "panda", -/// "panda", -/// "raspberry", -/// ); -/// assert_eq!(key, buffer); -/// ``` -pub fn long_term_credential_digest(username: &str, password: &str, realm: &str) -> [u8; 16] { - let mut hasher = Md5::new(); - hasher.update([username, realm, password].join(":")); - hasher.finalize().into() -} - -/// HMAC SHA1 digest. -/// -/// # Test -/// -/// ``` -/// let buffer = [ -/// 0x00u8, 0x03, 0x00, 0x50, 0x21, 0x12, 0xa4, 0x42, 0x64, 0x4f, 0x5a, -/// 0x78, 0x6a, 0x56, 0x33, 0x62, 0x4b, 0x52, 0x33, 0x31, 0x00, 0x19, 0x00, -/// 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x05, 0x70, 0x61, 0x6e, -/// 0x64, 0x61, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x09, 0x72, 0x61, 0x73, -/// 0x70, 0x62, 0x65, 0x72, 0x72, 0x79, 0x00, 0x00, 0x00, 0x00, 0x15, 0x00, -/// 0x10, 0x31, 0x63, 0x31, 0x33, 0x64, 0x32, 0x62, 0x32, 0x34, 0x35, 0x62, -/// 0x33, 0x61, 0x37, 0x33, 0x34, -/// ]; -/// -/// let key = [ -/// 0x3eu8, 0x2f, 0x79, 0x1e, 0x1f, 0x14, 0xd1, 0x73, 0xfc, 0x91, 0xff, -/// 0x2f, 0x59, 0xb5, 0x0f, 0xd1, -/// ]; -/// -/// let sign = [ -/// 0xd6u8, 0x78, 0x26, 0x99, 0x0e, 0x15, 0x56, 0x15, 0xe5, 0xf4, 0x24, -/// 0x74, 0xe2, 0x3c, 0x26, 0xc5, 0xb1, 0x03, 0xb2, 0x6d, -/// ]; -/// -/// let hmac_output = turn_server::stun::util::hmac_sha1(&key, &[&buffer]) -/// .unwrap() -/// .into_bytes(); -/// assert_eq!(hmac_output.as_slice(), &sign); -/// ``` -pub fn hmac_sha1(key: &[u8], source: &[&[u8]]) -> Result>, StunError> { - match Hmac::::new_from_slice(key) { - Err(_) => Err(StunError::SummaryFailed), - Ok(mut mac) => { - for buf in source { - mac.update(buf); - } - - Ok(mac.finalize()) - } - } -} - -/// CRC32 Fingerprint. -/// -/// # Test -/// -/// ``` -/// assert_eq!(turn_server::stun::util::fingerprint(b"1"), 3498621689); -/// ``` -pub fn fingerprint(bytes: &[u8]) -> u32 { - Crc::::new(&CRC_32_ISO_HDLC).checksum(bytes) ^ 0x5354_554e -} diff --git a/src/turn/operations/allocate.rs b/src/turn/operations/allocate.rs deleted file mode 100644 index cd8da909..00000000 --- a/src/turn/operations/allocate.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::net::SocketAddr; - -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::{ - MessageEncoder, MessageRef, - attribute::{ - Error, ErrorCode, ErrorKind, Lifetime, Nonce, Realm, ReqeestedTransport, Software, XorMappedAddress, - XorRelayedAddress, - }, - method::{ALLOCATE_ERROR, ALLOCATE_RESPONSE}, -}; - -/// return allocate error response -#[inline(always)] -fn reject<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>, err: ErrorKind) -> Option> { - { - let mut message = MessageEncoder::extend(ALLOCATE_ERROR, req.message, req.bytes); - message.append::(Error::from(err)); - message.append::(&req.service.sessions.get_nonce(&req.address).get_ref()?.0); - message.append::(&req.service.realm); - message.flush(None).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(ALLOCATE_ERROR), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// return allocate ok response -/// -/// NOTE: The use of randomized port assignments to avoid certain -/// types of attacks is described in [RFC6056]. It is RECOMMENDED -/// that a TURN server implement a randomized port assignment -/// algorithm from [RFC6056]. This is especially applicable to -/// servers that choose to pre-allocate a number of ports from the -/// underlying OS and then later assign them to allocations; for -/// example, a server may choose this technique to implement the -/// EVEN-PORT attribute. -#[inline(always)] -fn resolve<'a, T: Observer>( - req: Requet<'_, 'a, T, MessageRef<'_>>, - integrity: &[u8; 16], - port: u16, -) -> Option> { - { - let mut message = MessageEncoder::extend(ALLOCATE_RESPONSE, req.message, req.bytes); - message.append::(SocketAddr::new(req.service.interface.ip(), port)); - message.append::(req.address.address); - message.append::(600); - message.append::(&req.service.software); - message.flush(Some(integrity)).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(ALLOCATE_RESPONSE), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// process allocate request -/// -/// [rfc8489](https://tools.ietf.org/html/rfc8489) -/// -/// In all cases, the server SHOULD only allocate ports from the range -/// 49152 - 65535 (the Dynamic and/or Private Port range [PORT-NUMBERS]), -/// unless the TURN server application knows, through some means not -/// specified here, that other applications running on the same host as -/// the TURN server application will not be impacted by allocating ports -/// outside this range. This condition can often be satisfied by running -/// the TURN server application on a dedicated machine and/or by -/// arranging that any other applications on the machine allocate ports -/// before the TURN server application starts. In any case, the TURN -/// server SHOULD NOT allocate ports in the range 0 - 1023 (the Well- -/// Known Port range) to discourage clients from using TURN to run -/// standard services. -pub fn process<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>) -> Option> { - if req.message.get::().is_none() { - return reject(req, ErrorKind::ServerError); - } - - let (username, integrity) = match req.auth() { - None => return reject(req, ErrorKind::Unauthorized), - Some(it) => it, - }; - - let port = match req.service.sessions.allocate(req.address) { - None => return reject(req, ErrorKind::AllocationQuotaReached), - Some(it) => it, - }; - - req.service.observer.allocated(&req.address, username, port); - resolve(req, &integrity, port) -} diff --git a/src/turn/operations/binding.rs b/src/turn/operations/binding.rs deleted file mode 100644 index 2a20d3ce..00000000 --- a/src/turn/operations/binding.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::{ - MessageEncoder, MessageRef, - attribute::{MappedAddress, ResponseOrigin, Software, XorMappedAddress}, - method::BINDING_RESPONSE, -}; - -/// process binding request -/// -/// [rfc8489](https://tools.ietf.org/html/rfc8489) -/// -/// In the Binding request/response transaction, a Binding request is -/// sent from a STUN client to a STUN server. When the Binding request -/// arrives at the STUN server, it may have passed through one or more -/// NATs between the STUN client and the STUN server (in Figure 1, there -/// are two such NATs). As the Binding request message passes through a -/// NAT, the NAT will modify the source transport address (that is, the -/// source IP address and the source port) of the packet. As a result, -/// the source transport address of the request received by the server -/// will be the public IP address and port created by the NAT closest to -/// the server. This is called a "reflexive transport address". The -/// STUN server copies that source transport address into an XOR-MAPPED- -/// ADDRESS attribute in the STUN Binding response and sends the Binding -/// response back to the STUN client. As this packet passes back through -/// a NAT, the NAT will modify the destination transport address in the -/// IP header, but the transport address in the XOR-MAPPED-ADDRESS -/// attribute within the body of the STUN response will remain untouched. -/// In this way, the client can learn its reflexive transport address -/// allocated by the outermost NAT with respect to the STUN server. -pub fn process<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>) -> Option> { - { - let mut message = MessageEncoder::extend(BINDING_RESPONSE, &req.message, req.bytes); - message.append::(req.address.address); - message.append::(req.address.address); - message.append::(req.service.interface); - message.append::(&req.service.software); - message.flush(None).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(BINDING_RESPONSE), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} diff --git a/src/turn/operations/channel_bind.rs b/src/turn/operations/channel_bind.rs deleted file mode 100644 index 13a30b93..00000000 --- a/src/turn/operations/channel_bind.rs +++ /dev/null @@ -1,108 +0,0 @@ -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::{ - MessageEncoder, MessageRef, - attribute::{ChannelNumber, Error, ErrorCode, ErrorKind, Realm, XorPeerAddress}, - method::{CHANNEL_BIND_ERROR, CHANNEL_BIND_RESPONSE}, -}; - -/// return channel binding error response -#[inline(always)] -fn reject<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>, err: ErrorKind) -> Option> { - { - let mut message = MessageEncoder::extend(CHANNEL_BIND_ERROR, req.message, req.bytes); - message.append::(Error::from(err)); - message.append::(&req.service.realm); - message.flush(None).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(CHANNEL_BIND_ERROR), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// return channel binding ok response -#[inline(always)] -fn resolve<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>, integrity: &[u8; 16]) -> Option> { - { - MessageEncoder::extend(CHANNEL_BIND_RESPONSE, req.message, req.bytes) - .flush(Some(integrity)) - .ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(CHANNEL_BIND_RESPONSE), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// process channel binding request -/// -/// The server MAY impose restrictions on the IP address and port values -/// allowed in the XOR-PEER-ADDRESS attribute; if a value is not allowed, -/// the server rejects the request with a 403 (Forbidden) error. -/// -/// If the request is valid, but the server is unable to fulfill the -/// request due to some capacity limit or similar, the server replies -/// with a 508 (Insufficient Capacity) error. -/// -/// Otherwise, the server replies with a ChannelBind success response. -/// There are no required attributes in a successful ChannelBind -/// response. -/// -/// If the server can satisfy the request, then the server creates or -/// refreshes the channel binding using the channel number in the -/// CHANNEL-NUMBER attribute and the transport address in the XOR-PEER- -/// ADDRESS attribute. The server also installs or refreshes a -/// permission for the IP address in the XOR-PEER-ADDRESS attribute as -/// described in Section 9. -/// -/// NOTE: A server need not do anything special to implement -/// idempotency of ChannelBind requests over UDP using the -/// "stateless stack approach". Retransmitted ChannelBind requests -/// will simply refresh the channel binding and the corresponding -/// permission. Furthermore, the client must wait 5 minutes before -/// binding a previously bound channel number or peer address to a -/// different channel, eliminating the possibility that the -/// transaction would initially fail but succeed on a -/// retransmission. -pub fn process<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>) -> Option> { - let peer = match req.message.get::() { - None => return reject(req, ErrorKind::BadRequest), - Some(it) => it, - }; - - if !req.verify_ip(&peer) { - return reject(req, ErrorKind::PeerAddressFamilyMismatch); - } - - let number = match req.message.get::() { - None => return reject(req, ErrorKind::BadRequest), - Some(it) => it, - }; - - if !(0x4000..=0x7FFF).contains(&number) { - return reject(req, ErrorKind::BadRequest); - } - - let (username, integrity) = match req.auth() { - None => return reject(req, ErrorKind::Unauthorized), - Some(it) => it, - }; - - if !req - .service - .sessions - .bind_channel(&req.address, &req.service.endpoint, peer.port(), number) - { - return reject(req, ErrorKind::Forbidden); - } - - req.service.observer.channel_bind(&req.address, username, number); - resolve(req, &integrity) -} diff --git a/src/turn/operations/channel_data.rs b/src/turn/operations/channel_data.rs deleted file mode 100644 index a8db3ccc..00000000 --- a/src/turn/operations/channel_data.rs +++ /dev/null @@ -1,49 +0,0 @@ -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::ChannelData; - -/// process channel data -/// -/// If the ChannelData message is received on a channel that is not bound -/// to any peer, then the message is silently discarded. -/// -/// On the client, it is RECOMMENDED that the client discard the -/// ChannelData message if the client believes there is no active -/// permission towards the peer. On the server, the receipt of a -/// ChannelData message MUST NOT refresh either the channel binding or -/// the permission towards the peer. -/// -/// On the server, if no errors are detected, the server relays the -/// application data to the peer by forming a UDP datagram as follows: -/// -/// * the source transport address is the relayed transport address of the -/// allocation, where the allocation is determined by the 5-tuple on which the -/// ChannelData message arrived; -/// -/// * the destination transport address is the transport address to which the -/// channel is bound; -/// -/// * the data following the UDP header is the contents of the data field of the -/// ChannelData message. -/// -/// The resulting UDP datagram is then sent to the peer. Note that if -/// the Length field in the ChannelData message is 0, then there will be -/// no data in the UDP datagram, but the UDP datagram is still formed and -/// sent [(Section 4.1 of [RFC6263])](https://tools.ietf.org/html/rfc6263#section-4.1). -pub fn process<'a, T: Observer>(bytes: &'a [u8], req: Requet<'_, 'a, T, ChannelData<'a>>) -> Option> { - let relay = req - .service - .sessions - .get_channel_relay_address(&req.address, req.message.number)?; - - Some(Response { - method: ResponseMethod::ChannelData, - endpoint: if req.service.endpoint != relay.endpoint { - Some(relay.endpoint) - } else { - None - }, - relay: Some(relay.address), - bytes, - }) -} diff --git a/src/turn/operations/create_permission.rs b/src/turn/operations/create_permission.rs deleted file mode 100644 index 5565de28..00000000 --- a/src/turn/operations/create_permission.rs +++ /dev/null @@ -1,108 +0,0 @@ -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::{ - MessageEncoder, MessageRef, - attribute::{Error, ErrorCode, ErrorKind, Realm, Software, XorPeerAddress}, - method::{CREATE_PERMISSION_ERROR, CREATE_PERMISSION_RESPONSE}, -}; - -/// return create permission error response -#[inline(always)] -fn reject<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>, err: ErrorKind) -> Option> { - { - let mut message = MessageEncoder::extend(CREATE_PERMISSION_ERROR, req.message, req.bytes); - message.append::(Error::from(err)); - message.append::(&req.service.realm); - message.flush(None).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(CREATE_PERMISSION_ERROR), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// return create permission ok response -#[inline(always)] -fn resolve<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>, integrity: &[u8; 16]) -> Option> { - { - let mut message = MessageEncoder::extend(CREATE_PERMISSION_RESPONSE, req.message, req.bytes); - message.append::(&req.service.software); - message.flush(Some(integrity)).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(CREATE_PERMISSION_RESPONSE), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// process create permission request -/// -/// [rfc8489](https://tools.ietf.org/html/rfc8489) -/// -/// When the server receives the CreatePermission request, it processes -/// as per [Section 5](https://tools.ietf.org/html/rfc8656#section-5) -/// plus the specific rules mentioned here. -/// -/// The message is checked for validity. The CreatePermission request -/// MUST contain at least one XOR-PEER-ADDRESS attribute and MAY contain -/// multiple such attributes. If no such attribute exists, or if any of -/// these attributes are invalid, then a 400 (Bad Request) error is -/// returned. If the request is valid, but the server is unable to -/// satisfy the request due to some capacity limit or similar, then a 508 -/// (Insufficient Capacity) error is returned. -/// -/// If an XOR-PEER-ADDRESS attribute contains an address of an address -/// family that is not the same as that of a relayed transport address -/// for the allocation, the server MUST generate an error response with -/// the 443 (Peer Address Family Mismatch) response code. -/// -/// The server MAY impose restrictions on the IP address allowed in the -/// XOR-PEER-ADDRESS attribute; if a value is not allowed, the server -/// rejects the request with a 403 (Forbidden) error. -/// -/// If the message is valid and the server is capable of carrying out the -/// request, then the server installs or refreshes a permission for the -/// IP address contained in each XOR-PEER-ADDRESS attribute as described -/// in [Section 9](https://tools.ietf.org/html/rfc8656#section-9). -/// The port portion of each attribute is ignored and may be any arbitrary -/// value. -/// -/// The server then responds with a CreatePermission success response. -/// There are no mandatory attributes in the success response. -/// -/// > NOTE: A server need not do anything special to implement idempotency of -/// > CreatePermission requests over UDP using the "stateless stack approach". -/// > Retransmitted CreatePermission requests will simply refresh the -/// > permissions. -pub fn process<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>) -> Option> { - let (username, integrity) = match req.auth() { - None => return reject(req, ErrorKind::Unauthorized), - Some(it) => it, - }; - - let mut ports = Vec::with_capacity(15); - for it in req.message.get_all::() { - if !req.verify_ip(&it) { - return reject(req, ErrorKind::PeerAddressFamilyMismatch); - } - - ports.push(it.port()); - } - - if !req - .service - .sessions - .create_permission(&req.address, &req.service.endpoint, &ports) - { - return reject(req, ErrorKind::Forbidden); - } - - req.service.observer.create_permission(&req.address, username, &ports); - resolve(req, &integrity) -} diff --git a/src/turn/operations/indication.rs b/src/turn/operations/indication.rs deleted file mode 100644 index d15d80c5..00000000 --- a/src/turn/operations/indication.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::net::SocketAddr; - -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::{ - MessageEncoder, MessageRef, - attribute::{Data, XorPeerAddress}, - method::DATA_INDICATION, -}; - -/// process send indication request -/// -/// When the server receives a Send indication, it processes as per -/// [Section 5](https://tools.ietf.org/html/rfc8656#section-5) plus -/// the specific rules mentioned here. -/// -/// The message is first checked for validity. The Send indication MUST -/// contain both an XOR-PEER-ADDRESS attribute and a DATA attribute. If -/// one of these attributes is missing or invalid, then the message is -/// discarded. Note that the DATA attribute is allowed to contain zero -/// bytes of data. -/// -/// The Send indication may also contain the DONT-FRAGMENT attribute. If -/// the server is unable to set the DF bit on outgoing UDP datagrams when -/// this attribute is present, then the server acts as if the DONT- -/// FRAGMENT attribute is an unknown comprehension-required attribute -/// (and thus the Send indication is discarded). -/// -/// The server also checks that there is a permission installed for the -/// IP address contained in the XOR-PEER-ADDRESS attribute. If no such -/// permission exists, the message is discarded. Note that a Send -/// indication never causes the server to refresh the permission. -/// -/// The server MAY impose restrictions on the IP address and port values -/// allowed in the XOR-PEER-ADDRESS attribute; if a value is not allowed, -/// the server silently discards the Send indication. -/// -/// If everything is OK, then the server forms a UDP datagram as follows: -/// -/// * the source transport address is the relayed transport address of the -/// allocation, where the allocation is determined by the 5-tuple on which the -/// Send indication arrived; -/// -/// * the destination transport address is taken from the XOR-PEER-ADDRESS -/// attribute; -/// -/// * the data following the UDP header is the contents of the value field of -/// the DATA attribute. -/// -/// The handling of the DONT-FRAGMENT attribute (if present), is -/// described in Sections [14](https://tools.ietf.org/html/rfc8656#section-14) -/// and [15](https://tools.ietf.org/html/rfc8656#section-15). -/// -/// The resulting UDP datagram is then sent to the peer. -pub fn process<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>) -> Option> { - let peer = req.message.get::()?; - let data = req.message.get::()?; - - let relay = req.service.sessions.get_relay_address(&req.address, peer.port())?; - let local_port = req - .service - .sessions - .get_session(&req.address) - .get_ref()? - .allocate - .port?; - - { - let mut message = MessageEncoder::extend(DATA_INDICATION, &req.message, req.bytes); - message.append::(SocketAddr::new(req.service.interface.ip(), local_port)); - message.append::(data); - message.flush(None).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(DATA_INDICATION), - endpoint: if req.service.endpoint != relay.endpoint { - Some(relay.endpoint) - } else { - None - }, - relay: Some(relay.address), - bytes: req.bytes, - }) -} diff --git a/src/turn/operations/mod.rs b/src/turn/operations/mod.rs deleted file mode 100644 index a2941887..00000000 --- a/src/turn/operations/mod.rs +++ /dev/null @@ -1,316 +0,0 @@ -pub mod allocate; -pub mod binding; -pub mod channel_bind; -pub mod channel_data; -pub mod create_permission; -pub mod indication; -pub mod refresh; - -use super::{ - Observer, - sessions::{SessionAddr, Sessions}, -}; - -use crate::stun::{ - Decoder, MessageRef, Payload, StunError, - attribute::{Nonce, UserName}, - method::{ - ALLOCATE_REQUEST, BINDING_REQUEST, CHANNEL_BIND_REQUEST, CREATE_PERMISSION_REQUEST, REFRESH_REQUEST, - SEND_INDICATION, StunMethod, - }, -}; - -use std::{net::SocketAddr, sync::Arc}; - -use bytes::BytesMut; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ResponseMethod { - Stun(StunMethod), - ChannelData, -} - -/// The context of the service. -/// -/// A service corresponds to a Net Endpoint, different sockets have different -/// addresses and so on, but other things are basically the same. -pub struct ServiceContext { - pub realm: String, - pub software: String, - pub sessions: Arc>, - pub endpoint: SocketAddr, - pub interface: SocketAddr, - pub interfaces: Arc>, - pub observer: T, -} - -/// The request of the service. -pub struct Requet<'a, 'b, T, M> -where - T: Observer + 'static, -{ - pub address: &'a SessionAddr, - pub bytes: &'b mut BytesMut, - pub service: &'a ServiceContext, - pub message: &'a M, -} - -impl<'a, 'b, T> Requet<'a, 'b, T, MessageRef<'a>> -where - T: Observer + 'static, -{ - /// Check if the ip address belongs to the current turn server. - #[inline(always)] - pub fn verify_ip(&self, address: &SocketAddr) -> bool { - self.service.interfaces.iter().any(|item| item.ip() == address.ip()) - } - - /// The key for the HMAC depends on whether long-term or short-term - /// credentials are in use. For long-term credentials, the key is 16 - /// bytes: - /// - /// key = MD5(username ":" realm ":" SASLprep(password)) - /// - /// That is, the 16-byte key is formed by taking the MD5 hash of the - /// result of concatenating the following five fields: (1) the username, - /// with any quotes and trailing nulls removed, as taken from the - /// USERNAME attribute (in which case SASLprep has already been applied); - /// (2) a single colon; (3) the realm, with any quotes and trailing nulls - /// removed; (4) a single colon; and (5) the password, with any trailing - /// nulls removed and after processing using SASLprep. For example, if - /// the username was 'user', the realm was 'realm', and the password was - /// 'pass', then the 16-byte HMAC key would be the result of performing - /// an MD5 hash on the string 'user:realm:pass', the resulting hash being - /// 0x8493fbc53ba582fb4c044c456bdc40eb. - /// - /// For short-term credentials: - /// - /// key = SASLprep(password) - /// - /// where MD5 is defined in RFC 1321 [RFC1321] and SASLprep() is defined - /// in RFC 4013 [RFC4013]. - /// - /// The structure of the key when used with long-term credentials - /// facilitates deployment in systems that also utilize SIP. Typically, - /// SIP systems utilizing SIP's digest authentication mechanism do not - /// actually store the password in the database. Rather, they store a - /// value called H(A1), which is equal to the key defined above. - /// - /// Based on the rules above, the hash used to construct MESSAGE- - /// INTEGRITY includes the length field from the STUN message header. - /// Prior to performing the hash, the MESSAGE-INTEGRITY attribute MUST be - /// inserted into the message (with dummy content). The length MUST then - /// be set to point to the length of the message up to, and including, - /// the MESSAGE-INTEGRITY attribute itself, but excluding any attributes - /// after it. Once the computation is performed, the value of the - /// MESSAGE-INTEGRITY attribute can be filled in, and the value of the - /// length in the STUN header can be set to its correct value -- the - /// length of the entire message. Similarly, when validating the - /// MESSAGE-INTEGRITY, the length field should be adjusted to point to - /// the end of the MESSAGE-INTEGRITY attribute prior to calculating the - /// HMAC. Such adjustment is necessary when attributes, such as - /// FINGERPRINT, appear after MESSAGE-INTEGRITY. - #[inline(always)] - pub fn auth(&self) -> Option<(&str, [u8; 16])> { - let username = self.message.get::()?; - let integrity = self - .service - .sessions - .get_integrity(&self.address, username, self.service.realm.as_str())?; - - // if nonce is not empty, check nonce - if let Some(nonce) = self.message.get::() { - if self.service.sessions.get_nonce(&self.address).get_ref()?.0.as_str() != nonce { - return None; - } - } - - self.message.integrity(&integrity).ok()?; - Some((username, integrity)) - } -} - -/// The response of the service. -pub struct Response<'a> { - pub bytes: &'a [u8], - pub method: ResponseMethod, - pub relay: Option, - pub endpoint: Option, -} - -/// process udp message and return message + address -pub struct Operationer -where - T: Observer + 'static, -{ - service: ServiceContext, - address: SessionAddr, - decoder: Decoder, - bytes: BytesMut, -} - -impl Operationer -where - T: Observer + 'static, -{ - pub fn new(service: ServiceContext) -> Self { - Self { - address: SessionAddr { - address: "0.0.0.0:0".parse().unwrap(), - interface: service.interface, - }, - bytes: BytesMut::with_capacity(4096), - decoder: Decoder::default(), - service, - } - } - - /// process udp data - /// - /// receive STUN encoded Bytes, - /// and return any Bytes that can be responded to and the target address. - /// Note: unknown message is not process. - /// - /// In a typical configuration, a TURN client is connected to a private - /// network [RFC1918] and, through one or more NATs, to the public - /// Internet. On the public Internet is a TURN server. Elsewhere in the - /// Internet are one or more peers with which the TURN client wishes to - /// communicate. These peers may or may not be behind one or more NATs. - /// The client uses the server as a relay to send packets to these peers - /// and to receive packets from these peers. - /// - /// ```text - /// Peer A - /// Server-Reflexive +---------+ - /// Transport Address | | - /// 192.0.2.150:32102 | | - /// | /| | - /// TURN | / ^| Peer A | - /// Client's Server | / || | - /// Host Transport Transport | // || | - /// Address Address | // |+---------+ - /// 198.51.100.2:49721 192.0.2.15:3478 |+-+ // Peer A - /// | | ||N| / Host Transport - /// | +-+ | ||A|/ Address - /// | | | | v|T| 203.0.113.2:49582 - /// | | | | /+-+ - /// +---------+| | | |+---------+ / +---------+ - /// | || |N| || | // | | - /// | TURN |v | | v| TURN |/ | | - /// | Client |----|A|-------| Server |------------------| Peer B | - /// | | | |^ | |^ ^| | - /// | | |T|| | || || | - /// +---------+ | || +---------+| |+---------+ - /// | || | | - /// | || | | - /// +-+| | | - /// | | | - /// | | | - /// Client's | Peer B - /// Server-Reflexive Relayed Transport - /// Transport Address Transport Address Address - /// 192.0.2.1:7000 192.0.2.15:50000 192.0.2.210:49191 - /// - /// Figure 1 - /// ``` - /// - /// Figure 1 shows a typical deployment. In this figure, the TURN client - /// and the TURN server are separated by a NAT, with the client on the - /// private side and the server on the public side of the NAT. This NAT - /// is assumed to be a "bad" NAT; for example, it might have a mapping - /// property of "address-and-port-dependent mapping" (see [RFC4787]). - /// - /// The client talks to the server from a (IP address, port) combination - /// called the client's "host transport address". (The combination of an - /// IP address and port is called a "transport address".) - /// - /// The client sends TURN messages from its host transport address to a - /// transport address on the TURN server that is known as the "TURN - /// server transport address". The client learns the TURN server - /// transport address through some unspecified means (e.g., - /// configuration), and this address is typically used by many clients - /// simultaneously. - /// - /// Since the client is behind a NAT, the server sees packets from the - /// client as coming from a transport address on the NAT itself. This - /// address is known as the client's "server-reflexive transport - /// address"; packets sent by the server to the client's server-reflexive - /// transport address will be forwarded by the NAT to the client's host - /// transport address. - /// - /// The client uses TURN commands to create and manipulate an ALLOCATION - /// on the server. An allocation is a data structure on the server. - /// This data structure contains, amongst other things, the relayed - /// transport address for the allocation. The relayed transport address - /// is the transport address on the server that peers can use to have the - /// server relay data to the client. An allocation is uniquely - /// identified by its relayed transport address. - /// - /// Once an allocation is created, the client can send application data - /// to the server along with an indication of to which peer the data is - /// to be sent, and the server will relay this data to the intended peer. - /// The client sends the application data to the server inside a TURN - /// message; at the server, the data is extracted from the TURN message - /// and sent to the peer in a UDP datagram. In the reverse direction, a - /// peer can send application data in a UDP datagram to the relayed - /// transport address for the allocation; the server will then - /// encapsulate this data inside a TURN message and send it to the client - /// along with an indication of which peer sent the data. Since the TURN - /// message always contains an indication of which peer the client is - /// communicating with, the client can use a single allocation to - /// communicate with multiple peers. - /// - /// When the peer is behind a NAT, the client must identify the peer - /// using its server-reflexive transport address rather than its host - /// transport address. For example, to send application data to Peer A - /// in the example above, the client must specify 192.0.2.150:32102 (Peer - /// A's server-reflexive transport address) rather than 203.0.113.2:49582 - /// (Peer A's host transport address). - /// - /// Each allocation on the server belongs to a single client and has - /// either one or two relayed transport addresses that are used only by - /// that allocation. Thus, when a packet arrives at a relayed transport - /// address on the server, the server knows for which client the data is - /// intended. - /// - /// The client may have multiple allocations on a server at the same - /// time. - pub fn route<'a, 'b: 'a>( - &'b mut self, - bytes: &'b [u8], - address: SocketAddr, - ) -> Result>, StunError> { - self.address.address = address; - - Ok(match self.decoder.decode(bytes)? { - Payload::ChannelData(channel) => channel_data::process( - bytes, - Requet { - bytes: &mut self.bytes, - service: &self.service, - address: &self.address, - message: &channel, - }, - ), - Payload::Message(message) => { - let method = message.method(); - let req = Requet { - bytes: &mut self.bytes, - service: &self.service, - address: &self.address, - message: &message, - }; - - match method { - BINDING_REQUEST => binding::process(req), - ALLOCATE_REQUEST => allocate::process(req), - CREATE_PERMISSION_REQUEST => create_permission::process(req), - CHANNEL_BIND_REQUEST => channel_bind::process(req), - REFRESH_REQUEST => refresh::process(req), - SEND_INDICATION => indication::process(req), - _ => None, - } - } - }) - } -} diff --git a/src/turn/operations/refresh.rs b/src/turn/operations/refresh.rs deleted file mode 100644 index 01dc91b7..00000000 --- a/src/turn/operations/refresh.rs +++ /dev/null @@ -1,98 +0,0 @@ -use super::{Observer, Requet, Response, ResponseMethod}; - -use crate::stun::{ - MessageEncoder, MessageRef, - attribute::{Error, ErrorCode, ErrorKind, Lifetime}, - method::{REFRESH_ERROR, REFRESH_RESPONSE}, -}; - -/// return refresh error response -#[inline(always)] -fn reject<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>, err: ErrorKind) -> Option> { - { - let mut message = MessageEncoder::extend(REFRESH_ERROR, &req.message, req.bytes); - message.append::(Error::from(err)); - message.flush(None).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(REFRESH_ERROR), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// return refresh ok response -#[inline(always)] -pub fn resolve<'a, T: Observer>( - req: Requet<'_, 'a, T, MessageRef<'_>>, - lifetime: u32, - integrity: &[u8; 16], -) -> Option> { - { - let mut message = MessageEncoder::extend(REFRESH_RESPONSE, &req.message, req.bytes); - message.append::(lifetime); - message.flush(Some(integrity)).ok()?; - } - - Some(Response { - method: ResponseMethod::Stun(REFRESH_RESPONSE), - bytes: req.bytes, - endpoint: None, - relay: None, - }) -} - -/// process refresh request -/// -/// If the server receives a Refresh Request with a REQUESTED-ADDRESS- -/// FAMILY attribute and the attribute value does not match the address -/// family of the allocation, the server MUST reply with a 443 (Peer -/// Address Family Mismatch) Refresh error response. -/// -/// The server computes a value called the "desired lifetime" as follows: -/// if the request contains a LIFETIME attribute and the attribute value -/// is zero, then the "desired lifetime" is zero. Otherwise, if the -/// request contains a LIFETIME attribute, then the server computes the -/// minimum of the client's requested lifetime and the server's maximum -/// allowed lifetime. If this computed value is greater than the default -/// lifetime, then the "desired lifetime" is the computed value. -/// Otherwise, the "desired lifetime" is the default lifetime. -/// -/// Subsequent processing depends on the "desired lifetime" value: -/// -/// * If the "desired lifetime" is zero, then the request succeeds and the -/// allocation is deleted. -/// -/// * If the "desired lifetime" is non-zero, then the request succeeds and the -/// allocation's time-to-expiry is set to the "desired lifetime". -/// -/// If the request succeeds, then the server sends a success response -/// containing: -/// -/// * A LIFETIME attribute containing the current value of the time-to-expiry -/// timer. -/// -/// NOTE: A server need not do anything special to implement -/// idempotency of Refresh requests over UDP using the "stateless -/// stack approach". Retransmitted Refresh requests with a non- -/// zero "desired lifetime" will simply refresh the allocation. A -/// retransmitted Refresh request with a zero "desired lifetime" -/// will cause a 437 (Allocation Mismatch) response if the -/// allocation has already been deleted, but the client will treat -/// this as equivalent to a success response (see below). -pub fn process<'a, T: Observer>(req: Requet<'_, 'a, T, MessageRef<'_>>) -> Option> { - let (username, integrity) = match req.auth() { - None => return reject(req, ErrorKind::Unauthorized), - Some(it) => it, - }; - - let lifetime = req.message.get::().unwrap_or(600); - if !req.service.sessions.refresh(&req.address, lifetime) { - return reject(req, ErrorKind::AllocationMismatch); - } - - req.service.observer.refresh(&req.address, username, lifetime); - resolve(req, lifetime, &integrity) -} diff --git a/src/turn/sessions.rs b/src/turn/sessions.rs deleted file mode 100644 index 0b83f3a1..00000000 --- a/src/turn/sessions.rs +++ /dev/null @@ -1,1258 +0,0 @@ -use super::Observer; -use crate::stun::util::long_term_credential_digest; - -use std::{ - hash::Hash, - net::SocketAddr, - ops::{Deref, DerefMut, Range}, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, - thread::{self, sleep}, - time::Duration, -}; - -use ahash::{HashMap, HashMapExt}; -use parking_lot::{Mutex, RwLock, RwLockReadGuard}; -use rand::{Rng, distr::Alphanumeric}; - -/// Authentication information for the session. -/// -/// Digest data is data that summarises usernames and passwords by means of -/// long-term authentication. -#[derive(Debug, Clone)] -pub struct Auth { - pub username: String, - pub integrity: [u8; 16], -} - -/// Assignment information for the session. -/// -/// Sessions are all bound to only one port and one channel. -#[derive(Debug, Clone)] -pub struct Allocate { - pub port: Option, - pub channels: Vec, -} - -/// turn session information. -/// -/// A user can have many sessions. -/// -/// The default survival time for a session is 600 seconds. -#[derive(Debug, Clone)] -pub struct Session { - pub auth: Auth, - pub allocate: Allocate, - pub permissions: Vec, - pub expires: u64, -} - -/// The identifier of the session or addr. -/// -/// Each session needs to be identified by a combination of three pieces of -/// information: the addr address, and the transport protocol. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct SessionAddr { - pub address: SocketAddr, - pub interface: SocketAddr, -} - -/// The addr used to record the current session. -/// -/// This is used when forwarding data. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Endpoint { - pub address: SocketAddr, - pub endpoint: SocketAddr, -} - -/// A specially optimised timer. -/// -/// This timer does not stack automatically and needs to be stacked externally -/// and manually. -/// -/// ``` -/// use turn_server::turn::sessions::Timer; -/// -/// let timer = Timer::default(); -/// -/// assert_eq!(timer.get(), 0); -/// assert_eq!(timer.add(), 1); -/// assert_eq!(timer.get(), 1); -/// ``` -#[derive(Default)] -pub struct Timer(AtomicU64); - -impl Timer { - pub fn get(&self) -> u64 { - self.0.load(Ordering::Relaxed) - } - - pub fn add(&self) -> u64 { - self.0.fetch_add(1, Ordering::Relaxed) + 1 - } -} - -#[derive(Default)] -pub struct State { - sessions: RwLock>, - port_allocate_pool: Mutex, - // Records the sessions corresponding to each assigned port, which will be needed when looking - // up sessions assigned to this port based on the port number. - port_mapping_table: RwLock>, - // Records the nonce value for each network connection, which is independent of the session - // because it can exist before it is authenticated. - address_nonce_tanle: RwLock>, - // Stores the address to which the session should be forwarded when it sends indication to a - // port. This is written when permissions are created to allow a certain address to be - // forwarded to the current session. - port_relay_table: RwLock>>, - // Indicates to which session the data sent by a session to a channel should be forwarded. - channel_relay_table: RwLock>>, -} - -pub struct Sessions { - timer: Timer, - state: State, - observer: T, -} - -impl Sessions { - pub fn new(observer: T) -> Arc { - let this = Arc::new(Self { - state: State::default(), - timer: Timer::default(), - observer, - }); - - // This is a background thread that silently handles expiring sessions and - // cleans up session information when it expires. - let this_ = Arc::downgrade(&this); - thread::spawn(move || { - let mut address = Vec::with_capacity(255); - - while let Some(this) = this_.upgrade() { - // The timer advances one second and gets the current time offset. - let now = this.timer.add(); - - // This is the part that deletes the session information. - { - // Finds sessions that have expired. - { - this.state - .sessions - .read() - .iter() - .filter(|(_, v)| v.expires <= now) - .for_each(|(k, _)| address.push(*k)); - } - - // Delete the expired sessions. - if !address.is_empty() { - this.remove_session(&address); - address.clear(); - } - } - - // Because nonce does not follow session creation, nonce is created for each - // addr, so nonce deletion is handled independently. - { - this.state - .address_nonce_tanle - .read() - .iter() - .filter(|(_, v)| v.1 <= now) - .for_each(|(k, _)| address.push(*k)); - - if !address.is_empty() { - this.remove_nonce(&address); - address.clear(); - } - } - - // Fixing a second tick. - sleep(Duration::from_secs(1)); - } - }); - - this - } - - fn remove_session(&self, addrs: &[SessionAddr]) { - let mut sessions = self.state.sessions.write(); - let mut port_allocate_pool = self.state.port_allocate_pool.lock(); - let mut port_mapping_table = self.state.port_mapping_table.write(); - let mut port_relay_table = self.state.port_relay_table.write(); - let mut channel_relay_table = self.state.channel_relay_table.write(); - - addrs.iter().for_each(|k| { - port_relay_table.remove(k); - channel_relay_table.remove(k); - - if let Some(session) = sessions.remove(k) { - // Removes the session-bound port from the port binding table and - // releases the port back into the allocation pool. - if let Some(port) = session.allocate.port { - port_mapping_table.remove(&port); - port_allocate_pool.restore(port); - } - - // Notifies that the external session has been closed. - self.observer.closed(k, &session.auth.username); - } - }); - } - - fn remove_nonce(&self, addrs: &[SessionAddr]) { - let mut address_nonce_tanle = self.state.address_nonce_tanle.write(); - - addrs.iter().for_each(|k| { - address_nonce_tanle.remove(k); - }); - } - - /// Get session for addr. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// assert!(sessions.get_session(&addr).get_ref().is_none()); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// - /// let lock = sessions.get_session(&addr); - /// let session = lock.get_ref().unwrap(); - /// assert_eq!(session.auth.username, "test"); - /// assert_eq!(session.allocate.port, None); - /// assert_eq!(session.allocate.channels.len(), 0); - /// ``` - pub fn get_session<'a, 'b>( - &'a self, - key: &'b SessionAddr, - ) -> ReadLock<'b, 'a, SessionAddr, Table> { - ReadLock { - lock: self.state.sessions.read(), - key, - } - } - - /// Get nonce for addr. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest {} - /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// let a = sessions.get_nonce(&addr).get_ref().unwrap().clone(); - /// assert!(a.0.len() == 16); - /// assert!(a.1 == 600 || a.1 == 601 || a.1 == 602); - /// - /// let b = sessions.get_nonce(&addr).get_ref().unwrap().clone(); - /// assert_eq!(a.0, b.0); - /// assert!(b.1 == 600 || b.1 == 601 || b.1 == 602); - /// ``` - pub fn get_nonce<'a, 'b>( - &'a self, - key: &'b SessionAddr, - ) -> ReadLock<'b, 'a, SessionAddr, Table> { - // If no nonce is created, create a new one. - { - if !self.state.address_nonce_tanle.read().contains_key(key) { - self.state.address_nonce_tanle.write().insert( - *key, - ( - // A random string of length 16. - { - let mut rng = rand::rng(); - std::iter::repeat(()) - .map(|_| rng.sample(Alphanumeric) as char) - .take(16) - .collect::() - .to_lowercase() - }, - // Current time stacks for 600 seconds. - self.timer.get() + 600, - ), - ); - } - } - - ReadLock { - lock: self.state.address_nonce_tanle.read(), - key, - } - } - - /// Get digest for addr. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// assert_eq!(sessions.get_integrity(&addr, "test1", "test"), None); - /// - /// assert_eq!(sessions.get_integrity(&addr, "test", "test"), Some(digest)); - /// - /// assert_eq!(sessions.get_integrity(&addr, "test", "test"), Some(digest)); - /// ``` - pub fn get_integrity(&self, addr: &SessionAddr, username: &str, realm: &str) -> Option<[u8; 16]> { - // Already authenticated, get the cached digest directly. - { - if let Some(it) = self.state.sessions.read().get(addr) { - return Some(it.auth.integrity); - } - } - - // Get the current user's password from an external observer and create a - // digest. - let password = self.observer.get_password(username)?; - let integrity = long_term_credential_digest(&username, &password, realm); - - // Record a new session. - { - self.state.sessions.write().insert( - *addr, - Session { - permissions: Vec::with_capacity(10), - expires: self.timer.get() + 600, - auth: Auth { - username: username.to_string(), - integrity, - }, - allocate: Allocate { - channels: Vec::with_capacity(10), - port: None, - }, - }, - ); - } - - Some(integrity) - } - - pub fn allocated(&self) -> usize { - self.state.port_allocate_pool.lock().len() - } - - /// Assign a port number to the session. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// - /// { - /// let lock = sessions.get_session(&addr); - /// let session = lock.get_ref().unwrap(); - /// assert_eq!(session.auth.username, "test"); - /// assert_eq!(session.allocate.port, None); - /// assert_eq!(session.allocate.channels.len(), 0); - /// } - /// - /// let port = sessions.allocate(&addr).unwrap(); - /// { - /// let lock = sessions.get_session(&addr); - /// let session = lock.get_ref().unwrap(); - /// assert_eq!(session.auth.username, "test"); - /// assert_eq!(session.allocate.port, Some(port)); - /// assert_eq!(session.allocate.channels.len(), 0); - /// } - /// - /// assert_eq!(sessions.allocate(&addr), Some(port)); - /// ``` - pub fn allocate(&self, addr: &SessionAddr) -> Option { - let mut lock = self.state.sessions.write(); - let session = lock.get_mut(addr)?; - - // If the port has already been allocated, re-allocation is not allowed. - if let Some(port) = session.allocate.port { - return Some(port); - } - - // Records the port assigned to the current session and resets the alive time. - let port = self.state.port_allocate_pool.lock().alloc(None)?; - session.expires = self.timer.get() + 600; - session.allocate.port = Some(port); - - // Write the allocation port binding table. - self.state.port_mapping_table.write().insert(port, *addr); - Some(port) - } - - /// Create permission for session. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let endpoint = "127.0.0.1:3478".parse().unwrap(); - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let peer_addr = SessionAddr { - /// address: "127.0.0.1:8081".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// sessions.get_integrity(&peer_addr, "test", "test"); - /// - /// let port = sessions.allocate(&addr).unwrap(); - /// let peer_port = sessions.allocate(&peer_addr).unwrap(); - /// - /// assert!(!sessions.create_permission(&addr, &endpoint, &[port])); - /// assert!(sessions.create_permission(&addr, &endpoint, &[peer_port])); - /// - /// assert!(!sessions.create_permission(&peer_addr, &endpoint, &[peer_port])); - /// assert!(sessions.create_permission(&peer_addr, &endpoint, &[port])); - /// ``` - pub fn create_permission(&self, addr: &SessionAddr, endpoint: &SocketAddr, ports: &[u16]) -> bool { - let mut sessions = self.state.sessions.write(); - let mut port_relay_table = self.state.port_relay_table.write(); - let port_mapping_table = self.state.port_mapping_table.read(); - - // Finds information about the current session. - let session = if let Some(it) = sessions.get_mut(addr) { - it - } else { - return false; - }; - - // The port number assigned to the current session. - let local_port = if let Some(it) = session.allocate.port { - it - } else { - return false; - }; - - // You cannot create permissions for yourself. - if ports.contains(&local_port) { - return false; - } - - // Each peer port must be present. - let mut peers = Vec::with_capacity(15); - for port in ports { - if let Some(it) = port_mapping_table.get(&port) { - peers.push((it, *port)); - } else { - return false; - } - } - - // Create a port forwarding mapping relationship for each peer session. - for (peer, port) in peers { - port_relay_table - .entry(*peer) - .or_insert_with(|| HashMap::with_capacity(20)) - .insert( - local_port, - Endpoint { - address: addr.address, - endpoint: *endpoint, - }, - ); - - // Do not store the same peer ports to the permission list over and over again. - if !session.permissions.contains(&port) { - session.permissions.push(port); - } - } - - true - } - - /// Binding a channel to the session. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let endpoint = "127.0.0.1:3478".parse().unwrap(); - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let peer_addr = SessionAddr { - /// address: "127.0.0.1:8081".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// sessions.get_integrity(&peer_addr, "test", "test"); - /// - /// let port = sessions.allocate(&addr).unwrap(); - /// let peer_port = sessions.allocate(&peer_addr).unwrap(); - /// assert_eq!( - /// sessions - /// .get_session(&addr) - /// .get_ref() - /// .unwrap() - /// .allocate - /// .channels - /// .len(), - /// 0 - /// ); - /// - /// assert_eq!( - /// sessions - /// .get_session(&peer_addr) - /// .get_ref() - /// .unwrap() - /// .allocate - /// .channels - /// .len(), - /// 0 - /// ); - /// - /// assert!(sessions.bind_channel(&addr, &endpoint, peer_port, 0x4000)); - /// assert!(sessions.bind_channel(&peer_addr, &endpoint, port, 0x4000)); - /// assert_eq!( - /// sessions - /// .get_session(&addr) - /// .get_ref() - /// .unwrap() - /// .allocate - /// .channels, - /// vec![0x4000] - /// ); - /// - /// assert_eq!( - /// sessions - /// .get_session(&peer_addr) - /// .get_ref() - /// .unwrap() - /// .allocate - /// .channels, - /// vec![0x4000] - /// ); - /// ``` - pub fn bind_channel(&self, addr: &SessionAddr, endpoint: &SocketAddr, port: u16, channel: u16) -> bool { - // Finds the address of the bound opposing port. - let peer = if let Some(it) = self.state.port_mapping_table.read().get(&port) { - *it - } else { - return false; - }; - - // Records the channel used for the current session. - { - let mut lock = self.state.sessions.write(); - let session = if let Some(it) = lock.get_mut(addr) { - it - } else { - return false; - }; - - if !session.allocate.channels.contains(&channel) { - session.allocate.channels.push(channel); - } - } - - // Binding ports also creates permissions. - if !self.create_permission(addr, endpoint, &[port]) { - return false; - } - - // Create channel forwarding mapping relationships for peers. - self.state - .channel_relay_table - .write() - .entry(peer) - .or_insert_with(|| HashMap::with_capacity(10)) - .insert( - channel, - Endpoint { - address: addr.address, - endpoint: *endpoint, - }, - ); - - true - } - - /// Gets the peer of the current session bound channel. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let endpoint = "127.0.0.1:3478".parse().unwrap(); - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let peer_addr = SessionAddr { - /// address: "127.0.0.1:8081".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// sessions.get_integrity(&peer_addr, "test", "test"); - /// - /// let port = sessions.allocate(&addr).unwrap(); - /// let peer_port = sessions.allocate(&peer_addr).unwrap(); - /// - /// assert!(sessions.bind_channel(&addr, &endpoint, peer_port, 0x4000)); - /// assert!(sessions.bind_channel(&peer_addr, &endpoint, port, 0x4000)); - /// assert_eq!( - /// sessions - /// .get_channel_relay_address(&addr, 0x4000) - /// .unwrap() - /// .endpoint, - /// endpoint - /// ); - /// - /// assert_eq!( - /// sessions - /// .get_channel_relay_address(&peer_addr, 0x4000) - /// .unwrap() - /// .endpoint, - /// endpoint - /// ); - /// ``` - pub fn get_channel_relay_address(&self, addr: &SessionAddr, channel: u16) -> Option { - self.state.channel_relay_table.read().get(&addr)?.get(&channel).copied() - } - - /// Get the address of the port binding. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let endpoint = "127.0.0.1:3478".parse().unwrap(); - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let peer_addr = SessionAddr { - /// address: "127.0.0.1:8081".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// sessions.get_integrity(&peer_addr, "test", "test"); - /// - /// let port = sessions.allocate(&addr).unwrap(); - /// let peer_port = sessions.allocate(&peer_addr).unwrap(); - /// - /// assert!(sessions.create_permission(&addr, &endpoint, &[peer_port])); - /// assert!(sessions.create_permission(&peer_addr, &endpoint, &[port])); - /// - /// assert_eq!( - /// sessions - /// .get_relay_address(&addr, peer_port) - /// .unwrap() - /// .endpoint, - /// endpoint - /// ); - /// - /// assert_eq!( - /// sessions - /// .get_relay_address(&peer_addr, port) - /// .unwrap() - /// .endpoint, - /// endpoint - /// ); - /// ``` - pub fn get_relay_address(&self, addr: &SessionAddr, port: u16) -> Option { - self.state.port_relay_table.read().get(&addr)?.get(&port).copied() - } - - /// Refresh the session for addr. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::*; - /// - /// #[derive(Clone)] - /// struct ObserverTest; - /// - /// impl Observer for ObserverTest { - /// fn get_password(&self, username: &str) -> Option { - /// if username == "test" { - /// Some("test".to_string()) - /// } else { - /// None - /// } - /// } - /// } - /// - /// let addr = SessionAddr { - /// address: "127.0.0.1:8080".parse().unwrap(), - /// interface: "127.0.0.1:3478".parse().unwrap(), - /// }; - /// - /// let digest = [ - /// 174, 238, 187, 253, 117, 209, 73, 157, 36, 56, 143, 91, 155, 16, 224, - /// 239, - /// ]; - /// - /// let sessions = Sessions::new(ObserverTest); - /// - /// assert!(sessions.get_session(&addr).get_ref().is_none()); - /// - /// sessions.get_integrity(&addr, "test", "test"); - /// - /// let expires = sessions.get_session(&addr).get_ref().unwrap().expires; - /// assert!(expires == 600 || expires == 601 || expires == 602); - /// - /// assert!(sessions.refresh(&addr, 0)); - /// - /// assert!(sessions.get_session(&addr).get_ref().is_none()); - /// ``` - pub fn refresh(&self, addr: &SessionAddr, lifetime: u32) -> bool { - if lifetime > 3600 { - return false; - } - - if lifetime == 0 { - self.remove_session(&[*addr]); - self.remove_nonce(&[*addr]); - } else { - if let Some(session) = self.state.sessions.write().get_mut(addr) { - session.expires = self.timer.get() + lifetime as u64; - } else { - return false; - } - - if let Some(nonce) = self.state.address_nonce_tanle.write().get_mut(addr) { - nonce.1 = self.timer.get() + lifetime as u64; - } - } - - true - } -} - -/// The default HashMap is created without allocating capacity. To improve -/// performance, the turn server needs to pre-allocate the available capacity. -/// -/// So here the HashMap is rewrapped to allocate a large capacity (number of -/// ports that can be allocated) at the default creation time as well. -pub struct Table(HashMap); - -impl Default for Table { - fn default() -> Self { - Self(HashMap::with_capacity(PortAllocatePools::capacity())) - } -} - -impl AsRef> for Table { - fn as_ref(&self) -> &HashMap { - &self.0 - } -} - -impl Deref for Table { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Table { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// Used to lengthen the timing of the release of a readable lock guard and to -/// provide a more convenient way for external access to the lock's internal -/// data. -pub struct ReadLock<'a, 'b, K, R> { - key: &'a K, - lock: RwLockReadGuard<'b, R>, -} - -impl<'a, 'b, K, V> ReadLock<'a, 'b, K, Table> -where - K: Eq + Hash, -{ - pub fn get_ref(&self) -> Option<&V> { - self.lock.get(self.key) - } -} - -/// Bit Flag -#[derive(PartialEq, Eq)] -pub enum Bit { - Low, - High, -} - -/// Random Port -/// -/// Recently, awareness has been raised about a number of "blind" attacks -/// (i.e., attacks that can be performed without the need to sniff the -/// packets that correspond to the transport protocol instance to be -/// attacked) that can be performed against the Transmission Control -/// Protocol (TCP) [RFC0793] and similar protocols. The consequences of -/// these attacks range from throughput reduction to broken connections -/// or data corruption [RFC5927] [RFC4953] [Watson]. -/// -/// All these attacks rely on the attacker's ability to guess or know the -/// five-tuple (Protocol, Source Address, Source port, Destination -/// Address, Destination Port) that identifies the transport protocol -/// instance to be attacked. -/// -/// Services are usually located at fixed, "well-known" ports [IANA] at -/// the host supplying the service (the server). Client applications -/// connecting to any such service will contact the server by specifying -/// the server IP address and service port number. The IP address and -/// port number of the client are normally left unspecified by the client -/// application and thus are chosen automatically by the client -/// networking stack. Ports chosen automatically by the networking stack -/// are known as ephemeral ports [Stevens]. -/// -/// While the server IP address, the well-known port, and the client IP -/// address may be known by an attacker, the ephemeral port of the client -/// is usually unknown and must be guessed. -/// -/// # Test -/// -/// ``` -/// use std::collections::HashSet; -/// use turn_server::turn::sessions::*; -/// -/// let mut pool = PortAllocatePools::default(); -/// let mut ports = HashSet::with_capacity(PortAllocatePools::capacity()); -/// -/// while let Some(port) = pool.alloc(None) { -/// ports.insert(port); -/// } -/// -/// assert_eq!(PortAllocatePools::capacity() + 1, ports.len()); -/// ``` -pub struct PortAllocatePools { - pub buckets: Vec, - allocated: usize, - bit_len: u32, - peak: usize, -} - -impl Default for PortAllocatePools { - fn default() -> Self { - Self { - buckets: vec![0; Self::bucket_size()], - peak: Self::bucket_size() - 1, - bit_len: Self::bit_len(), - allocated: 0, - } - } -} - -impl PortAllocatePools { - /// compute bucket size. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::*; - /// - /// assert_eq!(PortAllocatePools::bucket_size(), 256); - /// ``` - pub fn bucket_size() -> usize { - (Self::capacity() as f32 / 64.0).ceil() as usize - } - - /// compute bucket last bit max offset. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::*; - /// - /// assert_eq!(PortAllocatePools::bit_len(), 63); - /// ``` - pub fn bit_len() -> u32 { - (Self::capacity() as f32 % 64.0).ceil() as u32 - } - - /// get pools capacity. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::Bit; - /// use turn_server::turn::sessions::PortAllocatePools; - /// - /// assert_eq!(PortAllocatePools::capacity(), 65535 - 49152); - /// ``` - pub const fn capacity() -> usize { - 65535 - 49152 - } - - /// get port range. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::*; - /// - /// assert_eq!(PortAllocatePools::port_range(), 49152..65535); - /// ``` - pub const fn port_range() -> Range { - 49152..65535 - } - - /// get pools allocated size. - /// - /// ``` - /// use turn_server::turn::sessions::PortAllocatePools; - /// - /// let mut pools = PortAllocatePools::default(); - /// assert_eq!(pools.len(), 0); - /// - /// pools.alloc(None).unwrap(); - /// assert_eq!(pools.len(), 1); - /// ``` - pub fn len(&self) -> usize { - self.allocated - } - - /// get pools allocated size is empty. - /// - /// ``` - /// use turn_server::turn::sessions::PortAllocatePools; - /// - /// let mut pools = PortAllocatePools::default(); - /// assert_eq!(pools.len(), 0); - /// assert_eq!(pools.is_empty(), true); - /// ``` - pub fn is_empty(&self) -> bool { - self.allocated == 0 - } - - /// random assign a port. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::PortAllocatePools; - /// - /// let mut pool = PortAllocatePools::default(); - /// - /// assert_eq!(pool.alloc(Some(0)), Some(49152)); - /// assert_eq!(pool.alloc(Some(0)), Some(49153)); - /// - /// assert!(pool.alloc(None).is_some()); - /// ``` - pub fn alloc(&mut self, start_index: Option) -> Option { - let mut index = None; - let mut start = start_index.unwrap_or_else(|| rand::rng().random_range(0..self.peak as u16) as usize); - - // When the partition lookup has gone through the entire partition list, the - // lookup should be stopped, and the location where it should be stopped is - // recorded here. - let previous = if start == 0 { self.peak } else { start - 1 }; - - loop { - // Finds the first high position in the partition. - if let Some(i) = { - let bucket = self.buckets[start]; - if bucket < u64::MAX { - let offset = bucket.leading_ones(); - - // Check to see if the jump is beyond the partition list or the lookup exceeds - // the maximum length of the allocation table. - if start == self.peak && offset > self.bit_len { - None - } else { - Some(offset) - } - } else { - None - } - } { - index = Some(i as usize); - break; - } - - // As long as it doesn't find it, it continues to re-find it from the next - // partition. - if start == self.peak { - start = 0; - } else { - start += 1; - } - - // Already gone through all partitions, lookup failed. - if start == previous { - break; - } - } - - // Writes to the partition, marking the current location as already allocated. - let index = index?; - self.set_bit(start, index, Bit::High); - self.allocated += 1; - - // The actual port number is calculated from the partition offset position. - let num = (start * 64 + index) as u16; - let port = Self::port_range().start + num; - Some(port) - } - - /// write bit flag in the bucket. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::Bit; - /// use turn_server::turn::sessions::PortAllocatePools; - /// - /// let mut pool = PortAllocatePools::default(); - /// - /// assert_eq!(pool.alloc(Some(0)), Some(49152)); - /// assert_eq!(pool.alloc(Some(0)), Some(49153)); - /// - /// pool.set_bit(0, 0, Bit::High); - /// pool.set_bit(0, 1, Bit::High); - /// - /// assert_eq!(pool.alloc(Some(0)), Some(49154)); - /// assert_eq!(pool.alloc(Some(0)), Some(49155)); - /// ``` - pub fn set_bit(&mut self, bucket: usize, index: usize, bit: Bit) { - let high_mask = 1 << (63 - index); - let mask = match bit { - Bit::Low => u64::MAX ^ high_mask, - Bit::High => high_mask, - }; - - let value = self.buckets[bucket]; - self.buckets[bucket] = match bit { - Bit::High => value | mask, - Bit::Low => value & mask, - }; - } - - /// restore port in the buckets. - /// - /// # Test - /// - /// ``` - /// use turn_server::turn::sessions::PortAllocatePools; - /// - /// let mut pool = PortAllocatePools::default(); - /// - /// assert_eq!(pool.alloc(Some(0)), Some(49152)); - /// assert_eq!(pool.alloc(Some(0)), Some(49153)); - /// - /// pool.restore(49152); - /// pool.restore(49153); - /// - /// assert_eq!(pool.alloc(Some(0)), Some(49152)); - /// assert_eq!(pool.alloc(Some(0)), Some(49153)); - /// ``` - pub fn restore(&mut self, port: u16) { - assert!(Self::port_range().contains(&port)); - - // Calculate the location in the partition from the port number. - let offset = (port - Self::port_range().start) as usize; - let bucket = offset / 64; - let index = offset - (bucket * 64); - - // Gets the bit value in the port position in the partition, if it is low, no - // processing is required. - if { - match (self.buckets[bucket] & (1 << (63 - index))) >> (63 - index) { - 0 => Bit::Low, - 1 => Bit::High, - _ => panic!(), - } - } == Bit::Low - { - return; - } - - self.set_bit(bucket, index, Bit::Low); - self.allocated -= 1; - } -} diff --git a/tests/integration_test.rs b/tests/integration_test.rs deleted file mode 100644 index 5495771d..00000000 --- a/tests/integration_test.rs +++ /dev/null @@ -1,589 +0,0 @@ -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, LazyLock}, - time::Duration, -}; - -use anyhow::{Ok, Result, ensure}; -use base64::{Engine, prelude::BASE64_STANDARD}; -use bytes::BytesMut; -use rand::seq::SliceRandom; -use tokio::{ - net::UdpSocket, - time::{sleep, timeout}, -}; - -use turn_server::{ - config::{Api, Auth, Config, Interface, Log, Transport as TurnTransport, Turn}, - startup, - stun::{ - ChannelData, Decoder, MessageEncoder, MessageRef, Payload, - attribute::{ - ChannelNumber, Data, ErrorCode, ErrorKind, Lifetime, MappedAddress, Nonce, Realm, ReqeestedTransport, - ResponseOrigin, Transport, UserName, XorMappedAddress, XorPeerAddress, XorRelayedAddress, - }, - method::{ - ALLOCATE_ERROR, ALLOCATE_REQUEST, ALLOCATE_RESPONSE, BINDING_REQUEST, BINDING_RESPONSE, - CHANNEL_BIND_REQUEST, CHANNEL_BIND_RESPONSE, CREATE_PERMISSION_REQUEST, CREATE_PERMISSION_RESPONSE, - DATA_INDICATION, REFRESH_REQUEST, REFRESH_RESPONSE, SEND_INDICATION, StunMethod, - }, - util::{hmac_sha1, long_term_credential_digest}, - }, -}; - -static TOKEN: LazyLock<[u8; 12]> = LazyLock::new(|| { - let mut rng = rand::rng(); - let mut token = [0u8; 12]; - token.shuffle(&mut rng); - token -}); - -pub async fn create_turn_server(bind: SocketAddr, auth: Auth, api: Api) -> Result<()> { - tokio::spawn(async move { - startup(Arc::new(Config { - log: Log::default(), - turn: Turn { - realm: "localhost".to_string(), - interfaces: vec![Interface { - transport: TurnTransport::UDP, - external: bind, - bind, - }], - }, - auth, - api, - })) - .await - .unwrap(); - }); - - sleep(Duration::from_secs(3)).await; - Ok(()) -} - -struct Operationer { - decoder: Decoder, - socket: UdpSocket, - recv_bytes: [u8; 1500], - send_bytes: BytesMut, -} - -impl Operationer { - async fn new(server: SocketAddr) -> Result { - let socket = UdpSocket::bind("127.0.0.1:0").await?; - socket.connect(server).await?; - - Ok(Self { - send_bytes: BytesMut::with_capacity(1500), - decoder: Decoder::default(), - recv_bytes: [0u8; 1500], - socket, - }) - } - - fn local_addr(&self) -> Result { - Ok(self.socket.local_addr()?) - } - - fn create_message(&mut self, method: StunMethod) -> MessageEncoder<'_> { - MessageEncoder::new(method, &TOKEN, &mut self.send_bytes) - } - - fn create_channel_data(&mut self, number: u16, bytes: &[u8]) { - ChannelData { number, bytes }.encode(&mut self.send_bytes); - } - - async fn send(&self) -> Result<()> { - self.socket.send(&self.send_bytes).await?; - Ok(()) - } - - async fn read_message(&mut self) -> Result> { - let size = timeout(Duration::from_secs(1), self.socket.recv(&mut self.recv_bytes)).await??; - - if let Payload::Message(message) = self.decoder.decode(&self.recv_bytes[..size])? { - if message.token() != TOKEN.as_slice() { - Err(anyhow::anyhow!("Message token does not match")) - } else { - Ok(message) - } - } else { - Err(anyhow::anyhow!("payload not a message")) - } - } - - async fn read_channel_data(&mut self) -> Result> { - let size = timeout(Duration::from_secs(1), self.socket.recv(&mut self.recv_bytes)).await??; - - if let Payload::ChannelData(channel_data) = self.decoder.decode(&self.recv_bytes[..size])? { - Ok(channel_data) - } else { - Err(anyhow::anyhow!("payload not a channel data")) - } - } -} - -pub struct Credentials { - pub username: String, - pub password: String, -} - -#[derive(Default)] -struct State { - digest: [u8; 16], - nonce: String, - realm: String, -} - -pub struct TurnClient { - operationer: Operationer, - credentials: Credentials, - server: SocketAddr, - state: State, -} - -impl TurnClient { - pub async fn new(server: SocketAddr, credentials: Credentials) -> Result { - Ok(Self { - operationer: Operationer::new(server).await?, - state: State::default(), - credentials, - server, - }) - } - - pub async fn binding(&mut self) -> Result<()> { - { - let mut message = self.operationer.create_message(BINDING_REQUEST); - message.flush(None)?; - - self.operationer.send().await?; - } - - let local_addr = self.operationer.local_addr()?; - let message = self.operationer.read_message().await?; - - ensure!(message.method() == BINDING_RESPONSE); - ensure!(message.get::() == Some(local_addr)); - ensure!(message.get::() == Some(local_addr)); - ensure!(message.get::() == Some(self.server)); - Ok(()) - } - - pub async fn allocate(&mut self) -> Result { - { - { - let mut message = self.operationer.create_message(ALLOCATE_REQUEST); - message.append::(Transport::UDP); - message.flush(None)?; - - self.operationer.send().await?; - } - - let message = self.operationer.read_message().await?; - - ensure!(message.method() == ALLOCATE_ERROR); - ensure!(message.get::().unwrap().code == ErrorKind::Unauthorized as u16); - - self.state.nonce = message.get::().unwrap().to_string(); - self.state.realm = message.get::().unwrap().to_string(); - self.state.digest = long_term_credential_digest( - &self.credentials.username, - &self.credentials.password, - &self.state.realm, - ); - } - - { - let mut message = self.operationer.create_message(ALLOCATE_REQUEST); - message.append::(Transport::UDP); - message.append::(&self.credentials.username); - message.append::(&self.state.realm); - message.append::(&self.state.nonce); - message.flush(Some(&self.state.digest))?; - - self.operationer.send().await?; - } - - let local_addr = self.operationer.local_addr()?; - let message = self.operationer.read_message().await?; - - ensure!(message.method() == ALLOCATE_RESPONSE); - message.integrity(&self.state.digest)?; - - let relay = message.get::().unwrap(); - - ensure!(relay.ip() == self.server.ip()); - ensure!(message.get::() == Some(local_addr)); - ensure!(message.get::() == Some(600)); - - Ok(relay.port()) - } - - pub async fn create_permission(&mut self, port: u16) -> Result<()> { - { - let mut peer = self.server.clone(); - peer.set_port(port); - - let mut message = self.operationer.create_message(CREATE_PERMISSION_REQUEST); - message.append::(peer); - message.append::(&self.credentials.username); - message.append::(&self.state.realm); - message.append::(&self.state.nonce); - message.flush(Some(&self.state.digest))?; - - self.operationer.send().await?; - } - - let message = self.operationer.read_message().await?; - - ensure!(message.method() == CREATE_PERMISSION_RESPONSE); - message.integrity(&self.state.digest)?; - - Ok(()) - } - - pub async fn channel_bind(&mut self, port: u16, channel: u16) -> Result<()> { - { - let mut peer = self.server.clone(); - peer.set_port(port); - - let mut message = self.operationer.create_message(CHANNEL_BIND_REQUEST); - message.append::(channel); - message.append::(peer); - message.append::(&self.credentials.username); - message.append::(&self.state.realm); - message.append::(&self.state.nonce); - message.flush(Some(&self.state.digest))?; - - self.operationer.send().await?; - } - - let message = self.operationer.read_message().await?; - - ensure!(message.method() == CHANNEL_BIND_RESPONSE); - message.integrity(&self.state.digest)?; - - Ok(()) - } - - pub async fn refresh(&mut self, lifetime: u32) -> Result<()> { - { - let mut message = self.operationer.create_message(REFRESH_REQUEST); - message.append::(lifetime); - message.append::(&self.credentials.username); - message.append::(&self.state.realm); - message.append::(&self.state.nonce); - message.flush(Some(&self.state.digest))?; - - self.operationer.send().await?; - } - - let message = self.operationer.read_message().await?; - - ensure!(message.method() == REFRESH_RESPONSE); - message.integrity(&self.state.digest)?; - - ensure!(message.get::() == Some(lifetime)); - - Ok(()) - } - - pub async fn send_indication(&mut self, port: u16, data: &[u8]) -> Result<()> { - let mut peer = self.server.clone(); - peer.set_port(port); - - let mut message = self.operationer.create_message(SEND_INDICATION); - message.append::(peer); - message.append::(data); - message.flush(None)?; - - self.operationer.send().await?; - Ok(()) - } - - pub async fn recv_indication(&mut self) -> Result<(u16, &[u8])> { - let message = self.operationer.read_message().await?; - - ensure!(message.method() == DATA_INDICATION); - - let peer = message.get::().unwrap(); - let data = message.get::().unwrap(); - Ok((peer.port(), data)) - } - - pub async fn send_channel_data(&mut self, channel: u16, data: &[u8]) -> Result<()> { - self.operationer.create_channel_data(channel, data); - self.operationer.send().await?; - Ok(()) - } - - pub async fn recv_channel_data(&mut self) -> Result<(u16, &[u8])> { - let message = self.operationer.read_channel_data().await?; - Ok((message.number, message.bytes)) - } -} - -fn encode_password(username: &str, password: &str) -> Result { - Ok(BASE64_STANDARD.encode( - hmac_sha1(password.as_bytes(), &[username.as_bytes()])? - .into_bytes() - .as_slice(), - )) -} - -#[tokio::test] -async fn integration_testing() -> Result<()> { - create_turn_server( - "127.0.0.1:3478".parse()?, - Auth { - static_auth_secret: Some("static_auth_secret".to_string()), - static_credentials: { - let mut it = HashMap::with_capacity(1); - it.insert("static_credentials".to_string(), "static_credentials".to_string()); - it - }, - }, - Api::default(), - ) - .await?; - - let mut turn_1 = TurnClient::new( - "127.0.0.1:3478".parse()?, - Credentials { - username: "static_credentials".to_string(), - password: "static_credentials".to_string(), - }, - ) - .await?; - - let mut turn_2 = TurnClient::new( - "127.0.0.1:3478".parse()?, - Credentials { - username: "static_credentials".to_string(), - password: "static_credentials".to_string(), - }, - ) - .await?; - - let mut turn_3 = TurnClient::new( - "127.0.0.1:3478".parse()?, - Credentials { - username: "static_auth_secret".to_string(), - password: encode_password("static_auth_secret", "static_auth_secret")?, - }, - ) - .await?; - - let mut turn_4 = TurnClient::new( - "127.0.0.1:3478".parse()?, - Credentials { - username: "static_auth_secret".to_string(), - password: encode_password("static_auth_secret", "static_auth_secret")?, - }, - ) - .await?; - - { - turn_1.binding().await?; - turn_2.binding().await?; - turn_3.binding().await?; - } - - let turn_1_port = turn_1.allocate().await?; - let turn_2_port = turn_2.allocate().await?; - let turn_3_port = turn_3.allocate().await?; - let turn_4_port = turn_4.allocate().await?; - - assert_eq!(turn_1.allocate().await?, turn_1_port); - assert_eq!(turn_2.allocate().await?, turn_2_port); - assert_eq!(turn_3.allocate().await?, turn_3_port); - assert_eq!(turn_4.allocate().await?, turn_4_port); - - { - turn_1.create_permission(turn_2_port).await?; - turn_1.create_permission(turn_3_port).await?; - turn_1.create_permission(turn_4_port).await?; - turn_1.channel_bind(turn_2_port, 0x4000).await?; - turn_1.channel_bind(turn_3_port, 0x4001).await?; - turn_1.channel_bind(turn_4_port, 0x4002).await?; - turn_1.refresh(600).await?; - - turn_2.create_permission(turn_1_port).await?; - turn_2.create_permission(turn_3_port).await?; - turn_2.channel_bind(turn_1_port, 0x4000).await?; - turn_2.channel_bind(turn_3_port, 0x4002).await?; - turn_2.refresh(600).await?; - - turn_3.create_permission(turn_1_port).await?; - turn_3.create_permission(turn_2_port).await?; - turn_3.channel_bind(turn_1_port, 0x4001).await?; - turn_3.channel_bind(turn_2_port, 0x4002).await?; - turn_3.refresh(600).await?; - - turn_4.create_permission(turn_1_port).await?; - turn_4.channel_bind(turn_1_port, 0x4002).await?; - turn_4.refresh(600).await?; - - assert!(turn_1.channel_bind(turn_2_port, 0x4000).await.is_ok()); - assert!(turn_1.channel_bind(turn_3_port, 0x4001).await.is_ok()); - assert!(turn_1.channel_bind(turn_4_port, 0x4002).await.is_ok()); - assert!(turn_2.channel_bind(turn_1_port, 0x4000).await.is_ok()); - assert!(turn_2.channel_bind(turn_3_port, 0x4002).await.is_ok()); - assert!(turn_3.channel_bind(turn_1_port, 0x4001).await.is_ok()); - assert!(turn_3.channel_bind(turn_2_port, 0x4002).await.is_ok()); - assert!(turn_4.channel_bind(turn_1_port, 0x4002).await.is_ok()); - } - - { - let data = "1 forwards to 2,3,4 channel data".as_bytes(); - turn_1.send_channel_data(0x4000, data).await?; - let ret = turn_2.recv_channel_data().await?; - assert_eq!(ret.0, 0x4000); - assert_eq!(ret.1, data); - - turn_1.send_channel_data(0x4001, data).await?; - let ret = turn_3.recv_channel_data().await?; - assert_eq!(ret.0, 0x4001); - assert_eq!(ret.1, data); - - turn_1.send_channel_data(0x4002, data).await?; - let ret = turn_4.recv_channel_data().await?; - assert_eq!(ret.0, 0x4002); - assert_eq!(ret.1, data); - } - - { - let data = "2 forwards to 1,3 channel data".as_bytes(); - turn_2.send_channel_data(0x4000, data).await?; - let ret = turn_1.recv_channel_data().await?; - assert_eq!(ret.0, 0x4000); - assert_eq!(ret.1, data); - assert!(turn_3.recv_channel_data().await.is_err()); - assert!(turn_4.recv_channel_data().await.is_err()); - - turn_2.send_channel_data(0x4002, data).await?; - let ret = turn_3.recv_channel_data().await?; - assert_eq!(ret.0, 0x4002); - assert_eq!(ret.1, data); - assert!(turn_1.recv_channel_data().await.is_err()); - assert!(turn_4.recv_channel_data().await.is_err()); - - turn_2.send_channel_data(0x4001, data).await?; - assert!(turn_1.recv_channel_data().await.is_err()); - assert!(turn_3.recv_channel_data().await.is_err()); - assert!(turn_4.recv_channel_data().await.is_err()); - } - - { - let data = "3 forwards to 1,2 channel data".as_bytes(); - turn_3.send_channel_data(0x4001, data).await?; - let ret = turn_1.recv_channel_data().await?; - assert_eq!(ret.0, 0x4001); - assert_eq!(ret.1, data); - assert!(turn_2.recv_channel_data().await.is_err()); - assert!(turn_4.recv_channel_data().await.is_err()); - - turn_3.send_channel_data(0x4002, data).await?; - let ret = turn_2.recv_channel_data().await?; - assert_eq!(ret.0, 0x4002); - assert_eq!(ret.1, data); - assert!(turn_1.recv_channel_data().await.is_err()); - assert!(turn_4.recv_channel_data().await.is_err()); - - turn_3.send_channel_data(0x4000, data).await?; - assert!(turn_2.recv_channel_data().await.is_err()); - assert!(turn_1.recv_channel_data().await.is_err()); - assert!(turn_4.recv_channel_data().await.is_err()); - } - - { - let data = "4 forwards to 1 channel data".as_bytes(); - turn_4.send_channel_data(0x4002, data).await?; - let ret = turn_1.recv_channel_data().await?; - assert_eq!(ret.0, 0x4002); - assert_eq!(ret.1, data); - assert!(turn_2.recv_channel_data().await.is_err()); - assert!(turn_3.recv_channel_data().await.is_err()); - - turn_4.send_channel_data(0x4000, data).await?; - assert!(turn_1.recv_channel_data().await.is_err()); - assert!(turn_2.recv_channel_data().await.is_err()); - assert!(turn_3.recv_channel_data().await.is_err()); - - turn_4.send_channel_data(0x4001, data).await?; - assert!(turn_1.recv_channel_data().await.is_err()); - assert!(turn_2.recv_channel_data().await.is_err()); - assert!(turn_3.recv_channel_data().await.is_err()); - } - - { - let data = "1 forwards to 2,3,4".as_bytes(); - turn_1.send_indication(turn_2_port, data).await?; - let ret = turn_2.recv_indication().await?; - assert_eq!(ret.0, turn_1_port); - assert_eq!(ret.1, data); - - turn_1.send_indication(turn_3_port, data).await?; - let ret = turn_3.recv_indication().await?; - assert_eq!(ret.0, turn_1_port); - assert_eq!(ret.1, data); - - turn_1.send_indication(turn_4_port, data).await?; - let ret = turn_4.recv_indication().await?; - assert_eq!(ret.0, turn_1_port); - assert_eq!(ret.1, data); - } - - { - let data = "2 forwards to 1,3".as_bytes(); - turn_2.send_indication(turn_1_port, data).await?; - let ret = turn_1.recv_indication().await?; - assert_eq!(ret.0, turn_2_port); - assert_eq!(ret.1, data); - - turn_2.send_indication(turn_3_port, data).await?; - let ret = turn_3.recv_indication().await?; - assert_eq!(ret.0, turn_2_port); - assert_eq!(ret.1, data); - - turn_2.send_indication(turn_4_port, data).await?; - assert!(turn_4.recv_indication().await.is_err()); - } - - { - let data = "3 forwards to 1,2".as_bytes(); - turn_3.send_indication(turn_1_port, data).await?; - let ret = turn_1.recv_indication().await?; - assert_eq!(ret.0, turn_3_port); - assert_eq!(ret.1, data); - - turn_3.send_indication(turn_2_port, data).await?; - let ret = turn_2.recv_indication().await?; - assert_eq!(ret.0, turn_3_port); - assert_eq!(ret.1, data); - - turn_3.send_indication(turn_4_port, data).await?; - assert!(turn_4.recv_indication().await.is_err()); - } - - { - let data = "4 forwards to 1".as_bytes(); - turn_4.send_indication(turn_1_port, data).await?; - let ret = turn_1.recv_indication().await?; - assert_eq!(ret.0, turn_4_port); - assert_eq!(ret.1, data); - - turn_4.send_indication(turn_3_port, data).await?; - assert!(turn_3.recv_indication().await.is_err()); - } - - { - turn_1.refresh(0).await?; - turn_2.refresh(0).await?; - turn_3.refresh(0).await?; - } - - Ok(()) -} diff --git a/tests/samples/AllocateRequest.bin b/tests/samples/AllocateRequest.bin new file mode 100644 index 00000000..551ad6ab Binary files /dev/null and b/tests/samples/AllocateRequest.bin differ diff --git a/tests/samples/AllocateResponse.bin b/tests/samples/AllocateResponse.bin new file mode 100644 index 00000000..05a5985b Binary files /dev/null and b/tests/samples/AllocateResponse.bin differ diff --git a/tests/samples/BindingRequest.bin b/tests/samples/BindingRequest.bin new file mode 100644 index 00000000..1ba1a226 Binary files /dev/null and b/tests/samples/BindingRequest.bin differ diff --git a/tests/samples/BindingResponse.bin b/tests/samples/BindingResponse.bin new file mode 100644 index 00000000..e1acc53f Binary files /dev/null and b/tests/samples/BindingResponse.bin differ diff --git a/tests/samples/ChannelBindRequest.bin b/tests/samples/ChannelBindRequest.bin new file mode 100644 index 00000000..0728f5c0 Binary files /dev/null and b/tests/samples/ChannelBindRequest.bin differ diff --git a/tests/samples/ChannelBindResponse.bin b/tests/samples/ChannelBindResponse.bin new file mode 100644 index 00000000..eb27e25d Binary files /dev/null and b/tests/samples/ChannelBindResponse.bin differ diff --git a/tests/samples/ChannelData.bin b/tests/samples/ChannelData.bin new file mode 100644 index 00000000..4d6887ec Binary files /dev/null and b/tests/samples/ChannelData.bin differ diff --git a/tests/samples/CreatePermissionRequest.bin b/tests/samples/CreatePermissionRequest.bin new file mode 100644 index 00000000..d37134e8 Binary files /dev/null and b/tests/samples/CreatePermissionRequest.bin differ diff --git a/tests/samples/CreatePermissionResponse.bin b/tests/samples/CreatePermissionResponse.bin new file mode 100644 index 00000000..c2022b2a Binary files /dev/null and b/tests/samples/CreatePermissionResponse.bin differ diff --git a/tests/samples/DataIndication.bin b/tests/samples/DataIndication.bin new file mode 100644 index 00000000..c4d954d2 Binary files /dev/null and b/tests/samples/DataIndication.bin differ diff --git a/tests/samples/RefreshRequest.bin b/tests/samples/RefreshRequest.bin new file mode 100644 index 00000000..1671d0aa Binary files /dev/null and b/tests/samples/RefreshRequest.bin differ diff --git a/tests/samples/RefreshResponse.bin b/tests/samples/RefreshResponse.bin new file mode 100644 index 00000000..398e1c87 Binary files /dev/null and b/tests/samples/RefreshResponse.bin differ diff --git a/tests/samples/SendIndication.bin b/tests/samples/SendIndication.bin new file mode 100644 index 00000000..8f1e52e5 Binary files /dev/null and b/tests/samples/SendIndication.bin differ diff --git a/tests/samples/UnauthorizedAllocateRequest.bin b/tests/samples/UnauthorizedAllocateRequest.bin new file mode 100644 index 00000000..a6ee7d97 Binary files /dev/null and b/tests/samples/UnauthorizedAllocateRequest.bin differ diff --git a/tests/samples/UnauthorizedAllocateResponse.bin b/tests/samples/UnauthorizedAllocateResponse.bin new file mode 100644 index 00000000..59c690f3 Binary files /dev/null and b/tests/samples/UnauthorizedAllocateResponse.bin differ diff --git a/turn-server.service b/turn-server.service index 8ca2366c..938f0d9c 100644 --- a/turn-server.service +++ b/turn-server.service @@ -5,7 +5,7 @@ After=network.target [Service] Type=simple Restart=always -ExecStart=/usr/local/bin/turn-server --config=/etc/turn-server/config.toml +ExecStart=/usr/local/bin/turn-server --config=/etc/turn-server/config.json [Install] WantedBy=multi-user.target diff --git a/turn-server.toml b/turn-server.toml index 378f8eb3..66a496d9 100644 --- a/turn-server.toml +++ b/turn-server.toml @@ -1,83 +1,156 @@ -[turn] +[server] +# +# Port range, the maximum range is 49152 - 65535. +# +port-range = "49152..65535" +# +# +# Maximum number of threads the TURN server can use. +# +max-threads = 12 +# +# # turn server realm # -# specify the domain where the server is located. -# for a single node, this configuration is fixed, -# but each node can be configured as a different domain. -# this is a good idea to divide the nodes by namespace. +# specify the domain where the server is located. for a single node, +# this configuration is fixed, but each node can be configured as a +# different domain. this is a good idea to divide the nodes by namespace. # realm = "localhost" - +# +# # turn server listen interfaces # # The address and port to which the UDP Server is bound. Multiple # addresses can be bound at the same time. The binding address supports # ipv4 and ipv6. # -[[turn.interfaces]] +[[server.interfaces]] transport = "udp" -bind = "127.0.0.1:3478" +listen = "127.0.0.1:3478" +# +# # external address # -# specify the node external address and port. -# for the case of exposing the service to the outside, -# you need to manually specify the server external IP -# address and service listening port. +# specify the node external address and port. for the case of +# exposing the service to the outside, you need to manually +# specify the server external IP address and service listening +# port. +# external = "127.0.0.1:3478" # -# [[turn.interfaces]] +# +# Idle timeout +# +# If no packet is received within the specified number of +# seconds, the connection will be closed to prevent resources +# from being occupied for a long time. +# +idle-timeout = 20 +# +# +# Maximum Transmission Unit (MTU) size for network packets. +# +mtu = 1500 +# +# +# ssl configuration +# +# [server.interfaces.ssl] +# private-key = "/etc/turn-rs/tls/private.key" +# certificate-chain = "/etc/turn-rs/tls/certificate.crt" +# +# +# Additional interface examples (commented): +# +# [[server.interfaces]] # transport = "tcp" -# bind = "127.0.0.1:3478" +# listen = "127.0.0.1:3478" # external = "127.0.0.1:3478" - -# [[turn.interfaces]] +# +# [[server.interfaces]] # transport = "udp" -# bind = "[::1]:3478" +# listen = "[::1]:3478" # external = "[::1]:3478" - -# [[turn.interfaces]] -# transport = "tcp" -# bind = "[::1]:3478" -# external = "[::1]:3478" - -[api] -# controller bind +# +# +# +# rpc server listen # # This option specifies the http server binding address used to control # the turn server. # -# Warn: This http server does not contain any means of authentication, -# and sensitive information and dangerous operations can be obtained -# through this service, please do not expose it directly to an unsafe -# environment. +# [api] +# listen = "127.0.0.1:3000" # -bind = "127.0.0.1:3000" - -[log] -# log level # -# An enum representing the available verbosity levels of the logger. +# Set a timeout on for all request handlers. # +# timeout = 5 +# +# +# ssl configuration +# +# [api.ssl] +# private-key = "/etc/turn-rs/tls/private.key" +# certificate-chain = "/etc/turn-rs/tls/certificate.crt" +# +# +# +# [hooks] +# +# hooks server endpoint +# +# The endpoint of the hooks server +# +# endpoint = "http://127.0.0.1:3000" +# +# +# Maximum number of channels for the hooks server. +# +# max-channel-size = 1024 +# +# +# ssl configuration +# +# [hooks.ssl] +# private-key = "/etc/turn-rs/tls/private.key" +# certificate-chain = "/etc/turn-rs/tls/certificate.crt" +# +# +# +# log level +# An enum representing the available verbosity levels of the logger. +[log] level = "info" - +# +# +# [auth] -# Static authentication key value (string) that applies only to the TURN -# REST API. # -# If set, the turn server will not request external services via the HTTP +# Enable hooks authentication. +# +# This option can be used to enable the hooks authentication. +# +enable-hooks-auth = false +# +# +# Static authentication key value (string) that applies only to the TURN REST API. +# If set, the turn server will not request external services via the GRPC # Hooks API to obtain the key. # -# static_auth_secret = "" - - +# static-auth-secret = "" +# +# # static user password # -# This option can be used to specify the -# static identity authentication information used by the turn server for -# verification. Note: this is a high-priority authentication method, turn -# The server will try to use static authentication first, and then use -# external control service authentication. +# This option can be used to specify the static identity authentication +# information used by the turn server for verification. +# +# Note: this is a high-priority authentication method, the server +# will try to use static authentication first, and then use external +# control service authentication. # -[auth.static_credentials] -# user1 = "test" -# user2 = "test" +[auth.static-credentials] +user1 = "test" +user2 = "test"