diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 942c8d32c..8cb25ae85 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -4,7 +4,7 @@ env: CARGO_TERM_COLOR: always RUST_TOOLCHAIN: stable RUST_TOOLCHAIN_NIGHTLY: nightly - RUST_TOOLCHAIN_MSRV: 1.91.0 + RUST_TOOLCHAIN_MSRV: 1.93.0 RUST_TOOLCHAIN_BETA: beta CARGO_INCREMENTAL: 0 CARGO_PROFILE_TEST_DEBUG: 0 @@ -20,7 +20,7 @@ jobs: strategy: matrix: toolchain: - - 1.91.0 # MSRV + - 1.93.0 # MSRV - stable - beta os: @@ -57,7 +57,7 @@ jobs: strategy: matrix: toolchain: - - 1.91.0 # MSRV + - 1.93.0 # MSRV - stable - beta os: @@ -135,6 +135,33 @@ jobs: - name: Run example tests (cargo test) run: cargo nextest run --all-features --examples --workspace --no-tests=pass + test-ffi-apple-example-transparent-proxy: + runs-on: macos-latest + needs: + - precheck-rust + - precheck-docs + env: + RUSTFLAGS: -D warnings + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{env.RUST_TOOLCHAIN}} + targets: aarch64-apple-darwin, x86_64-apple-darwin + - uses: Swatinem/rust-cache@v2 + with: + key: macos-latest-${{ env.RUST_TOOLCHAIN }}-apple-transparent-proxy + env-vars: "RUST_TOOLCHAIN=${{ env.RUST_TOOLCHAIN }}" + - name: Check Xcode toolchain + run: | + xcodebuild -version + xcrun --show-sdk-path + - name: Install just and xcodegen + run: brew install just xcodegen + - name: Run transparent proxy qa + working-directory: ./ffi/apple/examples/transparent_proxy + run: just qa + test-rust-linux-musl: strategy: matrix: @@ -520,6 +547,7 @@ jobs: - precheck-rust-tier2 - check-rust - test-rust-base + - test-ffi-apple-example-transparent-proxy - test-rust-linux-musl - test-rust-extra - test-rust-e2e diff --git a/CHANGELOG.md b/CHANGELOG.md index cb98fb7ea..9bbe56ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,7 @@ where the parameter types `Request` and `Response` make no sense. * **Memory Management**: Updated internal usage of `SmallVec` and `SmolStr` to reduce allocations across the workspace. * These dependencies are also re-exported under `rama-utils` * **Upstream Sync**: Massive sync with upstream forks including `hyper`, `h2` (1xx informational responses support), `tungstenite`, and `tower-http`. -* **MSRV**: Bumped Rust Minimum Supported Rust Version to **1.91**. +* **MSRV**: Bumped Rust Minimum Supported Rust Version to **1.93**. * **EasyWebClient**: Refactored to be more explicit with `connector_builder` and support for `jit_layers`. ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1c741336..6caff2127 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ just qa Before you can do this you do require the following to be installed: -* `Rust`, version 1.91 or beyond: +* `Rust`, version 1.93 or beyond: * `just` (to run _just_ (config) files): What you will also need to have installed is: diff --git a/Cargo.lock b/Cargo.lock index d71da9d4b..f563e18a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,9 +175,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" dependencies = [ "compression-codecs", "compression-core", @@ -229,9 +229,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-fips-sys" -version = "0.13.11" +version = "0.13.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df6ea8e07e2df15b9f09f2ac5ee2977369b06d116f0c4eb5fa4ad443b73c7f53" +checksum = "5ed8cd42adddefbdb8507fb7443fa9b666631078616b78f70ed22117b5c27d90" dependencies = [ "bindgen", "cc", @@ -243,9 +243,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-fips-sys", "aws-lc-sys", @@ -255,9 +255,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "bindgen", "cc", @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -1294,19 +1294,19 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -1434,9 +1434,9 @@ dependencies = [ [[package]] name = "honggfuzz" -version = "0.5.58" +version = "0.5.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8319f3cc8fe416e7aa1ab95dcc04fd49f35397a47d0b2f0f225f6dba346a07" +checksum = "9724607fd5cc535fceca960d7f684a1735c4e96c26b4e43986fe8ce798c2550e" dependencies = [ "arbitrary", "lazy_static", @@ -1682,9 +1682,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" @@ -1738,9 +1738,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1819,19 +1819,18 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.0", "libc", ] [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", "pkg-config", @@ -1849,9 +1848,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2031,9 +2030,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -2482,18 +2481,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -2502,9 +2501,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2592,9 +2591,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -2725,9 +2724,9 @@ checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" [[package]] name = "psl" -version = "2.1.192" +version = "2.1.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a44aab7d263c3aa2716d68589ff0b1e3a54ad1642a7a28592f5e64626fe1d2d" +checksum = "dcaf2b49c1b3cd056976b7ad695a2349901e8ac1777615676d9c56ffaaa9314d" dependencies = [ "psl-types", ] @@ -2762,9 +2761,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2775,6 +2774,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radix_trie" version = "0.3.0" @@ -2814,6 +2819,7 @@ dependencies = [ "rama-http-backend", "rama-http-core", "rama-net", + "rama-net-apple-networkextension", "rama-proxy", "rama-socks5", "rama-tcp", @@ -2842,9 +2848,9 @@ dependencies = [ [[package]] name = "rama-boring" -version = "0.5.12" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a8c66935959385543d370f6f1485461d520028d4ac9ae852131ec1c9226869b" +checksum = "ddc9b815c8dce0288dc1ecebf53039913c82977604766e48b72e80db99b06327" dependencies = [ "bitflags 2.11.0", "foreign-types", @@ -2855,9 +2861,9 @@ dependencies = [ [[package]] name = "rama-boring-sys" -version = "0.5.12" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50c9107d147c768182b5d6494670c0a54eadc99ee3eb24784a5b5504480eed8" +checksum = "e7b68b0916fb03989d677548d40c3e25cd24d16da95d32b8c5861ce9805a03ce" dependencies = [ "bindgen", "cmake", @@ -2867,9 +2873,9 @@ dependencies = [ [[package]] name = "rama-boring-tokio" -version = "0.5.12" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7bccdd6efce0302b0fdd0e8b8ff8fba6202841bdfeeac357be115b9f42dd6b" +checksum = "2ff8d421e3e7f5b7a08efcdf1c5292ad11307fb9a97847731f0696e3e15c9047" dependencies = [ "rama-boring", "rama-boring-sys", @@ -2950,6 +2956,7 @@ version = "0.3.0-rc1" dependencies = [ "ahash", "hickory-resolver", + "pin-project-lite", "rama-core", "rama-net", "rama-utils", @@ -3099,6 +3106,7 @@ dependencies = [ "rama-unix", "rama-utils", "tokio", + "tokio-util", ] [[package]] @@ -3216,12 +3224,29 @@ dependencies = [ "serde", "serde_json", "sha2", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tokio-test", "venndb", ] +[[package]] +name = "rama-net-apple-networkextension" +version = "0.3.0-rc1" +dependencies = [ + "parking_lot", + "pin-project-lite", + "rama-core", + "rama-macros", + "rama-net", + "rama-tcp", + "rama-udp", + "rama-utils", + "tokio", + "tokio-test", + "tracing", +] + [[package]] name = "rama-proxy" version = "0.3.0-rc1" @@ -3247,6 +3272,7 @@ name = "rama-socks5" version = "0.3.0-rc1" dependencies = [ "byteorder", + "parking_lot", "rama-core", "rama-dns", "rama-net", @@ -3365,6 +3391,7 @@ name = "rama-udp" version = "0.3.0-rc1" dependencies = [ "rama-core", + "rama-dns", "rama-net", "tokio", "tokio-util", @@ -3444,7 +3471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ "chacha20", - "getrandom 0.4.1", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -3634,9 +3661,9 @@ checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "resolv-conf" @@ -3684,9 +3711,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.11.0", "errno", @@ -3697,9 +3724,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", @@ -3976,9 +4003,9 @@ dependencies = [ [[package]] name = "smol_str" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" dependencies = [ "borsh", "serde_core", @@ -3996,12 +4023,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4128,12 +4155,12 @@ checksum = "591ef38edfb78ca4771ee32cf494cb8771944bee237a9b91fc9c1424ac4b777b" [[package]] name = "tempfile" -version = "3.25.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4360,9 +4387,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -4370,7 +4397,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -4390,9 +4417,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -4419,7 +4446,7 @@ dependencies = [ "postgres-protocol", "postgres-types", "rand 0.9.2", - "socket2 0.6.2", + "socket2 0.6.3", "tokio", "tokio-util", "whoami", @@ -4473,28 +4500,19 @@ dependencies = [ [[package]] name = "toml" -version = "1.0.3+spec-1.1.0" +version = "1.0.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +checksum = "c94c3321114413476740df133f0d8862c61d87c8d26f04c6841e033c8c80db47" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", "winnow", ] -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_datetime" version = "1.0.0+spec-1.1.0" @@ -4506,12 +4524,12 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "winnow", ] @@ -4878,12 +4896,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.21.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "atomic", - "getrandom 0.4.1", + "getrandom 0.4.2", "js-sys", "wasm-bindgen", ] @@ -4997,9 +5015,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -5010,9 +5028,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5020,9 +5038,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -5033,9 +5051,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.111" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -5076,9 +5094,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.88" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -5517,9 +5535,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -5680,18 +5698,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index f19f8c6d4..af2c82bef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ members = [ "rama-macros", "rama-macros/tests/macros", "rama-net", + "rama-net-apple-networkextension", "rama-proxy", "rama-socks5", "rama-tcp", @@ -75,7 +76,7 @@ categories = [ "web-programming::http-server", ] authors = ["Glen De Cauwsemaecker "] -rust-version = "1.91.0" +rust-version = "1.93.0" [workspace.dependencies] ahash = "0.8" @@ -92,11 +93,7 @@ bitflags = "2.10" brotli = "8" byteorder = "1.5" bytes = "1" -chrono = { version = "0.4", default-features = false, features = [ - "serde", - "oldtime", - "clock", -] } +chrono = { version = "0.4", default-features = false, features = ["serde", "oldtime", "clock"] } clap = { version = "4.5", features = ["derive", "unstable-v5", "wrap_help"] } compression-codecs = "0.4" compression-core = "0.4" @@ -112,10 +109,7 @@ futures = "0.3" futures-channel = "0.3" futures-util = { version = "0.3", default-features = false } hex = "0.4" -hickory-resolver = { version = "0.25", default-features = false, features = [ - "tokio", - "system-config", -] } +hickory-resolver = { version = "0.25", default-features = false, features = ["tokio", "system-config"] } honggfuzz = "0.5" http = "1" http-body = "1" @@ -140,23 +134,11 @@ mime = "0.3.17" mime_guess = { version = "2", default-features = false } moka = "0.12" nom = "8.0.0" -opentelemetry = { version = "0.31", default-features = false, features = [ - "trace", -] } +opentelemetry = { version = "0.31", default-features = false, features = ["trace"] } opentelemetry-http = { version = "0.31", default-features = false } -opentelemetry-otlp = { version = "0.31", default-features = false, features = [ - "internal-logs", - "logs", - "metrics", - "trace", -] } -opentelemetry-semantic-conventions = { version = "0.31", features = [ - "semconv_experimental", -] } -opentelemetry_sdk = { version = "0.31", default-features = false, features = [ - "trace", - "rt-tokio", -] } +opentelemetry-otlp = { version = "0.31", default-features = false, features = ["internal-logs", "logs", "metrics", "trace"] } +opentelemetry-semantic-conventions = { version = "0.31", features = ["semconv_experimental"] } +opentelemetry_sdk = { version = "0.31", default-features = false, features = ["trace", "rt-tokio"] } parking_lot = "0.12" percent-encoding = "2.3" pin-project-lite = "0.2" @@ -173,8 +155,8 @@ quickcheck_macros = "1.1" quote = "1.0" radix_trie = "0.3" rama = { version = "0.3.0-rc1", path = "." } -rama-boring = "0.5.12" -rama-boring-tokio = "0.5.12" +rama-boring = { version = "0.6.0" } +rama-boring-tokio = { version = "0.6.0" } rama-core = { version = "0.3.0-rc1", path = "./rama-core" } rama-crypto = { version = "0.3.0-rc1", path = "./rama-crypto" } rama-dns = { version = "0.3.0-rc1", path = "./rama-dns" } @@ -189,6 +171,7 @@ rama-http-headers = { version = "0.3.0-rc1", path = "./rama-http-headers" } rama-http-types = { version = "0.3.0-rc1", path = "./rama-http-types" } rama-macros = { version = "0.3.0-rc1", path = "./rama-macros" } rama-net = { version = "0.3.0-rc1", path = "./rama-net" } +rama-net-apple-networkextension = { version = "0.3.0-rc1", path = "./rama-net-apple-networkextension" } rama-proxy = { version = "0.3.0-rc1", path = "./rama-proxy" } rama-socks5 = { version = "0.3.0-rc1", path = "./rama-socks5" } rama-tcp = { version = "0.3.0-rc1", path = "./rama-tcp" } @@ -206,12 +189,7 @@ ratatui = "0.30" rawzip = { version = "0.4" } rcgen = { version = "0.14", default-features = false, features = ["pem", "aws_lc_rs", "x509-parser"] } regex = "1.12" -rustls = { version = "0.23", default-features = false, features = [ - "logging", - "std", - "tls12", - "aws_lc_rs", -] } +rustls = { version = "0.23", default-features = false, features = ["logging", "std", "tls12", "aws_lc_rs"] } rustls-native-certs = "0.8" rustls-pki-types = "^1" rustversion = "1.0" @@ -235,11 +213,7 @@ terminal-prompt = "0.2" tokio = "1.48" tokio-graceful = "0.2" tokio-postgres = "0.7" -tokio-rustls = { version = "0.26", default-features = false, features = [ - "logging", - "tls12", - "aws_lc_rs", -] } +tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "aws_lc_rs"] } tokio-stream = "0.1" tokio-test = "0.4" tokio-util = "0.7" @@ -362,7 +336,8 @@ cli = [ "http", ] net = ["dep:rama-net"] -dns = ["net", "dep:rama-dns", "rama-socks5?/dns"] +net-apple-networkextension = ["net", "dep:rama-net-apple-networkextension"] +dns = ["net", "dep:rama-dns"] tcp = ["dns", "dep:rama-tcp"] udp = ["net", "dep:rama-udp"] ws = ["dep:rama-ws", "http"] @@ -423,6 +398,7 @@ rama-http = { workspace = true, optional = true } rama-http-backend = { workspace = true, optional = true } rama-http-core = { workspace = true, optional = true } rama-net = { workspace = true, optional = true } +rama-net-apple-networkextension = { workspace = true, optional = true } rama-proxy = { workspace = true, optional = true } rama-socks5 = { workspace = true, optional = true } rama-tcp = { workspace = true, optional = true } @@ -596,6 +572,10 @@ required-features = ["http-full"] name = "http_mitm_proxy_boring" required-features = ["http-full", "boring"] +[[example]] +name = "http_mitm_relay_proxy_boring" +required-features = ["http-full", "boring"] + [[example]] name = "http_mitm_proxy_rustls" required-features = ["http-full", "rustls"] diff --git a/README.md b/README.md index b9e36b4b1..c40ac1b53 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml @@ -292,6 +292,7 @@ Here is a list of all `rama` crates: used by all other `rama` code, as well as some other _core_ utilities - [`rama-crypto`](https://crates.io/crates/rama-crypto): rama crypto primitives and dependencies - [`rama-net`](https://crates.io/crates/rama-net): rama network types and utilities +- [`rama-net-apple-networkextension`](https://crates.io/crates/rama-net-apple-networkextension): Apple Network Extension support for rama - [`rama-dns`](https://crates.io/crates/rama-dns): DNS support for rama - [`rama-unix`](https://crates.io/crates/rama-unix): Unix (domain) socket support for rama - [`rama-tcp`](https://crates.io/crates/rama-tcp): TCP support for rama @@ -460,7 +461,7 @@ and continue to happen with community/ecosystem support. ### Minimum supported Rust version -rama's MSRV is `1.91`. +rama's MSRV is `1.93`. [Using GitHub Actions we also test](https://github.com/plabayo/rama/blob/main/.github/workflows/CI.yml) if `rama` on that version still works on the stable and beta versions of _rust_ as well. diff --git a/benches/e2e_http_client_server.rs b/benches/e2e_http_client_server.rs index a20759406..f49727431 100644 --- a/benches/e2e_http_client_server.rs +++ b/benches/e2e_http_client_server.rs @@ -187,10 +187,10 @@ where )); match params.version { - HttpVersion::Http1 => HttpServer::http1(Executor::default()) + HttpVersion::Http1 => HttpServer::new_http1(Executor::default()) .service(http_service) .boxed(), - HttpVersion::Http2 => HttpServer::h2(Executor::default()) + HttpVersion::Http2 => HttpServer::new_h2(Executor::default()) .service(http_service) .boxed(), } diff --git a/docs/book/src/crate.md b/docs/book/src/crate.md index bc6e58d13..22a084655 100644 --- a/docs/book/src/crate.md +++ b/docs/book/src/crate.md @@ -32,6 +32,7 @@ Here is a list of all `rama` crates: used by all other `rama` code, as well as some other _core_ utilities - [`rama-crypto`](https://crates.io/crates/rama-crytpo): rama crypto primitives and dependencies - [`rama-net`](https://crates.io/crates/rama-net): rama network types and utilities +- [`rama-net-apple-networkextension`](https://crates.io/crates/rama-net-apple-networkextension): Apple Network Extension support for rama - [`rama-dns`](https://crates.io/crates/rama-dns): DNS support for rama - [`rama-unix`](https://crates.io/crates/rama-unix): Unix (domain) socket support for rama - [`rama-tcp`](https://crates.io/crates/rama-tcp): TCP support for rama @@ -133,7 +134,7 @@ and continue to happen with community/ecosystem support. ### Minimum supported Rust version -Rama's MSRV is `1.91`. +Rama's MSRV is `1.93`. [Using GitHub Actions we also test](https://github.com/plabayo/rama/blob/main/.github/workflows/CI.yml) if `rama` on that version still works on the stable and beta versions of _rust_ as well. diff --git a/docs/book/src/ecosystem.md b/docs/book/src/ecosystem.md index 1a0e290e7..b8cd49b07 100644 --- a/docs/book/src/ecosystem.md +++ b/docs/book/src/ecosystem.md @@ -33,6 +33,7 @@ Here is a list of all `rama` crates: used by all other `rama` code, as well as some other _core_ utilities - [`rama-crypto`](https://crates.io/crates/rama-crytpo): rama crypto primitives and dependencies - [`rama-net`](https://crates.io/crates/rama-net): rama network types and utilities +- [`rama-net-apple-networkextension`](https://crates.io/crates/rama-net-apple-networkextension): Apple Network Extension support for rama - [`rama-dns`](https://crates.io/crates/rama-dns): DNS support for rama - [`rama-unix`](https://crates.io/crates/rama-unix): Unix (domain) socket support for rama - [`rama-tcp`](https://crates.io/crates/rama-tcp): TCP support for rama diff --git a/docs/book/src/proxies/mitm.md b/docs/book/src/proxies/mitm.md index d59ab9cd6..d0e333635 100644 --- a/docs/book/src/proxies/mitm.md +++ b/docs/book/src/proxies/mitm.md @@ -17,6 +17,11 @@ proxying them to the target host using Boring for TLS. - Similar to [/examples/http_connect_proxy.rs](https://github.com/plabayo/rama/tree/main/examples/http_connect_proxy.rs) but with MITM capabilities for both HTTP and HTTPS requests. + +- [/examples/http_mitm_relay_proxy_boring.rs](https://github.com/plabayo/rama/tree/main/examples/http_mitm_relay_proxy_boring.rs): + Similar to [/examples/http_mitm_proxy_boring.rs](https://github.com/plabayo/rama/tree/main/examples/http_mitm_proxy_boring.rs), but with a more advanced flow, + and usually the kind of approach more desired for MITM proxies, + especially transparent proxies. - [/examples/http_mitm_proxy_rustls.rs](https://github.com/plabayo/rama/tree/main/examples/http_mitm_proxy_rustls.rs): A minimal HTTP proxy that accepts both HTTP/1.1 and HTTP/2 connections, diff --git a/examples/README.md b/examples/README.md index 85e2e42ba..be24f4004 100644 --- a/examples/README.md +++ b/examples/README.md @@ -81,6 +81,7 @@ The following examples show how you can integrate ACME into you webservices (ACM - [`http_connect_proxy.rs`](./http_connect_proxy.rs) - HTTP CONNECT proxy implementation - [`http_mitm_proxy_rustls.rs`](./http_mitm_proxy_rustls.rs) - MITM proxy using Rustls - [`http_mitm_proxy_boring.rs`](./http_mitm_proxy_boring.rs) - MITM proxy using BoringSSL +- [`http_mitm_relay_proxy_boring.rs`](./http_mitm_relay_proxy_boring.rs) - MITM proxy using BoringSSL with a more advanced relay approach ### Http within TLS Proxies diff --git a/examples/acme_http_challenge.rs b/examples/acme_http_challenge.rs index 62310ce6c..0a7b0ff2f 100644 --- a/examples/acme_http_challenge.rs +++ b/examples/acme_http_challenge.rs @@ -258,7 +258,7 @@ async fn main() { ) .into_layer(http_service); - TcpListener::bind(ADDR, exec) + TcpListener::bind_address(ADDR, exec) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/acme_tls_challenge_using_boring.rs b/examples/acme_tls_challenge_using_boring.rs index f5b6f6c8f..fc12d1416 100644 --- a/examples/acme_tls_challenge_using_boring.rs +++ b/examples/acme_tls_challenge_using_boring.rs @@ -201,7 +201,7 @@ async fn main() { let tcp_service = TlsAcceptorLayer::new(acceptor_data).layer(service_fn(internal_tcp_service_fn)); - TcpListener::bind("127.0.0.1:5001", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:5001", Executor::graceful(guard)) .await .expect("bind TCP Listener: tls") .serve(tcp_service) @@ -258,7 +258,7 @@ async fn main() { ) .into_layer(http_service); - TcpListener::bind(ADDR, Executor::graceful(guard)) + TcpListener::bind_address(ADDR, Executor::graceful(guard)) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/acme_tls_challenge_using_rustls.rs b/examples/acme_tls_challenge_using_rustls.rs index 64b65c974..20c835c04 100644 --- a/examples/acme_tls_challenge_using_rustls.rs +++ b/examples/acme_tls_challenge_using_rustls.rs @@ -188,7 +188,7 @@ async fn main() { let tcp_service = TlsAcceptorLayer::new(acceptor_data).layer(service_fn(internal_tcp_service_fn)); - TcpListener::bind("127.0.0.1:5001", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:5001", Executor::graceful(guard)) .await .expect("bind TCP Listener: tls") .serve(tcp_service) @@ -248,7 +248,7 @@ async fn main() { ) .into_layer(http_service); - TcpListener::bind(ADDR, exec) + TcpListener::bind_address(ADDR, exec) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/grpc/src/shared/tests/compression/bidirectional_stream.rs b/examples/grpc/src/shared/tests/compression/bidirectional_stream.rs index bff984fd5..04f748407 100644 --- a/examples/grpc/src/shared/tests/compression/bidirectional_stream.rs +++ b/examples/grpc/src/shared/tests/compression/bidirectional_stream.rs @@ -73,7 +73,7 @@ async fn client_enabled_server_enabled(encoding: CompressionEncoding) { ) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( diff --git a/examples/grpc/src/shared/tests/compression/client_stream.rs b/examples/grpc/src/shared/tests/compression/client_stream.rs index 2d0223ea5..1497837ce 100644 --- a/examples/grpc/src/shared/tests/compression/client_stream.rs +++ b/examples/grpc/src/shared/tests/compression/client_stream.rs @@ -65,7 +65,7 @@ async fn client_enabled_server_enabled(encoding: CompressionEncoding) { ) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( @@ -110,7 +110,7 @@ async fn client_disabled_server_enabled(encoding: CompressionEncoding) { ) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( diff --git a/examples/grpc/src/shared/tests/compression/compressing_request.rs b/examples/grpc/src/shared/tests/compression/compressing_request.rs index a4b924190..18c5bb3b5 100644 --- a/examples/grpc/src/shared/tests/compression/compressing_request.rs +++ b/examples/grpc/src/shared/tests/compression/compressing_request.rs @@ -114,7 +114,7 @@ async fn client_enabled_server_enabled_multi_encoding(encoding: CompressionEncod ) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( diff --git a/examples/grpc/src/shared/tests/compression/compressing_response.rs b/examples/grpc/src/shared/tests/compression/compressing_response.rs index e1dde44cf..f74b29e11 100644 --- a/examples/grpc/src/shared/tests/compression/compressing_response.rs +++ b/examples/grpc/src/shared/tests/compression/compressing_response.rs @@ -122,7 +122,7 @@ async fn client_enabled_server_disabled(encoding: CompressionEncoding) { }) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( @@ -248,7 +248,7 @@ async fn server_replying_with_unsupported_encoding(encoding: CompressionEncoding let grpc_svc = MapOutputLayer::new(add_weird_content_encoding).into_layer(svc); - let server = HttpServer::h2(Executor::default()).service(grpc_svc); + let server = HttpServer::new_h2(Executor::default()).service(grpc_svc); let client = test_client::TestClient::new( mock_io_client(move || server.clone()), @@ -335,7 +335,7 @@ async fn disabling_compression_on_response_but_keeping_compression_on_stream( }) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( diff --git a/examples/grpc/src/shared/tests/compression/server_stream.rs b/examples/grpc/src/shared/tests/compression/server_stream.rs index ae23de999..67511ec18 100644 --- a/examples/grpc/src/shared/tests/compression/server_stream.rs +++ b/examples/grpc/src/shared/tests/compression/server_stream.rs @@ -40,7 +40,7 @@ async fn client_enabled_server_enabled(encoding: CompressionEncoding) { }) .into_layer(svc); - HttpServer::h2(Executor::default()).service(grpc_svc) + HttpServer::new_h2(Executor::default()).service(grpc_svc) }; let client = test_client::TestClient::new( diff --git a/examples/grpc/src/shared/tests/integration/client_layer.rs b/examples/grpc/src/shared/tests/integration/client_layer.rs index 3be34071e..9eacae067 100644 --- a/examples/grpc/src/shared/tests/integration/client_layer.rs +++ b/examples/grpc/src/shared/tests/integration/client_layer.rs @@ -39,14 +39,14 @@ async fn connect_supports_standard_rama_http_layers() { let graceful = Shutdown::new(async { drop(rx.await) }); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::local_ipv4(0), exec) + let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), exec) .await .unwrap(); let addr = listener.local_addr().unwrap(); let jh = graceful.spawn_task_fn(async move |guard| { listener - .serve(HttpServer::h2(Executor::graceful(guard)).service(svc)) + .serve(HttpServer::new_h2(Executor::graceful(guard)).service(svc)) .await; }); diff --git a/examples/grpc/src/shared/tests/integration/http2_keep_alive.rs b/examples/grpc/src/shared/tests/integration/http2_keep_alive.rs index 2047c0f4b..078d59851 100644 --- a/examples/grpc/src/shared/tests/integration/http2_keep_alive.rs +++ b/examples/grpc/src/shared/tests/integration/http2_keep_alive.rs @@ -37,13 +37,13 @@ async fn http2_keepalive_does_not_cause_panics() { let graceful = Shutdown::new(async { drop(rx.await) }); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::local_ipv4(0), exec) + let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), exec) .await .unwrap(); let addr = listener.local_addr().unwrap(); let jh = graceful.spawn_task_fn(async move |guard| { - let mut server = HttpServer::h2(Executor::graceful(guard.clone())); + let mut server = HttpServer::new_h2(Executor::graceful(guard.clone())); server .h2_mut() .set_keep_alive_interval(Duration::from_secs(10)); @@ -74,13 +74,13 @@ async fn http2_keepalive_does_not_cause_panics_on_client_side() { let graceful = Shutdown::new(async { drop(rx.await) }); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::local_ipv4(0), exec) + let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), exec) .await .unwrap(); let addr = listener.local_addr().unwrap(); let jh = graceful.spawn_task_fn(async move |guard| { - let mut server = HttpServer::h2(Executor::graceful(guard.clone())); + let mut server = HttpServer::new_h2(Executor::graceful(guard.clone())); server .h2_mut() .set_keep_alive_interval(Duration::from_secs(5)); diff --git a/examples/grpc/src/shared/tests/integration/http2_max_header_list_size.rs b/examples/grpc/src/shared/tests/integration/http2_max_header_list_size.rs index 9f49282a0..614cc59f4 100644 --- a/examples/grpc/src/shared/tests/integration/http2_max_header_list_size.rs +++ b/examples/grpc/src/shared/tests/integration/http2_max_header_list_size.rs @@ -53,13 +53,13 @@ async fn test_http_max_header_list_size_and_long_errors() { let graceful = Shutdown::new(async { drop(rx.await) }); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::local_ipv4(0), exec) + let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), exec) .await .unwrap(); let addr = format!("http://{}", listener.local_addr().unwrap()); let jh = graceful.spawn_task_fn(async move |guard| { - let mut http_server = HttpServer::h2(Executor::graceful(guard.clone())); + let mut http_server = HttpServer::new_h2(Executor::graceful(guard.clone())); http_server.h2_mut().set_max_pending_accept_reset_streams(0); let tcp_service = MapInputLayer::new(|stream: TcpStream| { diff --git a/examples/grpc/src/shared/tests/integration/max_message_size.rs b/examples/grpc/src/shared/tests/integration/max_message_size.rs index 2bb014787..32865e23b 100644 --- a/examples/grpc/src/shared/tests/integration/max_message_size.rs +++ b/examples/grpc/src/shared/tests/integration/max_message_size.rs @@ -124,7 +124,7 @@ async fn response_stream_limit() { let svc = test1_server::Test1Server::new(Svc); - let server = HttpServer::h2(Executor::default()).service(svc); + let server = HttpServer::new_h2(Executor::default()).service(svc); let client = test1_client::Test1Client::new( super::mock_io_client(move || server.clone()), @@ -198,12 +198,9 @@ fn assert_test_case(case: TestCase) { match (case.expected_code, res) { (Some(_), Ok(())) => panic!("Expected failure, but got success"), - (Some(code), Err(status)) => { - if status.code() != code { - panic!("Expected failure, got failure but wrong code, got: {status:?}") - } + (Some(code), Err(status)) if status.code() != code => { + panic!("Expected failure, got failure but wrong code, got: {status:?}") } - (None, Err(status)) => panic!("Expected success, but got failure, got: {status:?}"), _ => (), @@ -257,7 +254,7 @@ async fn max_message_run(case: &TestCase) -> Result<(), Status> { svc.set_max_encoding_message_size(size); } - let server = HttpServer::h2(Executor::default()).service(svc); + let server = HttpServer::new_h2(Executor::default()).service(svc); let mut client = test1_client::Test1Client::new( super::mock_io_client(move || server.clone()), diff --git a/examples/grpc/src/shared/tests/integration/timeout.rs b/examples/grpc/src/shared/tests/integration/timeout.rs index 8a503071b..70cbed88d 100644 --- a/examples/grpc/src/shared/tests/integration/timeout.rs +++ b/examples/grpc/src/shared/tests/integration/timeout.rs @@ -97,7 +97,7 @@ async fn run_service_in_background(latency: Duration, server_timeout: Duration) let svc = test_server::TestServer::new(Svc { latency }); - let listener = TcpListener::bind(SocketAddress::local_ipv4(0), Executor::default()) + let listener = TcpListener::bind_address(SocketAddress::local_ipv4(0), Executor::default()) .await .unwrap(); let addr = listener.local_addr().unwrap(); @@ -111,7 +111,7 @@ async fn run_service_in_background(latency: Duration, server_timeout: Duration) tokio::spawn(async move { listener - .serve(HttpServer::h2(Executor::default()).service(grpc_svc)) + .serve(HttpServer::new_h2(Executor::default()).service(grpc_svc)) .await; }); diff --git a/examples/grpc/src/shared/tests/web/grpc.rs b/examples/grpc/src/shared/tests/web/grpc.rs index e1c6b744c..308971a07 100644 --- a/examples/grpc/src/shared/tests/web/grpc.rs +++ b/examples/grpc/src/shared/tests/web/grpc.rs @@ -107,7 +107,7 @@ async fn smoke_error() { async fn bind() -> (TcpListener, String) { let addr = SocketAddress::local_ipv4(0); - let lis = TcpListener::bind(addr, Executor::default()) + let lis = TcpListener::bind_address(addr, Executor::default()) .await .expect("listener"); let url = format!("http://{}", lis.local_addr().unwrap()); @@ -127,7 +127,7 @@ async fn grpc(accept_h1: bool) -> (impl Future, String) { .await; } else { listener - .serve(HttpServer::h2(Executor::default()).service(http_svc)) + .serve(HttpServer::new_h2(Executor::default()).service(http_svc)) .await; } }; @@ -147,7 +147,7 @@ async fn grpc_web(accept_h1: bool) -> (impl Future, String) { .await; } else { listener - .serve(HttpServer::h2(Executor::default()).service(http_svc)) + .serve(HttpServer::new_h2(Executor::default()).service(http_svc)) .await; } }; diff --git a/examples/grpc/src/shared/tests/web/grpc_web.rs b/examples/grpc/src/shared/tests/web/grpc_web.rs index e6f679059..03b898d97 100644 --- a/examples/grpc/src/shared/tests/web/grpc_web.rs +++ b/examples/grpc/src/shared/tests/web/grpc_web.rs @@ -69,7 +69,7 @@ async fn text_request() { async fn spawn() -> String { let addr = SocketAddress::local_ipv4(0); - let listener = TcpListener::bind(addr, Executor::default()) + let listener = TcpListener::bind_address(addr, Executor::default()) .await .expect("listener"); let url = format!("http://{}", listener.local_addr().unwrap()); diff --git a/examples/grpc/src/shared/tests/web/mod.rs b/examples/grpc/src/shared/tests/web/mod.rs index 3424559a3..87613658e 100644 --- a/examples/grpc/src/shared/tests/web/mod.rs +++ b/examples/grpc/src/shared/tests/web/mod.rs @@ -3,7 +3,7 @@ use std::pin::Pin; use rama::{ futures::stream::{self, Stream}, http::grpc::{Request, Response, Status, Streaming}, - stream::StreamExt, + stream::StreamExt as _, telemetry::tracing, }; diff --git a/examples/haproxy_client_ip.rs b/examples/haproxy_client_ip.rs index fc8cf8447..f27f222d6 100644 --- a/examples/haproxy_client_ip.rs +++ b/examples/haproxy_client_ip.rs @@ -78,7 +78,7 @@ async fn main() { )), )); - TcpListener::bind("127.0.0.1:62025", exec) + TcpListener::bind_address("127.0.0.1:62025", exec) .await .expect("bind TCP Listener") .serve( diff --git a/examples/http_abort.rs b/examples/http_abort.rs index 6730b7b0e..dcbfec804 100644 --- a/examples/http_abort.rs +++ b/examples/http_abort.rs @@ -79,7 +79,7 @@ async fn main() { graceful.spawn_task_fn(async |guard| { tracing::info!("running service at: {ADDRESS}"); let exec = Executor::graceful(guard); - TcpListener::bind(ADDRESS, exec) + TcpListener::bind_address(ADDRESS, exec) .await .unwrap() .serve(tcp_svc) diff --git a/examples/http_anti_bot_infinite_resource.rs b/examples/http_anti_bot_infinite_resource.rs index 4100fd36e..b7962bc2a 100644 --- a/examples/http_anti_bot_infinite_resource.rs +++ b/examples/http_anti_bot_infinite_resource.rs @@ -101,7 +101,7 @@ async fn main() { let exec = Executor::graceful(graceful.guard()); let tcp_server = TcpListener::build(exec) - .bind(address) + .bind_address(address) .await .expect("bind tcp server"); diff --git a/examples/http_anti_bot_zip_bomb.rs b/examples/http_anti_bot_zip_bomb.rs index 537973706..bd13f6ef8 100644 --- a/examples/http_anti_bot_zip_bomb.rs +++ b/examples/http_anti_bot_zip_bomb.rs @@ -81,7 +81,7 @@ async fn main() { let address = SocketAddress::local_ipv4(62036); tracing::info!("running service at: {address}"); let exec = Executor::graceful(graceful.guard()); - let tcp_server = TcpListener::bind(address, exec) + let tcp_server = TcpListener::bind_address(address, exec) .await .expect("bind tcp server"); diff --git a/examples/http_connect_proxy.rs b/examples/http_connect_proxy.rs index 32e338fd6..e7dfabbf7 100644 --- a/examples/http_connect_proxy.rs +++ b/examples/http_connect_proxy.rs @@ -58,7 +58,7 @@ use rama::{ Layer, Service, - extensions::{Extensions, ExtensionsMut, ExtensionsRef, InputExtensions}, + extensions::{Extensions, ExtensionsRef, InputExtensions}, http::{ Body, Request, Response, StatusCode, client::EasyHttpWebClient, @@ -66,26 +66,21 @@ use rama::{ proxy_auth::ProxyAuthLayer, remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, trace::TraceLayer, - upgrade::UpgradeLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, }, matcher::{DomainMatcher, HttpMatcher, MethodMatcher}, server::HttpServer, - service::web::{ - extract::Path, - match_service, - response::{IntoResponse, Json}, - }, + service::web::{extract::Path, match_service, response::Json}, }, layer::{ConsumeErrLayer, HijackLayer}, net::{ - http::RequestContext, - proxy::ProxyTarget, + proxy::IoForwardService, stream::{ClientSocketInfo, layer::http::BodyLimitLayer}, user::credentials::basic, }, rt::Executor, service::service_fn, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -121,7 +116,7 @@ async fn main() { graceful.spawn_task_fn(async move |guard| { let exec = Executor::graceful(guard); - let tcp_service = TcpListener::build(exec.clone()).bind("127.0.0.1:62001").await.expect("bind tcp proxy to 127.0.0.1:62001"); + let tcp_service = TcpListener::build(exec.clone()).bind_address("127.0.0.1:62001").await.expect("bind tcp proxy to 127.0.0.1:62001"); let http_service = HttpServer::auto(exec.clone()) .service(( @@ -158,8 +153,11 @@ async fn main() { UpgradeLayer::new( exec.clone(), MethodMatcher::CONNECT, - service_fn(http_connect_accept), - ConsumeErrLayer::default().into_layer(Forwarder::ctx(exec)), + DefaultHttpProxyConnectReplyService::new(), + ( + ConsumeErrLayer::default(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec), + ).into_layer(IoForwardService::new()), ), RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop(), @@ -178,25 +176,6 @@ async fn main() { .expect("graceful shutdown"); } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_plain_proxy(req: Request) -> Result { let client = EasyHttpWebClient::default(); match client.serve(req).await { diff --git a/examples/http_har_replay.rs b/examples/http_har_replay.rs index 036f5d6b2..05dffaa0b 100644 --- a/examples/http_har_replay.rs +++ b/examples/http_har_replay.rs @@ -19,6 +19,7 @@ use rama::{ graceful::Shutdown, http::{ Request, Response, Uri, + body::util::BodyExt, client::EasyHttpWebClient, layer::{ compression::{CompressionLayer, predicate::Always}, @@ -43,7 +44,6 @@ use rama::{ }, utils::{backoff::ExponentialBackoff, rng::HasherRng}, }; -use rama_http::body::util::BodyExt; use std::{convert::Infallible, fs, sync::Arc, time::Duration}; use tokio::sync::oneshot; @@ -81,8 +81,8 @@ async fn main() { let exec = Executor::graceful(graceful.guard()); let traffic_writer = BidirectionalWriter::stdout_unbounded( &exec, - Some(rama_http::layer::traffic_writer::WriterMode::All), - Some(rama_http::layer::traffic_writer::WriterMode::All), + Some(rama::http::layer::traffic_writer::WriterMode::All), + Some(rama::http::layer::traffic_writer::WriterMode::All), ); let client = ( @@ -215,7 +215,7 @@ async fn run_server(addr: SocketAddress, log_file: Arc) { })), ); - TcpListener::bind(ADDRESS, exec) + TcpListener::bind_address(ADDRESS, exec) .await .unwrap() .serve(Abortable::new(http_svc)) diff --git a/examples/http_https_socks5_and_socks5h_connect_proxy.rs b/examples/http_https_socks5_and_socks5h_connect_proxy.rs index 3fcc6ceb8..9dd0ee325 100644 --- a/examples/http_https_socks5_and_socks5h_connect_proxy.rs +++ b/examples/http_https_socks5_and_socks5h_connect_proxy.rs @@ -25,7 +25,7 @@ use rama::{ Layer, Service, - extensions::{ExtensionsMut, ExtensionsRef, InputExtensions}, + extensions::{ExtensionsRef, InputExtensions}, http::{ Body, Request, Response, StatusCode, client::EasyHttpWebClient, @@ -33,27 +33,22 @@ use rama::{ proxy_auth::ProxyAuthLayer, remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, trace::TraceLayer, - upgrade::UpgradeLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, }, matcher::MethodMatcher, server::HttpServer, - service::web::response::IntoResponse, }, layer::ConsumeErrLayer, net::{ - http::RequestContext, - proxy::ProxyTarget, + proxy::IoForwardService, stream::ClientSocketInfo, - tls::{ - SecureTransport, - server::{SelfSignedData, TlsPeekRouter}, - }, + tls::server::{SelfSignedData, TlsPeekRouter}, user::credentials::basic, }, proxy::socks5::{Socks5Acceptor, server::Socks5PeekRouter}, rt::Executor, service::service_fn, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -120,7 +115,7 @@ async fn main() { let exec = Executor::graceful(graceful.guard()); - let tcp_service = TcpListener::bind("127.0.0.1:62029", exec.clone()) + let tcp_service = TcpListener::bind_address("127.0.0.1:62029", exec.clone()) .await .expect("bind http+https+socks5+socks5h proxy to 127.0.0.1:62029"); @@ -135,8 +130,12 @@ async fn main() { UpgradeLayer::new( exec.clone(), MethodMatcher::CONNECT, - service_fn(http_connect_accept), - ConsumeErrLayer::default().into_layer(Forwarder::ctx(exec)), + DefaultHttpProxyConnectReplyService::new(), + ( + ConsumeErrLayer::default(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec), + ) + .into_layer(IoForwardService::new()), ), RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop(), @@ -158,30 +157,6 @@ async fn main() { .expect("graceful shutdown"); } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - tracing::info!( - "proxy secure transport ingress: {:?}", - req.extensions().get::() - ); - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_plain_proxy(req: Request) -> Result { let client = EasyHttpWebClient::default(); match client.serve(req).await { diff --git a/examples/http_mitm_proxy_boring.rs b/examples/http_mitm_proxy_boring.rs index cb0d9bc93..44c021f36 100644 --- a/examples/http_mitm_proxy_boring.rs +++ b/examples/http_mitm_proxy_boring.rs @@ -54,8 +54,7 @@ use rama::{ Layer, Service, error::{BoxError, ErrorContext}, - extensions::Extensions, - extensions::{ExtensionsMut, ExtensionsRef}, + extensions::{Extensions, ExtensionsRef}, futures::SinkExt, http::{ Body, Request, Response, StatusCode, Version, @@ -75,7 +74,7 @@ use rama::{ required_header::AddRequiredRequestHeadersLayer, trace::TraceLayer, traffic_writer::{self, RequestWriterLayer}, - upgrade::{UpgradeLayer, Upgraded}, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer, Upgraded}, }, matcher::MethodMatcher, proto::RequestHeaders, @@ -83,15 +82,13 @@ use rama::{ service::web::response::IntoResponse, ws::{ AsyncWebSocket, Message, ProtocolError, - handshake::{client::HttpClientWebSocketExt, server::WebSocketMatcher}, + handshake::{client::HttpClientWebSocketExt, matcher::WebSocketMatcher}, protocol::{Role, WebSocketConfig}, }, }, layer::{AddInputExtensionLayer, ConsumeErrLayer}, matcher::Matcher, net::{ - http::RequestContext, - proxy::ProxyTarget, stream::layer::http::BodyLimitLayer, tls::{ ApplicationProtocol, SecureTransport, @@ -156,7 +153,7 @@ async fn main() -> Result<(), BoxError> { graceful.spawn_task(async { let tcp_service = TcpListener::build(exec.clone()) - .bind("127.0.0.1:62017") + .bind_address("127.0.0.1:62017") .await .expect("bind tcp proxy to 127.0.0.1:62017"); @@ -171,7 +168,7 @@ async fn main() -> Result<(), BoxError> { UpgradeLayer::new( exec, MethodMatcher::CONNECT, - service_fn(http_connect_accept), + DefaultHttpProxyConnectReplyService::new(), service_fn(http_connect_proxy), ), ) @@ -198,25 +195,6 @@ async fn main() -> Result<(), BoxError> { Ok(()) } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { // In the past we deleted the request context here, as such: // ``` diff --git a/examples/http_mitm_proxy_rustls.rs b/examples/http_mitm_proxy_rustls.rs index d431c9ae3..9323c0793 100644 --- a/examples/http_mitm_proxy_rustls.rs +++ b/examples/http_mitm_proxy_rustls.rs @@ -35,7 +35,7 @@ use rama::{ Layer, Service, error::{BoxError, ErrorContext}, - extensions::{ExtensionsMut, ExtensionsRef}, + extensions::ExtensionsRef, http::{ Body, Request, Response, StatusCode, Version, client::EasyHttpWebClient, @@ -47,16 +47,14 @@ use rama::{ remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, required_header::AddRequiredRequestHeadersLayer, trace::TraceLayer, - upgrade::{UpgradeLayer, Upgraded}, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer, Upgraded}, }, matcher::MethodMatcher, server::HttpServer, - service::web::response::IntoResponse, }, layer::{AddInputExtensionLayer, ConsumeErrLayer}, net::{ - http::RequestContext, proxy::ProxyTarget, stream::layer::http::BodyLimitLayer, - tls::server::SelfSignedData, user::credentials::basic, + stream::layer::http::BodyLimitLayer, tls::server::SelfSignedData, user::credentials::basic, }, rt::Executor, service::service_fn, @@ -104,7 +102,7 @@ async fn main() -> Result<(), BoxError> { graceful.spawn_task(async { let tcp_service = TcpListener::build(exec.clone()) - .bind("127.0.0.1:62019") + .bind_address("127.0.0.1:62019") .await .expect("bind tcp proxy to 127.0.0.1:62019"); @@ -119,7 +117,7 @@ async fn main() -> Result<(), BoxError> { UpgradeLayer::new( exec, MethodMatcher::CONNECT, - service_fn(http_connect_accept), + DefaultHttpProxyConnectReplyService::new(), service_fn(http_connect_proxy), ), ) @@ -146,25 +144,6 @@ async fn main() -> Result<(), BoxError> { Ok(()) } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { // In the past we deleted the request context here, as such: // ``` diff --git a/examples/http_mitm_relay_proxy_boring.rs b/examples/http_mitm_relay_proxy_boring.rs new file mode 100644 index 000000000..2ef76c724 --- /dev/null +++ b/examples/http_mitm_relay_proxy_boring.rs @@ -0,0 +1,176 @@ +//! This example shows how one can begin with creating a MITM proxy, +//! using a relay approach. This in contrast to `http_mitm_proxy_boring`, +//! where the flow is rather linear, here the approach is to handshake more like a dance. +//! +//! It is as such a more complex flow, but the advantage is that your proxy's +//! TLS acceptor will mimic the certificate and server (TLS) settings from +//! the target and the http client (egress) will nicely be 1:1 tied +//! the ingress traffic and mirror it. +//! +//! Note that this proxy is not production ready, and is only meant +//! to show you how one might start. +//! +//! # Run the example +//! +//! ```sh +//! cargo run --example http_mitm_relay_proxy_boring --features=http-full,boring +//! ``` +//! +//! ## Expected output +//! +//! The server will start and listen on `:62049`. You can use `curl` to interact with the service: +//! +//! ```sh +//! curl -v -x http://127.0.0.1:62049 --proxy-user 'john:secret' http://www.example.com/ +//! curl -k -v -x http://127.0.0.1:62049 --proxy-user 'john:secret' https://www.example.com/ +//! ``` + +use rama::{ + Layer, Service, + error::{BoxError, ErrorContext}, + extensions::ExtensionsMut, + http::{ + HeaderName, HeaderValue, + client::EasyHttpWebClient, + layer::{ + map_response_body::MapResponseBodyLayer, + proxy_auth::ProxyAuthLayer, + set_header::{SetRequestHeaderLayer, SetResponseHeaderLayer}, + trace::TraceLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, + }, + matcher::MethodMatcher, + proxy::mitm::{DefaultErrorResponse, HttpMitmRelay}, + server::HttpServer, + }, + io::Io, + layer::{ArcLayer, ConsumeErrLayer}, + net::{ + http::server::HttpPeekRouter, + proxy::IoForwardService, + stream::layer::http::BodyLimitLayer, + tls::server::{PeekTlsClientHelloService, SelfSignedData}, + user::credentials::basic, + }, + rt::Executor, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, + telemetry::tracing::{ + self, + level_filters::LevelFilter, + subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}, + }, + tls::boring::proxy::TlsMitmRelay, +}; + +use std::{convert::Infallible, sync::Arc, time::Duration}; + +#[tokio::main] +async fn main() -> Result<(), BoxError> { + tracing::subscriber::registry() + .with(fmt::layer()) + .with( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); + + let graceful = rama::graceful::Shutdown::default(); + let exec = Executor::graceful(graceful.guard()); + + let mitm_svc = new_mitm_svc(exec.clone()).context("build MITM service")?; + + graceful.spawn_task_fn(async move |guard| { + let tcp_service = TcpListener::build(Executor::graceful(guard.clone())) + .bind_address("127.0.0.1:62049") + .await + .expect("bind tcp proxy to 127.0.0.1:62049"); + + let http_service = HttpServer::auto(exec).service(Arc::new( + ( + TraceLayer::new_for_http(), + ConsumeErrLayer::default(), + // See [`ProxyAuthLayer::with_labels`] for more information, + // e.g. can also be used to extract upstream proxy filters + ProxyAuthLayer::new(basic!("john", "secret")), + UpgradeLayer::new( + Executor::graceful(guard.clone()), + MethodMatcher::CONNECT, + DefaultHttpProxyConnectReplyService::new(), + mitm_svc, + ), + ( + SetRequestHeaderLayer::overriding( + HeaderName::from_static("x-observed"), + HeaderValue::from_static("1"), + ), + SetResponseHeaderLayer::overriding( + HeaderName::from_static("x-proxy"), + HeaderValue::from_static(rama::utils::info::NAME), + ), + SetResponseHeaderLayer::overriding( + HeaderName::from_static("x-proxy-version"), + HeaderValue::from_static(rama::utils::info::VERSION), + ), + ), + ) + .into_layer(EasyHttpWebClient::default_with_executor( + Executor::graceful(guard), + )), + )); + + tcp_service + .serve(BodyLimitLayer::symmetric(2 * 1024 * 1024).into_layer(http_service)) + .await; + }); + + graceful + .shutdown_with_limit(Duration::from_secs(30)) + .await + .context("graceful shutdown")?; + + Ok(()) +} + +fn new_mitm_svc( + exec: Executor, +) -> Result + Clone, BoxError> { + let http_mitm_relay = HttpMitmRelay::new(exec.clone()).with_http_middleware(( + ConsumeErrLayer::trace_as_debug().with_response(DefaultErrorResponse::new()), + MapResponseBodyLayer::new_boxed_streaming_body(), + TraceLayer::new_for_http(), + SetRequestHeaderLayer::overriding( + HeaderName::from_static("x-observed"), + HeaderValue::from_static("1"), + ), + SetResponseHeaderLayer::overriding( + HeaderName::from_static("x-proxy"), + HeaderValue::from_static(rama::utils::info::NAME), + ), + SetResponseHeaderLayer::overriding( + HeaderName::from_static("x-proxy-version"), + HeaderValue::from_static(rama::utils::info::VERSION), + ), + ArcLayer::new(), + )); + let maybe_http_relay = + HttpPeekRouter::new(http_mitm_relay).with_fallback(IoForwardService::new()); + + let tls_mitm_relay = TlsMitmRelay::try_new_with_cached_self_signed_issuer(&SelfSignedData { + organisation_name: Some("HTTP MITM Relay Proxy Boring Example".to_owned()), + ..Default::default() + }) + .context("build TLS mitm relay")?; + + let app_mitm_layer = + PeekTlsClientHelloService::new(tls_mitm_relay.into_layer(maybe_http_relay.clone())) + .with_fallback(maybe_http_relay); + + Ok(Arc::new( + ( + ConsumeErrLayer::trace_as_debug(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec), + ) + .into_layer(app_mitm_layer), + )) +} diff --git a/examples/http_nd_json.rs b/examples/http_nd_json.rs index 0f42f7a08..27d65952b 100644 --- a/examples/http_nd_json.rs +++ b/examples/http_nd_json.rs @@ -87,7 +87,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::default_ipv4(62041), exec.clone()) + let listener = TcpListener::bind_address(SocketAddress::default_ipv4(62041), exec.clone()) .await .expect("tcp port to be bound"); let bind_address = listener.local_addr().expect("retrieve bind address"); diff --git a/examples/http_pooled_client.rs b/examples/http_pooled_client.rs index 670010f5b..984689642 100644 --- a/examples/http_pooled_client.rs +++ b/examples/http_pooled_client.rs @@ -107,7 +107,7 @@ async fn run_server(addr: &str, ready: Sender<()>) { HttpServer::default().service(WebService::default().with_get("/", "Hello, World!")); let serve = TcpListener::build(Executor::default()) - .bind(addr) + .bind_address(addr) .await .expect("bind TCP Listener") .serve(LimitLayer::new(FirstConnOnly::new()).into_layer(http_service)); diff --git a/examples/http_record_har.rs b/examples/http_record_har.rs index d4b48c93e..2cdb1d4e2 100644 --- a/examples/http_record_har.rs +++ b/examples/http_record_har.rs @@ -36,7 +36,7 @@ use rama::{ Layer, Service, error::{BoxError, ErrorContext}, - extensions::{ExtensionsMut, ExtensionsRef}, + extensions::ExtensionsRef, http::{ HeaderValue, Request, Response, StatusCode, client::EasyHttpWebClient, @@ -53,7 +53,7 @@ use rama::{ remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, required_header::AddRequiredRequestHeadersLayer, trace::TraceLayer, - upgrade::{UpgradeLayer, Upgraded}, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer, Upgraded}, }, matcher::{DomainMatcher, MethodMatcher}, server::HttpServer, @@ -61,8 +61,6 @@ use rama::{ }, layer::{AddInputExtensionLayer, ConsumeErrLayer, HijackLayer}, net::{ - http::RequestContext, - proxy::ProxyTarget, stream::layer::http::BodyLimitLayer, tls::{ ApplicationProtocol, SecureTransport, @@ -140,7 +138,7 @@ async fn main() -> Result<(), BoxError> { let exec = Executor::graceful(guard); let tcp_service = TcpListener::build(exec.clone()) - .bind("127.0.0.1:62040") + .bind_address("127.0.0.1:62040") .await .expect("bind tcp proxy to 127.0.0.1:62040"); @@ -179,7 +177,7 @@ async fn main() -> Result<(), BoxError> { UpgradeLayer::new( exec, MethodMatcher::CONNECT, - service_fn(http_connect_accept), + DefaultHttpProxyConnectReplyService::new(), service_fn(http_connect_proxy), ), ) @@ -206,25 +204,6 @@ async fn main() -> Result<(), BoxError> { Ok(()) } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> { // In the past we deleted the request context here, as such: // ``` diff --git a/examples/http_service_fs.rs b/examples/http_service_fs.rs index e1901c534..b88aebf9d 100644 --- a/examples/http_service_fs.rs +++ b/examples/http_service_fs.rs @@ -26,7 +26,7 @@ use rama::{ #[tokio::main] async fn main() { - let listener = TcpListener::bind("127.0.0.1:62009", Executor::default()) + let listener = TcpListener::bind_address("127.0.0.1:62009", Executor::default()) .await .expect("bind TCP Listener"); diff --git a/examples/http_service_hello.rs b/examples/http_service_hello.rs index db6b764f2..224ea18b3 100644 --- a/examples/http_service_hello.rs +++ b/examples/http_service_hello.rs @@ -125,7 +125,7 @@ async fn main() { let tcp_http_service = HttpServer::auto(exec.clone()).service(Arc::new(http_service)); - TcpListener::bind("127.0.0.1:62010", exec) + TcpListener::bind_address("127.0.0.1:62010", exec) .await .expect("bind TCP Listener") .serve( diff --git a/examples/http_service_include_dir.rs b/examples/http_service_include_dir.rs index a18c9d913..37e467104 100644 --- a/examples/http_service_include_dir.rs +++ b/examples/http_service_include_dir.rs @@ -33,7 +33,7 @@ const ASSETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/test-files"); #[tokio::main] async fn main() { - let listener = TcpListener::bind("127.0.0.1:62037", Executor::default()) + let listener = TcpListener::bind_address("127.0.0.1:62037", Executor::default()) .await .expect("bind TCP Listener"); diff --git a/examples/http_sse.rs b/examples/http_sse.rs index aaca373ca..4c46022f5 100644 --- a/examples/http_sse.rs +++ b/examples/http_sse.rs @@ -94,7 +94,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::default_ipv4(62027), exec.clone()) + let listener = TcpListener::bind_address(SocketAddress::default_ipv4(62027), exec.clone()) .await .expect("tcp port to be bound"); let bind_address = listener.local_addr().expect("retrieve bind address"); diff --git a/examples/http_sse_datastar_hello.rs b/examples/http_sse_datastar_hello.rs index f13c72b2e..45bdf5ee0 100644 --- a/examples/http_sse_datastar_hello.rs +++ b/examples/http_sse_datastar_hello.rs @@ -96,7 +96,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::default_ipv4(62031), exec.clone()) + let listener = TcpListener::bind_address(SocketAddress::default_ipv4(62031), exec.clone()) .await .expect("tcp port to be bound"); let bind_address = listener.local_addr().expect("retrieve bind address"); diff --git a/examples/http_sse_datastar_test_suite.rs b/examples/http_sse_datastar_test_suite.rs index eef608afb..c24207129 100644 --- a/examples/http_sse_datastar_test_suite.rs +++ b/examples/http_sse_datastar_test_suite.rs @@ -67,7 +67,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::default_ipv4(62036), exec.clone()) + let listener = TcpListener::bind_address(SocketAddress::default_ipv4(62036), exec.clone()) .await .expect("tcp port to be bound"); let bind_address = listener.local_addr().expect("retrieve bind address"); @@ -101,7 +101,7 @@ pub mod handlers { ElementPatchMode, ExecuteScript, PatchElements, execute_script::{ScriptAttribute, ScriptType}, }; - use rama_utils::str::NonEmptyStr; + use rama::utils::str::NonEmptyStr; use serde::Deserialize; use serde_json::{Map, Value}; diff --git a/examples/http_sse_json.rs b/examples/http_sse_json.rs index 9bceaad5e..b90ffbdb3 100644 --- a/examples/http_sse_json.rs +++ b/examples/http_sse_json.rs @@ -104,7 +104,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::default_ipv4(62028), exec.clone()) + let listener = TcpListener::bind_address(SocketAddress::default_ipv4(62028), exec.clone()) .await .expect("tcp port to be bound"); let bind_address = listener.local_addr().expect("retrieve bind address"); diff --git a/examples/http_telemetry.rs b/examples/http_telemetry.rs index fb2fb6056..c6e804be0 100644 --- a/examples/http_telemetry.rs +++ b/examples/http_telemetry.rs @@ -158,7 +158,7 @@ async fn main() { // service setup & go TcpListener::build(exec) - .bind("127.0.0.1:62012") + .bind_address("127.0.0.1:62012") .await .unwrap() .serve( diff --git a/examples/https_connect_proxy.rs b/examples/https_connect_proxy.rs index 5cbc3e63a..7458e96d6 100644 --- a/examples/https_connect_proxy.rs +++ b/examples/https_connect_proxy.rs @@ -25,27 +25,26 @@ use rama::{ Layer, Service, - extensions::{ExtensionsMut, ExtensionsRef}, graceful::Shutdown, http::{ Body, Request, Response, StatusCode, client::EasyHttpWebClient, - layer::{proxy_auth::ProxyAuthLayer, trace::TraceLayer, upgrade::UpgradeLayer}, + layer::{ + proxy_auth::ProxyAuthLayer, + trace::TraceLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, + }, matcher::MethodMatcher, server::HttpServer, - service::web::response::IntoResponse, }, layer::ConsumeErrLayer, net::{ - http::RequestContext, - proxy::ProxyTarget, - stream::layer::http::BodyLimitLayer, - tls::{SecureTransport, server::SelfSignedData}, + proxy::IoForwardService, stream::layer::http::BodyLimitLayer, tls::server::SelfSignedData, user::credentials::basic, }, rt::Executor, service::service_fn, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -115,7 +114,7 @@ async fn main() { shutdown.spawn_task_fn(async |guard| { let exec = Executor::graceful(guard); let tcp_service = TcpListener::build(exec.clone()) - .bind("127.0.0.1:62016") + .bind_address("127.0.0.1:62016") .await .expect("bind tcp proxy to 127.0.0.1:62016"); @@ -129,8 +128,12 @@ async fn main() { UpgradeLayer::new( exec.clone(), MethodMatcher::CONNECT, - service_fn(http_connect_accept), - ConsumeErrLayer::default().into_layer(Forwarder::ctx(exec)), + DefaultHttpProxyConnectReplyService::new(), + ( + ConsumeErrLayer::default(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec), + ) + .into_layer(IoForwardService::new()), ), ) .into_layer(service_fn(http_plain_proxy)), @@ -154,30 +157,6 @@ async fn main() { .expect("graceful shutdown"); } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - tracing::info!( - "proxy secure transport ingress: {:?}", - req.extensions().get::() - ); - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_plain_proxy(req: Request) -> Result { let client = EasyHttpWebClient::default(); let uri = req.uri().clone(); diff --git a/examples/https_web_service_with_hsts.rs b/examples/https_web_service_with_hsts.rs index 8edd583df..4778850d8 100644 --- a/examples/https_web_service_with_hsts.rs +++ b/examples/https_web_service_with_hsts.rs @@ -106,7 +106,7 @@ async fn main() { shutdown.spawn_task_fn(async |guard| { let exec = Executor::graceful(guard); let tcp_service = TcpListener::build(exec.clone()) - .bind("127.0.0.1:62043") + .bind_address("127.0.0.1:62043") .await .expect("bind tcp proxy to 127.0.0.1:62043"); @@ -125,7 +125,7 @@ async fn main() { shutdown.spawn_task_fn(async |guard| { let exec = Executor::graceful(guard); let tcp_service = TcpListener::build(exec.clone()) - .bind("127.0.0.1:62044") + .bind_address("127.0.0.1:62044") .await .expect("bind tcp proxy to 127.0.0.1:62044"); diff --git a/examples/mtls_tunnel_and_service.rs b/examples/mtls_tunnel_and_service.rs index 56a8b9d87..053aa8a28 100644 --- a/examples/mtls_tunnel_and_service.rs +++ b/examples/mtls_tunnel_and_service.rs @@ -38,12 +38,9 @@ use rama::{ }, }, layer::TraceErrLayer, - net::address::SocketAddress, + net::{address::SocketAddress, proxy::IoForwardService}, rt::Executor, - tcp::{ - client::service::{Forwarder, TcpConnector}, - server::TcpListener, - }, + tcp::{client::service::TcpConnector, proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -144,7 +141,7 @@ async fn main() { server.port = %SERVER_AUTHORITY.port, "start mtls (https) web service", ); - TcpListener::bind(SERVER_AUTHORITY.to_string(), executor) + TcpListener::bind_address(SERVER_AUTHORITY.to_string(), executor) .await .unwrap_or_else(|e| { panic!("bind TCP Listener ({SERVER_AUTHORITY}): mtls (https): web service: {e}") @@ -162,17 +159,21 @@ async fn main() { ); let exec = Executor::graceful(guard.clone()); - let forwarder = Forwarder::new(exec.clone(), SERVER_AUTHORITY).with_connector( - TlsConnectorLayer::tunnel(Some(SERVER_AUTHORITY.ip_addr.into())) - .with_connector_data(tls_client_data) - .into_layer(TcpConnector::new(exec.clone())), - ); + let forwarder = ( + TraceErrLayer::new(), + IoToProxyBridgeIoLayer::new(exec.clone(), SERVER_AUTHORITY).with_connector( + TlsConnectorLayer::tunnel(Some(SERVER_AUTHORITY.ip_addr.into())) + .with_connector_data(tls_client_data) + .into_layer(TcpConnector::new(exec.clone())), + ), + ) + .into_layer(IoForwardService::new()); // L4 Proxy Service - TcpListener::bind(TUNNEL_AUTHORITY, exec) + TcpListener::bind_address(TUNNEL_AUTHORITY, exec) .await .expect("bind TCP Listener: mTLS TCP Tunnel Proxys") - .serve(TraceErrLayer::new().into_layer(forwarder)) + .serve(forwarder) .await; }); diff --git a/examples/proxy_connectivity_check.rs b/examples/proxy_connectivity_check.rs index a05aa3825..e4feded7f 100644 --- a/examples/proxy_connectivity_check.rs +++ b/examples/proxy_connectivity_check.rs @@ -34,7 +34,7 @@ use rama::{ Layer, Service, - extensions::{ExtensionsMut, ExtensionsRef, InputExtensions}, + extensions::{ExtensionsRef, InputExtensions}, http::{ Body, Request, Response, StatusCode, client::EasyHttpWebClient, @@ -42,23 +42,16 @@ use rama::{ proxy_auth::ProxyAuthLayer, remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, trace::TraceLayer, - upgrade::UpgradeLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, }, matcher::{DomainMatcher, MethodMatcher}, server::HttpServer, - service::web::{ - StaticService, - response::{Html, IntoResponse}, - }, + service::web::{StaticService, response::Html}, }, layer::{ConsumeErrLayer, HijackLayer}, net::{ - address::SocketAddress, - http::{RequestContext, server::HttpPeekRouter}, - proxy::ProxyTarget, - stream::ClientSocketInfo, - tls::SecureTransport, - user::credentials::basic, + address::SocketAddress, http::server::HttpPeekRouter, proxy::IoForwardService, + stream::ClientSocketInfo, user::credentials::basic, }, proxy::socks5::{ Socks5Acceptor, @@ -66,7 +59,7 @@ use rama::{ }, rt::Executor, service::service_fn, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -144,7 +137,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(62030), exec.clone()) + let tcp_service = TcpListener::bind_address(SocketAddress::default_ipv4(62030), exec.clone()) .await .expect("bind tcp interface for connectivity example"); @@ -166,15 +159,22 @@ async fn main() { UpgradeLayer::new( exec.clone(), MethodMatcher::CONNECT, - service_fn(http_connect_accept), - ConsumeErrLayer::default().into_layer(Forwarder::ctx(exec.clone())), + DefaultHttpProxyConnectReplyService::new(), + ( + ConsumeErrLayer::default(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec.clone()), + ) + .into_layer(IoForwardService::new()), ), ) .into_layer(proxy_service.clone()), ); let socks5_svc = HttpPeekRouter::new(HttpServer::auto(exec.clone()).service(proxy_service)) - .with_fallback(Forwarder::ctx(exec.clone())); + .with_fallback( + IoToProxyBridgeIoLayer::extension_proxy_target(exec.clone()) + .into_layer(IoForwardService::new()), + ); let socks5_acceptor = Socks5Acceptor::new(exec.clone()) .with_authorizer(basic!("john", "secret").into_authorizer()) .with_connector(LazyConnector::new(socks5_svc)); @@ -189,30 +189,6 @@ async fn main() { .expect("graceful shutdown"); } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - tracing::info!( - "proxy secure transport ingress: {:?}", - req.extensions().get::() - ); - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_plain_proxy(req: Request) -> Result { let client = EasyHttpWebClient::default(); match client.serve(req).await { diff --git a/examples/socks5_and_http_proxy.rs b/examples/socks5_and_http_proxy.rs index 21b903de0..7f0c7d01c 100644 --- a/examples/socks5_and_http_proxy.rs +++ b/examples/socks5_and_http_proxy.rs @@ -23,7 +23,7 @@ use rama::{ Layer, Service, - extensions::{ExtensionsMut, ExtensionsRef, InputExtensions}, + extensions::{ExtensionsRef, InputExtensions}, http::{ Body, Request, Response, StatusCode, client::EasyHttpWebClient, @@ -31,21 +31,17 @@ use rama::{ proxy_auth::ProxyAuthLayer, remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, trace::TraceLayer, - upgrade::UpgradeLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, }, matcher::MethodMatcher, server::HttpServer, - service::web::response::IntoResponse, }, layer::ConsumeErrLayer, - net::{ - http::RequestContext, proxy::ProxyTarget, stream::ClientSocketInfo, - user::credentials::basic, - }, + net::{proxy::IoForwardService, stream::ClientSocketInfo, user::credentials::basic}, proxy::socks5::{Socks5Acceptor, server::Socks5PeekRouter}, rt::Executor, service::service_fn, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -69,7 +65,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let tcp_service = TcpListener::bind("127.0.0.1:62023", exec.clone()) + let tcp_service = TcpListener::bind_address("127.0.0.1:62023", exec.clone()) .await .expect("bind socks5+http proxy to 127.0.0.1:62023"); @@ -84,8 +80,12 @@ async fn main() { UpgradeLayer::new( exec.clone(), MethodMatcher::CONNECT, - service_fn(http_connect_accept), - ConsumeErrLayer::default().into_layer(Forwarder::ctx(exec)), + DefaultHttpProxyConnectReplyService::new(), + ( + ConsumeErrLayer::default(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec), + ) + .into_layer(IoForwardService::new()), ), RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop(), @@ -103,25 +103,6 @@ async fn main() { .expect("graceful shutdown"); } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_plain_proxy(req: Request) -> Result { let client = EasyHttpWebClient::default(); match client.serve(req).await { diff --git a/examples/socks5_bind_proxy.rs b/examples/socks5_bind_proxy.rs index 99c250f6d..8a694598f 100644 --- a/examples/socks5_bind_proxy.rs +++ b/examples/socks5_bind_proxy.rs @@ -108,9 +108,10 @@ async fn main() { } async fn spawn_socks5_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::local_ipv4(63010), Executor::default()) - .await - .expect("bind socks5 BIND proxy on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::local_ipv4(63010), Executor::default()) + .await + .expect("bind socks5 BIND proxy on open port"); let bind_addr = tcp_service .local_addr() @@ -119,7 +120,7 @@ async fn spawn_socks5_server() -> SocketAddress { let socks5_acceptor = Socks5Acceptor::new(Executor::default()) .with_authorizer(basic!("john", "secret").into_authorizer()) - .with_binder(DefaultBinder::default().with_bind_interface(SocketAddress::local_ipv4(0))); + .with_binder(DefaultBinder::default().with_bind_address(SocketAddress::local_ipv4(0))); tokio::spawn(tcp_service.serve(socks5_acceptor)); diff --git a/examples/socks5_connect_proxy.rs b/examples/socks5_connect_proxy.rs index db5d2575e..065078f87 100644 --- a/examples/socks5_connect_proxy.rs +++ b/examples/socks5_connect_proxy.rs @@ -58,7 +58,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let tcp_service = TcpListener::bind("127.0.0.1:62021", exec) + let tcp_service = TcpListener::bind_address("127.0.0.1:62021", exec) .await .expect("bind proxy to 127.0.0.1:62021"); let socks5_acceptor = diff --git a/examples/socks5_connect_proxy_mitm_proxy.rs b/examples/socks5_connect_proxy_mitm_proxy.rs index e7206795c..c47dfa02c 100644 --- a/examples/socks5_connect_proxy_mitm_proxy.rs +++ b/examples/socks5_connect_proxy_mitm_proxy.rs @@ -89,7 +89,7 @@ async fn main() { let auto_https_service = TlsPeekRouter::new(https_service).with_fallback(http_service); - let tcp_service = TcpListener::bind("127.0.0.1:62022", exec.clone()) + let tcp_service = TcpListener::bind_address("127.0.0.1:62022", exec.clone()) .await .expect("bind proxy to 127.0.0.1:62022"); let socks5_acceptor = Socks5Acceptor::new(exec) diff --git a/examples/socks5_connect_proxy_over_tls.rs b/examples/socks5_connect_proxy_over_tls.rs index 269aad510..eb091f9a2 100644 --- a/examples/socks5_connect_proxy_over_tls.rs +++ b/examples/socks5_connect_proxy_over_tls.rs @@ -130,9 +130,10 @@ async fn main() { } async fn spawn_socks5_over_tls_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63011), Executor::default()) - .await - .expect("bind socks5-over-tls CONNECT proxy on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63011), Executor::default()) + .await + .expect("bind socks5-over-tls CONNECT proxy on open port"); let bind_addr = tcp_service .local_addr() @@ -154,9 +155,10 @@ async fn spawn_socks5_over_tls_server() -> SocketAddress { } async fn spawn_http_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63012), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63012), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() diff --git a/examples/socks5_udp_associate.rs b/examples/socks5_udp_associate.rs index 34572185b..5358415ac 100644 --- a/examples/socks5_udp_associate.rs +++ b/examples/socks5_udp_associate.rs @@ -29,7 +29,7 @@ use rama::{ level_filters::LevelFilter, subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}, }, - udp::bind_udp, + udp::bind_udp_with_address, }; use std::convert::Infallible; @@ -60,7 +60,7 @@ async fn main() { .await .expect("initiate socks5 UDP Associate handshake"); - let udp_server = bind_udp(SocketAddress::local_ipv4(0)) + let udp_server = bind_udp_with_address(SocketAddress::local_ipv4(0)) .await .expect("bind udp server"); @@ -99,7 +99,7 @@ async fn main() { }); let mut udp_socket_relay = udp_binder - .bind(SocketAddress::local_ipv4(0)) + .bind_address(SocketAddress::local_ipv4(0)) .await .expect("server to be connected"); @@ -135,7 +135,7 @@ async fn main() { } async fn spawn_socks5_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::local_ipv4(0), Executor::default()) + let tcp_service = TcpListener::bind_address(SocketAddress::local_ipv4(0), Executor::default()) .await .expect("bind socks5 UDP Associate proxy on open port"); @@ -148,7 +148,7 @@ async fn spawn_socks5_server() -> SocketAddress { .with_authorizer(basic!("john", "secret").into_authorizer()) .with_udp_associator( DefaultUdpRelay::default() - .with_bind_interface(SocketAddress::local_ipv4(0)) + .with_bind_address(SocketAddress::local_ipv4(0)) .with_sync_inspector(udp_packet_inspect), ); diff --git a/examples/socks5_udp_associate_framed.rs b/examples/socks5_udp_associate_framed.rs index 62258895a..a7fbfd7e0 100644 --- a/examples/socks5_udp_associate_framed.rs +++ b/examples/socks5_udp_associate_framed.rs @@ -32,7 +32,7 @@ use rama::{ level_filters::LevelFilter, subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt}, }, - udp::{UdpFramed, bind_udp}, + udp::{UdpFramed, bind_udp_with_address}, }; use std::convert::Infallible; @@ -63,7 +63,7 @@ async fn main() { .await .expect("initiate socks5 UDP Associate handshake"); - let udp_server = bind_udp(SocketAddress::local_ipv4(0)) + let udp_server = bind_udp_with_address(SocketAddress::local_ipv4(0)) .await .expect("bind udp server"); @@ -103,7 +103,7 @@ async fn main() { }); let udp_socket_relay = udp_binder - .bind(SocketAddress::local_ipv4(0)) + .bind_address(SocketAddress::local_ipv4(0)) .await .expect("server to be connected"); @@ -141,7 +141,7 @@ async fn main() { } async fn spawn_socks5_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::local_ipv4(0), Executor::default()) + let tcp_service = TcpListener::bind_address(SocketAddress::local_ipv4(0), Executor::default()) .await .expect("bind socks5 UDP Associate proxy on open port"); @@ -154,7 +154,7 @@ async fn spawn_socks5_server() -> SocketAddress { .with_authorizer(basic!("john", "secret").into_authorizer()) .with_udp_associator( DefaultUdpRelay::default() - .with_bind_interface(SocketAddress::local_ipv4(0)) + .with_bind_address(SocketAddress::local_ipv4(0)) .with_sync_inspector(udp_packet_inspect), ); diff --git a/examples/tcp_listener_fd_passing.rs b/examples/tcp_listener_fd_passing.rs index 7782f02e7..53bd523c5 100644 --- a/examples/tcp_listener_fd_passing.rs +++ b/examples/tcp_listener_fd_passing.rs @@ -69,7 +69,7 @@ mod unix_example { println!("Creating TCP listener..."); // Create listener - let listener = TcpListener::bind("127.0.0.1:62046", Executor::default()).await?; + let listener = TcpListener::bind_address("127.0.0.1:62046", Executor::default()).await?; let addr = listener.local_addr()?; println!("✓ Listening on {addr}"); diff --git a/examples/tcp_listener_hello.rs b/examples/tcp_listener_hello.rs index d8f0b3ca1..9fbde5662 100644 --- a/examples/tcp_listener_hello.rs +++ b/examples/tcp_listener_hello.rs @@ -17,10 +17,10 @@ //! You should see a response with `HTTP/1.1 200 OK` and a body with the source code of this example. use rama::{ + io::Io, net::{address::SocketAddress, stream::Socket}, rt::Executor, service::service_fn, - stream::Stream, tcp::server::TcpListener, }; @@ -35,14 +35,14 @@ const ADDR: SocketAddress = SocketAddress::local_ipv4(62500); #[tokio::main] async fn main() { println!("Listening on: {ADDR}"); - TcpListener::bind(ADDR, Executor::default()) + TcpListener::bind_address(ADDR, Executor::default()) .await .expect("bind TCP Listener") .serve(service_fn(handle)) .await; } -async fn handle(mut stream: impl Socket + Stream + Unpin) -> Result<(), Infallible> { +async fn handle(mut stream: impl Socket + Io + Unpin) -> Result<(), Infallible> { println!( "Incoming connection from: {}", stream diff --git a/examples/tcp_listener_layers.rs b/examples/tcp_listener_layers.rs index 51b917699..6162572b5 100644 --- a/examples/tcp_listener_layers.rs +++ b/examples/tcp_listener_layers.rs @@ -48,7 +48,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind("0.0.0.0:62501", exec) + let listener = TcpListener::bind_address("0.0.0.0:62501", exec) .await .expect("bind TCP Listener"); diff --git a/examples/tcp_nd_json.rs b/examples/tcp_nd_json.rs index 72c44a85d..1097cd8c9 100644 --- a/examples/tcp_nd_json.rs +++ b/examples/tcp_nd_json.rs @@ -87,7 +87,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); let exec = Executor::graceful(graceful.guard()); - let listener = TcpListener::bind(SocketAddress::default_ipv4(62042), exec.clone()) + let listener = TcpListener::bind_address(SocketAddress::default_ipv4(62042), exec.clone()) .await .expect("tcp port to be bound"); let bind_address = listener.local_addr().expect("retrieve bind address"); diff --git a/examples/tls_boring_dynamic_certs.rs b/examples/tls_boring_dynamic_certs.rs index db44cfca9..42193dc52 100644 --- a/examples/tls_boring_dynamic_certs.rs +++ b/examples/tls_boring_dynamic_certs.rs @@ -117,7 +117,7 @@ async fn main() { ) .into_layer(http_service); - TcpListener::bind("127.0.0.1:64801", exec) + TcpListener::bind_address("127.0.0.1:64801", exec) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/tls_boring_termination.rs b/examples/tls_boring_termination.rs index 558a8c8b1..64bf4ed91 100644 --- a/examples/tls_boring_termination.rs +++ b/examples/tls_boring_termination.rs @@ -39,6 +39,7 @@ use rama::{ net::{ address::HostWithPort, forwarded::Forwarded, + proxy::IoForwardService, stream::SocketInfo, tls::{ SecureTransport, @@ -50,10 +51,7 @@ use rama::{ }, rt::Executor, service::service_fn, - tcp::{ - client::service::{Forwarder, TcpConnector}, - server::TcpListener, - }, + tcp::{client::service::TcpConnector, proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -91,20 +89,19 @@ async fn main() { let client_hello = st.client_hello().unwrap(); tracing::debug!("secure connection established: client hello = {client_hello:?}"); }), + IoToProxyBridgeIoLayer::new( + Executor::graceful(guard.clone()), + HostWithPort::local_ipv4(62801), + ) + .with_connector( + // ha proxy protocol used to forwarded the client original IP + HaProxyClientLayer::tcp() + .into_layer(TcpConnector::new(Executor::graceful(guard.clone()))), + ), ) - .into_layer( - Forwarder::new( - Executor::graceful(guard.clone()), - HostWithPort::local_ipv4(62801), - ) - .with_connector( - // ha proxy protocol used to forwarded the client original IP - HaProxyClientLayer::tcp() - .into_layer(TcpConnector::new(Executor::graceful(guard.clone()))), - ), - ); - - TcpListener::bind("127.0.0.1:63801", Executor::graceful(guard.clone())) + .into_layer(IoForwardService::new()); + + TcpListener::bind_address("127.0.0.1:63801", Executor::graceful(guard.clone())) .await .expect("bind TCP Listener: tls") .serve(tcp_service) @@ -119,7 +116,7 @@ async fn main() { let tcp_service = (ConsumeErrLayer::default(), HaProxyServerLayer::new()).into_layer(http_service); - TcpListener::bind("127.0.0.1:62801", exec) + TcpListener::bind_address("127.0.0.1:62801", exec) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/tls_rustls_dynamic_certs.rs b/examples/tls_rustls_dynamic_certs.rs index 1a2c53bac..ba909f81b 100644 --- a/examples/tls_rustls_dynamic_certs.rs +++ b/examples/tls_rustls_dynamic_certs.rs @@ -123,7 +123,7 @@ async fn main() { ) .into_layer(http_service); - TcpListener::bind("127.0.0.1:64802", exec) + TcpListener::bind_address("127.0.0.1:64802", exec) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/tls_rustls_dynamic_config.rs b/examples/tls_rustls_dynamic_config.rs index 9678d85b3..eeddbddf5 100644 --- a/examples/tls_rustls_dynamic_config.rs +++ b/examples/tls_rustls_dynamic_config.rs @@ -99,7 +99,7 @@ async fn main() { ) .into_layer(http_service); - TcpListener::bind("127.0.0.1:64804", exec) + TcpListener::bind_address("127.0.0.1:64804", exec) .await .expect("bind TCP Listener: http") .serve(tcp_service) diff --git a/examples/tls_rustls_termination.rs b/examples/tls_rustls_termination.rs index 0357d0942..409afa109 100644 --- a/examples/tls_rustls_termination.rs +++ b/examples/tls_rustls_termination.rs @@ -43,9 +43,10 @@ use rama::{ Layer, extensions::ExtensionsRef, graceful::Shutdown, + io::Io, layer::ConsumeErrLayer, net::{ - address::HostWithPort, forwarded::Forwarded, stream::SocketInfo, + address::HostWithPort, forwarded::Forwarded, proxy::IoForwardService, stream::SocketInfo, tls::server::SelfSignedData, }, proxy::haproxy::{ @@ -53,11 +54,7 @@ use rama::{ }, rt::Executor, service::service_fn, - stream::Stream, - tcp::{ - client::service::{Forwarder, TcpConnector}, - server::TcpListener, - }, + tcp::{client::service::TcpConnector, proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -92,8 +89,9 @@ async fn main() { // create tls proxy shutdown.spawn_task_fn(async move |guard| { - let tcp_service = TlsAcceptorLayer::new(acceptor_data).into_layer( - Forwarder::new( + let tcp_service = ( + TlsAcceptorLayer::new(acceptor_data), + IoToProxyBridgeIoLayer::new( Executor::graceful(guard.clone()), HostWithPort::local_ipv4(62800), ) @@ -102,9 +100,10 @@ async fn main() { HaProxyClientLayer::tcp() .into_layer(TcpConnector::new(Executor::graceful(guard.clone()))), ), - ); + ) + .into_layer(IoForwardService::new()); - TcpListener::bind("127.0.0.1:63800", Executor::graceful(guard.clone())) + TcpListener::bind_address("127.0.0.1:63800", Executor::graceful(guard.clone())) .await .expect("bind TCP Listener: tls") .serve(tcp_service) @@ -116,7 +115,7 @@ async fn main() { let tcp_service = (ConsumeErrLayer::default(), HaProxyServerLayer::new()) .into_layer(service_fn(internal_tcp_service_fn)); - TcpListener::bind("127.0.0.1:62800", Executor::graceful(guard.clone())) + TcpListener::bind_address("127.0.0.1:62800", Executor::graceful(guard.clone())) .await .expect("bind TCP Listener: http") .serve(tcp_service) @@ -131,7 +130,7 @@ async fn main() { async fn internal_tcp_service_fn(mut stream: S) -> Result<(), Infallible> where - S: Stream + Unpin + ExtensionsRef, + S: Io + Unpin + ExtensionsRef, { // REMARK: builds on the assumption that we are using the haproxy protocol let client_addr = stream diff --git a/examples/tls_sni_proxy_mitm.rs b/examples/tls_sni_proxy_mitm.rs index 2004edb63..aecbbfe64 100644 --- a/examples/tls_sni_proxy_mitm.rs +++ b/examples/tls_sni_proxy_mitm.rs @@ -100,24 +100,25 @@ use rama::{ server::HttpServer, service::web::response::IntoResponse, }, + io::Io, layer::{AddInputExtensionLayer, ConsumeErrLayer}, net::{ Protocol, address::{Domain, HostWithPort, SocketAddress}, client::{ConnectorTarget, pool::http::HttpPooledConnectorConfig}, http::RequestContext, + proxy::IoForwardService, tls::{ ApplicationProtocol, client::ServerVerifyMode, server::{ - ServerAuth, ServerCertIssuerData, ServerConfig, SniPeekStream, SniRequest, + ServerAuth, ServerCertIssuerData, ServerConfig, SniPrefixedIo, SniRequest, SniRouter, }, }, }, rt::Executor, - stream::Stream, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -167,7 +168,7 @@ async fn main() -> Result<(), BoxError> { const INTERFACE: SocketAddress = SocketAddress::local_ipv4(62045); tracing::info!("bind SNI MITM proxy to {INTERFACE}"); - let tcp_listener = TcpListener::bind(INTERFACE, exec.clone()) + let tcp_listener = TcpListener::bind_address(INTERFACE, exec.clone()) .await .context("bind tcp proxy") .context_field("interface", INTERFACE)?; @@ -253,8 +254,8 @@ struct SniRouterService { impl Service> for SniRouterService where - S: Stream + Unpin + ExtensionsMut, - T: Service, Output = (), Error: Into>, + S: Io + Unpin + ExtensionsMut, + T: Service, Output = (), Error: Into>, { type Output = (); type Error = BoxError; @@ -289,13 +290,14 @@ where .context_field("sni", sni)?; } else { // preserve traffic as is, no MITM even - Forwarder::new( + IoToProxyBridgeIoLayer::new( self.exec.clone(), HostWithPort { host: sni.clone().into(), port: Protocol::HTTPS_DEFAULT_PORT, }, ) + .into_layer(IoForwardService::new()) .serve(stream) .await .context("forward data") diff --git a/examples/tls_sni_router.rs b/examples/tls_sni_router.rs index 4de5640fa..cd917e2d1 100644 --- a/examples/tls_sni_router.rs +++ b/examples/tls_sni_router.rs @@ -48,13 +48,14 @@ use rama::{ extensions::ExtensionsMut, graceful::{Shutdown, ShutdownGuard}, http::{layer::trace::TraceLayer, server::HttpServer, service::web::Router}, + io::Io, net::{ address::{Domain, SocketAddress}, + proxy::IoForwardService, tls::server::{SelfSignedData, ServerAuth, ServerConfig, SniRequest, SniRouter}, }, rt::Executor, - stream::Stream, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing::{ self, Instrument as _, level_filters::LevelFilter, @@ -85,13 +86,13 @@ async fn main() { spawn_https_server(shutdown.guard(), NAME_BAZ, INTERFACE_BAZ); shutdown.spawn_task_fn(async move |guard| { - let interface = SocketAddress::default_ipv4(62026); + let socket_address = SocketAddress::default_ipv4(62026); tracing::info!( - network.local.address = %interface.ip_addr, - network.local.port = %interface.port, + network.local.address = %socket_address.ip_addr, + network.local.port = %socket_address.port, "[tcp] spawn sni router: bind and go", ); - TcpListener::bind(interface, Executor::graceful(guard.clone())) + TcpListener::bind_address(socket_address, Executor::graceful(guard.clone())) .await .expect("bind TCP Listener for SNI router") .serve(SniRouter::new(SniRouterSvc { @@ -124,7 +125,7 @@ struct SniRouterSvc { impl Service> for SniRouterSvc where - S: Stream + ExtensionsMut + Unpin, + S: Io + ExtensionsMut + Unpin, { type Output = (); type Error = BoxError; @@ -159,13 +160,14 @@ where "forward incoming connection", ); - Forwarder::new(self.exec.clone(), fwd_interface) + IoToProxyBridgeIoLayer::new(self.exec.clone(), fwd_interface) + .into_layer(IoForwardService::new()) .serve(stream) .await } } -fn spawn_https_server(guard: ShutdownGuard, name: &'static str, interface: SocketAddress) { +fn spawn_https_server(guard: ShutdownGuard, name: &'static str, socket_address: SocketAddress) { let tls_server_config = ServerConfig::new(ServerAuth::SelfSigned(SelfSignedData { common_name: Some(format!("{name}.local").parse().expect("encode common name")), ..Default::default() @@ -175,11 +177,11 @@ fn spawn_https_server(guard: ShutdownGuard, name: &'static str, interface: Socke guard.into_spawn_task_fn(async move |guard| { tracing::info!( host.name = %name, - network.local.address = %interface.ip_addr, - network.local.port = %interface.port, + network.local.address = %socket_address.ip_addr, + network.local.port = %socket_address.port, "[tcp] spawn https server: bind and go", ); - TcpListener::bind(interface, Executor::graceful(guard.clone())) + TcpListener::bind_address(socket_address, Executor::graceful(guard.clone())) .await .expect("bind TCP Listener for web server") .serve(TlsAcceptorLayer::new(acceptor_data).into_layer( diff --git a/examples/udp_codec.rs b/examples/udp_codec.rs index 2ff6b18bc..7b00c5c95 100644 --- a/examples/udp_codec.rs +++ b/examples/udp_codec.rs @@ -31,7 +31,7 @@ use rama::{ futures::{FutureExt, SinkExt, StreamExt}, net::address::SocketAddress, stream::codec::BytesCodec, - udp::{UdpFramed, bind_udp}, + udp::{UdpFramed, bind_udp_with_address}, }; // everything else is provided by the standard library, community crates or tokio @@ -43,11 +43,11 @@ use tokio::{io, time}; #[tokio::main] async fn main() -> Result<(), BoxError> { let mut a = UdpFramed::new( - bind_udp(SocketAddress::local_ipv4(0)).await?, + bind_udp_with_address(SocketAddress::local_ipv4(0)).await?, BytesCodec::new(), ); let mut b = UdpFramed::new( - bind_udp(SocketAddress::local_ipv4(0)).await?, + bind_udp_with_address(SocketAddress::local_ipv4(0)).await?, BytesCodec::new(), ); diff --git a/examples/unix_socket.rs b/examples/unix_socket.rs index 102d487d9..9978eca97 100644 --- a/examples/unix_socket.rs +++ b/examples/unix_socket.rs @@ -27,10 +27,10 @@ mod unix_example { error::BoxError, extensions::ExtensionsRef, graceful::ShutdownGuard, + io::Io, layer::AddInputExtensionLayer, rt::Executor, service::service_fn, - stream::Stream, telemetry::tracing::{ self, level_filters::LevelFilter, @@ -62,9 +62,7 @@ mod unix_example { .expect("bind Unix socket"); graceful.spawn_task_fn(async |guard| { - async fn handle( - mut stream: impl Stream + Unpin + ExtensionsRef, - ) -> Result<(), BoxError> { + async fn handle(mut stream: impl Io + Unpin + ExtensionsRef) -> Result<(), BoxError> { let mut buf = [0u8; 1024]; let mut cancelled = Box::pin( diff --git a/examples/unix_socket_http.rs b/examples/unix_socket_http.rs index 504a1ab3f..27128146e 100644 --- a/examples/unix_socket_http.rs +++ b/examples/unix_socket_http.rs @@ -61,7 +61,7 @@ mod unix_example { ); listener .serve( - HttpServer::http1(exec) + HttpServer::new_http1(exec) .service(Arc::new(Router::new().with_get("/ping", "pong"))), ) .await; diff --git a/examples/ws_chat_server.rs b/examples/ws_chat_server.rs index c4e1b2925..5d9e005ee 100644 --- a/examples/ws_chat_server.rs +++ b/examples/ws_chat_server.rs @@ -56,7 +56,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); graceful.spawn_task_fn(async |guard| { - let server = HttpServer::http1(Executor::graceful(guard.clone())).service(Arc::new(Router::new().with_get("/", Html(INDEX)).with_get( + let server = HttpServer::new_http1(Executor::graceful(guard.clone())).service(Arc::new(Router::new().with_get("/", Html(INDEX)).with_get( "/chat", WebSocketAcceptor::new().into_service(service_fn( async |mut ws: ServerWebSocket| { @@ -122,7 +122,7 @@ async fn main() { info!("or connect directly to ws://127.0.0.1:62033/chat (via 'rama')"); - TcpListener::bind("127.0.0.1:62033", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:62033", Executor::graceful(guard)) .await .expect("bind TCP Listener") .serve(AddInputExtensionLayer::new(State::default()).into_layer(server)) diff --git a/examples/ws_echo_server.rs b/examples/ws_echo_server.rs index 290fb1d8b..409097a3b 100644 --- a/examples/ws_echo_server.rs +++ b/examples/ws_echo_server.rs @@ -44,7 +44,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); graceful.spawn_task_fn(async |guard| { - let server = HttpServer::http1(Executor::graceful(guard.clone())).service(Arc::new( + let server = HttpServer::new_http1(Executor::graceful(guard.clone())).service(Arc::new( Router::new().with_get("/", Html(INDEX)).with_get( "/echo", ConsumeErrLayer::trace_as_debug() @@ -53,7 +53,7 @@ async fn main() { )); info!("open web echo chat @ http://127.0.0.1:62032"); info!("or connect directly to ws://127.0.0.1:62032/echo (via 'rama')"); - TcpListener::bind("127.0.0.1:62032", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:62032", Executor::graceful(guard)) .await .expect("bind TCP Listener") .serve(server) diff --git a/examples/ws_echo_server_with_compression.rs b/examples/ws_echo_server_with_compression.rs index 779bed689..3b57ac4bc 100644 --- a/examples/ws_echo_server_with_compression.rs +++ b/examples/ws_echo_server_with_compression.rs @@ -44,7 +44,7 @@ async fn main() { let graceful = rama::graceful::Shutdown::default(); graceful.spawn_task_fn(async |guard| { - let server = HttpServer::http1(Executor::graceful(guard.clone())).service(Arc::new( + let server = HttpServer::new_http1(Executor::graceful(guard.clone())).service(Arc::new( Router::new().with_get("/", Html(INDEX)).with_get( "/echo", ConsumeErrLayer::trace_as_debug().into_layer( @@ -56,7 +56,7 @@ async fn main() { )); info!("open web echo chat @ http://127.0.0.1:62038"); info!("or connect directly to ws://127.0.0.1:62038/echo (via 'rama')"); - TcpListener::bind("127.0.0.1:62038", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:62038", Executor::graceful(guard)) .await .expect("bind TCP Listener") .serve(server) diff --git a/examples/ws_over_h2.rs b/examples/ws_over_h2.rs index 044fbc998..fc0c0bc14 100644 --- a/examples/ws_over_h2.rs +++ b/examples/ws_over_h2.rs @@ -63,7 +63,7 @@ async fn main() { let acceptor_data = TlsAcceptorData::try_from(tls_server_config).expect("create acceptor data"); graceful.spawn_task_fn(async |guard| { - let mut h2 = HttpServer::h2(Executor::graceful(guard.clone())); + let mut h2 = HttpServer::new_h2(Executor::graceful(guard.clone())); h2.h2_mut().set_enable_connect_protocol(); // required for WS sockets let server = h2.service(Arc::new( Router::new().with_get("/", Html(INDEX)).with_connect( @@ -77,7 +77,7 @@ async fn main() { info!("open web echo chat @ https://127.0.0.1:62035"); info!("or connect directly to wss://127.0.0.1:62035/echo (via 'rama')"); - TcpListener::bind("127.0.0.1:62035", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:62035", Executor::graceful(guard)) .await .expect("bind TCP Listener") .serve(tls_server) diff --git a/examples/ws_tls_server.rs b/examples/ws_tls_server.rs index c5da355cc..28b7e5dad 100644 --- a/examples/ws_tls_server.rs +++ b/examples/ws_tls_server.rs @@ -57,7 +57,7 @@ async fn main() { let acceptor_data = TlsAcceptorData::try_from(tls_server_config).expect("create acceptor data"); graceful.spawn_task_fn(async |guard| { - let server = HttpServer::http1(Executor::graceful(guard.clone())).service(Arc::new( + let server = HttpServer::new_http1(Executor::graceful(guard.clone())).service(Arc::new( Router::new().with_get("/", Html(INDEX)).with_get( "/echo", ConsumeErrLayer::trace_as_debug() @@ -69,7 +69,7 @@ async fn main() { info!("open web echo chat @ https://127.0.0.1:62034"); info!("or connect directly to wss://127.0.0.1:62034/echo (via 'rama')"); - TcpListener::bind("127.0.0.1:62034", Executor::graceful(guard)) + TcpListener::bind_address("127.0.0.1:62034", Executor::graceful(guard)) .await .expect("bind TCP Listener") .serve(tls_server) diff --git a/ffi/apple/.gitignore b/ffi/apple/.gitignore new file mode 100644 index 000000000..69181b2c9 --- /dev/null +++ b/ffi/apple/.gitignore @@ -0,0 +1,16 @@ + +build/ +DerivedData/ +*.xcarchive +*.xcresult +*.ipa +*.xcworkspace +!*.xcworkspace/contents.xcworkspacedata +*.xcuserdata/ +*.xcuserstate +*.xcodeproj/xcuserdata/ +.build/ +.swiftpm/ +Package.resolved +*.xcodeproj +target/ diff --git a/ffi/apple/RamaAppleNetworkExtension/Package.swift b/ffi/apple/RamaAppleNetworkExtension/Package.swift new file mode 100644 index 000000000..ee4ff761b --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "RamaAppleNetworkExtension", + platforms: [ + .macOS(.v12) + ], + products: [ + .library( + name: "RamaAppleNetworkExtension", + targets: ["RamaAppleNetworkExtension"] + ) + ], + targets: [ + .target( + name: "RamaAppleNEFFI", + path: "Sources/RamaAppleNEFFI" + ), + .target( + name: "RamaAppleNetworkExtension", + dependencies: ["RamaAppleNEFFI"], + path: "Sources/RamaAppleNetworkExtension" + ), + ] +) diff --git a/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/RamaAppleNEFFI.h b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/RamaAppleNEFFI.h new file mode 100644 index 000000000..79377d130 --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/RamaAppleNEFFI.h @@ -0,0 +1,2 @@ +#pragma once +#include "rama_apple_ne_ffi.h" diff --git a/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/module.modulemap b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/module.modulemap new file mode 100644 index 000000000..83428c028 --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/module.modulemap @@ -0,0 +1,4 @@ +module RamaAppleNEFFI { + header "rama_apple_ne_ffi.h" + export * +} diff --git a/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/rama_apple_ne_ffi.c b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/rama_apple_ne_ffi.c new file mode 100644 index 000000000..834fb9daf --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/rama_apple_ne_ffi.c @@ -0,0 +1,6 @@ +#include "rama_apple_ne_ffi.h" + +// This file intentionally has no implementation. +// The actual symbols are provided by the user linked Rust static library. +// This translation unit exists so tooling such as SwiftPM +// builds a C module for the header. diff --git a/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/rama_apple_ne_ffi.h b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/rama_apple_ne_ffi.h new file mode 100644 index 000000000..a8213a841 --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNEFFI/include/rama_apple_ne_ffi.h @@ -0,0 +1,327 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// Opaque transparent proxy engine handle managed by Rust. +typedef struct RamaTransparentProxyEngine RamaTransparentProxyEngine; +/// Opaque TCP flow/session handle managed by Rust. +typedef struct RamaTransparentProxyTcpSession RamaTransparentProxyTcpSession; +/// Opaque UDP flow/session handle managed by Rust. +typedef struct RamaTransparentProxyUdpSession RamaTransparentProxyUdpSession; + +/// Borrowed byte view. +/// +/// Ownership is retained by the caller. `ptr` may be NULL only if `len == 0`. +typedef struct { + /// Borrowed pointer to bytes. + const uint8_t* ptr; + /// Number of bytes at `ptr`. + size_t len; +} RamaBytesView; + +/// Owned byte buffer allocated by Rust. +/// +/// Must be released with `rama_owned_bytes_free`. +typedef struct { + /// Owned allocation pointer (or NULL when empty). + uint8_t* ptr; + /// Number of initialized bytes. + size_t len; + /// Allocation capacity. + size_t cap; +} RamaBytesOwned; + +/// Log level for `rama_log`. +typedef enum { + /// Extremely verbose diagnostic logs. + RAMA_LOG_LEVEL_TRACE = 0, + /// Debug logs. + RAMA_LOG_LEVEL_DEBUG = 1, + /// Informational logs. + RAMA_LOG_LEVEL_INFO = 2, + /// Warning logs. + RAMA_LOG_LEVEL_WARN = 3, + /// Error logs. + RAMA_LOG_LEVEL_ERROR = 4, +} RamaLogLevel; + +/// Transport protocol for one intercepted flow. +typedef enum { + /// TCP flow. + RAMA_FLOW_PROTOCOL_TCP = 1, + /// UDP flow. + RAMA_FLOW_PROTOCOL_UDP = 2, +} RamaTransparentProxyFlowProtocol; + +/// Protocol filter used by network interception rules. +typedef enum { + /// Match any protocol. + RAMA_RULE_PROTOCOL_ANY = 0, + /// Match TCP only. + RAMA_RULE_PROTOCOL_TCP = 1, + /// Match UDP only. + RAMA_RULE_PROTOCOL_UDP = 2, +} RamaTransparentProxyRuleProtocol; + +/// Endpoint metadata (`host:port`) for one flow side. +/// +/// If endpoint is not available, set `host_utf8 = NULL`, `host_utf8_len = 0`, +/// and `port = 0`. +/// +/// Apple references: +/// - https://developer.apple.com/documentation/networkextension/neappproxytcpflow/remoteendpoint +/// - https://developer.apple.com/documentation/networkextension/neappproxyudpflow +typedef struct { + /// UTF-8 hostname/IP bytes (not NUL-terminated). May be NULL. + const char* host_utf8; + /// Length of `host_utf8` bytes. + size_t host_utf8_len; + /// TCP/UDP port. + uint16_t port; +} RamaTransparentProxyFlowEndpoint; + +/// Per-flow metadata passed from Swift to Rust. +/// +/// String fields are not C strings. They are UTF-8 byte slices +/// (`pointer + length`) and are not required to be NUL-terminated. +/// Optional string fields are absent when encoded as (`NULL`, `0`). +/// +/// Apple references: +/// - https://developer.apple.com/documentation/networkextension/neappproxyflow/metadata +/// - https://developer.apple.com/documentation/networkextension/neflowmetadata/sourceappsigningidentifier +typedef struct { + /// One of `RamaTransparentProxyFlowProtocol`. + uint32_t protocol; + /// Intended remote endpoint of this flow. + RamaTransparentProxyFlowEndpoint remote_endpoint; + /// Local endpoint assigned to this flow (if known). + RamaTransparentProxyFlowEndpoint local_endpoint; + /// Source app signing identifier UTF-8 bytes (not NUL-terminated). May be NULL. + const char* source_app_signing_identifier_utf8; + /// Length of `source_app_signing_identifier_utf8`. + size_t source_app_signing_identifier_utf8_len; + /// Source app bundle identifier UTF-8 bytes (not NUL-terminated). May be NULL. + const char* source_app_bundle_identifier_utf8; + /// Length of `source_app_bundle_identifier_utf8`. + size_t source_app_bundle_identifier_utf8_len; +} RamaTransparentProxyFlowMeta; + +/// One transparent-proxy network rule used to build Apple NE settings. +/// +/// Apple reference: +/// - https://developer.apple.com/documentation/networkextension/nenetworkrule +typedef struct { + /// Optional remote network address UTF-8 bytes (not NUL-terminated). May be NULL. + const char* remote_network_utf8; + /// Length of `remote_network_utf8`. + size_t remote_network_utf8_len; + /// Prefix length for remote network (CIDR). + /// Only valid when `remote_prefix_is_set` is true. + uint8_t remote_prefix; + /// Whether `remote_prefix` is explicitly set. + bool remote_prefix_is_set; + /// Optional local network address UTF-8 bytes (not NUL-terminated). May be NULL. + const char* local_network_utf8; + /// Length of `local_network_utf8`. + size_t local_network_utf8_len; + /// Prefix length for local network (CIDR). + /// Only valid when `local_prefix_is_set` is true. + uint8_t local_prefix; + /// Whether `local_prefix` is explicitly set. + bool local_prefix_is_set; + /// One of `RamaTransparentProxyRuleProtocol`. + uint32_t protocol; +} RamaTransparentProxyNetworkRule; + +/// Transparent proxy configuration returned by Rust to Swift. +/// +/// This structure owns its memory and must be released exactly once with +/// `rama_transparent_proxy_config_free`. +/// +/// Apple references: +/// - https://developer.apple.com/documentation/networkextension/netransparentproxynetworksettings +/// - https://developer.apple.com/documentation/networkextension/netransparentproxyprovider +typedef struct { + /// Placeholder tunnel remote address UTF-8 bytes (not NUL-terminated). + const char* tunnel_remote_address_utf8; + /// Length of `tunnel_remote_address_utf8`. + size_t tunnel_remote_address_utf8_len; + /// Pointer to `rules_len` rules (may be NULL when empty). + const RamaTransparentProxyNetworkRule* rules; + /// Number of rules at `rules`. + size_t rules_len; +} RamaTransparentProxyConfig; + +/// Initialization config passed once before using engine APIs. +/// +/// All string fields are UTF-8 byte slices (`pointer + length`) and are not +/// required to be NUL-terminated. +typedef struct { + /// Writable storage directory for Rust-managed state (certs, cache, etc). + /// May be NULL/0 to let Rust choose a fallback directory. + const char* storage_dir_utf8; + /// Length of `storage_dir_utf8`. + size_t storage_dir_utf8_len; + /// Optional shared app-group directory if available. + /// May be NULL/0 when no app-group is configured. + const char* app_group_dir_utf8; + /// Length of `app_group_dir_utf8`. + size_t app_group_dir_utf8_len; +} RamaTransparentProxyInitConfig; + +typedef void (*RamaTcpServerBytesFn)(void* context, RamaBytesView bytes); +typedef void (*RamaTcpServerClosedFn)(void* context); + +/// Callbacks Swift provides for Rust TCP session events. +typedef struct { + /// Opaque user context passed back to callbacks. + void* context; + /// Called when Rust has bytes to write to client-side TCP flow. + RamaTcpServerBytesFn on_server_bytes; + /// Called when Rust closes server-side TCP direction. + RamaTcpServerClosedFn on_server_closed; +} RamaTransparentProxyTcpSessionCallbacks; + +typedef void (*RamaUdpServerDatagramFn)(void* context, RamaBytesView bytes); +typedef void (*RamaUdpServerClosedFn)(void* context); + +/// Callbacks Swift provides for Rust UDP session events. +typedef struct { + /// Opaque user context passed back to callbacks. + void* context; + /// Called when Rust has one datagram to write to client-side UDP flow. + RamaUdpServerDatagramFn on_server_datagram; + /// Called when Rust closes server-side UDP flow. + RamaUdpServerClosedFn on_server_closed; +} RamaTransparentProxyUdpSessionCallbacks; + +// Logging + +/// Forward a log message to Rust tracing. +/// +/// `message` is borrowed for the duration of the call. +void rama_log( + uint32_t level, + RamaBytesView message +); + + +// Engine lifecycle + +/// Initialize Rust-side transparent proxy subsystem (idempotent). +/// +/// `config` may be NULL. In that case Rust uses internal fallback paths. +bool rama_transparent_proxy_initialize(const RamaTransparentProxyInitConfig* config); + +/// Fetch transparent proxy configuration for NETransparentProxyProvider setup. +/// +/// Returns an owned pointer, or NULL on failure. +/// Caller must release it with `rama_transparent_proxy_config_free`. +RamaTransparentProxyConfig* rama_transparent_proxy_get_config(void); + +/// Free a config previously returned by `rama_transparent_proxy_get_config`. +/// +/// NULL is allowed and ignored. +void rama_transparent_proxy_config_free( + RamaTransparentProxyConfig* config +); + +/// Ask Rust whether a flow should be intercepted. +/// +/// Returns false if `meta` is NULL. +bool rama_transparent_proxy_should_intercept_flow( + const RamaTransparentProxyFlowMeta* meta +); + +/// Allocate a new transparent proxy engine. +/// +/// Returns NULL on failure. +RamaTransparentProxyEngine* rama_transparent_proxy_engine_new(void); + +/// Free an engine previously returned by `rama_transparent_proxy_engine_new`. +/// +/// NULL is allowed and ignored. +void rama_transparent_proxy_engine_free(RamaTransparentProxyEngine* engine); + +/// Start the transparent proxy engine. +/// +/// NULL is allowed and ignored. +void rama_transparent_proxy_engine_start(RamaTransparentProxyEngine* engine); + +/// Stop the transparent proxy engine with provider stop reason. +/// +/// NULL is allowed and ignored. +/// Apple reference: +/// - https://developer.apple.com/documentation/networkextension/neproviderstopreason +void rama_transparent_proxy_engine_stop(RamaTransparentProxyEngine* engine, int32_t reason); + +// TCP flow lifecycle + +/// Create a TCP session for one intercepted flow. +/// +/// `meta` may be NULL (Rust will fall back to default TCP metadata). +/// Returns NULL if session creation is rejected/fails. +RamaTransparentProxyTcpSession* rama_transparent_proxy_engine_new_tcp_session( + RamaTransparentProxyEngine* engine, + const RamaTransparentProxyFlowMeta* meta, + RamaTransparentProxyTcpSessionCallbacks callbacks +); + +/// Free a TCP session. +/// +/// NULL is allowed and ignored. +void rama_transparent_proxy_tcp_session_free(RamaTransparentProxyTcpSession* session); + +/// Deliver client->server TCP bytes into Rust session. +/// +/// `bytes` is borrowed for duration of the call. +void rama_transparent_proxy_tcp_session_on_client_bytes( + RamaTransparentProxyTcpSession* session, + RamaBytesView bytes +); + +/// Signal EOF on client->server TCP direction. +void rama_transparent_proxy_tcp_session_on_client_eof(RamaTransparentProxyTcpSession* session); + +// UDP flow lifecycle + +/// Create a UDP session for one intercepted flow. +/// +/// `meta` may be NULL (Rust will fall back to default UDP metadata). +/// Returns NULL if session creation is rejected/fails. +RamaTransparentProxyUdpSession* rama_transparent_proxy_engine_new_udp_session( + RamaTransparentProxyEngine* engine, + const RamaTransparentProxyFlowMeta* meta, + RamaTransparentProxyUdpSessionCallbacks callbacks +); + +/// Free a UDP session. +/// +/// NULL is allowed and ignored. +void rama_transparent_proxy_udp_session_free(RamaTransparentProxyUdpSession* session); + +/// Deliver one client->server UDP datagram into Rust session. +/// +/// `bytes` is borrowed for duration of the call. +void rama_transparent_proxy_udp_session_on_client_datagram( + RamaTransparentProxyUdpSession* session, + RamaBytesView bytes +); + +/// Signal UDP flow closure from client side. +void rama_transparent_proxy_udp_session_on_client_close(RamaTransparentProxyUdpSession* session); + +// RAII + +/// Free Rust-owned byte buffer returned over FFI. +void rama_owned_bytes_free(RamaBytesOwned bytes); + +#ifdef __cplusplus +} +#endif diff --git a/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNetworkExtension/FFI/RamaFFI.swift b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNetworkExtension/FFI/RamaFFI.swift new file mode 100644 index 000000000..311dea3e5 --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNetworkExtension/FFI/RamaFFI.swift @@ -0,0 +1,376 @@ +import Foundation +import RamaAppleNEFFI + +struct RamaTransparentProxyFlowMetaBridge { + var protocolRaw: UInt32 + var remoteHost: String? + var remotePort: UInt16 + var localHost: String? + var localPort: UInt16 + var sourceAppSigningIdentifier: String? + var sourceAppBundleIdentifier: String? +} + +struct RamaTransparentProxyRuleBridge { + var remoteNetwork: String? + var remotePrefix: UInt8? + var localNetwork: String? + var localPrefix: UInt8? + var protocolRaw: UInt32 +} + +struct RamaTransparentProxyConfigBridge { + var tunnelRemoteAddress: String + var rules: [RamaTransparentProxyRuleBridge] +} + +final class TcpSessionCallbackBox { + let onServerBytes: (Data) -> Void + let onServerClosed: () -> Void + + init(onServerBytes: @escaping (Data) -> Void, onServerClosed: @escaping () -> Void) { + self.onServerBytes = onServerBytes + self.onServerClosed = onServerClosed + } +} + +final class UdpSessionCallbackBox { + let onServerDatagram: (Data) -> Void + let onServerClosed: () -> Void + + init(onServerDatagram: @escaping (Data) -> Void, onServerClosed: @escaping () -> Void) { + self.onServerDatagram = onServerDatagram + self.onServerClosed = onServerClosed + } +} + +private func dataFromView(_ view: RamaBytesView) -> Data { + guard let ptr = view.ptr, view.len > 0 else { + return Data() + } + return Data(bytes: ptr, count: Int(view.len)) +} + +private func stringFromUtf8(_ ptr: UnsafePointer?, _ len: Int) -> String? { + guard let ptr, len > 0 else { return nil } + let raw = UnsafeRawPointer(ptr).assumingMemoryBound(to: UInt8.self) + let buffer = UnsafeBufferPointer(start: raw, count: len) + return String(decoding: buffer, as: UTF8.self) +} + +private func withUtf8OrNil( + _ value: String?, + _ body: (UnsafePointer?, Int) -> T +) -> T { + guard let value else { + return body(nil, 0) + } + + var bytes = Array(value.utf8) + return bytes.withUnsafeMutableBufferPointer { buffer in + guard let base = buffer.baseAddress else { + return body(nil, 0) + } + let ptr = UnsafeRawPointer(base).assumingMemoryBound(to: CChar.self) + return body(ptr, buffer.count) + } +} + +private func withFlowMeta( + _ meta: RamaTransparentProxyFlowMetaBridge, + _ body: (UnsafePointer) -> T +) -> T { + withUtf8OrNil(meta.remoteHost) { remoteHostPtr, remoteHostLen in + withUtf8OrNil(meta.localHost) { localHostPtr, localHostLen in + withUtf8OrNil(meta.sourceAppSigningIdentifier) { signingIdPtr, signingIdLen in + withUtf8OrNil(meta.sourceAppBundleIdentifier) { bundleIdPtr, bundleIdLen in + var cMeta = RamaTransparentProxyFlowMeta( + protocol: meta.protocolRaw, + remote_endpoint: RamaTransparentProxyFlowEndpoint( + host_utf8: remoteHostPtr, + host_utf8_len: remoteHostLen, + port: meta.remotePort, + ), + local_endpoint: RamaTransparentProxyFlowEndpoint( + host_utf8: localHostPtr, + host_utf8_len: localHostLen, + port: meta.localPort, + ), + source_app_signing_identifier_utf8: signingIdPtr, + source_app_signing_identifier_utf8_len: signingIdLen, + source_app_bundle_identifier_utf8: bundleIdPtr, + source_app_bundle_identifier_utf8_len: bundleIdLen + ) + return withUnsafePointer(to: &cMeta) { metaPtr in + body(metaPtr) + } + } + } + } + } +} + +private let ramaTcpOnServerBytesCallback: + @convention(c) (UnsafeMutableRawPointer?, RamaBytesView) + -> Void = { context, view in + guard let context else { return } + let box = Unmanaged.fromOpaque(context).takeUnretainedValue() + let data = dataFromView(view) + if data.isEmpty { return } + box.onServerBytes(data) + } + +private let ramaTcpOnServerClosedCallback: @convention(c) (UnsafeMutableRawPointer?) -> Void = { + context in + guard let context else { return } + let box = Unmanaged.fromOpaque(context).takeUnretainedValue() + box.onServerClosed() +} + +private let ramaUdpOnServerDatagramCallback: + @convention(c) ( + UnsafeMutableRawPointer?, RamaBytesView + ) -> Void = { context, view in + guard let context else { return } + let box = Unmanaged.fromOpaque(context).takeUnretainedValue() + let data = dataFromView(view) + if data.isEmpty { return } + box.onServerDatagram(data) + } + +private let ramaUdpOnServerClosedCallback: @convention(c) (UnsafeMutableRawPointer?) -> Void = { + context in + guard let context else { return } + let box = Unmanaged.fromOpaque(context).takeUnretainedValue() + box.onServerClosed() +} + +final class RamaTransparentProxyEngineHandle { + private var enginePtr: OpaquePointer? + + init() { + self.enginePtr = rama_transparent_proxy_engine_new() + } + + deinit { + if let p = enginePtr { + rama_transparent_proxy_engine_free(p) + } + } + + static func initialize(storageDir: String?, appGroupDir: String?) -> Bool { + return withUtf8OrNil(storageDir) { storagePtr, storageLen in + withUtf8OrNil(appGroupDir) { appGroupPtr, appGroupLen in + var cConfig = RamaTransparentProxyInitConfig( + storage_dir_utf8: storagePtr, + storage_dir_utf8_len: storageLen, + app_group_dir_utf8: appGroupPtr, + app_group_dir_utf8_len: appGroupLen + ) + return withUnsafePointer(to: &cConfig) { cfgPtr in + rama_transparent_proxy_initialize(cfgPtr) + } + } + } + } + + static func log(level: UInt32, message: String) { + let data = Data(message.utf8) + data.withUnsafeBytes { raw in + let ptr = raw.bindMemory(to: UInt8.self).baseAddress + let view = RamaBytesView(ptr: ptr, len: raw.count) + rama_log(level, view) + } + } + + static func config() -> RamaTransparentProxyConfigBridge? { + guard let outPtr = rama_transparent_proxy_get_config() else { return nil } + defer { rama_transparent_proxy_config_free(outPtr) } + let out = outPtr.pointee + guard + let tunnelRemoteAddress = stringFromUtf8( + out.tunnel_remote_address_utf8, + Int(out.tunnel_remote_address_utf8_len) + ) + else { + return nil + } + + var rules: [RamaTransparentProxyRuleBridge] = [] + if let ptr = out.rules, out.rules_len > 0 { + let buffer: UnsafeBufferPointer = + UnsafeBufferPointer(start: ptr, count: Int(out.rules_len)) + for cRule in buffer { + rules.append( + RamaTransparentProxyRuleBridge( + remoteNetwork: stringFromUtf8( + cRule.remote_network_utf8, + Int(cRule.remote_network_utf8_len) + ), + remotePrefix: cRule.remote_prefix_is_set ? cRule.remote_prefix : nil, + localNetwork: stringFromUtf8( + cRule.local_network_utf8, + Int(cRule.local_network_utf8_len) + ), + localPrefix: cRule.local_prefix_is_set ? cRule.local_prefix : nil, + protocolRaw: cRule.protocol + ) + ) + } + } + + return RamaTransparentProxyConfigBridge( + tunnelRemoteAddress: tunnelRemoteAddress, + rules: rules + ) + } + + static func shouldIntercept(meta: RamaTransparentProxyFlowMetaBridge) -> Bool { + RamaTransparentProxyEngineHandle.log( + level: UInt32(RAMA_LOG_LEVEL_DEBUG.rawValue), + message: + "shouldIntercept call protocol=\(meta.protocolRaw) remote=\(meta.remoteHost ?? ""):\(meta.remotePort) local=\(meta.localHost ?? ""):\(meta.localPort)" + ) + let result = withFlowMeta(meta) { metaPtr in + rama_transparent_proxy_should_intercept_flow(metaPtr) + } + RamaTransparentProxyEngineHandle.log( + level: UInt32(RAMA_LOG_LEVEL_DEBUG.rawValue), + message: "shouldIntercept result=\(result)" + ) + return result + } + + func start() { + guard let p = enginePtr else { return } + rama_transparent_proxy_engine_start(p) + } + + func stop(reason: Int32) { + guard let p = enginePtr else { return } + rama_transparent_proxy_engine_stop(p, reason) + } + + func newTcpSession( + meta: RamaTransparentProxyFlowMetaBridge, + onServerBytes: @escaping (Data) -> Void, + onServerClosed: @escaping () -> Void + ) -> RamaTcpSessionHandle? { + guard let p = enginePtr else { return nil } + + let callbackBox = Unmanaged.passRetained( + TcpSessionCallbackBox(onServerBytes: onServerBytes, onServerClosed: onServerClosed)) + let callbacks = RamaTransparentProxyTcpSessionCallbacks( + context: callbackBox.toOpaque(), + on_server_bytes: ramaTcpOnServerBytesCallback, + on_server_closed: ramaTcpOnServerClosedCallback + ) + + let sessionPtr: OpaquePointer? = withFlowMeta(meta) { metaPtr in + rama_transparent_proxy_engine_new_tcp_session(p, metaPtr, callbacks) + } + guard let sessionPtr else { + callbackBox.release() + return nil + } + + return RamaTcpSessionHandle(sessionPtr: sessionPtr, callbackBox: callbackBox) + } + + func newUdpSession( + meta: RamaTransparentProxyFlowMetaBridge, + onServerDatagram: @escaping (Data) -> Void, + onServerClosed: @escaping () -> Void + ) -> RamaUdpSessionHandle? { + guard let p = enginePtr else { return nil } + + let callbackBox = Unmanaged.passRetained( + UdpSessionCallbackBox( + onServerDatagram: onServerDatagram, + onServerClosed: onServerClosed + )) + let callbacks = RamaTransparentProxyUdpSessionCallbacks( + context: callbackBox.toOpaque(), + on_server_datagram: ramaUdpOnServerDatagramCallback, + on_server_closed: ramaUdpOnServerClosedCallback + ) + + let sessionPtr: OpaquePointer? = withFlowMeta(meta) { metaPtr in + rama_transparent_proxy_engine_new_udp_session(p, metaPtr, callbacks) + } + guard let sessionPtr else { + callbackBox.release() + return nil + } + + return RamaUdpSessionHandle(sessionPtr: sessionPtr, callbackBox: callbackBox) + } +} + +final class RamaTcpSessionHandle { + private var sessionPtr: OpaquePointer? + private let callbackBox: Unmanaged + + fileprivate init(sessionPtr: OpaquePointer, callbackBox: Unmanaged) { + self.sessionPtr = sessionPtr + self.callbackBox = callbackBox + } + + deinit { + if let p = sessionPtr { + rama_transparent_proxy_tcp_session_free(p) + } + callbackBox.release() + } + + func onClientBytes(_ data: Data) { + guard let s = sessionPtr else { return } + guard !data.isEmpty else { return } + + data.withUnsafeBytes { raw in + let base = raw.bindMemory(to: UInt8.self).baseAddress + guard let base else { return } + let view = RamaBytesView(ptr: base, len: Int(data.count)) + rama_transparent_proxy_tcp_session_on_client_bytes(s, view) + } + } + + func onClientEof() { + guard let s = sessionPtr else { return } + rama_transparent_proxy_tcp_session_on_client_eof(s) + } +} + +final class RamaUdpSessionHandle { + private var sessionPtr: OpaquePointer? + private let callbackBox: Unmanaged + + fileprivate init(sessionPtr: OpaquePointer, callbackBox: Unmanaged) { + self.sessionPtr = sessionPtr + self.callbackBox = callbackBox + } + + deinit { + if let p = sessionPtr { + rama_transparent_proxy_udp_session_free(p) + } + callbackBox.release() + } + + func onClientDatagram(_ data: Data) { + guard let s = sessionPtr else { return } + guard !data.isEmpty else { return } + + data.withUnsafeBytes { raw in + let base = raw.bindMemory(to: UInt8.self).baseAddress + guard let base else { return } + let view = RamaBytesView(ptr: base, len: Int(data.count)) + rama_transparent_proxy_udp_session_on_client_datagram(s, view) + } + } + + func onClientClose() { + guard let s = sessionPtr else { return } + rama_transparent_proxy_udp_session_on_client_close(s) + } +} diff --git a/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNetworkExtension/Provider/RamaTransparentProxyProvider.swift b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNetworkExtension/Provider/RamaTransparentProxyProvider.swift new file mode 100644 index 000000000..6156411e9 --- /dev/null +++ b/ffi/apple/RamaAppleNetworkExtension/Sources/RamaAppleNetworkExtension/Provider/RamaTransparentProxyProvider.swift @@ -0,0 +1,647 @@ +import Darwin +import Foundation +import NetworkExtension +import RamaAppleNEFFI + +private final class TcpClientWritePump { + private let flow: NEAppProxyTCPFlow + private let logger: (String) -> Void + private let queue = DispatchQueue(label: "rama.tproxy.tcp.write", qos: .utility) + private var pending: [Data] = [] + private var writing = false + private var closed = false + + init(flow: NEAppProxyTCPFlow, logger: @escaping (String) -> Void) { + self.flow = flow + self.logger = logger + } + + func enqueue(_ data: Data) { + guard !data.isEmpty else { return } + queue.async { + if self.closed { return } + self.pending.append(data) + self.flushLocked() + } + } + + func close() { + queue.async { + self.closed = true + self.pending.removeAll(keepingCapacity: false) + } + } + + private func flushLocked() { + if writing || pending.isEmpty || closed { + return + } + + writing = true + let chunk = pending.removeFirst() + flow.write(chunk) { error in + self.queue.async { + self.writing = false + // TODO: see if there are some errors we can filter out (e.g. disconnect...) + if let error { + self.logger("flow.write error: \(error)") + self.closed = true + self.pending.removeAll(keepingCapacity: false) + self.flow.closeReadWithError(error) + self.flow.closeWriteWithError(error) + return + } + + self.flushLocked() + } + } + } +} + +private final class UdpClientWritePump { + private let flow: NEAppProxyUDPFlow + private let logger: (String) -> Void + private let queue = DispatchQueue(label: "rama.tproxy.udp.write", qos: .utility) + private var pending: [Data] = [] + private var writing = false + private var closed = false + private var sentByEndpoint: NWEndpoint? + + init(flow: NEAppProxyUDPFlow, logger: @escaping (String) -> Void) { + self.flow = flow + self.logger = logger + } + + func setSentByEndpoint(_ endpoint: NWEndpoint?) { + queue.async { + if endpoint != nil { + self.sentByEndpoint = endpoint + } + self.flushLocked() + } + } + + func enqueue(_ data: Data) { + guard !data.isEmpty else { return } + queue.async { + if self.closed { return } + self.pending.append(data) + self.flushLocked() + } + } + + func close() { + queue.async { + self.closed = true + self.pending.removeAll(keepingCapacity: false) + } + } + + private func flushLocked() { + if writing || pending.isEmpty || closed { + return + } + + guard let endpoint = sentByEndpoint else { + return + } + + writing = true + let chunk = pending.removeFirst() + flow.writeDatagrams([chunk], sentBy: [endpoint]) { error in + self.queue.async { + self.writing = false + if let error { + self.logger("udp writeDatagrams error: \(error)") + self.closed = true + self.pending.removeAll(keepingCapacity: false) + self.flow.closeReadWithError(error) + self.flow.closeWriteWithError(error) + return + } + + self.flushLocked() + } + } + } +} + +public final class RamaTransparentProxyProvider: NETransparentProxyProvider { + private var engine: RamaTransparentProxyEngineHandle? + private let stateQueue = DispatchQueue(label: "rama.tproxy.state") + private var tcpSessions: [ObjectIdentifier: RamaTcpSessionHandle] = [:] + private var udpSessions: [ObjectIdentifier: RamaUdpSessionHandle] = [:] + + public override func startProxy( + options: [String: Any]?, completionHandler: @escaping (Error?) -> Void + ) { + let storageDir = Self.defaultRustStorageDirectory()?.path + guard RamaTransparentProxyEngineHandle.initialize(storageDir: storageDir, appGroupDir: nil) + else { + completionHandler(NSError(domain: "RamaTransparentProxy", code: 1)) + return + } + logInfo("extension startProxy") + + guard let startup = RamaTransparentProxyEngineHandle.config() else { + logError("failed to get transparent proxy config from rust") + completionHandler(NSError(domain: "RamaTransparentProxy", code: 2)) + return + } + + let settings = NETransparentProxyNetworkSettings( + tunnelRemoteAddress: startup.tunnelRemoteAddress + ) + var builtRules: [NENetworkRule] = [] + for (idx, rule) in startup.rules.enumerated() { + if let built = Self.makeNetworkRule(rule) { + builtRules.append(built) + logInfo( + "include rule[\(idx)] remote=\(rule.remoteNetwork ?? "") remotePrefix=\(rule.remotePrefix.map(String.init) ?? "") local=\(rule.localNetwork ?? "") localPrefix=\(rule.localPrefix.map(String.init) ?? "") proto=\(rule.protocolRaw)" + ) + } else { + logError( + "invalid rule[\(idx)] remote=\(rule.remoteNetwork ?? "") remotePrefix=\(rule.remotePrefix.map(String.init) ?? "") local=\(rule.localNetwork ?? "") localPrefix=\(rule.localPrefix.map(String.init) ?? "") proto=\(rule.protocolRaw)" + ) + } + } + settings.includedNetworkRules = builtRules + logInfo("included network rules count=\(builtRules.count)") + + setTunnelNetworkSettings(settings) { error in + if let error { + self.logError("setTunnelNetworkSettings error: \(error)") + completionHandler(error) + return + } + + self.logInfo("setTunnelNetworkSettings ok") + self.engine = RamaTransparentProxyEngineHandle() + self.logInfo("engine created") + self.engine?.start() + self.logInfo("engine started") + completionHandler(nil) + } + } + + public override func stopProxy( + with reason: NEProviderStopReason, completionHandler: @escaping () -> Void + ) { + logInfo("extension stopProxy reason=\(reason.rawValue)") + self.engine?.stop(reason: Int32(reason.rawValue)) + self.engine = nil + stateQueue.async { + self.tcpSessions.removeAll(keepingCapacity: false) + self.udpSessions.removeAll(keepingCapacity: false) + } + completionHandler() + } + + public override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool { + if let tcp = flow as? NEAppProxyTCPFlow { + let meta = Self.tcpMeta(flow: tcp) + if !RamaTransparentProxyEngineHandle.shouldIntercept(meta: meta) { + logDebug("handleNewFlow tcp bypassed by rust callback") + return false + } + handleTcpFlow(tcp, meta: meta) + return true + } + + if let udp = flow as? NEAppProxyUDPFlow { + let meta = Self.udpMeta( + flow: udp, + remoteEndpoint: nil, + localEndpoint: Self.udpLocalEndpoint(flow: udp) + ) + if !RamaTransparentProxyEngineHandle.shouldIntercept(meta: meta) { + logDebug("handleNewFlow udp bypassed by rust callback") + return false + } + handleUdpFlow(udp) + return true + } + + logDebug("handleNewFlow unsupported type=\(String(describing: type(of: flow)))") + return false + } + + private func handleTcpFlow(_ flow: NEAppProxyTCPFlow, meta: RamaTransparentProxyFlowMetaBridge) + { + let writer = TcpClientWritePump(flow: flow) { [weak self] msg in + self?.logDebug(msg) + } + let flowId = ObjectIdentifier(flow) + + guard + let session = engine?.newTcpSession( + meta: meta, + onServerBytes: { data in + writer.enqueue(data) + }, + onServerClosed: { [weak self] in + writer.close() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + self?.stateQueue.async { + self?.tcpSessions.removeValue(forKey: flowId) + } + } + ) + else { + logDebug("failed to create tcp session") + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + return + } + + stateQueue.async { + self.tcpSessions[flowId] = session + } + + flow.open(withLocalEndpoint: nil) { error in + if let error { + self.logDebug("flow.open error: \(error)") + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + session.onClientEof() + self.stateQueue.async { + self.tcpSessions.removeValue(forKey: flowId) + } + return + } + self.logTrace("flow.open ok (tcp)") + self.tcpReadLoop(flow: flow, session: session) + } + } + + private func handleUdpFlow(_ flow: NEAppProxyUDPFlow) { + let writer = UdpClientWritePump(flow: flow) { [weak self] msg in + self?.logDebug(msg) + } + let flowId = ObjectIdentifier(flow) + + flow.open(withLocalEndpoint: nil) { error in + if let error { + self.logDebug("udp flow.open error: \(error)") + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + return + } + self.logTrace("flow.open ok (udp)") + self.udpReadLoop(flow: flow, writer: writer, session: nil, flowId: flowId) + } + } + + private func tcpReadLoop(flow: NEAppProxyTCPFlow, session: RamaTcpSessionHandle) { + flow.readData { data, error in + if let error { + // TODO: see if there are some errors we can filter out (e.g. disconnect...) + self.logDebug("flow.readData error: \(error)") + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + session.onClientEof() + self.stateQueue.async { + self.tcpSessions.removeValue(forKey: ObjectIdentifier(flow)) + } + return + } + + guard let data, !data.isEmpty else { + self.logTrace("flow.readData eof") + session.onClientEof() + return + } + + session.onClientBytes(data) + self.tcpReadLoop(flow: flow, session: session) + } + } + + private func udpReadLoop( + flow: NEAppProxyUDPFlow, + writer: UdpClientWritePump, + session: RamaUdpSessionHandle?, + flowId: ObjectIdentifier + ) { + flow.readDatagrams { datagrams, endpoints, error in + if let error { + self.logDebug("flow.readDatagrams error: \(error)") + flow.closeReadWithError(error) + flow.closeWriteWithError(error) + session?.onClientClose() + self.stateQueue.async { + self.udpSessions.removeValue(forKey: ObjectIdentifier(flow)) + } + return + } + + guard let datagrams, !datagrams.isEmpty else { + self.logTrace("flow.readDatagrams eof") + session?.onClientClose() + return + } + + let endpoint = endpoints?.first + writer.setSentByEndpoint(endpoint) + + var activeSession = session + if activeSession == nil { + let meta = Self.udpMeta( + flow: flow, + remoteEndpoint: endpoint, + localEndpoint: Self.udpLocalEndpoint(flow: flow) + ) + if !RamaTransparentProxyEngineHandle.shouldIntercept(meta: meta) { + self.logTrace("udp flow bypassed by rust callback") + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + return + } + + activeSession = self.engine?.newUdpSession( + meta: meta, + onServerDatagram: { data in + writer.enqueue(data) + }, + onServerClosed: { [weak self] in + writer.close() + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + self?.stateQueue.async { + self?.udpSessions.removeValue(forKey: flowId) + } + } + ) + + guard let createdSession = activeSession else { + self.logDebug("failed to create udp session") + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + return + } + + self.stateQueue.async { + self.udpSessions[flowId] = createdSession + } + } + + guard let activeSession else { + flow.closeReadWithError(nil) + flow.closeWriteWithError(nil) + return + } + + for datagram in datagrams where !datagram.isEmpty { + activeSession.onClientDatagram(datagram) + } + + self.udpReadLoop(flow: flow, writer: writer, session: activeSession, flowId: flowId) + } + } + + private static func makeNetworkRule(_ rule: RamaTransparentProxyRuleBridge) + -> NENetworkRule? + { + let remote = networkEndpoint(from: rule.remoteNetwork) + let local = networkEndpoint(from: rule.localNetwork) + let proto = networkRuleProtocol(rule.protocolRaw) + + // Host/domain-only rule (no local matcher): use destination-host initializer. + // This avoids forcing CIDR for non-IP hosts (e.g. example.com). + if let remote, local == nil, rule.remotePrefix == nil { + return NENetworkRule( + destinationHost: remote, + protocol: proto + ) + } + + guard + let remotePrefix = resolvedPrefix( + endpoint: remote, + networkText: rule.remoteNetwork, + explicitPrefix: rule.remotePrefix + ), + let localPrefix = resolvedPrefix( + endpoint: local, + networkText: rule.localNetwork, + explicitPrefix: rule.localPrefix + ) + else { + return nil + } + + return NENetworkRule( + remoteNetwork: remote, + remotePrefix: remotePrefix, + localNetwork: local, + localPrefix: localPrefix, + protocol: proto, + direction: .outbound + ) + } + + private static func resolvedPrefix( + endpoint: NWHostEndpoint?, + networkText: String?, + explicitPrefix: UInt8? + ) -> Int? { + guard endpoint != nil else { return 0 } + if let explicitPrefix { return Int(explicitPrefix) } + guard let networkText else { return nil } + return inferredHostPrefix(networkText) + } + + private static func inferredHostPrefix(_ text: String) -> Int? { + var v4 = in_addr() + if text.withCString({ inet_pton(AF_INET, $0, &v4) }) == 1 { + return 32 + } + var v6 = in6_addr() + if text.withCString({ inet_pton(AF_INET6, $0, &v6) }) == 1 { + return 128 + } + return nil + } + + private static func networkEndpoint(from network: String?) -> NWHostEndpoint? { + guard let network, !network.isEmpty else { return nil } + return NWHostEndpoint(hostname: network, port: "0") + } + + private static func networkRuleProtocol(_ raw: UInt32) -> NENetworkRule.`Protocol` { + switch raw { + case UInt32(RAMA_RULE_PROTOCOL_TCP.rawValue): return .TCP + case UInt32(RAMA_RULE_PROTOCOL_UDP.rawValue): return .UDP + default: return .any + } + } + + private static func tcpMeta(flow: NEAppProxyTCPFlow) -> RamaTransparentProxyFlowMetaBridge { + let remote: Any? + if #available(macOS 15.0, *) { + remote = flow.remoteFlowEndpoint + } else { + remote = flow.remoteEndpoint + } + let remoteEndpoint = endpointHostPort(remote) + let localEndpoint = endpointHostPort(bestEffortLocalEndpoint(flow)) + let appMeta = sourceAppMeta(flow) + return RamaTransparentProxyFlowMetaBridge( + protocolRaw: UInt32(RAMA_FLOW_PROTOCOL_TCP.rawValue), + remoteHost: remoteEndpoint?.host, + remotePort: remoteEndpoint?.port ?? 0, + localHost: localEndpoint?.host, + localPort: localEndpoint?.port ?? 0, + sourceAppSigningIdentifier: appMeta.signingIdentifier, + sourceAppBundleIdentifier: appMeta.bundleIdentifier + ) + } + + private static func udpMeta( + flow: NEAppProxyUDPFlow?, + remoteEndpoint: Any?, + localEndpoint: Any? + ) -> RamaTransparentProxyFlowMetaBridge { + let remote = endpointHostPort(remoteEndpoint) + let local = endpointHostPort(localEndpoint) + let appMeta = sourceAppMeta(flow) + return RamaTransparentProxyFlowMetaBridge( + protocolRaw: UInt32(RAMA_FLOW_PROTOCOL_UDP.rawValue), + remoteHost: remote?.host, + remotePort: remote?.port ?? 0, + localHost: local?.host, + localPort: local?.port ?? 0, + sourceAppSigningIdentifier: appMeta.signingIdentifier, + sourceAppBundleIdentifier: appMeta.bundleIdentifier + ) + } + + private static func sourceAppMeta(_ flow: NEAppProxyFlow?) -> ( + signingIdentifier: String?, bundleIdentifier: String? + ) { + guard let flow else { return (nil, nil) } + let raw = flow.metaData.sourceAppSigningIdentifier.trimmingCharacters( + in: .whitespacesAndNewlines) + guard !raw.isEmpty else { return (nil, nil) } + // Apple documents this as "almost always equivalent to bundle identifier". + return (raw, raw) + } + + private static func udpLocalEndpoint(flow: NEAppProxyUDPFlow) -> Any? { + if #available(macOS 15.0, *) { + return flow.localFlowEndpoint + } + return bestEffortLocalEndpoint(flow) + } + + private static func bestEffortLocalEndpoint(_ flow: NEAppProxyFlow) -> Any? { + let object = flow as NSObject + if object.responds(to: NSSelectorFromString("localEndpoint")) { + return object.value(forKey: "localEndpoint") + } + if object.responds(to: NSSelectorFromString("localFlowEndpoint")) { + return object.value(forKey: "localFlowEndpoint") + } + return nil + } + + private static func endpointHostPort(_ endpoint: Any?) -> (host: String, port: UInt16)? { + guard let endpoint else { return nil } + + if let hostEndpoint = endpoint as? NWHostEndpoint { + let host = hostEndpoint.hostname.trimmingCharacters(in: .whitespacesAndNewlines) + guard !host.isEmpty, let port = UInt16(hostEndpoint.port) else { + return nil + } + return (host, port) + } + + let raw = String(describing: endpoint) + guard !raw.isEmpty else { return nil } + return parseEndpointString(raw) + } + + private static func parseEndpointString(_ raw: String) -> (host: String, port: UInt16)? { + // IPv6 endpoint descriptions may be formatted as: + // - 2a02:...:1.53 + // - [2a02:...:1]:53 + // while IPv4/domain typically use host:port. + + if raw.hasPrefix("["), let closeIdx = raw.lastIndex(of: "]") { + let host = String(raw[raw.index(after: raw.startIndex).. URL? { + guard + let base = FileManager.default.urls( + for: .applicationSupportDirectory, + in: .userDomainMask + ).first + else { + return nil + } + return + base + .appendingPathComponent("rama", isDirectory: true) + .appendingPathComponent("tproxy", isDirectory: true) + } +} diff --git a/ffi/apple/examples/transparent_proxy/README.md b/ffi/apple/examples/transparent_proxy/README.md new file mode 100644 index 000000000..6a1a9db13 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/README.md @@ -0,0 +1,37 @@ +# Transparent Proxy (MacOS) Example + +This example shows how to link a Rust staticlib that implements the +Rama NetworkExtension C ABI into a macOS Transparent Proxy extension. + +## Build + +```sh +cd ffi/apple/examples/transparent_proxy +just build-tproxy +``` + +This builds a universal staticlib at: + +``` +ffi/apple/examples/transparent_proxy/tproxy_rs/target/universal/librama_tproxy_example.a +``` + +## Xcode + +`/RamaTransparentProxyExample.xcodeproj` is generated using `xcodegen generate`. + +## Logs + +Stream all logs (host, extension, and Rust (incl. rama)): + +```sh +log stream --info --debug \ + --predicate 'subsystem == "org.ramaproxy.example.tproxy"' +``` + +Or if you want historical logs: + +```sh +log show --last 1h --style compact --info --debug \ + --predicate 'subsystem == "org.ramaproxy.example.tproxy"' +``` diff --git a/ffi/apple/examples/transparent_proxy/justfile b/ffi/apple/examples/transparent_proxy/justfile new file mode 100644 index 000000000..aef74770d --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/justfile @@ -0,0 +1,55 @@ +set working-directory := '.' + +[working-directory: './tproxy_rs'] +build-tproxy-rs: + cargo build --target aarch64-apple-darwin && \ + cargo build --target x86_64-apple-darwin && \ + mkdir -p target/universal && \ + lipo -create \ + -output target/universal/librama_tproxy_example.a \ + target/aarch64-apple-darwin/debug/librama_tproxy_example.a \ + target/x86_64-apple-darwin/debug/librama_tproxy_example.a + +[working-directory: './tproxy_app'] +build-tproxy-app: + xcodegen generate && \ + xcodebuild -project RamaTransparentProxyExample.xcodeproj -scheme RamaTransparentProxyExampleHost -configuration Debug CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO CODE_SIGN_IDENTITY="" build + +[working-directory: './tproxy_app'] +build-tproxy-app-with-signing: + xcodegen generate && \ + xcodebuild -project RamaTransparentProxyExample.xcodeproj -scheme RamaTransparentProxyExampleHost -allowProvisioningUpdates -configuration Debug"" build + +build-tproxy: build-tproxy-rs build-tproxy-app + +build-tproxy-with-signing: build-tproxy-rs build-tproxy-app-with-signing + +[working-directory: './tproxy_rs'] +clean: + cargo clean + +[working-directory: './tproxy_rs'] +clippy *ARGS: + cargo clippy --all-targets --all-features {{ARGS}} + +[working-directory: './tproxy_rs'] +clippy-fix *ARGS: + just clippy --fix {{ARGS}} + +[working-directory: './tproxy_rs'] +rust-doc *ARGS: + cargo doc --all-features --no-deps {{ARGS}} + +[working-directory: './tproxy_rs'] +rust-test *ARGS: + @cargo install cargo-nextest --locked + cargo nextest run --all-features --no-tests=pass {{ARGS}} + +[working-directory: './tproxy_rs'] +rust-test-ignored: + @cargo install cargo-nextest --locked + cargo nextest run --all-features --run-ignored=only --no-tests=pass + +qa-rust: clippy rust-doc rust-test rust-test-ignored + +qa: qa-rust build-tproxy diff --git a/ffi/apple/examples/transparent_proxy/tproxy_app/Extension/Extension.entitlements b/ffi/apple/examples/transparent_proxy/tproxy_app/Extension/Extension.entitlements new file mode 100644 index 000000000..7956b2a09 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_app/Extension/Extension.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/ffi/apple/examples/transparent_proxy/tproxy_app/Extension/Info.plist b/ffi/apple/examples/transparent_proxy/tproxy_app/Extension/Info.plist new file mode 100644 index 000000000..3405c2461 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_app/Extension/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleName + RamaTransparentProxyExampleExtension + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundlePackageType + XPC! + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.app-proxy + NSExtensionPrincipalClass + RamaAppleNetworkExtension.RamaTransparentProxyProvider + + + diff --git a/ffi/apple/examples/transparent_proxy/tproxy_app/Host/Host.entitlements b/ffi/apple/examples/transparent_proxy/tproxy_app/Host/Host.entitlements new file mode 100644 index 000000000..7956b2a09 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_app/Host/Host.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.developer.networking.networkextension + + app-proxy-provider + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/ffi/apple/examples/transparent_proxy/tproxy_app/Host/Info.plist b/ffi/apple/examples/transparent_proxy/tproxy_app/Host/Info.plist new file mode 100644 index 000000000..72a747d6f --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_app/Host/Info.plist @@ -0,0 +1,16 @@ + + + + + CFBundleName + RamaTransparentProxyExampleHost + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundlePackageType + APPL + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + + diff --git a/ffi/apple/examples/transparent_proxy/tproxy_app/Host/main.swift b/ffi/apple/examples/transparent_proxy/tproxy_app/Host/main.swift new file mode 100644 index 000000000..2566170a2 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_app/Host/main.swift @@ -0,0 +1,404 @@ +import AppKit +import Foundation +import NetworkExtension +import OSLog + +final class HostController: NSObject, NSApplicationDelegate { + private let extensionBundleId = "org.ramaproxy.example.tproxy.provider" + private let managerDescription = "Rama Transparent Proxy Example" + private let managerServerAddress = "127.0.0.1" + private let logSubsystem = "org.ramaproxy.example.tproxy" + private let hostLogCategory = "host-app" + private lazy var hostLogger = Logger(subsystem: logSubsystem, category: hostLogCategory) + + private var statusItem: NSStatusItem? + private var statusMenuItem: NSMenuItem? + private var startMenuItem: NSMenuItem? + private var stopMenuItem: NSMenuItem? + + private var activeManager: NETransparentProxyManager? + private var statusObserver: NSObjectProtocol? + private var statusTimer: DispatchSourceTimer? + + func applicationDidFinishLaunching(_ notification: Notification) { + setupStatusItem() + log("host app launched") + startProxy() + } + + func applicationWillTerminate(_ notification: Notification) { + if let statusObserver { + NotificationCenter.default.removeObserver(statusObserver) + } + statusTimer?.cancel() + statusTimer = nil + log("host app terminated") + } + + func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { + guard let manager = activeManager else { + return .terminateNow + } + + switch manager.connection.status { + case .connected, .connecting, .reasserting: + log("quit requested: stopping proxy first") + stopProxy { sender.reply(toApplicationShouldTerminate: true) } + return .terminateLater + default: + return .terminateNow + } + } + + @objc private func startProxyAction(_: Any?) { + startProxy() + } + + @objc private func stopProxyAction(_: Any?) { + stopProxy(completion: nil) + } + + @objc private func refreshAction(_: Any?) { + refreshManagerAndStatus() + } + + @objc private func quitAction(_: Any?) { + NSApplication.shared.terminate(nil) + } + + private func setupStatusItem() { + let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + if let button = statusItem.button { + button.title = "🦙 tproxy demo" + } + + let menu = NSMenu() + + let statusItemMenu = NSMenuItem(title: "Status: loading", action: nil, keyEquivalent: "") + statusItemMenu.isEnabled = false + menu.addItem(statusItemMenu) + + menu.addItem(NSMenuItem.separator()) + + let startItem = NSMenuItem( + title: "Start Proxy", action: #selector(startProxyAction(_:)), keyEquivalent: "s") + startItem.target = self + menu.addItem(startItem) + + let stopItem = NSMenuItem( + title: "Stop Proxy", action: #selector(stopProxyAction(_:)), keyEquivalent: "x") + stopItem.target = self + menu.addItem(stopItem) + + let refreshItem = NSMenuItem( + title: "Refresh Status", action: #selector(refreshAction(_:)), keyEquivalent: "r") + refreshItem.target = self + menu.addItem(refreshItem) + + menu.addItem(NSMenuItem.separator()) + + let quitItem = NSMenuItem( + title: "Quit", action: #selector(quitAction(_:)), keyEquivalent: "q") + quitItem.target = self + menu.addItem(quitItem) + + statusItem.menu = menu + + self.statusItem = statusItem + self.statusMenuItem = statusItemMenu + self.startMenuItem = startItem + self.stopMenuItem = stopItem + } + + private func refreshManagerAndStatus() { + loadManager { [weak self] manager in + guard let self else { return } + guard let manager else { + self.setStatus(status: .invalid, detail: "manager unavailable") + return + } + + self.activeManager = manager + self.installStatusObserver(manager: manager) + self.startStatusTimer(manager: manager) + self.setStatus(status: manager.connection.status, detail: nil) + } + } + + private func startProxy() { + loadOrCreateAndConfigureManager { [weak self] manager in + guard let self else { return } + guard let manager else { + self.setStatus(status: .invalid, detail: "configuration failed") + return + } + + self.activeManager = manager + self.installStatusObserver(manager: manager) + self.startStatusTimer(manager: manager) + switch manager.connection.status { + case .connected, .connecting, .reasserting: + self.log("proxy already active; skipping start") + self.setStatus(status: manager.connection.status, detail: nil) + return + default: + break + } + + do { + self.log("calling startVPNTunnel") + try manager.connection.startVPNTunnel() + self.log("transparent proxy start requested") + self.setStatus(status: manager.connection.status, detail: nil) + } catch { + self.logError("startVPNTunnel error", error) + self.setStatus(status: .disconnected, detail: "start failed") + } + } + } + + private func stopProxy(completion: (() -> Void)?) { + loadManager { [weak self] manager in + guard let self else { + completion?() + return + } + guard let manager else { + self.setStatus(status: .invalid, detail: "manager unavailable") + completion?() + return + } + + self.log("calling stopVPNTunnel") + manager.connection.stopVPNTunnel() + self.setStatus(status: manager.connection.status, detail: nil) + completion?() + } + } + + private func loadManager(completion: @escaping (NETransparentProxyManager?) -> Void) { + NETransparentProxyManager.loadAllFromPreferences { managers, error in + if let error { + self.logError("loadAllFromPreferences error", error) + completion(nil) + return + } + + let manager = self.selectManager(from: managers) + self.log( + "loadAllFromPreferences ok (count=\(managers?.count ?? 0), selected=\(manager != nil))" + ) + completion(manager) + } + } + + private func loadOrCreateAndConfigureManager( + completion: @escaping (NETransparentProxyManager?) -> Void + ) { + NETransparentProxyManager.loadAllFromPreferences { managers, error in + if let error { + self.logError("loadAllFromPreferences error", error) + completion(nil) + return + } + + let existingManager = self.selectManager(from: managers) + let manager = existingManager ?? NETransparentProxyManager() + let isExisting = existingManager != nil + let changed = self.configure(manager: manager) + + if isExisting, !changed { + self.log("reusing installed manager without saving preferences") + completion(manager) + return + } + + self.log(isExisting ? "saving updated preferences" : "saving new preferences") + self.save(manager: manager, fallbackManager: existingManager, completion: completion) + } + } + + private func configure(manager: NETransparentProxyManager) -> Bool { + var changed = false + + let proto = (manager.protocolConfiguration as? NETunnelProviderProtocol) + ?? NETunnelProviderProtocol() + + if proto.providerBundleIdentifier != extensionBundleId { + proto.providerBundleIdentifier = extensionBundleId + changed = true + } + + if proto.serverAddress != managerServerAddress { + proto.serverAddress = managerServerAddress + changed = true + } + + let providerConfiguration = proto.providerConfiguration ?? [:] + if !providerConfiguration.isEmpty { + proto.providerConfiguration = [:] + changed = true + } else if proto.providerConfiguration == nil { + proto.providerConfiguration = [:] + changed = true + } + + if manager.localizedDescription != managerDescription { + manager.localizedDescription = managerDescription + changed = true + } + + if manager.protocolConfiguration == nil + || !self.protocolMatchesExpected(manager.protocolConfiguration as? NETunnelProviderProtocol) + { + manager.protocolConfiguration = proto + changed = true + } + + if !manager.isEnabled { + manager.isEnabled = true + changed = true + } + + return changed + } + + private func protocolMatchesExpected(_ proto: NETunnelProviderProtocol?) -> Bool { + guard let proto else { + return false + } + + return proto.providerBundleIdentifier == extensionBundleId + && proto.serverAddress == managerServerAddress + && (proto.providerConfiguration ?? [:]).isEmpty + } + + private func save( + manager: NETransparentProxyManager, + fallbackManager: NETransparentProxyManager?, + completion: @escaping (NETransparentProxyManager?) -> Void + ) { + manager.saveToPreferences { saveError in + if let saveError { + self.logError("saveToPreferences error", saveError) + if let fallbackManager { + self.log("falling back to existing manager after save failure") + completion(fallbackManager) + return + } + completion(nil) + return + } + + self.log("saveToPreferences ok; loading") + manager.loadFromPreferences { loadError in + if let loadError { + self.logError("loadFromPreferences error", loadError) + completion(nil) + return + } + completion(manager) + } + } + } + + private func selectManager(from managers: [NETransparentProxyManager]?) + -> NETransparentProxyManager? + { + guard let managers, !managers.isEmpty else { + return nil + } + if let exact = managers.first(where: { manager in + guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol else { + return false + } + return proto.providerBundleIdentifier == self.extensionBundleId + }) { + return exact + } + return managers.first + } + + private func installStatusObserver(manager: NETransparentProxyManager) { + if let statusObserver { + NotificationCenter.default.removeObserver(statusObserver) + } + + statusObserver = NotificationCenter.default.addObserver( + forName: .NEVPNStatusDidChange, + object: manager.connection, + queue: .main + ) { [weak self] _ in + guard let self else { return } + self.setStatus(status: manager.connection.status, detail: nil) + } + + log("installed status observer") + } + + private func startStatusTimer(manager: NETransparentProxyManager) { + statusTimer?.cancel() + let timer = DispatchSource.makeTimerSource(queue: .main) + timer.schedule(deadline: .now() + 1.0, repeating: 5.0) + timer.setEventHandler { [weak self] in + guard let self else { return } + self.setStatus(status: manager.connection.status, detail: nil) + } + timer.resume() + statusTimer = timer + } + + private func setStatus(status: NEVPNStatus, detail: String?) { + let statusText = statusString(status) + let title = detail.map { "Status: \(statusText) (\($0))" } ?? "Status: \(statusText)" + statusMenuItem?.title = title + + switch status { + case .connected: + startMenuItem?.isEnabled = false + stopMenuItem?.isEnabled = true + case .connecting, .reasserting: + startMenuItem?.isEnabled = false + stopMenuItem?.isEnabled = true + default: + startMenuItem?.isEnabled = true + stopMenuItem?.isEnabled = false + } + + if let button = statusItem?.button { + button.title = "🦙 tproxy demo" + button.toolTip = title + } + + log("status=\(statusText)") + } + + private func statusString(_ status: NEVPNStatus) -> String { + switch status { + case .invalid: return "invalid" + case .disconnected: return "disconnected" + case .connecting: return "connecting" + case .connected: return "connected" + case .reasserting: return "reasserting" + case .disconnecting: return "disconnecting" + @unknown default: return "unknown" + } + } + + private func log(_ message: String) { + hostLogger.info("\(message, privacy: .public)") + } + + private func logError(_ prefix: String, _ error: Error) { + let ns = error as NSError + hostLogger.error( + "\(prefix, privacy: .public): domain=\(ns.domain, privacy: .public) code=\(ns.code, privacy: .public) userInfo=\(String(describing: ns.userInfo), privacy: .public)" + ) + } +} + +let app = NSApplication.shared +let delegate = HostController() +app.delegate = delegate +app.setActivationPolicy(.accessory) +app.run() diff --git a/ffi/apple/examples/transparent_proxy/tproxy_app/Project.yml b/ffi/apple/examples/transparent_proxy/tproxy_app/Project.yml new file mode 100644 index 000000000..05ddbda7b --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_app/Project.yml @@ -0,0 +1,69 @@ +name: RamaTransparentProxyExample +options: + bundleIdPrefix: ADPG6C355H + deploymentTarget: + macOS: "12.0" + +packages: + RamaAppleNetworkExtension: + path: ../../../RamaAppleNetworkExtension + +targets: + RamaTransparentProxyExampleHost: + type: application + platform: macOS + sources: + - path: Host + dependencies: + - target: RamaTransparentProxyExampleExtension + embed: true + codeSign: true + postBuildScripts: + - name: Sign Embedded Extension + basedOnDependencyAnalysis: false + script: | + set -euo pipefail + if [ "${CODE_SIGNING_ALLOWED:-YES}" != "YES" ]; then + exit 0 + fi + APP="$TARGET_BUILD_DIR/$FULL_PRODUCT_NAME/Contents/PlugIns/RamaTransparentProxyExampleExtension.appex" + if [ -d "$APP" ]; then + /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" --entitlements "$PROJECT_DIR/Extension/Extension.entitlements" --timestamp=none "$APP" + fi + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: org.ramaproxy.example.tproxy + INFOPLIST_FILE: Host/Info.plist + CODE_SIGN_ENTITLEMENTS: Host/Host.entitlements + CODE_SIGNING_ALLOWED: YES + CODE_SIGNING_REQUIRED: YES + CODE_SIGN_STYLE: Automatic + CODE_SIGN_IDENTITY: "Apple Development" + DEVELOPMENT_TEAM: "ADPG6C355H" + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION: YES + + RamaTransparentProxyExampleExtension: + type: app-extension + platform: macOS + sources: + - path: Extension + dependencies: + - package: RamaAppleNetworkExtension + product: RamaAppleNetworkExtension + settings: + base: + PRODUCT_BUNDLE_IDENTIFIER: org.ramaproxy.example.tproxy.provider + INFOPLIST_FILE: Extension/Info.plist + CODE_SIGN_ENTITLEMENTS: Extension/Extension.entitlements + CODE_SIGNING_ALLOWED: YES + CODE_SIGNING_REQUIRED: YES + CODE_SIGN_STYLE: Automatic + CODE_SIGN_IDENTITY: "Apple Development" + DEVELOPMENT_TEAM: "ADPG6C355H" + CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION: YES + ENABLE_DEBUG_DYLIB: NO + OTHER_LDFLAGS: + - "$(SRCROOT)/../tproxy_rs/target/universal/librama_tproxy_example.a" + - "-framework" + - "NetworkExtension" + - "-lz" diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/Cargo.lock b/ffi/apple/examples/transparent_proxy/tproxy_rs/Cargo.lock new file mode 100644 index 000000000..ad1d331b5 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/Cargo.lock @@ -0,0 +1,3970 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[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 = "asynk-strim" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52697735bdaac441a29391a9e97102c74c6ef0f9b60a40cf109b1b404e29d2f6" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-lc-fips-sys" +version = "0.13.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6ea8e07e2df15b9f09f2ac5ee2977369b06d116f0c4eb5fa4ad443b73c7f53" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "regex", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +dependencies = [ + "aws-lc-fips-sys", + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "base64" +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.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "cfg_aliases", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[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 = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", + "zstd", + "zstd-safe", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[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.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "endian-type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "fastrand", + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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 = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e709f3e3d22866f9c25b3aff01af289b18422cc8b4262fb19103ee80fe513d" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libz-sys" +version = "1.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "pin-utils", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3eede3bdf92f3b4f9dc04072a9ce5ab557d5ec9038773bf9ffcd5588b3cc05b" + +[[package]] +name = "md5" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a6d09a73194e6b66df7c8f1b680f156d916a1a942abf2de06823dd02b7855d" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2366db2dca4d2ad033cad11e6ee42844fd727007af5ad04a1730f4cb8163bf" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "thiserror", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ae4f5991976fd48df6d843de219ca6d31b01daaab2dad5af2badeded372bd" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand 0.9.2", + "thiserror", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c381df33c98266b5f08186583660090a4ffa0889e76c7e9a5e175f645a67fa" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-s390_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-aarch_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c350df4d49b5b9e3ca79f7e646fde2377b199e13cfa87320308397e1f37e1a4c" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55a63e6c7244f19b5c6393f025017eb5d793fd5467823a099740a7a4222440c" + +[[package]] +name = "protoc-bin-vendored-linux-s390_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dba5565db4288e935d5330a07c264a4ee8e4a5b4a4e6f4e83fad824cc32f3b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8854774b24ee28b7868cd71dccaae8e02a2365e67a4a87a6cd11ee6cdbdf9cf5" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b38b07546580df720fa464ce124c4b03630a6fb83e05c336fea2a241df7e5d78" + +[[package]] +name = "protoc-bin-vendored-macos-aarch_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89278a9926ce312e51f1d999fee8825d324d603213344a9a706daa009f1d8092" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81745feda7ccfb9471d7a4de888f0652e806d5795b61480605d4943176299756" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95067976aca6421a523e491fce939a3e65249bac4b977adee0ee9771568e8aa3" + +[[package]] +name = "psl" +version = "2.1.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "530defb7f90261f0f83c9af4534efe2e2e7912b265c7890fd402e55f60791fd4" +dependencies = [ + "psl-types", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radix_trie" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rama" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "base64", + "opentelemetry-otlp", + "pin-project-lite", + "rama-core", + "rama-crypto", + "rama-dns", + "rama-grpc", + "rama-http", + "rama-http-backend", + "rama-http-core", + "rama-net", + "rama-net-apple-networkextension", + "rama-socks5", + "rama-tcp", + "rama-tls-boring", + "rama-tls-rustls", + "rama-ua", + "rama-udp", + "rama-unix", + "rama-utils", + "rama-ws", + "rustversion", + "serde", + "tokio", + "tokio-util", + "tracing-subscriber", +] + +[[package]] +name = "rama-boring" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc9b815c8dce0288dc1ecebf53039913c82977604766e48b72e80db99b06327" +dependencies = [ + "bitflags", + "foreign-types", + "libc", + "openssl-macros", + "rama-boring-sys", +] + +[[package]] +name = "rama-boring-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b68b0916fb03989d677548d40c3e25cd24d16da95d32b8c5861ce9805a03ce" +dependencies = [ + "bindgen", + "cmake", + "fs_extra", + "fslock", +] + +[[package]] +name = "rama-boring-tokio" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ff8d421e3e7f5b7a08efcdf1c5292ad11307fb9a97847731f0696e3e15c9047" +dependencies = [ + "rama-boring", + "rama-boring-sys", + "tokio", +] + +[[package]] +name = "rama-core" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "asynk-strim", + "bytes", + "futures", + "parking_lot", + "pin-project-lite", + "rama-error", + "rama-macros", + "rama-utils", + "serde", + "serde_json", + "tokio", + "tokio-graceful", + "tokio-stream", + "tokio-util", + "tracing", + "tracing-appender", +] + +[[package]] +name = "rama-crypto" +version = "0.3.0-rc1" +dependencies = [ + "aws-lc-rs", + "base64", + "rama-core", + "rama-utils", + "rcgen", + "rustls-pki-types", + "serde", + "serde_json", + "x509-parser", +] + +[[package]] +name = "rama-dns" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "hickory-resolver", + "pin-project-lite", + "rama-core", + "rama-net", + "rama-utils", + "rand 0.10.0", + "serde", + "tokio", +] + +[[package]] +name = "rama-error" +version = "0.3.0-rc1" + +[[package]] +name = "rama-grpc" +version = "0.3.0-rc1" +dependencies = [ + "base64", + "flate2", + "pin-project-lite", + "prost", + "prost-types", + "radix_trie", + "rama-core", + "rama-grpc-build", + "rama-http", + "rama-http-core", + "rama-http-types", + "rama-net", + "rama-utils", + "sync_wrapper", + "tokio", + "zstd", +] + +[[package]] +name = "rama-grpc-build" +version = "0.3.0-rc1" +dependencies = [ + "prettyplease", + "proc-macro-crate", + "proc-macro2", + "prost-build", + "prost-types", + "protoc-bin-vendored", + "quote", + "rama-utils", + "syn", +] + +[[package]] +name = "rama-http" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "async-compression", + "base64", + "bitflags", + "chrono", + "compression-codecs", + "compression-core", + "const_format", + "csv", + "flate2", + "http", + "http-range-header", + "httpdate", + "iri-string", + "matchit", + "parking_lot", + "pin-project-lite", + "radix_trie", + "rama-core", + "rama-http-headers", + "rama-http-types", + "rama-net", + "rama-utils", + "rand 0.10.0", + "rawzip", + "serde", + "serde_html_form", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "rama-http-backend" +version = "0.3.0-rc1" +dependencies = [ + "pin-project-lite", + "rama-core", + "rama-http", + "rama-http-core", + "rama-http-headers", + "rama-http-types", + "rama-net", + "rama-tcp", + "rama-unix", + "rama-utils", + "tokio", + "tokio-util", +] + +[[package]] +name = "rama-http-core" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "atomic-waker", + "futures-channel", + "httparse", + "httpdate", + "indexmap", + "itoa", + "parking_lot", + "pin-project-lite", + "rama-core", + "rama-http", + "rama-http-types", + "rama-net", + "rama-utils", + "slab", + "tokio", + "tokio-test", + "want", +] + +[[package]] +name = "rama-http-headers" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "base64", + "chrono", + "const_format", + "httpdate", + "rama-core", + "rama-http-types", + "rama-macros", + "rama-net", + "rama-utils", + "rand 0.10.0", + "serde", + "sha1", +] + +[[package]] +name = "rama-http-types" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "bytes", + "const_format", + "fnv", + "http", + "http-body", + "http-body-util", + "itoa", + "memchr", + "mime", + "mime_guess", + "nom 8.0.0", + "pin-project-lite", + "rama-core", + "rama-macros", + "rama-utils", + "rand 0.10.0", + "serde", + "serde_json", + "sync_wrapper", + "tokio", +] + +[[package]] +name = "rama-macros" +version = "0.3.0-rc1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rama-net" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "const_format", + "flume", + "hex", + "ipnet", + "itertools 0.14.0", + "md5", + "nom 8.0.0", + "parking_lot", + "percent-encoding", + "pin-project-lite", + "psl", + "radix_trie", + "rama-core", + "rama-http-types", + "rama-macros", + "rama-utils", + "serde", + "sha2", + "socket2 0.6.2", + "tokio", +] + +[[package]] +name = "rama-net-apple-networkextension" +version = "0.3.0-rc1" +dependencies = [ + "parking_lot", + "pin-project-lite", + "rama-core", + "rama-macros", + "rama-net", + "rama-tcp", + "rama-udp", + "rama-utils", + "tokio", + "tracing", +] + +[[package]] +name = "rama-socks5" +version = "0.3.0-rc1" +dependencies = [ + "byteorder", + "rama-core", + "rama-dns", + "rama-net", + "rama-tcp", + "rama-udp", + "rama-utils", + "rand 0.10.0", + "tokio", +] + +[[package]] +name = "rama-tcp" +version = "0.3.0-rc1" +dependencies = [ + "pin-project-lite", + "rama-core", + "rama-dns", + "rama-http-types", + "rama-net", + "rama-utils", + "rand 0.10.0", + "tokio", +] + +[[package]] +name = "rama-tls-boring" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "brotli", + "flate2", + "flume", + "itertools 0.14.0", + "moka", + "parking_lot", + "pin-project-lite", + "rama-boring", + "rama-boring-tokio", + "rama-core", + "rama-http-types", + "rama-net", + "rama-ua", + "rama-utils", + "schannel", + "tokio", + "zstd", +] + +[[package]] +name = "rama-tls-rustls" +version = "0.3.0-rc1" +dependencies = [ + "pin-project-lite", + "rama-core", + "rama-http-types", + "rama-net", + "rama-utils", + "rcgen", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "webpki-roots", + "x509-parser", +] + +[[package]] +name = "rama-ua" +version = "0.3.0-rc1" +dependencies = [ + "ahash", + "itertools 0.14.0", + "rama-core", + "rama-http", + "rama-net", + "rama-utils", + "rand 0.10.0", + "serde", + "serde_html_form", + "serde_json", +] + +[[package]] +name = "rama-udp" +version = "0.3.0-rc1" +dependencies = [ + "rama-core", + "rama-dns", + "rama-net", + "tokio", + "tokio-util", +] + +[[package]] +name = "rama-unix" +version = "0.3.0-rc1" +dependencies = [ + "libc", + "pin-project-lite", + "rama-core", + "rama-net", + "tokio", +] + +[[package]] +name = "rama-utils" +version = "0.3.0-rc1" +dependencies = [ + "const_format", + "parking_lot", + "pin-project-lite", + "rama-macros", + "regex", + "serde", + "smallvec", + "smol_str", + "tokio", + "wildcard", +] + +[[package]] +name = "rama-ws" +version = "0.3.0-rc1" +dependencies = [ + "flate2", + "rama-core", + "rama-http", + "rama-net", + "rama-utils", + "rand 0.10.0", + "tokio", +] + +[[package]] +name = "rama_tproxy_example" +version = "0.0.0" +dependencies = [ + "rama", + "tokio", + "tracing-oslog", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rawzip" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a111bd8bfbbf5c3740c1de3e09bcf5bb487b8d1dbeef5690c03acbb9e65450aa" + +[[package]] +name = "rcgen" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[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.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_html_form" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0946d52b4b7e28823148aebbeceb901012c595ad737920d504fa8634bb099e6f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smol_str" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7a918bd2a9951d18ee6e48f076843e8e73a9a5d22cf05bcd4b7a81bdd04e17" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-graceful" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45740b38b48641855471cd402922e89156bdfbd97b69b45eeff170369cc18c7d" +dependencies = [ + "loom", + "pin-project-lite", + "slab", + "tokio", + "tracing", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "tonic" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" +dependencies = [ + "crossbeam-channel", + "parking_lot", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-oslog" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76902d2a8d5f9f55a81155c08971734071968c90f2d9bfe645fe700579b2950" +dependencies = [ + "cc", + "cfg-if", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "chrono", + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[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 = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +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 = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[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.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1adf1535672f5b7824f817792b1afd731d7e843d2d04ec8f27e8cb51edd8ac" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e638317c08b21663aed4d2b9a2091450548954695ff4efa75bff5fa546b3b1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c64760850114d03d5f65457e96fc988f11f01d38fbaa51b254e4ab5809102af" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60eecd4fe26177cfa3339eb00b4a36445889ba3ad37080c2429879718e20ca41" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "wildcard" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b0540e91e49de3817c314da0dd3bc518093ceacc6ea5327cb0e1eb073e5189" +dependencies = [ + "thiserror", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "aws-lc-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/Cargo.toml b/ffi/apple/examples/transparent_proxy/tproxy_rs/Cargo.toml new file mode 100644 index 000000000..535284070 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/Cargo.toml @@ -0,0 +1,64 @@ +[package] +name = "rama_tproxy_example" +version = "0.0.0" +edition = "2024" +publish = false + +[lib] +crate-type = ["staticlib"] + +[workspace] + +[dependencies] +rama = { path = "../../../../../", features = ["net-apple-networkextension", "tcp", "udp", "dns", "socks5", "ws", "http-full", "boring"] } +tokio = { version = "1.48", features = ["net"] } +tracing-oslog = "0.3" + +[lints.rust] +unused_imports = "warn" +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)', 'cfg(loom)'] } + +[lints.clippy] +all = { level = "warn", priority = -1 } +todo = "warn" +empty_enums = "warn" +enum_glob_use = "warn" +equatable_if_let = "warn" +mem_forget = "warn" +unused_self = "warn" +filter_map_next = "warn" +needless_continue = "warn" +needless_borrow = "warn" +match_wildcard_for_single_variants = "warn" +if_let_mutex = "warn" +implicit_clone = "warn" +await_holding_lock = "warn" +imprecise_flops = "warn" +suboptimal_flops = "warn" +lossy_float_literal = "warn" +rest_pat_in_fully_bound_structs = "warn" +fn_params_excessive_bools = "warn" +exit = "warn" +inefficient_to_string = "warn" +linkedlist = "warn" +macro_use_imports = "warn" +manual_let_else = "warn" +match_same_arms = "warn" +needless_pass_by_ref_mut = "warn" +needless_pass_by_value = "warn" +option_option = "warn" +redundant_clone = "warn" +ref_option = "warn" +verbose_file_reads = "warn" +unnested_or_patterns = "warn" +str_to_string = "warn" +type_complexity = "allow" +return_self_not_must_use = "warn" +single_match_else = "warn" +trivially_copy_pass_by_ref = "warn" +use_self = "warn" +uninlined_format_args = "warn" +fallible_impl_from = "warn" + +[lints.rustdoc] +broken_intra_doc_links = "deny" diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/headers.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/headers.rs new file mode 100644 index 000000000..4811761fd --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/headers.rs @@ -0,0 +1,38 @@ +use rama::{ + http::{ + HeaderName, HeaderValue, + headers::{HeaderEncode, TypedHeader}, + }, + telemetry::tracing, +}; + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct XRamaTransparentProxyObservedHeader; + +impl XRamaTransparentProxyObservedHeader { + #[inline(always)] + pub fn new() -> Self { + Self + } +} + +impl TypedHeader for XRamaTransparentProxyObservedHeader { + fn name() -> &'static HeaderName { + static NAME: HeaderName = HeaderName::from_static("x-rama-tproxy-observed"); + &NAME + } +} + +impl HeaderEncode for XRamaTransparentProxyObservedHeader { + fn encode>(&self, values: &mut E) { + values.extend([ + HeaderValue::try_from(format!("seen-by-{}", std::process::id())).unwrap_or_else( + |err| { + tracing::warn!("failed to create proxy observed header: {err}"); + HeaderValue::from_static("seen") + }, + ), + ]); + } +} diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/hijack.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/hijack.rs new file mode 100644 index 000000000..29a08f8cc --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/hijack.rs @@ -0,0 +1,56 @@ +use std::convert::Infallible; + +use rama::{ + Service, + http::{ + HeaderValue, Request, Response, StatusCode, + header::CONTENT_TYPE, + service::web::{ + Router, + response::{Html, IntoResponse}, + }, + }, +}; + +pub fn new_service( + root_ca_pem: &'static [u8], +) -> impl Service { + Router::new() + .with_get("/", Html(STATIC_INDEX_PAGE)) + .with_get("/ping", StatusCode::OK) + .with_get("/data/root.ca.pem", move || { + let mut resp = root_ca_pem.into_response(); + resp.headers_mut().insert( + CONTENT_TYPE, + HeaderValue::from_static("application/x-pem-file"), + ); + std::future::ready(resp) + }) +} + +const STATIC_INDEX_PAGE: &str = r#" + + + + + Rama Transparent Proxy Demo + + + +
+

Rama Transparent Proxy Demo

+

Domain hijacked by the transparent proxy runtime.

+

Your proxy is active. This endpoint is served locally by the Rust MITM stack.

+

Install the proxy root certificate to trust MITM traffic:

+

Download Root CA PEM

+
+ + +"#; diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/mod.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/mod.rs new file mode 100644 index 000000000..0ee0cd35e --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/http/mod.rs @@ -0,0 +1,2 @@ +pub mod headers; +pub mod hijack; diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/lib.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/lib.rs new file mode 100644 index 000000000..24df78b99 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/lib.rs @@ -0,0 +1,430 @@ +use rama::{ + net::apple::networkextension::{ + ffi::{BytesOwned, BytesView, tproxy as ffi_tproxy}, + tproxy::{ + TransparentProxyConfig, TransparentProxyEngine, TransparentProxyEngineBuilder, + TransparentProxyFlowMeta, TransparentProxyFlowProtocol, TransparentProxyNetworkRule, + TransparentProxyRuleProtocol, + }, + }, + telemetry::tracing, +}; + +mod http; +mod tcp; +mod tls; +mod udp; +mod utils; + +pub type RamaTransparentProxyEngine = TransparentProxyEngine; +pub type RamaTransparentProxyTcpSession = + rama::net::apple::networkextension::tproxy::TransparentProxyTcpSession; +pub type RamaTransparentProxyUdpSession = + rama::net::apple::networkextension::tproxy::TransparentProxyUdpSession; + +pub type RamaTransparentProxyFlowMeta = ffi_tproxy::TransparentProxyFlowMeta; +pub type RamaTransparentProxyConfig = ffi_tproxy::TransparentProxyConfig; +pub type RamaTransparentProxyInitConfig = ffi_tproxy::TransparentProxyInitConfig; +pub type RamaTransparentProxyTcpSessionCallbacks = ffi_tproxy::TransparentProxyTcpSessionCallbacks; +pub type RamaTransparentProxyUdpSessionCallbacks = ffi_tproxy::TransparentProxyUdpSessionCallbacks; + +#[unsafe(no_mangle)] +/// # Safety +/// +/// This function is FFI entrypoint and may be called from Swift/C. +pub unsafe extern "C" fn rama_transparent_proxy_initialize( + config: *const RamaTransparentProxyInitConfig, +) -> bool { + if !config.is_null() { + // SAFETY: pointer validity is guaranteed by FFI contract. + let config = unsafe { &*config }; + // SAFETY: pointer + length validity is guaranteed by FFI contract. + if let Some(storage_dir) = unsafe { config.storage_dir() } { + self::tls::certs::set_mitm_base_dir(storage_dir.clone()); + tracing::info!(path = %storage_dir.display(), "configured MITM storage directory"); + } + // SAFETY: pointer + length validity is guaranteed by FFI contract. + if let Some(app_group_dir) = unsafe { config.app_group_dir() } { + tracing::debug!(path = %app_group_dir.display(), "received app-group directory"); + } + } + + let init_status = self::utils::init_tracing(); + tracing::info!("rama proxy initialized: {init_status}"); + init_status +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// Returned [`RamaTransparentProxyConfig`] should be valid. +pub unsafe extern "C" fn rama_transparent_proxy_get_config() -> *mut RamaTransparentProxyConfig { + let config = TransparentProxyConfig::new().with_rules(vec![ + TransparentProxyNetworkRule::any().with_protocol(TransparentProxyRuleProtocol::Tcp), + ]); + + let ffi_cfg = RamaTransparentProxyConfig::from_rust_type(&config); + Box::into_raw(Box::new(ffi_cfg)) +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `config` must be either null or a pointer returned by +/// `rama_transparent_proxy_get_config` that was not freed yet. +pub unsafe extern "C" fn rama_transparent_proxy_config_free( + config: *mut RamaTransparentProxyConfig, +) { + if config.is_null() { + return; + } + // SAFETY: `config` came from `Box::into_raw` in `rama_transparent_proxy_get_config`. + let config = unsafe { Box::from_raw(config) }; + // SAFETY: guaranteed by function contract above. + unsafe { config.free() } +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `meta` must be either null or a valid pointer to `RamaTransparentProxyFlowMeta`. +pub unsafe extern "C" fn rama_transparent_proxy_should_intercept_flow( + meta: *const RamaTransparentProxyFlowMeta, +) -> bool { + if meta.is_null() { + tracing::debug!("rama_transparent_proxy_should_intercept_flow: null meta; ignore traffic"); + return false; + } + + // SAFETY: pointer validity is guaranteed by FFI contract. + let meta = unsafe { (*meta).as_owned_rust_type() }; + + tracing::trace!( + protocol = ?meta.protocol, + remote = ?meta.remote_endpoint, + local = ?meta.local_endpoint, + "flow intercept decision: evaluating (rust callback entered)" + ); + + if meta.protocol != TransparentProxyFlowProtocol::Tcp { + return false; + } + + if meta.remote_endpoint.is_none() { + return false; + }; + + true +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// This function is FFI entrypoint and may be called from Swift/C. +pub unsafe extern "C" fn rama_transparent_proxy_engine_new() -> *mut RamaTransparentProxyEngine { + let engine_builder = + TransparentProxyEngineBuilder::new().with_udp_service(self::udp::new_service()); + + let engine = match self::tcp::try_new_service() { + Ok(tcp_svc) => engine_builder.with_tcp_service(tcp_svc).build(), + Err(err) => { + tracing::error!( + "failed to build new TCP svc: {err}; skip forwarding TCP stream; report bug please!" + ); + engine_builder.build() + } + }; + + Box::into_raw(Box::new(engine)) +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `engine` must either be null or a pointer returned by +/// `rama_transparent_proxy_engine_new` that has not been freed. +pub unsafe extern "C" fn rama_transparent_proxy_engine_free( + engine: *mut RamaTransparentProxyEngine, +) { + if engine.is_null() { + return; + } + + // SAFETY: `engine` came from `Box::into_raw` in `rama_transparent_proxy_engine_new`. + unsafe { drop(Box::from_raw(engine)) }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `engine` must be a valid pointer returned by +/// `rama_transparent_proxy_engine_new`. +pub unsafe extern "C" fn rama_transparent_proxy_engine_start( + engine: *mut RamaTransparentProxyEngine, +) { + if engine.is_null() { + return; + } + + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*engine).start() }; + + tracing::info!("rama transparent proxy engine started"); +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `engine` must be a valid pointer returned by +/// `rama_transparent_proxy_engine_new`. +pub unsafe extern "C" fn rama_transparent_proxy_engine_stop( + engine: *mut RamaTransparentProxyEngine, + reason: i32, +) { + if engine.is_null() { + return; + } + + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*engine).stop(reason) }; + + tracing::info!("rama transparent proxy engine stopped"); +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `engine` must be valid and `meta` must be either null or point to a valid +/// `RamaTransparentProxyFlowMeta`. +pub unsafe extern "C" fn rama_transparent_proxy_engine_new_tcp_session( + engine: *mut RamaTransparentProxyEngine, + meta: *const RamaTransparentProxyFlowMeta, + callbacks: RamaTransparentProxyTcpSessionCallbacks, +) -> *mut RamaTransparentProxyTcpSession { + if engine.is_null() { + return std::ptr::null_mut(); + } + + let typed_meta = if meta.is_null() { + TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::Tcp) + } else { + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*meta).as_owned_rust_type() } + }; + + let context = callbacks.context as usize; + let on_server_bytes = callbacks.on_server_bytes; + let on_server_closed = callbacks.on_server_closed; + + // SAFETY: pointer validity is guaranteed by FFI contract. + let engine = unsafe { &*engine }; + let session = engine.new_tcp_session( + typed_meta, + move |bytes| { + let Some(callback) = on_server_bytes else { + return; + }; + if bytes.is_empty() { + return; + } + // SAFETY: callback pointer is provided by Swift and expected callable. + unsafe { + callback( + context as *mut std::ffi::c_void, + BytesView { + ptr: bytes.as_ptr(), + len: bytes.len(), + }, + ); + } + }, + move || { + if let Some(callback) = on_server_closed { + // SAFETY: callback pointer is provided by Swift and expected callable. + unsafe { callback(context as *mut std::ffi::c_void) }; + } + }, + ); + + match session { + Some(session) => Box::into_raw(Box::new(session)), + None => std::ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `session` must either be null or a pointer returned by +/// `rama_transparent_proxy_engine_new_tcp_session`. +pub unsafe extern "C" fn rama_transparent_proxy_tcp_session_free( + session: *mut RamaTransparentProxyTcpSession, +) { + if session.is_null() { + return; + } + + // SAFETY: `session` came from `Box::into_raw` in session constructor. + unsafe { drop(Box::from_raw(session)) }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `session` must be valid. `bytes` must reference readable memory for this call. +pub unsafe extern "C" fn rama_transparent_proxy_tcp_session_on_client_bytes( + session: *mut RamaTransparentProxyTcpSession, + bytes: BytesView, +) { + if session.is_null() { + return; + } + + // SAFETY: caller guarantees bytes view validity for this call. + let slice = unsafe { bytes.into_slice() }; + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*session).on_client_bytes(slice) }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `session` must be valid. +pub unsafe extern "C" fn rama_transparent_proxy_tcp_session_on_client_eof( + session: *mut RamaTransparentProxyTcpSession, +) { + if session.is_null() { + return; + } + + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*session).on_client_eof() }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `engine` must be valid and `meta` must be either null or point to a valid +/// `RamaTransparentProxyFlowMeta`. +pub unsafe extern "C" fn rama_transparent_proxy_engine_new_udp_session( + engine: *mut RamaTransparentProxyEngine, + meta: *const RamaTransparentProxyFlowMeta, + callbacks: RamaTransparentProxyUdpSessionCallbacks, +) -> *mut RamaTransparentProxyUdpSession { + if engine.is_null() { + return std::ptr::null_mut(); + } + + let typed_meta = if meta.is_null() { + TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::Udp) + } else { + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*meta).as_owned_rust_type() } + }; + + let context = callbacks.context as usize; + let on_server_datagram = callbacks.on_server_datagram; + let on_server_closed = callbacks.on_server_closed; + + // SAFETY: pointer validity is guaranteed by FFI contract. + let engine = unsafe { &*engine }; + let session = engine.new_udp_session( + typed_meta, + move |bytes| { + let Some(callback) = on_server_datagram else { + return; + }; + if bytes.is_empty() { + return; + } + // SAFETY: callback pointer is provided by Swift and expected callable. + unsafe { + callback( + context as *mut std::ffi::c_void, + BytesView { + ptr: bytes.as_ptr(), + len: bytes.len(), + }, + ); + } + }, + move || { + if let Some(callback) = on_server_closed { + // SAFETY: callback pointer is provided by Swift and expected callable. + unsafe { callback(context as *mut std::ffi::c_void) }; + } + }, + ); + + match session { + Some(session) => Box::into_raw(Box::new(session)), + None => std::ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `session` must either be null or a pointer returned by +/// `rama_transparent_proxy_engine_new_udp_session`. +pub unsafe extern "C" fn rama_transparent_proxy_udp_session_free( + session: *mut RamaTransparentProxyUdpSession, +) { + if session.is_null() { + return; + } + + // SAFETY: `session` came from `Box::into_raw` in session constructor. + unsafe { drop(Box::from_raw(session)) }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `session` must be valid. `bytes` must reference readable memory for this call. +pub unsafe extern "C" fn rama_transparent_proxy_udp_session_on_client_datagram( + session: *mut RamaTransparentProxyUdpSession, + bytes: BytesView, +) { + if session.is_null() { + return; + } + + // SAFETY: caller guarantees bytes view validity for this call. + let slice = unsafe { bytes.into_slice() }; + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*session).on_client_datagram(slice) }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `session` must be valid. +pub unsafe extern "C" fn rama_transparent_proxy_udp_session_on_client_close( + session: *mut RamaTransparentProxyUdpSession, +) { + if session.is_null() { + return; + } + + // SAFETY: pointer validity is guaranteed by FFI contract. + unsafe { (*session).on_client_close() }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `message.ptr` must be readable for `message.len` bytes for this call. +pub unsafe extern "C" fn rama_log(level: u32, message: BytesView) { + // SAFETY: guaranteed by function contract above. + unsafe { rama::net::apple::networkextension::ffi::log_callback(level, message) }; +} + +#[unsafe(no_mangle)] +/// # Safety +/// +/// `bytes` must have been returned by this Rust FFI layer and not freed yet. +pub unsafe extern "C" fn rama_owned_bytes_free(bytes: BytesOwned) { + // SAFETY: guaranteed by function contract above. + unsafe { bytes.free() }; +} diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tcp.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tcp.rs new file mode 100644 index 000000000..fd819ed4d --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tcp.rs @@ -0,0 +1,196 @@ +use std::{convert::Infallible, sync::Arc, time::Duration}; + +use rama::{ + Layer, Service, + combinators::Either, + error::{BoxError, ErrorContext as _}, + extensions::ExtensionsMut, + http::{ + Request, Response, + layer::{ + dpi_proxy_credential::DpiProxyCredentialExtractorLayer, + set_header::{SetRequestHeaderLayer, SetResponseHeaderLayer}, + upgrade::{ + HttpProxyConnectRelayServiceRequestMatcher, mitm::HttpUpgradeMitmRelayLayer, + }, + }, + matcher::DomainMatcher, + proxy::mitm::{DefaultErrorResponse, HttpMitmRelay}, + ws::handshake::matcher::HttpWebSocketRelayServiceRequestMatcher, + }, + io::{BridgeIo, Io}, + layer::{ArcLayer, ConsumeErrLayer, HijackLayer}, + net::{ + address::Domain, + apple::networkextension::TcpFlow, + client::ConnectorService, + http::server::HttpPeekRouter, + proxy::IoForwardService, + socket::{SocketOptions, opts::TcpKeepAlive}, + tls::server::PeekTlsClientHelloService, + }, + proxy::socks5::{proxy::mitm::Socks5MitmRelayService, server::Socks5PeekRouter}, + rt::Executor, + tcp::{client::service::TcpConnector, proxy::IoToProxyBridgeIoLayer}, + tls::boring::proxy::{TlsMitmRelay, cert_issuer::BoringMitmCertIssuer}, +}; + +const HIJACK_DOMAIN: Domain = Domain::from_static("mitm.ramaproxy.org"); + +const HTTP_PEEK_DURATION: Duration = Duration::from_secs(8); + +const TCP_KEEPALIVE_TIME: Duration = Duration::from_mins(1); +const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15); +const TCP_KEEPALIVE_RETRIES: u32 = 5; + +// TODO: +// - switch to protected app storage (macos) +// - test + verify http connect +// - test + verify WS + +// TOOD: do something fun with HTML :) replacing something innocent + +pub(super) fn try_new_service() +-> Result, BoxError> { + let (ca_crt, ca_key) = crate::tls::certs::load_or_create_mitm_ca_crt_key_pair() + .context("load/create MITM CA Crt/Key pair")?; + let ca_crt_pem_bytes: &[u8] = ca_crt + .to_pem() + .context("encode root ca cert to pem")? + .leak(); + + let tls_mitm_relay = TlsMitmRelay::new_cached_in_memory(ca_crt, ca_key); + + // TODO: get actual graceful executor here... + let exec = Executor::default(); + + let mitm_svc = new_tcp_service_inner(exec.clone(), tls_mitm_relay, ca_crt_pem_bytes, false); + + Ok(( + ConsumeErrLayer::trace_as_debug(), + IoToProxyBridgeIoLayer::extension_proxy_target_with_connector(tcp_connector_service(exec)), + ) + .into_layer(mitm_svc)) +} + +fn new_tcp_service_inner( + exec: Executor, + tls_mitm_relay: TlsMitmRelay, + ca_crt_pem_bytes: &'static [u8], + within_connect_tunnel: bool, +) -> impl Service, Output = (), Error = Infallible> + Clone +where + Issuer: BoringMitmCertIssuer> + Clone, + Ingress: Io + Unpin + ExtensionsMut, + Egress: Io + Unpin + ExtensionsMut, +{ + let http_mitm_svc = if within_connect_tunnel { + Either::A(HttpMitmRelay::new(exec).with_http_middleware( + http_relay_middleware_within_connect_tunnel(ca_crt_pem_bytes), + )) + } else { + let mut relay = HttpMitmRelay::new(exec.clone()).with_http_middleware( + http_relay_middleware(exec, tls_mitm_relay.clone(), ca_crt_pem_bytes), + ); + relay.h2_mut().set_enable_connect_protocol(); + Either::B(relay) + }; + + let maybe_http_mitm_svc = HttpPeekRouter::new(http_mitm_svc) + .with_peek_timeout(HTTP_PEEK_DURATION) + .with_fallback(IoForwardService::new()); + + let app_mitm_layer = + PeekTlsClientHelloService::new(tls_mitm_relay.into_layer(maybe_http_mitm_svc.clone())) + .with_fallback(maybe_http_mitm_svc); + + if within_connect_tunnel { + return Either::A(ConsumeErrLayer::trace_as_debug().into_layer(app_mitm_layer)); + } + + let socks5_mitm_relay = Socks5MitmRelayService::new(app_mitm_layer.clone()); + let mitm_svc = Socks5PeekRouter::new(socks5_mitm_relay).with_fallback(app_mitm_layer); + + Either::B(ConsumeErrLayer::trace_as_debug().into_layer(mitm_svc)) +} + +fn http_relay_middleware( + exec: Executor, + tls_mitm_relay: TlsMitmRelay, + ca_crt_pem_bytes: &'static [u8], +) -> impl Layer + Clone> ++ Send ++ Sync ++ 'static ++ Clone +where + S: Service, + Issuer: BoringMitmCertIssuer> + Clone, +{ + ( + ConsumeErrLayer::trace_as_debug().with_response(DefaultErrorResponse::new()), + SetResponseHeaderLayer::if_not_present_typed( + crate::http::headers::XRamaTransparentProxyObservedHeader::new(), + ), + SetRequestHeaderLayer::if_not_present_typed( + crate::http::headers::XRamaTransparentProxyObservedHeader::new(), + ), + HttpUpgradeMitmRelayLayer::new( + exec.clone(), + ( + HttpProxyConnectRelayServiceRequestMatcher::new( + new_tcp_service_inner(exec, tls_mitm_relay, ca_crt_pem_bytes, true).boxed(), + ), + HttpWebSocketRelayServiceRequestMatcher::new( + ConsumeErrLayer::trace_as_debug().into_layer(IoForwardService::new()), + ), + ), + ), + DpiProxyCredentialExtractorLayer::new(), + HijackLayer::new( + DomainMatcher::exact(HIJACK_DOMAIN), + Arc::new(crate::http::hijack::new_service(ca_crt_pem_bytes)), + ), + ArcLayer::new(), + ) +} + +fn http_relay_middleware_within_connect_tunnel( + ca_crt_pem_bytes: &'static [u8], +) -> impl Layer + Clone> ++ Send ++ Sync ++ 'static ++ Clone +where + S: Service, +{ + ( + ConsumeErrLayer::trace_as_debug().with_response(DefaultErrorResponse::new()), + SetResponseHeaderLayer::if_not_present_typed( + crate::http::headers::XRamaTransparentProxyObservedHeader::new(), + ), + SetRequestHeaderLayer::if_not_present_typed( + crate::http::headers::XRamaTransparentProxyObservedHeader::new(), + ), + HijackLayer::new( + DomainMatcher::exact(HIJACK_DOMAIN), + Arc::new(crate::http::hijack::new_service(ca_crt_pem_bytes)), + ), + ArcLayer::new(), + ) +} + +fn tcp_connector_service( + exec: Executor, +) -> impl ConnectorService + Clone { + TcpConnector::new(exec).with_connector(Arc::new(SocketOptions { + keep_alive: Some(true), + tcp_keep_alive: Some(TcpKeepAlive { + time: Some(TCP_KEEPALIVE_TIME), + interval: Some(TCP_KEEPALIVE_INTERVAL), + retries: Some(TCP_KEEPALIVE_RETRIES), + }), + ..SocketOptions::default_tcp() + })) +} diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tls/certs.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tls/certs.rs new file mode 100644 index 000000000..8a7e4ebbb --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tls/certs.rs @@ -0,0 +1,84 @@ +use std::{fs, path::PathBuf, sync::OnceLock}; + +use rama::{ + error::{BoxError, ErrorContext as _}, + net::{address::Domain, tls::server::SelfSignedData}, + telemetry::tracing, + tls::boring::{ + core::{ + pkey::{PKey, Private}, + x509::X509, + }, + server::utils::self_signed_server_auth_gen_ca, + }, +}; + +pub fn load_or_create_mitm_ca_crt_key_pair() -> Result<(X509, PKey), BoxError> { + let root_dir = root_ca_base_dir()?; + + let cert_path = root_dir.join("root.ca.pem"); + let key_path = root_dir.join("root.ca.key.pem"); + + if cert_path.is_file() && key_path.is_file() { + tracing::info!( + "crt/key files exist: try to load existing CA crt/key from disk and fail otherwise!" + ); + + let cert_pem = fs::read(&cert_path).context("read root ca cert file as PEM bytes")?; + let key_pem = fs::read(&key_path).context("read root ca key file as PEM bytes")?; + let cert = X509::from_pem(&cert_pem).context("parse root ca cert PEM bytes into X509")?; + let key = PKey::private_key_from_pem(&key_pem) + .context("parse root ca private key PEM bytes into PKey")?; + return Ok((cert, key)); + } + + tracing::info!( + "no CA crt/key pair found... create new ones and store under {}", + root_dir.display() + ); + + if let Some(parent) = cert_path.parent() { + fs::create_dir_all(parent).context("create root ca directory")?; + } + + let (root_cert, root_key) = self_signed_server_auth_gen_ca(&SelfSignedData { + organisation_name: Some("Rama Transparent Proxy Example".to_owned()), + common_name: Some(Domain::from_static("rama-tproxy-mitm-ca.localhost")), + ..Default::default() + }) + .context("generate self-signed root ca")?; + + let cert_pem_bytes = root_cert.to_pem().context("encode root ca cert to pem")?; + let key_pem_bytes = root_key + .private_key_to_pem_pkcs8() + .context("encode root ca key to pkcs8 pem")?; + + let cert_pem_str = + String::from_utf8(cert_pem_bytes).context("root ca cert pem not valid utf-8")?; + let key_pem_str = + String::from_utf8(key_pem_bytes).context("root ca key pem not valid utf-8")?; + + fs::write(&cert_path, cert_pem_str.as_bytes()).context("write root ca cert pem to disk")?; + fs::write(&key_path, key_pem_str.as_bytes()).context("write root ca key pem to disk")?; + + tracing::info!( + cert_path = %cert_path.display(), + key_path = %key_path.display(), + "generated and persisted MITM root CA" + ); + + Ok((root_cert, root_key)) +} + +fn root_ca_base_dir() -> Result { + MITM_BASE_DIR + .get() + .cloned() + .context("missing MITM_BASE_DIR; proxy not properly initialised?") +} + +static MITM_BASE_DIR: OnceLock = OnceLock::new(); + +pub fn set_mitm_base_dir(path: PathBuf) { + let _ = MITM_BASE_DIR.set(path); +} diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tls/mod.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tls/mod.rs new file mode 100644 index 000000000..e410b93df --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/tls/mod.rs @@ -0,0 +1 @@ +pub mod certs; diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/udp.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/udp.rs new file mode 100644 index 000000000..0a0e55965 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/udp.rs @@ -0,0 +1,98 @@ +use std::convert::Infallible; + +use rama::{ + Service, + extensions::ExtensionsRef as _, + net::{apple::networkextension::UdpFlow, proxy::ProxyTarget}, + service::service_fn, + telemetry::tracing, + udp::bind_udp_socket_with_connect_default_dns, +}; + +pub(super) fn new_service() -> impl Service { + service_fn(service) +} + +/// UDP flow handler used by the transparent proxy engine. +/// +/// This resolves the remote target, binds a local UDP socket, connects it to the upstream, +/// then forwards datagrams in both directions until either side closes or an error occurs. +async fn service(mut flow: UdpFlow) -> Result<(), Infallible> { + let Some(ProxyTarget(target_addr)) = flow.extensions().get().cloned() else { + tracing::error!("tproxy udp missing target endpoint, draining flow"); + while flow.recv().await.is_some() {} + return Ok(()); + }; + + let socket = match bind_udp_socket_with_connect_default_dns( + target_addr.clone(), + Some(flow.extensions()), + ) + .await + { + Ok(socket) => socket, + Err(err) => { + tracing::error!(error = %err, "tproxy udp bind failed w/ bind + connect to address: {target_addr}"); + while flow.recv().await.is_some() {} + return Ok(()); + } + }; + + tracing::info!( + remote = %target_addr, + local_addr = ?socket.local_addr().ok(), + peer_addr = ?socket.peer_addr().ok(), + "tproxy udp forwarding started" + ); + + let mut up_packets: u64 = 0; + let mut down_packets: u64 = 0; + let mut up_bytes: u64 = 0; + let mut down_bytes: u64 = 0; + + let mut buf = vec![0u8; 64 * 1024]; + loop { + tokio::select! { + maybe_datagram = flow.recv() => { + let Some(datagram) = maybe_datagram else { + break; + }; + if datagram.is_empty() { + continue; + } + + up_packets += 1; + up_bytes += datagram.len() as u64; + + if let Err(err) = socket.send(&datagram).await { + tracing::error!(error = %err, "tproxy udp upstream send failed"); + break; + } + } + recv_result = socket.recv(&mut buf) => { + match recv_result { + Ok(0) => break, + Ok(n) => { + down_packets += 1; + down_bytes += n as u64; + flow.send(rama::bytes::Bytes::copy_from_slice(&buf[..n])); + } + Err(err) => { + tracing::error!(error = %err, "tproxy udp upstream recv failed"); + break; + } + } + } + } + } + + tracing::info!( + up_packets, + up_bytes, + down_packets, + down_bytes, + "tproxy udp forwarding done" + ); + + Ok(()) +} diff --git a/ffi/apple/examples/transparent_proxy/tproxy_rs/src/utils.rs b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/utils.rs new file mode 100644 index 000000000..f868e9204 --- /dev/null +++ b/ffi/apple/examples/transparent_proxy/tproxy_rs/src/utils.rs @@ -0,0 +1,44 @@ +use std::sync::OnceLock; + +use rama::{ + error::{BoxError, ErrorContext as _}, + telemetry::tracing::subscriber::{ + self, filter, layer::SubscriberExt as _, util::SubscriberInitExt as _, + }, +}; +use tracing_oslog::OsLogger; + +pub(super) fn init_tracing() -> bool { + static CTX: OnceLock> = OnceLock::new(); + CTX.get_or_init(|| match setup_tracing() { + Ok(ctx) => Some(ctx), + Err(err) => { + eprintln!("failed to setup tracing: {err}"); + None + } + }) + .is_some() +} + +#[derive(Debug)] +struct TraceContext; + +fn setup_tracing() -> Result { + let stderr_layer = subscriber::fmt::layer() + .json() + .with_target(true) + .with_current_span(true) + .with_span_list(true) + .with_writer(std::io::stderr); + + let oslog_layer = OsLogger::new("org.ramaproxy.example.tproxy", "transparent-proxy"); + + subscriber::registry() + .with(filter::LevelFilter::DEBUG) // TODO: make customisable log level + .with(stderr_layer) + .with(oslog_layer) + .try_init() + .context("init tracing subsriber")?; + + Ok(TraceContext) +} diff --git a/justfile b/justfile index 510039758..598d261c7 100644 --- a/justfile +++ b/justfile @@ -25,12 +25,22 @@ check: check-crate CRATE: cargo check -p {{CRATE}} --all-targets --all-features +check-crate-linux CRATE: + cargo check -p {{CRATE}} --target x86_64-unknown-linux-gnu --all-features + cargo check -p {{CRATE}} --target aarch64-unknown-linux-gnu --all-features + check-links: lychee . clippy: cargo clippy --workspace --all-targets --all-features +clippy-beta: + cargo +beta clippy --workspace --all-targets --all-features + +clippy-beta-crate CRATE: + cargo +beta clippy -p {{CRATE}} --all-targets --all-features + clippy-crate CRATE: cargo clippy -p {{CRATE}} --all-targets --all-features @@ -99,6 +109,9 @@ qa-crate CRATE: just test-crate {{CRATE}} just test-doc-crate {{CRATE}} +qa-ffi-apple: + just ./ffi/apple/examples/transparent_proxy/qa + qa-full: qa hack test-ignored test-ignored-release test-loom fuzz-60s check-links bench-e2e-http-client-server *ARGS: @@ -106,6 +119,7 @@ bench-e2e-http-client-server *ARGS: clean: cargo clean + just ./ffi/apple/examples/transparent_proxy/clean upgrades: @cargo install cargo-upgrades diff --git a/rama-cli/Cargo.toml b/rama-cli/Cargo.toml index 4f6a745eb..66ab37d9a 100644 --- a/rama-cli/Cargo.toml +++ b/rama-cli/Cargo.toml @@ -24,15 +24,7 @@ clap = { workspace = true } hex = { workspace = true } itertools = { workspace = true } mimalloc = { workspace = true, optional = true } -rama = { version = "0.3.0-rc1", path = "..", features = [ - "boring", - "cli", - "tcp", - "udp", - "http-full", - "proxy-full", - "opentelemetry", -] } +rama = { version = "0.3.0-rc1", path = "..", features = ["boring", "cli", "tcp", "udp", "http-full", "proxy-full", "opentelemetry"] } ratatui = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/rama-cli/README.md b/rama-cli/README.md index d9b494454..d2032a381 100644 --- a/rama-cli/README.md +++ b/rama-cli/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-cli/src/cmd/serve/discard/mod.rs b/rama-cli/src/cmd/serve/discard/mod.rs index 9923ae421..954c3d88f 100644 --- a/rama-cli/src/cmd/serve/discard/mod.rs +++ b/rama-cli/src/cmd/serve/discard/mod.rs @@ -13,13 +13,13 @@ use rama::{ ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::{ConcurrentPolicy, UnlimitedPolicy}, }, - net::{socket::Interface, stream::service::DiscardService}, + net::{address::SocketAddress, stream::service::DiscardService}, rt::Executor, stream::{codec::BytesCodec, io::StreamReader}, tcp::server::TcpListener, telemetry::tracing::{self, Instrument}, tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer}, - udp::{UdpFramed, bind_udp}, + udp::{UdpFramed, bind_udp_with_address}, }; use clap::{Args, ValueEnum}; @@ -30,9 +30,9 @@ use crate::utils::tls::try_new_server_config; #[derive(Debug, Args)] /// rama discard (rfc863) service pub struct CliCommandDiscard { - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:9")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(9))] + bind: SocketAddress, #[arg(short = 'c', long, default_value_t = 0)] /// the number of concurrent connections to allow @@ -113,7 +113,7 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandDiscard) -> Result<(), cfg.bind ); let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind TCP discard service socket")?; @@ -142,7 +142,7 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandDiscard) -> Result<(), "starting UDP discard service: bind interface = {:?}", cfg.bind ); - let udp_socket = bind_udp(cfg.bind.clone()) + let udp_socket = bind_udp_with_address(cfg.bind) .await .context("bind UDP discard service socket")?; diff --git a/rama-cli/src/cmd/serve/echo/mod.rs b/rama-cli/src/cmd/serve/echo/mod.rs index 99906848e..f0c984862 100644 --- a/rama-cli/src/cmd/serve/echo/mod.rs +++ b/rama-cli/src/cmd/serve/echo/mod.rs @@ -13,7 +13,7 @@ use rama::{ limit::policy::{ConcurrentPolicy, UnlimitedPolicy}, }, net::{ - socket::Interface, + address::SocketAddress, stream::service::EchoService, tls::{ApplicationProtocol, server::ServerConfig}, }, @@ -23,7 +23,7 @@ use rama::{ telemetry::tracing::{self, Instrument}, tls::boring::server::{TlsAcceptorData, TlsAcceptorLayer}, ua::profile::UserAgentDatabase, - udp::bind_udp, + udp::bind_udp_with_address, }; use clap::{Args, ValueEnum}; @@ -35,9 +35,9 @@ use crate::utils::{http::HttpVersion, tls::try_new_server_config}; #[derive(Debug, Clone, Args)] /// rama echo service (rich https echo or else raw tcp/udp bytes) pub struct CliCommandEcho { - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:8080")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] + bind: SocketAddress, #[arg(short = 'c', long)] /// the number of concurrent connections to allow @@ -176,7 +176,7 @@ async fn bind_echo_http_service( cfg.bind ); let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind tcp socker for http(s) echo service")?; @@ -266,7 +266,7 @@ async fn bind_echo_tcp_service( tracing::info!("starting TCP echo service: bind interface = {:?}", cfg.bind); let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind TCP echo service socket")?; @@ -321,7 +321,7 @@ async fn bind_echo_udp_service( } tracing::info!("starting UDP echo service: bind interface = {:?}", cfg.bind); - let udp_socket = bind_udp(cfg.bind.clone()) + let udp_socket = bind_udp_with_address(cfg.bind) .await .context("bind UDP echo service socket")?; diff --git a/rama-cli/src/cmd/serve/fp/mod.rs b/rama-cli/src/cmd/serve/fp/mod.rs index bf125255a..c7842bc6e 100644 --- a/rama-cli/src/cmd/serve/fp/mod.rs +++ b/rama-cli/src/cmd/serve/fp/mod.rs @@ -33,7 +33,7 @@ use rama::{ ConsumeErrLayer, Layer, LimitLayer, TimeoutLayer, limit::policy::{ConcurrentPolicy, UnlimitedPolicy}, }, - net::{socket::Interface, stream::layer::http::BodyLimitLayer, tls::ApplicationProtocol}, + net::{address::SocketAddress, stream::layer::http::BodyLimitLayer, tls::ApplicationProtocol}, proxy::haproxy::server::HaProxyLayer, rt::Executor, service::service_fn, @@ -64,9 +64,9 @@ pub struct StorageAuthorized; #[derive(Debug, Args)] /// rama fp service (used for FP collection in purpose of UA emulation) pub struct CliCommandFingerprint { - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:8080")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] + bind: SocketAddress, #[arg(short = 'c', long, default_value_t = 0)] /// the number of concurrent connections to allow @@ -260,7 +260,7 @@ where }; let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind fp service")?; @@ -313,7 +313,7 @@ where tcp_listener .serve( tcp_service_builder - .into_layer(HttpServer::http1(exec).service(http_service)), + .into_layer(HttpServer::new_http1(exec).service(http_service)), ) .await; } @@ -325,7 +325,8 @@ where ); tcp_listener .serve( - tcp_service_builder.into_layer(HttpServer::h2(exec).service(http_service)), + tcp_service_builder + .into_layer(HttpServer::new_h2(exec).service(http_service)), ) .await; } diff --git a/rama-cli/src/cmd/serve/fs/mod.rs b/rama-cli/src/cmd/serve/fs/mod.rs index a6bb274c5..706a3c0d4 100644 --- a/rama-cli/src/cmd/serve/fs/mod.rs +++ b/rama-cli/src/cmd/serve/fs/mod.rs @@ -5,7 +5,7 @@ use rama::{ error::{BoxError, ErrorContext}, graceful::ShutdownGuard, http::service::fs::DirectoryServeMode, - net::{socket::Interface, tls::ApplicationProtocol}, + net::{address::SocketAddress, tls::ApplicationProtocol}, rt::Executor, tcp::server::TcpListener, telemetry::tracing, @@ -25,9 +25,9 @@ pub struct CliCommandFs { #[arg()] path: Option, - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:8080")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] + bind: SocketAddress, #[arg(short = 'c', long, default_value_t = 0)] /// the number of concurrent connections to allow @@ -98,7 +98,7 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandFs) -> Result<(), BoxEr tracing::info!("starting serve service on: bind interface = {}", cfg.bind); let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind serve service")?; diff --git a/rama-cli/src/cmd/serve/httptest/mod.rs b/rama-cli/src/cmd/serve/httptest/mod.rs index fb954ec76..d83216975 100644 --- a/rama-cli/src/cmd/serve/httptest/mod.rs +++ b/rama-cli/src/cmd/serve/httptest/mod.rs @@ -20,7 +20,7 @@ use rama::{ ConsumeErrLayer, Layer, LimitLayer, TimeoutLayer, limit::policy::{ConcurrentPolicy, UnlimitedPolicy}, }, - net::{socket::Interface, stream::layer::http::BodyLimitLayer, tls::ApplicationProtocol}, + net::{address::SocketAddress, stream::layer::http::BodyLimitLayer, tls::ApplicationProtocol}, rt::Executor, tcp::server::TcpListener, telemetry::tracing, @@ -38,9 +38,9 @@ mod endpoint; #[derive(Debug, Args)] /// rama http test service pub struct CliCommandHttpTest { - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:8080")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] + bind: SocketAddress, #[arg(short = 'c', long, default_value_t = 0)] /// the number of concurrent connections to allow @@ -142,7 +142,7 @@ where }; let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind http test service")?; @@ -194,7 +194,7 @@ where tcp_listener .serve( tcp_service_builder - .into_layer(HttpServer::http1(exec).service(http_service)), + .into_layer(HttpServer::new_http1(exec).service(http_service)), ) .await; } @@ -206,7 +206,8 @@ where ); tcp_listener .serve( - tcp_service_builder.into_layer(HttpServer::h2(exec).service(http_service)), + tcp_service_builder + .into_layer(HttpServer::new_h2(exec).service(http_service)), ) .await; } diff --git a/rama-cli/src/cmd/serve/ip/mod.rs b/rama-cli/src/cmd/serve/ip/mod.rs index 128355390..44d9fd17b 100644 --- a/rama-cli/src/cmd/serve/ip/mod.rs +++ b/rama-cli/src/cmd/serve/ip/mod.rs @@ -5,7 +5,7 @@ use rama::{ combinators::Either, error::{BoxError, ErrorContext}, graceful::ShutdownGuard, - net::{socket::Interface, tls::ApplicationProtocol}, + net::{address::SocketAddress, tls::ApplicationProtocol}, rt::Executor, tcp::server::TcpListener, telemetry::tracing, @@ -19,9 +19,9 @@ use crate::utils::tls::try_new_server_config; #[derive(Debug, Args)] /// rama ip service (returns the ip address of the client) pub struct CliCommandIp { - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:8080")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] + bind: SocketAddress, #[arg(long, short = 'c', default_value_t = 0)] /// the number of concurrent connections to allow @@ -96,7 +96,7 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandIp) -> Result<(), BoxEr tracing::info!("starting ip service: bind interface = {}", cfg.bind); let tcp_listener = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind ip service")?; diff --git a/rama-cli/src/cmd/serve/proxy/mod.rs b/rama-cli/src/cmd/serve/proxy/mod.rs index d5a656cbc..6a1ba9bdb 100644 --- a/rama-cli/src/cmd/serve/proxy/mod.rs +++ b/rama-cli/src/cmd/serve/proxy/mod.rs @@ -5,7 +5,6 @@ use rama::{ Layer, Service, combinators::Either, error::{BoxError, ErrorContext}, - extensions::ExtensionsMut, graceful::ShutdownGuard, http::{ Request, Response, StatusCode, @@ -13,7 +12,7 @@ use rama::{ layer::{ remove_header::{RemoveRequestHeaderLayer, RemoveResponseHeaderLayer}, trace::TraceLayer, - upgrade::UpgradeLayer, + upgrade::{DefaultHttpProxyConnectReplyService, UpgradeLayer}, }, matcher::MethodMatcher, server::HttpServer, @@ -23,13 +22,10 @@ use rama::{ ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::{ConcurrentPolicy, UnlimitedPolicy}, }, - net::{ - http::RequestContext, proxy::ProxyTarget, socket::Interface, - stream::layer::http::BodyLimitLayer, - }, + net::{address::SocketAddress, proxy::IoForwardService, stream::layer::http::BodyLimitLayer}, rt::Executor, service::service_fn, - tcp::{client::service::Forwarder, server::TcpListener}, + tcp::{proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing, }; use std::{convert::Infallible, time::Duration}; @@ -37,9 +33,9 @@ use std::{convert::Infallible, time::Duration}; #[derive(Debug, Args)] /// rama proxy server pub struct CliCommandProxy { - /// the interface to bind to - #[arg(long, default_value = "127.0.0.1:8080")] - bind: Interface, + /// the address to bind to + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] + bind: SocketAddress, #[arg(long, short = 'c', default_value_t = 0)] /// the number of concurrent connections to allow (0 = no limit) @@ -56,7 +52,7 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandProxy) -> Result<(), Bo let exec = Executor::graceful(graceful); let tcp_service = TcpListener::build(exec.clone()) - .bind(cfg.bind.clone()) + .bind_address(cfg.bind) .await .context("bind proxy service")?; @@ -71,8 +67,12 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandProxy) -> Result<(), Bo UpgradeLayer::new( exec.clone(), MethodMatcher::CONNECT, - service_fn(http_connect_accept), - ConsumeErrLayer::default().into_layer(Forwarder::ctx(exec)), + DefaultHttpProxyConnectReplyService::new(), + ( + ConsumeErrLayer::default(), + IoToProxyBridgeIoLayer::extension_proxy_target(exec), + ) + .into_layer(IoForwardService::new()), ), RemoveResponseHeaderLayer::hop_by_hop(), RemoveRequestHeaderLayer::hop_by_hop(), @@ -109,25 +109,6 @@ pub async fn run(graceful: ShutdownGuard, cfg: CliCommandProxy) -> Result<(), Bo Ok(()) } -async fn http_connect_accept(mut req: Request) -> Result<(Response, Request), Response> { - match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { - Ok(authority) => { - tracing::info!( - server.address = %authority.host, - server.port = authority.port, - "accept CONNECT (lazy): insert proxy target into context", - ); - req.extensions_mut().insert(ProxyTarget(authority)); - } - Err(err) => { - tracing::error!("error extracting authority: {err:?}"); - return Err(StatusCode::BAD_REQUEST.into_response()); - } - } - - Ok((StatusCode::OK.into_response(), req)) -} - async fn http_plain_proxy(req: Request) -> Result { let client = EasyHttpWebClient::default(); match client.serve(req).await { diff --git a/rama-cli/src/cmd/serve/stunnel/mod.rs b/rama-cli/src/cmd/serve/stunnel/mod.rs index 621be8caf..fb1903d01 100644 --- a/rama-cli/src/cmd/serve/stunnel/mod.rs +++ b/rama-cli/src/cmd/serve/stunnel/mod.rs @@ -31,7 +31,7 @@ use rama::{ graceful::ShutdownGuard, net::{ address::{HostWithPort, SocketAddress}, - socket::Interface, + proxy::IoForwardService, tls::{ DataEncoding, client::ServerVerifyMode, @@ -39,10 +39,7 @@ use rama::{ }, }, rt::Executor, - tcp::{ - client::service::{Forwarder, TcpConnector}, - server::TcpListener, - }, + tcp::{client::service::TcpConnector, proxy::IoToProxyBridgeIoLayer, server::TcpListener}, telemetry::tracing, tls::boring::{ client::{TlsConnectorDataBuilder, TlsConnectorLayer}, @@ -75,11 +72,11 @@ pub enum StunnelSubcommand { #[derive(Debug, Args)] pub struct ExitNodeArgs { - #[arg(long, default_value = "127.0.0.1:8002")] - /// address and port to listen on for incoming TLS connections - pub bind: Interface, + #[arg(long, default_value_t = SocketAddress::local_ipv4(8002))] + /// address to listen on for incoming TLS connections + pub bind: SocketAddress, - #[arg(long, default_value = "127.0.0.1:8080")] + #[arg(long, default_value_t = SocketAddress::local_ipv4(8080))] /// backend address to forward decrypted connections to pub forward: SocketAddress, @@ -100,9 +97,9 @@ pub struct ExitNodeArgs { #[derive(Debug, Args)] pub struct EntryNodeArgs { - #[arg(long, default_value = "127.0.0.1:8003")] - /// address and port to listen on - pub bind: Interface, + #[arg(long, default_value_t = SocketAddress::local_ipv4(8003))] + /// address to listen on + pub bind: SocketAddress, #[arg(long, default_value = "127.0.0.1:8002", value_name = "HOST:PORT")] /// server to connect to @@ -146,7 +143,7 @@ async fn run_exit_node(graceful: ShutdownGuard, cfg: ExitNodeArgs) -> Result<(), let exec = Executor::graceful(graceful); - let tcp_listener = TcpListener::bind(cfg.bind.clone(), exec.clone()) + let tcp_listener = TcpListener::bind_address(cfg.bind, exec.clone()) .await .context("bind stunnel exit node")?; @@ -163,8 +160,9 @@ async fn run_exit_node(graceful: ShutdownGuard, cfg: ExitNodeArgs) -> Result<(), forward_addr ); - let tcp_service = - TlsAcceptorLayer::new(acceptor_data).into_layer(Forwarder::new(exec, forward_addr)); + let tcp_service = TlsAcceptorLayer::new(acceptor_data).into_layer( + IoToProxyBridgeIoLayer::new(exec, forward_addr).into_layer(IoForwardService::new()), + ); tcp_listener.serve(tcp_service).await; }); @@ -175,7 +173,7 @@ async fn run_entry_node(graceful: ShutdownGuard, cfg: EntryNodeArgs) -> Result<( let tls_connector_data = build_tls_connector(&cfg)?; let exec = Executor::graceful(graceful); - let tcp_listener = TcpListener::bind(cfg.bind.clone(), exec.clone()) + let tcp_listener = TcpListener::bind_address(cfg.bind, exec.clone()) .await .context("bind stunnel entry node")?; @@ -192,11 +190,13 @@ async fn run_entry_node(graceful: ShutdownGuard, cfg: EntryNodeArgs) -> Result<( connect_authority ); - let tcp_service = Forwarder::new(exec.clone(), connect_authority).with_connector( - TlsConnectorLayer::secure() - .with_connector_data(tls_connector_data) - .into_layer(TcpConnector::new(exec)), - ); + let tcp_service = IoToProxyBridgeIoLayer::new(exec.clone(), connect_authority) + .with_connector( + TlsConnectorLayer::secure() + .with_connector_data(tls_connector_data) + .into_layer(TcpConnector::new(exec)), + ) + .into_layer(IoForwardService::new()); tcp_listener.serve(tcp_service).await; }); diff --git a/rama-core/README.md b/rama-core/README.md index be1def36a..820ac991e 100644 --- a/rama-core/README.md +++ b/rama-core/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-core/src/futures/delay.rs b/rama-core/src/futures/delay.rs new file mode 100644 index 000000000..9f39c1d44 --- /dev/null +++ b/rama-core/src/futures/delay.rs @@ -0,0 +1,80 @@ +use pin_project_lite::pin_project; +use std::{ + pin::Pin, + task::{self, Poll, ready}, + time::Duration, +}; +use tokio::time::Sleep; + +use super::Stream; + +pin_project! { + /// Stream which has a delay prior to starting... + #[derive(Debug)] + #[must_use = "streams do nothing unless polled"] + pub struct DelayStream { + #[pin] + delay: Option, + has_delayed: bool, + + // The stream to throttle + #[pin] + stream: S, + } +} + +impl DelayStream { + pub fn new(dur: Duration, stream: S) -> Self { + let has_delayed = dur.is_zero(); + Self { + delay: (!has_delayed).then(|| tokio::time::sleep(dur)), + has_delayed, + stream, + } + } + + /// Acquires a reference to the underlying stream that this combinator is + /// pulling from. + pub fn get_ref(&self) -> &S { + &self.stream + } + + /// Acquires a mutable reference to the underlying stream that this combinator + /// is pulling from. + /// + /// Note that care must be taken to avoid tampering with the state of the stream + /// which may otherwise confuse this combinator. + pub fn get_mut(&mut self) -> &mut S { + &mut self.stream + } + + /// Consumes this combinator, returning the underlying stream. + /// + /// Note that this may discard intermediate state of this combinator, so care + /// should be taken to avoid losing resources when this is called. + pub fn into_inner(self) -> S { + self.stream + } +} + +impl Stream for DelayStream { + type Item = T::Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + let me = self.project(); + + if !*me.has_delayed + && let Some(delay) = me.delay.as_pin_mut() + { + ready!(delay.poll(cx)); + *me.has_delayed = true; + } + + me.stream.poll_next(cx) + } + + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { + self.stream.size_hint() + } +} diff --git a/rama-core/src/futures/graceful.rs b/rama-core/src/futures/graceful.rs new file mode 100644 index 000000000..771272360 --- /dev/null +++ b/rama-core/src/futures/graceful.rs @@ -0,0 +1,80 @@ +use pin_project_lite::pin_project; +use std::{ + pin::Pin, + task::{self, Poll}, +}; + +use super::Stream; + +pin_project! { + /// Stream which aborts in case the cancel [`Future`] is fulfilled... + #[derive(Debug)] + #[must_use = "streams do nothing unless polled"] + pub struct GracefulStream { + #[pin] + cancel: F, + + #[pin] + stream: S, + + done: bool, + } +} + +impl GracefulStream { + pub fn new(cancel: F, stream: S) -> Self { + Self { + cancel, + stream, + done: false, + } + } + + /// Acquires a reference to the underlying stream that this combinator is + /// pulling from. + pub fn get_ref(&self) -> &S { + &self.stream + } + + /// Acquires a mutable reference to the underlying stream that this combinator + /// is pulling from. + /// + /// Note that care must be taken to avoid tampering with the state of the stream + /// which may otherwise confuse this combinator. + pub fn get_mut(&mut self) -> &mut S { + &mut self.stream + } + + /// Consumes this combinator, returning the underlying stream. + /// + /// Note that this may discard intermediate state of this combinator, so care + /// should be taken to avoid losing resources when this is called. + pub fn into_inner(self) -> S { + self.stream + } +} + +impl Stream for GracefulStream { + type Item = S::Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + let mut me = self.project(); + + if *me.done { + tracing::trace!("stream called after cancel triggered"); + return Poll::Ready(None); + } + + if me.cancel.as_mut().poll(cx).is_ready() { + tracing::trace!("stream cancelled; return Ready(None)"); + *me.done = true; + return Poll::Ready(None); + } + + me.stream.poll_next(cx) + } + + fn size_hint(&self) -> (usize, Option) { + self.stream.size_hint() + } +} diff --git a/rama-core/src/futures/mod.rs b/rama-core/src/futures/mod.rs new file mode 100644 index 000000000..bb89effb4 --- /dev/null +++ b/rama-core/src/futures/mod.rs @@ -0,0 +1,24 @@ +//! Re-export of the [futures](https://docs.rs/futures/latest/futures/) +//! and [asynk-strim](https://docs.rs/asynk-strim/latest/asynk_strim/) crates. +//! +//! Plus also additional utilities shipped with Rama. +//! +//! Exported for your convenience and because it is so fundamental to rama. + +#[doc(inline)] +pub use ::futures::*; + +#[doc(inline)] +pub use ::asynk_strim as async_stream; + +mod delay; +pub use delay::DelayStream; + +mod zip; +pub use zip::{TryZip, Zip, try_zip, zip}; + +mod graceful; +pub use graceful::GracefulStream; + +#[cfg(test)] +mod tests; diff --git a/rama-core/src/futures/tests.rs b/rama-core/src/futures/tests.rs new file mode 100644 index 000000000..d85e7d5b9 --- /dev/null +++ b/rama-core/src/futures/tests.rs @@ -0,0 +1,64 @@ +use super::{DelayStream, FutureExt as _, GracefulStream, StreamExt as _, stream}; + +use std::time::Duration; +use tokio::time; + +#[tokio::test] +async fn delays_first_item_only() { + time::pause(); + + let dur = Duration::from_millis(10); + let mut s = std::pin::pin!(DelayStream::new(dur, stream::iter([1u8, 2, 3]))); + + tokio::time::sleep(Duration::from_micros(100)).await; + assert_eq!(s.next().now_or_never(), None); + + tokio::time::sleep(Duration::from_millis(10)).await; + assert_eq!(s.next().await, Some(1)); + assert_eq!(s.next().await, Some(2)); + assert_eq!(s.next().await, Some(3)); + assert_eq!(s.next().await, None); +} + +#[tokio::test] +async fn immediate_when_duration_zero() { + let mut s = std::pin::pin!(DelayStream::new(Duration::ZERO, stream::iter([10u8, 20]))); + + assert_eq!(s.next().now_or_never().unwrap(), Some(10)); + assert_eq!(s.next().now_or_never().unwrap(), Some(20)); + assert_eq!(s.next().now_or_never().unwrap(), None); +} + +#[tokio::test] +async fn graceful_stream_cancels_pending_stream() { + time::pause(); + + let (tx, rx) = tokio::sync::oneshot::channel::<()>(); + let cancel = async move { + let _ = rx.await; + }; + + let delayed = DelayStream::new(Duration::from_secs(10), stream::iter([1u8, 2, 3])); + let mut s = std::pin::pin!(GracefulStream::new(cancel, delayed)); + + tokio::time::sleep(Duration::from_millis(1)).await; + assert_eq!(s.next().now_or_never(), None); + + let _ = tx.send(()); + assert_eq!(s.next().await, None); + assert_eq!(s.next().await, None); +} + +#[tokio::test] +async fn graceful_stream_stops_after_cancel_even_if_stream_has_more_items() { + let (tx, rx) = tokio::sync::oneshot::channel::<()>(); + let cancel = async move { + let _ = rx.await; + }; + let mut s = std::pin::pin!(GracefulStream::new(cancel, stream::iter([1u8, 2, 3]))); + + assert_eq!(s.next().await, Some(1)); + let _ = tx.send(()); + assert_eq!(s.next().await, None); + assert_eq!(s.next().await, None); +} diff --git a/rama-core/src/futures/zip.rs b/rama-core/src/futures/zip.rs new file mode 100644 index 000000000..42f987fb4 --- /dev/null +++ b/rama-core/src/futures/zip.rs @@ -0,0 +1,180 @@ +use pin_project_lite::pin_project; +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +/// Joins two futures, waiting for both to complete. +/// +/// # Examples +/// +/// ``` +/// use rama_core::futures; +/// +/// # #[tokio::main] +/// # async fn main() { +/// let a = async { 1 }; +/// let b = async { 2 }; +/// +/// assert_eq!(futures::zip(a, b).await, (1, 2)); +/// # } +/// ``` +pub fn zip(future1: F1, future2: F2) -> Zip +where + F1: Future, + F2: Future, +{ + Zip { + future1: Some(future1), + future2: Some(future2), + output1: None, + output2: None, + } +} + +pin_project! { + /// Future for the [`zip()`] function. + #[derive(Debug)] + #[must_use = "futures do nothing unless you `.await` or poll them"] + pub struct Zip + where + F1: Future, + F2: Future, + { + #[pin] + future1: Option, + output1: Option, + #[pin] + future2: Option, + output2: Option, + } + +} + +/// Extracts the contents of two options and zips them, handling `(Some(_), None)` cases +fn take_zip_from_parts(o1: &mut Option, o2: &mut Option) -> Poll<(T1, T2)> { + match (o1.take(), o2.take()) { + (Some(t1), Some(t2)) => Poll::Ready((t1, t2)), + (o1x, o2x) => { + *o1 = o1x; + *o2 = o2x; + Poll::Pending + } + } +} + +impl Future for Zip +where + F1: Future, + F2: Future, +{ + type Output = (F1::Output, F2::Output); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + if let Some(future) = this.future1.as_mut().as_pin_mut() + && let Poll::Ready(out) = future.poll(cx) + { + *this.output1 = Some(out); + + this.future1.set(None); + } + + if let Some(future) = this.future2.as_mut().as_pin_mut() + && let Poll::Ready(out) = future.poll(cx) + { + *this.output2 = Some(out); + + this.future2.set(None); + } + + take_zip_from_parts(this.output1, this.output2) + } +} + +/// Joins two fallible futures, waiting for both to complete or one of them to error. +/// +/// # Examples +/// +/// ``` +/// use rama_core::futures; +/// +/// # #[tokio::main] +/// # async fn main() { +/// let a = async { Ok::(1) }; +/// let b = async { Err::(2) }; +/// +/// assert_eq!(futures::try_zip(a, b).await, Err(2)); +/// # } +/// ``` +pub fn try_zip(future1: F1, future2: F2) -> TryZip +where + F1: Future>, + F2: Future>, +{ + TryZip { + future1: Some(future1), + future2: Some(future2), + output1: None, + output2: None, + } +} + +pin_project! { + /// Future for the [`try_zip()`] function. + #[derive(Debug)] + #[must_use = "futures do nothing unless you `.await` or poll them"] + pub struct TryZip { + #[pin] + future1: Option, + output1: Option, + #[pin] + future2: Option, + output2: Option, + + } + +} + +impl Future for TryZip +where + F1: Future>, + F2: Future>, +{ + type Output = Result<(T1, T2), E>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + if let Some(future) = this.future1.as_mut().as_pin_mut() + && let Poll::Ready(out) = future.poll(cx) + { + match out { + Ok(t) => { + *this.output1 = Some(t); + + this.future1.set(None); + } + + Err(err) => return Poll::Ready(Err(err)), + } + } + + if let Some(future) = this.future2.as_mut().as_pin_mut() + && let Poll::Ready(out) = future.poll(cx) + { + match out { + Ok(t) => { + *this.output2 = Some(t); + + this.future2.set(None); + } + + Err(err) => return Poll::Ready(Err(err)), + } + } + + take_zip_from_parts(this.output1, this.output2).map(Ok) + } +} diff --git a/rama-core/src/io/bridge.rs b/rama-core/src/io/bridge.rs new file mode 100644 index 000000000..c79f9223f --- /dev/null +++ b/rama-core/src/io/bridge.rs @@ -0,0 +1,58 @@ +use crate::extensions::{ExtensionsMut, ExtensionsRef}; + +/// A bidirectional bridge between two [`Io`] objects. +/// +/// Often this is for Client-Server communication, +/// but in a P2P like topology it can also be equal nodes. +/// +/// [`ExtensionsRef`] and [`ExtensionsMut`] is implemented +/// in function of `Io1` as it is assumed that in flows where +/// [`BridgeIo`] is used that we keep moving from "left" (`Io1`) +/// to "right" (`Io2`) until we start to actually relay bytes, +/// handshakes or any kind of data. +/// +/// If you ever use `Io2`'s extensions you can do so explicitly. +/// +/// [`Io`]: super::Io +pub struct BridgeIo(pub Io1, pub Io2); + +impl ExtensionsRef for BridgeIo { + #[inline(always)] + fn extensions(&self) -> &crate::extensions::Extensions { + let Self(left, _) = self; + left.extensions() + } +} +impl ExtensionsMut for BridgeIo { + #[inline(always)] + fn extensions_mut(&mut self) -> &mut crate::extensions::Extensions { + let Self(left, _) = self; + left.extensions_mut() + } +} + +impl super::PeekIoProvider for BridgeIo +where + Ingress: super::Io, + Egress: super::Io, +{ + type PeekIo = Ingress; + type Mapped = BridgeIo; + + #[inline(always)] + fn peek_io_mut(&mut self) -> &mut Self::PeekIo { + let Self(ingress, _egress) = self; + ingress + } + + #[inline(always)] + fn map_peek_io(self, map: F) -> Self::Mapped + where + PeekedIngress: super::Io, + F: FnOnce(Self::PeekIo) -> PeekedIngress, + { + let Self(ingress, egress) = self; + let peek_ingress = map(ingress); + BridgeIo(peek_ingress, egress) + } +} diff --git a/rama-core/src/io/mod.rs b/rama-core/src/io/mod.rs new file mode 100644 index 000000000..c68952368 --- /dev/null +++ b/rama-core/src/io/mod.rs @@ -0,0 +1,66 @@ +use tokio::io::{AsyncRead, AsyncWrite}; + +mod read; +#[doc(inline)] +pub use read::{ChainReader, HeapReader, StackReader}; + +mod prefix; +#[doc(inline)] +pub use prefix::PrefixedIo; + +pub mod rewind; +pub mod timeout; + +mod bridge; +pub use bridge::BridgeIo; + +/// A generic transport of bytes is a type that implements `AsyncRead`, `AsyncWrite` and `Send`. +/// This is specific to Rama and is directly linked to the supertraits of `Tokio`. +pub trait Io: AsyncRead + AsyncWrite + Send + 'static {} + +impl Io for T where T: AsyncRead + AsyncWrite + Send + 'static {} + +/// A higher level trait that can be used by services which +/// wish to peek into I/O, often as part of Deep Protocol Inspections (DPI). +/// +/// It is implemented for any [`Io`], returning itself. +/// [`BridgeIo`] also implements it, assuming that the first element +/// is the ingress side that is ok to be peeked. +pub trait PeekIoProvider: Send + 'static { + /// The type that can be peeked. + type PeekIo: Io; + + /// The mapped `Self` type produced as a result of + /// mapping the `PeekIo` type. + type Mapped: Send + 'static; + + /// Retrieve a mutable reference to the Peekable type. + fn peek_io_mut(&mut self) -> &mut Self::PeekIo; + + /// Once peeking is finished one can reproduce `self` + /// by mapping the Peeked Io type and produce a new type, + /// usually with the peeked data in-memory as prefix. + fn map_peek_io(self, map: F) -> Self::Mapped + where + PeekedIo: Io, + F: FnOnce(Self::PeekIo) -> PeekedIo; +} + +impl PeekIoProvider for T { + type PeekIo = T; + type Mapped = PeekedIo; + + #[inline(always)] + fn peek_io_mut(&mut self) -> &mut Self::PeekIo { + self + } + + #[inline(always)] + fn map_peek_io(self, map: F) -> Self::Mapped + where + PeekedIo: Io, + F: FnOnce(Self::PeekIo) -> PeekedIo, + { + map(self) + } +} diff --git a/rama-core/src/stream/peek.rs b/rama-core/src/io/prefix.rs similarity index 77% rename from rama-core/src/stream/peek.rs rename to rama-core/src/io/prefix.rs index d2fcb4f5b..3ae22aff3 100644 --- a/rama-core/src/stream/peek.rs +++ b/rama-core/src/io/prefix.rs @@ -10,49 +10,53 @@ use pin_project_lite::pin_project; use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, ReadBuf}; pin_project! { - /// a stream which has peeked some data of the inner stream, - /// to be read first prior to any other reading + /// a stream which has some data prefixed + /// to be read first prior to any other reading. + /// + /// The source of that prefix data is often the result + /// of data which was "peeked" from the inner I/O, + /// although that is not required. /// /// It's similar to `ChainReader`, except that writing is also /// supported and happening directly in function of the inner stream. #[derive(Debug, Clone)] - pub struct PeekStream { - done_peek: bool, + pub struct PrefixedIo { + prefix_eof: bool, #[pin] - peek: P, + prefix: P, #[pin] inner: S, } } -impl PeekStream { - /// Create a new [`PeekStream`] for the given peek - /// [`AsyncRead`] and inner [`Stream`] which implements [`ExtensionsMut`]. +impl PrefixedIo { + /// Create a new [`PrefixedIo`] for the given prefix + /// [`AsyncRead`] and inner [`Io`] which implements [`ExtensionsMut`]. /// - /// [`Stream`]: super::Stream - pub fn new(peek: P, inner: S) -> Self { + /// [`Io`]: super::Io + pub fn new(prefix: P, inner: S) -> Self { Self { - done_peek: false, - peek, + prefix_eof: false, + prefix, inner, } } } -impl ExtensionsRef for PeekStream { +impl ExtensionsRef for PrefixedIo { fn extensions(&self) -> &Extensions { self.inner.extensions() } } -impl ExtensionsMut for PeekStream { +impl ExtensionsMut for PrefixedIo { fn extensions_mut(&mut self) -> &mut Extensions { self.inner.extensions_mut() } } #[warn(clippy::missing_trait_methods)] -impl AsyncRead for PeekStream +impl AsyncRead for PrefixedIo where P: AsyncRead, S: AsyncRead, @@ -64,11 +68,11 @@ where ) -> Poll> { let me = self.project(); - if !*me.done_peek { + if !*me.prefix_eof { let rem = buf.remaining(); - ready!(me.peek.poll_read(cx, buf))?; + ready!(me.prefix.poll_read(cx, buf))?; if buf.remaining() == rem { - *me.done_peek = true; + *me.prefix_eof = true; } else { return Poll::Ready(Ok(())); } @@ -78,7 +82,7 @@ where } #[warn(clippy::missing_trait_methods)] -impl AsyncBufRead for PeekStream +impl AsyncBufRead for PrefixedIo where P: AsyncBufRead, S: AsyncBufRead, @@ -86,10 +90,10 @@ where fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let me = self.project(); - if !*me.done_peek { - match ready!(me.peek.poll_fill_buf(cx)?) { + if !*me.prefix_eof { + match ready!(me.prefix.poll_fill_buf(cx)?) { [] => { - *me.done_peek = true; + *me.prefix_eof = true; } buf => return Poll::Ready(Ok(buf)), } @@ -99,24 +103,24 @@ where fn consume(self: Pin<&mut Self>, amt: usize) { let me = self.project(); - if !*me.done_peek { - me.peek.consume(amt) + if !*me.prefix_eof { + me.prefix.consume(amt) } else { me.inner.consume(amt) } } } -impl Read for PeekStream +impl Read for PrefixedIo where P: Read, S: Read, { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - if !self.done_peek { - let n = self.peek.read(buf)?; + if !self.prefix_eof { + let n = self.prefix.read(buf)?; if n == 0 { - self.done_peek = true; + self.prefix_eof = true; } else { return Ok(n); } @@ -126,7 +130,7 @@ where } #[warn(clippy::missing_trait_methods)] -impl AsyncWrite for PeekStream +impl AsyncWrite for PrefixedIo where S: AsyncWrite, { @@ -169,7 +173,7 @@ where } } -impl Write for PeekStream +impl Write for PrefixedIo where S: Write, { @@ -256,10 +260,10 @@ mod tests { } #[tokio::test] - async fn test_peek_stream_read() { + async fn test_prefix_stream_read() { #[derive(Debug)] struct TestCase { - peek_data: &'static str, + prefix_data: &'static str, inner_data: &'static str, expected_reads: &'static [&'static str], } @@ -267,9 +271,9 @@ mod tests { impl TestCase { async fn test_sync_and_async(&self) { let new_stream = || { - let peek_data = Cursor::new(self.peek_data); + let prefix_data = Cursor::new(self.prefix_data); let inner_data = Cursor::new(self.inner_data); - PeekStream::new(peek_data, ServiceInput::new(inner_data)) + PrefixedIo::new(prefix_data, ServiceInput::new(inner_data)) }; test_multi_read_async::(&mut new_stream(), self.expected_reads).await; @@ -278,7 +282,7 @@ mod tests { } TestCase::<10> { - peek_data: "hello", + prefix_data: "hello", inner_data: " world", expected_reads: &["hello", " world", ""], } @@ -286,7 +290,7 @@ mod tests { .await; TestCase::<5> { - peek_data: "hello world", + prefix_data: "hello world", inner_data: "next data", expected_reads: &["hello", " worl", "d", "next ", "data", ""], } @@ -294,7 +298,7 @@ mod tests { .await; TestCase::<2> { - peek_data: "peek", + prefix_data: "peek", inner_data: "inner", expected_reads: &["pe", "ek", "in", "ne", "r", ""], } @@ -302,7 +306,7 @@ mod tests { .await; TestCase::<8> { - peek_data: "", + prefix_data: "", inner_data: "inner data", expected_reads: &["inner da", "ta", ""], } @@ -310,7 +314,7 @@ mod tests { .await; TestCase::<10> { - peek_data: "", + prefix_data: "", inner_data: "inner data", expected_reads: &["inner data", ""], } @@ -318,7 +322,7 @@ mod tests { .await; TestCase::<12> { - peek_data: "", + prefix_data: "", inner_data: "inner data", expected_reads: &["inner data", ""], } @@ -326,10 +330,10 @@ mod tests { .await; } - fn new_peek_write_stream() -> PeekStream>, ServiceInput>>> { - let peek_data = Cursor::new(Vec::new()); + fn new_prefix_write_stream() -> PrefixedIo>, ServiceInput>>> { + let prefix_data = Cursor::new(Vec::new()); let inner_data = Cursor::new(Vec::new()); - PeekStream::new(peek_data, ServiceInput::new(inner_data)) + PrefixedIo::new(prefix_data, ServiceInput::new(inner_data)) } async fn test_multi_write_async(mut stream: impl AsyncWrite + Unpin, cases: &[&str]) { @@ -345,7 +349,7 @@ mod tests { } #[tokio::test] - async fn test_peek_stream_write() { + async fn test_prefix_stream_write() { #[derive(Debug)] struct TestCase<'a> { writes: &'a [&'static str], @@ -353,18 +357,18 @@ mod tests { impl TestCase<'_> { async fn test_sync_and_async(&self) { - let mut stream = new_peek_write_stream(); + let mut stream = new_prefix_write_stream(); test_multi_write_async(&mut stream, self.writes).await; - assert!(!stream.done_peek, "[async] writes: {:?}", self.writes); + assert!(!stream.prefix_eof, "[async] writes: {:?}", self.writes); assert_eq!( - stream.peek.position(), + stream.prefix.position(), 0, "[async] writes: {:?}", self.writes ); assert!( - stream.peek.into_inner().is_empty(), + stream.prefix.into_inner().is_empty(), "[async] writes: {:?}", self.writes ); @@ -376,18 +380,18 @@ mod tests { self.writes, ); - let mut stream = new_peek_write_stream(); + let mut stream = new_prefix_write_stream(); test_multi_write_sync(&mut stream, self.writes); - assert!(!stream.done_peek, "[sync] writes: {:?}", self.writes); + assert!(!stream.prefix_eof, "[sync] writes: {:?}", self.writes); assert_eq!( - stream.peek.position(), + stream.prefix.position(), 0, "[sync] writes: {:?}", self.writes ); assert!( - stream.peek.into_inner().is_empty(), + stream.prefix.into_inner().is_empty(), "[sync] writes: {:?}", self.writes, ); diff --git a/rama-core/src/stream/read.rs b/rama-core/src/io/read.rs similarity index 100% rename from rama-core/src/stream/read.rs rename to rama-core/src/io/read.rs diff --git a/rama-core/src/stream/rewind.rs b/rama-core/src/io/rewind.rs similarity index 100% rename from rama-core/src/stream/rewind.rs rename to rama-core/src/io/rewind.rs diff --git a/rama-core/src/io/timeout.rs b/rama-core/src/io/timeout.rs new file mode 100644 index 000000000..1d02adcef --- /dev/null +++ b/rama-core/src/io/timeout.rs @@ -0,0 +1,692 @@ +use pin_project_lite::pin_project; +use rama_utils::macros::generate_set_and_with; +use std::{ + io::{self, SeekFrom}, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; +use tokio::{ + io::{AsyncRead, AsyncSeek, AsyncWrite, ReadBuf}, + time::{Instant, Sleep, sleep_until}, +}; + +pin_project! { + #[derive(Debug)] + struct TimeoutState { + timeout: Option, + #[pin] + cur: Sleep, + active: bool, + } +} + +impl TimeoutState { + #[inline] + fn new() -> Self { + Self { + timeout: None, + cur: sleep_until(Instant::now()), + active: false, + } + } + + #[inline] + fn timeout(&self) -> Option { + self.timeout + } + + #[inline] + fn set_timeout(&mut self, timeout: Option) { + debug_assert!( + !self.active, + "set_timeout is only expected before a timeout becomes active" + ); + self.timeout = timeout; + } + + #[inline] + fn set_timeout_pinned(mut self: Pin<&mut Self>, timeout: Option) { + *self.as_mut().project().timeout = timeout; + self.reset(); + } + + #[inline] + fn reset(self: Pin<&mut Self>) { + let this = self.project(); + + if *this.active { + *this.active = false; + this.cur.reset(Instant::now()); + } + } + + #[inline] + fn poll_check(self: Pin<&mut Self>, cx: &mut Context<'_>) -> io::Result<()> { + let mut this = self.project(); + + let timeout = match this.timeout { + Some(timeout) => *timeout, + None => return Ok(()), + }; + + if !*this.active { + this.cur.as_mut().reset(Instant::now() + timeout); + *this.active = true; + } + + match this.cur.poll(cx) { + Poll::Ready(()) => { + *this.active = false; + Err(io::Error::from(io::ErrorKind::TimedOut)) + } + Poll::Pending => Ok(()), + } + } +} + +pin_project! { + /// An `AsyncRead`er which applies a timeout to read operations. + #[derive(Debug)] + pub struct TimeoutReader { + #[pin] + reader: R, + #[pin] + state: TimeoutState, + } +} + +impl TimeoutReader +where + R: AsyncRead, +{ + /// Returns a new `TimeoutReader` wrapping the specified reader. + /// + /// There is initially no timeout. + pub fn new(reader: R) -> Self { + Self { + reader, + state: TimeoutState::new(), + } + } + + /// Returns the current read timeout. + pub fn timeout(&self) -> Option { + self.state.timeout() + } + + generate_set_and_with! { + /// Sets the read timeout. + /// + /// This can only be used before the reader is pinned; + /// use [`set_timeout_pinned`](Self::set_timeout_pinned) + /// otherwise. + pub fn timeout(mut self, timeout: Option) -> Self { + self.state.set_timeout(timeout); + self + } + } + + /// Sets the read timeout. + /// + /// This will reset any pending timeout. Use [`set_timeout`](Self::set_timeout) instead if the reader is not yet + /// pinned. + pub fn set_timeout_pinned(self: Pin<&mut Self>, timeout: Option) { + self.project().state.set_timeout_pinned(timeout); + } + + /// Returns a shared reference to the inner reader. + pub fn get_ref(&self) -> &R { + &self.reader + } + + /// Returns a mutable reference to the inner reader. + pub fn get_mut(&mut self) -> &mut R { + &mut self.reader + } + + /// Returns a pinned mutable reference to the inner reader. + pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut R> { + self.project().reader + } + + /// Consumes the `TimeoutReader`, returning the inner reader. + pub fn into_inner(self) -> R { + self.reader + } +} + +impl AsyncRead for TimeoutReader +where + R: AsyncRead, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.project(); + let r = this.reader.poll_read(cx, buf); + match r { + Poll::Pending => this.state.poll_check(cx)?, + Poll::Ready(_) => this.state.reset(), + } + r + } +} + +impl AsyncWrite for TimeoutReader +where + R: AsyncWrite, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + self.project().reader.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().reader.poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().reader.poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + self.project().reader.poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.reader.is_write_vectored() + } +} + +impl AsyncSeek for TimeoutReader +where + R: AsyncSeek, +{ + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { + self.project().reader.start_seek(position) + } + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().reader.poll_complete(cx) + } +} + +pin_project! { + /// An `AsyncWrite`er which applies a timeout to write operations. + #[derive(Debug)] + pub struct TimeoutWriter { + #[pin] + writer: W, + #[pin] + state: TimeoutState, + } +} + +impl TimeoutWriter +where + W: AsyncWrite, +{ + /// Returns a new `TimeoutWriter` wrapping the specified writer. + /// + /// There is initially no timeout. + pub fn new(writer: W) -> Self { + Self { + writer, + state: TimeoutState::new(), + } + } + + /// Returns the current write timeout. + pub fn timeout(&self) -> Option { + self.state.timeout() + } + + generate_set_and_with! { + /// Sets the write timeout. + /// + /// This can only be used before the writer is pinned; + /// use [`set_timeout_pinned`](Self::set_timeout_pinned) + /// otherwise. + pub fn timeout(mut self, timeout: Option) -> Self { + self.state.set_timeout(timeout); + self + } + } + + /// Sets the write timeout. + /// + /// This will reset any pending timeout. Use [`set_timeout`](Self::set_timeout) + /// instead if the writer is not yet pinned. + pub fn set_timeout_pinned(self: Pin<&mut Self>, timeout: Option) { + self.project().state.set_timeout_pinned(timeout); + } + + /// Returns a shared reference to the inner writer. + pub fn get_ref(&self) -> &W { + &self.writer + } + + /// Returns a mutable reference to the inner writer. + pub fn get_mut(&mut self) -> &mut W { + &mut self.writer + } + + /// Returns a pinned mutable reference to the inner writer. + pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut W> { + self.project().writer + } + + /// Consumes the `TimeoutWriter`, returning the inner writer. + pub fn into_inner(self) -> W { + self.writer + } +} + +impl AsyncWrite for TimeoutWriter +where + W: AsyncWrite, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + let this = self.project(); + let r = this.writer.poll_write(cx, buf); + match r { + Poll::Pending => this.state.poll_check(cx)?, + Poll::Ready(_) => this.state.reset(), + } + r + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let r = this.writer.poll_flush(cx); + match r { + Poll::Pending => this.state.poll_check(cx)?, + Poll::Ready(_) => this.state.reset(), + } + r + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let this = self.project(); + let r = this.writer.poll_shutdown(cx); + match r { + Poll::Pending => this.state.poll_check(cx)?, + Poll::Ready(_) => this.state.reset(), + } + r + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + let this = self.project(); + let r = this.writer.poll_write_vectored(cx, bufs); + match r { + Poll::Pending => this.state.poll_check(cx)?, + Poll::Ready(_) => this.state.reset(), + } + r + } + + fn is_write_vectored(&self) -> bool { + self.writer.is_write_vectored() + } +} + +impl AsyncRead for TimeoutWriter +where + W: AsyncRead, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.project().writer.poll_read(cx, buf) + } +} + +impl AsyncSeek for TimeoutWriter +where + W: AsyncSeek, +{ + fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> io::Result<()> { + self.project().writer.start_seek(position) + } + fn poll_complete(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().writer.poll_complete(cx) + } +} + +pin_project! { + /// A io which applies read and write timeouts to an inner io. + #[derive(Debug)] + pub struct TimeoutIo { + #[pin] + io: TimeoutReader> + } +} + +impl TimeoutIo +where + S: AsyncRead + AsyncWrite, +{ + /// Returns a new `TimeoutIo` wrapping the specified io. + /// + /// There is initially no read or write timeout. + pub fn new(io: S) -> Self { + let writer = TimeoutWriter::new(io); + let io = TimeoutReader::new(writer); + Self { io } + } + + /// Returns the current read timeout. + pub fn read_timeout(&self) -> Option { + self.io.timeout() + } + + generate_set_and_with! { + /// Sets the read timeout. + /// + /// This can only be used before the io is pinned; use + /// [`set_read_timeout_pinned`](Self::set_read_timeout_pinned) otherwise. + pub fn read_timeout(mut self, timeout: Option) -> Self { + self.io.maybe_set_timeout(timeout); + self + } + } + + /// Sets the read timeout. + /// + /// This will reset any pending read timeout. Use [`set_read_timeout`](Self::set_read_timeout) instead if the io + /// has not yet been pinned. + pub fn set_read_timeout_pinned(self: Pin<&mut Self>, timeout: Option) { + self.project().io.set_timeout_pinned(timeout) + } + + /// Returns the current write timeout. + pub fn write_timeout(&self) -> Option { + self.io.get_ref().timeout() + } + + generate_set_and_with! { + /// Sets the write timeout. + /// + /// This can only be used before the io is pinned; use + /// [`set_write_timeout_pinned`](Self::set_write_timeout_pinned) otherwise. + pub fn write_timeout(mut self, timeout: Option) -> Self { + self.io.get_mut().maybe_set_timeout(timeout); + self + } + } + + /// Sets the write timeout. + /// + /// This will reset any pending write timeout. Use [`set_write_timeout`](Self::set_write_timeout) instead if the + /// io has not yet been pinned. + pub fn set_write_timeout_pinned(self: Pin<&mut Self>, timeout: Option) { + self.project().io.get_pin_mut().set_timeout_pinned(timeout) + } + + /// Returns a shared reference to the inner io. + pub fn get_ref(&self) -> &S { + self.io.get_ref().get_ref() + } + + /// Returns a mutable reference to the inner io. + pub fn get_mut(&mut self) -> &mut S { + self.io.get_mut().get_mut() + } + + /// Returns a pinned mutable reference to the inner io. + pub fn get_pin_mut(self: Pin<&mut Self>) -> Pin<&mut S> { + self.project().io.get_pin_mut().get_pin_mut() + } + + /// Consumes the io, returning the inner io. + pub fn into_inner(self) -> S { + self.io.into_inner().into_inner() + } +} + +impl AsyncRead for TimeoutIo +where + S: AsyncRead + AsyncWrite, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.project().io.poll_read(cx, buf) + } +} + +impl AsyncWrite for TimeoutIo +where + S: AsyncRead + AsyncWrite, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + self.project().io.poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().io.poll_flush(cx) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + self.project().io.poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + self.project().io.poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.io.is_write_vectored() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::futures::FutureExt as _; + + use std::pin::pin; + use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex}; + + pin_project! { + struct DelayIo { + #[pin] + sleep: Sleep, + } + } + + impl DelayIo { + fn new(until: Instant) -> Self { + Self { + sleep: sleep_until(until), + } + } + } + + impl AsyncRead for DelayIo { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context, + _buf: &mut ReadBuf, + ) -> Poll> { + match self.project().sleep.poll(cx) { + Poll::Ready(()) => Poll::Ready(Ok(())), + Poll::Pending => Poll::Pending, + } + } + } + + impl AsyncWrite for DelayIo { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context, + buf: &[u8], + ) -> Poll> { + match self.project().sleep.poll(cx) { + Poll::Ready(()) => Poll::Ready(Ok(buf.len())), + Poll::Pending => Poll::Pending, + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context) -> Poll> { + Poll::Ready(Ok(())) + } + } + + #[tokio::test] + async fn read_timeout() { + let reader = DelayIo::new(Instant::now() + Duration::from_millis(150)); + let mut reader = pin!(TimeoutReader::new(reader).with_timeout(Duration::from_millis(100))); + + let r = reader.read(&mut [0]).await; + assert_eq!(r.err().unwrap().kind(), io::ErrorKind::TimedOut); + + let _ = reader.read(&mut [0]).await.unwrap(); + } + + #[tokio::test] + async fn read_ok() { + let reader = DelayIo::new(Instant::now() + Duration::from_millis(100)); + let mut reader = pin!(TimeoutReader::new(reader).with_timeout(Duration::from_millis(500))); + + let _ = reader.read(&mut [0]).await.unwrap(); + } + + #[tokio::test] + async fn write_timeout() { + let writer = DelayIo::new(Instant::now() + Duration::from_millis(150)); + let mut writer = pin!(TimeoutWriter::new(writer).with_timeout(Duration::from_millis(100))); + + let r = writer.write(&[0]).await; + assert_eq!(r.err().unwrap().kind(), io::ErrorKind::TimedOut); + + let _ = writer.write(&[0]).await.unwrap(); + } + + #[tokio::test] + async fn write_ok() { + let writer = DelayIo::new(Instant::now() + Duration::from_millis(100)); + let mut writer = pin!(TimeoutWriter::new(writer).with_timeout(Duration::from_millis(500))); + + let _ = writer.write(&[0]).await.unwrap(); + } + + #[tokio::test] + async fn read_timeout_disabled() { + let reader = DelayIo::new(Instant::now() + Duration::from_millis(20)); + let mut reader = pin!(TimeoutReader::new(reader)); + + let _ = reader.read(&mut [0]).await.unwrap(); + } + + #[tokio::test] + async fn write_timeout_disabled() { + let writer = DelayIo::new(Instant::now() + Duration::from_millis(20)); + let mut writer = pin!(TimeoutWriter::new(writer)); + + let _ = writer.write(&[0]).await.unwrap(); + } + + #[tokio::test] + async fn read_set_timeout_pinned_resets_pending_timer() { + let reader = DelayIo::new(Instant::now() + Duration::from_millis(150)); + let mut reader = pin!(TimeoutReader::new(reader).with_timeout(Duration::from_millis(100))); + + let mut buf = [0]; + + { + let mut pinned_reader = reader.as_mut(); + let mut fut = pin!(pinned_reader.read(&mut buf)); + assert!(fut.as_mut().now_or_never().is_none()); + } + + reader + .as_mut() + .set_timeout_pinned(Some(Duration::from_millis(500))); + + let _ = reader.read(&mut [0]).await.unwrap(); + } + + #[tokio::test] + async fn write_set_timeout_pinned_resets_pending_timer() { + let writer = DelayIo::new(Instant::now() + Duration::from_millis(150)); + let mut writer = pin!(TimeoutWriter::new(writer).with_timeout(Duration::from_millis(100))); + + { + let mut pinned_writer = writer.as_mut(); + let mut fut = pin!(pinned_writer.write(&[0])); + assert!(fut.as_mut().now_or_never().is_none()); + } + + writer + .as_mut() + .set_timeout_pinned(Some(Duration::from_millis(500))); + + let _ = writer.write(&[0]).await.unwrap(); + } + + #[tokio::test] + async fn rw_test() { + let (mut writer, reader) = duplex(16); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_millis(10)).await; + writer.write_all(b"f").await.unwrap(); + tokio::time::sleep(Duration::from_millis(500)).await; + let _ = writer.write_all(b"f").await; // this may hit an eof + }); + + let mut s = pin!(TimeoutIo::new(reader).with_read_timeout(Duration::from_millis(100))); + + let _ = s.read(&mut [0]).await.unwrap(); + let r = s.read(&mut [0]).await; + + match r { + Ok(v) => panic!("unexpected success: value = {v}"), + Err(ref e) if e.kind() == io::ErrorKind::TimedOut => (), + Err(e) => panic!("{e:?}"), + } + } + + #[tokio::test] + async fn timeout_io_write_timeout() { + let io = DelayIo::new(Instant::now() + Duration::from_millis(150)); + let mut io = pin!(TimeoutIo::new(io).with_write_timeout(Duration::from_millis(100))); + + let r = io.write(&[0]).await; + assert_eq!(r.err().unwrap().kind(), io::ErrorKind::TimedOut); + } +} diff --git a/rama-core/src/layer/arc.rs b/rama-core/src/layer/arc.rs new file mode 100644 index 000000000..1ab86963d --- /dev/null +++ b/rama-core/src/layer/arc.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use crate::Layer; + +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +/// [`Layer`] for [`Arc`]ing a [`crate::Service`]. +pub struct ArcLayer; + +impl ArcLayer { + #[inline(always)] + pub fn new() -> Self { + Self + } +} + +impl Layer for ArcLayer { + type Service = Arc; + + #[inline(always)] + fn layer(&self, inner: S) -> Self::Service { + Arc::new(inner) + } +} diff --git a/rama-core/src/layer/consume_err.rs b/rama-core/src/layer/consume_err.rs index d822157b6..130ad39cf 100644 --- a/rama-core/src/layer/consume_err.rs +++ b/rama-core/src/layer/consume_err.rs @@ -2,7 +2,7 @@ use crate::{Layer, Service, error::BoxError}; use rama_utils::macros::define_inner_service_accessors; use std::{convert::Infallible, fmt}; -use sealed::{DefaultOutput, StaticOutput, Trace}; +use sealed::DefaultOutput; /// Consumes this service's error value and returns [`Infallible`]. #[derive(Clone)] @@ -318,21 +318,21 @@ where } } -mod sealed { - #[derive(Debug, Clone)] - /// A sealed new type to prevent downstream users from - /// passing the trace level directly to the [`ConsumeErr::new`] method. - /// - /// [`ConsumeErr::new`]: crate::layer::ConsumeErr::new - pub struct Trace(pub tracing::Level); +#[derive(Debug, Clone)] +/// A sealed new type to prevent downstream users from +/// passing the trace level directly to the [`ConsumeErr::new`] method. +/// +/// [`ConsumeErr::new`]: crate::layer::ConsumeErr::new +pub struct Trace(tracing::Level); + +#[derive(Debug, Clone)] +#[non_exhaustive] +/// A sealed type to indicate static output is to be used. +pub struct StaticOutput(R); +mod sealed { #[derive(Debug, Clone)] #[non_exhaustive] /// A sealed type to indicate default output is to be used. pub struct DefaultOutput; - - #[derive(Debug, Clone)] - #[non_exhaustive] - /// A sealed type to indicate static output is to be used. - pub struct StaticOutput(pub(super) R); } diff --git a/rama-core/src/layer/mod.rs b/rama-core/src/layer/mod.rs index 0f6233533..343640eeb 100644 --- a/rama-core/src/layer/mod.rs +++ b/rama-core/src/layer/mod.rs @@ -926,7 +926,7 @@ mod map_err; #[doc(inline)] pub use map_err::{MapErr, MapErrLayer}; -mod consume_err; +pub mod consume_err; #[doc(inline)] pub use consume_err::{ConsumeErr, ConsumeErrLayer}; @@ -956,6 +956,9 @@ pub use add_extension::{ AddInputExtension, AddInputExtensionLayer, AddOutputExtension, AddOutputExtensionLayer, }; +mod arc; +pub use arc::ArcLayer; + pub mod get_extension; pub use get_extension::{ GetInputExtension, GetInputExtensionLayer, GetOutputExtension, GetOutputExtensionLayer, diff --git a/rama-core/src/lib.rs b/rama-core/src/lib.rs index d2d2c742c..326690a1e 100644 --- a/rama-core/src/lib.rs +++ b/rama-core/src/lib.rs @@ -41,6 +41,7 @@ pub use service::Service; pub mod layer; pub use layer::Layer; +pub mod io; pub mod stream; pub mod combinators; @@ -64,196 +65,4 @@ pub mod bytes { pub use ::bytes::*; } -pub mod futures { - //! Re-export of the [futures](https://docs.rs/futures/latest/futures/) - //! and [asynk-strim](https://docs.rs/asynk-strim/latest/asynk_strim/) crates. - //! - //! Exported for your convenience and because it is so fundamental to rama. - - use pin_project_lite::pin_project; - use std::{ - pin::Pin, - task::{Context, Poll}, - }; - - #[doc(inline)] - pub use ::futures::*; - - #[doc(inline)] - pub use ::asynk_strim as async_stream; - - /// Joins two futures, waiting for both to complete. - /// - /// # Examples - /// - /// ``` - /// use rama_core::futures; - /// - /// # #[tokio::main] - /// # async fn main() { - /// let a = async { 1 }; - /// let b = async { 2 }; - /// - /// assert_eq!(futures::zip(a, b).await, (1, 2)); - /// # } - /// ``` - pub fn zip(future1: F1, future2: F2) -> Zip - where - F1: Future, - F2: Future, - { - Zip { - future1: Some(future1), - future2: Some(future2), - output1: None, - output2: None, - } - } - - pin_project! { - /// Future for the [`zip()`] function. - #[derive(Debug)] - #[must_use = "futures do nothing unless you `.await` or poll them"] - pub struct Zip - where - F1: Future, - F2: Future, - { - #[pin] - future1: Option, - output1: Option, - #[pin] - future2: Option, - output2: Option, - } - - } - - /// Extracts the contents of two options and zips them, handling `(Some(_), None)` cases - fn take_zip_from_parts(o1: &mut Option, o2: &mut Option) -> Poll<(T1, T2)> { - match (o1.take(), o2.take()) { - (Some(t1), Some(t2)) => Poll::Ready((t1, t2)), - (o1x, o2x) => { - *o1 = o1x; - *o2 = o2x; - Poll::Pending - } - } - } - - impl Future for Zip - where - F1: Future, - F2: Future, - { - type Output = (F1::Output, F2::Output); - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.project(); - - if let Some(future) = this.future1.as_mut().as_pin_mut() - && let Poll::Ready(out) = future.poll(cx) - { - *this.output1 = Some(out); - - this.future1.set(None); - } - - if let Some(future) = this.future2.as_mut().as_pin_mut() - && let Poll::Ready(out) = future.poll(cx) - { - *this.output2 = Some(out); - - this.future2.set(None); - } - - take_zip_from_parts(this.output1, this.output2) - } - } - - /// Joins two fallible futures, waiting for both to complete or one of them to error. - /// - /// # Examples - /// - /// ``` - /// use rama_core::futures; - /// - /// # #[tokio::main] - /// # async fn main() { - /// let a = async { Ok::(1) }; - /// let b = async { Err::(2) }; - /// - /// assert_eq!(futures::try_zip(a, b).await, Err(2)); - /// # } - /// ``` - pub fn try_zip(future1: F1, future2: F2) -> TryZip - where - F1: Future>, - F2: Future>, - { - TryZip { - future1: Some(future1), - future2: Some(future2), - output1: None, - output2: None, - } - } - - pin_project! { - /// Future for the [`try_zip()`] function. - #[derive(Debug)] - #[must_use = "futures do nothing unless you `.await` or poll them"] - pub struct TryZip { - #[pin] - future1: Option, - output1: Option, - #[pin] - future2: Option, - output2: Option, - - } - - } - - impl Future for TryZip - where - F1: Future>, - F2: Future>, - { - type Output = Result<(T1, T2), E>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.project(); - - if let Some(future) = this.future1.as_mut().as_pin_mut() - && let Poll::Ready(out) = future.poll(cx) - { - match out { - Ok(t) => { - *this.output1 = Some(t); - - this.future1.set(None); - } - - Err(err) => return Poll::Ready(Err(err)), - } - } - - if let Some(future) = this.future2.as_mut().as_pin_mut() - && let Poll::Ready(out) = future.poll(cx) - { - match out { - Ok(t) => { - *this.output2 = Some(t); - - this.future2.set(None); - } - - Err(err) => return Poll::Ready(Err(err)), - } - } - - take_zip_from_parts(this.output1, this.output2).map(Ok) - } - } -} +pub mod futures; diff --git a/rama-core/src/matcher/mod.rs b/rama-core/src/matcher/mod.rs index 88ac76503..a498e32ce 100644 --- a/rama-core/src/matcher/mod.rs +++ b/rama-core/src/matcher/mod.rs @@ -18,6 +18,8 @@ use crate::extensions::ExtensionsMut; use rama_macros::paste; use rama_utils::macros::all_the_tuples_no_last_special_case; +pub mod service; + mod op_or; #[doc(inline)] pub use op_or::Or; diff --git a/rama-core/src/matcher/service/dynamic_dispatch.rs b/rama-core/src/matcher/service/dynamic_dispatch.rs new file mode 100644 index 000000000..07f82bbe5 --- /dev/null +++ b/rama-core/src/matcher/service/dynamic_dispatch.rs @@ -0,0 +1,146 @@ +use std::{fmt, pin::Pin, sync::Arc}; + +use super::{ServiceMatch, ServiceMatcher}; + +/// Dynamic-dispatch interface for [`ServiceMatcher`]. +/// +/// This is mainly useful behind [`BoxServiceMatcher`], but is public so +/// crates building their own matcher containers can reuse the same pattern. +pub trait DynServiceMatcher: Send + Sync + 'static { + /// The value returned when a match succeeds. + type Service: Send + 'static; + /// The error that can happen while evaluating the matcher. + type Error: Send + 'static; + /// The input after matcher evaluation. + type ModifiedInput: Send + 'static; + + /// Attempt to select a service for `input`. + #[allow(clippy::type_complexity)] + fn match_service_box( + &self, + input: Input, + ) -> Pin< + Box< + dyn Future< + Output = Result, Self::Error>, + > + Send + + '_, + >, + >; +} + +impl DynServiceMatcher for T +where + T: ServiceMatcher, +{ + type Service = T::Service; + type Error = T::Error; + type ModifiedInput = T::ModifiedInput; + + fn match_service_box( + &self, + input: Input, + ) -> Pin< + Box< + dyn Future< + Output = Result, Self::Error>, + > + Send + + '_, + >, + > { + Box::pin(self.match_service(input)) + } +} + +/// A boxed [`ServiceMatcher`]. +/// +/// This gives dynamic dispatch without constraining the selected value. +pub struct BoxServiceMatcher { + inner: Arc< + dyn DynServiceMatcher< + Input, + Service = SelectedService, + Error = Error, + ModifiedInput = ModifiedInput, + > + Send + + Sync + + 'static, + >, +} + +impl Clone + for BoxServiceMatcher +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl + BoxServiceMatcher +{ + /// Create a boxed matcher from a concrete matcher implementation. + #[inline] + pub fn new(matcher: T) -> Self + where + T: ServiceMatcher< + Input, + Service = SelectedService, + Error = Error, + ModifiedInput = ModifiedInput, + >, + { + Self { + inner: Arc::new(matcher), + } + } +} + +impl fmt::Debug + for BoxServiceMatcher +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BoxServiceMatcher").finish() + } +} + +impl ServiceMatcher + for BoxServiceMatcher +where + Input: 'static, + SelectedService: Send + 'static, + Error: Send + 'static, + ModifiedInput: Send + 'static, +{ + type Service = SelectedService; + type Error = Error; + type ModifiedInput = ModifiedInput; + + #[inline] + fn match_service( + &self, + input: Input, + ) -> impl Future, Self::Error>> + + Send + + '_ { + self.inner.match_service_box(input) + } + + async fn into_match_service( + self, + input: Input, + ) -> Result, Self::Error> + where + Self: Sized, + Input: Send, + { + self.inner.match_service_box(input).await + } + + #[inline] + fn boxed(self) -> Self { + self + } +} diff --git a/rama-core/src/matcher/service/match_service_pair.rs b/rama-core/src/matcher/service/match_service_pair.rs new file mode 100644 index 000000000..c24a933e3 --- /dev/null +++ b/rama-core/src/matcher/service/match_service_pair.rs @@ -0,0 +1,82 @@ +use std::convert::Infallible; + +use crate::{ + extensions::{Extensions, ExtensionsMut}, + matcher::Matcher, +}; + +use super::{ServiceMatch, ServiceMatcher}; + +/// Couples a plain [`crate::matcher::Matcher`] with a concrete service. +/// +/// This wrapper exists because `(M, S)` cannot be implemented directly +/// without overlapping the tuple-based `ServiceMatcher` chain impls. +pub struct MatcherServicePair(pub M, pub S); + +impl MatcherServicePair { + /// Create a new matcher-service pair. + #[inline] + pub fn new(matcher: M, service: S) -> Self { + Self(matcher, service) + } +} + +impl From<(M, S)> for MatcherServicePair { + fn from(value: (M, S)) -> Self { + Self(value.0, value.1) + } +} + +impl ServiceMatcher for MatcherServicePair +where + Input: Send + ExtensionsMut + 'static, + S: Send + Sync + Clone + 'static, + M: Matcher, +{ + type Service = S; + type Error = Infallible; + type ModifiedInput = Input; + + async fn match_service( + &self, + mut input: Input, + ) -> Result, Self::Error> { + let Self(matcher, service) = self; + let mut ext = Extensions::new(); + if matcher.matches(Some(&mut ext), &input) { + input.extensions_mut().extend(ext); + Ok(ServiceMatch { + input, + service: Some(service.clone()), + }) + } else { + Ok(ServiceMatch { + input, + service: None, + }) + } + } + + async fn into_match_service( + self, + mut input: Input, + ) -> Result, Self::Error> + where + Input: Send, + { + let Self(matcher, service) = self; + let mut ext = Extensions::new(); + if matcher.matches(Some(&mut ext), &input) { + input.extensions_mut().extend(ext); + Ok(ServiceMatch { + input, + service: Some(service), + }) + } else { + Ok(ServiceMatch { + input, + service: None, + }) + } + } +} diff --git a/rama-core/src/matcher/service/mod.rs b/rama-core/src/matcher/service/mod.rs new file mode 100644 index 000000000..b522837d1 --- /dev/null +++ b/rama-core/src/matcher/service/mod.rs @@ -0,0 +1,96 @@ +use std::sync::Arc; + +mod dynamic_dispatch; +mod match_service_pair; +mod tuples; + +pub use self::{ + dynamic_dispatch::{BoxServiceMatcher, DynServiceMatcher}, + match_service_pair::MatcherServicePair, +}; + +/// The result of attempting to select a service for an input. +/// +/// The original input is always returned so that matcher-added extensions +/// can continue through the pipeline even when no service matched. +#[derive(Debug, Clone)] +pub struct ServiceMatch { + /// The input after matcher evaluation. + pub input: Input, + /// The selected service, if any matcher accepted the input. + pub service: Option, +} + +/// Selects a concrete service for an input. +/// +/// This is useful when the service decision itself depends on runtime input, +/// while still preserving the selected value for later processing. +pub trait ServiceMatcher: Send + Sync + 'static { + /// The value returned when a match succeeds. + type Service: Send + 'static; + /// The error that can happen while evaluating the matcher. + type Error: Send + 'static; + /// Input returned by matching functions, + /// it might be same as the original input but it can also be modified. + type ModifiedInput: Send + 'static; + + /// Attempt to select a service for `input`. + fn match_service( + &self, + input: Input, + ) -> impl Future, Self::Error>> + + Send + + '_; + + /// Attempt to select a service for `input`, consuming the matcher. + /// + /// Override this when the matcher stores services by value and can return + /// them without cloning. + fn into_match_service( + self, + input: Input, + ) -> impl Future, Self::Error>> + Send + where + Self: Sized, + Input: Send, + { + async move { self.match_service(input).await } + } + + /// Box this matcher for dynamic dispatch. + fn boxed(self) -> BoxServiceMatcher + where + Self: Sized, + { + BoxServiceMatcher::new(self) + } +} + +impl ServiceMatcher for Arc +where + M: ServiceMatcher, +{ + type Service = M::Service; + type Error = M::Error; + type ModifiedInput = M::ModifiedInput; + + fn match_service( + &self, + input: Input, + ) -> impl Future, Self::Error>> + + Send + + '_ { + (**self).match_service(input) + } + + async fn into_match_service( + self, + input: Input, + ) -> Result, Self::Error> + where + Self: Sized, + Input: Send, + { + (*self).match_service(input).await + } +} diff --git a/rama-core/src/matcher/service/tuples.rs b/rama-core/src/matcher/service/tuples.rs new file mode 100644 index 000000000..e1878950f --- /dev/null +++ b/rama-core/src/matcher/service/tuples.rs @@ -0,0 +1,172 @@ +use rama_error::{BoxError, ErrorContext}; + +use crate::extensions::ExtensionsMut; + +use super::{ServiceMatch, ServiceMatcher}; + +macro_rules! impl_service_matcher_tuple { + ($either:ident, $first_variant:ident => $first_ty:ident : $first_var:ident, $($variant:ident => $rest_ty:ident : $rest_var:ident),+ $(,)?) => { + impl ServiceMatcher + for ($first_ty, $($rest_ty),+) + where + Input: Send + ExtensionsMut + 'static, + ModifiedInput: Send + 'static, + $first_ty: ServiceMatcher, ModifiedInput = ModifiedInput>, + $( + $rest_ty: ServiceMatcher< + ModifiedInput, + Error: Into, + ModifiedInput = ModifiedInput, + >, + )+ + { + type Service = crate::combinators::$either<$first_ty::Service, $($rest_ty::Service),+>; + type Error = BoxError; + type ModifiedInput = ModifiedInput; + + async fn match_service( + &self, + input: Input, + ) -> Result, Self::Error> { + let ($first_var, $($rest_var),+) = self; + + let ServiceMatch { input, service } = $first_var.match_service(input).await.into_box_error()?; + if let Some(service) = service { + return Ok(ServiceMatch { + input, + service: Some(crate::combinators::$either::$first_variant(service)), + }); + } + + $( + let ServiceMatch { input, service } = $rest_var.match_service(input).await.into_box_error()?; + if let Some(service) = service { + return Ok(ServiceMatch { + input, + service: Some(crate::combinators::$either::$variant(service)), + }); + } + )+ + + Ok(ServiceMatch { + input, + service: None, + }) + } + + async fn into_match_service( + self, + input: Input, + ) -> Result, Self::Error> + where + Input: Send, + { + let ($first_var, $($rest_var),+) = self; + + let ServiceMatch { input, service } = $first_var.into_match_service(input).await.into_box_error()?; + if let Some(service) = service { + return Ok(ServiceMatch { + input, + service: Some(crate::combinators::$either::$first_variant(service)), + }); + } + + $( + let ServiceMatch { input, service } = $rest_var.into_match_service(input).await.into_box_error()?; + if let Some(service) = service { + return Ok(ServiceMatch { + input, + service: Some(crate::combinators::$either::$variant(service)), + }); + } + )+ + + Ok(ServiceMatch { + input, + service: None, + }) + } + } + }; +} + +impl_service_matcher_tuple!(Either, A => SM1: sm1, B => SM2: sm2); +impl_service_matcher_tuple!(Either3, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3); +impl_service_matcher_tuple!(Either4, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3, D => SM4: sm4); +impl_service_matcher_tuple!(Either5, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3, D => SM4: sm4, E => SM5: sm5); +impl_service_matcher_tuple!(Either6, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3, D => SM4: sm4, E => SM5: sm5, F => SM6: sm6); +impl_service_matcher_tuple!(Either7, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3, D => SM4: sm4, E => SM5: sm5, F => SM6: sm6, G => SM7: sm7); +impl_service_matcher_tuple!(Either8, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3, D => SM4: sm4, E => SM5: sm5, F => SM6: sm6, G => SM7: sm7, H => SM8: sm8); +impl_service_matcher_tuple!(Either9, A => SM1: sm1, B => SM2: sm2, C => SM3: sm3, D => SM4: sm4, E => SM5: sm5, F => SM6: sm6, G => SM7: sm7, H => SM8: sm8, I => SM9: sm9); + +macro_rules! impl_service_matcher_either { + ($either:ident, $first:ident $(, $rest:ident)* $(,)?) => { + impl ServiceMatcher + for crate::combinators::$either<$first $(, $rest)*> + where + Input: Send + 'static, + ModifiedInput: Send + 'static, + $first: ServiceMatcher, + $( + $rest: ServiceMatcher< + Input, + ModifiedInput = ModifiedInput, + Error: Into<$first::Error>, + >, + )* + { + type Service = crate::combinators::$either<$first::Service $(, $rest::Service)*>; + type Error = $first::Error; + type ModifiedInput = ModifiedInput; + + async fn match_service( + &self, + input: Input, + ) -> Result, Self::Error> { + match self { + crate::combinators::$either::$first(matcher) => { + matcher.match_service(input).await.map(|sm| ServiceMatch { + input: sm.input, + service: sm.service.map(crate::combinators::$either::$first), + }) + } + $( + crate::combinators::$either::$rest(matcher) => { + matcher.match_service(input).await.map_err(Into::into).map(|sm| ServiceMatch { + input: sm.input, + service: sm.service.map(crate::combinators::$either::$rest), + }) + } + )* + } + } + + async fn into_match_service( + self, + input: Input, + ) -> Result, Self::Error> + where + Input: Send, + { + match self { + crate::combinators::$either::$first(matcher) => { + matcher.into_match_service(input).await.map(|sm| ServiceMatch { + input: sm.input, + service: sm.service.map(crate::combinators::$either::$first), + }) + } + $( + crate::combinators::$either::$rest(matcher) => { + matcher.into_match_service(input).await.map_err(Into::into).map(|sm| ServiceMatch { + input: sm.input, + service: sm.service.map(crate::combinators::$either::$rest), + }) + } + )* + } + } + } + }; +} + +crate::combinators::impl_either!(impl_service_matcher_either); diff --git a/rama-core/src/rt/executor.rs b/rama-core/src/rt/executor.rs index f74d55943..d1ee5df0a 100644 --- a/rama-core/src/rt/executor.rs +++ b/rama-core/src/rt/executor.rs @@ -52,4 +52,10 @@ impl Executor { pub fn guard(&self) -> Option<&ShutdownGuard> { self.guard.as_ref() } + + /// Consume itself as the internal shutdown guard, if any + #[must_use] + pub fn into_guard(self) -> Option { + self.guard + } } diff --git a/rama-core/src/stream/json/stream/mod.rs b/rama-core/src/stream/json/io/mod.rs similarity index 100% rename from rama-core/src/stream/json/stream/mod.rs rename to rama-core/src/stream/json/io/mod.rs diff --git a/rama-core/src/stream/json/stream/read.rs b/rama-core/src/stream/json/io/read.rs similarity index 100% rename from rama-core/src/stream/json/stream/read.rs rename to rama-core/src/stream/json/io/read.rs diff --git a/rama-core/src/stream/json/stream/write.rs b/rama-core/src/stream/json/io/write.rs similarity index 100% rename from rama-core/src/stream/json/stream/write.rs rename to rama-core/src/stream/json/io/write.rs diff --git a/rama-core/src/stream/json/mod.rs b/rama-core/src/stream/json/mod.rs index a2be94367..f1a5d6c2f 100644 --- a/rama-core/src/stream/json/mod.rs +++ b/rama-core/src/stream/json/mod.rs @@ -8,10 +8,10 @@ mod config; pub use config::{EmptyLineHandling, ParseConfig}; mod engine; -mod stream; -pub use stream::read::JsonReadStream; -pub use stream::write::JsonWriteStream; +mod io; +pub use io::read::JsonReadStream; +pub use io::write::JsonWriteStream; mod codec; pub use codec::{JsonDecoder, JsonEncoder}; diff --git a/rama-core/src/stream/mod.rs b/rama-core/src/stream/mod.rs index 0013fc7b2..c953d780c 100644 --- a/rama-core/src/stream/mod.rs +++ b/rama-core/src/stream/mod.rs @@ -1,23 +1,5 @@ -use tokio::io::{AsyncRead, AsyncWrite}; - -mod read; -#[doc(inline)] -pub use read::{ChainReader, HeapReader, StackReader}; - -mod peek; -#[doc(inline)] -pub use peek::PeekStream; - -pub mod rewind; - pub mod json; -/// A stream is a type that implements `AsyncRead`, `AsyncWrite` and `Send`. -/// This is specific to Rama and is directly linked to the supertraits of `Tokio`. -pub trait Stream: AsyncRead + AsyncWrite + Send + 'static {} - -impl Stream for T where T: AsyncRead + AsyncWrite + Send + 'static {} - pub mod codec { //! Adaptors from `AsyncRead`/`AsyncWrite` to Stream/Sink //! diff --git a/rama-dns/Cargo.toml b/rama-dns/Cargo.toml index 746d5c34c..06aad7433 100644 --- a/rama-dns/Cargo.toml +++ b/rama-dns/Cargo.toml @@ -23,6 +23,7 @@ default = [] [dependencies] ahash = { workspace = true } hickory-resolver = { workspace = true } +pin-project-lite = { workspace = true } rama-core = { workspace = true } rama-net = { workspace = true } rama-utils = { workspace = true } diff --git a/rama-dns/README.md b/rama-dns/README.md index 53395abf0..33c9dfdf5 100644 --- a/rama-dns/README.md +++ b/rama-dns/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-dns/src/client/resolver/address/happy_eyeball.rs b/rama-dns/src/client/resolver/address/happy_eyeball.rs new file mode 100644 index 000000000..07f1f7916 --- /dev/null +++ b/rama-dns/src/client/resolver/address/happy_eyeball.rs @@ -0,0 +1,218 @@ +use std::{ + net::IpAddr, + pin::Pin, + task::{self, Poll}, + time::Duration, +}; + +use pin_project_lite::pin_project; +use rama_core::{ + error::{BoxError, ErrorExt, extra::OpaqueError}, + extensions::Extensions, + futures::{DelayStream, Stream, stream}, + stream::{StreamExt as _, adapters::Merge}, +}; +use rama_net::{ + address::Host, + mode::{ConnectIpMode, DnsResolveIpMode}, +}; + +use super::DnsAddresssResolverOverwrite; +use crate::client::resolver::DnsAddressResolver; + +/// Extension trait to easily stream IP lookups using the Happy Eyeball algorithm +pub trait HappyEyeballAddressResolverExt: private::HappyEyeballAddressResolverExtSeal { + /// Build a happy eyeballs address resolver using + /// a reference to the current address resolver. + fn happy_eyeballs_resolver( + &self, + host: impl Into, + ) -> HappyEyeballAddressResolver<'_, Self>; +} + +impl HappyEyeballAddressResolverExt for R { + fn happy_eyeballs_resolver( + &self, + host: impl Into, + ) -> HappyEyeballAddressResolver<'_, Self> { + HappyEyeballAddressResolver { + host: host.into(), + resolver: self, + extensions: None, + } + } +} + +mod private { + pub trait HappyEyeballAddressResolverExtSeal: + crate::client::resolver::DnsAddressResolver + { + } + impl HappyEyeballAddressResolverExtSeal for R {} +} + +/// Happy eyeball address resolver, respecting the IP preferences and DNS modes. +pub struct HappyEyeballAddressResolver<'a, R> { + host: Host, + resolver: &'a R, + extensions: Option<&'a Extensions>, +} + +impl<'a, R> HappyEyeballAddressResolver<'a, R> { + rama_utils::macros::generate_set_and_with! { + pub fn extensions(mut self, extensions: Option<&'a Extensions>) -> Self { + self.extensions = extensions; + self + } + } +} + +impl<'a, R: crate::client::resolver::DnsAddressResolver> HappyEyeballAddressResolver<'a, R> { + pub fn lookup_ip(self) -> impl Stream> + Send + 'a { + let ip_mode = self + .extensions + .as_ref() + .and_then(|ext| ext.get().copied()) + .unwrap_or_default(); + let dns_mode = self + .extensions + .as_ref() + .and_then(|ext| ext.get().copied()) + .unwrap_or_default(); + + let domain = match self.host { + Host::Name(domain) => domain, + Host::Address(ip) => { + //check if IP Version is allowed + return HappyEyeballIpStream::Once { + stream: rama_core::stream::once(match (ip, ip_mode) { + (IpAddr::V4(_), ConnectIpMode::Ipv6) => { + Err(BoxError::from("IPv4 address is not allowed").into_opaque_error()) + } + (IpAddr::V6(_), ConnectIpMode::Ipv4) => { + Err(BoxError::from("IPv6 address is not allowed").into_opaque_error()) + } + _ => { + // if the host is already defined as an allowed IP address + // we can directly connect to it + Ok(ip) + } + }), + }; + } + }; + + let maybe_dns_overwrite = self + .extensions + .as_ref() + .and_then(|ext| ext.get::()); + + let make_ipv4_stream = || { + stream::StreamExt::flatten(stream::iter( + maybe_dns_overwrite + .as_ref() + .map(|resolver| resolver.lookup_ipv4(domain.clone())), + )) + .chain( + self.resolver + .lookup_ipv4(domain.clone()) + .map(|result| result.map_err(ErrorExt::into_opaque_error)), + ) + .map(|result| result.map(IpAddr::V4)) + }; + + let make_ipv6_stream = || { + stream::StreamExt::flatten(stream::iter( + maybe_dns_overwrite + .as_ref() + .map(|resolver| resolver.lookup_ipv6(domain.clone())), + )) + .chain( + self.resolver + .lookup_ipv6(domain.clone()) + .map(|result| result.map_err(ErrorExt::into_opaque_error)), + ) + .map(|result| result.map(IpAddr::V6)) + }; + + match dns_mode { + DnsResolveIpMode::Dual => { + let ipv6_stream = make_ipv6_stream(); + let ipv4_stream = make_ipv4_stream(); + + HappyEyeballIpStream::Dual { + stream: ipv6_stream + .merge(DelayStream::new(Duration::from_micros(42), ipv4_stream)), + } + } + DnsResolveIpMode::DualPreferIpV4 => { + let ipv4_stream = make_ipv4_stream(); + let ipv6_stream = make_ipv6_stream(); + + HappyEyeballIpStream::DualPreferIpV4 { + stream: ipv4_stream + .merge(DelayStream::new(Duration::from_micros(42), ipv6_stream)), + } + } + DnsResolveIpMode::SingleIpV4 => HappyEyeballIpStream::SingleIpV4 { + stream: make_ipv4_stream(), + }, + DnsResolveIpMode::SingleIpV6 => HappyEyeballIpStream::SingleIpV6 { + stream: make_ipv6_stream(), + }, + } + } +} + +pin_project! { + #[project = HappyEyeballIpStreamProj] + enum HappyEyeballIpStream { + Dual { + #[pin] + stream: Merge>, + }, + DualPreferIpV4 { + #[pin] + stream: Merge>, + }, + Once { + #[pin] + stream: rama_core::stream::Once>, + }, + SingleIpV4 { + #[pin] + stream: V4, + }, + SingleIpV6 { + #[pin] + stream: V6, + } + } +} + +impl>, V6: Stream>> + Stream for HappyEyeballIpStream +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + match self.project() { + HappyEyeballIpStreamProj::Dual { stream } => stream.poll_next(cx), + HappyEyeballIpStreamProj::DualPreferIpV4 { stream } => stream.poll_next(cx), + HappyEyeballIpStreamProj::Once { stream } => stream.poll_next(cx), + HappyEyeballIpStreamProj::SingleIpV4 { stream } => stream.poll_next(cx), + HappyEyeballIpStreamProj::SingleIpV6 { stream } => stream.poll_next(cx), + } + } + + #[inline(always)] + fn size_hint(&self) -> (usize, Option) { + match self { + Self::Dual { stream } => stream.size_hint(), + Self::DualPreferIpV4 { stream } => stream.size_hint(), + Self::Once { stream } => stream.size_hint(), + Self::SingleIpV4 { stream } => stream.size_hint(), + Self::SingleIpV6 { stream } => stream.size_hint(), + } + } +} diff --git a/rama-dns/src/client/resolver/address/mod.rs b/rama-dns/src/client/resolver/address/mod.rs index aedcd9bc0..23b1d28b4 100644 --- a/rama-dns/src/client/resolver/address/mod.rs +++ b/rama-dns/src/client/resolver/address/mod.rs @@ -17,6 +17,10 @@ use tokio::time::Instant; use crate::client::EmptyDnsResolver; +mod happy_eyeball; +#[doc(inline)] +pub use self::happy_eyeball::{HappyEyeballAddressResolver, HappyEyeballAddressResolverExt}; + mod overwrite; #[doc(inline)] pub use self::overwrite::DnsAddresssResolverOverwrite; diff --git a/rama-dns/src/client/resolver/mod.rs b/rama-dns/src/client/resolver/mod.rs index 3d301c107..afe7904ed 100644 --- a/rama-dns/src/client/resolver/mod.rs +++ b/rama-dns/src/client/resolver/mod.rs @@ -9,7 +9,10 @@ use rama_core::error::extra::OpaqueError; use rama_core::futures::{FutureExt as _, Stream, TryStreamExt as _}; use rama_net::address::Domain; -pub use self::address::{BoxDnsAddressResolver, DnsAddressResolver, DnsAddresssResolverOverwrite}; +pub use self::address::{ + BoxDnsAddressResolver, DnsAddressResolver, DnsAddresssResolverOverwrite, + HappyEyeballAddressResolver, HappyEyeballAddressResolverExt, +}; mod txt; pub use self::txt::{BoxDnsTxtResolver, DnsTxtResolver}; diff --git a/rama-error/README.md b/rama-error/README.md index c7dba7928..8e7c84c0f 100644 --- a/rama-error/README.md +++ b/rama-error/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-grpc-build/README.md b/rama-grpc-build/README.md index 7c3014881..bad6debbb 100644 --- a/rama-grpc-build/README.md +++ b/rama-grpc-build/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-grpc/README.md b/rama-grpc/README.md index 202b39f92..3f49bcce6 100644 --- a/rama-grpc/README.md +++ b/rama-grpc/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-haproxy/README.md b/rama-haproxy/README.md index 4eb04b393..7e4b40822 100644 --- a/rama-haproxy/README.md +++ b/rama-haproxy/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-haproxy/src/client/layer.rs b/rama-haproxy/src/client/layer.rs index f8e195628..a4337fea9 100644 --- a/rama-haproxy/src/client/layer.rs +++ b/rama-haproxy/src/client/layer.rs @@ -6,7 +6,7 @@ use rama_core::{ bytes::Bytes, error::{BoxError, ErrorContext}, extensions::{ChainableExtensions, ExtensionsRef}, - stream::Stream, + io::Io, }; use rama_net::{ client::{ConnectorService, EstablishedClientConnection}, @@ -205,7 +205,7 @@ impl Clone for HaProxyService { impl Service for HaProxyService where - S: ConnectorService, + S: ConnectorService, P: Send + 'static, Input: Send + ExtensionsRef + 'static, { @@ -250,7 +250,7 @@ where impl Service for HaProxyService where - S: ConnectorService, + S: ConnectorService, P: protocol::Protocol + Send + 'static, Input: Send + ExtensionsRef + 'static, { diff --git a/rama-haproxy/src/server/layer.rs b/rama-haproxy/src/server/layer.rs index fb073bc3d..e1461ed66 100644 --- a/rama-haproxy/src/server/layer.rs +++ b/rama-haproxy/src/server/layer.rs @@ -3,7 +3,7 @@ use rama_core::{ Layer, Service, error::{BoxError, ErrorContext, ErrorExt}, extensions::ExtensionsMut, - stream::{HeapReader, PeekStream, Stream}, + io::{HeapReader, Io, PrefixedIo}, telemetry::tracing, }; use rama_net::forwarded::{Forwarded, ForwardedElement}; @@ -80,8 +80,8 @@ impl HaProxyService { impl Service for HaProxyService where - S: Service, Error: Into>, - IO: Stream + Unpin + ExtensionsMut, + S: Service, Error: Into>, + IO: Io + Unpin + ExtensionsMut, { type Output = S::Output; type Error = BoxError; @@ -121,7 +121,7 @@ where ); let mem = HeapReader::new(peek_buf[..n].into()); - let stream = PeekStream::new(mem, stream); + let stream = PrefixedIo::new(mem, stream); return self.inner.serve(stream).await.into_box_error(); } } else { @@ -225,7 +225,7 @@ where // put back the data that is read too much let mem: HeapReader = buffer[consumed..read].into(); - let stream = PeekStream::new(mem, stream); + let stream = PrefixedIo::new(mem, stream); // read the rest of the data self.inner.serve(stream).await.into_box_error() @@ -238,7 +238,7 @@ mod test { use super::*; - async fn echo(mut stream: impl Stream + Unpin) -> Result, BoxError> { + async fn echo(mut stream: impl Io + Unpin) -> Result, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; Ok(v) diff --git a/rama-http-backend/Cargo.toml b/rama-http-backend/Cargo.toml index f0f9f7557..08993afd1 100644 --- a/rama-http-backend/Cargo.toml +++ b/rama-http-backend/Cargo.toml @@ -37,6 +37,7 @@ rama-net = { workspace = true, features = ["http"] } rama-tcp = { workspace = true, features = ["http"] } rama-utils = { workspace = true } tokio = { workspace = true, features = ["macros"] } +tokio-util = { workspace = true } [target.'cfg(target_family = "unix")'.dependencies] rama-unix = { workspace = true } diff --git a/rama-http-backend/README.md b/rama-http-backend/README.md index 81341a609..78977498e 100644 --- a/rama-http-backend/README.md +++ b/rama-http-backend/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-http-backend/src/client/conn.rs b/rama-http-backend/src/client/conn.rs index 3234c8005..a8bc2abe8 100644 --- a/rama-http-backend/src/client/conn.rs +++ b/rama-http-backend/src/client/conn.rs @@ -3,8 +3,8 @@ use rama_core::{ Layer, Service, error::{BoxError, ErrorContext, ErrorExt as _, extra::OpaqueError}, extensions::{ExtensionsMut, ExtensionsRef}, + io::Io, rt::Executor, - stream::Stream, }; use rama_http::{ StreamingBody, @@ -52,186 +52,199 @@ impl HttpConnector { define_inner_service_accessors!(); } -impl Service> for HttpConnector +/// Establish an HTTP connection on the pre-established IO (bytes) stream +/// with the given http request as context for the initial setup. +pub async fn http_connect( + mut io: IO, + req: Request, + exec: Executor, +) -> Result< + EstablishedClientConnection, Request>, + OpaqueError, +> where - S: ConnectorService, Connection: Stream + Unpin>, + IO: Io + Unpin + ExtensionsMut, BodyIn: StreamingBody> + Unpin + Send + 'static, // Body type this connector will be able to send, this is not necessarily the same one that // was used in the request that created this connection BodyConnection: StreamingBody> + Unpin + Send + 'static, { - type Output = EstablishedClientConnection, Request>; - type Error = OpaqueError; + // TODO this is way to tricky, this needs to be here on the io extensions + // Not the ones we clone, ideally the exentions should all just use the same store + // We can solve this by making them clonable + io.extensions_mut().get_or_insert(ConnectionHealth::default); - async fn serve(&self, req: Request) -> Result { - let EstablishedClientConnection { - input: req, - mut conn, - } = self - .inner - .connect(req) - .await - .map_err(Into::into) - .into_opaque_error()?; + let extensions = io.extensions().clone(); - // TODO this is way to tricky, this needs to be here on the io extensions - // Not the ones we clone, ideally the exentions should all just use the same store - // We can solve this by making them clonable - conn.extensions_mut() - .get_or_insert(ConnectionHealth::default); + let server_address = req + .extensions() + .get::() + .map(|ctx| ctx.authority.host.to_str()) + .or_else(|| req.uri().host().map(Into::into)) + .or_else(|| { + req.headers() + .get(HOST) + .and_then(|v| v.to_str().ok()) + .map(Into::into) + }) + .unwrap_or_default(); - let extensions = conn.extensions().clone(); - - let server_address = req - .extensions() - .get::() - .map(|ctx| ctx.authority.host.to_str()) - .or_else(|| req.uri().host().map(Into::into)) - .or_else(|| { - req.headers() - .get(HOST) - .and_then(|v| v.to_str().ok()) - .map(Into::into) - }) - .unwrap_or_default(); + match req.version() { + Version::HTTP_2 => { + tracing::trace!(url.full = %req.uri(), "create h2 client executor"); - let io = Box::pin(conn); + let mut builder = rama_http_core::client::conn::http2::Builder::new(exec.clone()); - match req.version() { - Version::HTTP_2 => { - tracing::trace!(url.full = %req.uri(), "create h2 client executor"); - - let mut builder = - rama_http_core::client::conn::http2::Builder::new(self.exec.clone()); + if req.extensions().get::().is_some() { + // e.g. used for h2 bootstrap support for WebSocket + builder.set_enable_connect_protocol(1); + } - if req.extensions().get::().is_some() { - // e.g. used for h2 bootstrap support for WebSocket - builder.set_enable_connect_protocol(1); + if let Some(params) = req + .extensions() + .get::() + .or_else(|| req.extensions().get()) + { + if let Some(order) = params.headers_pseudo_order.clone() { + builder.set_headers_pseudo_order(order); } - - if let Some(params) = req - .extensions() - .get::() - .or_else(|| req.extensions().get()) - { - if let Some(order) = params.headers_pseudo_order.clone() { - builder.set_headers_pseudo_order(order); - } - if let Some(ref frames) = params.early_frames { - let v = frames.as_slice().to_vec(); - builder.set_early_frames(v); - } - if let Some(sz) = params.init_stream_window_size { - builder.set_initial_stream_window_size(sz); - } - if let Some(sz) = params.init_connection_window_size { - builder.set_initial_connection_window_size(sz); - } - if let Some(d) = params.keep_alive_interval { - builder.set_keep_alive_interval(d); - } - if let Some(d) = params.keep_alive_timeout { - builder.set_keep_alive_timeout(d); - } - if let Some(keep_alive) = params.keep_alive_while_idle { - builder.set_keep_alive_while_idle(keep_alive); - } - if let Some(sz) = params.max_header_list_size { - builder.set_max_header_list_size(sz); - } - if let Some(adaptive_window) = params.adaptive_window { - builder.set_adaptive_window(adaptive_window); - } - } else if let Some(pseudo_order) = - req.extensions().get::().cloned() - { - builder.set_headers_pseudo_order(pseudo_order); + if let Some(ref frames) = params.early_frames { + let v = frames.as_slice().to_vec(); + builder.set_early_frames(v); + } + if let Some(sz) = params.init_stream_window_size { + builder.set_initial_stream_window_size(sz); + } + if let Some(sz) = params.init_connection_window_size { + builder.set_initial_connection_window_size(sz); + } + if let Some(d) = params.keep_alive_interval { + builder.set_keep_alive_interval(d); + } + if let Some(d) = params.keep_alive_timeout { + builder.set_keep_alive_timeout(d); + } + if let Some(keep_alive) = params.keep_alive_while_idle { + builder.set_keep_alive_while_idle(keep_alive); + } + if let Some(sz) = params.max_header_list_size { + builder.set_max_header_list_size(sz); } + if let Some(adaptive_window) = params.adaptive_window { + builder.set_adaptive_window(adaptive_window); + } + } else if let Some(pseudo_order) = req.extensions().get::().cloned() + { + builder.set_headers_pseudo_order(pseudo_order); + } - let (sender, conn) = builder.handshake(io).await.into_opaque_error()?; + let (sender, conn) = builder.handshake(io).await.into_opaque_error()?; - let conn_span = tracing::trace_root_span!( - "h2::conn::serve", - otel.kind = "client", - http.request.method = %req.method().as_str(), - url.full = %req.uri(), - url.path = %req.uri().path(), - url.query = req.uri().query().unwrap_or_default(), - url.scheme = %req.uri().scheme().map(|s| s.as_str()).unwrap_or_default(), - network.protocol.name = "http", - network.protocol.version = version_as_protocol_version(req.version()), - user_agent.original = %req.headers().get(USER_AGENT).and_then(|v| v.to_str().ok()).unwrap_or_default(), - server.address = %server_address, - server.service.name = %server_address, - ); + let conn_span = tracing::trace_root_span!( + "h2::conn::serve", + otel.kind = "client", + http.request.method = %req.method().as_str(), + url.full = %req.uri(), + url.path = %req.uri().path(), + url.query = req.uri().query().unwrap_or_default(), + url.scheme = %req.uri().scheme().map(|s| s.as_str()).unwrap_or_default(), + network.protocol.name = "http", + network.protocol.version = version_as_protocol_version(req.version()), + user_agent.original = %req.headers().get(USER_AGENT).and_then(|v| v.to_str().ok()).unwrap_or_default(), + server.address = %server_address, + server.service.name = %server_address, + ); - self.exec.spawn_task( - async move { - if let Err(err) = conn.await { - tracing::debug!("connection failed: {err:?}"); - } + exec.into_spawn_task( + async move { + if let Err(err) = conn.await { + tracing::debug!("connection failed: {err:?}"); } - .instrument(conn_span), - ); + } + .instrument(conn_span), + ); - let svc = HttpClientService { - sender: SendRequest::Http2(sender), - extensions, - }; + let svc = HttpClientService { + sender: SendRequest::Http2(sender), + extensions, + }; - Ok(EstablishedClientConnection { - input: req, - conn: svc, - }) + Ok(EstablishedClientConnection { + input: req, + conn: svc, + }) + } + Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09 => { + tracing::trace!(url.full = %req.uri(), "create ~h1 client executor"); + let mut builder = rama_http_core::client::conn::http1::Builder::new(); + if let Some(params) = req.extensions().get::() { + builder.set_title_case_headers(params.title_header_case); } - Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09 => { - tracing::trace!(url.full = %req.uri(), "create ~h1 client executor"); - let mut builder = rama_http_core::client::conn::http1::Builder::new(); - if let Some(params) = req.extensions().get::() { - builder.set_title_case_headers(params.title_header_case); - } - let (sender, conn) = builder.handshake(io).await.into_opaque_error()?; - let conn = conn.with_upgrades(); + let (sender, conn) = builder.handshake(io).await.into_opaque_error()?; + let conn = conn.with_upgrades(); - let conn_span = tracing::trace_root_span!( - "h1::conn::serve", - otel.kind = "client", - http.request.method = %req.method().as_str(), - url.full = %req.uri(), - url.path = %req.uri().path(), - url.query = req.uri().query().unwrap_or_default(), - url.scheme = %req.uri().scheme().map(|s| s.as_str()).unwrap_or_default(), - network.protocol.name = "http", - network.protocol.version = version_as_protocol_version(req.version()), - user_agent.original = %req.headers().get(USER_AGENT).and_then(|v| v.to_str().ok()).unwrap_or_default(), - server.address = %server_address, - server.service.name = %server_address, - ); + let conn_span = tracing::trace_root_span!( + "h1::conn::serve", + otel.kind = "client", + http.request.method = %req.method().as_str(), + url.full = %req.uri(), + url.path = %req.uri().path(), + url.query = req.uri().query().unwrap_or_default(), + url.scheme = %req.uri().scheme().map(|s| s.as_str()).unwrap_or_default(), + network.protocol.name = "http", + network.protocol.version = version_as_protocol_version(req.version()), + user_agent.original = %req.headers().get(USER_AGENT).and_then(|v| v.to_str().ok()).unwrap_or_default(), + server.address = %server_address, + server.service.name = %server_address, + ); - self.exec.spawn_task( - async move { - if let Err(err) = conn.await { - tracing::debug!("connection failed: {err:?}"); - } + exec.into_spawn_task( + async move { + if let Err(err) = conn.await { + tracing::debug!("connection failed: {err:?}"); } - .instrument(conn_span), - ); + } + .instrument(conn_span), + ); - let svc = HttpClientService { - sender: SendRequest::Http1(Mutex::new(sender)), - extensions, - }; + let svc = HttpClientService { + sender: SendRequest::Http1(Mutex::new(sender)), + extensions, + }; - Ok(EstablishedClientConnection { - input: req, - conn: svc, - }) - } - version => Err(BoxError::from("unsupported Http version") - .context_debug_field("version", version) - .into_opaque_error()), + Ok(EstablishedClientConnection { + input: req, + conn: svc, + }) } + version => Err(BoxError::from("unsupported Http version") + .context_debug_field("version", version) + .into_opaque_error()), + } +} + +impl Service> for HttpConnector +where + S: ConnectorService, Connection: Io + Unpin>, + BodyIn: StreamingBody> + Unpin + Send + 'static, + // Body type this connector will be able to send, this is not necessarily the same one that + // was used in the request that created this connection + BodyConnection: + StreamingBody> + Unpin + Send + 'static, +{ + type Output = EstablishedClientConnection, Request>; + type Error = OpaqueError; + + #[inline] + async fn serve(&self, req: Request) -> Result { + let EstablishedClientConnection { input: req, conn } = self + .inner + .connect(req) + .await + .map_err(Into::into) + .into_opaque_error()?; + http_connect(conn, req, self.exec.clone()).await } } diff --git a/rama-http-backend/src/client/mod.rs b/rama-http-backend/src/client/mod.rs index 8024d4ff7..c5aaad3f9 100644 --- a/rama-http-backend/src/client/mod.rs +++ b/rama-http-backend/src/client/mod.rs @@ -6,6 +6,6 @@ pub use svc::HttpClientService; mod conn; #[doc(inline)] -pub use conn::{HttpConnector, HttpConnectorLayer}; +pub use conn::{HttpConnector, HttpConnectorLayer, http_connect}; pub mod proxy; diff --git a/rama-http-backend/src/client/proxy/layer/proxy_connector/connector.rs b/rama-http-backend/src/client/proxy/layer/proxy_connector/connector.rs index c06efade9..6e3eb3ae6 100644 --- a/rama-http-backend/src/client/proxy/layer/proxy_connector/connector.rs +++ b/rama-http-backend/src/client/proxy/layer/proxy_connector/connector.rs @@ -7,8 +7,8 @@ use std::fmt::Debug; use super::HttpProxyError; use rama_core::error::{BoxError, ErrorContext}; use rama_core::extensions::ExtensionsMut; +use rama_core::io::Io; use rama_core::rt::Executor; -use rama_core::stream::Stream; use rama_core::telemetry::tracing; use rama_http::HeaderMap; use rama_http::io::upgrade; @@ -79,7 +79,7 @@ impl InnerHttpProxyConnector { } /// Connect to the proxy server. - pub(super) async fn handshake( + pub(super) async fn handshake( self, stream: S, ) -> Result<(HeaderMap, upgrade::Upgraded), HttpProxyError> { @@ -109,7 +109,7 @@ impl InnerHttpProxyConnector { } } - async fn handshake_h1( + async fn handshake_h1( req: Request, stream: S, ) -> Result, HttpProxyError> { @@ -130,7 +130,7 @@ impl InnerHttpProxyConnector { .map_err(|err| HttpProxyError::Transport(BoxError::from(err))) } - async fn handshake_h2( + async fn handshake_h2( req: Request, stream: S, ) -> Result, HttpProxyError> { diff --git a/rama-http-backend/src/client/proxy/layer/proxy_connector/service.rs b/rama-http-backend/src/client/proxy/layer/proxy_connector/service.rs index 5bb3fde12..9fa90472c 100644 --- a/rama-http-backend/src/client/proxy/layer/proxy_connector/service.rs +++ b/rama-http-backend/src/client/proxy/layer/proxy_connector/service.rs @@ -6,7 +6,7 @@ use rama_core::{ Service, error::{BoxError, ErrorContext as _, ErrorExt}, extensions::{Extensions, ExtensionsMut, ExtensionsRef}, - stream::Stream, + io::Io, telemetry::tracing, }; use rama_http::{ @@ -103,7 +103,7 @@ impl HttpProxyConnector { impl Service for HttpProxyConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + Send + ExtensionsMut @@ -147,7 +147,7 @@ where "http proxy connector: preparing proxy connection for tls tunnel", ); input.extensions_mut().insert(TlsTunnel { - server_host: proxy_info.address.host.clone(), + sni: Some(proxy_info.address.host.clone()), }); } @@ -289,7 +289,7 @@ pin_project! { } } -impl MaybeHttpProxiedConnection { +impl MaybeHttpProxiedConnection { fn direct(conn: S) -> Self { Self { inner: Connection::Direct { conn }, diff --git a/rama-http-backend/src/lib.rs b/rama-http-backend/src/lib.rs index e3e006699..803fb4cb9 100644 --- a/rama-http-backend/src/lib.rs +++ b/rama-http-backend/src/lib.rs @@ -20,94 +20,8 @@ )] pub mod client; +pub mod proxy; pub mod server; #[cfg(test)] -mod tests { - use std::{ - convert::Infallible, - time::{Duration, Instant}, - }; - - use tokio::time::sleep; - - use rama_core::Layer as _; - use rama_core::futures::future::join; - use rama_core::{Service, rt::Executor, service::service_fn}; - use rama_http_types::{Body, Request, Response, Version}; - use rama_net::test_utils::client::MockConnectorService; - - use super::{client::HttpConnectorLayer, server::HttpServer}; - - #[tokio::test] - async fn test_http11_pipelining() { - let connector = HttpConnectorLayer::default().into_layer(MockConnectorService::new(|| { - HttpServer::auto(Executor::default()).service(service_fn(server_svc_fn)) - })); - - let conn = connector - .serve(create_test_request(Version::HTTP_11)) - .await - .unwrap() - .conn; - - // Http 1.1 should pipeline requests. Pipelining is important when trying to send multiple - // requests on the same connection. This is something we generally don't do, but we do - // trigger the same problem when we re-use a connection too fast. However triggering that - // bug consistently has proven very hard so we trigger this one instead. Both of them - // should be fixed by waiting for conn.isready().await before trying to send data on the connection. - // For http1.1 this will result in pipelining (http2 will still be multiplexed) - let start = Instant::now(); - let (res1, res2) = join( - conn.serve(create_test_request(Version::HTTP_11)), - conn.serve(create_test_request(Version::HTTP_11)), - ) - .await; - let duration = start.elapsed(); - - res1.unwrap(); - res2.unwrap(); - - assert!(duration > Duration::from_millis(200)); - } - - #[tokio::test] - async fn test_http2_multiplex() { - let connector = HttpConnectorLayer::default().into_layer(MockConnectorService::new(|| { - HttpServer::auto(Executor::default()).service(service_fn(server_svc_fn)) - })); - - let conn = connector - .serve(create_test_request(Version::HTTP_2)) - .await - .unwrap() - .conn; - - // We have an artificial sleep of 100ms, so multiplexing should be < 200ms - let start = Instant::now(); - let (res1, res2) = join( - conn.serve(create_test_request(Version::HTTP_2)), - conn.serve(create_test_request(Version::HTTP_2)), - ) - .await; - - let duration = start.elapsed(); - res1.unwrap(); - res2.unwrap(); - - assert!(duration < Duration::from_millis(200)); - } - - async fn server_svc_fn(_req: Request) -> Result { - sleep(Duration::from_millis(100)).await; - Ok(Response::new(Body::from("a random response body"))) - } - - fn create_test_request(version: Version) -> Request { - Request::builder() - .uri("https://www.example.com") - .version(version) - .body(Body::from("a reandom request body")) - .unwrap() - } -} +mod tests; diff --git a/rama-http-backend/src/proxy/mitm.rs b/rama-http-backend/src/proxy/mitm.rs new file mode 100644 index 000000000..6d998ae1c --- /dev/null +++ b/rama-http-backend/src/proxy/mitm.rs @@ -0,0 +1,307 @@ +use std::convert::Infallible; + +use rama_core::{ + Layer, Service, + error::{BoxError, ErrorContext as _}, + extensions::ExtensionsMut, + futures::{GracefulStream, StreamExt}, + graceful::{Shutdown, ShutdownGuard}, + io::{BridgeIo, Io}, + layer::{ + ArcLayer, ConsumeErrLayer, + consume_err::{StaticOutput, Trace}, + }, + rt::Executor, + service::service_fn, + stream::wrappers::ReceiverStream, + telemetry::tracing, +}; +use rama_http::{ + Body, HeaderName, HeaderValue, Request, Response, StatusCode, + service::web::response::IntoResponse, +}; +use rama_http_core::server::conn::auto::Builder as AutoConnBuilder; +use rama_http_core::server::conn::http1::Builder as Http1ConnBuilder; +use rama_http_core::server::conn::http2::Builder as H2ConnBuilder; +use rama_net::client::EstablishedClientConnection; +use rama_utils::macros::generate_set_and_with; + +use tokio::sync::{mpsc, oneshot}; +use tokio_util::sync::CancellationToken; + +use crate::{ + client::{HttpClientService, http_connect}, + server::HttpServer, +}; + +#[derive(Debug, Clone, Copy, Default)] +/// Default [`Response`] used in case the inner (egress) +/// client of the [`HttpMitmRelay`] is erroring. +pub struct DefaultErrorResponse; + +impl DefaultErrorResponse { + #[inline(always)] + pub fn new() -> Self { + Self + } + + #[inline(always)] + fn response() -> Response { + ( + [ + ( + HeaderName::from_static("x-proxy-framework-name"), + HeaderValue::from_static(rama_utils::info::NAME), + ), + ( + HeaderName::from_static("x-proxy-framework-version"), + HeaderValue::from_static(rama_utils::info::VERSION), + ), + ], + StatusCode::BAD_GATEWAY, + ) + .into_response() + } +} + +impl From for Response { + #[inline(always)] + fn from(_: DefaultErrorResponse) -> Self { + DefaultErrorResponse::response() + } +} + +/// Default middleware used by [`HttpMitmRelay`], +/// most likely you'll want to overwrite it with custom middleware, +/// unless you do not require MITM middleware. +pub type DefaultMiddleware = ( + ConsumeErrLayer>, + ArcLayer, +); + +#[derive(Debug, Clone)] +/// A utility that can be used by MITM services such as transparent proxies, +/// in order to relay HTTP requests and responses between a client and server, +/// as part of a deep protocol inspection protocol (DPI) flow. +/// +/// Useful if you have a fairly standard MITM http flow and already +/// have pre-established ingress and egress connections (e.g. because +/// you already MITM'd the { + http_server: HttpServer, + relay_buffer: Option, + middleware: M, + exec: Executor, +} + +impl HttpMitmRelay { + #[inline(always)] + #[must_use] + /// Create a new [`HttpMitmRelay`], ready to serve. + pub fn new(exec: Executor) -> Self { + Self { + http_server: HttpServer::auto(exec.clone()), + relay_buffer: None, + middleware: ( + ConsumeErrLayer::trace_as_debug().with_response(DefaultErrorResponse), + ArcLayer::new(), + ), + exec, + } + } + + /// Set HTTP middleware to use between server and client. + /// + /// By default the identity middleware `()` is used, + /// which preserves the requests and responses as is... + pub fn with_http_middleware(self, middleware: M) -> HttpMitmRelay { + HttpMitmRelay { + http_server: self.http_server, + relay_buffer: self.relay_buffer, + middleware, + exec: self.exec, + } + } +} + +impl HttpMitmRelay { + #[inline(always)] + /// Http1 builder. + pub fn http1(&self) -> &Http1ConnBuilder { + self.http_server.http1() + } + + #[inline(always)] + /// Http1 mutable builder. + pub fn http1_mut(&mut self) -> &mut Http1ConnBuilder { + self.http_server.http1_mut() + } + + #[inline(always)] + /// H2 builder. + pub fn h2(&self) -> &H2ConnBuilder { + self.http_server.h2() + } + + #[inline(always)] + /// H2 mutable builder. + pub fn h2_mut(&mut self) -> &mut H2ConnBuilder { + self.http_server.h2_mut() + } + + generate_set_and_with! { + /// Set an explicit buffer size for the relay buffer. + /// + /// By default, or in case the value is `None` or `Some(0)`, + /// it will use the value of the h2 server settings its max streams. + pub fn relay_buffer(mut self, n: Option) -> Self { + self.relay_buffer = n; + self + } + } +} + +impl Service> for HttpMitmRelay +where + Ingress: Io + Unpin + ExtensionsMut, + Egress: Io + Unpin + ExtensionsMut, + M: Layer> + Send + Sync + 'static + Clone, + M::Service: Service + Clone, +{ + type Output = (); + type Error = BoxError; + + async fn serve( + &self, + BridgeIo(ingress_stream, egress_stream): BridgeIo, + ) -> Result { + let token = CancellationToken::new(); + let cancelled = token.clone().cancelled_owned(); + + // TODO: see if , + // warrants this logic to change also slightly, relating to the graceful setup... + + let graceful_guard = self.exec.guard().cloned(); + let graceful = Shutdown::new(async move { + if let Some(guard) = graceful_guard { + tokio::select! { + _ = cancelled => { + tracing::trace!("HTTP MITM Relay: Shutdown: cancelation token"); + }, + _ = guard.cancelled() => { + tracing::trace!("HTTP MITM Relay: Shutdown: parent guard cancellation"); + }, + } + } else { + let _ = cancelled.await; + } + }); + + let _cancel_guard = token.drop_guard(); + + let (req_tx, req_rx) = tokio::sync::mpsc::channel( + self.relay_buffer + .unwrap_or_else(|| self.http_server.h2().max_concurrent_streams() as usize) + .max(1), + ); + + let middleware = self.middleware.clone(); + graceful.spawn_task_fn(async move |guard| { + http_relay_service_egress(egress_stream, guard, req_rx, middleware).await; + tracing::trace!("http_relay_service_egress = done"); + }); + + let graceful_shutdown_fut = graceful.shutdown(); + + let result = self.http_server + .serve( + ingress_stream, + service_fn(move |req: Request| { + let req_tx = req_tx.clone(); + async move { + let (tx, rx) = tokio::sync::oneshot::channel(); + if let Err(err) = req_tx.send(ReqJob { req, reply: tx }).await { + tracing::debug!("failed to schedule http request for MITM relay: {err}"); + return Ok(DefaultErrorResponse::response()); + } + match rx.await { + Ok(resp) => Ok(resp), + Err(err) => { + tracing::debug!( + "failed to receive http response from MITM relay executor: {err}" + ); + Ok(DefaultErrorResponse::response()) + } + } + } + }), + ) + .await + .context("serve HTTP MITM relay"); + + graceful_shutdown_fut.await; + result + } +} + +#[derive(Debug)] +struct ReqJob { + req: Request, + reply: oneshot::Sender, +} + +async fn http_relay_service_egress( + egress_stream: Egress, + guard: ShutdownGuard, + req_rx: mpsc::Receiver, + middleware: Middleware, +) where + Egress: Io + Unpin + ExtensionsMut, + Middleware: Layer>, + Middleware::Service: Service + Clone, +{ + let cancelled = std::pin::pin!(guard.clone_weak().into_cancelled()); + let mut req_stream = GracefulStream::new(cancelled, ReceiverStream::new(req_rx)); + + let Some(ReqJob { req, reply }) = req_stream.next().await else { + tracing::debug!("failed to receive initial request for HTTP MITM relay... return early"); + return; + }; + + let (req, core_client) = match http_connect(egress_stream, req, Executor::graceful(guard)).await + { + Ok(EstablishedClientConnection { input, conn }) => (input, conn), + Err(err) => { + tracing::debug!("failed to establish egress HTTP connection: {err}"); + if reply.send(DefaultErrorResponse::response()).is_err() { + tracing::trace!("failed to send BAD_GATEWAY response (svc error: {err})"); + } + return; + } + }; + + let client = middleware.into_layer(core_client); + + let mut job_req = req; + let mut job_reply = reply; + + tracing::debug!("egress http side ready; HTTP MITM relay loop ready and starting"); + + loop { + let client = client.clone(); + tokio::spawn(async move { + let Ok(resp) = client.serve(job_req).await; + if job_reply.send(resp).is_err() { + tracing::trace!("failed to send received response"); + } + }); + + let Some(job) = req_stream.next().await else { + tracing::debug!("(ingress) request stream exhausted; abort relay loop"); + return; + }; + + job_req = job.req; + job_reply = job.reply; + } +} diff --git a/rama-http-backend/src/proxy/mod.rs b/rama-http-backend/src/proxy/mod.rs new file mode 100644 index 000000000..ee6ce246e --- /dev/null +++ b/rama-http-backend/src/proxy/mod.rs @@ -0,0 +1 @@ +pub mod mitm; diff --git a/rama-http-backend/src/server/core_conn.rs b/rama-http-backend/src/server/core_conn.rs index 6a43d5346..0705f56fb 100644 --- a/rama-http-backend/src/server/core_conn.rs +++ b/rama-http-backend/src/server/core_conn.rs @@ -73,7 +73,7 @@ mod private { use rama_core::extensions::ExtensionsMut; use rama_core::futures::FutureExt; use rama_core::graceful::ShutdownGuard; - use rama_core::stream::Stream; + use rama_core::io::Io; use rama_core::telemetry::tracing; use rama_http::service::web::response::IntoResponse; use rama_http_core::service::RamaHttpService; @@ -91,7 +91,7 @@ mod private { guard: Option, ) -> impl Future + Send + '_ where - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, S: Service + Clone, Response: IntoResponse + Send + 'static; } @@ -105,7 +105,7 @@ mod private { guard: Option, ) -> HttpServeResult where - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, S: Service + Clone, Response: IntoResponse + Send + 'static, { @@ -147,7 +147,7 @@ mod private { guard: Option, ) -> HttpServeResult where - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, S: Service + Clone, Response: IntoResponse + Send + 'static, { @@ -189,7 +189,7 @@ mod private { guard: Option, ) -> HttpServeResult where - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, S: Service + Clone, Response: IntoResponse + Send + 'static, { diff --git a/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/mod.rs b/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/mod.rs new file mode 100644 index 000000000..14b241cc7 --- /dev/null +++ b/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/mod.rs @@ -0,0 +1,7 @@ +mod response; +pub use self::response::DefaultHttpProxyConnectReplyService; + +mod service_matcher; +pub use self::service_matcher::{ + HttpProxyConnectRelayServiceRequestMatcher, HttpProxyConnectRelayServiceResponseMatcher, +}; diff --git a/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/response.rs b/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/response.rs new file mode 100644 index 000000000..470fa03b5 --- /dev/null +++ b/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/response.rs @@ -0,0 +1,49 @@ +use rama_core::{Service, extensions::ExtensionsMut as _, telemetry::tracing}; +use rama_http::{Request, Response, StatusCode, service::web::response::IntoResponse as _}; +use rama_net::{http::RequestContext, proxy::ProxyTarget}; + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +/// A default [`Service`] which responds on an http (proxy) connect with +/// a default http response and which injects +/// the destination address as the proxy target. +/// +/// It can also be used for other HTTP connect purposes, +/// but that is not what the service is intended for. +pub struct DefaultHttpProxyConnectReplyService; + +impl DefaultHttpProxyConnectReplyService { + #[inline(always)] + #[must_use] + /// Create a new [`DefaultHttpProxyConnectReplyService`]. + pub fn new() -> Self { + Self + } +} + +impl Service> for DefaultHttpProxyConnectReplyService +where + Body: Send + 'static, +{ + type Output = (Response, Request); + type Error = Response; + + async fn serve(&self, mut req: Request) -> Result { + match RequestContext::try_from(&req).map(|ctx| ctx.host_with_port()) { + Ok(authority) => { + tracing::info!( + server.address = %authority.host, + server.port = authority.port, + "accept CONNECT: insert proxy target into extensions", + ); + req.extensions_mut().insert(ProxyTarget(authority)); + } + Err(err) => { + tracing::error!("error extracting authority: {err:?}"); + return Err(StatusCode::BAD_REQUEST.into_response()); + } + } + + Ok((StatusCode::OK.into_response(), req)) + } +} diff --git a/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/service_matcher.rs b/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/service_matcher.rs new file mode 100644 index 000000000..c320d5162 --- /dev/null +++ b/rama-http-backend/src/server/layer/upgrade/http_proxy_connect/service_matcher.rs @@ -0,0 +1,108 @@ +use std::convert::Infallible; + +use rama_core::matcher::service::{ServiceMatch, ServiceMatcher}; +use rama_http::{Request, Response}; +use rama_http_types::proxy::is_req_http_proxy_connect; +use rama_net::proxy::IoForwardService; + +#[derive(Debug, Clone)] +/// Default matcher that can be used for Http proxy connects. +/// +/// Request matches for an http proxy connect request return +/// a [`HttpProxyConnectRelayServiceResponseMatcher`] instance which +/// will match on any success responses... +pub struct HttpProxyConnectRelayServiceRequestMatcher { + relay_svc: S, +} + +impl Default for HttpProxyConnectRelayServiceRequestMatcher { + fn default() -> Self { + Self { + relay_svc: IoForwardService::new(), + } + } +} + +impl HttpProxyConnectRelayServiceRequestMatcher { + #[inline(always)] + #[must_use] + /// Create a new [`HttpProxyConnectRelayServiceRequestMatcher`]. + pub fn new(relay_svc: S) -> Self { + Self { relay_svc } + } +} + +impl ServiceMatcher> for HttpProxyConnectRelayServiceRequestMatcher +where + S: Clone + Send + Sync + 'static, + Body: Send + 'static, +{ + type Service = HttpProxyConnectRelayServiceResponseMatcher; + type Error = Infallible; + type ModifiedInput = Request; + + async fn match_service( + &self, + req: Request, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: is_req_http_proxy_connect(&req).then(|| { + HttpProxyConnectRelayServiceResponseMatcher { + relay_svc: self.relay_svc.clone(), + } + }), + input: req, + }) + } + + async fn into_match_service( + self, + req: Request, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: is_req_http_proxy_connect(&req).then(|| { + HttpProxyConnectRelayServiceResponseMatcher { + relay_svc: self.relay_svc, + } + }), + input: req, + }) + } +} + +#[derive(Debug, Clone)] +/// Created by [`HttpProxyConnectRelayServiceRequestMatcher`] for a valid http proxy connect +/// request match, this response matcher half ensures the returned status code is successfull. +pub struct HttpProxyConnectRelayServiceResponseMatcher { + relay_svc: S, +} + +impl ServiceMatcher> for HttpProxyConnectRelayServiceResponseMatcher +where + S: Clone + Send + Sync + 'static, + Body: Send + 'static, +{ + type Service = S; + type Error = Infallible; + type ModifiedInput = Response; + + async fn match_service( + &self, + res: Response, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: res.status().is_success().then(|| self.relay_svc.clone()), + input: res, + }) + } + + async fn into_match_service( + self, + res: Response, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: res.status().is_success().then_some(self.relay_svc), + input: res, + }) + } +} diff --git a/rama-http-backend/src/server/layer/upgrade/mitm/layer.rs b/rama-http-backend/src/server/layer/upgrade/mitm/layer.rs new file mode 100644 index 000000000..71007ffc4 --- /dev/null +++ b/rama-http-backend/src/server/layer/upgrade/mitm/layer.rs @@ -0,0 +1,41 @@ +use rama_core::{Layer, rt::Executor}; + +use crate::server::layer::upgrade::mitm::HttpUpgradeMitmRelay; + +#[derive(Debug, Clone)] +/// Layer used to create the middleware [`HttpUpgradeMitmRelay`] service. +pub struct HttpUpgradeMitmRelayLayer { + exec: Executor, + nested_matcher_svc: M, +} + +impl HttpUpgradeMitmRelayLayer { + #[inline(always)] + #[must_use] + /// Create a new [`HttpUpgradeMitmRelayLayer`] used to produce + /// the middleware [`HttpUpgradeMitmRelay`] service. + pub const fn new(exec: Executor, nested_matcher_svc: U) -> Self { + Self { + exec, + nested_matcher_svc, + } + } +} + +impl Layer for HttpUpgradeMitmRelayLayer { + type Service = HttpUpgradeMitmRelay; + + #[inline(always)] + fn layer(&self, inner_svc: S) -> Self::Service { + Self::Service::new( + self.exec.clone(), + self.nested_matcher_svc.clone(), + inner_svc, + ) + } + + #[inline(always)] + fn into_layer(self, inner_svc: S) -> Self::Service { + Self::Service::new(self.exec, self.nested_matcher_svc, inner_svc) + } +} diff --git a/rama-http-backend/src/server/layer/upgrade/mitm/mod.rs b/rama-http-backend/src/server/layer/upgrade/mitm/mod.rs new file mode 100644 index 000000000..7a2b7e21c --- /dev/null +++ b/rama-http-backend/src/server/layer/upgrade/mitm/mod.rs @@ -0,0 +1,5 @@ +mod svc; +pub use self::svc::HttpUpgradeMitmRelay; + +mod layer; +pub use self::layer::HttpUpgradeMitmRelayLayer; diff --git a/rama-http-backend/src/server/layer/upgrade/mitm/svc.rs b/rama-http-backend/src/server/layer/upgrade/mitm/svc.rs new file mode 100644 index 000000000..618aec661 --- /dev/null +++ b/rama-http-backend/src/server/layer/upgrade/mitm/svc.rs @@ -0,0 +1,141 @@ +use std::convert::Infallible; + +use rama_core::{ + Service, bytes, + error::BoxError, + extensions::{ExtensionsMut, ExtensionsRef as _}, + io::BridgeIo, + matcher::service::{ServiceMatch, ServiceMatcher}, + rt::Executor, + telemetry::tracing::{self, Instrument as _}, +}; +use rama_http::{ + Body, Request, Response, StreamingBody, io::upgrade::Upgraded, + opentelemetry::version_as_protocol_version, service::web::response::IntoResponse, +}; + +#[derive(Debug, Clone)] +/// Http middleware that can be used by MITM proxies, +/// such as transparent (L4) proxies to relay a HTTP upgrade-request +/// as-is and pipe the upgraded upgrade request on both ends +/// via the upgrade (bridgeIo) svc. +pub struct HttpUpgradeMitmRelay { + exec: Executor, + nested_matcher_svc: M, + inner_svc: S, +} + +impl HttpUpgradeMitmRelay { + #[inline(always)] + #[must_use] + /// Create a new [`HttpUpgradeMitmRelay`]. + pub const fn new(exec: Executor, nested_matcher_svc: M, inner_svc: S) -> Self { + Self { + exec, + nested_matcher_svc, + inner_svc, + } + } +} + +impl Service> + for HttpUpgradeMitmRelay +where + M: ServiceMatcher< + Request, + Error: IntoResponse, + ModifiedInput = Request, + Service: ServiceMatcher< + Response, + Error: Into, + ModifiedInput = Response, + Service: Service, Output = (), Error = Infallible>, + >, + >, + S: Service>, + ReqBody: StreamingBody> + Send + Sync + 'static, + ModReqBody: StreamingBody> + Send + Sync + 'static, + ResBody: StreamingBody> + Send + Sync + 'static, + ModResBody: StreamingBody> + Send + Sync + 'static, +{ + type Output = Response; + type Error = S::Error; + + async fn serve(&self, req: Request) -> Result { + let ServiceMatch { + input: req, + service: maybe_res_svc_matcher, + } = match self.nested_matcher_svc.match_service(req).await { + Ok(sm) => sm, + Err(err) => return Ok(err.into_response()), + }; + + if let Some(res_svc_matcher) = maybe_res_svc_matcher { + tracing::debug!( + "HttpUpgradeMitmRelay: upgrade MITM relay req match made... opening request upgrade handle option" + ); + + let on_upgrade_ingress = rama_http::io::upgrade::handle_upgrade(&req); + let req_extensions = req.extensions().clone(); + + let relay_upgrade_span = tracing::trace_root_span!( + "upgrade::mitm_relay::serve", + otel.kind = "server", + http.request.method = %req.method().as_str(), + url.full = %req.uri(), + url.path = %req.uri().path(), + url.query = req.uri().query().unwrap_or_default(), + url.scheme = %req.uri().scheme().map(|s| s.as_str()).unwrap_or_default(), + network.protocol.name = "http", + network.protocol.version = version_as_protocol_version(req.version()), + ); + + let res = self.inner_svc.serve(req.map(Body::new)).await?; + + let ServiceMatch { + input: res, + service: maybe_relay_svc, + } = res_svc_matcher + .match_service(res) + .await + .map_err(Into::into)?; + + if let Some(relay_svc) = maybe_relay_svc { + let on_upgrade_egress = rama_http::io::upgrade::handle_upgrade(&res); + let res_extensions = res.extensions().clone(); + + tracing::trace!("HttpUpgradeMitmRelay: spawn relay svc on its own task"); + + self.exec.spawn_task(async move { + tracing::debug!( + "HttpUpgradeMitmRelay: spawned task active" + ); + + let (mut ingress_stream, mut egress_stream) = match tokio::try_join!(on_upgrade_ingress, on_upgrade_egress) { + Ok(streams) => streams, + Err(err) => { + tracing::debug!("HttpUpgradeMitmRelay: relay task: one or both sides filed to upgrade: {err}"); + return; + } + }; + + ingress_stream.extensions_mut().extend(req_extensions); + egress_stream.extensions_mut().extend(res_extensions); + + tracing::trace!( + "HttpUpgradeMitmRelay: relay task: bidirectional upgrade complete: continue serving via upgrade relay svc" + ); + relay_svc.serve(BridgeIo(ingress_stream, egress_stream)).await; + }.instrument(relay_upgrade_span)); + + Ok(res.map(Body::new)) + } else { + tracing::trace!("HttpUpgradeMitmRelay: aborted: no response match"); + Ok(res.map(Body::new)) + } + } else { + let res = self.inner_svc.serve(req.map(Body::new)).await?; + Ok(res.map(Body::new)) + } + } +} diff --git a/rama-http-backend/src/server/layer/upgrade/mod.rs b/rama-http-backend/src/server/layer/upgrade/mod.rs index b995fc05b..41b68d330 100644 --- a/rama-http-backend/src/server/layer/upgrade/mod.rs +++ b/rama-http-backend/src/server/layer/upgrade/mod.rs @@ -11,3 +11,11 @@ mod layer; pub use layer::UpgradeLayer; pub use rama_http::io::upgrade::Upgraded; + +mod http_proxy_connect; +pub use http_proxy_connect::{ + DefaultHttpProxyConnectReplyService, HttpProxyConnectRelayServiceRequestMatcher, + HttpProxyConnectRelayServiceResponseMatcher, +}; + +pub mod mitm; diff --git a/rama-http-backend/src/server/layer/upgrade/service.rs b/rama-http-backend/src/server/layer/upgrade/service.rs index 75f40ecc6..f5ba88112 100644 --- a/rama-http-backend/src/server/layer/upgrade/service.rs +++ b/rama-http-backend/src/server/layer/upgrade/service.rs @@ -137,6 +137,7 @@ where Err(e) => Ok(e), }; } + self.inner.serve(req).await } } diff --git a/rama-http-backend/src/server/service.rs b/rama-http-backend/src/server/service.rs index c9bb0bc04..f19f121a9 100644 --- a/rama-http-backend/src/server/service.rs +++ b/rama-http-backend/src/server/service.rs @@ -6,16 +6,14 @@ use rama_core::Service; use rama_core::error::BoxError; use rama_core::extensions::ExtensionsMut; use rama_core::graceful::ShutdownGuard; +use rama_core::io::Io; use rama_core::rt::Executor; -use rama_core::stream::Stream; use rama_http::service::web::response::IntoResponse; use rama_http_core::server::conn::auto::Builder as AutoConnBuilder; -use rama_http_core::server::conn::auto::Http1Builder as InnerAutoHttp1Builder; -use rama_http_core::server::conn::auto::Http2Builder as InnerAutoHttp2Builder; use rama_http_core::server::conn::http1::Builder as Http1ConnBuilder; use rama_http_core::server::conn::http2::Builder as H2ConnBuilder; use rama_http_types::Request; -use rama_net::socket::Interface; +use rama_net::address::SocketAddress; use rama_tcp::server::TcpListener; use std::convert::Infallible; use std::fmt; @@ -45,7 +43,7 @@ impl Default for HttpServer { impl HttpServer { /// Create a new http/1.1 `Builder` with default settings. #[must_use] - pub fn http1(exec: Executor) -> Self { + pub fn new_http1(exec: Executor) -> Self { Self { builder: Http1ConnBuilder::new(), exec, @@ -55,6 +53,11 @@ impl HttpServer { impl HttpServer { /// Http1 configuration. + pub fn http1(&mut self) -> &Http1ConnBuilder { + &self.builder + } + + /// Http1 mutable configuration. pub fn http1_mut(&mut self) -> &mut Http1ConnBuilder { &mut self.builder } @@ -63,7 +66,7 @@ impl HttpServer { impl HttpServer { /// Create a new h2 `Builder` with default settings. #[must_use] - pub fn h2(exec: Executor) -> Self { + pub fn new_h2(exec: Executor) -> Self { Self { builder: H2ConnBuilder::new(exec.clone()), exec, @@ -73,6 +76,11 @@ impl HttpServer { impl HttpServer { /// H2 configuration. + pub fn h2(&self) -> &H2ConnBuilder { + &self.builder + } + + /// H2 mutable configuration. pub fn h2_mut(&mut self) -> &mut H2ConnBuilder { &mut self.builder } @@ -90,14 +98,28 @@ impl HttpServer { } impl HttpServer { - /// Http1 configuration. - pub fn http1_mut(&mut self) -> InnerAutoHttp1Builder<'_> { + #[inline(always)] + /// Http1 builder. + pub fn http1(&self) -> &Http1ConnBuilder { self.builder.http1() } - /// H2 configuration. - pub fn h2_mut(&mut self) -> InnerAutoHttp2Builder<'_> { - self.builder.http2() + #[inline(always)] + /// Http1 mutable builder. + pub fn http1_mut(&mut self) -> &mut Http1ConnBuilder { + self.builder.http1_mut() + } + + #[inline(always)] + /// H2 builder. + pub fn h2(&self) -> &H2ConnBuilder { + self.builder.h2() + } + + #[inline(always)] + /// H2 mutable builder. + pub fn h2_mut(&mut self) -> &mut H2ConnBuilder { + self.builder.h2_mut() } } @@ -120,23 +142,23 @@ where where S: Service + Clone, Response: IntoResponse + Send + 'static, - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, { self.builder .http_core_serve_connection(stream, service, self.exec.guard().cloned()) .await } - /// Listen for connections on the given [`Interface`], serving HTTP connections. + /// Listen for connections on the given [`SocketAddress`], serving HTTP connections. /// /// It's a shortcut in case you don't need to operate on the transport layer directly. - pub async fn listen(self, interface: I, service: S) -> HttpServeResult + pub async fn listen(self, address: A, service: S) -> HttpServeResult where S: Service + Clone, Response: IntoResponse + Send + 'static, - I: TryInto>, + A: TryInto>, { - let tcp = TcpListener::bind(interface, self.exec.clone()).await?; + let tcp = TcpListener::bind_address(address, self.exec.clone()).await?; let service = HttpService { guard: self.exec.guard().cloned(), builder: Arc::new(self.builder), @@ -204,7 +226,7 @@ where B: HttpCoreConnServer, S: Service + Clone, Response: IntoResponse + Send + 'static, - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, { type Output = (); type Error = rama_core::error::BoxError; diff --git a/rama-http-backend/src/tests.rs b/rama-http-backend/src/tests.rs new file mode 100644 index 000000000..c94f74fd3 --- /dev/null +++ b/rama-http-backend/src/tests.rs @@ -0,0 +1,327 @@ +use std::{ + convert::Infallible, + time::{Duration, Instant}, +}; + +use tokio::time::sleep; + +use rama_core::{ + Layer, Service, + futures::future::{join, join_all}, + graceful::Shutdown, + io::BridgeIo, + layer::ArcLayer, + layer::ConsumeErrLayer, + rt::Executor, + service::service_fn, +}; +use rama_http::{ + HeaderName, HeaderValue, + body::util::BodyExt as _, + layer::set_header::{SetRequestHeaderLayer, SetResponseHeaderLayer}, +}; +use rama_http_types::{Body, Request, Response, StatusCode, Version}; +use rama_net::test_utils::client::{MockConnectorService, MockSocket}; +use tokio_util::sync::CancellationToken; + +use crate::proxy::mitm::DefaultErrorResponse; + +use super::{ + client::{HttpConnectorLayer, http_connect}, + proxy::mitm::HttpMitmRelay, + server::HttpServer, +}; + +#[tokio::test] +async fn test_http11_pipelining() { + let connector = HttpConnectorLayer::default().into_layer(MockConnectorService::new(|| { + HttpServer::auto(Executor::default()).service(service_fn(server_svc_fn)) + })); + + let conn = connector + .serve(create_test_request(Version::HTTP_11)) + .await + .unwrap() + .conn; + + // Http 1.1 should pipeline requests. Pipelining is important when trying to send multiple + // requests on the same connection. This is something we generally don't do, but we do + // trigger the same problem when we re-use a connection too fast. However triggering that + // bug consistently has proven very hard so we trigger this one instead. Both of them + // should be fixed by waiting for conn.isready().await before trying to send data on the connection. + // For http1.1 this will result in pipelining (http2 will still be multiplexed) + let start = Instant::now(); + let (res1, res2) = join( + conn.serve(create_test_request(Version::HTTP_11)), + conn.serve(create_test_request(Version::HTTP_11)), + ) + .await; + let duration = start.elapsed(); + + res1.unwrap(); + res2.unwrap(); + + assert!(duration > Duration::from_millis(200)); +} + +#[tokio::test] +async fn test_http2_multiplex() { + let connector = HttpConnectorLayer::default().into_layer(MockConnectorService::new(|| { + HttpServer::auto(Executor::default()).service(service_fn(server_svc_fn)) + })); + + let conn = connector + .serve(create_test_request(Version::HTTP_2)) + .await + .unwrap() + .conn; + + // We have an artificial sleep of 100ms, so multiplexing should be < 200ms + let start = Instant::now(); + let (res1, res2) = join( + conn.serve(create_test_request(Version::HTTP_2)), + conn.serve(create_test_request(Version::HTTP_2)), + ) + .await; + + let duration = start.elapsed(); + res1.unwrap(); + res2.unwrap(); + + assert!(duration < Duration::from_millis(200)); +} + +#[tokio::test] +async fn test_http11_handles_4_concurrent_requests() { + let connector = HttpConnectorLayer::default().into_layer(MockConnectorService::new(|| { + HttpServer::auto(Executor::default()).service(service_fn(server_svc_fn)) + })); + + let conn = connector + .serve(create_test_request(Version::HTTP_11)) + .await + .unwrap() + .conn; + + let responses = + join_all((0..4).map(|_| conn.serve(create_test_request(Version::HTTP_11)))).await; + + assert_eq!(responses.len(), 4); + for response in responses { + let response = response.unwrap(); + assert_eq!(response.status(), 200); + let body = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(body, "a random response body"); + } +} + +#[tokio::test] +async fn test_http2_handles_200_concurrent_requests() { + let connector = HttpConnectorLayer::default().into_layer(MockConnectorService::new(|| { + HttpServer::auto(Executor::default()).service(service_fn(server_svc_fn)) + })); + + let conn = connector + .serve(create_test_request(Version::HTTP_2)) + .await + .unwrap() + .conn; + + let responses = + join_all((0..200).map(|_| conn.serve(create_test_request(Version::HTTP_2)))).await; + + assert_eq!(responses.len(), 200); + for response in responses { + let response = response.unwrap(); + assert_eq!(response.status(), 200); + let body = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(body, "a random response body"); + } +} + +async fn server_svc_fn(_: Request) -> Result { + sleep(Duration::from_millis(100)).await; + Ok(Response::new(Body::from("a random response body"))) +} + +async fn mitm_relay_server_svc_fn(req: Request) -> Result { + assert!(req.headers().contains_key("x-observed-req")); + let body = req + .headers() + .get("x-test-id") + .and_then(|v| v.to_str().ok()) + .map(|id| format!("a random response body ({id})")) + .unwrap_or_else(|| "a random response body".to_owned()); + Ok(Response::new(Body::from(body))) +} + +fn create_test_request(version: Version) -> Request { + Request::builder() + .uri("https://www.example.com") + .version(version) + .body(Body::from("a reandom request body")) + .unwrap() +} + +fn create_test_request_with_id(version: Version, id: usize) -> Request { + Request::builder() + .uri("https://www.example.com") + .version(version) + .header("x-test-id", id.to_string()) + .body(Body::from(format!("a reandom request body ({id})"))) + .unwrap() +} + +async fn test_mitm_relay_roundtrip_inner(version: Version) { + let (client_stream, relay_ingress_stream) = tokio::io::duplex(16 * 1024); + let (relay_egress_stream, server_stream) = tokio::io::duplex(16 * 1024); + + let token = CancellationToken::new(); + let graceful = Shutdown::new(token.clone().cancelled_owned()); + let cancel_drop_guard = token.drop_guard(); + + graceful.spawn_task_fn(async move |guard| { + HttpServer::auto(Executor::graceful(guard)) + .service(service_fn(mitm_relay_server_svc_fn)) + .serve(MockSocket::new(server_stream)) + .await + .unwrap(); + }); + + graceful.spawn_task_fn(async move |guard| { + HttpMitmRelay::new(Executor::graceful(guard)) + .with_http_middleware(( + ConsumeErrLayer::trace_as_debug().with_response(DefaultErrorResponse::new()), + SetRequestHeaderLayer::overriding( + HeaderName::from_static("x-observed-req"), + HeaderValue::from_static("1"), + ), + SetResponseHeaderLayer::overriding( + HeaderName::from_static("x-observed-res"), + HeaderValue::from_static("1"), + ), + ArcLayer::new(), + )) + .serve(BridgeIo( + MockSocket::new(relay_ingress_stream), + MockSocket::new(relay_egress_stream), + )) + .await + .unwrap(); + }); + + let request = create_test_request(version); + let conn = http_connect( + MockSocket::new(client_stream), + request, + Executor::graceful(graceful.guard()), + ) + .await + .unwrap() + .conn; + + let response = conn.serve(create_test_request(version)).await.unwrap(); + + assert!(response.headers().contains_key("x-observed-res")); + + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(bytes, "a random response body"); + + drop(conn); + cancel_drop_guard.disarm().cancel(); + let fut = graceful.shutdown(); + + fut.await; +} + +async fn test_mitm_relay_concurrency_inner(version: Version, n: usize) { + let (client_stream, relay_ingress_stream) = tokio::io::duplex(16 * 1024); + let (relay_egress_stream, server_stream) = tokio::io::duplex(16 * 1024); + + let token = CancellationToken::new(); + let graceful = Shutdown::new(token.clone().cancelled_owned()); + let cancel_drop_guard = token.drop_guard(); + + graceful.spawn_task_fn(async move |guard| { + HttpServer::auto(Executor::graceful(guard)) + .service(service_fn(mitm_relay_server_svc_fn)) + .serve(MockSocket::new(server_stream)) + .await + .unwrap(); + }); + + graceful.spawn_task_fn(async move |guard| { + HttpMitmRelay::new(Executor::graceful(guard)) + .with_http_middleware(( + ConsumeErrLayer::trace_as_debug().with_response(DefaultErrorResponse::new()), + SetRequestHeaderLayer::overriding( + HeaderName::from_static("x-observed-req"), + HeaderValue::from_static("1"), + ), + SetResponseHeaderLayer::overriding( + HeaderName::from_static("x-observed-res"), + HeaderValue::from_static("1"), + ), + ArcLayer::new(), + )) + .serve(BridgeIo( + MockSocket::new(relay_ingress_stream), + MockSocket::new(relay_egress_stream), + )) + .await + .unwrap(); + }); + + let request = create_test_request(version); + let conn = http_connect( + MockSocket::new(client_stream), + request, + Executor::graceful(graceful.guard()), + ) + .await + .unwrap() + .conn; + + let mut ids = Vec::with_capacity(n); + let mut futures = Vec::with_capacity(n); + for id in 0..n { + ids.push(id); + futures.push(conn.serve(create_test_request_with_id(version, id))); + } + + let responses = join_all(futures).await; + assert_eq!(responses.len(), n); + + for (id, response) in ids.into_iter().zip(responses) { + let response = response.unwrap(); + assert!(response.headers().contains_key("x-observed-res")); + assert_eq!(response.status(), StatusCode::OK); + let bytes = response.into_body().collect().await.unwrap().to_bytes(); + assert_eq!(bytes, format!("a random response body ({id})")); + } + + drop(conn); + cancel_drop_guard.disarm().cancel(); + graceful.shutdown().await; +} + +#[tokio::test] +async fn test_http11_mitm_relay_roundtrip() { + test_mitm_relay_roundtrip_inner(Version::HTTP_11).await; +} + +#[tokio::test] +async fn test_http2_mitm_relay_roundtrip() { + test_mitm_relay_roundtrip_inner(Version::HTTP_2).await; +} + +#[tokio::test] +async fn test_http11_mitm_relay_handles_4_concurrent_requests() { + test_mitm_relay_concurrency_inner(Version::HTTP_11, 4).await; +} + +#[tokio::test] +async fn test_http2_mitm_relay_handles_200_concurrent_requests() { + test_mitm_relay_concurrency_inner(Version::HTTP_2, 200).await; +} diff --git a/rama-http-core/README.md b/rama-http-core/README.md index 8ecc6e868..52a6bdd9d 100644 --- a/rama-http-core/README.md +++ b/rama-http-core/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-http-core/src/proto/h1/conn.rs b/rama-http-core/src/proto/h1/conn.rs index 6ab9d4609..4cc743798 100644 --- a/rama-http-core/src/proto/h1/conn.rs +++ b/rama-http-core/src/proto/h1/conn.rs @@ -626,11 +626,9 @@ where Version::HTTP_10 => self.state.disable_keep_alive(), // If response is version 1.1 and keep-alive is wanted, add // Connection: keep-alive header when not present - Version::HTTP_11 => { - if self.state.wants_keep_alive() { - head.headers - .insert(CONNECTION, HeaderValue::from_static("keep-alive")); - } + Version::HTTP_11 if self.state.wants_keep_alive() => { + head.headers + .insert(CONNECTION, HeaderValue::from_static("keep-alive")); } _ => (), } diff --git a/rama-http-core/src/server/conn/auto.rs b/rama-http-core/src/server/conn/auto.rs index 6603e37d2..c99c6f533 100644 --- a/rama-http-core/src/server/conn/auto.rs +++ b/rama-http-core/src/server/conn/auto.rs @@ -1,12 +1,12 @@ //! Http1 or Http2 connection. use std::convert::Infallible; +use std::io; use std::marker::PhantomPinned; use std::mem::MaybeUninit; use std::pin::Pin; use std::task::ready; use std::task::{Context, Poll}; -use std::{io, time::Duration}; use pin_project_lite::pin_project; use rama_core::Service; @@ -18,8 +18,8 @@ use tokio::io::ReadBuf; use rama_core::bytes::Bytes; use rama_core::error::BoxError; +use rama_core::io::rewind::Rewind; use rama_core::rt::Executor; -use rama_core::stream::rewind::Rewind; use crate::body::Incoming; @@ -46,14 +46,24 @@ impl Builder { } } - /// Http1 configuration. - pub fn http1(&mut self) -> Http1Builder<'_> { - Http1Builder { inner: self } + /// Http1 builder. + pub fn http1(&self) -> &http1::Builder { + &self.http1 } - /// Http2 configuration. - pub fn http2(&mut self) -> Http2Builder<'_> { - Http2Builder { inner: self } + /// Http1 nutable builder. + pub fn http1_mut(&mut self) -> &mut http1::Builder { + &mut self.http1 + } + + /// H2 builder. + pub fn h2(&self) -> &http2::Builder { + &self.http2 + } + + /// H2 mutable builder. + pub fn h2_mut(&mut self) -> &mut http2::Builder { + &mut self.http2 } /// Only accepts HTTP/2 @@ -62,7 +72,7 @@ impl Builder { /// /// [`serve_connection_with_upgrades`]: Builder::serve_connection_with_upgrades #[must_use] - pub fn http2_only(mut self) -> Self { + pub fn h2_only(mut self) -> Self { assert!(self.version.is_none()); self.version = Some(Version::H2); self @@ -98,6 +108,14 @@ impl Builder { } } + /// Gets the [`SETTINGS_MAX_CONCURRENT_STREAMS`][spec] option used + /// for HTTP2 connections. + /// + /// [spec]: https://httpwg.org/specs/rfc9113.html#SETTINGS_MAX_CONCURRENT_STREAMS + pub fn max_concurrent_streams(&self) -> u32 { + self.http2.max_concurrent_streams() + } + /// Bind a connection together with a [`Service`]. pub fn serve_connection(&self, io: I, service: S) -> Connection<'_, I, S> where @@ -515,374 +533,6 @@ where } } -/// Http1 part of builder. -pub struct Http1Builder<'a> { - inner: &'a mut Builder, -} - -impl Http1Builder<'_> { - /// Http2 configuration. - pub fn http2(&mut self) -> Http2Builder<'_> { - Http2Builder { inner: self.inner } - } - - rama_utils::macros::generate_set_and_with! { - /// Set whether the `date` header should be included in HTTP responses. - /// - /// Note that including the `date` header is recommended by RFC 7231. - /// - /// Default is `true`. - pub fn auto_date_header(mut self, enabled: bool) -> Self { - self.inner.http1.set_auto_date_header(enabled); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set whether HTTP/1 connections should support half-closures. - /// - /// Clients can chose to shutdown their write-side while waiting - /// for the server to respond. Setting this to `true` will - /// prevent closing the connection immediately if `read` - /// detects an EOF in the middle of a request. - /// - /// Default is `false`. - pub fn half_close(mut self, val: bool) -> Self { - self.inner.http1.set_half_close(val); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Enables or disables HTTP/1 keep-alive. - /// - /// Default is `true`. - pub fn keep_alive(mut self, val: bool) -> Self { - self.inner.http1.set_keep_alive(val); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set whether HTTP/1 connections will write header names as title case at - /// the socket level. - /// - /// Note that this setting does not affect HTTP/2. - /// - /// Default is `false`. - pub fn title_case_headers(mut self, enabled: bool) -> Self { - self.inner.http1.set_title_case_headers(enabled); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set whether HTTP/1 connections will silently ignored malformed header lines. - /// - /// If this is enabled and a header line does not start with a valid header - /// name, or does not include a colon at all, the line will be silently ignored - /// and no error will be reported. - /// - /// Default is `false`. - pub fn ignore_invalid_headers(mut self, enabled: bool) -> Self { - self.inner.http1.set_ignore_invalid_headers(enabled); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set the maximum number of headers. - /// - /// When a request is received, the parser will reserve a buffer to store headers for optimal - /// performance. - /// - /// If server receives more headers than the buffer size, it responds to the client with - /// "431 Request Header Fields Too Large". - /// - /// The headers is allocated on the stack by default, which has higher performance. After - /// setting this value, headers will be allocated in heap memory, that is, heap memory - /// allocation will occur for each request, and there will be a performance drop of about 5%. - /// - /// Note that this setting does not affect HTTP/2. - /// - /// Default is 100. - pub fn max_headers(mut self, val: Option) -> Self { - self.inner.http1.maybe_set_max_headers(val); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set a timeout for reading client request headers. If a client does not - /// transmit the entire header within this time, the connection is closed. - /// - /// Default is currently 30 seconds, but do not depend on that. - pub fn header_read_timeout(mut self, read_timeout: Duration) -> Self { - self.inner.http1.set_header_read_timeout(read_timeout); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set whether HTTP/1 connections should try to use vectored writes, - /// or always flatten into a single buffer. - /// - /// Note that setting this to false may mean more copies of body data, - /// but may also improve performance when an IO transport doesn't - /// support vectored writes well, such as most TLS implementations. - /// - /// Setting this to true will force hyper to use queued strategy - /// which may eliminate unnecessary cloning on some TLS backends - /// - /// Default is `auto`. In this mode rama-http-core will try to guess which - /// mode to use - pub fn writev(mut self, val: Option) -> Self { - self.inner.http1.maybe_set_writev(val); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set the maximum buffer size for the connection. - /// - /// Default is ~400kb. - /// - /// # Errors - /// - /// The minimum value allowed is 8192. This method errors if the passed `max` is less than the minimum. - pub fn max_buf_size(mut self, max: usize) -> Result { - self.inner.http1.try_set_max_buf_size(max)?; - Ok(self) - } - } - - rama_utils::macros::generate_set_and_with! { - /// Aggregates flushes to better support pipelined responses. - /// - /// Experimental, may have bugs. - /// - /// Default is `false`. - pub fn pipeline_flush(mut self, enabled: bool) -> Self { - self.inner.http1.set_pipeline_flush(enabled); - self - } - } - - /// Bind a connection together with a [`Service`]. - pub async fn serve_connection(&self, io: I, service: S) -> Result<(), BoxError> - where - S: Service, Output = Response, Error = Infallible> + Clone, - I: AsyncRead + AsyncWrite + Send + Unpin + ExtensionsMut + 'static, - { - self.inner.serve_connection(io, service).await - } - - /// Bind a connection together with a [`Service`], with the ability to - /// handle HTTP upgrades. This requires that the IO object implements - /// `Send`. - pub fn serve_connection_with_upgrades( - &self, - io: I, - service: S, - ) -> UpgradeableConnection<'_, I, S> - where - S: Service, Output = Response, Error = Infallible>, - I: AsyncRead + AsyncWrite + Send + Unpin + 'static + Send + 'static, - { - self.inner.serve_connection_with_upgrades(io, service) - } -} - -/// Http2 part of builder. -pub struct Http2Builder<'a> { - inner: &'a mut Builder, -} - -impl Http2Builder<'_> { - /// Http1 configuration. - pub fn http1(&mut self) -> Http1Builder<'_> { - Http1Builder { inner: self.inner } - } - - rama_utils::macros::generate_set_and_with! { - /// Configures the maximum number of pending reset streams allowed before a GOAWAY will be sent. - /// - /// This will default to the default value set by the [`h2` crate](https://crates.io/crates/h2). - /// As of v0.4.0, it is 20. - /// - /// See for more information. - pub fn max_pending_accept_reset_streams(mut self, max: Option) -> Self { - self.inner.http2.maybe_set_max_pending_accept_reset_streams(max); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Configures the maximum number of local reset streams allowed before a GOAWAY will be sent. - /// - /// If not set, rama-http-core will use a default, currently of 1024. - /// - /// If `None` is supplied, rama-http-core will not apply any limit. - /// This is not advised, as it can potentially expose servers to DOS vulnerabilities. - /// - /// See for more information. - pub fn max_local_error_reset_streams(mut self, max: Option) -> Self { - self.inner.http2.maybe_set_max_local_error_reset_streams(max); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets the [`SETTINGS_INITIAL_WINDOW_SIZE`][spec] option for HTTP2 - /// stream-level flow control. - /// - /// If not set, rama-http-core will use a default. - /// - /// [spec]: https://http2.github.io/http2-spec/#SETTINGS_INITIAL_WINDOW_SIZE - pub fn initial_stream_window_size(mut self, sz: u32) -> Self { - self.inner.http2.set_initial_stream_window_size(sz); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets the max connection-level flow control for HTTP2. - /// - /// If not set, rama-http-core will use a default. - pub fn initial_connection_window_size(mut self, sz: u32) -> Self { - self.inner.http2.set_initial_connection_window_size(sz); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets whether to use an adaptive flow control. - /// - /// Enabling this will override the limits set in - /// `http2_initial_stream_window_size` and - /// `http2_initial_connection_window_size`. - pub fn adaptive_window(mut self, enabled: bool) -> Self { - self.inner.http2.set_adaptive_window(enabled); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets the maximum frame size to use for HTTP2. - /// - /// If not set, rama-http-core will use a default. - pub fn max_frame_size(mut self, sz: u32) -> Self { - self.inner.http2.set_max_frame_size(sz); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets the [`SETTINGS_MAX_CONCURRENT_STREAMS`][spec] option for HTTP2 - /// connections. - /// - /// Default is 200. Passing `None` will remove any limit. - /// - /// [spec]: https://http2.github.io/http2-spec/#SETTINGS_MAX_CONCURRENT_STREAMS - pub fn max_concurrent_streams(mut self, max: u32) -> Self { - self.inner.http2.set_max_concurrent_streams(max); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets an interval for HTTP2 Ping frames should be sent to keep a - /// connection alive. - /// - /// Pass `None` to disable HTTP2 keep-alive. - /// - /// Default is currently disabled. - pub fn keep_alive_interval(mut self, interval: Option) -> Self { - self.inner.http2.maybe_set_keep_alive_interval(interval); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets a timeout for receiving an acknowledgement of the keep-alive ping. - /// - /// If the ping is not acknowledged within the timeout, the connection will - /// be closed. Does nothing if `http2_keep_alive_interval` is disabled. - /// - /// Default is 20 seconds. - pub fn keep_alive_timeout(mut self, timeout: Duration) -> Self { - self.inner.http2.set_keep_alive_timeout(timeout); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set the maximum write buffer size for each HTTP/2 stream. - /// - /// Default is currently ~400KB, but may change. - pub fn max_send_buf_size(mut self, max: u32) -> Self { - self.inner.http2.set_max_send_buf_size(max); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Enables the [extended CONNECT protocol]. - /// - /// [extended CONNECT protocol]: https://datatracker.ietf.org/doc/html/rfc8441#section-4 - pub fn enable_connect_protocol(mut self) -> Self { - self.inner.http2.set_enable_connect_protocol(); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Sets the max size of received header frames. - /// - /// Default is currently ~16MB, but may change. - pub fn max_header_list_size(mut self, max: u32) -> Self { - self.inner.http2.set_max_header_list_size(max); - self - } - } - - rama_utils::macros::generate_set_and_with! { - /// Set whether the `date` header should be included in HTTP responses. - /// - /// Note that including the `date` header is recommended by RFC 7231. - /// - /// Default is `true`. - pub fn auto_date_header(mut self, enabled: bool) -> Self { - self.inner.http2.set_auto_date_header(enabled); - self - } - } - - /// Bind a connection together with a [`Service`]. - pub async fn serve_connection(&self, io: I, service: S) -> Result<(), BoxError> - where - S: Service, Output = Response, Error = Infallible> + Clone, - I: AsyncRead + AsyncWrite + Send + Unpin + ExtensionsMut + 'static, - { - self.inner.serve_connection(io, service).await - } - - /// Bind a connection together with a [`Service`], with the ability to - /// handle HTTP upgrades. This requires that the IO object implements - /// `Send`. - pub fn serve_connection_with_upgrades( - &self, - io: I, - service: S, - ) -> UpgradeableConnection<'_, I, S> - where - S: Service, Output = Response, Error = Infallible>, - I: AsyncRead + AsyncWrite + Send + Unpin + 'static + Send + 'static, - { - self.inner.serve_connection_with_upgrades(io, service) - } -} - #[cfg(test)] mod tests { use crate::client::conn::http1; @@ -906,19 +556,11 @@ mod tests { #[test] fn configuration() { - // One liner. - auto::Builder::new(Executor::new()) - .http1() - .set_keep_alive(true) - .http2() - .maybe_set_keep_alive_interval(None); - // .serve_connection(io, service); - // Using variable. let mut builder = auto::Builder::new(Executor::new()); - builder.http1().set_keep_alive(true); - builder.http2().maybe_set_keep_alive_interval(None); + builder.http1_mut().set_keep_alive(true); + builder.h2_mut().maybe_set_keep_alive_interval(None); // builder.serve_connection(io, service); } @@ -1096,18 +738,14 @@ mod tests { .serve_connection(stream, RamaHttpService::new(service_fn(hello))) .await } else if h2_only { - builder = builder.http2_only(); + builder = builder.h2_only(); builder .serve_connection(stream, RamaHttpService::new(service_fn(hello))) .await } else { + builder.h2_mut().set_max_header_list_size(4096); builder - .http2() - .set_max_header_list_size(4096) - .serve_connection_with_upgrades( - stream, - RamaHttpService::new(service_fn(hello)), - ) + .serve_connection(stream, RamaHttpService::new(service_fn(hello))) .await } .unwrap(); diff --git a/rama-http-core/src/server/conn/http2.rs b/rama-http-core/src/server/conn/http2.rs index e6366407d..1efd42574 100644 --- a/rama-http-core/src/server/conn/http2.rs +++ b/rama-http-core/src/server/conn/http2.rs @@ -202,6 +202,14 @@ impl Builder { } } + /// Gets the [`SETTINGS_MAX_CONCURRENT_STREAMS`][spec] option used + /// for HTTP2 connections. + /// + /// [spec]: https://httpwg.org/specs/rfc9113.html#SETTINGS_MAX_CONCURRENT_STREAMS + pub fn max_concurrent_streams(&self) -> u32 { + self.h2_builder.max_concurrent_streams.unwrap_or(200) + } + rama_utils::macros::generate_set_and_with! { /// Sets an interval for HTTP2 Ping frames should be sent to keep a /// connection alive. diff --git a/rama-http-headers/README.md b/rama-http-headers/README.md index 7c53beece..0d3ac4663 100644 --- a/rama-http-headers/README.md +++ b/rama-http-headers/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-http-headers/src/util/entity.rs b/rama-http-headers/src/util/entity.rs index c28f143b5..2adee834d 100644 --- a/rama-http-headers/src/util/entity.rs +++ b/rama-http-headers/src/util/entity.rs @@ -118,13 +118,7 @@ impl> EntityTag { // "" b'"' => 1, // W/"" - b'W' => { - if length >= 4 && slice[1] == b'/' && slice[2] == b'"' { - 3 - } else { - return None; - } - } + b'W' if length >= 4 && slice[1] == b'/' && slice[2] == b'"' => 3, _ => return None, }; diff --git a/rama-http-types/README.md b/rama-http-types/README.md index 0816ab20e..1f8a311cb 100644 --- a/rama-http-types/README.md +++ b/rama-http-types/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-http-types/src/lib.rs b/rama-http-types/src/lib.rs index 7582dc16d..3ff341dc8 100644 --- a/rama-http-types/src/lib.rs +++ b/rama-http-types/src/lib.rs @@ -65,6 +65,8 @@ pub mod opentelemetry; pub mod conn; +pub mod proxy; + pub mod header { //! HTTP header types diff --git a/rama-http-types/src/proxy.rs b/rama-http-types/src/proxy.rs new file mode 100644 index 000000000..afc27980c --- /dev/null +++ b/rama-http-types/src/proxy.rs @@ -0,0 +1,38 @@ +use crate::{Method, Request, Version, proto::h2::ext::Protocol}; +use rama_core::{ + extensions::{Extensions, ExtensionsRef as _}, + matcher::Matcher, +}; + +/// Returns true if the provided reuqest is a HTTP Proxy Connect request. +pub fn is_req_http_proxy_connect(req: &Request) -> bool { + let http_version = req.version(); + if http_version <= Version::HTTP_11 { + req.method() == Method::CONNECT + } else if http_version == Version::HTTP_2 { + req.method() == Method::CONNECT && !req.extensions().contains::() + } else { + false + } +} + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +/// [`Matcher`] implementation which uses [`is_req_http_proxy_connect`]. +pub struct HttpProxyConnectMatcher; + +impl HttpProxyConnectMatcher { + #[inline(always)] + #[must_use] + /// Create a new [`HttpProxyConnectMatcher`]. + pub fn new() -> Self { + Self + } +} + +impl Matcher> for HttpProxyConnectMatcher { + #[inline(always)] + fn matches(&self, _ext: Option<&mut Extensions>, req: &Request) -> bool { + is_req_http_proxy_connect(req) + } +} diff --git a/rama-http/Cargo.toml b/rama-http/Cargo.toml index cdaee3d68..c8afc419f 100644 --- a/rama-http/Cargo.toml +++ b/rama-http/Cargo.toml @@ -34,22 +34,11 @@ tls = ["rama-net/tls"] [dependencies] ahash = { workspace = true } -async-compression = { workspace = true, features = [ - "tokio", - "brotli", - "zlib", - "gzip", - "zstd", -], optional = true } +async-compression = { workspace = true, features = ["tokio", "brotli", "zlib", "gzip", "zstd"], optional = true } base64 = { workspace = true } bitflags = { workspace = true } chrono = { workspace = true } -compression-codecs = { workspace = true, features = [ - "brotli", - "deflate", - "gzip", - "zstd", -], optional = true } +compression-codecs = { workspace = true, features = ["brotli", "deflate", "gzip", "zstd"], optional = true } compression-core = { workspace = true, optional = true } const_format = { workspace = true } csv = { workspace = true } diff --git a/rama-http/README.md b/rama-http/README.md index da9f3a773..c943bf74b 100644 --- a/rama-http/README.md +++ b/rama-http/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-http/src/io/upgrade.rs b/rama-http/src/io/upgrade.rs index c9ffd5855..ae6933713 100644 --- a/rama-http/src/io/upgrade.rs +++ b/rama-http/src/io/upgrade.rs @@ -48,8 +48,8 @@ use rama_core::error::BoxError; use rama_core::extensions::Extensions; use rama_core::extensions::ExtensionsMut; use rama_core::extensions::ExtensionsRef; -use rama_core::stream::Stream; -use rama_core::stream::rewind::Rewind; +use rama_core::io::Io; +use rama_core::io::rewind::Rewind; use rama_core::telemetry::tracing::trace; /// An upgraded HTTP connection. @@ -61,7 +61,7 @@ use rama_core::telemetry::tracing::trace; /// Alternatively, if the exact type is known, this can be deconstructed /// into its parts. pub struct Upgraded { - io: Rewind>, + io: Rewind>, extensions: Extensions, } @@ -151,7 +151,7 @@ impl Upgraded { /// Create a new [`Upgraded`] from an IO stream and existing buffer. pub fn new(io: T, read_buf: Bytes) -> Self where - T: Stream + Unpin + ExtensionsMut, + T: Io + Unpin + ExtensionsMut, { Self { extensions: io.extensions().clone(), @@ -163,7 +163,7 @@ impl Upgraded { /// /// On success, returns the downcasted parts. On error, returns the /// `Upgraded` back. - pub fn downcast(self) -> Result, Self> { + pub fn downcast(self) -> Result, Self> { let (io, buf) = self.io.into_inner(); match io.__downcast() { Ok(t) => Ok(Parts { @@ -179,25 +179,25 @@ impl Upgraded { } } -trait Io: Stream + Unpin { +trait UpgradeIo: Io + Unpin { fn __type_id(&self) -> TypeId { TypeId::of::() } } -impl Io for T {} +impl UpgradeIo for T {} -impl dyn Io { - fn __is(&self) -> bool { +impl dyn UpgradeIo { + fn __is(&self) -> bool { let t = TypeId::of::(); self.__type_id() == t } - fn __downcast(self: Box) -> Result, Box> { + fn __downcast(self: Box) -> Result, Box> { if self.__is::() { // Taken from `std::error::Error::downcast()`. unsafe { - let raw: *mut dyn Io = Box::into_raw(self); + let raw: *mut dyn UpgradeIo = Box::into_raw(self); Ok(Box::from_raw(raw as *mut T)) } } else { diff --git a/rama-http/src/layer/dpi_proxy_credential.rs b/rama-http/src/layer/dpi_proxy_credential.rs new file mode 100644 index 000000000..7279b79b7 --- /dev/null +++ b/rama-http/src/layer/dpi_proxy_credential.rs @@ -0,0 +1,87 @@ +//! Middleware that extracts credentials for an egress proxy +//! found in the Proxy-Authorization header of a passthrough request. +//! +//! See for more information: [`DpiProxyCredentialExtractor`] + +use crate::Request; +use crate::headers::{HeaderMapExt, ProxyAuthorization}; +use rama_core::extensions::ExtensionsMut; +use rama_core::telemetry::tracing; +use rama_core::{Layer, Service}; +use rama_http_types::proxy::is_req_http_proxy_connect; +use rama_net::user::credentials::DpiProxyCredential; +use rama_net::user::{Basic, Bearer, ProxyCredential}; +use rama_utils::macros::define_inner_service_accessors; + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +/// Layer that applies the [`DpiProxyCredentialExtractor`] middleware. +pub struct DpiProxyCredentialExtractorLayer; + +impl DpiProxyCredentialExtractorLayer { + #[inline(always)] + /// Creates a new [`DpiProxyCredentialExtractorLayer`]. + pub const fn new() -> Self { + Self + } +} + +impl Layer for DpiProxyCredentialExtractorLayer { + type Service = DpiProxyCredentialExtractor; + + #[inline(always)] + fn layer(&self, inner: S) -> Self::Service { + DpiProxyCredentialExtractor::new(inner) + } +} + +#[derive(Debug, Clone)] +/// Middleware that extracts credentials for an egress proxy +/// found in the Proxy-Authorization header of a passthrough request. +/// +/// This is useful for MITM proxies such as transparent (L4) proxies, to keep track of used +/// proxy credentials for meta purposes. +pub struct DpiProxyCredentialExtractor { + inner: S, +} + +impl DpiProxyCredentialExtractor { + #[inline(always)] + /// Creates a new [`DpiProxyCredentialExtractor`]. + pub const fn new(inner: S) -> Self { + Self { inner } + } + + define_inner_service_accessors!(); +} + +impl Service> for DpiProxyCredentialExtractor +where + S: Service>, + ReqBody: Send + 'static, +{ + type Output = S::Output; + type Error = S::Error; + + async fn serve(&self, mut req: Request) -> Result { + if is_req_http_proxy_connect(&req) { + tracing::trace!("DpiProxyCredentialExtractor: try to extract proxy authorization data"); + + if let Some(ProxyAuthorization::(credentials)) = req.headers().typed_get() { + tracing::debug!( + "DpiProxyCredentialExtractor: extracted Basic proxy auth: inserted in req extensions" + ); + req.extensions_mut() + .insert(DpiProxyCredential(ProxyCredential::Basic(credentials))); + } else if let Some(ProxyAuthorization::(token)) = req.headers().typed_get() { + tracing::debug!( + "DpiProxyCredentialExtractor: extracted Bearer proxy auth: inserted in req extensions" + ); + req.extensions_mut() + .insert(DpiProxyCredential(ProxyCredential::Bearer(token))); + } + } + + self.inner.serve(req).await + } +} diff --git a/rama-http/src/layer/mod.rs b/rama-http/src/layer/mod.rs index 9706d7776..5c72c66ad 100644 --- a/rama-http/src/layer/mod.rs +++ b/rama-http/src/layer/mod.rs @@ -23,6 +23,7 @@ pub mod classify; pub mod collect_body; pub mod cors; pub mod dns; +pub mod dpi_proxy_credential; pub mod error_handling; pub mod follow_redirect; pub mod forwarded; diff --git a/rama-macros/README.md b/rama-macros/README.md index 2b18e63de..9a9846d01 100644 --- a/rama-macros/README.md +++ b/rama-macros/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-net-apple-networkextension/Cargo.toml b/rama-net-apple-networkextension/Cargo.toml new file mode 100644 index 000000000..14dd61883 --- /dev/null +++ b/rama-net-apple-networkextension/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "rama-net-apple-networkextension" +description = "Apple Network Extension support for rama" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +keywords = ["io", "async", "non-blocking", "network", "rama"] +categories = ["asynchronous", "network-programming", "api-bindings", "security"] +authors = { workspace = true } +rust-version = { workspace = true } + +[package.metadata.cargo-public-api-crates] +allowed = [] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[features] +default = [] + +[dependencies] +parking_lot = { workspace = true } +pin-project-lite = { workspace = true } +rama-core = { workspace = true } +rama-macros = { workspace = true } +rama-net = { workspace = true } +rama-tcp = { workspace = true } +rama-udp = { workspace = true } +rama-utils = { workspace = true } +tokio = { workspace = true, features = ["io-util", "rt-multi-thread", "sync"] } +tracing = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } +tokio-test = { workspace = true } + +[lints] +workspace = true diff --git a/rama-net-apple-networkextension/README.md b/rama-net-apple-networkextension/README.md new file mode 100644 index 000000000..ff23c427c --- /dev/null +++ b/rama-net-apple-networkextension/README.md @@ -0,0 +1,51 @@ +[![rama banner](../docs/img/rama_banner.jpeg)](https://ramaproxy.org/) + +[![Crates.io][crates-badge]][crates-url] +[![Docs.rs][docs-badge]][docs-url] +[![MIT License][license-mit-badge]][license-mit-url] +[![Apache 2.0 License][license-apache-badge]][license-apache-url] +[![rust version][rust-version-badge]][rust-version-url] +[![Build Status][actions-badge]][actions-url] + +[![Discord][discord-badge]][discord-url] +[![Buy Me A Coffee][bmac-badge]][bmac-url] +[![GitHub Sponsors][ghs-badge]][ghs-url] +[![Paypal Donation][paypal-badge]][paypal-url] + +[crates-badge]: https://img.shields.io/crates/v/rama-net-apple-networkextension.svg +[crates-url]: https://crates.io/crates/rama-net-apple-networkextension +[docs-badge]: https://img.shields.io/docsrs/rama-net-apple-networkextension/latest +[docs-url]: https://docs.rs/rama-net-apple-networkextension/latest/rama_net/index.html +[license-mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT +[license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg +[license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust +[rust-version-url]: https://www.rust-lang.org +[actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main +[actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml + +[discord-badge]: https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white +[discord-url]: https://discord.gg/29EetaSYCD +[bmac-badge]: https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black +[bmac-url]: https://www.buymeacoffee.com/plabayo +[ghs-badge]: https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA +[ghs-url]: https://github.com/sponsors/plabayo +[paypal-badge]: https://img.shields.io/badge/paypal-contribution?style=for-the-badge&color=blue +[paypal-url]: https://www.paypal.com/donate/?hosted_button_id=P3KCGT2ACBVFE + +🦙 rama® (ラマ) is a modular service framework for the 🦀 Rust language to move and transform your network packets. +The reasons behind the creation of rama can be read in [the "Why Rama" chapter](https://ramaproxy.org/book/why_rama). + +## rama-net-apple-networkextension + +Rama network types and utilities. + +Apple Network Extension support for rama. + +Crate used by the end-user `rama` crate and `rama` crate authors alike. + +Learn more about `rama`: + +- Github: +- Book: diff --git a/rama-net-apple-networkextension/src/ffi/bytes.rs b/rama-net-apple-networkextension/src/ffi/bytes.rs new file mode 100644 index 000000000..f75ec34e3 --- /dev/null +++ b/rama-net-apple-networkextension/src/ffi/bytes.rs @@ -0,0 +1,69 @@ +use rama_core::error::BoxError; + +#[repr(C)] +#[derive(Debug)] +pub struct BytesOwned { + pub ptr: *mut u8, + pub len: usize, + pub cap: usize, +} + +impl BytesOwned { + /// # Safety + /// + /// `self` must come from this crate's FFI allocation path and must not have + /// been freed before. + pub unsafe fn free(self) { + let Self { ptr, len, cap } = self; + if ptr.is_null() || cap == 0 { + return; + } + + let vec_len = len.min(cap); + let vec_cap = cap; + // SAFETY: caller contract guarantees pointer/capacity originate from a `Vec`. + let _ = unsafe { Vec::from_raw_parts(ptr, vec_len, vec_cap) }; + } +} + +impl TryFrom> for BytesOwned { + type Error = BoxError; + + fn try_from(bytes: Vec) -> Result { + if bytes.is_empty() { + return Ok(Self { + ptr: std::ptr::null_mut(), + len: 0, + cap: 0, + }); + } + + let (ptr, vec_len, vec_cap) = bytes.into_raw_parts(); + Ok(Self { + ptr, + len: vec_len, + cap: vec_cap, + }) + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct BytesView { + pub ptr: *const u8, + pub len: usize, +} + +impl BytesView { + /// # Safety + /// + /// `self.ptr` must be valid for reads of `self.len` bytes for the returned + /// lifetime. + pub unsafe fn into_slice<'a>(self) -> &'a [u8] { + if self.ptr.is_null() || self.len == 0 { + return &[]; + } + // SAFETY: caller contract guarantees pointer validity. + unsafe { std::slice::from_raw_parts(self.ptr, self.len) } + } +} diff --git a/rama-net-apple-networkextension/src/ffi/log.rs b/rama-net-apple-networkextension/src/ffi/log.rs new file mode 100644 index 000000000..739f57c4b --- /dev/null +++ b/rama-net-apple-networkextension/src/ffi/log.rs @@ -0,0 +1,41 @@ +use super::BytesView; + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, +} + +impl LogLevel { + #[inline] + fn from_u32_or_debug(x: u32) -> Self { + if x <= Self::Error as u32 { + // SAFETY: repr(u32) and valid range 0..=4 maps to a real variant + unsafe { ::std::mem::transmute::(x) } + } else { + tracing::debug!("invalid raw u32 value transmuted as u32: {x} (defaulting it to DEBUG"); + Self::Debug + } + } +} + +/// # Safety +/// +/// `message.ptr` must be valid for reads of `message.len` bytes for the +/// duration of the call. +pub unsafe fn log_callback(level: u32, message: BytesView) { + // SAFETY: caller guarantees `message` is valid for the duration of this call. + let msg = String::from_utf8_lossy(unsafe { message.into_slice() }); + + match LogLevel::from_u32_or_debug(level) { + LogLevel::Trace => tracing::trace!("[FFI::log_callback] {}", msg.as_ref()), + LogLevel::Debug => tracing::debug!("[FFI::log_callback] {}", msg.as_ref()), + LogLevel::Info => tracing::info!("[FFI::log_callback] {}", msg.as_ref()), + LogLevel::Warn => tracing::warn!("[FFI::log_callback] {}", msg.as_ref()), + LogLevel::Error => tracing::error!("[FFI::log_callback] {}", msg.as_ref()), + } +} diff --git a/rama-net-apple-networkextension/src/ffi/mod.rs b/rama-net-apple-networkextension/src/ffi/mod.rs new file mode 100644 index 000000000..47a33a25c --- /dev/null +++ b/rama-net-apple-networkextension/src/ffi/mod.rs @@ -0,0 +1,7 @@ +mod bytes; +pub use bytes::{BytesOwned, BytesView}; + +mod log; +pub use log::{LogLevel, log_callback}; + +pub mod tproxy; diff --git a/rama-net-apple-networkextension/src/ffi/tproxy.rs b/rama-net-apple-networkextension/src/ffi/tproxy.rs new file mode 100644 index 000000000..2f4d6bc34 --- /dev/null +++ b/rama-net-apple-networkextension/src/ffi/tproxy.rs @@ -0,0 +1,285 @@ +use std::{ + ffi::{c_char, c_void}, + path::PathBuf, + ptr, +}; + +use rama_net::address::{Host, HostWithPort}; +use rama_utils::str::NonEmptyStr; + +use crate::ffi::BytesView; +use crate::tproxy::{self, TransparentProxyFlowProtocol}; + +#[repr(C)] +pub struct TransparentFlowEndpoint { + pub host_utf8: *const c_char, + pub host_utf8_len: usize, + pub port: u16, +} + +impl TransparentFlowEndpoint { + /// # Safety + /// + /// `self.host_utf8` must either be null, or point to at least + /// `self.host_utf8_len` bytes of valid UTF-8 for the duration of this call. + pub unsafe fn as_optional_host_with_port(&self) -> Option { + if self.port == 0 { + return None; + } + + // SAFETY: pointer + length validity is guaranteed by caller contract. + let host = unsafe { opt_utf8_to_host(self.host_utf8, self.host_utf8_len) }?; + Some(HostWithPort::new(host, self.port)) + } +} + +#[repr(C)] +pub struct TransparentProxyFlowMeta { + pub protocol: u32, + pub remote_endpoint: TransparentFlowEndpoint, + pub local_endpoint: TransparentFlowEndpoint, + pub source_app_signing_identifier_utf8: *const c_char, + pub source_app_signing_identifier_utf8_len: usize, + pub source_app_bundle_identifier_utf8: *const c_char, + pub source_app_bundle_identifier_utf8_len: usize, +} + +impl TransparentProxyFlowMeta { + /// # Safety + /// + /// All pointer + length fields in `self` must be valid for reads during + /// this call. + pub unsafe fn as_owned_rust_type(&self) -> tproxy::TransparentProxyFlowMeta { + tproxy::TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::from(self.protocol)) + .maybe_with_remote_endpoint( + // SAFETY: pointer + length validity is guaranteed by caller contract. + unsafe { self.remote_endpoint.as_optional_host_with_port() }, + ) + .maybe_with_local_endpoint( + // SAFETY: pointer + length validity is guaranteed by caller contract. + unsafe { self.local_endpoint.as_optional_host_with_port() }, + ) + .maybe_with_source_app_signing_identifier( + // SAFETY: pointer + length validity is guaranteed by caller contract. + unsafe { + opt_utf8_to_non_empty_str( + self.source_app_signing_identifier_utf8, + self.source_app_signing_identifier_utf8_len, + ) + }, + ) + .maybe_with_source_app_bundle_identifier( + // SAFETY: pointer + length validity is guaranteed by caller contract. + unsafe { + opt_utf8_to_non_empty_str( + self.source_app_bundle_identifier_utf8, + self.source_app_bundle_identifier_utf8_len, + ) + }, + ) + } +} + +#[repr(C)] +pub struct TransparentProxyNetworkRule { + pub remote_network_utf8: *const c_char, + pub remote_network_utf8_len: usize, + pub remote_prefix: u8, + pub remote_prefix_is_set: bool, + pub local_network_utf8: *const c_char, + pub local_network_utf8_len: usize, + pub local_prefix: u8, + pub local_prefix_is_set: bool, + pub protocol: u32, +} + +#[repr(C)] +pub struct TransparentProxyConfig { + pub tunnel_remote_address_utf8: *const c_char, + pub tunnel_remote_address_utf8_len: usize, + pub rules: *const TransparentProxyNetworkRule, + pub rules_len: usize, +} + +#[repr(C)] +pub struct TransparentProxyInitConfig { + pub storage_dir_utf8: *const c_char, + pub storage_dir_utf8_len: usize, + pub app_group_dir_utf8: *const c_char, + pub app_group_dir_utf8_len: usize, +} + +impl TransparentProxyInitConfig { + /// # Safety + /// + /// Pointer + length pairs in `self` must be valid for reads during this call. + pub unsafe fn storage_dir(&self) -> Option { + // SAFETY: pointer + length validity is guaranteed by caller contract. + unsafe { opt_utf8(self.storage_dir_utf8, self.storage_dir_utf8_len) }.map(PathBuf::from) + } + + /// # Safety + /// + /// Pointer + length pairs in `self` must be valid for reads during this call. + pub unsafe fn app_group_dir(&self) -> Option { + // SAFETY: pointer + length validity is guaranteed by caller contract. + unsafe { opt_utf8(self.app_group_dir_utf8, self.app_group_dir_utf8_len) }.map(PathBuf::from) + } +} + +impl TransparentProxyConfig { + /// Build an owned FFI representation from typed Rust config. + #[must_use] + pub fn from_rust_type(config: &tproxy::TransparentProxyConfig) -> Self { + let (tunnel_remote_address_utf8, tunnel_remote_address_utf8_len) = + alloc_str_utf8(config.tunnel_remote_address()); + + let mut rules = Vec::with_capacity(config.rules().len()); + for rule in config.rules() { + let (remote_network_utf8, remote_network_utf8_len) = + opt_string_as_utf8_array(rule.remote_network().map(ToString::to_string)); + let (local_network_utf8, local_network_utf8_len) = + opt_string_as_utf8_array(rule.local_network().map(ToString::to_string)); + + rules.push(TransparentProxyNetworkRule { + remote_network_utf8, + remote_network_utf8_len, + remote_prefix: rule.remote_prefix().unwrap_or(0), + remote_prefix_is_set: rule.remote_prefix().is_some(), + local_network_utf8, + local_network_utf8_len, + local_prefix: rule.local_prefix().unwrap_or(0), + local_prefix_is_set: rule.local_prefix().is_some(), + protocol: rule.protocol().as_u32(), + }); + } + + let boxed_rules = rules.into_boxed_slice(); + let rules_len = boxed_rules.len(); + let rules = if rules_len == 0 { + ptr::null() + } else { + Box::into_raw(boxed_rules) as *const TransparentProxyNetworkRule + }; + + Self { + tunnel_remote_address_utf8, + tunnel_remote_address_utf8_len, + rules, + rules_len, + } + } + + /// # Safety + /// + /// `self` must have been created by [`TransparentProxyConfig::from_rust_type`] + /// exactly once. Calling this twice on the same allocations is undefined behavior. + pub unsafe fn free(self) { + // SAFETY: this pointer/len pair came from `alloc_utf8` in `from_rust_type`. + unsafe { + free_utf8( + self.tunnel_remote_address_utf8, + self.tunnel_remote_address_utf8_len, + ) + }; + + if self.rules.is_null() || self.rules_len == 0 { + return; + } + + let rules_ptr = self.rules as *mut TransparentProxyNetworkRule; + let boxed_rules = { + let raw_slice = ptr::slice_from_raw_parts_mut(rules_ptr, self.rules_len); + // SAFETY: `raw_slice` was produced via `Box::into_raw` in `from_rust_type`. + unsafe { Box::from_raw(raw_slice) } + }; + + for rule in boxed_rules.iter() { + // SAFETY: these pointer/len pairs came from `alloc_opt_utf8` in `from_rust_type`. + unsafe { free_utf8(rule.remote_network_utf8, rule.remote_network_utf8_len) }; + // SAFETY: these pointer/len pairs came from `alloc_opt_utf8` in `from_rust_type`. + unsafe { free_utf8(rule.local_network_utf8, rule.local_network_utf8_len) }; + } + } +} + +#[repr(C)] +pub struct TransparentProxyTcpSessionCallbacks { + pub context: *mut c_void, + pub on_server_bytes: Option, + pub on_server_closed: Option, +} + +#[repr(C)] +pub struct TransparentProxyUdpSessionCallbacks { + pub context: *mut c_void, + pub on_server_datagram: Option, + pub on_server_closed: Option, +} + +fn opt_string_as_utf8_array(value: Option) -> (*const c_char, usize) { + if let Some(s) = value { + alloc_vec_utf8(s.into_bytes()) + } else { + (ptr::null(), 0) + } +} + +#[inline(always)] +fn alloc_str_utf8(value: &str) -> (*const c_char, usize) { + alloc_vec_utf8(value.as_bytes().to_vec()) +} + +fn alloc_vec_utf8(value: Vec) -> (*const c_char, usize) { + let boxed: Box<[u8]> = value.into_boxed_slice(); + let len = boxed.len(); + if len == 0 { + return (ptr::null(), 0); + } + (Box::into_raw(boxed) as *const u8 as *const c_char, len) +} + +/// # Safety +/// +/// `ptr/len` must come from `alloc_utf8` and must not be freed more than once. +unsafe fn free_utf8(ptr: *const c_char, len: usize) { + if ptr.is_null() || len == 0 { + return; + } + + let raw_slice = ptr::slice_from_raw_parts_mut(ptr as *mut u8, len); + // SAFETY: caller guarantees this points to memory allocated via `alloc_utf8`. + let _ = unsafe { Box::from_raw(raw_slice) }; +} + +/// # Safety +/// +/// `ptr` must be null or readable for `len` bytes and contain UTF-8. +unsafe fn opt_utf8_to_non_empty_str(ptr: *const c_char, len: usize) -> Option { + // SAFETY: pointer + length validity is guaranteed by caller contract. + let raw = unsafe { opt_utf8(ptr, len) }?; + raw.try_into().ok() +} + +/// # Safety +/// +/// `ptr` must be null or readable for `len` bytes and contain UTF-8. +unsafe fn opt_utf8_to_host(ptr: *const c_char, len: usize) -> Option { + // SAFETY: pointer + length validity is guaranteed by caller contract. + let raw = unsafe { opt_utf8(ptr, len) }?; + Host::try_from(raw).ok() +} + +/// # Safety +/// +/// `ptr` must be null or readable for `len` bytes and contain UTF-8. +unsafe fn opt_utf8<'a>(ptr: *const c_char, len: usize) -> Option<&'a str> { + if ptr.is_null() || len == 0 { + return None; + } + + // SAFETY: pointer + length validity is guaranteed by caller contract. + let raw = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }; + let text = std::str::from_utf8(raw).ok()?.trim(); + (!text.is_empty()).then_some(text) +} diff --git a/rama-net-apple-networkextension/src/lib.rs b/rama-net-apple-networkextension/src/lib.rs new file mode 100644 index 000000000..bf1286949 --- /dev/null +++ b/rama-net-apple-networkextension/src/lib.rs @@ -0,0 +1,32 @@ +//! Apple Network Extension support for rama. +//! +//! Official Apple documentation about the +//! Network Extension Framework can be consulted at: +//! . +//! +//! Learn more about `rama`: +//! +//! - Github: +//! - Book: + +#![doc( + html_favicon_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/old_logo.png" +)] +#![doc(html_logo_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/old_logo.png")] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg(target_vendor = "apple")] +#![cfg_attr(test, allow(clippy::float_cmp))] +#![cfg_attr( + not(test), + warn(clippy::print_stdout, clippy::dbg_macro), + deny(clippy::unwrap_used, clippy::expect_used) +)] + +#[doc(hidden)] +pub mod ffi; +pub mod tproxy; + +mod tcp; +mod udp; + +pub use self::{tcp::TcpFlow, udp::UdpFlow}; diff --git a/rama-net-apple-networkextension/src/tcp.rs b/rama-net-apple-networkextension/src/tcp.rs new file mode 100644 index 000000000..d7242cd52 --- /dev/null +++ b/rama-net-apple-networkextension/src/tcp.rs @@ -0,0 +1,95 @@ +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; + +use pin_project_lite::pin_project; +use rama_core::{ + ServiceInput, + extensions::{Extensions, ExtensionsMut, ExtensionsRef}, +}; +use tokio::io::{AsyncRead, AsyncWrite, DuplexStream, ReadBuf}; + +pin_project! { + /// A per-flow stream presented to the Rama user. + /// + /// This behaves like a normal bidirectional byte stream and implements + /// tokio [`AsyncRead`] + [`AsyncWrite`] + Rama [`Extensions`]. + pub struct TcpFlow { + #[pin] + inner: DuplexStream, + extensions: Extensions, + } +} + +impl TcpFlow { + #[must_use] + /// Create a new [`TcpFlow`]. + pub(crate) fn new(inner: DuplexStream) -> Self { + Self { + inner, + extensions: Extensions::new(), + } + } + + /// Consume the [`TcpFlow`] by mapping the input and + /// returning this as a new generic [`ServiceInput`]. + pub fn map_input(self, map: impl FnOnce(DuplexStream) -> Input) -> ServiceInput { + let Self { + inner: duplex_stream, + extensions, + } = self; + + ServiceInput { + input: map(duplex_stream), + extensions, + } + } +} + +impl ExtensionsRef for TcpFlow { + #[inline(always)] + fn extensions(&self) -> &Extensions { + &self.extensions + } +} + +impl ExtensionsMut for TcpFlow { + #[inline(always)] + fn extensions_mut(&mut self) -> &mut Extensions { + &mut self.extensions + } +} + +impl AsyncRead for TcpFlow { + #[inline(always)] + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.project().inner.poll_read(cx, buf) + } +} + +impl AsyncWrite for TcpFlow { + #[inline(always)] + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.project().inner.poll_write(cx, buf) + } + + #[inline(always)] + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().inner.poll_flush(cx) + } + + #[inline(always)] + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.project().inner.poll_shutdown(cx) + } +} diff --git a/rama-net-apple-networkextension/src/tproxy/engine.rs b/rama-net-apple-networkextension/src/tproxy/engine.rs new file mode 100644 index 000000000..231ec58d0 --- /dev/null +++ b/rama-net-apple-networkextension/src/tproxy/engine.rs @@ -0,0 +1,610 @@ +use rama_core::{ + bytes::Bytes, + extensions::{ExtensionsMut, ExtensionsRef}, + graceful::{Shutdown, ShutdownGuard}, + io::BridgeIo, + rt::Executor, + service::{BoxService, Service, service_fn}, +}; +use rama_net::{ + conn::is_connection_error, + proxy::{IoForwardService, ProxyTarget}, +}; +use rama_tcp::client::default_tcp_connect; + +use parking_lot::Mutex; +use rama_udp::bind_udp_socket_with_connect_default_dns; +use std::{convert::Infallible, sync::Arc}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + sync::{mpsc, oneshot, watch}, +}; + +use crate::{TcpFlow, UdpFlow, tproxy::TransparentProxyFlowMeta}; + +const DEFAULT_TCP_FLOW_BUFFER_SIZE: usize = 64 * 1024; // 64 KiB + +#[derive(Default)] +struct EngineState { + running: bool, + shutdown: Option, + stop_trigger: Option>, +} + +type TcpFlowService = BoxService; +type UdpFlowService = BoxService; +type BytesSink = Arc; +type ClosedSink = Arc; + +#[derive(Default)] +pub struct TransparentProxyEngineBuilder { + tcp_service: Option, + tcp_flow_buffer_size: Option, + udp_service: Option, + runtime: Option, +} + +impl TransparentProxyEngineBuilder { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + rama_utils::macros::generate_set_and_with! { + /// Set a custom [`TcpFlow`] [`Service`]. + /// + /// Default TCP Service (if UDP is intercepted at all, + /// forwards bytes as-is, without inspection). + pub fn tcp_service(mut self, svc: impl Service) -> Self + { + self.tcp_service = Some(svc.boxed()); + self + } + } + + rama_utils::macros::generate_set_and_with! { + /// Define what size to use for the TCP flow buffer (`None` will use default) + pub fn tcp_flow_buffer_size(mut self, size: Option) -> Self + { + self.tcp_flow_buffer_size = size; + self + } + } + + rama_utils::macros::generate_set_and_with! { + /// Set a custom [`UdpFlow`] [`Service`]. + /// + /// Default UDP Service (if UDP is intercepted at all, + /// forwards bytes as-is, without inspection). + pub fn udp_service(mut self, svc: impl Service) -> Self + { + self.udp_service = Some(svc.boxed()); + self + } + } + + rama_utils::macros::generate_set_and_with! { + /// define the Tokio runtime for the transparent proxy engine. + pub fn runtime(mut self, runtime: Option) -> Self { + self.runtime = runtime; + self + } + } + + #[must_use] + pub fn build(self) -> TransparentProxyEngine { + let tcp_service = self.tcp_service.unwrap_or_else(default_tcp_service); + let tcp_flow_buffer_size = self + .tcp_flow_buffer_size + .unwrap_or(DEFAULT_TCP_FLOW_BUFFER_SIZE); + let udp_service = self.udp_service.unwrap_or_else(default_udp_service); + let runtime = self.runtime.unwrap_or_else(build_default_runtime); + + TransparentProxyEngine { + rt: runtime, + tcp_service, + tcp_flow_buffer_size, + udp_service, + state: Mutex::new(EngineState::default()), + } + } +} + +pub struct TransparentProxyEngine { + rt: tokio::runtime::Runtime, + tcp_service: TcpFlowService, + tcp_flow_buffer_size: usize, + udp_service: UdpFlowService, + state: Mutex, +} + +impl TransparentProxyEngine { + pub fn start(&self) { + let mut state = self.state.lock(); + + if state.running { + tracing::trace!("transparent proxy engine already running"); + return; + } + + let (stop_tx, stop_rx) = oneshot::channel::<()>(); + let shutdown = { + let _enter = self.rt.enter(); + Shutdown::new(async move { + let _ = stop_rx.await; + }) + }; + + state.running = true; + state.shutdown = Some(shutdown); + state.stop_trigger = Some(stop_tx); + tracing::info!("transparent proxy engine started"); + } + + pub fn stop(&self, reason: i32) { + let (shutdown, stop_trigger) = { + let mut state = self.state.lock(); + + if !state.running { + tracing::trace!("transparent proxy engine already stopped"); + return; + } + + state.running = false; + (state.shutdown.take(), state.stop_trigger.take()) + }; + + tracing::info!(reason, "transparent proxy engine stopping"); + + if let Some(stop_trigger) = stop_trigger { + let _ = stop_trigger.send(()); + } + + if let Some(shutdown) = shutdown { + self.rt.block_on(async move { + shutdown.shutdown().await; + }); + } + + tracing::info!(reason, "transparent proxy engine stopped"); + } + + pub fn is_running(&self) -> bool { + let state = self.state.lock(); + state.running + } + + pub fn new_tcp_session( + &self, + meta: TransparentProxyFlowMeta, + on_server_bytes: F, + on_server_closed: G, + ) -> Option + where + F: Fn(Bytes) + Send + Sync + 'static, + G: Fn() + Send + Sync + 'static, + { + let guard = self.shutdown_guard()?; + + let (user_stream, internal_stream) = tokio::io::duplex(self.tcp_flow_buffer_size); + let (client_tx, client_rx) = mpsc::unbounded_channel::(); + let (eof_tx, eof_rx) = watch::channel(false); + + let service = self.tcp_service.clone(); + let bytes_sink: BytesSink = Arc::new(on_server_bytes); + let closed_sink: ClosedSink = Arc::new(on_server_closed); + let remote_endpoint = meta.remote_endpoint.clone(); + + tracing::debug!(protocol = ?meta.protocol, "new tcp session"); + + let _enter = self.rt.enter(); + + guard.spawn_task(run_tcp_bridge( + internal_stream, + client_rx, + eof_rx, + bytes_sink, + closed_sink, + )); + + let mut stream = TcpFlow::new(user_stream); + stream.extensions_mut().insert(Arc::new(meta)); + if let Some(remote) = remote_endpoint { + stream.extensions_mut().insert(ProxyTarget(remote)); + } + + guard.spawn_task_fn(async move |guard| { + stream.extensions_mut().insert(guard); + let _ = service.serve(stream).await; + }); + + Some(TransparentProxyTcpSession { client_tx, eof_tx }) + } + + #[allow(clippy::needless_pass_by_value)] + pub fn new_udp_session( + &self, + meta: TransparentProxyFlowMeta, + on_server_datagram: F, + on_server_closed: G, + ) -> Option + where + F: Fn(Bytes) + Send + Sync + 'static, + G: Fn() + Send + Sync + 'static, + { + let guard = self.shutdown_guard()?; + + let (client_tx, client_rx) = mpsc::unbounded_channel::(); + + let service = self.udp_service.clone(); + let datagram_sink: BytesSink = Arc::new(on_server_datagram); + let closed_sink: ClosedSink = Arc::new(on_server_closed); + let remote_endpoint = meta.remote_endpoint.clone(); + + tracing::debug!(protocol = ?meta.protocol, "new udp session"); + + let mut flow = UdpFlow::new(client_rx, datagram_sink); + flow.extensions_mut().insert(guard.clone()); + if let Some(remote) = remote_endpoint { + flow.extensions_mut().insert(ProxyTarget(remote)); + } + + let _enter = self.rt.enter(); + guard.spawn_task_fn(async move |guard| { + flow.extensions_mut().insert(guard); + let _ = service.serve(flow).await; + closed_sink(); + }); + + Some(TransparentProxyUdpSession { + client_tx: Some(client_tx), + }) + } + + fn shutdown_guard(&self) -> Option { + let state = self.state.lock(); + + if !state.running { + tracing::warn!("session rejected: engine not running"); + return None; + } + + let shutdown = state.shutdown.as_ref()?; + Some(shutdown.guard()) + } +} + +fn build_default_runtime() -> tokio::runtime::Runtime { + match tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(err) => panic!("failed to build tokio runtime: {err}"), + } +} + +pub struct TransparentProxyTcpSession { + client_tx: mpsc::UnboundedSender, + eof_tx: watch::Sender, +} + +impl TransparentProxyTcpSession { + pub fn on_client_bytes(&mut self, bytes: &[u8]) { + if bytes.is_empty() { + return; + } + let _ = self.client_tx.send(Bytes::copy_from_slice(bytes)); + } + + pub fn on_client_eof(&mut self) { + let _ = self.eof_tx.send(true); + } +} + +pub struct TransparentProxyUdpSession { + client_tx: Option>, +} + +impl TransparentProxyUdpSession { + pub fn on_client_datagram(&mut self, bytes: &[u8]) { + if bytes.is_empty() { + return; + } + + if let Some(tx) = self.client_tx.as_mut() { + let _ = tx.send(Bytes::copy_from_slice(bytes)); + } + } + + pub fn on_client_close(&mut self) { + self.client_tx = None; + } +} + +fn default_tcp_service() -> TcpFlowService { + tracing::debug!("using default tcp service (dumb L4 forward)"); + service_fn(|ingress_stream: TcpFlow| async move { + let Some(ProxyTarget(target)) = ingress_stream.extensions().get().cloned() else { + tracing::warn!("default tcp service missing target endpoint"); + return Ok(()); + }; + + let extensions = ingress_stream.extensions(); + let exec = extensions + .get() + .cloned() + .map(Executor::graceful) + .unwrap_or_default(); + + let Ok((egress_stream, _sock_addr)) = default_tcp_connect(extensions, target, exec).await + else { + tracing::warn!("default tcp connect failed"); + return Ok(()); + }; + + let req = BridgeIo(ingress_stream, egress_stream); + if let Err(err) = IoForwardService::new().serve(req).await { + tracing::warn!(%err, "default tcp forward failed"); + } + Ok(()) + }) + .boxed() +} + +fn default_udp_service() -> UdpFlowService { + tracing::debug!("using default udp service (dumb L4 forward)"); + service_fn(|mut flow: UdpFlow| async move { + let Some(ProxyTarget(target_addr)) = flow.extensions().get().cloned() else { + tracing::warn!("default udp service missing target endpoint"); + while flow.recv().await.is_some() {} + return Ok(()); + }; + + let socket = match bind_udp_socket_with_connect_default_dns( + target_addr.clone(), + Some(flow.extensions()), + ) + .await + { + Ok(socket) => socket, + Err(err) => { + tracing::error!(error = %err, "default udp (forward) service: udp bind failed w/ bind + connect to address: {target_addr}"); + while flow.recv().await.is_some() {} + return Ok(()); + } + }; + + tracing::info!( + remote = %target_addr, + local_addr = ?socket.local_addr().ok(), + peer_addr = ?socket.peer_addr().ok(), + "default udp (forward) service started" + ); + + let mut buf = vec![0u8; 64 * 1024]; + loop { + tokio::select! { + maybe_datagram = flow.recv() => { + let Some(datagram) = maybe_datagram else { break; }; + if let Err(err) = socket.send(&datagram).await { + tracing::warn!(%err, "default udp send failed"); + break; + } + } + recv_result = socket.recv(&mut buf) => { + match recv_result { + Ok(0) => break, + Ok(n) => flow.send(Bytes::copy_from_slice(&buf[..n])), + Err(err) => { + tracing::warn!(%err, "default udp recv failed"); + break; + } + } + } + } + } + Ok(()) + }) + .boxed() +} + +async fn run_tcp_bridge( + internal: tokio::io::DuplexStream, + mut client_rx: mpsc::UnboundedReceiver, + mut eof_rx: watch::Receiver, + on_server_bytes: BytesSink, + on_server_closed: ClosedSink, +) { + let (mut read_half, mut write_half) = tokio::io::split(internal); + let mut buf = vec![0u8; 16 * 1024]; + + loop { + tokio::select! { + maybe = client_rx.recv() => { + match maybe { + Some(bytes) => { + if let Err(err) = write_half.write_all(&bytes).await { + if is_connection_error(&err) { + tracing::trace!("tcp bridge write_all conn erorr: {err}"); + } else { + tracing::debug!("tcp bridge write_all failed: {err}"); + } + break; + } + } + None => { + let _ = write_half.shutdown().await; + } + } + } + _ = eof_rx.changed() => { + if *eof_rx.borrow() { + let _ = write_half.shutdown().await; + } + } + read_res = read_half.read(&mut buf) => { + match read_res { + Ok(0) => break, + Err(err) => { + if is_connection_error(&err) { + tracing::trace!("tcp bridge read_half conn erorr: {err}"); + } else { + tracing::debug!("tcp bridge read_half failed: {err}"); + } + break; + } + Ok(n) => on_server_bytes(Bytes::copy_from_slice(&buf[..n])), + } + } + } + } + + on_server_closed(); +} + +#[cfg(test)] +mod tests { + use crate::tproxy::TransparentProxyFlowProtocol; + + use super::*; + use parking_lot::Mutex; + use rama_net::address::HostWithPort; + use std::sync::Arc; + + #[test] + fn engine_start_stop_state() { + let engine = TransparentProxyEngineBuilder::new() + .with_runtime( + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(), + ) + .build(); + assert!(!engine.is_running()); + engine.start(); + assert!(engine.is_running()); + engine.stop(0); + assert!(!engine.is_running()); + } + + #[test] + fn session_rejected_if_not_running() { + let engine = TransparentProxyEngineBuilder::new() + .with_runtime( + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(), + ) + .build(); + let session = engine.new_tcp_session( + TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::Tcp), + |_| {}, + || {}, + ); + assert!(session.is_none()); + } + + #[test] + fn session_rejected_after_stop() { + let engine = TransparentProxyEngineBuilder::new() + .with_runtime( + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(), + ) + .build(); + engine.start(); + engine.stop(0); + let session = engine.new_tcp_session( + TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::Tcp), + |_| {}, + || {}, + ); + assert!(session.is_none()); + } + + #[test] + fn tcp_bridge_delivers_server_bytes() { + let got = Arc::new(Mutex::new(Vec::::new())); + let got_clone = got.clone(); + let (notify_tx, notify_rx) = std::sync::mpsc::channel::<()>(); + + let engine = TransparentProxyEngineBuilder::new() + .with_tcp_service(service_fn(|mut stream: TcpFlow| async move { + let _ = stream.write_all(b"pong").await; + Ok(()) + })) + .with_runtime( + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(), + ) + .build(); + + engine.start(); + let mut session = engine + .new_tcp_session( + TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::Tcp) + .with_remote_endpoint(HostWithPort::example_domain_with_port(80)), + move |bytes| { + let mut lock = got_clone.lock(); + lock.extend_from_slice(&bytes); + let _ = notify_tx.send(()); + }, + || {}, + ) + .expect("session"); + + session.on_client_bytes(b"ping"); + + let _ = notify_rx.recv_timeout(std::time::Duration::from_secs(1)); + engine.stop(0); + + let lock = got.lock(); + assert_eq!(lock.as_slice(), b"pong"); + } + + #[test] + fn udp_bridge_delivers_server_datagram() { + let got = Arc::new(Mutex::new(Vec::::new())); + let got_clone = got.clone(); + let (notify_tx, notify_rx) = std::sync::mpsc::channel::<()>(); + + let engine = TransparentProxyEngineBuilder::new() + .with_runtime( + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(), + ) + .with_udp_service(service_fn(|mut flow: UdpFlow| async move { + if let Some(datagram) = flow.recv().await { + flow.send(datagram); + } + Ok(()) + })) + .build(); + + engine.start(); + let mut session = engine + .new_udp_session( + TransparentProxyFlowMeta::new(TransparentProxyFlowProtocol::Udp) + .with_remote_endpoint(HostWithPort::local_ipv4(5353)), + move |bytes| { + let mut lock = got_clone.lock(); + lock.extend_from_slice(&bytes); + let _ = notify_tx.send(()); + }, + || {}, + ) + .expect("session"); + + session.on_client_datagram(b"ping"); + + let _ = notify_rx.recv_timeout(std::time::Duration::from_secs(1)); + engine.stop(0); + + let lock = got.lock(); + assert_eq!(lock.as_slice(), b"ping"); + } +} diff --git a/rama-net-apple-networkextension/src/tproxy/mod.rs b/rama-net-apple-networkextension/src/tproxy/mod.rs new file mode 100644 index 000000000..0a7a5eaff --- /dev/null +++ b/rama-net-apple-networkextension/src/tproxy/mod.rs @@ -0,0 +1,13 @@ +mod engine; +mod types; + +pub use self::{ + engine::{ + TransparentProxyEngine, TransparentProxyEngineBuilder, TransparentProxyTcpSession, + TransparentProxyUdpSession, + }, + types::{ + TransparentProxyConfig, TransparentProxyFlowMeta, TransparentProxyFlowProtocol, + TransparentProxyNetworkRule, TransparentProxyRuleProtocol, + }, +}; diff --git a/rama-net-apple-networkextension/src/tproxy/types.rs b/rama-net-apple-networkextension/src/tproxy/types.rs new file mode 100644 index 000000000..fe6ac6e45 --- /dev/null +++ b/rama-net-apple-networkextension/src/tproxy/types.rs @@ -0,0 +1,282 @@ +use rama_net::address::{Host, HostWithPort}; +use rama_utils::{ + macros::generate_set_and_with, + str::{NonEmptyStr, arcstr::ArcStr}, +}; + +/// Protocol filter used by transparent-proxy network rules. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TransparentProxyRuleProtocol { + /// Match both TCP and UDP. + Any = 0, + /// Match TCP only. + Tcp = 1, + /// Match UDP only. + Udp = 2, +} + +impl TransparentProxyRuleProtocol { + #[inline(always)] + pub fn as_u32(self) -> u32 { + self as u32 + } +} + +impl From for TransparentProxyRuleProtocol { + fn from(value: u32) -> Self { + if value <= Self::Udp as u32 { + // SAFETY: repr(u32) and valid range + unsafe { ::std::mem::transmute::(value) } + } else { + tracing::debug!( + "invalid raw u32 value transmuted as TransparentProxyRuleProtocol: {value} (defaulting it to Any)" + ); + Self::Any + } + } +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TransparentProxyFlowProtocol { + Tcp = 1, + Udp = 2, +} + +impl TransparentProxyFlowProtocol { + #[inline(always)] + pub fn as_u32(self) -> u32 { + self as u32 + } +} + +impl From for TransparentProxyFlowProtocol { + fn from(value: u32) -> Self { + if value <= Self::Udp as u32 { + // SAFETY: repr(u32) and valid range + unsafe { ::std::mem::transmute::(value) } + } else { + tracing::debug!( + "invalid raw u32 value transmuted as TransparentProxyFlowProtocol: {value} (defaulting it to TCP)" + ); + Self::Tcp + } + } +} + +/// One network interception rule for transparent proxy settings. +#[derive(Clone, Debug)] +pub struct TransparentProxyNetworkRule { + remote_network: Option, + remote_prefix: Option, + local_network: Option, + local_prefix: Option, + protocol: TransparentProxyRuleProtocol, +} + +impl TransparentProxyNetworkRule { + /// Create an "all traffic" rule. + #[must_use] + pub fn any() -> Self { + Self { + remote_network: None, + remote_prefix: None, + local_network: None, + local_prefix: None, + protocol: TransparentProxyRuleProtocol::Any, + } + } + + /// Optional remote network as domain or IP address. + #[must_use] + pub fn remote_network(&self) -> Option<&Host> { + self.remote_network.as_ref() + } + + /// Prefix length for `remote_network`, if set. + #[must_use] + pub const fn remote_prefix(&self) -> Option { + self.remote_prefix + } + + /// Optional local network as domain or IP address. + #[must_use] + pub fn local_network(&self) -> Option<&Host> { + self.local_network.as_ref() + } + + /// Prefix length for `local_network`, if set. + #[must_use] + pub const fn local_prefix(&self) -> Option { + self.local_prefix + } + + /// Rule protocol filter. + #[must_use] + pub const fn protocol(&self) -> TransparentProxyRuleProtocol { + self.protocol + } + + generate_set_and_with! { + /// Set remote network. + pub fn remote_network(mut self, network: impl Into) -> Self { + self.remote_network = Some(network.into()); + self + } + } + + generate_set_and_with! { + /// Set local network. + pub fn local_network(mut self, network: impl Into) -> Self { + self.local_network = Some(network.into()); + self + } + } + + generate_set_and_with! { + /// Set remote network prefix. + pub fn remote_network_prefix(mut self, prefix: u8) -> Self { + self.remote_prefix = Some(prefix); + self + } + } + + generate_set_and_with! { + /// Set local network prefix. + pub fn local_network_prefix(mut self, prefix: u8) -> Self { + self.local_prefix = Some(prefix); + self + } + } + + generate_set_and_with! { + /// Set protocol filter. + pub fn protocol(mut self, protocol: TransparentProxyRuleProtocol) -> Self { + self.protocol = protocol; + self + } + } +} + +/// Engine-level transparent proxy configuration. +/// +/// This configuration is long-lived and shared by all flows handled by one +/// [`crate::tproxy::TransparentProxyEngine`]. +#[derive(Clone, Debug)] +pub struct TransparentProxyConfig { + tunnel_remote_address: ArcStr, + rules: Vec, +} + +impl TransparentProxyConfig { + /// Create an empty configuration. + #[must_use] + pub fn new() -> Self { + Self { + tunnel_remote_address: ArcStr::from("127.0.0.1"), + rules: vec![TransparentProxyNetworkRule::any()], + } + } + + /// Placeholder tunnel remote address for `NETransparentProxyNetworkSettings`. + /// + /// Apple requires this field when constructing tunnel settings, even for + /// transparent proxy providers where this is not used as a real upstream. + #[must_use] + pub fn tunnel_remote_address(&self) -> &str { + &self.tunnel_remote_address + } + + /// Network interception rules for `NETransparentProxyNetworkSettings`. + #[must_use] + pub fn rules(&self) -> &[TransparentProxyNetworkRule] { + &self.rules + } + + generate_set_and_with! { + /// Set tunnel remote address placeholder. + pub fn tunnel_remote_address(mut self, tunnel_remote_address: ArcStr) -> Self { + self.tunnel_remote_address = tunnel_remote_address; + self + } + } + + generate_set_and_with! { + /// Set interception rules. + pub fn rules(mut self, rules: Vec) -> Self { + self.rules = rules; + self + } + } +} + +impl Default for TransparentProxyConfig { + fn default() -> Self { + Self::new() + } +} + +/// Per-flow transparent proxy metadata. +/// +/// This metadata is specific to one intercepted flow and is injected into the +/// flow input extensions for user services. +#[derive(Clone, Debug)] +pub struct TransparentProxyFlowMeta { + /// Transport protocol for this flow. + pub protocol: TransparentProxyFlowProtocol, + /// Remote endpoint for this flow, if known. + pub remote_endpoint: Option, + /// Local endpoint for this flow, if known. + pub local_endpoint: Option, + /// Signing identifier of the source app, if available. + pub source_app_signing_identifier: Option, + /// Bundle identifier of the source app, if available. + pub source_app_bundle_identifier: Option, +} + +impl TransparentProxyFlowMeta { + /// Create flow metadata from strongly typed fields. + #[must_use] + pub fn new(protocol: TransparentProxyFlowProtocol) -> Self { + Self { + protocol, + remote_endpoint: None, + local_endpoint: None, + source_app_signing_identifier: None, + source_app_bundle_identifier: None, + } + } + + generate_set_and_with! { + /// Set remote endpoint. + pub fn remote_endpoint(mut self, endpoint: Option) -> Self { + self.remote_endpoint = endpoint; + self + } + } + + generate_set_and_with! { + /// Set local endpoint. + pub fn local_endpoint(mut self, endpoint: Option) -> Self { + self.local_endpoint = endpoint; + self + } + } + + generate_set_and_with! { + /// Set source app signing identifier. + pub fn source_app_signing_identifier(mut self, value: Option) -> Self { + self.source_app_signing_identifier = value; + self + } + } + + generate_set_and_with! { + /// Set source app bundle identifier. + pub fn source_app_bundle_identifier(mut self, value: Option) -> Self { + self.source_app_bundle_identifier = value; + self + } + } +} diff --git a/rama-net-apple-networkextension/src/udp.rs b/rama-net-apple-networkextension/src/udp.rs new file mode 100644 index 000000000..bdda4d0d3 --- /dev/null +++ b/rama-net-apple-networkextension/src/udp.rs @@ -0,0 +1,49 @@ +use std::sync::Arc; + +use rama_core::{ + bytes::Bytes, + extensions::{Extensions, ExtensionsMut, ExtensionsRef}, +}; +use tokio::sync::mpsc; + +/// A per-flow UDP datagram socket abstraction for transparent proxy services. +pub struct UdpFlow { + incoming: mpsc::UnboundedReceiver, + outgoing: Arc, + extensions: Extensions, +} + +impl UdpFlow { + pub(crate) fn new( + incoming: mpsc::UnboundedReceiver, + outgoing: Arc, + ) -> Self { + Self { + incoming, + outgoing, + extensions: Extensions::new(), + } + } + + /// Receive one datagram from the intercepted client flow. + pub async fn recv(&mut self) -> Option { + self.incoming.recv().await + } + + /// Send one datagram back to the intercepted client flow. + pub fn send(&self, bytes: Bytes) { + (self.outgoing)(bytes); + } +} + +impl ExtensionsRef for UdpFlow { + fn extensions(&self) -> &Extensions { + &self.extensions + } +} + +impl ExtensionsMut for UdpFlow { + fn extensions_mut(&mut self) -> &mut Extensions { + &mut self.extensions + } +} diff --git a/rama-net/README.md b/rama-net/README.md index 84b690def..c64144c88 100644 --- a/rama-net/README.md +++ b/rama-net/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-net/src/http/server/mod.rs b/rama-net/src/http/server/mod.rs index f33b8f0e5..46f6621fc 100644 --- a/rama-net/src/http/server/mod.rs +++ b/rama-net/src/http/server/mod.rs @@ -4,4 +4,6 @@ //! is not covered by this router as this is done via sidechannel information instead (e.g. ALPN in TLS). pub mod peek; -pub use peek::{HttpPeekRouter, HttpPeekStream, NoHttpRejectError}; +pub use peek::{ + HttpPeekRouter, HttpPeekVersion, HttpPrefixedIo, NoHttpRejectError, peek_http_input, +}; diff --git a/rama-net/src/http/server/peek.rs b/rama-net/src/http/server/peek.rs index 85c570de0..e5330657b 100644 --- a/rama-net/src/http/server/peek.rs +++ b/rama-net/src/http/server/peek.rs @@ -3,9 +3,8 @@ use rama_core::{ Service, error::{BoxError, ErrorContext}, - extensions::ExtensionsMut, + io::{PeekIoProvider, PrefixedIo, StackReader}, service::RejectService, - stream::{PeekStream, StackReader}, telemetry::tracing, }; use std::time::Duration; @@ -123,119 +122,181 @@ impl HttpPeekRouter> { } } -impl Service for HttpPeekRouter, F> +impl Service for HttpPeekRouter, F> where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + PeekableInput: PeekIoProvider, Output: Send + 'static, - T: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + T: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, { type Output = Output; type Error = BoxError; - async fn serve(&self, stream: Stream) -> Result { - let (version, stream) = peek_http_stream(stream, self.peek_timeout).await?; + async fn serve(&self, input: PeekableInput) -> Result { + let (version, peek_input) = peek_http_input(input, self.peek_timeout).await?; if version.is_some() { tracing::trace!("http peek: serve[auto]: http acceptor; version = {version:?}"); - self.http_acceptor.0.serve(stream).await.into_box_error() + self.http_acceptor + .0 + .serve(peek_input) + .await + .into_box_error() } else { tracing::trace!("http peek: serve[auto]: fallback; version = {version:?}"); - self.fallback.serve(stream).await.into_box_error() + self.fallback.serve(peek_input).await.into_box_error() } } } -impl Service for HttpPeekRouter, F> +impl Service for HttpPeekRouter, F> where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + PeekableInput: PeekIoProvider, Output: Send + 'static, - T: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + T: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, { type Output = Output; type Error = BoxError; - async fn serve(&self, stream: Stream) -> Result { - let (version, stream) = peek_http_stream(stream, self.peek_timeout).await?; + async fn serve(&self, input: PeekableInput) -> Result { + let (version, peek_input) = peek_http_input(input, self.peek_timeout).await?; if version == Some(HttpPeekVersion::Http1x) { tracing::trace!("http peek: serve[http1]: http/1x acceptor; version = {version:?}"); - self.http_acceptor.0.serve(stream).await.into_box_error() + self.http_acceptor + .0 + .serve(peek_input) + .await + .into_box_error() } else { tracing::trace!("http peek: serve[http1]: fallback; version = {version:?}"); - self.fallback.serve(stream).await.into_box_error() + self.fallback.serve(peek_input).await.into_box_error() } } } -impl Service for HttpPeekRouter, F> +impl Service for HttpPeekRouter, F> where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + PeekableInput: PeekIoProvider, Output: Send + 'static, - T: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + T: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, { type Output = Output; type Error = BoxError; - async fn serve(&self, stream: Stream) -> Result { - let (version, stream) = peek_http_stream(stream, self.peek_timeout).await?; + async fn serve(&self, input: PeekableInput) -> Result { + let (version, peek_input) = peek_http_input(input, self.peek_timeout).await?; if version == Some(HttpPeekVersion::H2) { tracing::trace!("http peek: serve[h2]: http acceptor; version = {version:?}"); - self.http_acceptor.0.serve(stream).await.into_box_error() + self.http_acceptor + .0 + .serve(peek_input) + .await + .into_box_error() } else { tracing::trace!("http peek: serve[h2]: fallback; version = {version:?}"); - self.fallback.serve(stream).await.into_box_error() + self.fallback.serve(peek_input).await.into_box_error() } } } -impl Service for HttpPeekRouter, F> +impl Service + for HttpPeekRouter, F> where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + PeekableInput: PeekIoProvider, Output: Send + 'static, - T: Service, Output = Output, Error: Into>, - U: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + T: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + U: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, { type Output = Output; type Error = BoxError; - async fn serve(&self, stream: Stream) -> Result { - let (version, stream) = peek_http_stream(stream, self.peek_timeout).await?; + async fn serve(&self, input: PeekableInput) -> Result { + let (version, peek_input) = peek_http_input(input, self.peek_timeout).await?; match version { Some(HttpPeekVersion::H2) => { tracing::trace!("http peek: serve[dual]: h2 acceptor; version = {version:?}"); - self.http_acceptor.h2.serve(stream).await.into_box_error() + self.http_acceptor + .h2 + .serve(peek_input) + .await + .into_box_error() } Some(HttpPeekVersion::Http1x) => { tracing::trace!("http peek: serve[dual]: http/1x acceptor; version = {version:?}"); self.http_acceptor .http1 - .serve(stream) + .serve(peek_input) .await .into_box_error() } None => { tracing::trace!("http peek: serve[dual]: fallback; version = {version:?}"); - self.fallback.serve(stream).await.into_box_error() + self.fallback.serve(peek_input).await.into_box_error() } } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum HttpPeekVersion { +pub enum HttpPeekVersion { Http1x, H2, } -async fn peek_http_stream( - mut stream: Stream, +pub async fn peek_http_input( + mut input: PeekableInput, timeout: Option, -) -> Result<(Option, HttpPeekStream), BoxError> { +) -> Result< + ( + Option, + PeekableInput::Mapped>, + ), + BoxError, +> +where + PeekableInput: PeekIoProvider, +{ let mut peek_buf = [0u8; HTTP_HEADER_PEEK_LEN]; - let read_fut = stream.read(&mut peek_buf); + let read_fut = input.peek_io_mut().read(&mut peek_buf); let n = match timeout { Some(d) => tokio::time::timeout(d, read_fut).await.unwrap_or(Ok(0)), @@ -277,16 +338,16 @@ async fn peek_http_stream = PeekStream, S>; +/// [`PrefixedIo`] alias used by [`HttpPeekRouter`]. +pub type HttpPrefixedIo = PrefixedIo, S>; #[cfg(test)] mod test { @@ -296,7 +357,7 @@ mod test { }; use std::convert::Infallible; - use rama_core::stream::Stream; + use rama_core::io::Io; use super::*; @@ -502,9 +563,7 @@ mod test { async fn test_peek_router_read_eof() { const CONTENT: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\nfoobar"; - async fn http_service_fn( - mut stream: impl Stream + Unpin, - ) -> Result<&'static str, BoxError> { + async fn http_service_fn(mut stream: impl Io + Unpin) -> Result<&'static str, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; assert_eq!(CONTENT, v); @@ -543,9 +602,7 @@ mod test { } let http_service = service_fn(http_service_fn); - async fn other_service_fn( - mut stream: impl Stream + Unpin, - ) -> Result, BoxError> { + async fn other_service_fn(mut stream: impl Io + Unpin) -> Result, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; Ok(v) diff --git a/rama-net/src/http/uri/match_replace/slice.rs b/rama-net/src/http/uri/match_replace/slice.rs index 2a989e388..54fa7218e 100644 --- a/rama-net/src/http/uri/match_replace/slice.rs +++ b/rama-net/src/http/uri/match_replace/slice.rs @@ -90,7 +90,7 @@ mod tests { /// Assert that for every container view the output equals `want`. fn expect_all_views_eq(rules: &[UriMatchReplaceRule], input: &str, want: Option<&str>) { let got = apply_multiple_views(rules, input); - let want = want.map(str::to_string); + let want = want.map(str::to_owned); for (i, g) in got.into_iter().enumerate() { assert_eq!(g, want, "container idx {i} wrong result for input: {input}"); } diff --git a/rama-net/src/proxy/forward.rs b/rama-net/src/proxy/forward.rs index 232b8dd50..2a7b17cc7 100644 --- a/rama-net/src/proxy/forward.rs +++ b/rama-net/src/proxy/forward.rs @@ -4,45 +4,40 @@ use rama_core::{ error::{BoxError, ErrorExt}, }; -use rama_core::stream::Stream; - -use super::ProxyRequest; +use rama_core::io::{BridgeIo, Io}; #[derive(Debug, Clone, Default)] #[non_exhaustive] -/// A proxy [`Service`] which takes a [`ProxyRequest`] -/// and copies the bytes of both the source and target [`Stream`]s +/// A proxy [`Service`] which takes a [`BridgeIo`] +/// and copies the bytes of both the source and target [`Io`]s /// bidirectionally. -pub struct StreamForwardService; +pub struct IoForwardService; -impl StreamForwardService { +impl IoForwardService { #[inline] - /// Create a new [`StreamForwardService`]. + /// Create a new [`IoForwardService`]. #[must_use] pub fn new() -> Self { Self::default() } } -impl Service> for StreamForwardService +impl Service> for IoForwardService where - S: Stream + Unpin, - T: Stream + Unpin, + S: Io + Unpin, + T: Io + Unpin, { type Output = (); type Error = BoxError; async fn serve( &self, - ProxyRequest { - mut source, - mut target, - }: ProxyRequest, + BridgeIo(mut left, mut right): BridgeIo, ) -> Result { - match tokio::io::copy_bidirectional(&mut source, &mut target).await { + match tokio::io::copy_bidirectional(&mut left, &mut right).await { Ok((bytes_copied_north, bytes_copied_south)) => { tracing::trace!( - "(proxy) I/O stream forwarder finished: bytes north: {}; bytes south: {}", + "(proxy) I/O forwarder finished: bytes north: {}; bytes south: {}", bytes_copied_north, bytes_copied_south, ); @@ -52,7 +47,7 @@ where if crate::conn::is_connection_error(&err) { Ok(()) } else { - Err(err.context("(proxy) I/O stream forwarder")) + Err(err.context("(proxy) I/O forwarder")) } } } diff --git a/rama-net/src/proxy/mod.rs b/rama-net/src/proxy/mod.rs index ca9df47ed..91505aee9 100644 --- a/rama-net/src/proxy/mod.rs +++ b/rama-net/src/proxy/mod.rs @@ -2,13 +2,9 @@ use crate::address::HostWithPort; -mod request; -#[doc(inline)] -pub use request::ProxyRequest; - mod forward; #[doc(inline)] -pub use forward::StreamForwardService; +pub use forward::IoForwardService; #[derive(Debug, Clone, PartialEq, Eq, Hash)] /// Target [`HostWithPort`] for a proxy/forwarder service. diff --git a/rama-net/src/proxy/request.rs b/rama-net/src/proxy/request.rs deleted file mode 100644 index bdc3db4ef..000000000 --- a/rama-net/src/proxy/request.rs +++ /dev/null @@ -1,8 +0,0 @@ -/// A request to proxy between source and target (stream). -#[derive(Debug, Clone)] -pub struct ProxyRequest { - /// Source stream, which is usualy the initiator, e.g. the client. - pub source: S, - /// Target stream, which is usually the acceptor, e.g. the server. - pub target: T, -} diff --git a/rama-net/src/socket/device_name.rs b/rama-net/src/socket/device_name.rs new file mode 100644 index 000000000..19fd1b45b --- /dev/null +++ b/rama-net/src/socket/device_name.rs @@ -0,0 +1,238 @@ +use std::{fmt, str::FromStr}; + +use rama_core::error::{BoxError, ErrorContext as _}; +use rama_utils::str::smol_str::SmolStr; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// Name of a (network) interface device name, e.g. `eth0`. +pub struct DeviceName(SmolStr); + +impl DeviceName { + /// Create a new [`DeviceName`]. + #[must_use] + pub const fn new(name: &'static str) -> Self { + if !is_valid(name.as_bytes()) { + panic!("static str is not a valid (interface) device name"); + } + Self(SmolStr::new_static(name)) + } + + /// Return a reference to `self` as a byte slice. + #[must_use] + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } + + /// Return a reference to `self` as a string slice. + #[must_use] + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl fmt::Display for DeviceName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl FromStr for DeviceName { + type Err = BoxError; + + #[inline] + fn from_str(s: &str) -> Result { + Self::try_from(s) + } +} + +impl TryFrom for DeviceName { + type Error = BoxError; + + #[inline] + fn try_from(s: String) -> Result { + s.as_str().try_into() + } +} + +impl TryFrom<&String> for DeviceName { + type Error = BoxError; + + #[inline] + fn try_from(value: &String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&str> for DeviceName { + type Error = BoxError; + + fn try_from(s: &str) -> Result { + use rama_core::error::ErrorExt as _; + + if is_valid(s.as_bytes()) { + return Ok(Self(SmolStr::from(s))); + } + + Err(BoxError::from("invalid (interface) device name").context_str_field("str", s)) + } +} + +impl TryFrom> for DeviceName { + type Error = BoxError; + + fn try_from(bytes: Vec) -> Result { + Self::try_from(bytes.as_slice()) + } +} + +impl TryFrom<&[u8]> for DeviceName { + type Error = BoxError; + + fn try_from(bytes: &[u8]) -> Result { + let s = std::str::from_utf8(bytes).context("parse (interface) device name from bytes")?; + s.try_into() + } +} + +impl serde::Serialize for DeviceName { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let name = self.as_str(); + name.serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for DeviceName { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = >::deserialize(deserializer)?; + s.parse().map_err(serde::de::Error::custom) + } +} + +pub(super) const fn is_valid(s: &[u8]) -> bool { + if s.is_empty() || s.len() > DEVICE_MAX_LEN { + false + } else { + let mut i = 0; + if DEVICE_FIRST_CHARS[s[0] as usize] == 0 { + return false; + } + while i < s.len() { + if DEVICE_CHARS[s[i] as usize] == 0 { + return false; + } + i += 1; + } + true + } +} + +/// The maximum length of a device name. +const DEVICE_MAX_LEN: usize = 15; + +#[rustfmt::skip] +/// Valid byte values for a device name. +const DEVICE_CHARS: [u8; 256] = [ + // 0 1 2 3 4 5 6 7 8 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3x + 0, 0, 0, 0, 0, b'-', b'.', 0, b'0', b'1', // 4x + b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', 0, // 5x + 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', // 6x + b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x + b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x + b'Z', 0, 0, 0, 0, b'_', 0, b'a', b'b', b'c', // 9x + b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x + b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x + b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, // 12x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 13x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 15x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 17x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 18x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 19x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 22x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 23x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 24x + 0, 0, 0, 0, 0, 0 // 25x +]; + +#[rustfmt::skip] +const DEVICE_FIRST_CHARS: [u8; 256] = [ + // 0 1 2 3 4 5 6 7 8 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 5x + 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', // 6x + b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x + b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x + b'Z', 0, 0, 0, 0, 0, 0, b'a', b'b', b'c', // 9x + b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x + b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x + b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, // 12x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 13x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 15x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 17x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 18x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 19x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 22x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 23x + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 24x + 0, 0, 0, 0, 0, 0 // 25x +]; + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + #[test] + fn test_parse_valid_device_name() { + for s in [ + "eth0", + "eth0.100", + "br-lan", + "ens192", + "veth_abcd1234", + "lo", + ] { + let msg = format!("parsing '{s}'"); + + assert_eq!(s, s.parse::().expect(&msg).as_str()); + } + } + + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + #[test] + fn test_parse_display_device_name() { + for s in [ + "eth0", + "eth0.100", + "br-lan", + "ens192", + "veth_abcd1234", + "lo", + ] { + let msg = format!("parsing '{s}'"); + let name: DeviceName = s.parse().expect(&msg); + assert_eq!(name.to_string(), s, "{msg}"); + } + } +} diff --git a/rama-net/src/socket/interface.rs b/rama-net/src/socket/interface.rs deleted file mode 100644 index b2f0612f8..000000000 --- a/rama-net/src/socket/interface.rs +++ /dev/null @@ -1,712 +0,0 @@ -use crate::address::{SocketAddress, parse_utils::try_to_parse_str_to_ip}; -use rama_core::error::{BoxError, ErrorContext}; -use std::{ - fmt, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, - str::FromStr, - sync::Arc, -}; - -/// The interface to bind a [`Socket`] to. -/// -/// [`Socket`]: super::core::Socket -#[derive(Debug, Clone)] -pub enum Interface { - /// Bind to a [`Socket`] address (ip + port), the most common choice - /// - /// [`Socket`]: super::core::Socket - Address(SocketAddress), - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - #[cfg_attr( - docsrs, - doc(cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))) - )] - /// Bind to a network device interface name, using IPv4/TCP. - /// - /// Use [`SocketOptions`] if you want more finegrained control, - /// or make a raw [`Socket`] yourself. - /// - /// [`Socket`]: super::core::Socket - Device(DeviceName), - /// Bind to a socket with the following options. - Socket(Arc), -} - -impl Interface { - /// creates a new [`Interface`] from a [`SocketAddress`] - pub fn new_address(addr: impl Into) -> Self { - Self::Address(addr.into()) - } -} - -#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] -#[cfg_attr( - docsrs, - doc(cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))) -)] -pub use device::DeviceName; - -use super::SocketOptions; - -#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] -mod device { - use super::*; - use rama_utils::str::smol_str::SmolStr; - - #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] - /// Name of a (network) interface device name, e.g. `eth0`. - pub struct DeviceName(SmolStr); - - impl DeviceName { - /// Create a new [`DeviceName`]. - #[must_use] - pub const fn new(name: &'static str) -> Self { - if !is_valid(name.as_bytes()) { - panic!("static str is not a valid (interface) device name"); - } - Self(SmolStr::new_static(name)) - } - - /// Return a reference to `self` as a byte slice. - #[must_use] - pub fn as_bytes(&self) -> &[u8] { - self.0.as_bytes() - } - - /// Return a reference to `self` as a string slice. - #[must_use] - pub fn as_str(&self) -> &str { - self.0.as_str() - } - } - - impl fmt::Display for DeviceName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } - } - - impl FromStr for DeviceName { - type Err = BoxError; - - #[inline] - fn from_str(s: &str) -> Result { - Self::try_from(s) - } - } - - impl TryFrom for DeviceName { - type Error = BoxError; - - #[inline] - fn try_from(s: String) -> Result { - s.as_str().try_into() - } - } - - impl TryFrom<&String> for DeviceName { - type Error = BoxError; - - #[inline] - fn try_from(value: &String) -> Result { - value.as_str().try_into() - } - } - - impl TryFrom<&str> for DeviceName { - type Error = BoxError; - - fn try_from(s: &str) -> Result { - use rama_core::error::ErrorExt as _; - - if is_valid(s.as_bytes()) { - return Ok(Self(SmolStr::from(s))); - } - - Err(BoxError::from("invalid (interface) device name").context_str_field("str", s)) - } - } - - impl TryFrom> for DeviceName { - type Error = BoxError; - - fn try_from(bytes: Vec) -> Result { - Self::try_from(bytes.as_slice()) - } - } - - impl TryFrom<&[u8]> for DeviceName { - type Error = BoxError; - - fn try_from(bytes: &[u8]) -> Result { - let s = - std::str::from_utf8(bytes).context("parse (interface) device name from bytes")?; - s.try_into() - } - } - - impl serde::Serialize for DeviceName { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let name = self.as_str(); - name.serialize(serializer) - } - } - - impl<'de> serde::Deserialize<'de> for DeviceName { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = >::deserialize(deserializer)?; - s.parse().map_err(serde::de::Error::custom) - } - } - - impl Interface { - #[must_use] - pub const fn new_device(name: &'static str) -> Self { - let name = DeviceName::new(name); - Self::Device(name) - } - } - - pub(super) const fn is_valid(s: &[u8]) -> bool { - if s.is_empty() || s.len() > DEVICE_MAX_LEN { - false - } else { - let mut i = 0; - if DEVICE_FIRST_CHARS[s[0] as usize] == 0 { - return false; - } - while i < s.len() { - if DEVICE_CHARS[s[i] as usize] == 0 { - return false; - } - i += 1; - } - true - } - } - - /// The maximum length of a device name. - const DEVICE_MAX_LEN: usize = 15; - - #[rustfmt::skip] - /// Valid byte values for a device name. - const DEVICE_CHARS: [u8; 256] = [ - // 0 1 2 3 4 5 6 7 8 9 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3x - 0, 0, 0, 0, 0, b'-', b'.', 0, b'0', b'1', // 4x - b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b':', 0, // 5x - 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', // 6x - b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x - b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x - b'Z', 0, 0, 0, 0, b'_', 0, b'a', b'b', b'c', // 9x - b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x - b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x - b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, // 12x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 13x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 15x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 17x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 18x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 19x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 22x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 23x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 24x - 0, 0, 0, 0, 0, 0 // 25x - ]; - - #[rustfmt::skip] - const DEVICE_FIRST_CHARS: [u8; 256] = [ - // 0 1 2 3 4 5 6 7 8 9 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 1x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 2x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 3x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 4x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 5x - 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', // 6x - b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', // 7x - b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', // 8x - b'Z', 0, 0, 0, 0, 0, 0, b'a', b'b', b'c', // 9x - b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', // 10x - b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', // 11x - b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, // 12x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 13x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 14x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 15x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 17x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 18x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 19x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 21x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 22x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 23x - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 24x - 0, 0, 0, 0, 0, 0 // 25x - ]; -} - -impl Interface { - /// creates a new local ipv4 [`Interface`] for the given port - /// - /// # Example - /// - /// ``` - /// use rama_net::socket::Interface; - /// - /// let interface = Interface::local_ipv4(8080); - /// assert_eq!("127.0.0.1:8080", interface.to_string()); - /// ``` - #[inline] - #[must_use] - pub const fn local_ipv4(port: u16) -> Self { - Self::Address(SocketAddress::local_ipv4(port)) - } - - /// creates a new local ipv6 [`Interface`] for the given port. - /// - /// # Example - /// - /// ``` - /// use rama_net::socket::Interface; - /// - /// let interface = Interface::local_ipv6(8080); - /// assert_eq!("[::1]:8080", interface.to_string()); - /// ``` - #[inline] - #[must_use] - pub const fn local_ipv6(port: u16) -> Self { - Self::Address(SocketAddress::local_ipv6(port)) - } - - /// creates a new default ipv4 [`Interface`] for the given port - /// - /// # Example - /// - /// ``` - /// use rama_net::socket::Interface; - /// - /// let interface = Interface::default_ipv4(8080); - /// assert_eq!("0.0.0.0:8080", interface.to_string()); - /// ``` - #[inline] - #[must_use] - pub const fn default_ipv4(port: u16) -> Self { - Self::Address(SocketAddress::default_ipv4(port)) - } - - /// creates a new default ipv6 [`Interface`] for the given port. - /// - /// # Example - /// - /// ``` - /// use rama_net::socket::Interface; - /// - /// let interface = Interface::default_ipv6(8080); - /// assert_eq!("[::]:8080", interface.to_string()); - /// ``` - #[must_use] - pub const fn default_ipv6(port: u16) -> Self { - Self::Address(SocketAddress::default_ipv6(port)) - } - - /// creates a new broadcast ipv4 [`Interface`] for the given port - /// - /// # Example - /// - /// ``` - /// use rama_net::socket::Interface; - /// - /// let interface = Interface::broadcast_ipv4(8080); - /// assert_eq!("255.255.255.255:8080", interface.to_string()); - /// ``` - #[must_use] - pub const fn broadcast_ipv4(port: u16) -> Self { - Self::Address(SocketAddress::broadcast_ipv4(port)) - } -} - -impl From for Interface { - #[inline] - fn from(addr: SocketAddress) -> Self { - Self::Address(addr) - } -} - -impl From<&SocketAddress> for Interface { - #[inline] - fn from(addr: &SocketAddress) -> Self { - Self::Address(*addr) - } -} - -impl From for Interface { - #[inline] - fn from(addr: SocketAddr) -> Self { - Self::Address(addr.into()) - } -} - -impl From<&SocketAddr> for Interface { - #[inline] - fn from(addr: &SocketAddr) -> Self { - Self::Address(addr.into()) - } -} - -impl From for Interface { - #[inline] - fn from(addr: SocketAddrV4) -> Self { - Self::Address(addr.into()) - } -} - -impl From for Interface { - #[inline] - fn from(addr: SocketAddrV6) -> Self { - Self::Address(addr.into()) - } -} - -impl From<(IpAddr, u16)> for Interface { - #[inline] - fn from(twin: (IpAddr, u16)) -> Self { - Self::Address(twin.into()) - } -} - -impl From<(Ipv4Addr, u16)> for Interface { - #[inline] - fn from(twin: (Ipv4Addr, u16)) -> Self { - Self::Address(twin.into()) - } -} - -impl From<([u8; 4], u16)> for Interface { - #[inline] - fn from(twin: ([u8; 4], u16)) -> Self { - Self::Address(twin.into()) - } -} - -impl From<(Ipv6Addr, u16)> for Interface { - #[inline] - fn from(twin: (Ipv6Addr, u16)) -> Self { - Self::Address(twin.into()) - } -} - -impl From<([u8; 16], u16)> for Interface { - #[inline] - fn from(twin: ([u8; 16], u16)) -> Self { - Self::Address(twin.into()) - } -} - -impl From for Interface { - #[inline] - fn from(value: SocketOptions) -> Self { - Self::Socket(Arc::new(value)) - } -} - -impl From> for Interface { - #[inline] - fn from(value: Arc) -> Self { - Self::Socket(value) - } -} - -impl fmt::Display for Interface { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Address(socket_address) => write!(f, "{socket_address}"), - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - Self::Device(name) => write!(f, "{name}"), - Self::Socket(opts) => write!(f, "{opts:?}"), - } - } -} - -impl FromStr for Interface { - type Err = BoxError; - - #[inline] - fn from_str(s: &str) -> Result { - Self::try_from(s) - } -} - -impl TryFrom for Interface { - type Error = BoxError; - - #[inline] - fn try_from(s: String) -> Result { - s.as_str().try_into() - } -} - -impl TryFrom<&String> for Interface { - type Error = BoxError; - - #[inline] - fn try_from(value: &String) -> Result { - value.as_str().try_into() - } -} - -impl TryFrom<&str> for Interface { - type Error = BoxError; - - fn try_from(s: &str) -> Result { - let (ip_addr, port) = match crate::address::parse_utils::split_port_from_str(s) { - Ok(t) => t, - Err(err) => { - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - if let Ok(name) = DeviceName::try_from(s) { - return Ok(Self::Device(name)); - } - - return Err(err); - } - }; - - if let Some(ip_addr) = try_to_parse_str_to_ip(ip_addr) { - match ip_addr { - IpAddr::V6(_) if !s.starts_with('[') => Err(BoxError::from( - "missing brackets for IPv6 address with port", - )), - _ => Ok(Self::new_address((ip_addr, port))), - } - } else { - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - if let Ok(name) = DeviceName::try_from(s) { - return Ok(Self::Device(name)); - } - - Err(BoxError::from("invalid bind interface")) - } - } -} - -impl TryFrom> for Interface { - type Error = BoxError; - - fn try_from(bytes: Vec) -> Result { - Self::try_from(bytes.as_slice()) - } -} - -impl TryFrom<&[u8]> for Interface { - type Error = BoxError; - - fn try_from(bytes: &[u8]) -> Result { - let s = std::str::from_utf8(bytes).context("parse bind interface from bytes")?; - s.try_into() - } -} - -impl serde::Serialize for Interface { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let interface = self.to_string(); - interface.serialize(serializer) - } -} - -impl<'de> serde::Deserialize<'de> for Interface { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[allow(clippy::large_enum_variant)] - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum Variants { - Str(String), - Opts(SocketOptions), - } - - match Variants::deserialize(deserializer)? { - Variants::Str(s) => s.parse().map_err(serde::de::Error::custom), - Variants::Opts(opts) => Ok(Self::Socket(Arc::new(opts))), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn assert_eq_socket_address(s: &str, bind_address: Interface, ip_addr: &str, port: u16) { - match bind_address { - Interface::Address(socket_address) => { - assert_eq!(socket_address.ip_addr.to_string(), ip_addr, "parsing: {s}",); - assert_eq!(socket_address.port, port, "parsing: {s}"); - } - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - Interface::Device(name) => panic!("unexpected device name '{name}': parsing '{s}'"), - Interface::Socket(opts) => { - panic!("unexpected socket options '{opts:?}': parsing '{s}'") - } - } - } - - #[test] - fn test_parse_valid_socket_address() { - for (s, (expected_ip_addr, expected_port)) in [ - ("[::1]:80", ("::1", 80)), - ("127.0.0.1:80", ("127.0.0.1", 80)), - ( - "[2001:db8:3333:4444:5555:6666:7777:8888]:80", - ("2001:db8:3333:4444:5555:6666:7777:8888", 80), - ), - ] { - let msg = format!("parsing '{s}'"); - - assert_eq_socket_address(s, s.parse().expect(&msg), expected_ip_addr, expected_port); - assert_eq_socket_address( - s, - s.try_into().expect(&msg), - expected_ip_addr, - expected_port, - ); - assert_eq_socket_address( - s, - s.to_owned().try_into().expect(&msg), - expected_ip_addr, - expected_port, - ); - assert_eq_socket_address( - s, - s.as_bytes().try_into().expect(&msg), - expected_ip_addr, - expected_port, - ); - assert_eq_socket_address( - s, - s.as_bytes().to_vec().try_into().expect(&msg), - expected_ip_addr, - expected_port, - ); - } - } - - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - fn assert_eq_device_name(s: &str, bind_address: Interface) { - match bind_address { - Interface::Address(socket_address) => { - panic!("unexpected socket address '{socket_address}: parsing '{s}") - } - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - Interface::Device(name) => assert_eq!(s, name.as_str()), - Interface::Socket(opts) => { - panic!("unexpected socket options '{opts:?}': parsing '{s}'") - } - } - } - - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - #[test] - fn test_parse_valid_device_name() { - for s in [ - "eth0", - "eth0.100", - "br-lan", - "ens192", - "veth_abcd1234", - "lo", - ] { - let msg = format!("parsing '{s}'"); - - assert_eq_device_name(s, s.parse().expect(&msg)); - assert_eq_device_name(s, s.try_into().expect(&msg)); - assert_eq_device_name(s, s.to_owned().try_into().expect(&msg)); - assert_eq_device_name(s, s.as_bytes().try_into().expect(&msg)); - assert_eq_device_name(s, s.as_bytes().to_vec().try_into().expect(&msg)); - } - } - - #[test] - fn test_parse_invalid() { - for s in [ - "", - "-", - ".", - ":", - ":80", - "-.", - ".-", - "::1", - "127.0.0.1", - "[::1]", - "2001:db8:3333:4444:5555:6666:7777:8888", - "[2001:db8:3333:4444:5555:6666:7777:8888]", - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - "example.com", - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - "example.com:", - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - "example.com:-1", - "example.com:999999", - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - "example.com:80", - #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] - "example:com", - "[127.0.0.1]:80", - "2001:db8:3333:4444:5555:6666:7777:8888:80", - "eth#0", - "eth#0", - "abcdefghijklmnopqrstuvwxyz", - "GigabitEthernet0/1", - "ge-0/0/0", - ] { - let msg = format!("parsing '{s}'"); - assert!(s.parse::().is_err(), "{msg}"); - assert!(Interface::try_from(s).is_err(), "{msg}"); - assert!(Interface::try_from(s.to_owned()).is_err(), "{msg}"); - assert!(Interface::try_from(s.as_bytes()).is_err(), "{msg}"); - assert!(Interface::try_from(s.as_bytes().to_vec()).is_err(), "{msg}"); - } - } - - #[test] - fn test_parse_display_address() { - for (s, expected) in [("[::1]:80", "[::1]:80"), ("127.0.0.1:80", "127.0.0.1:80")] { - let msg = format!("parsing '{s}'"); - let bind_address: Interface = s.parse().expect(&msg); - assert_eq!(bind_address.to_string(), expected, "{msg}"); - } - } - - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - #[test] - fn test_parse_display_device_name() { - for s in [ - "eth0", - "eth0.100", - "br-lan", - "ens192", - "veth_abcd1234", - "lo", - ] { - let msg = format!("parsing '{s}'"); - let bind_address: Interface = s.parse().expect(&msg); - assert_eq!(bind_address.to_string(), s, "{msg}"); - } - } -} diff --git a/rama-net/src/socket/mod.rs b/rama-net/src/socket/mod.rs index 04537f59f..2827017e8 100644 --- a/rama-net/src/socket/mod.rs +++ b/rama-net/src/socket/mod.rs @@ -1,8 +1,7 @@ pub use ::socket2 as core; -mod interface; -#[doc(inline)] -pub use interface::Interface; +#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] +mod device_name; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] #[cfg_attr( @@ -10,7 +9,7 @@ pub use interface::Interface; doc(cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))) )] #[doc(inline)] -pub use interface::DeviceName; +pub use device_name::DeviceName; pub mod opts; #[doc(inline)] diff --git a/rama-net/src/socket/opts.rs b/rama-net/src/socket/opts.rs index 3eb85e6fa..b350e1979 100644 --- a/rama-net/src/socket/opts.rs +++ b/rama-net/src/socket/opts.rs @@ -25,6 +25,26 @@ pub enum Domain { Unix, } +impl From for Domain { + fn from(value: SocketAddr) -> Self { + if value.is_ipv4() { + Self::IPv4 + } else { + Self::IPv6 + } + } +} + +impl From for Domain { + fn from(value: SocketAddress) -> Self { + if value.ip_addr.is_ipv4() { + Self::IPv4 + } else { + Self::IPv6 + } + } +} + impl Domain { #[inline] #[must_use] @@ -370,42 +390,18 @@ impl From for SocketTcpKeepAlive { } impl SocketOptions { - /// Create a default TCP (Ipv4) [`SocketOptions`]. + /// Create a default TCP [`SocketOptions`]. #[inline] #[must_use] pub fn default_tcp() -> Self { Default::default() } - /// Create a default TCP (Ipv6) [`SocketOptions`]. - #[inline] - #[must_use] - pub fn default_tcp_v6() -> Self { - Self { - domain: Domain::IPv6, - r#type: Type::Stream, - protocol: Some(Protocol::TCP), - ..Default::default() - } - } - /// Create a default UDP (Ipv4) [`SocketOptions`]. + /// Create a default UDP [`SocketOptions`]. #[inline] #[must_use] pub fn default_udp() -> Self { Self { - domain: Domain::IPv4, - r#type: Type::Datagram, - protocol: Some(Protocol::UDP), - ..Default::default() - } - } - - /// Create a default UDP (Ipv6) [`SocketOptions`]. - #[inline] - #[must_use] - pub fn default_udp_v6() -> Self { - Self { - domain: Domain::IPv6, r#type: Type::Datagram, protocol: Some(Protocol::UDP), ..Default::default() @@ -415,7 +411,6 @@ impl SocketOptions { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SocketOptions { - pub domain: Domain, pub r#type: Type, pub protocol: Option, @@ -1063,9 +1058,9 @@ pub struct SocketOptions { } impl SocketOptions { - pub fn try_build_socket(&self) -> io::Result { + pub fn try_build_socket(&self, domain: Domain) -> io::Result { let socket = Socket::new( - self.domain.into(), + domain.into(), self.r#type.into(), self.protocol.map(Into::into), )?; diff --git a/rama-net/src/socket/svc.rs b/rama-net/src/socket/svc.rs index 864dd4766..337a41b8b 100644 --- a/rama-net/src/socket/svc.rs +++ b/rama-net/src/socket/svc.rs @@ -1,6 +1,7 @@ -use super::Interface; use rama_core::{Service, error::BoxError}; +use crate::address::SocketAddress; + /// Glue trait that is used as the trait bound for /// code creating/preparing a socket on one layer or another. /// @@ -13,24 +14,24 @@ pub trait SocketService: Send + Sync + 'static { type Error: Into + Send + 'static; /// Create a binding to a Unix/Linux/Windows socket. - fn bind( + fn bind_socket_with_address( &self, - interface: impl Into, + addr: impl Into, ) -> impl Future> + Send + '_; } impl SocketService for S where - S: Service + Send + 'static>, + S: Service + Send + 'static>, Socket: Send + 'static, { type Socket = Socket; type Error = S::Error; - fn bind( + fn bind_socket_with_address( &self, - interface: impl Into, + addr: impl Into, ) -> impl Future> + Send + '_ { - self.serve(interface.into()) + self.serve(addr.into()) } } diff --git a/rama-net/src/stream/layer/http/body_limit.rs b/rama-net/src/stream/layer/http/body_limit.rs index cccfded12..7c4a4fdab 100644 --- a/rama-net/src/stream/layer/http/body_limit.rs +++ b/rama-net/src/stream/layer/http/body_limit.rs @@ -1,4 +1,4 @@ -use rama_core::{Layer, Service, extensions::ExtensionsMut, stream::Stream}; +use rama_core::{Layer, Service, extensions::ExtensionsMut, io::Io}; use rama_http_types::BodyLimit; use rama_utils::macros::define_inner_service_accessors; use std::fmt; @@ -9,7 +9,7 @@ use std::fmt; /// it only is used to add the [`BodyLimit`] value to input [`Extensions`], /// such that the L7 http service can apply the limit when found in those [`Extensions`]. /// -/// [`Stream`]: rama_core::stream::Stream +/// [`Stream`]: rama_core::io::Io /// [`Extensions`]: rama_core::extensions::Extensions #[derive(Debug, Clone)] pub struct BodyLimitLayer { @@ -120,7 +120,7 @@ impl BodyLimitService { impl Service for BodyLimitService where S: Service, - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, { type Output = S::Output; type Error = S::Error; diff --git a/rama-net/src/stream/layer/mod.rs b/rama-net/src/stream/layer/mod.rs index 4a49054d8..9e72c91b1 100644 --- a/rama-net/src/stream/layer/mod.rs +++ b/rama-net/src/stream/layer/mod.rs @@ -1,4 +1,4 @@ -//! Rama middleware services that operate directly on network [`rama_core::stream::Stream`] types. +//! Rama middleware services that operate directly on network [`rama_core::io::Io`] types. //! //! Examples are services that can operate directly on a `TCP`, `TLS` or `UDP` stream. diff --git a/rama-net/src/stream/layer/opentelemetry.rs b/rama-net/src/stream/layer/opentelemetry.rs index e6580289e..319553a92 100644 --- a/rama-net/src/stream/layer/opentelemetry.rs +++ b/rama-net/src/stream/layer/opentelemetry.rs @@ -193,7 +193,7 @@ impl Service for NetworkMetricsService where S: Service, F: AttributesFactory, - Stream: rama_core::stream::Stream + ExtensionsRef, + Stream: rama_core::io::Io + ExtensionsRef, { type Output = S::Output; type Error = S::Error; diff --git a/rama-net/src/stream/layer/tracker/bytes.rs b/rama-net/src/stream/layer/tracker/bytes.rs index 838f0d0b8..32ad92641 100644 --- a/rama-net/src/stream/layer/tracker/bytes.rs +++ b/rama-net/src/stream/layer/tracker/bytes.rs @@ -6,8 +6,8 @@ //! is consumed by a protocol consumer, which is for example the case when you wish //! to track the bytes read and/or written for a Tcp stream that is owned by a Tls stream. //! -//! [`AsyncRead`]: crate::stream::AsyncRead -//! [`AsyncWrite`]: crate::stream::AsyncWrite +//! [`AsyncRead`]: tokio::io::AsyncRead +//! [`AsyncWrite`]: tokio::io::AsyncWrite use pin_project_lite::pin_project; use rama_core::{ @@ -33,8 +33,8 @@ pin_project! { /// to get the number of bytes read and/or written even though the [`BytesRWTracker`] /// is consumed by a protocol consumer. /// - /// [`AsyncRead`]: crate::stream::AsyncRead - /// [`AsyncWrite`]: crate::stream::AsyncWrite + /// [`AsyncRead`]: tokio::io::AsyncRead + /// [`AsyncWrite`]: tokio::io::AsyncWrite pub struct BytesRWTracker { read: Arc, written: Arc, @@ -69,8 +69,8 @@ impl BytesRWTracker { /// Create a new [`BytesRWTracker`] that wraps the /// given [`AsyncRead`] and/or [`AsyncWrite`]. /// - /// [`AsyncRead`]: crate::stream::AsyncRead - /// [`AsyncWrite`]: crate::stream::AsyncWrite + /// [`AsyncRead`]: tokio::io::AsyncRead + /// [`AsyncWrite`]: tokio::io::AsyncWrite pub fn new(stream: S) -> Self { Self { read: Arc::new(AtomicUsize::new(0)), @@ -108,8 +108,8 @@ impl BytesRWTracker { /// be updated but will still report the number of bytes read and/or /// written up to the point where this method was called. /// - /// [`AsyncRead`]: crate::stream::AsyncRead - /// [`AsyncWrite`]: crate::stream::AsyncWrite + /// [`AsyncRead`]: tokio::io::AsyncRead + /// [`AsyncWrite`]: tokio::io::AsyncWrite pub fn into_inner(self) -> S { self.stream } diff --git a/rama-net/src/stream/layer/tracker/incoming.rs b/rama-net/src/stream/layer/tracker/incoming.rs index aec20a1aa..0edc036d7 100644 --- a/rama-net/src/stream/layer/tracker/incoming.rs +++ b/rama-net/src/stream/layer/tracker/incoming.rs @@ -1,11 +1,11 @@ use super::bytes::BytesRWTracker; -use rama_core::{Layer, Service, extensions::ExtensionsMut, stream::Stream}; +use rama_core::{Layer, Service, extensions::ExtensionsMut, io::Io}; use rama_utils::macros::define_inner_service_accessors; /// A [`Service`] that wraps a [`Service`]'s input IO [`Stream`] with an atomic R/W tracker. /// /// [`Service`]: rama_core::Service -/// [`Stream`]: rama_core::stream::Stream +/// [`Stream`]: rama_core::io::Io #[derive(Debug, Clone)] pub struct IncomingBytesTrackerService { inner: S, @@ -25,7 +25,7 @@ impl IncomingBytesTrackerService { impl Service for IncomingBytesTrackerService where S: Service>, - IO: Stream + ExtensionsMut, + IO: Io + ExtensionsMut, { type Output = S::Output; type Error = S::Error; @@ -46,7 +46,7 @@ where /// /// [`Layer`]: rama_core::Layer /// [`Service`]: rama_core::Service -/// [`Stream`]: rama_core::stream::Stream +/// [`Stream`]: rama_core::io::Io #[derive(Debug, Clone)] #[non_exhaustive] pub struct IncomingBytesTrackerLayer; diff --git a/rama-net/src/stream/layer/tracker/outgoing.rs b/rama-net/src/stream/layer/tracker/outgoing.rs index b565d7b4b..aae489567 100644 --- a/rama-net/src/stream/layer/tracker/outgoing.rs +++ b/rama-net/src/stream/layer/tracker/outgoing.rs @@ -1,12 +1,12 @@ use super::bytes::BytesRWTracker; use crate::client::{ConnectorService, EstablishedClientConnection}; -use rama_core::{Layer, Service, extensions::ExtensionsMut, stream::Stream}; +use rama_core::{Layer, Service, extensions::ExtensionsMut, io::Io}; use rama_utils::macros::define_inner_service_accessors; /// A [`Service`] that wraps a [`Service`]'s output IO [`Stream`] with an atomic R/W tracker. /// /// [`Service`]: rama_core::Service -/// [`Stream`]: rama_core::stream::Stream +/// [`Stream`]: rama_core::io::Io #[derive(Debug, Clone)] pub struct OutgoingBytesTrackerService { inner: S, @@ -25,7 +25,7 @@ impl OutgoingBytesTrackerService { impl Service for OutgoingBytesTrackerService where - S: ConnectorService, + S: ConnectorService, Input: Send + 'static, { type Output = EstablishedClientConnection, Input>; @@ -44,7 +44,7 @@ where /// /// [`Layer`]: rama_core::Layer /// [`Service`]: rama_core::Service -/// [`Stream`]: rama_core::stream::Stream +/// [`Stream`]: rama_core::io::Io #[derive(Debug, Clone)] #[non_exhaustive] pub struct OutgoingBytesTrackerLayer; diff --git a/rama-net/src/stream/mod.rs b/rama-net/src/stream/mod.rs index d33f5241e..2f5ee734d 100644 --- a/rama-net/src/stream/mod.rs +++ b/rama-net/src/stream/mod.rs @@ -1,6 +1,6 @@ //! Utilities that operate on a [`Stream`] //! -//! [`Stream`]: rama_core::stream::Stream +//! [`Stream`]: rama_core::io::Io pub mod matcher; diff --git a/rama-net/src/stream/service/discard.rs b/rama-net/src/stream/service/discard.rs index 1068fea2d..0fe677a79 100644 --- a/rama-net/src/stream/service/discard.rs +++ b/rama-net/src/stream/service/discard.rs @@ -1,7 +1,7 @@ use rama_core::{ Service, error::{BoxError, ErrorContext as _}, - stream::Stream, + io::Io, }; /// An async service which discard all the incoming bytes, @@ -47,7 +47,7 @@ impl Default for DiscardService { impl Service for DiscardService where - S: Stream + 'static, + S: Io + 'static, { type Output = u64; type Error = BoxError; diff --git a/rama-net/src/stream/service/echo.rs b/rama-net/src/stream/service/echo.rs index afd2afb38..fcf91dc50 100644 --- a/rama-net/src/stream/service/echo.rs +++ b/rama-net/src/stream/service/echo.rs @@ -1,7 +1,7 @@ use rama_core::{ Service, error::{BoxError, ErrorContext as _}, - stream::Stream, + io::Io, }; /// An async service which echoes the incoming bytes back on the same stream. @@ -44,7 +44,7 @@ impl Default for EchoService { impl Service for EchoService where - S: Stream + 'static, + S: Io + 'static, { type Output = u64; type Error = BoxError; diff --git a/rama-net/src/stream/service/mod.rs b/rama-net/src/stream/service/mod.rs index 0f81bd494..359f061e1 100644 --- a/rama-net/src/stream/service/mod.rs +++ b/rama-net/src/stream/service/mod.rs @@ -1,4 +1,4 @@ -//! Rama services that operate directly on [`rama_core::stream::Stream`] types. +//! Rama services that operate directly on [`rama_core::io::Io`] types. //! //! Examples are services that can operate directly on a `TCP`, `TLS` or `UDP` stream. diff --git a/rama-net/src/tls/client/mod.rs b/rama-net/src/tls/client/mod.rs index e3def615d..bd4c40208 100644 --- a/rama-net/src/tls/client/mod.rs +++ b/rama-net/src/tls/client/mod.rs @@ -15,7 +15,7 @@ pub use hello::{ClientHello, ClientHelloExtension, ECHClientHello}; mod parser; pub use parser::{ extract_sni_from_client_hello_handshake, extract_sni_from_client_hello_record, - parse_client_hello, + parse_client_hello, parse_client_hello_handshake, }; mod config; diff --git a/rama-net/src/tls/client/parser.rs b/rama-net/src/tls/client/parser.rs index 4debc43ac..dd8ec0367 100644 --- a/rama-net/src/tls/client/parser.rs +++ b/rama-net/src/tls/client/parser.rs @@ -40,6 +40,43 @@ pub fn parse_client_hello(i: &[u8]) -> Result { } } +/// Parse a [`ClientHello`] from the raw handshake "wire" bytes. +/// +/// Same as [`parse_client_hello`] but for the outer tls handshake, +/// instead of just the record +pub fn parse_client_hello_handshake(i: &[u8]) -> Result { + match parse_client_hello_handshake_inner(i) { + Err(err) => Err(BoxError::from("parse client hello handshake message") + .context_debug_field("err", err.to_owned())), + Ok((i, hello)) => { + if i.is_empty() { + Ok(hello) + } else { + Err(BoxError::from( + "parse client hello handshake message: unexpected trailer content", + )) + } + } + } +} + +fn parse_client_hello_handshake_inner(i: &[u8]) -> IResult<&[u8], ClientHello> { + // verify content type and tls version + let (i, _) = verify(take(3usize), |s: &[u8]| { + matches!(s, [0x16, 0x03, 0x00..=0x04]) + }) + .parse(i)?; + + // skip record length + let (i, _) = be_u16(i)?; + + // verify handshake type and drop the handshake length + let (i, _) = verify(take(4usize), |s: &[u8]| matches!(s, [0x01, ..])).parse(i)?; + + // now it's time for the record + parse_client_hello_inner(i) +} + /// Parse a [`ClientHello`] from the raw incoming "wire" client handshake bytes to find the SNI Host value. /// /// Same as [`extract_sni_from_client_hello_record`] but handles the full handshake bytes, meaning diff --git a/rama-net/src/tls/mod.rs b/rama-net/src/tls/mod.rs index c8b8384d5..cfb6a63b6 100644 --- a/rama-net/src/tls/mod.rs +++ b/rama-net/src/tls/mod.rs @@ -20,7 +20,7 @@ pub mod server; /// to configure the connection in function on an tls tunnel. pub struct TlsTunnel { /// The server name to use for the connection. - pub server_host: crate::address::Host, + pub sni: Option, } #[derive(Debug, Clone, Default)] diff --git a/rama-net/src/tls/server/config.rs b/rama-net/src/tls/server/config.rs index e26424c9c..b417b0379 100644 --- a/rama-net/src/tls/server/config.rs +++ b/rama-net/src/tls/server/config.rs @@ -93,7 +93,7 @@ impl Default for CacheKind { fn default() -> Self { Self::MemCache { max_size: CACHE_KIND_DEFAULT_MAX_SIZE, - ttl: Some(std::time::Duration::from_hours(24 * 7)), // 7 days + ttl: Some(std::time::Duration::from_hours(24 * 89)), // 89 days } } } diff --git a/rama-net/src/tls/server/mod.rs b/rama-net/src/tls/server/mod.rs index cabeb4c50..ccf70c458 100644 --- a/rama-net/src/tls/server/mod.rs +++ b/rama-net/src/tls/server/mod.rs @@ -9,8 +9,15 @@ pub use config::{ mod peek; #[doc(inline)] -pub use peek::{NoTlsRejectError, TlsPeekRouter, TlsPeekStream}; +pub use peek::{NoTlsRejectError, TlsPeekRouter, TlsPrefixedIo}; + +mod peek_client_hello; +#[doc(inline)] +pub use peek_client_hello::{ + InputWithClientHello, PeekTlsClientHelloService, TlsClientHelloPrefixedIo, + peek_client_hello_from_input, +}; mod sni; #[doc(inline)] -pub use sni::{SniPeekStream, SniRequest, SniRouter}; +pub use sni::{SniPrefixedIo, SniRequest, SniRouter}; diff --git a/rama-net/src/tls/server/peek.rs b/rama-net/src/tls/server/peek.rs index e261fa218..04b1a3a69 100644 --- a/rama-net/src/tls/server/peek.rs +++ b/rama-net/src/tls/server/peek.rs @@ -1,9 +1,8 @@ use rama_core::{ Service, error::{BoxError, ErrorContext}, - extensions::ExtensionsMut, + io::{PeekIoProvider, PrefixedIo, StackReader}, service::RejectService, - stream::{PeekStream, StackReader}, telemetry::tracing, }; use tokio::io::AsyncReadExt; @@ -42,19 +41,28 @@ impl TlsPeekRouter { } } -impl Service for TlsPeekRouter +impl Service for TlsPeekRouter where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + PeekableInput: PeekIoProvider, Output: Send + 'static, - T: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + T: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, { type Output = Output; type Error = BoxError; - async fn serve(&self, mut stream: Stream) -> Result { + async fn serve(&self, mut input: PeekableInput) -> Result { let mut peek_buf = [0u8; TLS_HEADER_PEEK_LEN]; - let n = stream + let n = input + .peek_io_mut() .read(&mut peek_buf) .await .context("try to read tls prefix header")?; @@ -68,23 +76,23 @@ where peek_buf.copy_within(0..n, offset); } - let mut peek = StackReader::new(peek_buf); - peek.skip(offset); + let mut peek_stack_data = StackReader::new(peek_buf); + peek_stack_data.skip(offset); - let stream = PeekStream::new(peek, stream); + let mapped_input = input.map_peek_io(|io| PrefixedIo::new(peek_stack_data, io)); if is_tls { - self.tls_acceptor.serve(stream).await.into_box_error() + self.tls_acceptor.serve(mapped_input).await.into_box_error() } else { - self.fallback.serve(stream).await.into_box_error() + self.fallback.serve(mapped_input).await.into_box_error() } } } const TLS_HEADER_PEEK_LEN: usize = 5; -/// [`PeekStream`] alias used by [`TlsPeekRouter`]. -pub type TlsPeekStream = PeekStream, S>; +/// [`PrefixedIo`] alias used by [`TlsPeekRouter`]. +pub type TlsPrefixedIo = PrefixedIo, S>; #[cfg(test)] mod test { @@ -94,7 +102,7 @@ mod test { }; use std::convert::Infallible; - use rama_core::stream::Stream; + use rama_core::io::Io; use super::*; @@ -136,7 +144,7 @@ mod test { async fn test_peek_router_read_eof() { const CONTENT: &[u8] = b"\x16\x03\x03\x00\x2afoo"; - async fn tls_service_fn(mut stream: impl Stream + Unpin) -> Result<&'static str, BoxError> { + async fn tls_service_fn(mut stream: impl Io + Unpin) -> Result<&'static str, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; assert_eq!(CONTENT, v); @@ -166,9 +174,7 @@ mod test { } let tls_service = service_fn(tls_service_fn); - async fn plain_service_fn( - mut stream: impl Stream + Unpin, - ) -> Result, BoxError> { + async fn plain_service_fn(mut stream: impl Io + Unpin) -> Result, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; Ok(v) diff --git a/rama-net/src/tls/server/peek_client_hello.rs b/rama-net/src/tls/server/peek_client_hello.rs new file mode 100644 index 000000000..9d8ae4067 --- /dev/null +++ b/rama-net/src/tls/server/peek_client_hello.rs @@ -0,0 +1,404 @@ +use rama_core::{ + Service, + error::{BoxError, ErrorContext}, + io::{HeapReader, PeekIoProvider, PrefixedIo}, + service::RejectService, + telemetry::tracing, +}; +use tokio::io::AsyncReadExt; + +use crate::tls::client::{ClientHello, parse_client_hello_handshake}; + +use super::NoTlsRejectError; + +/// A peek [`Service`] which returns the [`ClientHello`] to the inner +/// service for tls-detected traffic, and otherwise make use of the Reject service. +/// +/// The difference with [`SniRouter`] is that the entire +/// [`ClientHello`] is parsed and returned instead of just the SNI. Use the router +/// in case all you care about is the SNI, given it is more efficient. +/// +/// By default non-tls traffic is rejected using [`RejectService`]. +/// Use [`PeekTlsClientHelloService::with_fallback`] to configure the fallback service. +/// +/// Use the standalone [`peek_client_hello_from_input`] function if you prefer +/// this is as a standalone function instead. +/// +/// [`SniRouter`]: super::SniRouter +#[derive(Debug, Clone)] +pub struct PeekTlsClientHelloService> { + service: S, + fallback: F, +} + +impl PeekTlsClientHelloService { + /// Create a new [`PeekTlsClientHelloService`]. + pub fn new(service: S) -> Self { + Self { + service, + fallback: RejectService::new(NoTlsRejectError), + } + } + + /// Attach a fallback [`Service`] to this [`PeekTlsClientHelloService`]. + /// + /// Used in case the traffic is not Tls traffic (defined by the first bytes). + pub fn with_fallback(self, fallback: F) -> PeekTlsClientHelloService { + PeekTlsClientHelloService { + service: self.service, + fallback, + } + } +} + +/// Functional API to try to peek TLS:CH from an existing I/O input, +/// returning the stream as-is with the read data prefixed from memory. +/// +/// Use [`PeekTlsClientHelloService`] if you prefer it as a rama [`Service`] instead. +pub async fn peek_client_hello_from_input( + mut input: PeekableInput, +) -> Result< + ( + PeekableInput::Mapped>, + Option, + ), + std::io::Error, +> +where + PeekableInput: PeekIoProvider, +{ + let mut peek_buf = [0u8; TLS_HEADER_PEEK_LEN]; + let peekable_io = input.peek_io_mut(); + + let n = peekable_io.read(&mut peek_buf).await?; + + let is_tls = n == TLS_HEADER_PEEK_LEN && matches!(peek_buf, [0x16, 0x03, 0x00..=0x04, ..]); + tracing::trace!("tls prefix header read (is tls: {is_tls})"); + + if !is_tls { + if TLS_HEADER_PEEK_LEN.saturating_sub(n) > 0 { + tracing::trace!("move tls peek buffer cursor due to reading not enough (read: {n})"); + } + + let prefix_data = HeapReader::from(&peek_buf[..n.min(TLS_HEADER_PEEK_LEN)]); + let peeked_input = input.map_peek_io(|io| PrefixedIo::new(prefix_data, io)); + + tracing::trace!("return early for non-tls traffic: missing peek header"); + return Ok((peeked_input, None)); + } + + let n = ((peek_buf[3] as usize) << 8) | (peek_buf[4] as usize); + let record_size = (n + TLS_HEADER_PEEK_LEN).min(2048); // limit to 2k bytes, should be plenty for a record that's usually <=500 bytes + + let mut v = vec![0u8; record_size]; + v[..TLS_HEADER_PEEK_LEN].copy_from_slice(&peek_buf[..]); + let read_size = peekable_io.read(&mut v[TLS_HEADER_PEEK_LEN..]).await?; + + if read_size != n { + tracing::trace!( + read_size, + "unexpected read size for client hello handshake data: try regardless..." + ); + } + let maybe_client_hello = + parse_client_hello_handshake(&v[..record_size - (n.saturating_sub(read_size))]) + .inspect_err(|err| { + tracing::debug!( + "failed parse client hello handshake bytes: {err}; return as non-tls traffic", + ) + }) + .ok(); + + let prefix_data = HeapReader::from(v); + let peeked_input = input.map_peek_io(|io| PrefixedIo::new(prefix_data, io)); + + Ok((peeked_input, maybe_client_hello)) +} + +impl Service for PeekTlsClientHelloService +where + PeekableInput: PeekIoProvider, + Output: Send + 'static, + S: Service< + InputWithClientHello< + PeekableInput::Mapped>, + >, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, +{ + type Output = Output; + type Error = BoxError; + + async fn serve(&self, input: PeekableInput) -> Result { + let (peeked_input, maybe_client_hello) = peek_client_hello_from_input(input) + .await + .context("I/O error while peeking TLS:CH from existing input")?; + + if let Some(client_hello) = maybe_client_hello { + self.service + .serve(InputWithClientHello { + input: peeked_input, + client_hello, + }) + .await + .map_err(Into::into) + } else { + self.fallback.serve(peeked_input).await.map_err(Into::into) + } + } +} + +const TLS_HEADER_PEEK_LEN: usize = 5; + +/// [`PrefixedIo`] alias used by [`PeekTlsClientHelloService`]. +pub type TlsClientHelloPrefixedIo = PrefixedIo; + +/// An `input` with a Client Hello (tls) attached to it, +/// usually used in combination with [`PeekTlsClientHelloService`]. +#[derive(Debug, Clone)] +pub struct InputWithClientHello { + pub input: Input, + pub client_hello: ClientHello, +} + +#[cfg(test)] +mod test { + use rama_core::{ + ServiceInput, + service::{RejectError, service_fn}, + }; + use std::convert::Infallible; + + use rama_core::io::Io; + + use super::*; + + const CH_ONE_ONE_ONE_ONE: &[u8] = &[ + 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0x02, 0x15, 0xfd, 0xe2, + 0x92, 0xc0, 0x46, 0x9f, 0x92, 0xbe, 0xd7, 0xe9, 0x1a, 0x3c, 0x50, 0x5e, 0x55, 0x49, 0x17, + 0xa6, 0xf8, 0xa5, 0xca, 0xa4, 0x6d, 0x60, 0xcc, 0xea, 0xf7, 0x25, 0xf0, 0x6e, 0x20, 0x41, + 0x20, 0x18, 0x66, 0x5c, 0xae, 0x08, 0xb0, 0x10, 0x96, 0x3c, 0xad, 0xb4, 0x13, 0xe1, 0x92, + 0xce, 0x96, 0xad, 0x9d, 0x45, 0x05, 0xb7, 0xa6, 0x4c, 0x01, 0x71, 0x08, 0x74, 0x0d, 0x1f, + 0x35, 0x00, 0x2a, 0x3a, 0x3a, 0x13, 0x01, 0x13, 0x02, 0x13, 0x03, 0xc0, 0x2c, 0xc0, 0x2b, + 0xcc, 0xa9, 0xc0, 0x30, 0xc0, 0x2f, 0xcc, 0xa8, 0xc0, 0x0a, 0xc0, 0x09, 0xc0, 0x14, 0xc0, + 0x13, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x35, 0x00, 0x2f, 0xc0, 0x08, 0xc0, 0x12, 0x00, 0x0a, + 0x01, 0x00, 0x01, 0x89, 0xda, 0xda, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x00, 0x12, 0x00, + 0x00, 0x0f, 0x6f, 0x6e, 0x65, 0x2e, 0x6f, 0x6e, 0x65, 0x2e, 0x6f, 0x6e, 0x65, 0x2e, 0x6f, + 0x6e, 0x65, 0x00, 0x17, 0x00, 0x00, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x0a, 0x00, 0x0c, + 0x00, 0x0a, 0xfa, 0xfa, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0b, 0x00, + 0x02, 0x01, 0x00, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, + 0x16, 0x00, 0x14, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05, 0x08, 0x05, + 0x05, 0x01, 0x08, 0x06, 0x06, 0x01, 0x02, 0x01, 0x00, 0x12, 0x00, 0x00, 0x00, 0x33, 0x00, + 0x2b, 0x00, 0x29, 0xfa, 0xfa, 0x00, 0x01, 0x00, 0x00, 0x1d, 0x00, 0x20, 0x7c, 0xe1, 0xc6, + 0xc2, 0x01, 0x69, 0x42, 0xba, 0x2b, 0xec, 0x07, 0x2f, 0x04, 0xbd, 0xb6, 0x2a, 0x7e, 0x04, + 0x6b, 0x96, 0x98, 0x51, 0x4e, 0x80, 0xb3, 0x2a, 0x4c, 0x4f, 0x1f, 0x39, 0x82, 0x2b, 0x00, + 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, 0x2b, 0x00, 0x0b, 0x0a, 0x6a, 0x6a, 0x03, 0x04, 0x03, + 0x03, 0x03, 0x02, 0x03, 0x01, 0x00, 0x1b, 0x00, 0x03, 0x02, 0x00, 0x01, 0x3a, 0x3a, 0x00, + 0x01, 0x00, 0x00, 0x15, 0x00, 0xd3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + const TLS_BUT_NO_SNI: &[u8] = &[ + 0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc, 0x03, 0x03, 0x28, 0x5b, 0x8f, 0x90, + 0x22, 0x2a, 0x90, 0x95, 0x89, 0xa9, 0x62, 0x1f, 0xdb, 0x68, 0xbe, 0x4c, 0x0e, 0xdf, 0xe4, + 0x76, 0x50, 0x48, 0xa5, 0x40, 0x56, 0x5f, 0x9a, 0xba, 0x19, 0x29, 0x66, 0xdd, 0x20, 0x7a, + 0x7f, 0x7e, 0xc7, 0xbd, 0xfb, 0x88, 0x07, 0xd9, 0xf5, 0x99, 0xfa, 0xf3, 0x0d, 0x37, 0x30, + 0x52, 0x4d, 0x44, 0xe4, 0x26, 0xc0, 0xd1, 0x9a, 0xcd, 0x78, 0xf6, 0x7a, 0xf1, 0x7a, 0x66, + 0xe1, 0x00, 0x3e, 0x13, 0x02, 0x13, 0x03, 0x13, 0x01, 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, + 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, 0x00, 0x9e, 0xc0, 0x24, 0xc0, + 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, 0x00, 0x39, + 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, 0x00, 0x3c, 0x00, + 0x35, 0x00, 0x2f, 0x00, 0xff, 0x01, 0x00, 0x01, 0x75, 0x00, 0x0b, 0x00, 0x04, 0x03, 0x00, + 0x01, 0x02, 0x00, 0x0a, 0x00, 0x16, 0x00, 0x14, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x1e, 0x00, + 0x19, 0x00, 0x18, 0x01, 0x00, 0x01, 0x01, 0x01, 0x02, 0x01, 0x03, 0x01, 0x04, 0x00, 0x10, + 0x00, 0x0e, 0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31, 0x2e, + 0x31, 0x00, 0x16, 0x00, 0x00, 0x00, 0x17, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x0d, + 0x00, 0x2a, 0x00, 0x28, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x08, 0x07, 0x08, 0x08, 0x08, + 0x09, 0x08, 0x0a, 0x08, 0x0b, 0x08, 0x04, 0x08, 0x05, 0x08, 0x06, 0x04, 0x01, 0x05, 0x01, + 0x06, 0x01, 0x03, 0x03, 0x03, 0x01, 0x03, 0x02, 0x04, 0x02, 0x05, 0x02, 0x06, 0x02, 0x00, + 0x2b, 0x00, 0x05, 0x04, 0x03, 0x04, 0x03, 0x03, 0x00, 0x2d, 0x00, 0x02, 0x01, 0x01, 0x00, + 0x33, 0x00, 0x26, 0x00, 0x24, 0x00, 0x1d, 0x00, 0x20, 0xe0, 0xb9, 0xfb, 0x5a, 0xd5, 0x60, + 0x30, 0x39, 0xad, 0xfb, 0xd3, 0x94, 0xa2, 0xff, 0x08, 0x71, 0x9b, 0xcc, 0x6f, 0xbe, 0x9e, + 0xcc, 0x7b, 0xad, 0x3c, 0xd0, 0xde, 0xe8, 0x3e, 0x5d, 0xba, 0x6b, 0x00, 0x15, 0x00, 0xca, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + + #[tokio::test] + async fn test_client_hello_peek_service() { + let tls_service = service_fn(async |input: InputWithClientHello<_>| { + let sni = input + .client_hello + .ext_server_name() + .map(ToString::to_string); + Ok::<_, Infallible>(sni) + }); + let plain_service = service_fn(async || Ok::<_, Infallible>(Some("plain".to_owned()))); + + let peek_tls_svc = PeekTlsClientHelloService::new(tls_service).with_fallback(plain_service); + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new(b"".to_vec()))) + .await + .unwrap(); + assert_eq!(Some("plain".to_owned()), response); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new( + CH_ONE_ONE_ONE_ONE.to_vec(), + ))) + .await + .unwrap(); + assert_eq!(Some("one.one.one.one".to_owned()), response); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new(b"foo".to_vec()))) + .await + .unwrap(); + assert_eq!(Some("plain".to_owned()), response); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new(b"foobar".to_vec()))) + .await + .unwrap(); + assert_eq!(Some("plain".to_owned()), response); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new( + TLS_BUT_NO_SNI.to_vec(), + ))) + .await + .unwrap(); + assert_eq!(None, response); + } + + #[tokio::test] + async fn test_peek_router_read_eof() { + async fn tls_service_fn( + InputWithClientHello { + mut input, + client_hello, + }: InputWithClientHello, + ) -> Result<&'static str, BoxError> { + let mut v = Vec::default(); + let _ = input.read_to_end(&mut v).await?; + assert_eq!(CH_ONE_ONE_ONE_ONE, v); + assert!(client_hello.ext_server_name().is_some()); + assert_eq!( + "one.one.one.one", + client_hello.ext_server_name().unwrap().to_string() + ); + Ok("ok") + } + let tls_service = service_fn(tls_service_fn); + + let peek_tls_svc = + PeekTlsClientHelloService::new(tls_service).with_fallback(RejectService::< + &'static str, + RejectError, + >::new( + RejectError::default() + )); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new( + CH_ONE_ONE_ONE_ONE.to_vec(), + ))) + .await + .unwrap(); + assert_eq!("ok", response); + } + + #[tokio::test] + async fn test_peek_router_read_no_tls_eof() { + let cases = ["", "foo", "abcd", "abcde", "foobarbazbananas"]; + for content in cases { + async fn tls_service_fn() -> Result, BoxError> { + Ok("tls".as_bytes().to_vec()) + } + let tls_service = service_fn(tls_service_fn); + + async fn plain_service_fn(mut stream: impl Io + Unpin) -> Result, BoxError> { + let mut v = Vec::default(); + let _ = stream.read_to_end(&mut v).await?; + Ok(v) + } + let plain_service = service_fn(plain_service_fn); + + let peek_tls_svc = + PeekTlsClientHelloService::new(tls_service).with_fallback(plain_service); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new( + content.as_bytes().to_vec(), + ))) + .await + .unwrap(); + + assert_eq!(content.as_bytes(), &response[..]); + } + } + + #[tokio::test] + async fn test_peek_router_read_tls_no_sni_eof() { + async fn tls_service_fn( + InputWithClientHello { + mut input, + client_hello, + }: InputWithClientHello, + ) -> Result<&'static str, BoxError> { + let mut v = Vec::default(); + let _ = input.read_to_end(&mut v).await?; + assert_eq!(TLS_BUT_NO_SNI, v); + assert!(client_hello.ext_server_name().is_none()); + Ok("ok") + } + let tls_service = service_fn(tls_service_fn); + + let peek_tls_svc = + PeekTlsClientHelloService::new(tls_service).with_fallback(RejectService::< + &'static str, + RejectError, + >::new( + RejectError::default() + )); + + let response = peek_tls_svc + .serve(ServiceInput::new(std::io::Cursor::new( + TLS_BUT_NO_SNI.to_vec(), + ))) + .await + .unwrap(); + assert_eq!("ok", response); + } +} diff --git a/rama-net/src/tls/server/sni.rs b/rama-net/src/tls/server/sni.rs index ffff53909..720f2eef0 100644 --- a/rama-net/src/tls/server/sni.rs +++ b/rama-net/src/tls/server/sni.rs @@ -10,15 +10,15 @@ use rama_core::{ Service, error::{BoxError, ErrorContext}, extensions::ExtensionsMut, + io::{HeapReader, PrefixedIo, StackReader}, service::RejectService, - stream::{HeapReader, PeekStream, StackReader}, telemetry::tracing, }; use tokio::io::{AsyncBufRead, AsyncRead, AsyncReadExt, AsyncWrite, ReadBuf}; use crate::{address::Domain, tls::client::extract_sni_from_client_hello_handshake}; -use super::{NoTlsRejectError, TlsPeekStream}; +use super::{NoTlsRejectError, TlsPrefixedIo}; /// A [`Service`] router that can be used to support /// routing of tls traffic as well as non-tls traffic. @@ -61,10 +61,10 @@ impl SniRouter { impl Service for SniRouter where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + Stream: rama_core::io::Io + Unpin + ExtensionsMut, Output: Send + 'static, S: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + F: Service, Output = Output, Error: Into>, { type Output = Output; type Error = BoxError; @@ -90,7 +90,7 @@ where let mut peek = StackReader::new(peek_buf); peek.skip(offset); - let stream = PeekStream::new(peek, stream); + let stream = PrefixedIo::new(peek, stream); tracing::trace!("fallback to non-tls service"); return self.fallback.serve(stream).await.into_box_error(); @@ -117,7 +117,7 @@ where .context("parse client hello handshake bytes and extract SNI")?; let mem_reader = HeapReader::from(v); - let peek_stream = PeekStream::new(mem_reader, stream); + let peek_stream = PrefixedIo::new(mem_reader, stream); self.service .serve(SniRequest { @@ -131,8 +131,8 @@ where const TLS_HEADER_PEEK_LEN: usize = 5; -/// [`PeekStream`] alias used by [`SniRouter`]. -pub type SniPeekStream = PeekStream; +/// [`PrefixedIo`] alias used by [`SniRouter`]. +pub type SniPrefixedIo = PrefixedIo; pin_project! { /// A request ready for SNI routing, @@ -140,7 +140,7 @@ pin_project! { #[derive(Debug, Clone)] pub struct SniRequest { #[pin] - pub stream: SniPeekStream, + pub stream: SniPrefixedIo, pub sni: Option, } } @@ -289,7 +289,7 @@ mod test { }; use std::convert::Infallible; - use rama_core::stream::Stream; + use rama_core::io::Io; use super::*; @@ -417,7 +417,7 @@ mod test { #[tokio::test] async fn test_peek_router_read_eof() { async fn tls_service_fn( - SniRequest { mut stream, sni }: SniRequest, + SniRequest { mut stream, sni }: SniRequest, ) -> Result<&'static str, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; @@ -451,9 +451,7 @@ mod test { } let tls_service = service_fn(tls_service_fn); - async fn plain_service_fn( - mut stream: impl Stream + Unpin, - ) -> Result, BoxError> { + async fn plain_service_fn(mut stream: impl Io + Unpin) -> Result, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; Ok(v) @@ -476,7 +474,7 @@ mod test { #[tokio::test] async fn test_peek_router_read_tls_no_sni_eof() { async fn tls_service_fn( - SniRequest { mut stream, sni }: SniRequest, + SniRequest { mut stream, sni }: SniRequest, ) -> Result<&'static str, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; diff --git a/rama-net/src/user/credentials/mod.rs b/rama-net/src/user/credentials/mod.rs index f80859975..5e9535ce9 100644 --- a/rama-net/src/user/credentials/mod.rs +++ b/rama-net/src/user/credentials/mod.rs @@ -12,4 +12,4 @@ pub use bearer::{Bearer, bearer}; mod proxy; #[doc(inline)] -pub use proxy::ProxyCredential; +pub use proxy::{DpiProxyCredential, ProxyCredential}; diff --git a/rama-net/src/user/credentials/proxy.rs b/rama-net/src/user/credentials/proxy.rs index 3ca35705d..4741c9279 100644 --- a/rama-net/src/user/credentials/proxy.rs +++ b/rama-net/src/user/credentials/proxy.rs @@ -2,6 +2,12 @@ use std::fmt; use super::{Basic, Bearer}; +#[derive(Debug, Clone)] +/// Extension wrapper that can be used by +/// Deep Protocol Inspection (DPI) services which +/// processed an exchanged [`ProxyCredential`]. +pub struct DpiProxyCredential(pub ProxyCredential); + #[derive(Debug, Clone, PartialEq, Eq)] /// Proxy credentials. pub enum ProxyCredential { diff --git a/rama-proxy/README.md b/rama-proxy/README.md index eee9e9e7c..d7d1cecb2 100644 --- a/rama-proxy/README.md +++ b/rama-proxy/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-socks5/Cargo.toml b/rama-socks5/Cargo.toml index 4f47d589f..1e036fd13 100644 --- a/rama-socks5/Cargo.toml +++ b/rama-socks5/Cargo.toml @@ -19,20 +19,20 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] -dns = ["dep:rama-dns", "dep:rand"] [dependencies] byteorder = { workspace = true } rama-core = { workspace = true } -rama-dns = { workspace = true, optional = true } +rama-dns = { workspace = true } rama-net = { workspace = true, features = ["http"] } rama-tcp = { workspace = true, features = ["http"] } rama-udp = { workspace = true } rama-utils = { workspace = true } -rand = { workspace = true, optional = true } +rand = { workspace = true } tokio = { workspace = true } [dev-dependencies] +parking_lot = { workspace = true } tokio-test = { workspace = true } [lints] diff --git a/rama-socks5/README.md b/rama-socks5/README.md index 39aad0551..35e78f1f7 100644 --- a/rama-socks5/README.md +++ b/rama-socks5/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-socks5/src/client/bind.rs b/rama-socks5/src/client/bind.rs index 673a7ede1..d21225b1b 100644 --- a/rama-socks5/src/client/bind.rs +++ b/rama-socks5/src/client/bind.rs @@ -4,7 +4,7 @@ //! [`bind-flow`]: crate::proto::Command::Bind //! [`Client`]: crate::Socks5Client -use rama_core::stream::Stream; +use rama_core::io::Io; use rama_core::telemetry::tracing; use rama_net::address::{HostWithPort, SocketAddress}; use std::fmt; @@ -86,7 +86,7 @@ pub struct BindOutput { pub server: HostWithPort, } -impl Binder { +impl Binder { pub(crate) fn new( stream: S, requested_bind_address: Option, diff --git a/rama-socks5/src/client/core.rs b/rama-socks5/src/client/core.rs index e3005f723..bc13e6d56 100644 --- a/rama-socks5/src/client/core.rs +++ b/rama-socks5/src/client/core.rs @@ -1,5 +1,5 @@ use rama_core::error::BoxError; -use rama_core::stream::Stream; +use rama_core::io::Io; use rama_core::telemetry::tracing; use rama_net::address::{Host, HostWithPort, SocketAddress}; use rama_utils::collections::smallvec::smallvec; @@ -184,7 +184,7 @@ impl Client { /// In case the handshake was sucessfull it will return /// the local address used by the Socks5 (proxy) server /// to connect to the destination [`HostWithPort`] on behalf of this [`Client`]. - pub async fn handshake_connect( + pub async fn handshake_connect( &self, stream: &mut S, destination: &HostWithPort, @@ -233,9 +233,9 @@ impl Client { /// /// This method returns a [`Binder`] that contains the address to which the target server /// is to connect to the socks5 server on behalf of the client (callee of this call). - /// The [`Binder`] takes ownership over of the input [`Stream`] such that it can + /// The [`Binder`] takes ownership over of the input [`Io`] such that it can /// await the established connection from target server to socks5 server. - pub async fn handshake_bind( + pub async fn handshake_bind( &self, mut stream: S, requested_bind_address: Option, @@ -313,7 +313,7 @@ impl Client { /// socks5 proxy server to the required. /// /// [`Service`]: rama_core::Service - pub async fn handshake_udp( + pub async fn handshake_udp( &self, mut stream: S, ) -> Result, HandshakeError> { @@ -327,7 +327,7 @@ impl Client { Ok(UdpSocketRelayBinder::new(stream)) } - async fn handshake_headers_auth( + async fn handshake_headers_auth( &self, stream: &mut S, auth: &Socks5Auth, @@ -408,7 +408,7 @@ impl Client { Ok(auth_method) } - async fn handshake_headers_no_auth( + async fn handshake_headers_no_auth( &self, stream: &mut S, ) -> Result { diff --git a/rama-socks5/src/client/proxy_connector.rs b/rama-socks5/src/client/proxy_connector.rs index 14ea2ba3f..888a7ed72 100644 --- a/rama-socks5/src/client/proxy_connector.rs +++ b/rama-socks5/src/client/proxy_connector.rs @@ -6,28 +6,25 @@ use rama_core::{ Layer, Service, error::{BoxError, ErrorContext as _, ErrorExt}, extensions::ExtensionsMut, - stream::Stream, + io::Io, telemetry::tracing, }; +use rama_dns::client::{ + GlobalDnsResolver, + resolver::{BoxDnsAddressResolver, DnsAddressResolver}, +}; use rama_net::{ + Protocol, + address::Host, address::ProxyAddress, client::{ConnectorService, EstablishedClientConnection}, + mode::DnsResolveIpMode, transport::TryRefIntoTransportContext, user::ProxyCredential, }; -use rama_utils::macros::define_inner_service_accessors; - -#[cfg(feature = "dns")] -use ::{ - rama_dns::client::{ - GlobalDnsResolver, - resolver::{BoxDnsAddressResolver, DnsAddressResolver}, - }, - rama_net::{Protocol, address::Host, mode::DnsResolveIpMode}, - rama_utils::macros::generate_set_and_with, - std::net::IpAddr, - tokio::sync::mpsc, -}; +use rama_utils::macros::{define_inner_service_accessors, generate_set_and_with}; +use std::net::IpAddr; +use tokio::sync::mpsc; #[derive(Debug, Clone, Default)] /// A [`Layer`] which wraps the given service with a [`Socks5ProxyConnector`]. @@ -35,7 +32,6 @@ use ::{ /// See [`Socks5ProxyConnector`] for more information. pub struct Socks5ProxyConnectorLayer { required: bool, - #[cfg(feature = "dns")] dns_resolver: Option, } @@ -50,7 +46,6 @@ impl Socks5ProxyConnectorLayer { pub fn optional() -> Self { Self { required: false, - #[cfg(feature = "dns")] dns_resolver: None, } } @@ -65,13 +60,11 @@ impl Socks5ProxyConnectorLayer { pub fn required() -> Self { Self { required: true, - #[cfg(feature = "dns")] dns_resolver: None, } } } -#[cfg(feature = "dns")] impl Socks5ProxyConnectorLayer { generate_set_and_with! { /// Attach the [`Default`] [`DnsResolver`] to this [`Socks5ProxyConnectorLayer`]. @@ -81,7 +74,6 @@ impl Socks5ProxyConnectorLayer { /// /// In case of an error with resolving the domain address the connector /// will anyway use the domain instead of the ip. - #[cfg_attr(docsrs, doc(cfg(feature = "dns")))] pub fn default_dns_resolver(mut self) -> Self { self.dns_resolver = Some(GlobalDnsResolver::new().into_box_dns_address_resolver()); self @@ -96,7 +88,6 @@ impl Socks5ProxyConnectorLayer { /// /// In case of an error with resolving the domain address the connector /// will anyway use the domain instead of the ip. - #[cfg_attr(docsrs, doc(cfg(feature = "dns")))] pub fn dns_resolver(mut self, resolver: impl DnsAddressResolver) -> Self { self.dns_resolver = Some(resolver.into_box_dns_address_resolver()); self @@ -111,7 +102,6 @@ impl Layer for Socks5ProxyConnectorLayer { Socks5ProxyConnector { inner, required: self.required, - #[cfg(feature = "dns")] dns_resolver: self.dns_resolver.clone(), } } @@ -120,7 +110,6 @@ impl Layer for Socks5ProxyConnectorLayer { Socks5ProxyConnector { inner, required: self.required, - #[cfg(feature = "dns")] dns_resolver: self.dns_resolver, } } @@ -136,7 +125,6 @@ impl Layer for Socks5ProxyConnectorLayer { pub struct Socks5ProxyConnector { inner: S, required: bool, - #[cfg(feature = "dns")] dns_resolver: Option, } @@ -146,7 +134,6 @@ impl Socks5ProxyConnector { Self { inner, required, - #[cfg(feature = "dns")] dns_resolver: None, } } @@ -166,7 +153,6 @@ impl Socks5ProxyConnector { define_inner_service_accessors!(); } -#[cfg(feature = "dns")] impl Socks5ProxyConnector { generate_set_and_with! { /// Attach the [`Default`] [`DnsResolver`] to this [`Socks5ProxyConnector`]. @@ -176,7 +162,6 @@ impl Socks5ProxyConnector { /// /// In case of an error with resolving the domain address the connector /// will anyway use the domain instead of the ip. - #[cfg_attr(docsrs, doc(cfg(feature = "dns")))] pub fn default_dns_resolver(mut self) -> Self { self.dns_resolver = Some(GlobalDnsResolver::default().into_box_dns_address_resolver()); self @@ -191,7 +176,6 @@ impl Socks5ProxyConnector { /// /// In case of an error with resolving the domain address the connector /// will anyway use the domain instead of the ip. - #[cfg_attr(docsrs, doc(cfg(feature = "dns")))] pub fn dns_resolver(mut self, resolver: impl DnsAddressResolver) -> Self { self.dns_resolver = Some(resolver.into_box_dns_address_resolver()); self @@ -199,7 +183,6 @@ impl Socks5ProxyConnector { } } -#[cfg(feature = "dns")] impl Socks5ProxyConnector { async fn normalize_socks5_proxy_addr( &self, @@ -340,7 +323,7 @@ impl Socks5ProxyConnector { impl Service for Socks5ProxyConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + Send + ExtensionsMut @@ -362,7 +345,6 @@ where )); } - #[cfg(feature = "dns")] let address = match address { Some(addr) => { let addr = self diff --git a/rama-socks5/src/client/udp.rs b/rama-socks5/src/client/udp.rs index c983e9fc0..94b68d5a1 100644 --- a/rama-socks5/src/client/udp.rs +++ b/rama-socks5/src/client/udp.rs @@ -5,8 +5,8 @@ use rama_core::futures::Stream; use rama_core::stream::codec::{Decoder, Encoder}; use rama_core::telemetry::tracing; use rama_net::address::HostWithPort; -use rama_net::{address::SocketAddress, socket::Interface}; -use rama_udp::{UdpSocket, bind_udp}; +use rama_net::address::SocketAddress; +use rama_udp::{UdpSocket, bind_udp_with_address}; use std::pin::Pin; use std::task::{Context, Poll, ready}; use std::{fmt, io, net::SocketAddr}; @@ -23,20 +23,20 @@ pub struct UdpSocketRelayBinder { stream: S, } -impl UdpSocketRelayBinder { +impl UdpSocketRelayBinder { pub(crate) fn new(stream: S) -> Self { Self { stream } } - /// Bind the relay as an Udp socket on the given interface, + /// Bind the relay as an Udp socket on the given address, /// and complete the association handshake with as goal /// to have a relay proxy udp connection established at the end /// of this bind fn call. - pub async fn bind( + pub async fn bind_address( mut self, - interface: impl TryInto>, + address: impl TryInto>, ) -> Result, HandshakeError> { - let socket = bind_udp(interface).await.map_err(|err| { + let socket = bind_udp_with_address(address).await.map_err(|err| { HandshakeError::other(err).with_context("bind udp socket ready for sending") })?; @@ -124,7 +124,7 @@ impl fmt::Debug for UdpSocketRelay { } } -impl UdpSocketRelay { +impl UdpSocketRelay { /// Returns the local address that this socket is bound to. #[inline] pub fn local_addr(&self) -> io::Result { @@ -307,7 +307,7 @@ pub struct UdpFramedRelay { current_addr: Option, } -impl UdpFramedRelay { +impl UdpFramedRelay { /// Returns the local address that this relay's underlying [`UdpSocket`] is bound to. #[inline] pub fn local_addr(&self) -> io::Result { @@ -355,7 +355,7 @@ impl Unpin for UdpFramedRelay {} impl Stream for UdpFramedRelay where C: Decoder>, - S: rama_core::stream::Stream + Unpin, + S: rama_core::io::Io + Unpin, { type Item = Result<(C::Item, SocketAddress), BoxError>; @@ -404,7 +404,7 @@ where impl Sink<(I, SocketAddr)> for UdpFramedRelay where C: Encoder>, - S: rama_core::stream::Stream + Unpin, + S: rama_core::io::Io + Unpin, { type Error = BoxError; diff --git a/rama-socks5/src/lib.rs b/rama-socks5/src/lib.rs index 8d9466ca5..c9910609b 100644 --- a/rama-socks5/src/lib.rs +++ b/rama-socks5/src/lib.rs @@ -57,5 +57,7 @@ pub use client::{Socks5ProxyConnector, Socks5ProxyConnectorLayer}; pub mod server; pub use server::Socks5Acceptor; +pub mod proxy; + mod auth; pub use auth::Socks5Auth; diff --git a/rama-socks5/src/proxy/mitm/mod.rs b/rama-socks5/src/proxy/mitm/mod.rs new file mode 100644 index 000000000..8eb7ebe1c --- /dev/null +++ b/rama-socks5/src/proxy/mitm/mod.rs @@ -0,0 +1,615 @@ +use std::time::Duration; + +use rama_core::{ + error::{BoxError, ErrorContext as _}, + extensions::{self}, + io::Io, + rt::Executor, + telemetry::tracing, +}; +use rama_dns::client::{GlobalDnsResolver, resolver::DnsAddressResolver}; +use rama_net::{ + address::HostWithPort, + user::{ProxyCredential, credentials::DpiProxyCredential}, +}; +use rama_tcp::{ + TcpStream, + client::{TcpStreamConnector, tcp_connect}, +}; +use rama_utils::macros::generate_set_and_with; + +use crate::proto; + +mod service; +pub use self::service::Socks5MitmRelayService; + +// TODO: Replace tcp_connector with +// - egress_connector, which has to be a ConnectorService returning an Io... +// this decouples it from Tcp which honeslty shoult not be required here to be tied, +// even if usually it is... It also allows this stack to be layered + +#[derive(Debug, Clone)] +/// A utility that can be used by MITM services such as transparent proxies, +/// in order to relay a socks5 proxy connection between a client and server, +/// as part of a deep protocol inspection protocol (DPI) flow. +pub struct Socks5MitmRelay { + dns: Dns, + tcp_connector: Connector, + connect_timeout: Duration, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Outcome of [`Socks5MitmRelay::handshake`]. +pub enum Socks5MitmHandshakeOutcome { + /// Flow is not supported, skip traffic inspection and + /// resort to proxying bytes... + UnsupportedFlow, + /// Socks5 handshake complete, continue to inspect. + /// In case there were credentials negotiated in the flow, + /// they will also have been inserted in the input flow via + /// [`DpiProxyCredential`] in its extensions. + ContinueInspection, +} + +impl Socks5MitmRelay { + #[inline(always)] + /// Create a new [`Socks5MitmRelay`]. + pub fn new() -> Self { + Self { + dns: GlobalDnsResolver::new(), + tcp_connector: (), + connect_timeout: Duration::from_mins(2), + } + } +} + +impl Default for Socks5MitmRelay { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + +impl Socks5MitmRelay { + #[inline(always)] + /// Set the TCP connector to use + pub fn tcp_connector(self, connector: Connector) -> Socks5MitmRelay { + Socks5MitmRelay { + dns: self.dns, + tcp_connector: connector, + connect_timeout: self.connect_timeout, + } + } +} + +impl Socks5MitmRelay { + #[inline(always)] + /// Set the Dns (address) resolver to use + pub fn dns_resolver(self, dns: Dns) -> Socks5MitmRelay { + Socks5MitmRelay { + dns, + tcp_connector: self.tcp_connector, + connect_timeout: self.connect_timeout, + } + } +} + +impl Socks5MitmRelay { + generate_set_and_with! { + /// Overwrite the connect timeout to be used for tcp (egress) tcp connections, + /// to the actual intended socks5 servers. + pub fn connect_timeout(mut self, timeout: Duration) -> Self { + self.connect_timeout = if timeout.is_zero() { + Duration::from_mins(2) + } else { + timeout + }; + self + } + } +} + +impl Socks5MitmRelay +where + Dns: DnsAddressResolver + Clone, + Connector: TcpStreamConnector + Send + 'static> + Clone, +{ + /// Establish and MITM an handshake between the client and server. + pub async fn handshake( + &self, + ingress_stream: &mut S, + exec: Executor, + socks5_proxy_address: HostWithPort, + ) -> Result<(TcpStream, Socks5MitmHandshakeOutcome), BoxError> + where + S: Io + Unpin + extensions::ExtensionsMut, + { + let (mut egress_stream, _) = tokio::time::timeout( + self.connect_timeout, + tcp_connect( + ingress_stream.extensions(), + socks5_proxy_address, + self.dns.clone(), + self.tcp_connector.clone(), + exec, + ), + ) + .await + .context("tcp connection to egress socks5 proxy server timed out")? + .context("tcp connection to egress socks5 proxy server failed")?; + + let outcome = socks5_mitm_relay_handshake(ingress_stream, &mut egress_stream).await?; + Ok((egress_stream, outcome)) + } +} + +pub async fn socks5_mitm_relay_handshake( + ingress_stream: &mut Ingress, + egress_stream: &mut Egress, +) -> Result +where + Ingress: Io + Unpin + extensions::ExtensionsMut, + Egress: Io + Unpin + extensions::ExtensionsMut, +{ + let client_header = proto::client::Header::read_from(ingress_stream) + .await + .context("read client header")?; + + client_header + .write_to(egress_stream) + .await + .context("write client header: with ingress provided method")?; + + let server_header = proto::server::Header::read_from(egress_stream) + .await + .context("read egress socks5 proxy server header")?; + + server_header + .write_to(ingress_stream) + .await + .context("write server header: received from egress stream")?; + + match server_header.method { + proto::SocksMethod::NoAuthenticationRequired => { + proxy_socks5_handshake_request_response( + ingress_stream, + egress_stream, + server_header.method, + ) + .await + } + proto::SocksMethod::UsernamePassword => { + let client_auth_req = proto::client::UsernamePasswordRequest::read_from(ingress_stream) + .await + .context( + "read client auth sub-negotiation request from ingress: username-password", + )?; + + client_auth_req.write_to(egress_stream).await.context( + "write client auth-sub-negotation request to egress: received from egress stream", + )?; + + let server_auth_reply = + proto::server::UsernamePasswordResponse::read_from(egress_stream) + .await + .context( + "read server sub-negotiation response from egress: username-password auth", + )?; + + server_auth_reply.write_to(ingress_stream).await.context( + "write server auth-sub-negotation response to ingress: received from egress stream", + )?; + + if !server_auth_reply.success() { + // continue regular flow even if not succesfull as it is up to the + // conversing pair to decide when to stop, not the proxy... if client + // and server continue regardless of socks5 semantics, we should support that + tracing::debug!( + "server auth flow did not succeed: attempt to continue socks5 proxy relay flow regardless..." + ); + } + + ingress_stream + .extensions_mut() + .insert(DpiProxyCredential(ProxyCredential::Basic( + client_auth_req.basic, + ))); + + proxy_socks5_handshake_request_response( + ingress_stream, + egress_stream, + server_header.method, + ) + .await + } + method @ (proto::SocksMethod::GSSAPI + | proto::SocksMethod::ChallengeHandshakeAuthenticationProtocol + | proto::SocksMethod::ChallengeResponseAuthenticationMethod + | proto::SocksMethod::SecureSocksLayer + | proto::SocksMethod::NDSAuthentication + | proto::SocksMethod::MultiAuthenticationFramework + | proto::SocksMethod::JSONParameterBlock + | proto::SocksMethod::NoAcceptableMethods + | proto::SocksMethod::Unknown(_)) => { + tracing::debug!( + "supported SOCKS5 method {method:?}: forward bytes as is without further inspection..." + ); + + Ok(Socks5MitmHandshakeOutcome::UnsupportedFlow) + } + } +} + +async fn proxy_socks5_handshake_request_response( + ingress_stream: &mut Ingress, + egress_stream: &mut Egress, + negotiated_method: proto::SocksMethod, +) -> Result +where + Ingress: Io + Unpin + extensions::ExtensionsMut, + Egress: Io + Unpin + extensions::ExtensionsMut, +{ + let client_request = proto::client::Request::read_from(ingress_stream) + .await + .context("read client Socks5 request from ingress stream")?; + + tracing::trace!( + "socks5 client request w/ destination {} and negotiated method {:?}: client request received cmd {:?} from ingress stream", + client_request.destination, + negotiated_method, + client_request.command, + ); + + client_request + .write_to(egress_stream) + .await + .context("write client request: with ingress provided data")?; + + match client_request.command { + proto::Command::Connect => { + let server_reply = proto::server::Reply::read_from(egress_stream) + .await + .context("read server socks5 reply from egress stream")?; + + server_reply + .write_to(ingress_stream) + .await + .context("write server reply to ingress: received from egress stream")?; + + if server_reply.reply != proto::ReplyKind::Succeeded { + // continue regular flow even if not succesfull as it is up to the + // conversing pair to decide when to stop, not the proxy... if client + // and server continue regardless of socks5 semantics, we should support that + tracing::debug!( + "server req-resp flow did not succeed: attempt to continue socks5 proxy relay flow regardless..." + ); + } + + tracing::trace!( + bind_addr = %server_reply.bind_address, + "socks5 proxy relay connector: handshake (socks5_client <-> proxy <-> socks5_server) complete", + ); + + Ok(Socks5MitmHandshakeOutcome::ContinueInspection) + } + cmd + @ (proto::Command::Bind | proto::Command::UdpAssociate | proto::Command::Unknown(_)) => { + // Note that except for the unknown cmd, + // this unsupported flow for Bind and Udp-Associate is fine, + // given both are anyway about new sidechannel flows, which can + // be intercepted by the transparent proxy separately just fine without requiring + // further support here. + // + // It is only for the Connect flow that we need to actually relay in-place as there's + // no sidechannel for the data... the stream for the socks5 handshake is in that case + // also the data channel... + tracing::debug!( + "unsupported SOCKS5 method {cmd:?}: forward bytes as is without further inspection..." + ); + + Ok(Socks5MitmHandshakeOutcome::UnsupportedFlow) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use parking_lot::Mutex; + use rama_core::{ServiceInput, extensions::ExtensionsRef as _}; + use rama_net::{ + address::{Domain, Host}, + user::credentials::Basic, + }; + use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, + time::Duration, + }; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + + #[derive(Debug, Clone, Default)] + struct RecordingTcpConnector { + seen: Arc>>, + } + + impl RecordingTcpConnector { + fn seen_addrs(&self) -> Vec { + self.seen.lock().clone() + } + } + + impl TcpStreamConnector for RecordingTcpConnector { + type Error = std::io::Error; + + async fn connect(&self, addr: SocketAddr) -> Result { + self.seen.lock().push(addr); + Err(std::io::Error::other( + "recording connector denies connection", + )) + } + } + + fn new_socks_proxy_address(port: u16) -> HostWithPort { + HostWithPort::new(Host::Name(Domain::from_static("socks5.relay.test")), port) + } + + #[tokio::test] + async fn test_mitm_relay_handshake_uses_static_dns_and_custom_connector() { + // Egress connect fails before any socks5 bytes are consumed from ingress. + let mut ingress_stream = ServiceInput::new(tokio_test::io::Builder::new().build()); + + let connector = RecordingTcpConnector::default(); + let relay = Socks5MitmRelay::new() + .dns_resolver(Ipv4Addr::new(203, 0, 113, 10)) + .with_connect_timeout(Duration::from_millis(20)) + .tcp_connector(connector.clone()); + + let outcome = tokio::time::timeout( + Duration::from_millis(100), + relay.handshake( + &mut ingress_stream, + Executor::default(), + new_socks_proxy_address(1080), + ), + ) + .await; + assert!( + matches!(outcome, Ok(Err(_)) | Err(_)), + "connect should not succeed in in-memory connector test", + ); + + let seen = connector.seen_addrs(); + assert_eq!(seen.len(), 1); + assert_eq!( + seen[0], + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 10)), 1080) + ); + } + + #[tokio::test] + async fn test_proxy_method_and_request_connect_no_auth_continue_inspection() { + let mut ingress_stream = ServiceInput::new( + tokio_test::io::Builder::new() + .read(b"\x05\x01\x00") + .write(b"\x05\x00") + .read(b"\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb") + .write(b"\x05\x00\x00\x01\x7f\x00\x00\x01\x19\x64") + .build(), + ); + + let mut egress_stream = ServiceInput::new( + tokio_test::io::Builder::new() + .write(b"\x05\x01\x00") + .read(b"\x05\x00") + .write(b"\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb") + .read(b"\x05\x00\x00\x01\x7f\x00\x00\x01\x19\x64") + .build(), + ); + + let outcome = socks5_mitm_relay_handshake(&mut ingress_stream, &mut egress_stream) + .await + .expect("negotiate socks5 connect"); + assert_eq!(outcome, Socks5MitmHandshakeOutcome::ContinueInspection); + } + + #[tokio::test] + async fn test_proxy_connect_flow_supports_post_handshake_data_relay() { + let (ingress_proxy, mut ingress_client) = tokio::io::duplex(1024); + let (egress_proxy, mut egress_server) = tokio::io::duplex(1024); + + let mut ingress_stream = ServiceInput::new(ingress_proxy); + let mut egress_stream = ServiceInput::new(egress_proxy); + + let client_task = tokio::spawn(async move { + ingress_client + .write_all(b"\x05\x01\x00") + .await + .expect("client write socks header"); + let mut server_method = [0u8; 2]; + ingress_client + .read_exact(&mut server_method) + .await + .expect("client read server method"); + assert_eq!(&server_method, b"\x05\x00"); + + ingress_client + .write_all(b"\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb") + .await + .expect("client write connect request"); + let mut server_reply = [0u8; 10]; + ingress_client + .read_exact(&mut server_reply) + .await + .expect("client read connect reply"); + assert_eq!(&server_reply, b"\x05\x00\x00\x01\x7f\x00\x00\x01\x19\x64"); + + ingress_client + .write_all(b"PING") + .await + .expect("client write application data"); + let mut app_reply = [0u8; 4]; + ingress_client + .read_exact(&mut app_reply) + .await + .expect("client read application reply"); + assert_eq!(&app_reply, b"PONG"); + }); + + let server_task = tokio::spawn(async move { + let mut client_header = [0u8; 3]; + egress_server + .read_exact(&mut client_header) + .await + .expect("server read client header"); + assert_eq!(&client_header, b"\x05\x01\x00"); + egress_server + .write_all(b"\x05\x00") + .await + .expect("server write selected method"); + + let mut connect_request = [0u8; 10]; + egress_server + .read_exact(&mut connect_request) + .await + .expect("server read connect request"); + assert_eq!( + &connect_request, + b"\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb" + ); + egress_server + .write_all(b"\x05\x00\x00\x01\x7f\x00\x00\x01\x19\x64") + .await + .expect("server write connect reply"); + + let mut app_data = [0u8; 4]; + egress_server + .read_exact(&mut app_data) + .await + .expect("server read application data"); + assert_eq!(&app_data, b"PING"); + egress_server + .write_all(b"PONG") + .await + .expect("server write application reply"); + }); + + let outcome = socks5_mitm_relay_handshake(&mut ingress_stream, &mut egress_stream) + .await + .expect("negotiate socks5 connect"); + assert_eq!(outcome, Socks5MitmHandshakeOutcome::ContinueInspection); + + tokio::io::copy_bidirectional(&mut ingress_stream, &mut egress_stream) + .await + .expect("post-handshake relay bytes"); + + client_task.await.expect("client task"); + server_task.await.expect("server task"); + } + + #[tokio::test] + async fn test_proxy_method_and_request_connect_auth_sets_proxy_credential_extension() { + let mut ingress_stream = ServiceInput::new( + tokio_test::io::Builder::new() + .read(b"\x05\x01\x02") + .write(b"\x05\x02") + .read(b"\x01\x04john\x06secret") + .write(b"\x01\x00") + .read(b"\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb") + .write(b"\x05\x00\x00\x01\x7f\x00\x00\x01\x19\x64") + .build(), + ); + + let mut egress_stream = ServiceInput::new( + tokio_test::io::Builder::new() + .write(b"\x05\x01\x02") + .read(b"\x05\x02") + .write(b"\x01\x04john\x06secret") + .read(b"\x01\x00") + .write(b"\x05\x01\x00\x01\x01\x02\x03\x04\x01\xbb") + .read(b"\x05\x00\x00\x01\x7f\x00\x00\x01\x19\x64") + .build(), + ); + + let outcome = socks5_mitm_relay_handshake(&mut ingress_stream, &mut egress_stream) + .await + .expect("negotiate socks5 connect with auth"); + assert_eq!(outcome, Socks5MitmHandshakeOutcome::ContinueInspection); + + let credential = ingress_stream + .extensions() + .get::() + .expect("DPI proxy credential extension"); + assert_eq!( + credential.0, + ProxyCredential::Basic(Basic::new( + "john".try_into().expect("non-empty username"), + "secret".try_into().expect("non-empty password"), + )) + ); + } + + #[tokio::test] + async fn test_proxy_method_and_request_bind_returns_unsupported_and_keeps_stream() { + assert_unsupported_flow_roundtrip( + b"\x05\x00", + b"\x05\x02\x00\x01\x00\x00\x00\x00\x00\x00", + [0x05, 0x02, 0x00, 0x01, 0, 0, 0, 0, 0, 0], + ) + .await; + } + + #[tokio::test] + async fn test_proxy_method_and_request_udp_associate_returns_unsupported_and_keeps_stream() { + assert_unsupported_flow_roundtrip( + b"\x05\x00", + b"\x05\x03\x00\x01\x00\x00\x00\x00\x00\x00", + [0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0], + ) + .await; + } + + async fn assert_unsupported_flow_roundtrip( + server_header: &[u8], + client_request: &[u8], + expected_request: [u8; 10], + ) { + let mut ingress_stream = ServiceInput::new( + tokio_test::io::Builder::new() + .read(b"\x05\x01\x00") + .write(server_header) + .read(client_request) + .build(), + ); + + let mut egress_stream = ServiceInput::new( + tokio_test::io::Builder::new() + .write(b"\x05\x01\x00") + .read(server_header) + .write(client_request) + .build(), + ); + + let outcome = socks5_mitm_relay_handshake(&mut ingress_stream, &mut egress_stream) + .await + .expect("negotiate unsupported command"); + assert_eq!(outcome, Socks5MitmHandshakeOutcome::UnsupportedFlow); + + assert_eq!( + expected_request, + [ + 0x05, + client_request[1], + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00 + ] + ); + } +} diff --git a/rama-socks5/src/proxy/mitm/service.rs b/rama-socks5/src/proxy/mitm/service.rs new file mode 100644 index 000000000..f541ea80b --- /dev/null +++ b/rama-socks5/src/proxy/mitm/service.rs @@ -0,0 +1,78 @@ +use rama_core::{ + Service, + error::{BoxError, ErrorContext as _}, + extensions, + io::{BridgeIo, Io}, +}; +use rama_net::proxy::IoForwardService; + +use super::Socks5MitmHandshakeOutcome; + +#[derive(Debug, Clone)] +/// A service that can be used by MITM services such as transparent proxies, +/// in order to relay a socks5 proxy connection between a client and server, +/// as part of a deep protocol inspection protocol (DPI) flow. +pub struct Socks5MitmRelayService { + dpi_svc: I, + fallback_svc: F, +} + +impl Socks5MitmRelayService { + /// Create a new [`Socks5MitmRelayService`] using the given + /// provided inspector servicew to continue the DPI of (socks5) handshaked traffic with a + /// [`Socks5MitmHandshakeOutcome::ContinueInspection`] outcome. + /// + /// Use [`Self::with_fallback`] to define a custom [`Service`] + /// if you wish behaviour for unsupported flows other than + /// mindlessly proxying bytes using [`IoForwardService`] (the default). + pub fn new(dpi_svc: I) -> Self { + Self { + dpi_svc, + fallback_svc: IoForwardService::new(), + } + } + + /// Attach a fallback [`Service`] to this [`Socks5MitmRelayService`]. + /// + /// Used in case the handshaked resulted in a + /// [`Socks5MitmHandshakeOutcome::UnsupportedFlow`] outcome, + /// e.g. because the method or command was not compatible with DPI (or desired). + pub fn with_fallback(self, fallback_svc: F) -> Socks5MitmRelayService { + Socks5MitmRelayService { + dpi_svc: self.dpi_svc, + fallback_svc, + } + } +} + +impl Service> for Socks5MitmRelayService +where + I: Service, Output = (), Error: Into>, + F: Service, Output = (), Error: Into>, + Ingress: Io + Unpin + extensions::ExtensionsMut, + Egress: Io + Unpin + extensions::ExtensionsMut, +{ + type Output = (); + type Error = BoxError; + + async fn serve( + &self, + BridgeIo(mut ingress_stream, mut egress_stream): BridgeIo, + ) -> Result { + let outcome = super::socks5_mitm_relay_handshake(&mut ingress_stream, &mut egress_stream) + .await + .context("socks5 relay handshake using provided I/O bridge")?; + match outcome { + Socks5MitmHandshakeOutcome::ContinueInspection => self + .dpi_svc + .serve(BridgeIo(ingress_stream, egress_stream)) + .await + .context("serve socks5 handshake-relayed bridge I/O using DPI svc"), + Socks5MitmHandshakeOutcome::UnsupportedFlow => self + .fallback_svc + .serve(BridgeIo(ingress_stream, egress_stream)) + .await + .context("serve socks5 handshake-relayed bridge I/O using fallback svc"), + } + } +} diff --git a/rama-socks5/src/proxy/mod.rs b/rama-socks5/src/proxy/mod.rs new file mode 100644 index 000000000..ee6ce246e --- /dev/null +++ b/rama-socks5/src/proxy/mod.rs @@ -0,0 +1 @@ +pub mod mitm; diff --git a/rama-socks5/src/server/bind.rs b/rama-socks5/src/server/bind.rs index decb73b4c..d6ddd09bc 100644 --- a/rama-socks5/src/server/bind.rs +++ b/rama-socks5/src/server/bind.rs @@ -1,13 +1,14 @@ use std::{io, time::Duration}; +use rama_core::io::BridgeIo; use rama_core::rt::Executor; use rama_core::telemetry::tracing::{self, Instrument}; -use rama_core::{Service, error::BoxError, layer::timeout::DefaultTimeout, stream::Stream}; +use rama_core::{Service, error::BoxError, io::Io, layer::timeout::DefaultTimeout}; use rama_net::address::HostWithPort; use rama_net::{ address::{Host, SocketAddress}, - proxy::{ProxyRequest, StreamForwardService}, - socket::{Interface, SocketService}, + proxy::IoForwardService, + socket::SocketService, }; use rama_tcp::{TcpStream, server::TcpListener}; use rama_utils::macros::generate_set_and_with; @@ -39,7 +40,7 @@ pub trait Socks5BinderSeal: Send + Sync + 'static { impl Socks5BinderSeal for () where - S: Stream + Unpin, + S: Io + Unpin, { async fn accept_bind(&self, mut stream: S, destination: HostWithPort) -> Result<(), Error> { tracing::debug!( @@ -59,7 +60,7 @@ where } /// Default [`Binder`] type. -pub type DefaultBinder = Binder, StreamForwardService>; +pub type DefaultBinder = Binder, IoForwardService>; /// Only "useful" public [`Socks5Binder`] implementation, /// which actually is able to accept bind requests and process them. @@ -76,7 +77,7 @@ pub struct Binder { acceptor: A, service: S, - bind_interface: Option, + bind_address: Option, accept_timeout: Option, } @@ -91,7 +92,7 @@ impl Binder { Self { acceptor, service, - bind_interface: None, + bind_address: None, accept_timeout: None, } } @@ -105,46 +106,46 @@ impl Binder { Binder { acceptor, service: self.service, - bind_interface: self.bind_interface, + bind_address: self.bind_address, accept_timeout: self.accept_timeout, } } /// Overwrite the [`Binder`]'s [`Service`] - /// used to actually do the proxy between the source and incoming bind [`Stream`]. + /// used to actually do the proxy between the source and incoming bind [`Io`]. /// /// Any [`Service`] can be used as long as it has the signature: /// /// ```plain - /// (ProxyRequest) -> ((), Into) + /// (BridgeIo) -> ((), Into) /// ``` pub fn with_service(self, service: T) -> Binder { Binder { acceptor: self.acceptor, service, - bind_interface: self.bind_interface, + bind_address: self.bind_address, accept_timeout: self.accept_timeout, } } generate_set_and_with! { - /// Define the (network) [`Interface`] to bind to. + /// Define the [`SocketAddress`] to bind to. /// /// By default it will use the client's requested bind address, /// which is in many cases not what you want. - pub fn bind_interface(mut self, interface: impl Into) -> Self { - self.bind_interface = Some(interface.into()); + pub fn bind_address(mut self, addr: impl Into) -> Self { + self.bind_address = Some(addr.into()); self } } generate_set_and_with! { - /// Define the default (network) [`Interface`] to bind to (`0.0.0.0:0`). + /// Define the default [`SocketAddress`] to bind to (`0.0.0.0:0`). /// /// By default it will use the client's requested bind address, /// which is in many cases not what you want. - pub fn default_bind_interface(mut self) -> Self { - self.bind_interface = Some(SocketAddress::default_ipv4(0).into()); + pub fn default_bind_address(mut self) -> Self { + self.bind_address = Some(SocketAddress::default_ipv4(0)); self } } @@ -164,20 +165,20 @@ pub struct DefaultAcceptorFactory { exec: Executor, } -impl Service for DefaultAcceptorFactory { +impl Service for DefaultAcceptorFactory { type Output = TcpListener; type Error = BoxError; - async fn serve(&self, interface: Interface) -> Result { - let acceptor = TcpListener::bind(interface, self.exec.clone()).await?; + async fn serve(&self, addr: SocketAddress) -> Result { + let acceptor = TcpListener::bind_address(addr, self.exec.clone()).await?; Ok(acceptor) } } /// [`Acceptor`] created by an factory [`Service`] in function of a bind [`Service`]. pub trait Acceptor: Send + Sync + 'static { - /// The [`Stream`] returned by this [`Acceptor`]. - type Stream: Stream; + /// The [`Io`] returned by this [`Acceptor`]. + type Stream: Io; /// Returns the local address that this listener is bound to. fn local_addr(&self) -> io::Result; @@ -209,24 +210,21 @@ impl Default for DefaultBinder { fn default() -> Self { Self::new( DefaultTimeout::new(DefaultAcceptorFactory::default(), Duration::from_secs(30)), - StreamForwardService::default(), + IoForwardService::default(), ) } } impl Socks5BinderSeal for Binder where - S: Stream + Unpin, + S: Io + Unpin, F: SocketService>, - StreamService: Service< - ProxyRequest::Stream>, - Output = (), - Error: Into, - >, + StreamService: + Service::Stream>, Output = (), Error: Into>, { async fn accept_bind( &self, - mut stream: S, + mut ingress_stream: S, requested_bind_address: HostWithPort, ) -> Result<(), Error> { tracing::trace!("socks5 server: bind: try to create acceptor @ {requested_bind_address}"); @@ -241,7 +239,7 @@ where tracing::debug!("bind command does not accept domain {domain} as bind address",); let reply_kind = ReplyKind::AddressTypeNotSupported; Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: bind failed") @@ -250,28 +248,28 @@ where } Host::Address(ip_addr) => ip_addr, }; - let requested_interface = SocketAddress::new(requested_addr, requested_port); + let requested_address = SocketAddress::new(requested_addr, requested_port); - let bind_interface = if let Some(bind_interface) = self.bind_interface.clone() { + let bind_address = if let Some(bind_address) = self.bind_address { tracing::trace!( - "socks5 server: bind: use server-defined bind interface: {bind_interface}" + "socks5 server: bind: use server-defined bind interface: {bind_address}" ); - bind_interface + bind_address } else { tracing::debug!( - "socks5 server: bind: no server-defined bind interface: use requested client interface @ {requested_interface}" + "socks5 server: bind: no server-defined bind interface: use requested client interface @ {requested_address}" ); - requested_interface.into() + requested_address }; - let acceptor = match self.acceptor.bind(bind_interface.clone()).await { + let acceptor = match self.acceptor.bind_socket_with_address(bind_address).await { Ok(twin) => twin, Err(err) => { let err = err.into(); tracing::debug!("make bind listener failed: {err:?}"); let reply_kind = ReplyKind::GeneralServerFailure; Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: make bind listener failed") @@ -286,11 +284,11 @@ where Ok(addr) => addr, Err(err) => { tracing::debug!( - "retrieve local addr of (tcp) acceptor failed @ {bind_interface}: {err:?}", + "retrieve local addr of (tcp) acceptor failed @ {bind_address}: {err:?}", ); let reply_kind = ReplyKind::GeneralServerFailure; Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: make bind listener failed") @@ -300,7 +298,7 @@ where }; Reply::new(bind_address) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: bind: acceptor listener ready") @@ -312,10 +310,10 @@ where Some(duration) => match tokio::time::timeout(duration, accept_future).await { Ok(result) => result, Err(err) => { - tracing::debug!("accept future timed out @ {bind_interface}: {err:?}",); + tracing::debug!("accept future timed out @ {bind_address}: {err:?}",); let reply_kind = ReplyKind::TtlExpired; Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: bind failed") @@ -326,15 +324,15 @@ where None => accept_future.await, }; - let (target, incoming_addr) = match result { + let (incoming_stream, incoming_addr) = match result { Ok((stream, addr)) => (stream, addr), Err(err) => { let err: BoxError = err.into(); - tracing::debug!("socks5 server: abort: bind failed @ {bind_interface}: {err:?}",); + tracing::debug!("socks5 server: abort: bind failed @ {bind_address}: {err:?}",); let reply_kind = (&err).into(); Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: bind failed") @@ -346,25 +344,22 @@ where }; tracing::trace!( - "incoming connection {incoming_addr} received on bind interface {bind_interface}", + "incoming connection {incoming_addr} received on bind interface {bind_address}", ); Reply::new(incoming_addr) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: bind: connection received") })?; tracing::trace!( - "socks5 server @ {bind_interface}: bind: ready to serve from {incoming_addr}", + "socks5 server @ {bind_address}: bind: ready to serve from {incoming_addr}", ); self.service - .serve(ProxyRequest { - source: stream, - target, - }) + .serve(BridgeIo(ingress_stream, incoming_stream)) .instrument(tracing::trace_span!("socks5::bind::serve")) .await .map_err(|err| Error::service(err).with_context("serve bind pipe")) @@ -454,7 +449,7 @@ mod test { impl Socks5BinderSeal for MockBinder where - S: Stream + Unpin, + S: Io + Unpin, { async fn accept_bind( &self, diff --git a/rama-socks5/src/server/connect.rs b/rama-socks5/src/server/connect.rs index b28bc9f07..be59757eb 100644 --- a/rama-socks5/src/server/connect.rs +++ b/rama-socks5/src/server/connect.rs @@ -1,18 +1,17 @@ use rama_core::extensions::ExtensionsMut; +use rama_core::io::BridgeIo; use rama_core::rt::Executor; use rama_core::telemetry::tracing::{self, Instrument, trace_span}; -use rama_core::{Service, error::BoxError, stream::Stream}; +use rama_core::{Service, error::BoxError, io::Io}; use rama_net::address::HostWithPort; use rama_net::client::ConnectorService; use rama_net::{ client::EstablishedClientConnection, - proxy::{ProxyRequest, ProxyTarget, StreamForwardService}, + proxy::{IoForwardService, ProxyTarget}, stream::Socket, }; -use rama_tcp::client::{ - Request as TcpRequest, - service::{DefaultForwarder, TcpConnector}, -}; +use rama_tcp::client::{Request as TcpRequest, service::TcpConnector}; +use rama_tcp::proxy::IoToProxyBridgeIo; use rama_utils::macros::generate_set_and_with; use std::time::Duration; @@ -46,7 +45,7 @@ pub trait Socks5ConnectorSeal: Send + Sync + 'static { impl Socks5ConnectorSeal for () where - S: Stream + Unpin, + S: Io + Unpin, { async fn accept_connect(&self, mut stream: S, destination: HostWithPort) -> Result<(), Error> { tracing::trace!( @@ -65,14 +64,14 @@ where } /// Default [`Connector`] type. -pub type DefaultConnector = Connector; +pub type DefaultConnector = Connector; /// Proxy Forward [`Socks5Connector`] implementation, /// which actually is able to accept connect requests and process them. /// /// The [`Default`] implementation establishes a connection for the requested -/// destination [`HostWithPort`] and pipes the incoming [`Stream`] with the established -/// outgoing [`Stream`] by copying the bytes without doing anyting else with them. +/// destination [`HostWithPort`] and pipes the incoming [`Io`] with the established +/// outgoing [`Io`] by copying the bytes without doing anyting else with them. /// /// You can customise the [`Connector`] fully by creating it using [`Connector::new`] /// or overwrite any of the default components using either or both of [`Connector::with_connector`] @@ -135,7 +134,7 @@ impl Connector { impl Connector { /// Overwrite the [`Connector`]'s connector [`Service`] /// used to establish a Tcp connection used as the - /// [`Stream`] in the direction from target to source. + /// [`Io`] in the direction from target to source. /// /// Any [`Service`] can be used as long as it has the signature: /// @@ -153,12 +152,12 @@ impl Connector { } /// Overwrite the [`Connector`]'s [`Service`] - /// used to actually do the proxy between the source and target [`Stream`]. + /// used to actually do the proxy between the source and target [`Io`]. /// /// Any [`Service`] can be used as long as it has the signature: /// /// ```plain - /// (ProxyRequest) -> ((), Into) + /// (BridgeIo) -> ((), Into) /// ``` pub fn with_service(self, service: T) -> Connector { Connector { @@ -174,7 +173,7 @@ impl Default for DefaultConnector { fn default() -> Self { Self { connector: TcpConnector::default(), - service: StreamForwardService::default(), + service: IoForwardService::default(), hide_local_address: false, connect_timeout: Some(Duration::from_secs(60)), } @@ -184,22 +183,24 @@ impl Default for DefaultConnector { impl Socks5ConnectorSeal for Connector where - S: Stream + Unpin + ExtensionsMut, - InnerConnector: ConnectorService, + S: Io + Unpin + ExtensionsMut, + InnerConnector: ConnectorService, StreamService: - Service, Output = (), Error: Into>, + Service, Output = (), Error: Into>, { - async fn accept_connect(&self, mut stream: S, destination: HostWithPort) -> Result<(), Error> { + async fn accept_connect( + &self, + mut ingress_stream: S, + destination: HostWithPort, + ) -> Result<(), Error> { tracing::trace!( "socks5 server w/ destination {destination}: connect: try to establish connection", ); - // TODO: replace with timeout layer once possible - - // Clone so we also have them on stream still + // Clone so we also have them on (ingress) stream still let connect_future = self.connector.connect(TcpRequest::new_with_extensions( destination.clone(), - stream.extensions().clone(), + ingress_stream.extensions().clone(), )); let result = match self.connect_timeout { @@ -209,7 +210,7 @@ where tracing::debug!("connect future timed out: {err:?}",); let reply_kind = ReplyKind::TtlExpired; Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: connect failed") @@ -220,7 +221,10 @@ where None => connect_future.await, }; - let EstablishedClientConnection { conn: target, .. } = match result { + let EstablishedClientConnection { + conn: egress_stream, + .. + } = match result { Ok(ecs) => ecs, Err(err) => { let err: BoxError = err.into(); @@ -230,7 +234,7 @@ where let reply_kind = (&err).into(); Reply::error_reply(reply_kind) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| { Error::io(err).with_context("write server reply: connect failed") @@ -241,7 +245,7 @@ where } }; - let local_addr = target + let egress_addr_local = egress_stream .local_addr() .map(Into::into) .inspect_err(|err| { @@ -250,30 +254,27 @@ where ); }) .unwrap_or(HostWithPort::default_ipv4(0)); - let peer_addr = target.peer_addr(); + let egress_addr = egress_stream.peer_addr(); tracing::trace!( - "socks5 server w/ destination {destination}: connect: connection established, serve pipe: {local_addr} <-> {peer_addr:?}", + "socks5 server w/ destination {destination}: connect: connection established, serve pipe: {egress_addr_local} <-> {egress_addr:?}", ); Reply::new(if self.hide_local_address { HostWithPort::default_ipv4(0) } else { - local_addr.clone() + egress_addr_local.clone() }) - .write_to(&mut stream) + .write_to(&mut ingress_stream) .await .map_err(|err| Error::io(err).with_context("write server reply: connect succeeded"))?; tracing::trace!( - "socks5 server w/ destination {destination}: connect: reply sent, start serving source-target pipe: {local_addr} <-> {peer_addr:?}", + "socks5 server w/ destination {destination}: connect: reply sent, start serving source-target pipe: {egress_addr_local} <-> {egress_addr:?}", ); self.service - .serve(ProxyRequest { - source: stream, - target, - }) + .serve(BridgeIo(ingress_stream, egress_stream)) .instrument(trace_span!("socks5::connect::proxy::serve")) .await .map_err(|err| Error::service(err).with_context("serve connect pipe")) @@ -308,17 +309,24 @@ impl LazyConnector { } } -impl Default for LazyConnector { - fn default() -> Self { +impl LazyConnector> { + fn default_with_exec(exec: Executor) -> Self { Self { - service: DefaultForwarder::ctx(Executor::default()), + service: IoToProxyBridgeIo::extension_proxy_target(exec, IoForwardService::new()), } } } +impl Default for LazyConnector> { + #[inline(always)] + fn default() -> Self { + Self::default_with_exec(Executor::default()) + } +} + impl Socks5ConnectorSeal for LazyConnector where - S: Stream + Unpin + ExtensionsMut, + S: Io + Unpin + ExtensionsMut, StreamService: Service>, { async fn accept_connect(&self, mut stream: S, destination: HostWithPort) -> Result<(), Error> { @@ -398,7 +406,7 @@ mod test { impl Socks5ConnectorSeal for MockConnector where - S: Stream + Unpin, + S: Io + Unpin, { async fn accept_connect( &self, diff --git a/rama-socks5/src/server/mod.rs b/rama-socks5/src/server/mod.rs index fed5cb218..5b2a2f7fc 100644 --- a/rama-socks5/src/server/mod.rs +++ b/rama-socks5/src/server/mod.rs @@ -17,12 +17,12 @@ use rama_core::{ Service, error::BoxError, extensions::{Extensions, ExtensionsMut}, + io::Io, rt::Executor, - stream::Stream, telemetry::tracing, }; use rama_net::{ - socket::Interface, + address::SocketAddress, user::{self, authority::Authorizer}, }; use rama_tcp::{TcpStream, server::TcpListener}; @@ -30,7 +30,7 @@ use std::{fmt, sync::Arc}; mod peek; #[doc(inline)] -pub use peek::{NoSocks5RejectError, Socks5PeekRouter, Socks5PeekStream}; +pub use peek::{NoSocks5RejectError, Socks5PeekRouter, Socks5PrefixedIo}; mod connect; pub use connect::{Connector, DefaultConnector, LazyConnector, Socks5Connector}; @@ -342,7 +342,7 @@ impl Socks5Acceptor { U: Socks5UdpAssociator, A: Authorizer, B: Socks5Binder, - S: Stream + Unpin + ExtensionsMut, + S: Io + Unpin + ExtensionsMut, { let client_header = client::Header::read_from(&mut stream) .await @@ -412,7 +412,7 @@ impl Socks5Acceptor { } impl> Socks5Acceptor { - async fn handle_method( + async fn handle_method( &self, methods: &[SocksMethod], stream: &mut S, @@ -524,7 +524,7 @@ where U: Socks5UdpAssociator, A: Authorizer, B: Socks5Binder, - S: Stream + Unpin + ExtensionsMut, + S: Io + Unpin + ExtensionsMut, { type Output = (); type Error = Error; @@ -545,14 +545,14 @@ where A: Authorizer, B: Socks5Binder, { - /// Listen for connections on the given [`Interface`], serving Socks5(h) connections. + /// Listen for connections on the given [`SocketAddress`], serving Socks5(h) connections. /// /// It's a shortcut in case you don't need to operate on the transport layer directly. - pub async fn listen(self, interface: I) -> Result<(), BoxError> + pub async fn listen
(self, address: Address) -> Result<(), BoxError> where - I: TryInto>, + Address: TryInto>, { - let tcp = TcpListener::bind(interface, self.exec.clone()).await?; + let tcp = TcpListener::bind_address(address, self.exec.clone()).await?; tcp.serve(Arc::new(self)).await; Ok(()) } diff --git a/rama-socks5/src/server/peek.rs b/rama-socks5/src/server/peek.rs index 243785a77..d77d86b41 100644 --- a/rama-socks5/src/server/peek.rs +++ b/rama-socks5/src/server/peek.rs @@ -1,9 +1,8 @@ use rama_core::{ Service, error::{BoxError, ErrorContext}, - extensions::ExtensionsMut, + io::{PeekIoProvider, PrefixedIo, StackReader}, service::RejectService, - stream::{PeekStream, StackReader}, telemetry::tracing, }; use tokio::io::AsyncReadExt; @@ -47,19 +46,29 @@ impl Socks5PeekRouter { } } -impl Service for Socks5PeekRouter +impl Service for Socks5PeekRouter where - Stream: rama_core::stream::Stream + Unpin + ExtensionsMut, + PeekableInput: PeekIoProvider, Output: Send + 'static, - T: Service, Output = Output, Error: Into>, - F: Service, Output = Output, Error: Into>, + T: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, + F: Service< + PeekableInput::Mapped>, + Output = Output, + Error: Into, + >, { type Output = Output; type Error = BoxError; - async fn serve(&self, mut stream: Stream) -> Result { + async fn serve(&self, mut input: PeekableInput) -> Result { let mut peek_buf = [0u8; SOCKS5_HEADER_PEEK_LEN]; - let n = stream + let peekable_io = input.peek_io_mut(); + + let n = peekable_io .read(&mut peek_buf) .await .context("try to read socks5 prefix header")?; @@ -83,28 +92,30 @@ where let mut peek = StackReader::new(peek_buf); peek.skip(offset); - let stream = PeekStream::new(peek, stream); + let peeked_input = input.map_peek_io(|io| PrefixedIo::new(peek, io)); if is_socks5 { - self.socks5_acceptor.serve(stream).await.into_box_error() + self.socks5_acceptor + .serve(peeked_input) + .await + .into_box_error() } else { - self.fallback.serve(stream).await.into_box_error() + self.fallback.serve(peeked_input).await.into_box_error() } } } const SOCKS5_HEADER_PEEK_LEN: usize = 5; -/// [`PeekStream`] alias used by [`Socks5PeekRouter`]. -pub type Socks5PeekStream = PeekStream, S>; +/// [`PrefixedIo`] alias used by [`Socks5PeekRouter`]. +pub type Socks5PrefixedIo = PrefixedIo, S>; #[cfg(test)] mod test { - use rama_core::{ ServiceInput, + io::Io, service::{RejectError, service_fn}, - stream::Stream, }; use std::convert::Infallible; @@ -171,9 +182,7 @@ mod test { async fn test_peek_router_read_eof() { const CONTENT: &[u8] = b"\x05\x01\x00"; - async fn socks5_service_fn( - mut stream: impl Stream + Unpin, - ) -> Result<&'static str, BoxError> { + async fn socks5_service_fn(mut stream: impl Io + Unpin) -> Result<&'static str, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; assert_eq!(CONTENT, v); @@ -214,9 +223,7 @@ mod test { } let socks5_service = service_fn(socks5_service_fn); - async fn other_service_fn( - mut stream: impl Stream + Unpin, - ) -> Result, BoxError> { + async fn other_service_fn(mut stream: impl Io + Unpin) -> Result, BoxError> { let mut v = Vec::default(); let _ = stream.read_to_end(&mut v).await?; Ok(v) diff --git a/rama-socks5/src/server/udp/inspect.rs b/rama-socks5/src/server/udp/inspect.rs index 8c1b3fe4e..caf41189e 100644 --- a/rama-socks5/src/server/udp/inspect.rs +++ b/rama-socks5/src/server/udp/inspect.rs @@ -8,7 +8,6 @@ use rama_core::{Service, error::BoxError}; use rama_net::address::SocketAddress; use rama_udp::UdpSocket; -#[cfg(feature = "dns")] use ::rama_dns::client::resolver::BoxDnsAddressResolver; #[allow(clippy::too_many_arguments)] @@ -22,7 +21,7 @@ pub(super) trait UdpPacketProxy: Send + Sync + 'static { north_read_buf_size: usize, south: UdpSocket, south_read_buf_size: usize, - #[cfg(feature = "dns")] dns_resolver: Option, + dns_resolver: Option, ) -> impl Future> + Send; } @@ -34,13 +33,13 @@ pub struct DirectUdpRelay; impl UdpPacketProxy for DirectUdpRelay { async fn proxy_udp_packets( &self, - #[cfg_attr(not(feature = "dns"), expect(unused_variables))] extensions: Extensions, + extensions: Extensions, client_address: SocketAddress, north: UdpSocket, north_read_buf_size: usize, south: UdpSocket, south_read_buf_size: usize, - #[cfg(feature = "dns")] dns_resolver: Option, + dns_resolver: Option, ) -> Result<(), Error> { let relay = UdpSocketRelay::new( client_address, @@ -50,7 +49,6 @@ impl UdpPacketProxy for DirectUdpRelay { south_read_buf_size, ); - #[cfg(feature = "dns")] let relay = relay.maybe_with_dns_resolver(&extensions, dns_resolver); let mut relay = relay; @@ -147,7 +145,7 @@ where north_read_buf_size: usize, south: UdpSocket, south_read_buf_size: usize, - #[cfg(feature = "dns")] dns_resolver: Option, + dns_resolver: Option, ) -> Result<(), Error> { let relay = UdpSocketRelay::new( client_address, @@ -157,7 +155,6 @@ where south_read_buf_size, ); - #[cfg(feature = "dns")] let relay = relay.maybe_with_dns_resolver(&extensions, dns_resolver); let mut relay = relay; @@ -313,13 +310,13 @@ where { async fn proxy_udp_packets( &self, - #[cfg_attr(not(feature = "dns"), expect(unused_variables))] extensions: Extensions, + extensions: Extensions, client_address: SocketAddress, north: UdpSocket, north_read_buf_size: usize, south: UdpSocket, south_read_buf_size: usize, - #[cfg(feature = "dns")] dns_resolver: Option, + dns_resolver: Option, ) -> Result<(), Error> { let relay = UdpSocketRelay::new( client_address, @@ -329,7 +326,6 @@ where south_read_buf_size, ); - #[cfg(feature = "dns")] let relay = relay.maybe_with_dns_resolver(&extensions, dns_resolver); let mut relay = relay; diff --git a/rama-socks5/src/server/udp/mod.rs b/rama-socks5/src/server/udp/mod.rs index 96c19a55a..3a40cb503 100644 --- a/rama-socks5/src/server/udp/mod.rs +++ b/rama-socks5/src/server/udp/mod.rs @@ -1,17 +1,16 @@ use std::time::Duration; use rama_core::{ - Service, combinators::Either, error::BoxError, extensions::ExtensionsMut, - layer::timeout::DefaultTimeout, stream::Stream, telemetry::tracing, + Service, combinators::Either, error::BoxError, extensions::ExtensionsMut, io::Io, + layer::timeout::DefaultTimeout, telemetry::tracing, }; use rama_net::{ address::{Host, HostWithPort, SocketAddress}, - socket::{Interface, SocketService}, + socket::SocketService, }; -use rama_udp::{UdpSocket, bind_udp}; +use rama_udp::{UdpSocket, bind_udp_with_address}; use rama_utils::macros::generate_set_and_with; -#[cfg(feature = "dns")] use ::rama_dns::client::resolver::{BoxDnsAddressResolver, DnsAddressResolver}; use super::Error; @@ -47,12 +46,12 @@ pub trait Socks5UdpAssociatorSeal: Send + Sync + 'static { destination: HostWithPort, ) -> impl Future> + Send + '_ where - S: Stream + Unpin; + S: Io + Unpin; } impl Socks5UdpAssociatorSeal for () where - S: Stream + Unpin, + S: Io + Unpin, { async fn accept_udp_associate( &self, @@ -80,12 +79,12 @@ where /// [`Default`] binder [`Service`] implementation. pub struct DefaultUdpBinder; -impl Service for DefaultUdpBinder { +impl Service for DefaultUdpBinder { type Output = UdpSocket; type Error = BoxError; - async fn serve(&self, interface: Interface) -> Result { - let socket = bind_udp(interface).await?; + async fn serve(&self, addr: SocketAddress) -> Result { + let socket = bind_udp_with_address(addr).await?; Ok(socket) } } @@ -110,11 +109,10 @@ pub struct UdpRelay { binder: B, inspector: I, - #[cfg(feature = "dns")] dns_resolver: Option, - bind_north_interface: Interface, - bind_south_interface: Interface, + bind_north_address: SocketAddress, + bind_south_address: SocketAddress, north_buffer_size: usize, south_buffer_size: usize, @@ -128,10 +126,9 @@ impl UdpRelay { Self { binder, inspector: DirectUdpRelay::default(), - #[cfg(feature = "dns")] dns_resolver: None, - bind_north_interface: Interface::default_ipv4(0), - bind_south_interface: Interface::default_ipv4(0), + bind_north_address: SocketAddress::default_ipv4(0), + bind_south_address: SocketAddress::default_ipv4(0), north_buffer_size: 2048, south_buffer_size: 2048, relay_timeout: None, @@ -144,10 +141,9 @@ impl UdpRelay { UdpRelay { binder: self.binder, inspector: SyncUdpInspector(inspector), - #[cfg(feature = "dns")] dns_resolver: self.dns_resolver, - bind_north_interface: self.bind_north_interface, - bind_south_interface: self.bind_south_interface, + bind_north_address: self.bind_north_address, + bind_south_address: self.bind_south_address, north_buffer_size: self.north_buffer_size, south_buffer_size: self.south_buffer_size, relay_timeout: self.relay_timeout, @@ -160,10 +156,9 @@ impl UdpRelay { UdpRelay { binder: self.binder, inspector: AsyncUdpInspector(inspector), - #[cfg(feature = "dns")] dns_resolver: self.dns_resolver, - bind_north_interface: self.bind_north_interface, - bind_south_interface: self.bind_south_interface, + bind_north_address: self.bind_north_address, + bind_south_address: self.bind_south_address, north_buffer_size: self.north_buffer_size, south_buffer_size: self.south_buffer_size, relay_timeout: self.relay_timeout, @@ -179,10 +174,9 @@ impl UdpRelay { UdpRelay { binder, inspector: self.inspector, - #[cfg(feature = "dns")] dns_resolver: self.dns_resolver, - bind_north_interface: self.bind_north_interface, - bind_south_interface: self.bind_south_interface, + bind_north_address: self.bind_north_address, + bind_south_address: self.bind_south_address, north_buffer_size: self.north_buffer_size, south_buffer_size: self.south_buffer_size, relay_timeout: self.relay_timeout, @@ -190,33 +184,33 @@ impl UdpRelay { } generate_set_and_with! { - /// Define the (network) [`Interface`] to bind to, for both north and south direction. + /// Define the [`SocketAddress`] to bind to, for both north and south direction. /// /// By default it binds the udp sockets at `0.0.0.0:0`. - pub fn bind_interface(mut self, interface: impl Into) -> Self { - let interface = interface.into(); - self.bind_north_interface = interface.clone(); - self.bind_south_interface = interface; + pub fn bind_address(mut self, address: impl Into) -> Self { + let address = address.into(); + self.bind_north_address = address; + self.bind_south_address = address; self } } generate_set_and_with! { - /// Define the (network) [`Interface`] to bind to, for the north direction. + /// Define the [`SocketAddress`] to bind to, for the north direction. /// /// By default it binds the udp sockets at `0.0.0.0:0`. - pub fn bind_north_interface(mut self, interface: impl Into) -> Self { - self.bind_north_interface = interface.into(); + pub fn bind_north_address(mut self, address: impl Into) -> Self { + self.bind_north_address = address.into(); self } } generate_set_and_with! { - /// Define the (network) [`Interface`] to bind to, for the south direction. + /// Define the [`SocketAddress`] to bind to, for the south direction. /// /// By default it binds the udp sockets at `0.0.0.0:0`. - pub fn bind_south_interface(mut self, interface: impl Into) -> Self { - self.bind_south_interface = interface.into(); + pub fn bind_south_address(mut self, address: impl Into) -> Self { + self.bind_south_address = address.into(); self } } @@ -255,14 +249,12 @@ impl UdpRelay { } } -#[cfg(feature = "dns")] impl UdpRelay { generate_set_and_with! { /// Attach a the [`Default`] [`DnsResolver`] to this [`UdpRelay`]. /// /// It will be used to best-effort resolve the domain name, /// in case a domain name is passed to forward to the target server. - #[cfg_attr(docsrs, doc(cfg(feature = "dns")))] pub fn default_dns_resolver(mut self) -> Self { self.dns_resolver = None; self @@ -274,7 +266,6 @@ impl UdpRelay { /// /// It will be used to best-effort resolve the domain name, /// in case a domain name is passed to forward to the target server. - #[cfg_attr(docsrs, doc(cfg(feature = "dns")))] pub fn dns_resolver(mut self, resolver: impl DnsAddressResolver) -> Self { self.dns_resolver = Some(resolver.into_box_dns_address_resolver()); self @@ -284,16 +275,11 @@ impl UdpRelay { impl Default for DefaultUdpRelay { fn default() -> Self { - let relay = Self::new(DefaultTimeout::new( + Self::new(DefaultTimeout::new( DefaultUdpBinder::default(), Duration::from_secs(30), - )); - #[cfg(feature = "dns")] - { - relay.with_default_dns_resolver() - } - #[cfg(not(feature = "dns"))] - relay + )) + .with_default_dns_resolver() } } @@ -301,7 +287,7 @@ impl Socks5UdpAssociatorSeal for UdpRelay where B: SocketService, I: UdpPacketProxy, - S: Stream + Unpin + ExtensionsMut, + S: Io + Unpin + ExtensionsMut, { async fn accept_udp_associate( &self, @@ -337,7 +323,11 @@ where }; let client_address = SocketAddress::new(dest_addr, dest_port); - let socket_north = match self.binder.bind(self.bind_north_interface.clone()).await { + let socket_north = match self + .binder + .bind_socket_with_address(self.bind_north_address) + .await + { Ok(twin) => twin, Err(err) => { let err = err.into(); @@ -376,7 +366,11 @@ where } }; - let socket_south = match self.binder.bind(self.bind_south_interface.clone()).await { + let socket_south = match self + .binder + .bind_socket_with_address(self.bind_south_address) + .await + { Ok(twin) => twin, Err(err) => { let err = err.into(); @@ -419,7 +413,6 @@ where self.north_buffer_size, socket_south, self.south_buffer_size, - #[cfg(feature = "dns")] self.dns_resolver.clone(), ); diff --git a/rama-socks5/src/server/udp/relay.rs b/rama-socks5/src/server/udp/relay.rs index feee250bf..da62b7981 100644 --- a/rama-socks5/src/server/udp/relay.rs +++ b/rama-socks5/src/server/udp/relay.rs @@ -8,7 +8,6 @@ use rama_udp::UdpSocket; use crate::proto::udp::UdpHeader; -#[cfg(feature = "dns")] use ::{ rama_core::{error::ErrorContext, extensions::Extensions}, rama_dns::client::resolver::{BoxDnsAddressResolver, DnsAddressResolver}, @@ -31,9 +30,7 @@ pub(super) struct UdpSocketRelay { north_write_buf: BytesMut, - #[cfg(feature = "dns")] dns_resolve_mode: DnsResolveIpMode, - #[cfg(feature = "dns")] dns_resolver: Option, } @@ -71,9 +68,7 @@ impl UdpSocketRelay { b }, - #[cfg(feature = "dns")] dns_resolve_mode: DnsResolveIpMode::default(), - #[cfg(feature = "dns")] dns_resolver: None, } } @@ -366,27 +361,6 @@ fn is_fatal_io_error(err: &std::io::Error) -> bool { ) } -#[cfg(not(feature = "dns"))] -impl UdpSocketRelay { - pub(super) async fn authority_to_socket_address( - &self, - authority: HostWithPort, - ) -> Result { - let HostWithPort { host, port } = authority; - let ip_addr = match host { - Host::Name(domain) => { - return Err(BoxError::from( - "dns names as target not supported: no dns server defined", - ) - .context_field("domain", domain)); - } - Host::Address(ip_addr) => ip_addr, - }; - Ok((ip_addr, port).into()) - } -} - -#[cfg(feature = "dns")] impl UdpSocketRelay { pub(super) fn maybe_with_dns_resolver( mut self, diff --git a/rama-socks5/src/server/udp/test.rs b/rama-socks5/src/server/udp/test.rs index fcf8b70fb..c14ddd244 100644 --- a/rama-socks5/src/server/udp/test.rs +++ b/rama-socks5/src/server/udp/test.rs @@ -26,7 +26,7 @@ impl MockUdpAssociator { impl Socks5UdpAssociatorSeal for MockUdpAssociator where - S: Stream + Unpin, + S: Io + Unpin, { async fn accept_udp_associate( &self, diff --git a/rama-tcp/README.md b/rama-tcp/README.md index a2282d2ff..f324cf0c4 100644 --- a/rama-tcp/README.md +++ b/rama-tcp/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-tcp/src/client/connect.rs b/rama-tcp/src/client/connect.rs index 0b794666e..409e3f0f6 100644 --- a/rama-tcp/src/client/connect.rs +++ b/rama-tcp/src/client/connect.rs @@ -1,28 +1,25 @@ +use rama_core::combinators::Either; use rama_core::error::ErrorExt as _; use rama_core::extensions::Extensions; -use rama_core::futures::{Stream, TryStreamExt}; use rama_core::stream::StreamExt; +use rama_core::stream::wrappers::ReceiverStream; use rama_core::telemetry::tracing::{self, Instrument, trace_span}; use rama_core::{ error::{BoxError, ErrorContext}, rt::Executor, }; -use rama_dns::client::resolver::DnsAddressResolver; +use rama_dns::client::resolver::{DnsAddressResolver, HappyEyeballAddressResolverExt}; use rama_dns::client::{GlobalDnsResolver, resolver::DnsAddresssResolverOverwrite}; use rama_net::address::HostWithPort; -use rama_net::{ - address::{Domain, Host, SocketAddress}, - mode::{ConnectIpMode, DnsResolveIpMode}, - socket::SocketOptions, -}; -use std::sync::atomic::AtomicUsize; +use rama_net::mode::ConnectIpMode; +use rama_net::{address::SocketAddress, socket::SocketOptions}; +use rama_utils::macros::error::static_str_error; use std::{ net::{IpAddr, SocketAddr}, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, - time::Duration, }; use tokio::sync::{ Semaphore, @@ -53,6 +50,34 @@ impl TcpStreamConnector for () { } } +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +/// a [`TcpStreamConnector`] implementation which +/// denies all incoming tcp connector requests with a [`TcpConnectDeniedError`]. +pub struct DenyTcpStreamConnector; + +impl DenyTcpStreamConnector { + #[inline(always)] + /// Create a new [`Default`] [`DenyTcpStreamConnector`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } +} + +static_str_error! { + #[doc = "TCP connect denied"] + pub struct TcpConnectDeniedError; +} + +impl TcpStreamConnector for DenyTcpStreamConnector { + type Error = TcpConnectDeniedError; + + async fn connect(&self, _: SocketAddr) -> Result { + Err(TcpConnectDeniedError) + } +} + impl TcpStreamConnector for Arc { type Error = T::Error; @@ -80,15 +105,9 @@ impl TcpStreamConnector for SocketAddress { async fn connect(&self, addr: SocketAddr) -> Result { let bind_addr = *self; - let opts = match bind_addr.ip_addr { - IpAddr::V4(_ip) => SocketOptions { - address: Some(bind_addr), - ..SocketOptions::default_tcp() - }, - IpAddr::V6(_ip) => SocketOptions { - address: Some(bind_addr), - ..SocketOptions::default_tcp_v6() - }, + let opts = SocketOptions { + address: Some(bind_addr), + ..SocketOptions::default_tcp() }; tokio::task::spawn_blocking(move || tcp_connect_with_socket_opts(&opts, addr)) .await @@ -121,7 +140,7 @@ fn tcp_connect_with_socket_opts( addr: SocketAddr, ) -> Result { let socket = opts - .try_build_socket() + .try_build_socket(addr.into()) .context("try to build TCP socket's underlying OS socket")?; socket .connect(&addr.into()) @@ -202,314 +221,380 @@ pub async fn tcp_connect( exec: Executor, ) -> Result<(TcpStream, SocketAddr), BoxError> where - Dns: DnsAddressResolver + Clone, + Dns: DnsAddressResolver, Connector: TcpStreamConnector + Send + 'static> + Clone, { - let ip_mode = extensions.get().copied().unwrap_or_default(); - let dns_mode = extensions.get().copied().unwrap_or_default(); - let HostWithPort { host, port } = address; - let domain = match host { - Host::Name(domain) => domain, - Host::Address(ip) => { - //check if IP Version is allowed - match (ip, ip_mode) { - (IpAddr::V4(_), ConnectIpMode::Ipv6) => { - return Err(BoxError::from("IPv4 address is not allowed")); - } - (IpAddr::V6(_), ConnectIpMode::Ipv4) => { - return Err(BoxError::from("IPv6 address is not allowed")); - } - _ => (), - } - - // if the authority is already defined as an IP address, we can directly connect to it - let addr = (ip, port).into(); - let stream = connector - .connect(addr) - .await - .context("establish tcp client connection")?; - return Ok((stream, addr)); - } - }; let maybe_dns_overwrite = extensions.get::().cloned(); - tcp_connect_inner( - domain.clone(), - port, - dns_mode, - (maybe_dns_overwrite, dns), - connector.clone(), - ip_mode, - exec, - ) - .await -} + let dns_resolver = (maybe_dns_overwrite, dns); -async fn tcp_connect_inner( - domain: Domain, - port: u16, - dns_mode: DnsResolveIpMode, - dns: Dns, - connector: Connector, - connect_mode: ConnectIpMode, - exec: Executor, -) -> Result<(TcpStream, SocketAddr), BoxError> -where - Dns: DnsAddressResolver + Clone, - Connector: TcpStreamConnector + Send + 'static> + Clone, -{ - let (tx, mut rx) = channel(1); - let resolved_count = Arc::new(AtomicUsize::new(0)); + let connect_ip_mode = extensions.get().copied().unwrap_or(ConnectIpMode::Dual); + + let ip_stream = dns_resolver + .happy_eyeballs_resolver(host.clone()) + .with_extensions(extensions) + .lookup_ip(); + + let (tx, rx) = channel(1); + let recv_stream = ReceiverStream::new(rx); + + let mut output_stream = std::pin::pin!( + ip_stream + .map({ + let tx = tx.clone(); + move |result| Either::A((tx.clone(), result)) + }) + .merge(recv_stream.map(Either::B)) + ); + + drop(tx); + + let mut resolved_count = 0; let connected = Arc::new(AtomicBool::new(false)); let sem = Arc::new(Semaphore::new(3)); - if dns_mode.ipv4_supported() { - exec.spawn_task( - tcp_connect_inner_branch( - dns_mode, - dns.clone(), - connect_mode, - connector.clone(), - IpKind::Ipv4, - domain.clone(), - port, - tx.clone(), - connected.clone(), - resolved_count.clone(), - sem.clone(), - ) - .instrument(tracing::trace_span!( - "tcp::connect::dns_v4", - otel.kind = "client", - network.protocol.name = "tcp", - )), - ); - } + let mut index = 0; + while let Some(output) = output_stream.next().await { + index += 1; - if dns_mode.ipv6_supported() { - exec.into_spawn_task( - tcp_connect_inner_branch( - dns_mode, - dns.clone(), - connect_mode, - connector.clone(), - IpKind::Ipv6, - domain.clone(), - port, - tx.clone(), - connected.clone(), - resolved_count.clone(), - sem.clone(), - ) - .instrument(tracing::trace_span!( - "tcp::connect::dns_v6", - otel.kind = "client", - network.protocol.name = "tcp", - )), - ); - } + match output { + Either::A((tx, ip_result)) => { + let ip = match ip_result { + Ok(ip) => ip, + Err(err) => { + tracing::debug!("failed to resolve ip addr for host {host}: {err}"); + continue; + } + }; + resolved_count += 1; - drop(tx); - if let Some((stream, addr)) = rx.recv().await { - connected.store(true, Ordering::Release); - return Ok((stream, addr)); + match (ip, connect_ip_mode) { + (IpAddr::V4(_), ConnectIpMode::Ipv6) => { + tracing::debug!( + "resolved to ipv4 addr {ip} for host {host}: ignored due to ConnectIpMode::Ipv6" + ); + continue; + } + (IpAddr::V6(_), ConnectIpMode::Ipv4) => { + tracing::debug!( + "resolved to ipv6 addr {ip} for host {host}: ignored due to ConnectIpMode::Ipv4" + ); + continue; + } + (IpAddr::V4(_), ConnectIpMode::Ipv4 | ConnectIpMode::Dual) + | (IpAddr::V6(_), ConnectIpMode::Ipv6 | ConnectIpMode::Dual) => (), + }; + + let connector = connector.clone(); + let connected = connected.clone(); + let sem = sem.clone(); + + exec.spawn_task( + tcp_connect_inner_task(index, connector, ip, port, connected, tx, sem) + .instrument(trace_span!( + "tcp::connect", + otel.kind = "client", + network.protocol.name = "tcp", + network.peer.address = %ip, + server.host = %host, + %index, + )), + ); + } + Either::B(stream_and_addr) => { + connected.store(true, Ordering::Release); + return Ok(stream_and_addr); + } + } } - let resolve_count = resolved_count.load(Ordering::Acquire); - if resolve_count > 0 { + if resolved_count > 0 { Err( - BoxError::from("failed to connect to any resolved IP address") - .context_field("domain", domain) + BoxError::from("failed to (tcp) connect to any resolved IP address") + .context_field("host", host) .context_field("port", port) - .context_field("resolved_addr_count", resolve_count), + .context_field("resolved_addr_count", resolved_count), ) } else { - Err(BoxError::from("failed to resolve into any IP address") - .context_field("domain", domain) - .context_field("port", port)) + Err( + BoxError::from("failed to resolve into any IP address (as part of tcp connect)") + .context_field("host", host) + .context_field("port", port), + ) } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum IpKind { - Ipv4, - Ipv6, -} - -#[allow(clippy::too_many_arguments)] -async fn tcp_connect_inner_branch( - dns_mode: DnsResolveIpMode, - dns: Dns, - connect_mode: ConnectIpMode, +async fn tcp_connect_inner_task( + index: usize, connector: Connector, - ip_kind: IpKind, - domain: Domain, + ip: IpAddr, port: u16, - tx: Sender<(TcpStream, SocketAddr)>, connected: Arc, - resolved_count: Arc, + tx: Sender<(TcpStream, SocketAddr)>, sem: Arc, ) where - Dns: DnsAddressResolver + Clone, Connector: TcpStreamConnector + Send + 'static> + Clone, { - match ip_kind { - IpKind::Ipv4 => { - let ip_stream = dns.lookup_ipv4(domain.clone()).map_ok(IpAddr::V4); - tcp_connect_inner_branch_with_stream( - dns_mode, - connect_mode, - connector, - ip_kind, - ip_stream, - domain, - port, - tx, - connected, - resolved_count, - sem, - ) - .await; - } - IpKind::Ipv6 => { - let ip_stream = dns.lookup_ipv6(domain.clone()).map_ok(IpAddr::V6); - tcp_connect_inner_branch_with_stream( - dns_mode, - connect_mode, - connector, - ip_kind, - ip_stream, - domain, - port, - tx, - connected, - resolved_count, - sem, - ) - .await; + let _permit = match sem.acquire().await { + Ok(permit) => permit, + Err(err) => { + tracing::trace!("[IP | {ip}] #{index}: abort conn; failed to acquire permit: {err}"); + return; } }; -} -#[allow(clippy::too_many_arguments)] -async fn tcp_connect_inner_branch_with_stream( - dns_mode: DnsResolveIpMode, - connect_mode: ConnectIpMode, - connector: Connector, - ip_kind: IpKind, - ip_stream: impl Stream>, - domain: Domain, - port: u16, - tx: Sender<(TcpStream, SocketAddr)>, - connected: Arc, - resolved_count: Arc, - sem: Arc, -) where - Connector: TcpStreamConnector + Send + 'static> + Clone, - E: Into + Send + 'static, -{ - let mut ip_stream = std::pin::pin!(ip_stream); + if connected.load(Ordering::Acquire) { + tracing::trace!( + "[IP | {ip}] #{index}: abort spawned attempt to port {port} (connection already established)" + ); + return; + } - let (ipv4_delay_scalar, ipv6_delay_scalar) = match dns_mode { - DnsResolveIpMode::DualPreferIpV4 | DnsResolveIpMode::SingleIpV4 => (15 * 2, 21 * 2), - _ => (21 * 2, 15 * 2), - }; + tracing::trace!("[IP | {ip}] #{index}: tcp connect attempt to port {port}"); - let mut index = 0; - while let Some(ip_result) = ip_stream.next().await { - index += 1; - let ip = match ip_result { - Ok(ip) => ip, - Err(err) => { - tracing::debug!( - "[{ip_kind:?}] #{index}: result contained err: {}", - err.into(), + let addr = (ip, port).into(); + match connector.connect(addr).await { + Ok(stream) => { + tracing::trace!("[IP | {ip}] #{index}: tcp connection stablished to port {port}"); + if let Err(err) = tx.send((stream, addr)).await { + tracing::trace!( + "[IP | {ip}] #{index}: failed to send connected stream with peer port {port}: {err:?}" ); - continue; } - }; + } + Err(err) => { + let err = err.into_box_error(); + tracing::trace!( + "[IP | {ip}] #{index}: tcp connector failed to connect to port {port}: {err:?}" + ); + } + } +} - let addr = (ip, port).into(); +#[cfg(test)] +mod tests { + use std::{ + convert::Infallible, + net::{Ipv4Addr, Ipv6Addr}, + }; + + use super::*; + use rama_dns::client::{DenyAllDnsResolver, EmptyDnsResolver}; + use rama_net::mode::{ConnectIpMode, DnsResolveIpMode}; + + async fn test_generic_err( + dns: Dns, + connector: Connector, + extensions: Option, + ) where + Dns: DnsAddressResolver, + Connector: TcpStreamConnector + Send + 'static> + Clone, + { + let extensions = extensions.unwrap_or_default(); + + let _ = tcp_connect( + &extensions, + HostWithPort::example_domain_http(), + dns, + connector, + Executor::default(), + ) + .await + .unwrap_err(); + } - resolved_count.fetch_add(1, Ordering::AcqRel); + #[tokio::test] + async fn test_default_tcp_connect_with_dns_deny_and_connector_deny() { + let dns = DenyAllDnsResolver::new(); + let connector = DenyTcpStreamConnector::new(); + test_generic_err(dns, connector, None).await; + } - let sem = match (ip.is_ipv4(), connect_mode) { - (true, ConnectIpMode::Ipv6) => { - tracing::trace!( - "[{ip_kind:?}] #{index}: abort connect loop to {addr} (IPv4 address is not allowed)" - ); - continue; - } - (false, ConnectIpMode::Ipv4) => { - tracing::trace!( - "[{ip_kind:?}] #{index}: abort connect loop to {addr} (IPv6 address is not allowed)" - ); - continue; - } - _ => sem.clone(), - }; + #[tokio::test] + async fn test_default_tcp_connect_with_dns_nop_and_connector_deny() { + let dns = EmptyDnsResolver::new(); + let connector = DenyTcpStreamConnector::new(); + test_generic_err(dns, connector, None).await; + } + + #[tokio::test] + async fn test_default_tcp_connect_with_static_ip_and_connector_deny() { + test_generic_err(Ipv4Addr::LOCALHOST, DenyTcpStreamConnector, None).await; + test_generic_err( + IpAddr::V4(Ipv4Addr::LOCALHOST), + DenyTcpStreamConnector, + None, + ) + .await; + test_generic_err(Ipv6Addr::LOCALHOST, DenyTcpStreamConnector, None).await; + } + + #[derive(Debug, Clone)] + struct PanicTcpConnector; + + impl TcpStreamConnector for PanicTcpConnector { + type Error = Infallible; + + async fn connect(&self, _: SocketAddr) -> Result { + unreachable!() + } + } + + #[tokio::test] + async fn test_default_tcp_connect_with_incompatible_dns_mode_and_connector_return_dummy() { + test_generic_err(Ipv4Addr::LOCALHOST, PanicTcpConnector, { + let mut ext = Extensions::new(); + ext.insert(DnsResolveIpMode::SingleIpV6); + Some(ext) + }) + .await; + test_generic_err(Ipv6Addr::LOCALHOST, PanicTcpConnector, { + let mut ext = Extensions::new(); + ext.insert(DnsResolveIpMode::SingleIpV4); + Some(ext) + }) + .await; + } + + #[tokio::test] + async fn test_default_tcp_connect_with_incompatible_connect_ip_mode_and_connector_return_dummy() + { + test_generic_err(Ipv4Addr::LOCALHOST, PanicTcpConnector, { + let mut ext = Extensions::new(); + ext.insert(ConnectIpMode::Ipv6); + Some(ext) + }) + .await; + test_generic_err(Ipv6Addr::LOCALHOST, PanicTcpConnector, { + let mut ext = Extensions::new(); + ext.insert(ConnectIpMode::Ipv4); + Some(ext) + }) + .await; + } +} + +#[cfg(all(test, any(target_os = "windows", target_family = "unix")))] +mod unix_windows_tests { + use std::{ + convert::Infallible, + net::{Ipv4Addr, Ipv6Addr}, + }; - let tx = tx.clone(); - let connected = connected.clone(); + use rama_dns::client::DenyAllDnsResolver; + use rama_net::{mode::DnsResolveIpMode, socket}; - // back off retries exponentially - if index > 0 { - let delay = match ip_kind { - IpKind::Ipv4 => Duration::from_micros((ipv4_delay_scalar * index) as u64), - IpKind::Ipv6 => Duration::from_micros((ipv6_delay_scalar * index) as u64), + use super::*; + + #[derive(Debug, Clone)] + struct DummyTcpConnector; + + impl TcpStreamConnector for DummyTcpConnector { + type Error = Infallible; + + async fn connect(&self, addr: SocketAddr) -> Result { + let domain = match addr.ip() { + IpAddr::V4(_) => socket::core::Domain::IPV4, + IpAddr::V6(_) => socket::core::Domain::IPV6, }; - tokio::time::sleep(delay).await; + + let socket = socket::core::Socket::new( + domain, + socket::core::Type::STREAM, + Some(socket::core::Protocol::TCP), + ) + .expect("create dummy tcp socket"); + + let stream = TcpStream::try_from_socket(socket, Default::default()).unwrap(); + Ok(stream) } + } - if connected.load(Ordering::Acquire) { - tracing::trace!( - "[{ip_kind:?}] #{index}: abort connect loop to {addr} (connection already established)" - ); - return; + async fn test_generic_ok(dns: Dns, extensions: Option) + where + Dns: DnsAddressResolver, + { + let extensions = extensions.unwrap_or_default(); + + let _ = tcp_connect( + &extensions, + HostWithPort::example_domain_http(), + dns, + DummyTcpConnector, + Executor::default(), + ) + .await + .unwrap(); + } + + #[tokio::test] + async fn test_default_tcp_connect_happy_path_no_extensions() { + test_generic_ok(Ipv4Addr::LOCALHOST, None).await; + test_generic_ok(IpAddr::V4(Ipv4Addr::LOCALHOST), None).await; + test_generic_ok(Ipv6Addr::LOCALHOST, None).await; + } + + #[tokio::test] + async fn test_default_tcp_connect_happy_path_explicit_dns_mode() { + for dns_resolve_ip_mode in [ + DnsResolveIpMode::SingleIpV4, + DnsResolveIpMode::Dual, + DnsResolveIpMode::DualPreferIpV4, + ] { + test_generic_ok(Ipv4Addr::LOCALHOST, { + let mut ext = Extensions::new(); + ext.insert(dns_resolve_ip_mode); + Some(ext) + }) + .await; } - let connector = connector.clone(); - tokio::spawn(async move { - let _permit = match sem.acquire().await { - Ok(permit) => permit, - Err(err) => { - tracing::trace!( - "[{ip_kind:?}] #{index}: abort conn; failed to acquire permit: {err}" - ); - return; - } - }; - if connected.load(Ordering::Acquire) { - tracing::trace!( - "[{ip_kind:?}] #{index}: abort spawned attempt to {addr} (connection already established)" - ); - return; - } + for dns_resolve_ip_mode in [DnsResolveIpMode::SingleIpV6, DnsResolveIpMode::Dual] { + test_generic_ok(Ipv6Addr::LOCALHOST, { + let mut ext = Extensions::new(); + ext.insert(dns_resolve_ip_mode); + Some(ext) + }) + .await; + } + } - tracing::trace!("[{ip_kind:?}] #{index}: tcp connect attempt to {addr}"); + #[tokio::test] + async fn test_default_tcp_connect_happy_path_explicit_connect_ip_mode() { + for connect_ip_mode in [ConnectIpMode::Ipv4, ConnectIpMode::Dual] { + test_generic_ok(Ipv4Addr::LOCALHOST, { + let mut ext = Extensions::new(); + ext.insert(connect_ip_mode); + Some(ext) + }) + .await; + } - match connector.connect(addr).await { - Ok(stream) => { - tracing::trace!("[{ip_kind:?}] #{index}: tcp connection stablished to {addr}"); - if let Err(err) = tx.send((stream, addr)).await { - tracing::trace!( - "[{ip_kind:?}] #{index}: failed to send resolved IP address {addr}: {err:?}" - ); - } - } - Err(err) => { - let err = err.into_box_error(); - tracing::trace!("[{ip_kind:?}] #{index}: tcp connector failed to connect to {addr}: {err:?}"); - } - }; - }.instrument(trace_span!( - "tcp::connect", - otel.kind = "client", - network.protocol.name = "tcp", - network.peer.address = %ip, - server.address = %domain, - %index, - ))); + for connect_ip_mode in [ConnectIpMode::Ipv6, ConnectIpMode::Dual] { + test_generic_ok(Ipv6Addr::LOCALHOST, { + let mut ext = Extensions::new(); + ext.insert(connect_ip_mode); + Some(ext) + }) + .await; + } + } + + #[tokio::test] + async fn test_default_tcp_connect_happy_path_with_dns_overwrite() { + test_generic_ok(DenyAllDnsResolver::new(), { + let mut ext = Extensions::new(); + ext.insert(DnsAddresssResolverOverwrite::new(Ipv4Addr::LOCALHOST)); + Some(ext) + }) + .await; + + test_generic_ok(DenyAllDnsResolver::new(), { + let mut ext = Extensions::new(); + ext.insert(DnsAddresssResolverOverwrite::new(Ipv6Addr::LOCALHOST)); + Some(ext) + }) + .await; } } diff --git a/rama-tcp/src/client/mod.rs b/rama-tcp/src/client/mod.rs index 04c19cbad..7c607dde7 100644 --- a/rama-tcp/src/client/mod.rs +++ b/rama-tcp/src/client/mod.rs @@ -6,7 +6,10 @@ pub mod service; mod connect; #[doc(inline)] -pub use connect::{TcpStreamConnector, default_tcp_connect, tcp_connect}; +pub use connect::{ + DenyTcpStreamConnector, TcpConnectDeniedError, TcpStreamConnector, default_tcp_connect, + tcp_connect, +}; #[cfg(feature = "http")] mod request; diff --git a/rama-tcp/src/client/service/connector.rs b/rama-tcp/src/client/service/connector.rs index 8228134d1..bbc544d3b 100644 --- a/rama-tcp/src/client/service/connector.rs +++ b/rama-tcp/src/client/service/connector.rs @@ -26,8 +26,6 @@ pub struct TcpConnector { exec: Executor, } -impl TcpConnector {} - impl TcpConnector { /// Create a new [`TcpConnector`], which is used to establish a connection to a server. /// diff --git a/rama-tcp/src/client/service/forward.rs b/rama-tcp/src/client/service/forward.rs deleted file mode 100644 index 4e0d04dbb..000000000 --- a/rama-tcp/src/client/service/forward.rs +++ /dev/null @@ -1,104 +0,0 @@ -use super::TcpConnector; -use crate::client::Request as TcpRequest; -use rama_core::{ - Service, - error::{BoxError, ErrorContext as _}, - extensions::ExtensionsMut, - rt::Executor, - stream::Stream, -}; -use rama_net::{ - address::HostWithPort, - client::{ConnectorService, EstablishedClientConnection}, - proxy::{ProxyRequest, ProxyTarget, StreamForwardService}, -}; - -#[derive(Debug, Clone)] -enum ForwarderKind { - Static(HostWithPort), - Dynamic, -} - -/// A TCP forwarder. -#[derive(Debug, Clone)] -pub struct Forwarder { - kind: ForwarderKind, - connector: C, -} - -/// Default [`Forwarder`]. -pub type DefaultForwarder = Forwarder; - -impl DefaultForwarder { - /// Create a new static forwarder for the given target [`HostWithPort`] - pub fn new(exec: Executor, target: impl Into) -> Self { - Self { - kind: ForwarderKind::Static(target.into()), - connector: TcpConnector::new(exec), - } - } - - /// Create a new dynamic forwarder, which will fetch the target from the [`Extensions`] - /// - /// [`Extensions`]: rama_core::extensions::Extensions - #[must_use] - pub fn ctx(exec: Executor) -> Self { - Self { - kind: ForwarderKind::Dynamic, - connector: TcpConnector::new(exec), - } - } -} - -impl Forwarder { - /// Set a custom "connector" for this forwarder, overwriting - /// the default tcp forwarder which simply establishes a TCP connection. - /// - /// This can be useful for any custom middleware, but also to enrich with - /// rama-provided services for tls connections, HAproxy client endoding - /// or even an entirely custom tcp connector service. - pub fn with_connector(self, connector: T) -> Forwarder { - Forwarder { - kind: self.kind, - connector, - } - } -} - -impl Service for Forwarder -where - T: Stream + Unpin + ExtensionsMut, - C: ConnectorService, -{ - type Output = (); - type Error = BoxError; - - async fn serve(&self, source: T) -> Result { - let authority = match &self.kind { - ForwarderKind::Static(target) => target.clone(), - ForwarderKind::Dynamic => source - .extensions() - .get::() - .map(|f| f.0.clone()) - .context("missing forward authority")?, - }; - - // Clone them here so we also have them on source still - let extensions = source.extensions().clone(); - let req = TcpRequest::new_with_extensions(authority.clone(), extensions); - - let EstablishedClientConnection { conn: target, .. } = self - .connector - .connect(req) - .await - .context("establish tcp connection") - .context_field("authority", authority)?; - - let proxy_req = ProxyRequest { source, target }; - - StreamForwardService::default() - .serve(proxy_req) - .await - .into_box_error() - } -} diff --git a/rama-tcp/src/client/service/mod.rs b/rama-tcp/src/client/service/mod.rs index 70824c7ec..e274b95f8 100644 --- a/rama-tcp/src/client/service/mod.rs +++ b/rama-tcp/src/client/service/mod.rs @@ -1,9 +1,5 @@ //! TCP services for Rama. -mod forward; -#[doc(inline)] -pub use forward::{DefaultForwarder, Forwarder}; - mod connector; #[doc(inline)] pub use connector::TcpConnector; diff --git a/rama-tcp/src/client/service/select.rs b/rama-tcp/src/client/service/select.rs index cae9109fc..0ed5e8ac4 100644 --- a/rama-tcp/src/client/service/select.rs +++ b/rama-tcp/src/client/service/select.rs @@ -58,7 +58,7 @@ impl TcpStreamConnectorFactory for () { /// This struct cannot be created by third party crates /// and instead is to be used via other API's provided by this crate. #[derive(Debug, Clone)] -pub struct TcpStreamConnectorCloneFactory(pub(super) C); +pub struct TcpStreamConnectorCloneFactory(pub(crate) C); impl TcpStreamConnectorFactory for TcpStreamConnectorCloneFactory where diff --git a/rama-tcp/src/lib.rs b/rama-tcp/src/lib.rs index 34b39c029..d64d4b4e6 100644 --- a/rama-tcp/src/lib.rs +++ b/rama-tcp/src/lib.rs @@ -23,6 +23,8 @@ pub mod client; pub mod pool; +pub mod proxy; pub mod server; + pub mod stream; pub use stream::{TcpStream, TokioTcpStream}; diff --git a/rama-tcp/src/proxy/io_to_bridge_io.rs b/rama-tcp/src/proxy/io_to_bridge_io.rs new file mode 100644 index 000000000..5eae36971 --- /dev/null +++ b/rama-tcp/src/proxy/io_to_bridge_io.rs @@ -0,0 +1,222 @@ +use rama_core::{ + Layer, Service, + error::{BoxError, ErrorContext as _}, + extensions::ExtensionsRef, + io::{BridgeIo, Io}, + rt::Executor, + telemetry::tracing, +}; +use rama_net::{ + address::HostWithPort, + client::{ConnectorService, EstablishedClientConnection}, + proxy::ProxyTarget, +}; +use rama_utils::macros::define_inner_service_accessors; + +use crate::client::service::TcpConnector; + +// TOOD: in future we can move this out of rama-tcp... +// need to find some kind of input which is not tcp specific, +// at that point it us no longer bound to tcp at all + +#[derive(Debug, Clone)] +pub struct IoToProxyBridgeIo { + inner: S, + connector: C, + address_provider: AddressProvider, +} + +#[derive(Debug, Clone)] +enum AddressProvider { + Static(HostWithPort), + ExtensionProxyTarget, +} + +impl IoToProxyBridgeIo { + #[inline(always)] + /// Creates a new [`IoToProxyBridgeIo`] service, + /// which will use the provided target info to connect to. + /// + /// Use [`Self::extension_proxy_target`] if you wish to have it be done + /// using the [`ProxyTarget`] extension instead, failing the input flow + /// in case that extension not exist. + pub fn new(inner: S, exec: Executor, target: HostWithPort) -> Self { + Self { + inner, + connector: TcpConnector::new(exec), + address_provider: AddressProvider::Static(target), + } + } + + #[inline(always)] + /// Creates a new [`IoToProxyBridgeIo`] service, + /// which expects while serving that [`ProxyTarget`] + /// is available in the input's extension, and fail otherwise. + /// + /// Use [`Self::new`] if you wish to use a hardcoded target instead, + pub fn extension_proxy_target(exec: Executor, inner: S) -> Self { + Self { + inner, + connector: TcpConnector::new(exec), + address_provider: AddressProvider::ExtensionProxyTarget, + } + } + + define_inner_service_accessors!(); +} + +impl IoToProxyBridgeIo { + /// Set a custom "connector" for service, overwriting + /// the default tcp forwarder which simply establishes a TCP connection. + /// + /// This can be useful for any custom middleware, but also to enrich with + /// rama-provided services for tls connections, HAproxy client endoding + /// or even an entirely custom tcp connector service. + pub fn with_connector(self, connector: C) -> IoToProxyBridgeIo { + IoToProxyBridgeIo { + inner: self.inner, + connector, + address_provider: self.address_provider, + } + } +} + +/// A [`Layer`] that produces [`IoToProxyBridgeIo`] services. +#[derive(Debug, Clone)] +pub struct IoToProxyBridgeIoLayer { + connector: C, + address_provider: AddressProvider, +} + +impl IoToProxyBridgeIoLayer { + #[inline(always)] + /// Creates a new [`IoToProxyBridgeIoLayer`], + /// which will use by the [`IoToProxyBridgeIo`] [`Service`] the provided target info to connect to. + /// + /// Use [`Self::extension_proxy_target`] if you wish to have it be done + /// using the [`ProxyTarget`] extension instead, failing the input flow + /// in case that extension not exist. + pub fn new(exec: Executor, target: impl Into) -> Self { + Self { + connector: TcpConnector::new(exec), + address_provider: AddressProvider::Static(target.into()), + } + } + + #[inline(always)] + /// Creates a new [`IoToProxyBridgeIoLayer`], + /// which will create the [`IoToProxyBridgeIo`] [`Service`] + /// that with this constructor will expect while serving that [`ProxyTarget`] + /// is available in the input's extension, and fail otherwise. + /// + /// Use [`Self::new`] if you wish to use a hardcoded target instead, + pub fn extension_proxy_target(exec: Executor) -> Self { + Self { + connector: TcpConnector::new(exec), + address_provider: AddressProvider::ExtensionProxyTarget, + } + } +} + +impl IoToProxyBridgeIoLayer { + /// Set a custom "connector" for layer's service, overwriting + /// the default tcp forwarder which simply establishes a TCP connection. + /// + /// This can be useful for any custom middleware, but also to enrich with + /// rama-provided services for tls connections, HAproxy client endoding + /// or even an entirely custom tcp connector service. + pub fn with_connector(self, connector: C) -> IoToProxyBridgeIoLayer { + IoToProxyBridgeIoLayer { + connector, + address_provider: self.address_provider, + } + } +} + +impl IoToProxyBridgeIoLayer { + #[inline(always)] + /// Same as [`Self::new`] but using a custom connector. + pub fn new_with_connector(target: impl Into, connector: C) -> Self { + Self { + connector, + address_provider: AddressProvider::Static(target.into()), + } + } + + #[inline(always)] + /// Same as [`Self::extension_proxy_target`] but using a custom connector. + pub fn extension_proxy_target_with_connector(connector: C) -> Self { + Self { + connector, + address_provider: AddressProvider::ExtensionProxyTarget, + } + } +} + +impl Layer for IoToProxyBridgeIoLayer +where + C: Clone, +{ + type Service = IoToProxyBridgeIo; + + fn layer(&self, inner: S) -> Self::Service { + Self::Service { + inner, + connector: self.connector.clone(), + address_provider: self.address_provider.clone(), + } + } + + fn into_layer(self, inner: S) -> Self::Service { + Self::Service { + inner, + connector: self.connector.clone(), + address_provider: self.address_provider, + } + } +} + +impl Service for IoToProxyBridgeIo +where + S: Service, Error: Into>, + Ingress: Io + ExtensionsRef, + C: ConnectorService, +{ + type Output = S::Output; + type Error = BoxError; + + async fn serve(&self, ingress: Ingress) -> Result { + let egress_addr = match self.address_provider.clone() { + AddressProvider::Static(host_with_port) => host_with_port, + AddressProvider::ExtensionProxyTarget => { + if let Some(ProxyTarget(host_with_port)) = ingress.extensions().get().cloned() { + host_with_port + } else { + return Err(BoxError::from( + "missing ProxyTarget in IoToProxyBridgeIo: proxy target assumed to exist in ingress extensions", + )); + } + } + }; + + tracing::trace!( + "try to establish connection to egress as a means to create a BridgeIo: addr = {egress_addr}" + ); + + let extensions = ingress.extensions().clone(); + let tcp_req = crate::client::Request::new_with_extensions(egress_addr.clone(), extensions); + + let EstablishedClientConnection { + input: _, + conn: egress, + } = self + .connector + .connect(tcp_req) + .await + .context("establish tcp connection") + .context_field("address", egress_addr)?; + + let bridge_io = BridgeIo(ingress, egress); + self.inner.serve(bridge_io).await.into_box_error() + } +} diff --git a/rama-tcp/src/proxy/mod.rs b/rama-tcp/src/proxy/mod.rs new file mode 100644 index 000000000..961048791 --- /dev/null +++ b/rama-tcp/src/proxy/mod.rs @@ -0,0 +1,11 @@ +//! Proxy (service) utilities + +// TODO: in future we probably want to get rid of all these http feature gates in rama-tcp and rama-net... +// +// this is a clear sign of wrong boundaries that we need to fix soon, probably +// still before the actual 0.3 release + +#[cfg(feature = "http")] +mod io_to_bridge_io; +#[cfg(feature = "http")] +pub use self::io_to_bridge_io::{IoToProxyBridgeIo, IoToProxyBridgeIoLayer}; diff --git a/rama-tcp/src/server/listener.rs b/rama-tcp/src/server/listener.rs index 4eb6eb536..2246d422d 100644 --- a/rama-tcp/src/server/listener.rs +++ b/rama-tcp/src/server/listener.rs @@ -5,7 +5,6 @@ use rama_core::extensions::ExtensionsMut; use rama_core::rt::Executor; use rama_core::telemetry::tracing::{self, Instrument, trace_root_span}; use rama_net::address::SocketAddress; -use rama_net::socket::Interface; use rama_net::stream::Socket; use rama_net::stream::SocketInfo; use std::pin::pin; @@ -13,7 +12,7 @@ use std::{io, net::SocketAddr}; use tokio::net::TcpListener as TokioTcpListener; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] -use rama_net::socket::{DeviceName, SocketOptions}; +use rama_net::socket::{DeviceName, SocketOptions, opts::Domain}; use crate::TcpStream; @@ -98,46 +97,25 @@ impl TcpListenerBuilder { pub async fn bind_device> + Send + 'static>( self, name: N, + domain: Domain, + backlog: Option, ) -> Result { - tokio::task::spawn_blocking(|| { + tokio::task::spawn_blocking(move || { let name = name.try_into().map_err(Into::::into)?; let socket = SocketOptions { device: Some(name), ..SocketOptions::default_tcp() } - .try_build_socket() + .try_build_socket(domain) .context("create tcp ipv4 socket attached to device")?; socket - .listen(4096) + .listen(backlog.unwrap_or(4096)) .context("mark the socket as ready to accept incoming connection requests")?; bind_socket_internal(socket, self.exec) }) .await .context("await blocking bind socket task")? } - - /// Creates a new TcpListener, which will be bound to the specified interface. - /// - /// The returned listener is ready for accepting connections. - pub async fn bind>>( - self, - interface: I, - ) -> Result { - match interface.try_into().map_err(Into::::into)? { - Interface::Address(addr) => self.bind_address(addr).await, - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - Interface::Device(name) => self.bind_device(name).await, - Interface::Socket(opts) => { - let socket = opts - .try_build_socket() - .context("build socket from options")?; - socket - .listen(4096) - .context("mark the socket as ready to accept incoming connection requests")?; - self.bind_socket(socket).await - } - } - } } #[derive(Debug)] @@ -193,18 +171,12 @@ impl TcpListener { pub async fn bind_device> + Send + 'static>( name: N, exec: Executor, + domain: Domain, + backlog: Option, ) -> Result { - TcpListenerBuilder::new(exec).bind_device(name).await - } - - /// Creates a new TcpListener, which will be bound to the specified interface. - /// - /// The returned listener is ready for accepting connections. - pub async fn bind>>( - interface: I, - exec: Executor, - ) -> Result { - TcpListenerBuilder::new(exec).bind(interface).await + TcpListenerBuilder::new(exec) + .bind_device(name, domain, backlog) + .await } } diff --git a/rama-tcp/src/server/mod.rs b/rama-tcp/src/server/mod.rs index 179d289be..2221e5b77 100644 --- a/rama-tcp/src/server/mod.rs +++ b/rama-tcp/src/server/mod.rs @@ -14,7 +14,7 @@ //! //! #[tokio::main] //! async fn main() { -//! TcpListener::bind("127.0.0.1:9000", Executor::default()) +//! TcpListener::bind_address("127.0.0.1:9000", Executor::default()) //! .await //! .expect("bind TCP Listener") //! .serve(service_fn(async |mut stream: TcpStream| { diff --git a/rama-tcp/src/stream.rs b/rama-tcp/src/stream.rs index e43329c8f..933069351 100644 --- a/rama-tcp/src/stream.rs +++ b/rama-tcp/src/stream.rs @@ -9,6 +9,8 @@ use rama_core::{ extensions::Extensions, extensions::{ExtensionsMut, ExtensionsRef}, }; +#[cfg(any(target_os = "windows", target_family = "unix"))] +use rama_net::socket; use rama_net::{address::SocketAddress, stream::Socket}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; pub use tokio::net::TcpStream as TokioTcpStream; @@ -24,12 +26,36 @@ pin_project! { } impl TcpStream { + #[inline(always)] pub fn new(stream: TokioTcpStream) -> Self { Self { stream, extensions: Extensions::new(), } } + + #[cfg(any(target_os = "windows", target_family = "unix"))] + pub fn try_from_socket( + socket: socket::core::Socket, + extensions: Extensions, + ) -> Result { + let stream = std::net::TcpStream::from(socket); + Self::try_from_std_tcp_stream(stream, extensions) + } + + pub fn try_from_std_tcp_stream( + stream: std::net::TcpStream, + extensions: Extensions, + ) -> Result { + stream.set_nonblocking(true)?; + let stream = TokioTcpStream::from_std(stream)?; + Ok(Self::from_tokio_tcp_stream(stream, extensions)) + } + + #[inline(always)] + pub fn from_tokio_tcp_stream(stream: TokioTcpStream, extensions: Extensions) -> Self { + Self { stream, extensions } + } } impl From for TcpStream { diff --git a/rama-tls-acme/README.md b/rama-tls-acme/README.md index ba213560a..5c589b363 100644 --- a/rama-tls-acme/README.md +++ b/rama-tls-acme/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-tls-boring/README.md b/rama-tls-boring/README.md index 88cc7e735..2ff080284 100644 --- a/rama-tls-boring/README.md +++ b/rama-tls-boring/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/workflows/CI/badge.svg [actions-url]: https://github.com/plabayo/rama/actions diff --git a/rama-tls-boring/src/client/connector.rs b/rama-tls-boring/src/client/connector.rs index 3c056794c..e1aacab6d 100644 --- a/rama-tls-boring/src/client/connector.rs +++ b/rama-tls-boring/src/client/connector.rs @@ -2,10 +2,10 @@ use rama_boring_tokio::SslStream; use rama_core::conversion::RamaTryInto; use rama_core::error::{BoxError, ErrorContext as _, ErrorExt}; use rama_core::extensions::{Extensions, ExtensionsMut}; -use rama_core::stream::Stream; +use rama_core::io::Io; use rama_core::telemetry::tracing; use rama_core::{Layer, Service}; -use rama_net::address::Host; +use rama_net::address::Domain; use rama_net::client::{ConnectorService, EstablishedClientConnection}; use rama_net::tls::ApplicationProtocol; use rama_net::tls::client::NegotiatedTlsParameters; @@ -13,8 +13,8 @@ use rama_net::transport::TryRefIntoTransportContext; use rama_utils::macros::generate_set_and_with; use std::sync::Arc; -use super::{AutoTlsStream, TlsConnectorData, TlsConnectorDataBuilder, TlsStream}; -use crate::types::TlsTunnel; +use super::{AutoTlsStream, TlsConnectorData, TlsConnectorDataBuilder}; +use crate::{TlsStream, types::TlsTunnel}; #[cfg(feature = "http")] use rama_http_types::{Version, conn::TargetHttpVersion}; @@ -79,10 +79,10 @@ impl TlsConnectorLayer { /// Creates a new [`TlsConnectorLayer`] which will establish /// a secure connection if the request is to be tunneled. #[must_use] - pub fn tunnel(host: Option) -> Self { + pub fn tunnel(sni: Option) -> Self { Self { connector_data: None, - kind: ConnectorKindTunnel { host }, + kind: ConnectorKindTunnel { sni }, } } } @@ -178,8 +178,8 @@ impl TlsConnector { impl TlsConnector { /// Creates a new [`TlsConnector`] which will establish /// a secure connection if the request is to be tunneled. - pub const fn tunnel(inner: S, host: Option) -> Self { - Self::new(inner, ConnectorKindTunnel { host }) + pub const fn tunnel(inner: S, sni: Option) -> Self { + Self::new(inner, ConnectorKindTunnel { sni }) } } @@ -187,7 +187,7 @@ impl TlsConnector { impl Service for TlsConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + Send + ExtensionsMut @@ -221,10 +221,13 @@ where }); } - let host = transport_ctx.authority.host.clone(); + let (connector_data, connector_data_builder) = + self.connector_data(input.extensions(), transport_ctx.authority.host.as_domain())?; - let connector_data = self.connector_data(input.extensions_mut())?; - let (stream, negotiated_params) = handshake(connector_data, host, conn).await?; + // We dont have to insert, but it's nice to have... + input.extensions_mut().insert(connector_data_builder); + + let (stream, negotiated_params) = handshake(connector_data, conn).await?; tracing::trace!( server.address = %transport_ctx.authority.host, @@ -248,7 +251,7 @@ where impl Service for TlsConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + Send + ExtensionsMut @@ -271,10 +274,13 @@ where transport_ctx.app_protocol, ); - let host = transport_ctx.authority.host.clone(); + let (connector_data, connector_data_builder) = + self.connector_data(input.extensions(), transport_ctx.authority.host.as_domain())?; + + // We dont have to insert, but it's nice to have... + input.extensions_mut().insert(connector_data_builder); - let connector_data = self.connector_data(input.extensions_mut())?; - let (conn, negotiated_params) = handshake(connector_data, host, conn).await?; + let (conn, negotiated_params) = handshake(connector_data, conn).await?; let mut conn = TlsStream::new(conn); #[cfg(feature = "http")] @@ -291,7 +297,7 @@ where impl Service for TlsConnector where - S: ConnectorService, + S: ConnectorService, Input: Send + ExtensionsMut + 'static, { type Output = EstablishedClientConnection, Input>; @@ -301,14 +307,12 @@ where let EstablishedClientConnection { mut input, conn } = self.inner.connect(input).await.into_box_error()?; - let host = if let Some(host) = input - .extensions() - .get::() - .as_ref() - .map(|t| &t.server_host) - .or(self.kind.host.as_ref()) - { - host.clone() + let maybe_sni_overwrite = if let Some(tunnel) = input.extensions().get::() { + tunnel + .sni + .as_ref() + .and_then(|h| h.as_domain()) + .or(self.kind.sni.as_ref()) } else { tracing::trace!( "TlsConnector(tunnel): return inner connection: no Tls tunnel is requested" @@ -319,8 +323,13 @@ where }); }; - let connector_data = self.connector_data(input.extensions_mut())?; - let (stream, negotiated_params) = handshake(connector_data, host, conn).await?; + let (connector_data, connector_data_builder) = + self.connector_data(input.extensions(), maybe_sni_overwrite)?; + + // We dont have to insert, but it's nice to have... + input.extensions_mut().insert(connector_data_builder); + + let (stream, negotiated_params) = handshake(connector_data, conn).await?; let mut conn = AutoTlsStream::secure(stream); #[cfg(feature = "http")] @@ -364,7 +373,11 @@ fn set_target_http_version( } impl TlsConnector { - fn connector_data(&self, extensions: &mut Extensions) -> Result { + fn connector_data( + &self, + extensions: &Extensions, + maybe_sni_overwrite: Option<&Domain>, + ) -> Result<(TlsConnectorData, TlsConnectorDataBuilder), BoxError> { #[cfg(feature = "http")] let target_version = extensions .get::() @@ -383,72 +396,75 @@ impl TlsConnector { ); TlsConnectorDataBuilder::default() }; + let has_custom_sni = builder.server_name().is_some(); if let Some(base_builder) = self.connector_data.clone() { tracing::trace!("prepend connector data (base) config to TlsConnectorDataBuilder"); builder.prepend_base_config(base_builder); } + if !has_custom_sni && let Some(sni_overwrite) = maybe_sni_overwrite.cloned() { + builder.set_server_name(sni_overwrite); + } + #[cfg(feature = "http")] if let Some(target_version) = target_version { builder.try_set_rama_alpn_protos(&[target_version])?; } - // We dont have to insert, but it's nice to have... - extensions.insert(builder.clone()); - builder.build() + builder.build().map(|cfg| (cfg, builder)) } } pub async fn tls_connect( - server_host: Host, stream: T, connector_data: Option, ) -> Result, BoxError> where - T: Stream + Unpin + ExtensionsMut, + T: Io + Unpin + ExtensionsMut, { - let data = match connector_data { + let TlsConnectorData { + config, + store_server_certificate_chain: _, + server_name, + } = match connector_data { Some(connector_data) => connector_data, None => TlsConnectorDataBuilder::new().build()?, }; - let server_host = data.server_name.map(Host::Name).unwrap_or(server_host); - let stream: SslStream = - rama_boring_tokio::connect(data.config, &server_host.to_str(), stream) - .await - .map_err(|err| { - let maybe_ssl_code = err.code(); - if let Some(io_err) = err.as_io_error() { - BoxError::from(format!( - "boring ssl connector (connect): with io error: {io_err}" - )) - .context_field("domain", server_host) + let sni = server_name.as_ref().map(|sni| sni.as_str()); + let stream: SslStream = rama_boring_tokio::connect(config, sni, stream) + .await + .map_err(|err| { + let maybe_ssl_code = err.code(); + if let Some(io_err) = err.as_io_error() { + BoxError::from(format!( + "boring ssl connector (connect): with io error: {io_err}" + )) + .context_debug_field("sni", server_name) + .context_debug_field("code", maybe_ssl_code) + } else if let Some(err) = err.as_ssl_error_stack() { + err.context("boring ssl connector (connect): with ssl-error info") + .context_debug_field("sni", server_name) .context_debug_field("code", maybe_ssl_code) - } else if let Some(err) = err.as_ssl_error_stack() { - err.context("boring ssl connector (connect): with ssl-error info") - .context_field("domain", server_host) - .context_debug_field("code", maybe_ssl_code) - } else { - BoxError::from("boring ssl connector (connect): without error info") - .context_field("domain", server_host) - .context_debug_field("code", maybe_ssl_code) - } - })?; + } else { + BoxError::from("boring ssl connector (connect): without error info") + .context_debug_field("sni", server_name) + .context_debug_field("code", maybe_ssl_code) + } + })?; Ok(TlsStream::new(stream)) } async fn handshake( connector_data: TlsConnectorData, - server_host: Host, stream: T, ) -> Result<(SslStream, NegotiatedTlsParameters), BoxError> where - T: Stream + Unpin + ExtensionsMut, + T: Io + Unpin + ExtensionsMut, { let store_server_certificate_chain = connector_data.store_server_certificate_chain; - let TlsStream { inner: stream } = - tls_connect(server_host, stream, Some(connector_data)).await?; + let TlsStream { inner: stream } = tls_connect(stream, Some(connector_data)).await?; let params = match stream.ssl().session() { Some(ssl_session) => { @@ -513,12 +529,12 @@ pub struct ConnectorKindSecure; /// /// The connections will only be done if the [`TlsTunnel`] /// is present in the context for optional versions, -/// and using the hardcoded host otherwise. +/// and using the hardcoded domain otherwise. /// Context always overwrites though. /// /// [`TlsTunnel`]: rama_net::tls::TlsTunnel pub struct ConnectorKindTunnel { - host: Option, + sni: Option, } #[cfg(test)] diff --git a/rama-tls-boring/src/client/mod.rs b/rama-tls-boring/src/client/mod.rs index 3478baaff..6aa515ced 100644 --- a/rama-tls-boring/src/client/mod.rs +++ b/rama-tls-boring/src/client/mod.rs @@ -6,9 +6,6 @@ mod compress_certificate; mod tls_stream_auto; pub use tls_stream_auto::AutoTlsStream; -mod tls_stream; -pub use tls_stream::TlsStream; - pub use rama_boring_tokio::SslStream as BoringTlsStream; mod connector; diff --git a/rama-tls-boring/src/client/tls_stream.rs b/rama-tls-boring/src/client/tls_stream.rs deleted file mode 100644 index 4648e8278..000000000 --- a/rama-tls-boring/src/client/tls_stream.rs +++ /dev/null @@ -1,103 +0,0 @@ -use super::BoringTlsStream; -use pin_project_lite::pin_project; -use rama_boring::ssl::SslRef; -use rama_core::{ - extensions::{Extensions, ExtensionsMut, ExtensionsRef}, - stream::Stream, -}; -use std::fmt; -use tokio::io::{AsyncRead, AsyncWrite}; - -pin_project! { - /// A stream which can be either a secure or a plain stream. - pub struct TlsStream { - #[pin] - pub(super) inner: BoringTlsStream, - } -} - -impl TlsStream { - #[must_use] - pub fn new(inner: BoringTlsStream) -> Self { - Self { inner } - } - - #[must_use] - pub fn ssl_ref(&self) -> &SslRef { - self.inner.ssl() - } -} - -impl fmt::Debug for TlsStream { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("TlsStream") - .field("inner", &self.inner) - .finish() - } -} - -impl ExtensionsRef for TlsStream { - fn extensions(&self) -> &Extensions { - self.inner.get_ref().extensions() - } -} - -impl ExtensionsMut for TlsStream { - fn extensions_mut(&mut self) -> &mut Extensions { - self.inner.get_mut().extensions_mut() - } -} - -#[warn(clippy::missing_trait_methods)] -impl AsyncRead for TlsStream -where - S: Stream + Unpin, -{ - fn poll_read( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> std::task::Poll> { - self.project().inner.poll_read(cx, buf) - } -} - -#[warn(clippy::missing_trait_methods)] -impl AsyncWrite for TlsStream -where - S: Stream + Unpin, -{ - fn poll_write( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> std::task::Poll> { - self.project().inner.poll_write(cx, buf) - } - - fn poll_flush( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.project().inner.poll_flush(cx) - } - - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.project().inner.poll_shutdown(cx) - } - - fn poll_write_vectored( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - bufs: &[std::io::IoSlice<'_>], - ) -> std::task::Poll> { - self.project().inner.poll_write_vectored(cx, bufs) - } - - fn is_write_vectored(&self) -> bool { - self.inner.is_write_vectored() - } -} diff --git a/rama-tls-boring/src/client/tls_stream_auto.rs b/rama-tls-boring/src/client/tls_stream_auto.rs index 48ffec7e7..b8f9d2b11 100644 --- a/rama-tls-boring/src/client/tls_stream_auto.rs +++ b/rama-tls-boring/src/client/tls_stream_auto.rs @@ -3,7 +3,7 @@ use pin_project_lite::pin_project; use rama_boring::ssl::SslRef; use rama_core::{ extensions::{Extensions, ExtensionsMut, ExtensionsRef}, - stream::Stream, + io::Io, }; use std::fmt; use tokio::io::{AsyncRead, AsyncWrite}; @@ -88,7 +88,7 @@ impl ExtensionsMut for AutoTlsStream { #[warn(clippy::missing_trait_methods)] impl AsyncRead for AutoTlsStream where - S: Stream + Unpin, + S: Io + Unpin, { fn poll_read( self: std::pin::Pin<&mut Self>, @@ -105,7 +105,7 @@ where #[warn(clippy::missing_trait_methods)] impl AsyncWrite for AutoTlsStream where - S: Stream + Unpin, + S: Io + Unpin, { fn poll_write( self: std::pin::Pin<&mut Self>, diff --git a/rama-tls-boring/src/lib.rs b/rama-tls-boring/src/lib.rs index 1184779eb..c61e92311 100644 --- a/rama-tls-boring/src/lib.rs +++ b/rama-tls-boring/src/lib.rs @@ -28,11 +28,15 @@ pub struct RamaTlsBoringCrateMarker; pub mod client; +pub mod proxy; pub mod server; pub mod keylog; pub mod type_conversion; +mod tls_stream; +pub use tls_stream::TlsStream; + pub mod types { //! common tls types #[doc(inline)] diff --git a/rama-tls-boring/src/proxy/mitm/issuer/cache.rs b/rama-tls-boring/src/proxy/mitm/issuer/cache.rs new file mode 100644 index 000000000..a1cafc6e4 --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/issuer/cache.rs @@ -0,0 +1,120 @@ +use std::{fmt, num::NonZeroU64, time::Duration}; + +use moka::sync::Cache; +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_core::telemetry::tracing; +use rama_utils::collections::NonEmptyVec; + +use super::BoringMitmCertIssuer; + +#[derive(Debug, Clone)] +/// A [`BoringMitmCertIssuer`] which adds an in-memory +/// caching layer over the internal [`BoringMitmCertIssuer`], +/// allowing to reuse previously issued certs. +pub struct CachedBoringMitmCertIssuer { + issuer: T, + cache: Cache, IssuedCert>, +} + +#[derive(Debug, Clone, Copy)] +/// Config used by to create in-mem cache for [`CachedBoringMitmCertIssuer`] +pub struct BoringMitmCertIssuerCacheConfig { + pub max_size: NonZeroU64, + /// defaults to a default TTL (some) value if `None` is defined, + /// same one as used for `Default::default` + pub ttl: Option, +} + +impl Default for BoringMitmCertIssuerCacheConfig { + fn default() -> Self { + Self { + max_size: CACHE_KIND_DEFAULT_MAX_SIZE, + ttl: Some(CACHE_DEFAULT_TTL), + } + } +} + +const CACHE_DEFAULT_TTL: Duration = Duration::from_hours(24 * 89); // 89 DAYS + +#[allow(clippy::expect_used, reason = "32_000 != 0 🧌")] +const CACHE_KIND_DEFAULT_MAX_SIZE: NonZeroU64 = + NonZeroU64::new(32_000).expect("NonZeroU64: 32_000 != 0"); + +#[derive(Clone)] +struct IssuedCert { + crt_chain: NonEmptyVec, + key: PKey, +} + +impl fmt::Debug for IssuedCert { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("IssuedCert") + .field("crt_chain", &self.crt_chain) + .field("key", &"PKey") + .finish() + } +} + +impl CachedBoringMitmCertIssuer { + #[inline(always)] + /// Create a new [`CachedBoringMitmCertIssuer`]. + #[must_use] + pub fn new(issuer: T) -> Self { + Self::new_with_config(issuer, BoringMitmCertIssuerCacheConfig::default()) + } + + #[inline(always)] + /// Create a new [`CachedBoringMitmCertIssuer`] with the given config. + #[must_use] + pub fn new_with_config(issuer: T, cfg: BoringMitmCertIssuerCacheConfig) -> Self { + Self { + issuer, + cache: Cache::builder() + .time_to_live(match cfg.ttl { + None | Some(Duration::ZERO) => CACHE_DEFAULT_TTL, + Some(custom) => custom, + }) + .max_capacity(cfg.max_size.into()) + .build(), + } + } +} + +impl BoringMitmCertIssuer for CachedBoringMitmCertIssuer { + type Error = T::Error; + + #[inline(always)] + async fn issue_mitm_x509_cert( + &self, + original: X509, + ) -> Result<(NonEmptyVec, PKey), Self::Error> { + let signature = original.signature().as_slice(); + + if let Some(IssuedCert { crt_chain, key }) = self.cache.get(signature) { + tracing::debug!( + "reuse cached x509 cert pair for MITM boring crt issuer (signature: 0x{signature:x?}" + ); + return Ok((crt_chain, key)); + } + + let signature = signature.to_vec(); + let (crt_chain, key) = self.issuer.issue_mitm_x509_cert(original).await?; + + tracing::debug!( + "cached newly issued x509 cert pair for MITM boring crt issuer (signature: 0x{signature:x?}; return copy" + ); + + self.cache.insert( + signature, + IssuedCert { + crt_chain: crt_chain.clone(), + key: key.clone(), + }, + ); + + Ok((crt_chain, key)) + } +} diff --git a/rama-tls-boring/src/proxy/mitm/issuer/deny.rs b/rama-tls-boring/src/proxy/mitm/issuer/deny.rs new file mode 100644 index 000000000..cce152b27 --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/issuer/deny.rs @@ -0,0 +1,39 @@ +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_utils::{collections::NonEmptyVec, macros::error::static_str_error}; + +use super::BoringMitmCertIssuer; + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +/// a [`BoringMitmCertIssuer`] implementation which +/// denies all incoming cert issue requests with a [`CertIssueDeniedError`]. +pub struct DenyBoringMitmCertIssuer; + +impl DenyBoringMitmCertIssuer { + #[inline(always)] + /// Create a new [`Default`] [`DenyBoringMitmCertIssuer`]. + #[must_use] + pub fn new() -> Self { + Self::default() + } +} + +static_str_error! { + #[doc = "cert issueing denied"] + pub struct CertIssueDeniedError; +} + +impl BoringMitmCertIssuer for DenyBoringMitmCertIssuer { + type Error = CertIssueDeniedError; + + fn issue_mitm_x509_cert( + &self, + _: X509, + ) -> impl Future, PKey), Self::Error>> + Send + '_ + { + std::future::ready(Err(CertIssueDeniedError)) + } +} diff --git a/rama-tls-boring/src/proxy/mitm/issuer/either.rs b/rama-tls-boring/src/proxy/mitm/issuer/either.rs new file mode 100644 index 000000000..08d3949c6 --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/issuer/either.rs @@ -0,0 +1,35 @@ +use super::BoringMitmCertIssuer; + +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_utils::collections::NonEmptyVec; + +macro_rules! impl_boring_cert_issuer_either { + ($id:ident, $first:ident $(, $param:ident)* $(,)?) => { + impl<$first, $($param,)*> BoringMitmCertIssuer for rama_core::combinators::$id<$first $(,$param)*> + where + $first: BoringMitmCertIssuer, + $( + $param: BoringMitmCertIssuer>, + )* + { + type Error = $first::Error; + + async fn issue_mitm_x509_cert( + &self, + original: X509, + ) -> Result<(NonEmptyVec, PKey), Self::Error> { + match self { + rama_core::combinators::$id::$first(issuer) => issuer.issue_mitm_x509_cert(original).await, + $( + rama_core::combinators::$id::$param(issuer) => issuer.issue_mitm_x509_cert(original).await.map_err(Into::into), + )* + } + } + } + }; +} + +rama_core::combinators::impl_either!(impl_boring_cert_issuer_either); diff --git a/rama-tls-boring/src/proxy/mitm/issuer/memory.rs b/rama-tls-boring/src/proxy/mitm/issuer/memory.rs new file mode 100644 index 000000000..983839554 --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/issuer/memory.rs @@ -0,0 +1,63 @@ +use std::fmt; + +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_core::error::BoxError; +use rama_net::tls::server::SelfSignedData; +use rama_utils::collections::{NonEmptyVec, non_empty_vec}; + +use crate::server::utils::self_signed_server_auth_gen_ca; + +use super::BoringMitmCertIssuer; + +#[derive(Clone)] +/// A [`BoringMitmCertIssuer`] which mirrors the original reference +/// using its internal (in-memory) CA crt/key pair to sign. +pub struct InMemoryBoringMitmCertIssuer { + ca_crt: X509, + ca_key: PKey, +} + +impl fmt::Debug for InMemoryBoringMitmCertIssuer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("InMemoryBoringMitmCertIssuer") + .field("ca_crt", &self.ca_crt) + .field("ca_key", &"PKey") + .finish() + } +} + +impl InMemoryBoringMitmCertIssuer { + #[inline(always)] + /// Create a new [`InMemoryBoringMitmCertIssuer`]. + #[must_use] + pub fn new(ca_crt: X509, ca_key: PKey) -> Self { + Self { ca_crt, ca_key } + } + + #[inline(always)] + /// Create a new [`InMemoryBoringMitmCertIssuer`] with self-signed CA using the given data. + pub fn try_new_self_signed(data: &SelfSignedData) -> Result { + let (ca_cert, ca_privkey) = self_signed_server_auth_gen_ca(data)?; + Ok(Self::new(ca_cert, ca_privkey)) + } +} + +impl BoringMitmCertIssuer for InMemoryBoringMitmCertIssuer { + type Error = BoxError; + + #[inline(always)] + async fn issue_mitm_x509_cert( + &self, + original: X509, + ) -> Result<(NonEmptyVec, PKey), Self::Error> { + let (crt, key) = crate::server::utils::self_signed_server_auth_mirror_cert( + &original, + &self.ca_crt, + &self.ca_key, + )?; + Ok((non_empty_vec![crt, self.ca_crt.clone()], key)) + } +} diff --git a/rama-tls-boring/src/proxy/mitm/issuer/mod.rs b/rama-tls-boring/src/proxy/mitm/issuer/mod.rs new file mode 100644 index 000000000..93c90a20e --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/issuer/mod.rs @@ -0,0 +1,27 @@ +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_utils::collections::NonEmptyVec; + +mod cache; +mod deny; +mod either; +mod memory; +mod static_pair; + +pub trait BoringMitmCertIssuer: Sized + Send + Sync + 'static { + type Error: Send + 'static; + + fn issue_mitm_x509_cert( + &self, + original: X509, + ) -> impl Future, PKey), Self::Error>> + Send + '_; +} + +pub use self::{ + cache::{BoringMitmCertIssuerCacheConfig, CachedBoringMitmCertIssuer}, + deny::{CertIssueDeniedError, DenyBoringMitmCertIssuer}, + memory::InMemoryBoringMitmCertIssuer, + static_pair::StaticBoringMitmCertIssuer, +}; diff --git a/rama-tls-boring/src/proxy/mitm/issuer/static_pair.rs b/rama-tls-boring/src/proxy/mitm/issuer/static_pair.rs new file mode 100644 index 000000000..c7510b47d --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/issuer/static_pair.rs @@ -0,0 +1,48 @@ +use std::{convert::Infallible, fmt}; + +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_utils::collections::NonEmptyVec; + +use super::BoringMitmCertIssuer; + +#[derive(Clone)] +/// A [`BoringMitmCertIssuer`] which clones its own pair data, +/// for each issued cert, completely ignoring the original reference. +pub struct StaticBoringMitmCertIssuer { + crt_chain: NonEmptyVec, + key: PKey, +} + +impl fmt::Debug for StaticBoringMitmCertIssuer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("StaticBoringMitmCertIssuer") + .field("crt_chain", &self.crt_chain) + .field("key", &"PKey") + .finish() + } +} + +impl StaticBoringMitmCertIssuer { + #[inline(always)] + /// Create a new [`StaticBoringMitmCertIssuer`]. + #[must_use] + pub fn new(crt_chain: NonEmptyVec, key: PKey) -> Self { + Self { crt_chain, key } + } +} + +impl BoringMitmCertIssuer for StaticBoringMitmCertIssuer { + type Error = Infallible; + + #[inline(always)] + fn issue_mitm_x509_cert( + &self, + _: X509, + ) -> impl Future, PKey), Self::Error>> + Send + '_ + { + std::future::ready(Ok((self.crt_chain.clone(), self.key.clone()))) + } +} diff --git a/rama-tls-boring/src/proxy/mitm/mod.rs b/rama-tls-boring/src/proxy/mitm/mod.rs new file mode 100644 index 000000000..f1a9e6223 --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/mod.rs @@ -0,0 +1,347 @@ +use rama_boring::{ + pkey::{PKey, Private}, + x509::X509, +}; +use rama_core::{ + Layer, + conversion::RamaTryInto as _, + error::{BoxError, ErrorContext as _, ErrorExt as _}, + extensions::{self, ExtensionsMut as _}, + io::{BridgeIo, Io}, + telemetry::tracing, +}; +use rama_net::tls::KeyLogIntent; +use rama_net::tls::{ApplicationProtocol, client::NegotiatedTlsParameters, server::SelfSignedData}; +use std::io::{Cursor, ErrorKind}; + +use crate::core::ssl::{AlpnError, SslAcceptor, SslMethod, SslRef}; +use crate::{TlsStream, client, keylog::try_new_key_log_file_handle}; + +pub mod issuer; + +mod service; +pub use self::service::TlsMitmRelayService; + +#[derive(Debug, Clone)] +/// A utility that can be used by MITM services such as transparent proxies, +/// in order to relay (and MITM a TLS connection between a client and server, +/// as part of a deep protocol inspection protocol (DPI) flow. +pub struct TlsMitmRelay { + issuer: Issuer, + grease_enabled: bool, + keylog_intent: KeyLogIntent, +} + +impl TlsMitmRelay { + #[inline(always)] + /// Create a new [`TlsMitmRelay`]. + pub fn new(issuer: Issuer) -> Self { + Self { + issuer, + grease_enabled: true, + keylog_intent: KeyLogIntent::Environment, + } + } + + rama_utils::macros::generate_set_and_with! { + /// Set whether GREASE should be enabled for the ingress-side TLS acceptor. + /// + /// By default is is enabled (true). + pub fn grease_enabled(mut self, enabled: bool) -> Self { + self.grease_enabled = enabled; + self + } + } + + rama_utils::macros::generate_set_and_with! { + /// Set the [`keylog_intent`]. + /// + /// By default [`KeyLogIntent::Environment`] is used. + pub fn keylog_intent(mut self, intent: KeyLogIntent) -> Self { + self.keylog_intent = intent; + self + } + } +} + +impl TlsMitmRelay> { + #[inline(always)] + /// Create a new [`TlsMitmRelay`], + /// with a cache layer on top top of the provided issuer + /// toprovide reuse functionality of previously issued certs. + pub fn new_with_cached_issuer(issuer: Issuer) -> Self { + Self::new(self::issuer::CachedBoringMitmCertIssuer::new(issuer)) + } + + #[inline(always)] + /// Create a new [`TlsMitmRelay`], + /// with a cache layer (created by given config) + /// on top of the provided issuer to provide reuse functionality of previously issued certs. + pub fn new_with_cached_issuer_and_config( + issuer: Issuer, + cfg: self::issuer::BoringMitmCertIssuerCacheConfig, + ) -> Self { + Self::new(self::issuer::CachedBoringMitmCertIssuer::new_with_config( + issuer, cfg, + )) + } +} + +impl TlsMitmRelay { + #[inline(always)] + /// Create a new [`TlsMitmRelay`] with self-signed CA using the given data. + pub fn try_new_with_self_signed_issuer(data: &SelfSignedData) -> Result { + let issuer = self::issuer::InMemoryBoringMitmCertIssuer::try_new_self_signed(data)?; + Ok(Self::new(issuer)) + } + + #[inline(always)] + /// Create a new [`TlsMitmRelay`] with the provided CA pair. + pub fn new_in_memory(crt: X509, key: PKey) -> Self { + let issuer = self::issuer::InMemoryBoringMitmCertIssuer::new(crt, key); + Self::new(issuer) + } +} + +impl + TlsMitmRelay< + self::issuer::CachedBoringMitmCertIssuer, + > +{ + #[inline(always)] + /// Create a new [`TlsMitmRelay`] with self-signed CA using the given data, + /// with a cache layer on top to provide reuse functionality of previously issued certs. + pub fn try_new_with_cached_self_signed_issuer(data: &SelfSignedData) -> Result { + let issuer = self::issuer::InMemoryBoringMitmCertIssuer::try_new_self_signed(data)?; + Ok(Self::new_with_cached_issuer(issuer)) + } + + #[inline(always)] + /// Create a new [`TlsMitmRelay`] with self-signed CA using the given data, + /// with a cache layer (created by given config) + /// on top to provide reuse functionality of previously issued certs. + pub fn try_new_with_cached_self_signed_issuer_and_config( + data: &SelfSignedData, + cfg: self::issuer::BoringMitmCertIssuerCacheConfig, + ) -> Result { + let issuer = self::issuer::InMemoryBoringMitmCertIssuer::try_new_self_signed(data)?; + Ok(Self::new_with_cached_issuer_and_config(issuer, cfg)) + } + + #[inline(always)] + /// Create a new [`TlsMitmRelay`] with the provided CA pair, + /// with a cache layer on top to provide reuse functionality of previously issued certs. + pub fn new_cached_in_memory(crt: X509, key: PKey) -> Self { + let issuer = self::issuer::InMemoryBoringMitmCertIssuer::new(crt, key); + Self::new_with_cached_issuer(issuer) + } + + #[inline(always)] + /// Create a new [`TlsMitmRelay`] with the provided CA pair, + /// with a cache layer (created by given config) + /// on top to provide reuse functionality of previously issued certs. + pub fn new_cached_in_memory_with_config( + crt: X509, + key: PKey, + cfg: self::issuer::BoringMitmCertIssuerCacheConfig, + ) -> Self { + let issuer = self::issuer::InMemoryBoringMitmCertIssuer::new(crt, key); + Self::new_with_cached_issuer_and_config(issuer, cfg) + } +} + +impl TlsMitmRelay +where + Issuer: self::issuer::BoringMitmCertIssuer>, +{ + /// Establish and MITM an handshake between the client (ingress) and server (egress). + pub async fn handshake( + &self, + BridgeIo(ingress_stream, egress_stream): BridgeIo, + connector_data: Option, + ) -> Result, TlsStream>, BoxError> + where + Ingress: Io + Unpin + extensions::ExtensionsMut, + Egress: Io + Unpin + extensions::ExtensionsMut, + { + let store_server_certificate_chain = connector_data + .as_ref() + .map(|cd| cd.store_server_certificate_chain) + .unwrap_or_default(); + + let mut egress_tls_stream = + crate::client::tls_connect(egress_stream, connector_data).await?; + + let egress_ssl_ref = egress_tls_stream.ssl_ref(); + let source_cert = egress_ssl_ref + .peer_certificate() + .ok_or_else(|| BoxError::from("tls mitm relay: egress tls stream has no peer cert"))?; + + let (mirrored_leaf_cert_chain, mirrored_leaf_key) = self + .issuer + .issue_mitm_x509_cert(source_cert) + .await + .context("tls mitm relay: mirror server certificate")?; + + let mut acceptor_builder = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server()) + .context("tls mitm relay: create boring ssl acceptor")?; + acceptor_builder.set_grease_enabled(self.grease_enabled); + acceptor_builder + .set_default_verify_paths() + .context("tls mitm relay: set default verify paths")?; + for (i, crt) in mirrored_leaf_cert_chain.into_iter().enumerate() { + if i == 0 { + acceptor_builder + .set_certificate(crt.as_ref()) + .context("tls mitm relay: set certificate")?; + } else { + acceptor_builder + .add_extra_chain_cert(crt) + .context("tls mitm relay: add chain certificate")?; + } + } + acceptor_builder + .set_private_key(mirrored_leaf_key.as_ref()) + .context("tls mitm relay: set mirrored leaf private key")?; + acceptor_builder + .check_private_key() + .context("tls mitm relay: check mirrored private key")?; + + let maybe_negotiated_params = if let Some(ssl_session) = egress_ssl_ref.session() { + let protocol_version = ssl_session.protocol_version(); + + acceptor_builder + .set_min_proto_version(Some(protocol_version)) + .context("tls mitm relay: set min tls proto version") + .context_field("protocol_version", protocol_version)?; + acceptor_builder + .set_max_proto_version(Some(protocol_version)) + .context("tls mitm relay: set max tls proto version") + .context_field("protocol_version", protocol_version)?; + + let protocol_version = protocol_version.rama_try_into().map_err(|v| { + BoxError::from("boring ssl connector: cast min proto version") + .context_field("protocol_version", v) + })?; + + tracing::debug!( + "boring client (connector) protocol version: {protocol_version} (set as min/max)" + ); + + let application_layer_protocol = egress_ssl_ref + .selected_alpn_protocol() + .map(ApplicationProtocol::from); + + if let Some(selected_alpn_protocol) = application_layer_protocol.clone() { + tracing::debug!( + "boring client (connector) has selected ALPN {selected_alpn_protocol}" + ); + + acceptor_builder.set_alpn_select_callback( + move |_: &mut SslRef, client_alpns: &[u8]| { + let mut reader = Cursor::new(client_alpns); + loop { + let n = reader.position() as usize; + match ApplicationProtocol::decode_wire_format(&mut reader) { + Ok(proto) => { + if proto == selected_alpn_protocol { + let m = reader.position() as usize; + return Ok(&client_alpns[n + 1..m]); + } + } + Err(error) => { + return Err(if error.kind() == ErrorKind::UnexpectedEof { + tracing::debug!( + "failed to find ALPN (Unexpected EOF): {error}; NOACK" + ); + AlpnError::NOACK + } else { + tracing::debug!( + "failed to decode ALPN: {error}; ALERT_FATAL" + ); + AlpnError::ALERT_FATAL + }); + } + } + } + }, + ); + } + + let server_certificate_chain = match store_server_certificate_chain + .then(|| egress_ssl_ref.peer_cert_chain()) + .flatten() + { + Some(chain) => Some(chain.rama_try_into()?), + None => None, + }; + + Some(NegotiatedTlsParameters { + protocol_version, + application_layer_protocol, + peer_certificate_chain: server_certificate_chain, + }) + } else { + None + }; + + if let Some(keylog_filename) = self.keylog_intent.file_path().as_deref() { + let handle = try_new_key_log_file_handle(keylog_filename)?; + acceptor_builder.set_keylog_callback(move |_, line| { + let line = format!("{line}\n"); + handle.write_log_line(line); + }); + } + + tracing::debug!( + protocol = ?egress_ssl_ref.version(), + has_alpn = egress_ssl_ref.selected_alpn_protocol().is_some(), + "tls mitm relay: accepting ingress tls handshake with mirrored server hints", + ); + + let acceptor = acceptor_builder.build(); + let ingress_boring_ssl_stream = rama_boring_tokio::accept(&acceptor, ingress_stream) + .await + .map_err(|err| { + let maybe_ssl_code = err.code(); + if let Some(io_err) = err.as_io_error() { + BoxError::from(format!( + "tls mitm relay: ingress tls accept failed with io error: {io_err}" + )) + } else if let Some(err) = err.as_ssl_error_stack() { + BoxError::from(err).context("tls mitm relay: ingress tls accept ssl error") + } else { + BoxError::from("tls mitm relay: ingress tls accept failed") + } + .context_debug_field("code", maybe_ssl_code) + })?; + + if let Some(negotiated_params) = maybe_negotiated_params { + #[cfg(feature = "http")] + if let Some(proto) = negotiated_params.application_layer_protocol.as_ref() + && let Ok(neg_version) = rama_http_types::Version::try_from(proto) + { + egress_tls_stream + .extensions_mut() + .insert(rama_http_types::conn::TargetHttpVersion(neg_version)); + } + + egress_tls_stream.extensions_mut().insert(negotiated_params); + } + + let ingress_tls_stream = TlsStream::new(ingress_boring_ssl_stream); + Ok(BridgeIo(ingress_tls_stream, egress_tls_stream)) + } +} + +impl Layer for TlsMitmRelay { + type Service = TlsMitmRelayService; + + fn layer(&self, inner: S) -> Self::Service { + TlsMitmRelayService::new(self.clone(), inner) + } + + fn into_layer(self, inner: S) -> Self::Service { + TlsMitmRelayService::new(self, inner) + } +} diff --git a/rama-tls-boring/src/proxy/mitm/service.rs b/rama-tls-boring/src/proxy/mitm/service.rs new file mode 100644 index 000000000..2df1555e0 --- /dev/null +++ b/rama-tls-boring/src/proxy/mitm/service.rs @@ -0,0 +1,99 @@ +use rama_core::{ + Service, + error::{BoxError, ErrorContext as _}, + extensions, + io::{BridgeIo, Io}, + telemetry::tracing, +}; +use rama_net::tls::{client::ServerVerifyMode, server::InputWithClientHello}; + +use crate::{TlsStream, client::TlsConnectorDataBuilder, proxy::TlsMitmRelay}; + +#[derive(Debug, Clone)] +/// A utility that can be used by MITM services such as transparent proxies, +/// in order to relay (and MITM a TLS connection between a client and server, +/// as part of a deep protocol inspection protocol (DPI) flow. +pub struct TlsMitmRelayService { + relay: TlsMitmRelay, + inner: Inner, +} + +impl TlsMitmRelayService { + #[inline(always)] + #[must_use] + /// Create a new [`TlsMitmRelayService`] which is ready to serve, + /// bridged Io streams. It's a [`Service`] (layer) implementation + /// on top of [`TlsMitmRelay`]. + pub fn new(relay: TlsMitmRelay, inner: Inner) -> Self { + Self { relay, inner } + } +} + +impl Service> + for TlsMitmRelayService +where + Issuer: super::issuer::BoringMitmCertIssuer>, + Inner: Service, TlsStream>, Output = (), Error: Into>, + Ingress: Io + Unpin + extensions::ExtensionsMut, + Egress: Io + Unpin + extensions::ExtensionsMut, +{ + type Output = (); + type Error = BoxError; + + async fn serve(&self, input: BridgeIo) -> Result { + let maybe_connector_data = TlsConnectorDataBuilder::default() + .with_server_verify_mode(ServerVerifyMode::Disable) + .build() + .inspect_err(|err| { + tracing::debug!( + "failed to build default TlsConnectorData: {err}; try anyway without data" + ) + }) + .ok(); + + let tls_input = self + .relay + .handshake(input, maybe_connector_data) + .await + .context("tls MITM relay handshake")?; + + self.inner.serve(tls_input).await.map_err(Into::into) + } +} + +impl Service>> + for TlsMitmRelayService +where + Issuer: super::issuer::BoringMitmCertIssuer>, + Inner: Service, TlsStream>, Output = (), Error: Into>, + Ingress: Io + Unpin + extensions::ExtensionsMut, + Egress: Io + Unpin + extensions::ExtensionsMut, +{ + type Output = (); + type Error = BoxError; + + async fn serve( + &self, + InputWithClientHello { + input, + client_hello, + }: InputWithClientHello>, + ) -> Result { + let maybe_connector_data = TlsConnectorDataBuilder::try_from(client_hello) + .unwrap_or_default() + .with_server_verify_mode(ServerVerifyMode::Disable) + .build() + .inspect_err(|err| { + tracing::debug!("failed to build TlsConnectorData (from CH or default): {err}; try anyway without data") + }) + .ok(); + + let tls_input = self + .relay + .handshake(input, maybe_connector_data) + .await + .context("tls MITM relay handshake")?; + + self.inner.serve(tls_input).await.map_err(Into::into) + } +} diff --git a/rama-tls-boring/src/proxy/mod.rs b/rama-tls-boring/src/proxy/mod.rs new file mode 100644 index 000000000..aa14477c8 --- /dev/null +++ b/rama-tls-boring/src/proxy/mod.rs @@ -0,0 +1,29 @@ +//! Boring(ssl) proxy support for Rama. +//! +//! While a proxy can be seen as a combination of a server and a client, +//! this module provides explicit support for certain proxy flows. +//! +//! For example MITM support found in this module +//! is there to facilitate an explicit MITM flow such that +//! high level you have the following handshake: +//! +//! ```plain +//! client | --- client hello (A) ----> | proxy | | server | +//! | | | ------- client hello (B) ---> | | +//! | | | <------ server hello (C) ---- | | +//! | <--- server hello (D) ---- | | | | +//! ``` +//! +//! Where: +//! +//! 1. Client Hello of (B) is based on Client Hello of (A); +//! 2. Server config of (C) is based on server hello of (B); +//! 3. Issued cert for (C) is based on a mirror from the server cert used in (B). +//! +//! NOTE that (1) requires that you provide the CH converted +//! as connector data to the [`TlsMitmRelay`] prior to handshake (relay). +//! In other words, even though it is recommended, it is optional. + +mod mitm; +pub use self::mitm::issuer as cert_issuer; +pub use self::mitm::{TlsMitmRelay, TlsMitmRelayService}; diff --git a/rama-tls-boring/src/server/acceptor_data.rs b/rama-tls-boring/src/server/acceptor_data.rs index efb3d4c1a..3732df99e 100644 --- a/rama-tls-boring/src/server/acceptor_data.rs +++ b/rama-tls-boring/src/server/acceptor_data.rs @@ -1,21 +1,11 @@ use crate::core::{ - asn1::Asn1Time, - bn::{BigNum, MsbOption}, - hash::MessageDigest, nid::Nid, pkey::{PKey, Private}, - rsa::Rsa, - x509::{ - X509, X509NameBuilder, - extension::{BasicConstraints, KeyUsage, SubjectKeyIdentifier}, - }, + x509::X509, }; use moka::sync::Cache; use parking_lot::Mutex; -use rama_boring::{ - ssl::{ClientHello, NameType, SelectCertError, SslAcceptorBuilder, SslRef}, - x509::extension::{AuthorityKeyIdentifier, SubjectAlternativeName}, -}; +use rama_boring::ssl::{ClientHello, NameType, SelectCertError, SslAcceptorBuilder, SslRef}; use rama_boring_tokio::{AsyncSelectCertError, BoxSelectCertFinish}; use rama_core::conversion::RamaTryFrom; use rama_core::error::{BoxError, ErrorContext, ErrorExt as _}; @@ -153,24 +143,30 @@ impl TlsCertSource { let mut client_hello = client_hello; let ssl_ref = client_hello.ssl_mut(); - let domain = to_domain(ssl_ref, server_name.as_ref()).map_err(|err| { - tracing::error!("boring: failed getting host: {err:?}"); - SelectCertError::ERROR - })?; + let maybe_domain = + to_opt_domain(ssl_ref, server_name.as_ref()).map_err(|err| { + tracing::error!("boring: failed getting host: {err:?}"); + SelectCertError::ERROR + })?; - tracing::trace!(%domain, "try to use cached issued cert or generate new one"); - let issued_cert = match &cert_cache { - None => issue_cert_for_ca(&domain, &ca_cert, &ca_key) - .context("fresh issue of cert") - .map_err(|err| { - tracing::error!( - "boring: select certificate callback: issue failed: {err:?}" - ); - SelectCertError::ERROR - })?, - Some(cert_cache) => cert_cache + tracing::trace!( + ?maybe_domain, + "try to use cached issued cert or generate new one" + ); + let issued_cert = match (&cert_cache, maybe_domain.as_ref()) { + (None, _) | (_, None) => { + issue_cert_for_ca(maybe_domain.as_ref(), &ca_cert, &ca_key) + .context("fresh issue of cert") + .map_err(|err| { + tracing::error!( + "boring: select certificate callback: issue failed: {err:?}" + ); + SelectCertError::ERROR + })? + } + (Some(cert_cache), Some(domain)) => cert_cache .try_get_with(domain.clone(), || { - issue_cert_for_ca(&domain, &ca_cert, &ca_key) + issue_cert_for_ca(Some(domain), &ca_cert, &ca_key) }) .map_err(|err| { tracing::error!( @@ -180,12 +176,13 @@ impl TlsCertSource { })?, }; - add_issued_cert_to_ssl_ref(&domain, &issued_cert, ssl_ref).map_err(|err| { - tracing::error!( - "boring: select certificate callback: add certs to ssl ref: {err:?}" - ); - SelectCertError::ERROR - })?; + add_issued_cert_to_ssl_ref(maybe_domain.as_ref(), &issued_cert, ssl_ref) + .map_err(|err| { + tracing::error!( + "boring: select certificate callback: add certs to ssl ref: {err:?}" + ); + SelectCertError::ERROR + })?; Ok(()) }); @@ -206,7 +203,7 @@ impl TlsCertSource { } let ssl_ref = client_hello.ssl_mut(); - let host = to_domain(ssl_ref, server_name.as_ref()).map_err(|err| { + let maybe_host = to_opt_domain(ssl_ref, server_name.as_ref()).map_err(|err| { tracing::error!("boring: failed getting host: {err:?}"); AsyncSelectCertError{} })?; @@ -217,9 +214,9 @@ impl TlsCertSource { let server_name = server_name.clone(); Ok(Box::pin(async move { - let cache_key = issuer.norm_cn(&host).unwrap_or(&host); + let maybe_cache_key = maybe_host.as_ref().map(|host| issuer.norm_cn(host).unwrap_or(host)); - let issued_cert = if let Some(cached_cert) = cert_cache.as_ref().and_then(|cert_cache| cert_cache.get(cache_key)) { + let issued_cert = if let Some(cache_key) = maybe_cache_key && let Some(cached_cert) = cert_cache.as_ref().and_then(|cert_cache| cert_cache.get(cache_key)) { cached_cert } else { let auth_data = issuer.issue_cert(rama_client_hello, server_name).await.map_err(|err| { @@ -232,7 +229,7 @@ impl TlsCertSource { })? }; - if let Some(cert_cache) = cert_cache { + if let Some(cache_key) = maybe_cache_key && let Some(cert_cache) = cert_cache { cert_cache.insert(cache_key.clone(), issued_cert.clone()); } @@ -241,7 +238,7 @@ impl TlsCertSource { let ssl_ref = client_hello.ssl_mut(); add_issued_cert_to_ssl_ref( - &host, + maybe_host.as_ref(), &issued_cert, ssl_ref, ).map_err(|err| { @@ -321,7 +318,7 @@ impl TryFrom for TlsAcceptorData { match data.kind { ServerCertIssuerKind::SelfSigned(data) => { - let (ca_cert, ca_key) = self_signed_server_ca(&data) + let (ca_cert, ca_key) = super::utils::self_signed_server_auth_gen_ca(&data) .context("boring/TlsAcceptorData: CA: self-signed ca")?; TlsCertSourceKind::InMemoryIssuer { cert_cache, @@ -365,26 +362,27 @@ impl TryFrom for TlsAcceptorData { } } -fn to_domain(ssl_ref: &SslRef, server_name: Option<&Domain>) -> Result { +fn to_opt_domain( + ssl_ref: &SslRef, + server_name: Option<&Domain>, +) -> Result, BoxError> { let host = match (ssl_ref.servername(NameType::HOST_NAME), server_name) { (Some(sni), _) => { tracing::trace!("boring: server_name to host: use client SNI: {sni}"); - sni.parse().map_err(|err: BoxError| { + Some(sni.parse().map_err(|err: BoxError| { tracing::warn!("boring: invalid servername received in callback: {err:?}"); err.context("sni parse failed") - })? // from client (e.g. only possibility for SNI proxy) + })?) // from client (e.g. only possibility for SNI proxy) } (_, Some(host)) => { tracing::trace!("boring: server_name {host} not in sni: using context"); - host.clone() // from context (lower prio) + Some(host.clone()) // from context (lower prio) } // We aren't sure if we actually want this logic here or if this should be an error path // We will come back to this once we have some more data about this. (None, None) => { - tracing::warn!( - "boring: no host found in server_name or ctx: defaulting to 'localhost'" - ); - Domain::from_static("localhost") // fallback + tracing::debug!("boring: no host found in server_name or ctx: use None..."); + None } }; Ok(host) @@ -430,12 +428,12 @@ fn server_auth_data_to_private_key_and_ca_chain( } fn issue_cert_for_ca( - domain: &Domain, + domain: Option<&Domain>, ca_cert: &X509, ca_key: &PKey, ) -> Result { - tracing::trace!("generate certs for host {domain} using in-memory ca cert"); - let (cert, key) = self_signed_server_auth_gen_cert( + tracing::trace!("generate certs for domain {domain:?} using in-memory ca cert"); + let (cert, key) = super::utils::self_signed_server_auth_gen_cert( &SelfSignedData { organisation_name: Some( ca_cert @@ -446,14 +444,14 @@ fn issue_cert_for_ca( .map(|s| s.to_string()) .unwrap_or_else(|| "Anonymous".to_owned()), ), - common_name: Some(domain.clone()), + common_name: domain.cloned(), subject_alternative_names: None, }, ca_cert, ca_key, ) .context("issue certs in memory") - .context_field("domain", domain.clone())?; + .with_context_debug_field("domain", || domain.cloned())?; Ok(IssuedCert { cert_chain: vec![cert, ca_cert.clone()], @@ -462,11 +460,11 @@ fn issue_cert_for_ca( } fn add_issued_cert_to_ssl_ref( - domain: &Domain, + domain: Option<&Domain>, issued_cert: &IssuedCert, builder: &mut SslRef, ) -> Result<(), BoxError> { - tracing::trace!("add issued cert for host {domain} to (boring) SslAcceptorBuilder"); + tracing::trace!("add issued cert for host {domain:?} to (boring) SslAcceptorBuilder"); for (i, ca_cert) in issued_cert.cert_chain.iter().enumerate() { if i == 0 { @@ -483,258 +481,18 @@ fn add_issued_cert_to_ssl_ref( builder .set_private_key(issued_cert.key.as_ref()) .context("boring add issue cert to ssl ref: set private key")?; - // builder - // .check() - // .context("build boring ssl acceptor: issued in-mem: check private key")?; Ok(()) } fn self_signed_server_auth(data: &SelfSignedData) -> Result { - let (ca_cert, ca_privkey) = self_signed_server_auth_gen_ca(data).context("self-signed CA")?; - let (cert, privkey) = self_signed_server_auth_gen_cert(data, &ca_cert, &ca_privkey) - .context("self-signed cert using self-signed CA")?; + let (ca_cert, ca_privkey) = + super::utils::self_signed_server_auth_gen_ca(data).context("self-signed CA")?; + let (cert, privkey) = + super::utils::self_signed_server_auth_gen_cert(data, &ca_cert, &ca_privkey) + .context("self-signed cert using self-signed CA")?; Ok(IssuedCert { cert_chain: vec![cert, ca_cert], key: privkey, }) } - -#[inline] -/// Generate a self-signed server CA from the given [`SelfSignedData`]. -/// -/// This should not be used in production but mostly for experimental / testing purposes. -pub fn self_signed_server_ca(data: &SelfSignedData) -> Result<(X509, PKey), BoxError> { - self_signed_server_auth_gen_ca(data) -} - -/// Generate a server cert for the [`SelfSignedData`] using the given CA Cert + Key. -/// -/// In most cases you probably want more refined configuration and controls, -/// so in general we recommend to not use this utility outside of experimental or testing purposes. -pub fn self_signed_server_auth_gen_cert( - data: &SelfSignedData, - ca_cert: &X509, - ca_privkey: &PKey, -) -> Result<(X509, PKey), BoxError> { - let rsa = Rsa::generate(4096).context("generate 4096 RSA key")?; - let privkey = PKey::from_rsa(rsa).context("create private key from 4096 RSA key")?; - - let common_name = data - .common_name - .clone() - .unwrap_or(Domain::from_static("localhost")); - - let mut x509_name = X509NameBuilder::new().context("create x509 name builder")?; - x509_name - .append_entry_by_nid( - Nid::ORGANIZATIONNAME, - data.organisation_name.as_deref().unwrap_or("Anonymous"), - ) - .context("append organisation name to x509 name builder")?; - for subject_alt_name in data.subject_alternative_names.iter().flatten() { - x509_name - .append_entry_by_nid(Nid::SUBJECT_ALT_NAME, subject_alt_name.as_ref()) - .context("append subject alt name to x509 name builder")?; - } - x509_name - .append_entry_by_nid(Nid::COMMONNAME, common_name.as_str()) - .context("append common name to x509 name builder")?; - let x509_name = x509_name.build(); - - let mut cert_builder = X509::builder().context("create x509 (cert) builder")?; - cert_builder - .set_version(2) - .context("x509 cert builder: set version = 2")?; - let serial_number = { - let mut serial = BigNum::new().context("x509 cert builder: create big num (serial")?; - serial - .rand(159, MsbOption::MAYBE_ZERO, false) - .context("x509 cert builder: randomise serial number (big num)")?; - serial - .to_asn1_integer() - .context("x509 cert builder: convert serial to ASN1 integer")? - }; - cert_builder - .set_serial_number(&serial_number) - .context("x509 cert builder: set serial number")?; - cert_builder - .set_issuer_name(ca_cert.subject_name()) - .context("x509 cert builder: set issuer name")?; - cert_builder - .set_pubkey(&privkey) - .context("x509 cert builder: set pub key")?; - cert_builder - .set_subject_name(&x509_name) - .context("x509 cert builder: set subject name")?; - cert_builder - .set_pubkey(&privkey) - .context("x509 cert builder: set public key using private key (ref)")?; - let not_before = - Asn1Time::days_from_now(0).context("x509 cert builder: create ASN1Time for today")?; - cert_builder - .set_not_before(¬_before) - .context("x509 cert builder: set not before to today")?; - let not_after = Asn1Time::days_from_now(90) - .context("x509 cert builder: create ASN1Time for 90 days in future")?; - cert_builder - .set_not_after(¬_after) - .context("x509 cert builder: set not after to 90 days in future")?; - - cert_builder - .append_extension( - BasicConstraints::new() - .build() - .context("x509 cert builder: build basic constraints")? - .as_ref(), - ) - .context("x509 cert builder: add basic constraints as x509 extension")?; - cert_builder - .append_extension( - KeyUsage::new() - .critical() - .non_repudiation() - .digital_signature() - .key_encipherment() - .build() - .context("x509 cert builder: create key usage")? - .as_ref(), - ) - .context("x509 cert builder: add key usage x509 extension")?; - - let mut subject_alt_name = SubjectAlternativeName::new(); - subject_alt_name.dns(common_name.as_str()); - let subject_alt_name = subject_alt_name - .build(&cert_builder.x509v3_context(Some(ca_cert), None)) - .context("x509 cert builder: build subject alt name")?; - - cert_builder - .append_extension(subject_alt_name.as_ref()) - .context("x509 cert builder: add subject alt name")?; - - let subject_key_identifier = SubjectKeyIdentifier::new() - .build(&cert_builder.x509v3_context(Some(ca_cert), None)) - .context("x509 cert builder: build subject key id")?; - cert_builder - .append_extension(subject_key_identifier.as_ref()) - .context("x509 cert builder: add subject key id x509 extension")?; - - let auth_key_identifier = AuthorityKeyIdentifier::new() - .keyid(false) - .issuer(false) - .build(&cert_builder.x509v3_context(Some(ca_cert), None)) - .context("x509 cert builder: build auth key id")?; - cert_builder - .append_extension(auth_key_identifier.as_ref()) - .context("x509 cert builder: set auth key id extension")?; - - cert_builder - .sign(ca_privkey, MessageDigest::sha256()) - .context("x509 cert builder: sign cert")?; - - let cert = cert_builder.build(); - - Ok((cert, privkey)) -} - -fn self_signed_server_auth_gen_ca( - data: &SelfSignedData, -) -> Result<(X509, PKey), BoxError> { - let rsa = Rsa::generate(4096).context("generate 4096 RSA key")?; - let privkey = PKey::from_rsa(rsa).context("create private key from 4096 RSA key")?; - - let common_name = data - .common_name - .clone() - .unwrap_or(Domain::from_static("localhost")); - - let mut x509_name = X509NameBuilder::new().context("create x509 name builder")?; - x509_name - .append_entry_by_nid( - Nid::ORGANIZATIONNAME, - data.organisation_name.as_deref().unwrap_or("Anonymous"), - ) - .context("append organisation name to x509 name builder")?; - for subject_alt_name in data.subject_alternative_names.iter().flatten() { - x509_name - .append_entry_by_nid(Nid::SUBJECT_ALT_NAME, subject_alt_name.as_ref()) - .context("append subject alt name to x509 name builder")?; - } - x509_name - .append_entry_by_nid(Nid::COMMONNAME, common_name.as_str()) - .context("append common name to x509 name builder")?; - let x509_name = x509_name.build(); - - let mut ca_cert_builder = X509::builder().context("create x509 (cert) builder")?; - ca_cert_builder - .set_version(2) - .context("x509 cert builder: set version = 2")?; - let serial_number = { - let mut serial = BigNum::new().context("x509 cert builder: create big num (serial")?; - serial - .rand(159, MsbOption::MAYBE_ZERO, false) - .context("x509 cert builder: randomise serial number (big num)")?; - serial - .to_asn1_integer() - .context("x509 cert builder: convert serial to ASN1 integer")? - }; - ca_cert_builder - .set_serial_number(&serial_number) - .context("x509 cert builder: set serial number")?; - ca_cert_builder - .set_subject_name(&x509_name) - .context("x509 cert builder: set subject name")?; - ca_cert_builder - .set_issuer_name(&x509_name) - .context("x509 cert builder: set issuer (self-signed")?; - ca_cert_builder - .set_pubkey(&privkey) - .context("x509 cert builder: set public key using private key (ref)")?; - let not_before = - Asn1Time::days_from_now(0).context("x509 cert builder: create ASN1Time for today")?; - ca_cert_builder - .set_not_before(¬_before) - .context("x509 cert builder: set not before to today")?; - let not_after = Asn1Time::days_from_now(365 * 20) - .context("x509 cert builder: create ASN1Time for 20 years in future")?; - ca_cert_builder - .set_not_after(¬_after) - .context("x509 cert builder: set not after to 20 years in future")?; - - ca_cert_builder - .append_extension( - BasicConstraints::new() - .critical() - .ca() - .build() - .context("x509 cert builder: build basic constraints")? - .as_ref(), - ) - .context("x509 cert builder: add basic constraints as x509 extension")?; - ca_cert_builder - .append_extension( - KeyUsage::new() - .critical() - .key_cert_sign() - .crl_sign() - .build() - .context("x509 cert builder: create key usage")? - .as_ref(), - ) - .context("x509 cert builder: add key usage x509 extension")?; - - let subject_key_identifier = SubjectKeyIdentifier::new() - .build(&ca_cert_builder.x509v3_context(None, None)) - .context("x509 cert builder: build subject key id")?; - ca_cert_builder - .append_extension(subject_key_identifier.as_ref()) - .context("x509 cert builder: add subject key id x509 extension")?; - - ca_cert_builder - .sign(&privkey, MessageDigest::sha256()) - .context("x509 cert builder: sign cert")?; - - let cert = ca_cert_builder.build(); - - Ok((cert, privkey)) -} diff --git a/rama-tls-boring/src/server/mod.rs b/rama-tls-boring/src/server/mod.rs index 2dbe0c439..1f5a9a515 100644 --- a/rama-tls-boring/src/server/mod.rs +++ b/rama-tls-boring/src/server/mod.rs @@ -14,11 +14,7 @@ mod acceptor_data; #[doc(inline)] pub use acceptor_data::TlsAcceptorData; -pub mod utils { - //! Server Utilities - - pub use super::acceptor_data::{self_signed_server_auth_gen_cert, self_signed_server_ca}; -} +pub mod utils; mod service; #[doc(inline)] @@ -27,6 +23,3 @@ pub use service::TlsAcceptorService; mod layer; #[doc(inline)] pub use layer::TlsAcceptorLayer; - -mod tls_stream; -pub use tls_stream::TlsStream; diff --git a/rama-tls-boring/src/server/service.rs b/rama-tls-boring/src/server/service.rs index e2f553312..2979c3abc 100644 --- a/rama-tls-boring/src/server/service.rs +++ b/rama-tls-boring/src/server/service.rs @@ -1,8 +1,8 @@ use super::TlsAcceptorData; use crate::{ + TlsStream, core::ssl::{AlpnError, SslAcceptor, SslMethod, SslRef}, keylog::try_new_key_log_file_handle, - server::TlsStream, types::SecureTransport, }; use parking_lot::Mutex; @@ -11,7 +11,7 @@ use rama_core::{ conversion::RamaTryInto, error::{BoxError, ErrorContext, ErrorExt}, extensions::ExtensionsMut, - stream::Stream, + io::Io, telemetry::tracing::{debug, trace}, }; use rama_net::{ @@ -44,9 +44,12 @@ impl TlsAcceptorService { define_inner_service_accessors!(); } +// TODO provide stand-alone handshake based on pre-built acceptor... +// we need this acceptor based on server hello if possible + impl Service for TlsAcceptorService where - IO: Stream + Unpin + ExtensionsMut + 'static, + IO: Io + Unpin + ExtensionsMut + 'static, S: Service, Error: Into>, { type Output = S::Output; diff --git a/rama-tls-boring/src/server/utils/certs.rs b/rama-tls-boring/src/server/utils/certs.rs new file mode 100644 index 000000000..7304a5fbd --- /dev/null +++ b/rama-tls-boring/src/server/utils/certs.rs @@ -0,0 +1,395 @@ +use crate::core::{ + asn1::Asn1Time, + bn::{BigNum, MsbOption}, + dsa::Dsa, + ec::{EcGroup, EcKey}, + hash::MessageDigest, + nid::Nid, + pkey::{Id, PKey, Private}, + rand::rand_bytes, + rsa::Rsa, + x509::{ + X509, X509NameBuilder, X509Ref, + extension::{BasicConstraints, KeyUsage, SubjectKeyIdentifier}, + }, +}; +use rama_boring::x509::extension::{AuthorityKeyIdentifier, SubjectAlternativeName}; +use rama_core::error::{BoxError, ErrorContext}; +use rama_core::telemetry::tracing; +use rama_net::{address::Domain, tls::server::SelfSignedData}; + +/// Generate a server cert for the [`SelfSignedData`] using the given CA Cert + Key. +/// +/// In most cases you probably want more refined configuration and controls, +/// so in general we recommend to not use this utility outside of experimental or testing purposes. +pub fn self_signed_server_auth_gen_cert( + data: &SelfSignedData, + ca_cert: &X509, + ca_privkey: &PKey, +) -> Result<(X509, PKey), BoxError> { + let rsa = Rsa::generate(4096).context("generate 4096 RSA key")?; + let privkey = PKey::from_rsa(rsa).context("create private key from 4096 RSA key")?; + + let common_name = data + .common_name + .clone() + .unwrap_or(Domain::from_static("localhost")); + + let mut x509_name = X509NameBuilder::new().context("create x509 name builder")?; + x509_name + .append_entry_by_nid( + Nid::ORGANIZATIONNAME, + data.organisation_name.as_deref().unwrap_or("Anonymous"), + ) + .context("append organisation name to x509 name builder")?; + for subject_alt_name in data.subject_alternative_names.iter().flatten() { + x509_name + .append_entry_by_nid(Nid::SUBJECT_ALT_NAME, subject_alt_name.as_ref()) + .context("append subject alt name to x509 name builder")?; + } + x509_name + .append_entry_by_nid(Nid::COMMONNAME, common_name.as_str()) + .context("append common name to x509 name builder")?; + let x509_name = x509_name.build(); + + let mut cert_builder = X509::builder().context("create x509 (cert) builder")?; + cert_builder + .set_version(2) + .context("x509 cert builder: set version = 2")?; + let serial_number = { + let mut serial = BigNum::new().context("x509 cert builder: create big num (serial")?; + serial + .rand(159, MsbOption::MAYBE_ZERO, false) + .context("x509 cert builder: randomise serial number (big num)")?; + serial + .to_asn1_integer() + .context("x509 cert builder: convert serial to ASN1 integer")? + }; + cert_builder + .set_serial_number(&serial_number) + .context("x509 cert builder: set serial number")?; + cert_builder + .set_issuer_name(ca_cert.subject_name()) + .context("x509 cert builder: set issuer name")?; + cert_builder + .set_pubkey(&privkey) + .context("x509 cert builder: set pub key")?; + cert_builder + .set_subject_name(&x509_name) + .context("x509 cert builder: set subject name")?; + cert_builder + .set_pubkey(&privkey) + .context("x509 cert builder: set public key using private key (ref)")?; + let not_before = + Asn1Time::days_from_now(0).context("x509 cert builder: create ASN1Time for today")?; + cert_builder + .set_not_before(¬_before) + .context("x509 cert builder: set not before to today")?; + let not_after = Asn1Time::days_from_now(90) + .context("x509 cert builder: create ASN1Time for 90 days in future")?; + cert_builder + .set_not_after(¬_after) + .context("x509 cert builder: set not after to 90 days in future")?; + + cert_builder + .append_extension( + BasicConstraints::new() + .build() + .context("x509 cert builder: build basic constraints")? + .as_ref(), + ) + .context("x509 cert builder: add basic constraints as x509 extension")?; + cert_builder + .append_extension( + KeyUsage::new() + .critical() + .non_repudiation() + .digital_signature() + .key_encipherment() + .build() + .context("x509 cert builder: create key usage")? + .as_ref(), + ) + .context("x509 cert builder: add key usage x509 extension")?; + + let mut subject_alt_name = SubjectAlternativeName::new(); + subject_alt_name.dns(common_name.as_str()); + let subject_alt_name = subject_alt_name + .build(&cert_builder.x509v3_context(Some(ca_cert), None)) + .context("x509 cert builder: build subject alt name")?; + + cert_builder + .append_extension(subject_alt_name.as_ref()) + .context("x509 cert builder: add subject alt name")?; + + let subject_key_identifier = SubjectKeyIdentifier::new() + .build(&cert_builder.x509v3_context(Some(ca_cert), None)) + .context("x509 cert builder: build subject key id")?; + cert_builder + .append_extension(subject_key_identifier.as_ref()) + .context("x509 cert builder: add subject key id x509 extension")?; + + let auth_key_identifier = AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&cert_builder.x509v3_context(Some(ca_cert), None)) + .context("x509 cert builder: build auth key id")?; + cert_builder + .append_extension(auth_key_identifier.as_ref()) + .context("x509 cert builder: set auth key id extension")?; + + cert_builder + .sign(ca_privkey, MessageDigest::sha256()) + .context("x509 cert builder: sign cert")?; + + let cert = cert_builder.build(); + + Ok((cert, privkey)) +} + +/// Generate a mirrored server certificate based on a source certificate. +/// +/// The generated certificate mirrors identity data from `source_cert` (subject and SAN, when +/// present), but is signed by the provided `ca_cert` + `ca_privkey`. +pub fn self_signed_server_auth_mirror_cert( + source_cert: &X509Ref, + ca_cert: &X509, + ca_privkey: &PKey, +) -> Result<(X509, PKey), BoxError> { + let source_pubkey = source_cert + .public_key() + .context("x509 cert builder: read source public key")?; + let privkey = match source_pubkey.id() { + Id::RSA | Id::RSAPSS => { + let bits = source_pubkey.bits().max(2048); + let rsa = + Rsa::generate(bits).with_context(|| format!("generate {bits}-bit RSA key"))?; + PKey::from_rsa(rsa) + .with_context(|| format!("create private key from {bits}-bit RSA key"))? + } + Id::EC => { + let source_ec_key = source_pubkey + .ec_key() + .context("x509 cert builder: read source EC key")?; + let source_curve = source_ec_key + .group() + .curve_name() + .context("x509 cert builder: source EC key has unnamed curve")?; + let group = EcGroup::from_curve_name(source_curve) + .context("x509 cert builder: create mirrored EC group")?; + let ec_key = + EcKey::generate(&group).context("x509 cert builder: generate mirrored EC key")?; + PKey::from_ec_key(ec_key) + .context("x509 cert builder: create private key from EC key")? + } + Id::DSA => { + let bits = source_pubkey.bits().max(2048); + let dsa = + Dsa::generate(bits).with_context(|| format!("generate {bits}-bit DSA key"))?; + PKey::from_dsa(dsa) + .with_context(|| format!("create private key from {bits}-bit DSA key"))? + } + Id::ED25519 => { + let mut key = [0_u8; 32]; + rand_bytes(&mut key).context("generate Ed25519 private key bytes")?; + PKey::from_ed25519_private_key(&key) + .context("create private key from Ed25519 key bytes")? + } + Id::X25519 => { + let mut key = [0_u8; 32]; + rand_bytes(&mut key).context("generate X25519 private key bytes")?; + PKey::from_x25519_private_key(&key) + .context("create private key from X25519 key bytes")? + } + other => { + tracing::debug!( + key_type = ?other, + "source certificate key type not mirror-supported yet; falling back to RSA-2048" + ); + let rsa = Rsa::generate(2048).context("generate fallback 2048 RSA key")?; + PKey::from_rsa(rsa).context("create private key from fallback 2048 RSA key")? + } + }; + + let mut cert_builder = X509::builder().context("create x509 (cert) builder")?; + cert_builder + .set_version(2) + .context("x509 cert builder: set version = 2")?; + let serial_number = { + let mut serial = BigNum::new().context("x509 cert builder: create big num (serial")?; + serial + .rand(159, MsbOption::MAYBE_ZERO, false) + .context("x509 cert builder: randomise serial number (big num)")?; + serial + .to_asn1_integer() + .context("x509 cert builder: convert serial to ASN1 integer")? + }; + cert_builder + .set_serial_number(&serial_number) + .context("x509 cert builder: set serial number")?; + cert_builder + .set_issuer_name(ca_cert.subject_name()) + .context("x509 cert builder: set issuer name from CA")?; + cert_builder + .set_subject_name(source_cert.subject_name()) + .context("x509 cert builder: set mirrored subject name")?; + cert_builder + .set_pubkey(&privkey) + .context("x509 cert builder: set public key using generated private key (ref)")?; + + cert_builder + .set_not_before(source_cert.not_before()) + .context("x509 cert builder: mirror source not-before")?; + cert_builder + .set_not_after(source_cert.not_after()) + .context("x509 cert builder: mirror source not-after")?; + + for source_ext in source_cert.extensions() { + let ext_nid = source_ext.object().nid(); + if ext_nid == Nid::SUBJECT_KEY_IDENTIFIER || ext_nid == Nid::AUTHORITY_KEY_IDENTIFIER { + tracing::trace!( + ?ext_nid, + "skip source key identifier extension (will regenerate)" + ); + continue; + } + + cert_builder + .append_extension_der_payload( + source_ext.object(), + source_ext.critical(), + source_ext.data().as_slice(), + ) + .context("x509 cert builder: append mirrored source extension")?; + } + + let subject_key_identifier = SubjectKeyIdentifier::new() + .build(&cert_builder.x509v3_context(Some(ca_cert), None)) + .context("x509 cert builder: build mirrored subject key identifier")?; + cert_builder + .append_extension(subject_key_identifier.as_ref()) + .context("x509 cert builder: append mirrored subject key identifier")?; + + let auth_key_identifier = AuthorityKeyIdentifier::new() + .keyid(false) + .issuer(false) + .build(&cert_builder.x509v3_context(Some(ca_cert), None)) + .context("x509 cert builder: build mirrored authority key identifier")?; + cert_builder + .append_extension(auth_key_identifier.as_ref()) + .context("x509 cert builder: append mirrored authority key identifier")?; + + cert_builder + .sign(ca_privkey, MessageDigest::sha256()) + .context("x509 cert builder: sign mirrored cert")?; + + Ok((cert_builder.build(), privkey)) +} + +/// Generate a self-signed server CA from the given [`SelfSignedData`]. +/// +/// This should not be used in production but mostly for experimental / testing purposes. +pub fn self_signed_server_auth_gen_ca( + data: &SelfSignedData, +) -> Result<(X509, PKey), BoxError> { + let rsa = Rsa::generate(4096).context("generate 4096 RSA key")?; + let privkey = PKey::from_rsa(rsa).context("create private key from 4096 RSA key")?; + + let mut x509_name = X509NameBuilder::new().context("create x509 name builder")?; + x509_name + .append_entry_by_nid( + Nid::ORGANIZATIONNAME, + data.organisation_name.as_deref().unwrap_or("Anonymous"), + ) + .context("append organisation name to x509 name builder")?; + for subject_alt_name in data.subject_alternative_names.iter().flatten() { + x509_name + .append_entry_by_nid(Nid::SUBJECT_ALT_NAME, subject_alt_name.as_ref()) + .context("append subject alt name to x509 name builder")?; + } + + if let Some(cn) = data.common_name.as_ref() { + x509_name + .append_entry_by_nid(Nid::COMMONNAME, cn.as_str()) + .context("append common name to x509 name builder")?; + } + + let x509_name = x509_name.build(); + + let mut ca_cert_builder = X509::builder().context("create x509 (cert) builder")?; + ca_cert_builder + .set_version(2) + .context("x509 cert builder: set version = 2")?; + let serial_number = { + let mut serial = BigNum::new().context("x509 cert builder: create big num (serial")?; + serial + .rand(159, MsbOption::MAYBE_ZERO, false) + .context("x509 cert builder: randomise serial number (big num)")?; + serial + .to_asn1_integer() + .context("x509 cert builder: convert serial to ASN1 integer")? + }; + ca_cert_builder + .set_serial_number(&serial_number) + .context("x509 cert builder: set serial number")?; + ca_cert_builder + .set_subject_name(&x509_name) + .context("x509 cert builder: set subject name")?; + ca_cert_builder + .set_issuer_name(&x509_name) + .context("x509 cert builder: set issuer (self-signed")?; + ca_cert_builder + .set_pubkey(&privkey) + .context("x509 cert builder: set public key using private key (ref)")?; + let not_before = + Asn1Time::days_from_now(0).context("x509 cert builder: create ASN1Time for today")?; + ca_cert_builder + .set_not_before(¬_before) + .context("x509 cert builder: set not before to today")?; + let not_after = Asn1Time::days_from_now(365 * 20) + .context("x509 cert builder: create ASN1Time for 20 years in future")?; + ca_cert_builder + .set_not_after(¬_after) + .context("x509 cert builder: set not after to 20 years in future")?; + + ca_cert_builder + .append_extension( + BasicConstraints::new() + .critical() + .ca() + .build() + .context("x509 cert builder: build basic constraints")? + .as_ref(), + ) + .context("x509 cert builder: add basic constraints as x509 extension")?; + ca_cert_builder + .append_extension( + KeyUsage::new() + .critical() + .key_cert_sign() + .crl_sign() + .build() + .context("x509 cert builder: create key usage")? + .as_ref(), + ) + .context("x509 cert builder: add key usage x509 extension")?; + + let subject_key_identifier = SubjectKeyIdentifier::new() + .build(&ca_cert_builder.x509v3_context(None, None)) + .context("x509 cert builder: build subject key id")?; + ca_cert_builder + .append_extension(subject_key_identifier.as_ref()) + .context("x509 cert builder: add subject key id x509 extension")?; + + ca_cert_builder + .sign(&privkey, MessageDigest::sha256()) + .context("x509 cert builder: sign cert")?; + + let cert = ca_cert_builder.build(); + + Ok((cert, privkey)) +} + +#[cfg(test)] +#[path = "./certs_tests.rs"] +mod certs_tests; diff --git a/rama-tls-boring/src/server/utils/certs_tests.rs b/rama-tls-boring/src/server/utils/certs_tests.rs new file mode 100644 index 000000000..c2b3c6f1f --- /dev/null +++ b/rama-tls-boring/src/server/utils/certs_tests.rs @@ -0,0 +1,233 @@ +use super::*; + +use crate::core::{ + ec::{EcGroup, EcKey}, + nid::Nid, + pkey::Id, + x509::X509NameBuilder, +}; +use rama_net::{address::Domain, tls::server::SelfSignedData}; + +fn sample_data(common_name: &'static str) -> SelfSignedData { + SelfSignedData { + common_name: Some(Domain::from_static(common_name)), + organisation_name: Some("Rama Test".to_owned()), + ..Default::default() + } +} + +fn ext_by_nid(cert: &X509Ref, nid: Nid) -> Vec<&crate::core::x509::X509ExtensionRef> { + cert.extensions() + .filter(|ext| ext.object().nid() == nid) + .collect() +} + +fn build_self_signed_source_with_pkey( + pkey: &PKey, + common_name: &str, +) -> Result { + let mut x509_name = X509NameBuilder::new().context("create x509 name builder")?; + x509_name + .append_entry_by_nid(Nid::COMMONNAME, common_name) + .context("append common name to x509 name builder")?; + let x509_name = x509_name.build(); + + let mut cert_builder = X509::builder().context("create x509 cert builder")?; + cert_builder + .set_version(2) + .context("set version on source cert")?; + let serial_number = { + let mut serial = BigNum::new().context("create source serial big num")?; + serial + .rand(159, MsbOption::MAYBE_ZERO, false) + .context("randomise source serial")?; + serial + .to_asn1_integer() + .context("convert source serial to asn1 integer")? + }; + cert_builder + .set_serial_number(&serial_number) + .context("set source serial number")?; + cert_builder + .set_subject_name(&x509_name) + .context("set source subject")?; + cert_builder + .set_issuer_name(&x509_name) + .context("set source issuer")?; + cert_builder + .set_pubkey(pkey) + .context("set source public key")?; + let not_before = Asn1Time::days_from_now(0).context("source not before")?; + cert_builder + .set_not_before(¬_before) + .context("set source not before")?; + let not_after = Asn1Time::days_from_now(30).context("source not after")?; + cert_builder + .set_not_after(¬_after) + .context("set source not after")?; + + let san = SubjectAlternativeName::new() + .dns(common_name) + .build(&cert_builder.x509v3_context(None, None)) + .context("build source san")?; + cert_builder + .append_extension(san.as_ref()) + .context("append source san")?; + + cert_builder + .sign(pkey, MessageDigest::sha256()) + .context("sign source cert")?; + + Ok(cert_builder.build()) +} + +#[test] +fn gen_ca_basics() { + let data = sample_data("ca.rama.test"); + let (ca_cert, ca_key) = self_signed_server_auth_gen_ca(&data).expect("generate CA"); + + assert_eq!(ca_key.id(), Id::RSA); + assert!(ca_cert.verify(&ca_key).expect("verify self-signed ca cert")); + assert_eq!( + ca_cert.subject_name().to_der().expect("ca subject der"), + ca_cert.issuer_name().to_der().expect("ca issuer der") + ); + + let basic_constraints = ext_by_nid(ca_cert.as_ref(), Nid::BASIC_CONSTRAINTS); + assert_eq!(basic_constraints.len(), 1); + assert!(basic_constraints[0].critical()); + let key_usage = ext_by_nid(ca_cert.as_ref(), Nid::KEY_USAGE); + assert_eq!(key_usage.len(), 1); + assert!(key_usage[0].critical()); +} + +#[test] +fn gen_leaf_signed_by_ca_and_has_common_name_san() { + let ca_data = sample_data("ca.rama.test"); + let (ca_cert, ca_key) = self_signed_server_auth_gen_ca(&ca_data).expect("generate CA"); + + let leaf_data = sample_data("leaf.rama.test"); + let (leaf_cert, leaf_key) = + self_signed_server_auth_gen_cert(&leaf_data, &ca_cert, &ca_key).expect("generate leaf"); + + assert_eq!(leaf_key.id(), Id::RSA); + assert_eq!(ca_cert.issued(&leaf_cert), Ok(())); + assert_eq!( + leaf_cert.issuer_name().to_der().expect("leaf issuer der"), + ca_cert.subject_name().to_der().expect("ca subject der") + ); + + let cn = leaf_cert + .subject_name() + .entries_by_nid(Nid::COMMONNAME) + .next() + .expect("leaf common name"); + assert_eq!( + cn.data() + .as_utf8() + .expect("leaf common name utf8") + .to_string(), + "leaf.rama.test" + ); + + let san = leaf_cert.subject_alt_names().expect("leaf SAN"); + assert!( + san.iter() + .any(|name| name.dnsname() == Some("leaf.rama.test")) + ); +} + +#[test] +fn mirror_preserves_subject_validity_and_issuer() { + let ca_data = sample_data("ca.rama.test"); + let (ca_cert, ca_key) = self_signed_server_auth_gen_ca(&ca_data).expect("generate CA"); + let source_data = sample_data("source.rama.test"); + let (source_cert, _) = + self_signed_server_auth_gen_cert(&source_data, &ca_cert, &ca_key).expect("source cert"); + + let (mirrored_cert, mirrored_key) = + self_signed_server_auth_mirror_cert(source_cert.as_ref(), &ca_cert, &ca_key) + .expect("mirror cert"); + + assert_eq!(ca_cert.issued(&mirrored_cert), Ok(())); + assert_eq!( + source_cert + .subject_name() + .to_der() + .expect("source subject der"), + mirrored_cert + .subject_name() + .to_der() + .expect("mirrored subject der") + ); + assert_eq!(source_cert.not_before(), mirrored_cert.not_before()); + assert_eq!(source_cert.not_after(), mirrored_cert.not_after()); + assert_eq!( + mirrored_cert + .issuer_name() + .to_der() + .expect("mirrored issuer der"), + ca_cert.subject_name().to_der().expect("ca subject der") + ); + assert_eq!(mirrored_key.id(), Id::RSA); +} + +#[test] +fn mirror_copies_extensions_and_regenerates_key_ids() { + let ca_data = sample_data("ca.rama.test"); + let (ca_cert, ca_key) = self_signed_server_auth_gen_ca(&ca_data).expect("generate CA"); + let source_data = sample_data("source.rama.test"); + let (source_cert, _) = + self_signed_server_auth_gen_cert(&source_data, &ca_cert, &ca_key).expect("source cert"); + let (mirrored_cert, _) = + self_signed_server_auth_mirror_cert(source_cert.as_ref(), &ca_cert, &ca_key) + .expect("mirror cert"); + + let source_exts: Vec<_> = source_cert.extensions().collect(); + let mirrored_exts: Vec<_> = mirrored_cert.extensions().collect(); + + for source_ext in source_exts { + let nid = source_ext.object().nid(); + if nid == Nid::SUBJECT_KEY_IDENTIFIER || nid == Nid::AUTHORITY_KEY_IDENTIFIER { + continue; + } + let found = mirrored_exts.iter().any(|mirrored_ext| { + mirrored_ext.object().nid() == nid + && mirrored_ext.critical() == source_ext.critical() + && mirrored_ext.data().as_slice() == source_ext.data().as_slice() + }); + assert!(found, "missing mirrored extension for nid={nid:?}"); + } + + let source_skid = ext_by_nid(source_cert.as_ref(), Nid::SUBJECT_KEY_IDENTIFIER); + let mirror_skid = ext_by_nid(mirrored_cert.as_ref(), Nid::SUBJECT_KEY_IDENTIFIER); + assert_eq!(source_skid.len(), 1); + assert_eq!(mirror_skid.len(), 1); + assert_ne!( + source_skid[0].data().as_slice(), + mirror_skid[0].data().as_slice() + ); + + let source_akid = ext_by_nid(source_cert.as_ref(), Nid::AUTHORITY_KEY_IDENTIFIER); + let mirror_akid = ext_by_nid(mirrored_cert.as_ref(), Nid::AUTHORITY_KEY_IDENTIFIER); + assert_eq!(source_akid.len(), 1); + assert_eq!(mirror_akid.len(), 1); +} + +#[test] +fn mirror_uses_ec_key_for_ec_source() { + let ca_data = sample_data("ca.rama.test"); + let (ca_cert, ca_key) = self_signed_server_auth_gen_ca(&ca_data).expect("generate CA"); + + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).expect("ec group"); + let ec_key = EcKey::generate(&group).expect("generate ec key"); + let source_key = PKey::from_ec_key(ec_key).expect("pkey from ec key"); + let source_cert = build_self_signed_source_with_pkey(&source_key, "ec-source.rama.test") + .expect("source cert"); + + let (_, mirrored_key) = + self_signed_server_auth_mirror_cert(source_cert.as_ref(), &ca_cert, &ca_key) + .expect("mirror cert"); + + assert_eq!(mirrored_key.id(), Id::EC); +} diff --git a/rama-tls-boring/src/server/utils/mod.rs b/rama-tls-boring/src/server/utils/mod.rs new file mode 100644 index 000000000..3bc80ca99 --- /dev/null +++ b/rama-tls-boring/src/server/utils/mod.rs @@ -0,0 +1,7 @@ +//! Server Utilities + +mod certs; +pub use self::certs::{ + self_signed_server_auth_gen_ca, self_signed_server_auth_gen_cert, + self_signed_server_auth_mirror_cert, +}; diff --git a/rama-tls-boring/src/tests/e2e.rs b/rama-tls-boring/src/tests/e2e.rs index 8d6b0d100..08fe13949 100644 --- a/rama-tls-boring/src/tests/e2e.rs +++ b/rama-tls-boring/src/tests/e2e.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use rama_boring::ssl::SslCurve; use rama_core::{Layer, Service as _, ServiceInput, telemetry::tracing}; use rama_net::{ - address::Host, stream::service::EchoService, tls::{ client::ServerVerifyMode, @@ -56,7 +55,6 @@ async fn test_assumed_default_group_id_support() { }); let mut stream = tls_connect( - Host::EXAMPLE_NAME, ServiceInput::new(stream_client), Some( TlsConnectorDataBuilder::new() diff --git a/rama-tls-boring/src/server/tls_stream.rs b/rama-tls-boring/src/tls_stream.rs similarity index 97% rename from rama-tls-boring/src/server/tls_stream.rs rename to rama-tls-boring/src/tls_stream.rs index 7942cb918..10fd04f2d 100644 --- a/rama-tls-boring/src/server/tls_stream.rs +++ b/rama-tls-boring/src/tls_stream.rs @@ -6,7 +6,7 @@ use rama_boring_tokio::SslStream; use rama_core::{ extensions::Extensions, extensions::{ExtensionsMut, ExtensionsRef}, - stream::Stream, + io::Io, }; use tokio::io::{AsyncRead, AsyncWrite}; @@ -53,7 +53,7 @@ impl ExtensionsMut for TlsStream { #[warn(clippy::missing_trait_methods)] impl AsyncRead for TlsStream where - S: Stream + Unpin, + S: Io + Unpin, { fn poll_read( self: std::pin::Pin<&mut Self>, @@ -67,7 +67,7 @@ where #[warn(clippy::missing_trait_methods)] impl AsyncWrite for TlsStream where - S: Stream + Unpin, + S: Io + Unpin, { fn poll_write( self: std::pin::Pin<&mut Self>, diff --git a/rama-tls-rustls/README.md b/rama-tls-rustls/README.md index 4ea3b6da2..f05518b3e 100644 --- a/rama-tls-rustls/README.md +++ b/rama-tls-rustls/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/workflows/CI/badge.svg [actions-url]: https://github.com/plabayo/rama/actions diff --git a/rama-tls-rustls/src/client/connector.rs b/rama-tls-rustls/src/client/connector.rs index 679d64079..a293138a1 100644 --- a/rama-tls-rustls/src/client/connector.rs +++ b/rama-tls-rustls/src/client/connector.rs @@ -4,7 +4,7 @@ use crate::types::TlsTunnel; use rama_core::conversion::{RamaInto, RamaTryFrom}; use rama_core::error::{BoxError, ErrorContext}; use rama_core::extensions::{ExtensionsMut, ExtensionsRef}; -use rama_core::stream::Stream; +use rama_core::io::Io; use rama_core::telemetry::tracing; use rama_core::{Layer, Service}; use rama_net::address::Host; @@ -169,7 +169,7 @@ impl TlsConnector { impl Service for TlsConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + ExtensionsRef + Send @@ -204,7 +204,7 @@ where }); } - let server_host = transport_ctx.authority.host.clone(); + let server_host = &transport_ctx.authority.host; tracing::trace!( server.address = %transport_ctx.authority.host, @@ -215,7 +215,9 @@ where let connector_data = input.extensions().get::().cloned(); - let (stream, negotiated_params) = self.handshake(connector_data, server_host, conn).await?; + let (stream, negotiated_params) = self + .handshake(connector_data, Some(server_host), conn) + .await?; tracing::trace!( server.address = %transport_ctx.authority.host, @@ -239,7 +241,7 @@ where impl Service for TlsConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + Send + ExtensionsRef @@ -262,11 +264,13 @@ where transport_ctx.app_protocol, ); - let server_host = transport_ctx.authority.host.clone(); + let server_host = &transport_ctx.authority.host; let connector_data = input.extensions().get::().cloned(); - let (conn, negotiated_params) = self.handshake(connector_data, server_host, conn).await?; + let (conn, negotiated_params) = self + .handshake(connector_data, Some(server_host), conn) + .await?; let mut conn = TlsStream::new(conn); #[cfg(feature = "http")] @@ -283,7 +287,7 @@ where impl Service for TlsConnector where - S: ConnectorService, + S: ConnectorService, Input: Send + ExtensionsRef + 'static, { type Output = EstablishedClientConnection, Input>; @@ -293,14 +297,10 @@ where let EstablishedClientConnection { input, conn } = self.inner.connect(input).await.into_box_error()?; - let server_host = if let Some(host) = input - .extensions() - .get::() - .as_ref() - .map(|t| &t.server_host) - .or(self.kind.host.as_ref()) - { - host.clone() + let maybe_server_host = if let Some(tunnel) = input.extensions().get::() { + tunnel.sni.as_ref() + } else if let Some(hardcoded_sni) = self.kind.host.as_ref() { + Some(hardcoded_sni) } else { tracing::trace!( "TlsConnector(tunnel): return inner connection: no Tls tunnel is requested" @@ -314,7 +314,9 @@ where let connector_data = input.extensions().get::().cloned(); - let (conn, negotiated_params) = self.handshake(connector_data, server_host, conn).await?; + let (conn, negotiated_params) = self + .handshake(connector_data, maybe_server_host, conn) + .await?; let mut conn = AutoTlsStream::secure(conn); #[cfg(feature = "http")] @@ -334,18 +336,21 @@ impl TlsConnector { async fn handshake( &self, connector_data: Option, - server_host: Host, + maybe_server_host: Option<&Host>, stream: T, ) -> Result<(RustlsTlsStream, NegotiatedTlsParameters), BoxError> where - T: Stream + ExtensionsMut + Unpin, + T: Io + ExtensionsMut + Unpin, { let connector_data = connector_data .or(self.connector_data.clone()) .unwrap_or(TlsConnectorData::try_new_http_auto()?); let server_name = rustls_pki_types::ServerName::rama_try_from( - connector_data.server_name.unwrap_or(server_host), + connector_data + .server_name + .or_else(|| maybe_server_host.cloned()) + .context("server name missing")?, )?; let connector = RustlsConnector::from(connector_data.client_config); diff --git a/rama-tls-rustls/src/client/tls_stream_auto.rs b/rama-tls-rustls/src/client/tls_stream_auto.rs index bcc9e0a91..a1015c565 100644 --- a/rama-tls-rustls/src/client/tls_stream_auto.rs +++ b/rama-tls-rustls/src/client/tls_stream_auto.rs @@ -4,7 +4,7 @@ use super::RustlsTlsStream; use pin_project_lite::pin_project; use rama_core::{ extensions::{Extensions, ExtensionsMut, ExtensionsRef}, - stream::Stream, + io::Io, }; use tokio::io::{AsyncRead, AsyncWrite}; @@ -61,7 +61,7 @@ impl fmt::Debug for AutoTlsStreamData { #[warn(clippy::missing_trait_methods)] impl AsyncRead for AutoTlsStream where - S: Stream + Unpin, + S: Io + Unpin, { fn poll_read( self: std::pin::Pin<&mut Self>, @@ -78,7 +78,7 @@ where #[warn(clippy::missing_trait_methods)] impl AsyncWrite for AutoTlsStream where - S: Stream + Unpin, + S: Io + Unpin, { fn poll_write( self: std::pin::Pin<&mut Self>, diff --git a/rama-tls-rustls/src/server/service.rs b/rama-tls-rustls/src/server/service.rs index e2a2bfad8..0eb3c78cb 100644 --- a/rama-tls-rustls/src/server/service.rs +++ b/rama-tls-rustls/src/server/service.rs @@ -9,7 +9,7 @@ use rama_core::{ conversion::RamaInto, error::{BoxError, ErrorContext}, extensions::ExtensionsMut, - stream::Stream, + io::Io, }; use rama_net::tls::{ApplicationProtocol, client::NegotiatedTlsParameters}; use rama_utils::macros::define_inner_service_accessors; @@ -38,7 +38,7 @@ impl TlsAcceptorService { impl Service for TlsAcceptorService where - IO: Stream + Unpin + ExtensionsMut + 'static, + IO: Io + Unpin + ExtensionsMut + 'static, S: Service, Error: Into>, { type Output = S::Output; diff --git a/rama-tower/README.md b/rama-tower/README.md index 03a14081d..57eceb20c 100644 --- a/rama-tower/README.md +++ b/rama-tower/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-ua/README.md b/rama-ua/README.md index 33dbbc8dc..ff3710ed1 100644 --- a/rama-ua/README.md +++ b/rama-ua/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-udp/Cargo.toml b/rama-udp/Cargo.toml index ff72a88a9..3a2bad5a5 100644 --- a/rama-udp/Cargo.toml +++ b/rama-udp/Cargo.toml @@ -22,6 +22,7 @@ default = [] [dependencies] rama-core = { workspace = true } +rama-dns = { workspace = true } rama-net = { workspace = true } tokio = { workspace = true, features = ["macros", "net"] } tokio-util = { workspace = true, features = ["net"] } diff --git a/rama-udp/README.md b/rama-udp/README.md index b5693ba96..3a95e891d 100644 --- a/rama-udp/README.md +++ b/rama-udp/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-udp/src/lib.rs b/rama-udp/src/lib.rs index c6d7ad316..8792e7060 100644 --- a/rama-udp/src/lib.rs +++ b/rama-udp/src/lib.rs @@ -22,7 +22,10 @@ )] mod socket; -pub use socket::{UdpSocket, bind_udp, bind_udp_with_address, bind_udp_with_socket}; +pub use socket::{ + UdpSocket, bind_udp_socket_with_connect, bind_udp_socket_with_connect_default_dns, + bind_udp_with_address, bind_udp_with_socket, +}; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] pub use socket::bind_udp_with_device; diff --git a/rama-udp/src/socket.rs b/rama-udp/src/socket.rs index fe5980df3..dc4bbdce4 100644 --- a/rama-udp/src/socket.rs +++ b/rama-udp/src/socket.rs @@ -1,13 +1,171 @@ use std::net::SocketAddr; -use rama_core::error::{BoxError, ErrorContext as _}; +use rama_core::{ + error::{BoxError, ErrorContext as _, ErrorExt as _}, + extensions::Extensions, + futures::StreamExt, + telemetry::tracing, +}; +use rama_dns::client::{ + GlobalDnsResolver, + resolver::{DnsAddressResolver, HappyEyeballAddressResolverExt}, +}; +use rama_net::address::{HostWithPort, SocketAddress}; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] -use rama_net::socket::{DeviceName, SocketOptions}; -use rama_net::{address::SocketAddress, socket::Interface}; +use rama_net::socket::{DeviceName, SocketOptions, opts::Domain}; pub use tokio::net::UdpSocket; +/// Bind a [`UdpSocket`] to the local interface and connect +/// to the given host and port using the global DNS resolver, +/// in case the host is a Domain, otherwise the IpAddr will be used as-is. +/// +/// This is a convenience wrapper around [`bind_udp_socket_with_connect`] that uses +/// [`GlobalDnsResolver`]. +/// +/// Returns an error if the host is not compatible or cannot be resolve +/// or if all resolved addresses fail to connect. +/// +/// Calling `connect` on a UDP socket does not establish a transport level +/// session. It sets the default remote peer and allows using `send` instead +/// of `send_to`. +#[inline(always)] +pub async fn bind_udp_socket_with_connect_default_dns( + address: impl Into, + extensions: Option<&Extensions>, +) -> Result { + bind_udp_socket_with_connect(address, GlobalDnsResolver::new(), extensions).await +} + +/// Bind a [`UdpSocket`] to the local interface and connect +/// to the given host and port using the provided DNS resolver, +/// in case the host is a Domain, otherwise the IpAddr will be used as-is. +/// +/// The host, if a domain, is resolved using a Happy Eyeballs strategy and each resolved IP +/// address is attempted in order until the socket successfully connects. +/// The first successful connection attempt completes the function. The strategy +/// also respects IP/Dns connect/resolve preferences (e.g. Ipv6 addresses won't +/// be allowed if running in Ipv4 only modes), even if host was an Ip to begin with. +/// +/// Returns an error if the host is not compatible or +/// does not resolve to any IP address or +/// if all resolved addresses fail to connect. +/// +/// Connecting a UDP socket configures its default remote peer and +/// restricts incoming datagrams to that peer. It does not perform a +/// handshake or guarantee reachability. +pub async fn bind_udp_socket_with_connect( + address: impl Into, + dns: Dns, + extensions: Option<&Extensions>, +) -> Result +where + Dns: DnsAddressResolver, +{ + let HostWithPort { host, port } = address.into(); + let mut ip_stream = std::pin::pin!( + dns.happy_eyeballs_resolver(host.clone()) + .maybe_with_extensions(extensions) + .lookup_ip() + ); + + let mut ipv4_socket = None; + let mut ipv6_socket = None; + + let mut resolved_count = 0; + + while let Some(ip_result) = ip_stream.next().await { + let ip = match ip_result { + Ok(ip) => { + resolved_count += 1; + ip + } + Err(err) => { + tracing::debug!("failed to resolve IP address for host {host}: {err}"); + continue; + } + }; + + let address: SocketAddr = (ip, port).into(); + + if address.is_ipv4() { + let socket = if let Some(socket) = ipv4_socket.take() { + socket + } else { + match bind_udp_with_address(SocketAddress::default_ipv4(0)).await { + Ok(socket) => socket, + Err(err) => { + tracing::debug!( + "failed to bind default Ipv4 socket.. ignore ipv4 address {address} (host = {host}): err = {err}" + ); + continue; + } + } + }; + + match socket.connect(address).await { + Ok(()) => { + tracing::trace!( + "resolved#{resolved_count} udp socket connected to IpV4 address: {address} (resolved from host {host})" + ); + return Ok(socket); + } + Err(err) => { + ipv4_socket = Some(socket); + tracing::trace!( + "resolved#{resolved_count} udp socket failed to connect to IpV4 address: {address} (resolved from host {host}): err = {err}" + ); + } + } + } else { + let socket = if let Some(socket) = ipv6_socket.take() { + socket + } else { + match bind_udp_with_address(SocketAddress::default_ipv6(0)).await { + Ok(socket) => socket, + Err(err) => { + tracing::debug!( + "failed to bind default Ipv6 socket.. ignore IpV4 address {address} (host = {host}): err = {err}" + ); + continue; + } + } + }; + + match socket.connect(address).await { + Ok(()) => { + tracing::trace!( + "resolved#{resolved_count} udp socket connected to IpV6 address: {address} (resolved from host {host})" + ); + return Ok(socket); + } + Err(err) => { + ipv6_socket = Some(socket); + tracing::trace!( + "resolved#{resolved_count} udp socket failed to connect to IpV6 address: {address} (resolved from host {host}): err = {err}" + ); + } + } + } + } + + if resolved_count > 0 { + Err( + BoxError::from("failed to (udp) connect to any resolved IP address") + .context_field("host", host) + .context_field("port", port) + .context_field("resolved_addr_count", resolved_count), + ) + } else { + Err( + BoxError::from("failed to resolve into any IP address (as part of udp connect)") + .context_field("host", host) + .context_field("port", port), + ) + } +} + /// Creates a new [`UdpSocket`], which will be bound to the specified address. /// /// The returned socket is ready for accepting connections and connecting to others. @@ -58,7 +216,7 @@ pub async fn bind_udp_with_device< device: Some(name), ..SocketOptions::default_udp() } - .try_build_socket() + .try_build_socket(Domain::Unix) .context("create udp ipv4 socket attached to device")?; bind_socket_internal(socket) }) @@ -66,25 +224,6 @@ pub async fn bind_udp_with_device< .context("await blocking bind socket task")? } -/// Creates a new [`UdpSocket`], which will be bound to the specified [`Interface`]. -/// -/// The returned socket is ready for accepting connections and connecting to others. -pub async fn bind_udp>>( - interface: I, -) -> Result { - match interface.try_into().map_err(Into::::into)? { - Interface::Address(addr) => bind_udp_with_address(addr).await, - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - Interface::Device(name) => bind_udp_with_device(name).await, - Interface::Socket(opts) => { - let socket = opts - .try_build_socket() - .context("build udp socket from options")?; - bind_udp_with_socket(socket).await - } - } -} - fn bind_socket_internal(socket: rama_net::socket::core::Socket) -> Result { let socket = std::net::UdpSocket::from(socket); socket diff --git a/rama-unix/Cargo.toml b/rama-unix/Cargo.toml index bc466eb10..0a030e27e 100644 --- a/rama-unix/Cargo.toml +++ b/rama-unix/Cargo.toml @@ -21,14 +21,12 @@ rustdoc-args = ["--cfg", "docsrs"] default = [] [dependencies] +libc = { workspace = true } pin-project-lite = { workspace = true } rama-core = { workspace = true } rama-net = { workspace = true } tokio = { workspace = true, features = ["macros", "net"] } -[target.'cfg(target_family = "unix")'.dependencies] -libc = { workspace = true } - [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/rama-unix/README.md b/rama-unix/README.md index 9a2dc9daf..50ef75286 100644 --- a/rama-unix/README.md +++ b/rama-unix/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-unix/src/unix/address.rs b/rama-unix/src/address.rs similarity index 100% rename from rama-unix/src/unix/address.rs rename to rama-unix/src/address.rs diff --git a/rama-unix/src/unix/client/connector.rs b/rama-unix/src/client/connector.rs similarity index 100% rename from rama-unix/src/unix/client/connector.rs rename to rama-unix/src/client/connector.rs diff --git a/rama-unix/src/unix/client/mod.rs b/rama-unix/src/client/mod.rs similarity index 100% rename from rama-unix/src/unix/client/mod.rs rename to rama-unix/src/client/mod.rs diff --git a/rama-unix/src/unix/frame.rs b/rama-unix/src/frame.rs similarity index 100% rename from rama-unix/src/unix/frame.rs rename to rama-unix/src/frame.rs diff --git a/rama-unix/src/lib.rs b/rama-unix/src/lib.rs index d7d72372b..3b8f2d738 100644 --- a/rama-unix/src/lib.rs +++ b/rama-unix/src/lib.rs @@ -15,19 +15,90 @@ #![doc(html_logo_url = "https://raw.githubusercontent.com/plabayo/rama/main/docs/img/old_logo.png")] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(test, allow(clippy::float_cmp))] +#![cfg(target_family = "unix")] #![cfg_attr( not(test), warn(clippy::print_stdout, clippy::dbg_macro), deny(clippy::unwrap_used, clippy::expect_used) )] -#[cfg(target_family = "unix")] -mod unix; +use std::ops::{Deref, DerefMut}; -#[cfg(target_family = "unix")] -#[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] -#[doc(inline)] -pub use unix::*; +mod address; +pub use address::UnixSocketAddress; -#[cfg(target_family = "unix")] +pub mod client; +pub mod server; pub mod utils; + +mod stream; +#[doc(inline)] +pub use stream::{TokioUnixStream, UnixStream}; + +mod frame; +#[doc(inline)] +pub use frame::UnixDatagramFramed; + +pub use tokio::net::unix::SocketAddr as TokioSocketAddress; +pub use tokio::net::{UnixDatagram, UnixSocket}; + +#[derive(Debug, Clone)] +/// Information about the socket on the egress end. +pub struct ClientUnixSocketInfo(pub UnixSocketInfo); + +impl AsRef for ClientUnixSocketInfo { + fn as_ref(&self) -> &UnixSocketInfo { + &self.0 + } +} + +impl AsMut for ClientUnixSocketInfo { + fn as_mut(&mut self) -> &mut UnixSocketInfo { + &mut self.0 + } +} + +impl Deref for ClientUnixSocketInfo { + type Target = UnixSocketInfo; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl DerefMut for ClientUnixSocketInfo { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[derive(Debug, Clone)] +/// Connected unix socket information. +pub struct UnixSocketInfo { + local_addr: Option, + peer_addr: UnixSocketAddress, +} + +impl UnixSocketInfo { + /// Create a new [`UnixSocketInfo`]. + pub fn new( + local_addr: Option>, + peer_addr: impl Into, + ) -> Self { + Self { + local_addr: local_addr.map(Into::into), + peer_addr: peer_addr.into(), + } + } + + /// Try to get the address of the local unix (domain) socket. + #[must_use] + pub fn local_addr(&self) -> Option<&UnixSocketAddress> { + self.local_addr.as_ref() + } + + /// Get the address of the peer unix (domain) socket. + #[must_use] + pub fn peer_addr(&self) -> &UnixSocketAddress { + &self.peer_addr + } +} diff --git a/rama-unix/src/unix/server/listener.rs b/rama-unix/src/server/listener.rs similarity index 96% rename from rama-unix/src/unix/server/listener.rs rename to rama-unix/src/server/listener.rs index b12b1fa55..e923f7387 100644 --- a/rama-unix/src/unix/server/listener.rs +++ b/rama-unix/src/server/listener.rs @@ -15,7 +15,7 @@ use tokio::net::UnixListener as TokioUnixListener; use tokio::net::unix::SocketAddr; #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] -use rama_net::socket::SocketOptions; +use rama_net::socket::{SocketOptions, opts::Domain}; use crate::UnixSocketAddress; use crate::UnixSocketInfo; @@ -89,7 +89,7 @@ impl UnixListenerBuilder { }) } - /// Creates a new TcpListener, which will be bound to the specified interface. + /// Creates a new UnixListener, which will be bound to the specified interface. /// /// The returned listener is ready for accepting connections. #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] @@ -97,7 +97,8 @@ impl UnixListenerBuilder { self, opts: SocketOptions, ) -> Result { - let socket = tokio::task::spawn_blocking(move || opts.try_build_socket()).await??; + let socket = + tokio::task::spawn_blocking(move || opts.try_build_socket(Domain::Unix)).await??; Ok(self.bind_socket(socket)?) } } @@ -147,7 +148,7 @@ impl UnixListener { #[inline] #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] - /// Creates a new TcpListener, which will be bound to the specified (interface) device name. + /// Creates a new UnixListener, which will be bound to the specified (interface) device name. /// /// The returned listener is ready for accepting connections. pub async fn bind_socket_opts( diff --git a/rama-unix/src/unix/server/mod.rs b/rama-unix/src/server/mod.rs similarity index 100% rename from rama-unix/src/unix/server/mod.rs rename to rama-unix/src/server/mod.rs diff --git a/rama-unix/src/unix/stream.rs b/rama-unix/src/stream.rs similarity index 100% rename from rama-unix/src/unix/stream.rs rename to rama-unix/src/stream.rs diff --git a/rama-unix/src/unix/mod.rs b/rama-unix/src/unix/mod.rs deleted file mode 100644 index 00f8778b1..000000000 --- a/rama-unix/src/unix/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -mod address; -use std::ops::{Deref, DerefMut}; - -pub use address::UnixSocketAddress; - -pub mod client; -pub mod server; - -mod stream; -#[doc(inline)] -pub use stream::{TokioUnixStream, UnixStream}; - -mod frame; -#[doc(inline)] -pub use frame::UnixDatagramFramed; - -pub use tokio::net::unix::SocketAddr as TokioSocketAddress; -pub use tokio::net::{UnixDatagram, UnixSocket}; - -#[derive(Debug, Clone)] -/// Information about the socket on the egress end. -pub struct ClientUnixSocketInfo(pub UnixSocketInfo); - -impl AsRef for ClientUnixSocketInfo { - fn as_ref(&self) -> &UnixSocketInfo { - &self.0 - } -} - -impl AsMut for ClientUnixSocketInfo { - fn as_mut(&mut self) -> &mut UnixSocketInfo { - &mut self.0 - } -} - -impl Deref for ClientUnixSocketInfo { - type Target = UnixSocketInfo; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} -impl DerefMut for ClientUnixSocketInfo { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[derive(Debug, Clone)] -/// Connected unix socket information. -pub struct UnixSocketInfo { - local_addr: Option, - peer_addr: UnixSocketAddress, -} - -impl UnixSocketInfo { - /// Create a new [`UnixSocketInfo`]. - pub fn new( - local_addr: Option>, - peer_addr: impl Into, - ) -> Self { - Self { - local_addr: local_addr.map(Into::into), - peer_addr: peer_addr.into(), - } - } - - /// Try to get the address of the local unix (domain) socket. - #[must_use] - pub fn local_addr(&self) -> Option<&UnixSocketAddress> { - self.local_addr.as_ref() - } - - /// Get the address of the peer unix (domain) socket. - #[must_use] - pub fn peer_addr(&self) -> &UnixSocketAddress { - &self.peer_addr - } -} diff --git a/rama-utils/README.md b/rama-utils/README.md index 1323f2ec7..447ef97d6 100644 --- a/rama-utils/README.md +++ b/rama-utils/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-ws/README.md b/rama-ws/README.md index 8fb7e9b95..cbb52ac76 100644 --- a/rama-ws/README.md +++ b/rama-ws/README.md @@ -20,7 +20,7 @@ [license-mit-url]: https://github.com/plabayo/rama/blob/main/LICENSE-MIT [license-apache-badge]: https://img.shields.io/badge/license-APACHE-blue.svg [license-apache-url]: https://github.com/plabayo/rama/blob/main/LICENSE-APACHE -[rust-version-badge]: https://img.shields.io/badge/rustc-1.91+-blue?style=flat-square&logo=rust +[rust-version-badge]: https://img.shields.io/badge/rustc-1.93+-blue?style=flat-square&logo=rust [rust-version-url]: https://www.rust-lang.org [actions-badge]: https://github.com/plabayo/rama/actions/workflows/CI.yml/badge.svg?branch=main [actions-url]: https://github.com/plabayo/rama/actions/workflows/CI.yml diff --git a/rama-ws/src/handshake/matcher/mod.rs b/rama-ws/src/handshake/matcher/mod.rs new file mode 100644 index 000000000..a0b7be9c9 --- /dev/null +++ b/rama-ws/src/handshake/matcher/mod.rs @@ -0,0 +1,263 @@ +//! WebSocket matcher utilities + +use rama_core::{ + extensions::{Extensions, ExtensionsRef}, + matcher::Matcher, + telemetry::tracing::{self}, +}; +use rama_http::{ + Method, Request, Version, + headers::{self, HeaderMapExt}, + proto::h2::ext::Protocol, +}; + +mod service; +pub use self::service::{ + HttpWebSocketRelayServiceRequestMatcher, HttpWebSocketRelayServiceResponseMatcher, +}; + +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +/// WebSocket [`Matcher`] to match on incoming WebSocket requests. +/// +/// The [`Default`] ws matcher does already out of the box the basic checks: +/// +/// - for http/1.1: require GET method and `Upgrade: websocket` + `Connection: upgrade` headers +/// - for h2: require CONNECT method and `:protocol: websocket` pseudo header +pub struct WebSocketMatcher; + +impl WebSocketMatcher { + #[inline] + /// Create a new default [`WebSocketMatcher`]. + #[must_use] + pub fn new() -> Self { + Default::default() + } +} + +pub fn is_http_req_websocket_handshake(req: &Request) -> bool { + match req.version() { + version @ (Version::HTTP_10 | Version::HTTP_11) => { + match req.method() { + &Method::GET => (), + method => { + tracing::debug!( + http.version = ?version, + http.request.method = %method, + "WebSocketMatcher: h1: unexpected method found: no match", + ); + return false; + } + } + + if !req + .headers() + .typed_get::() + .map(|u| u.is_websocket()) + .unwrap_or_default() + { + tracing::trace!( + http.version = ?version, + "WebSocketMatcher: h1: no websocket upgrade header found: no match" + ); + return false; + } + + if !req + .headers() + .typed_get::() + .map(|c| c.contains_upgrade()) + .unwrap_or_default() + { + tracing::trace!( + http.version = ?version, + "WebSocketMatcher: h1: no connection upgrade header found: no match", + ); + return false; + } + } + version @ Version::HTTP_2 => { + match req.method() { + &Method::CONNECT => (), + method => { + tracing::debug!( + http.version = ?version, + http.request.method = %method, + "WebSocketMatcher: h2: unexpected method found: no match", + ); + return false; + } + } + + if !req + .extensions() + .get::() + .map(|p| p.as_str().trim().eq_ignore_ascii_case("websocket")) + .unwrap_or_default() + { + tracing::trace!( + http.version = ?version, + "WebSocketMatcher: h2: no websocket protocol (pseudo ext) found", + ); + return false; + } + } + version => { + tracing::debug!( + http.version = ?version, + "WebSocketMatcher: unexpected http version found: no match", + ); + return false; + } + } + + true +} + +impl Matcher> for WebSocketMatcher +where + Body: Send + 'static, +{ + #[inline(always)] + fn matches(&self, _ext: Option<&mut Extensions>, req: &Request) -> bool { + is_http_req_websocket_handshake(req) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rama_http::Body; + + macro_rules! request { + ( + $method:literal $version:literal $uri:literal + $( + $header_name:literal: $header_value:literal + )* + ) => { + request!( + $method $version $uri + $( + $header_name: $header_value + )* + w/ [] + ) + }; + ( + $method:literal $version:literal $uri:literal + $( + $header_name:literal: $header_value:literal + )* + w/ [$($extension:expr),* $(,)?] + ) => { + { + let req = Request::builder() + .uri($uri) + .version(match $version { + "HTTP/1.1" => Version::HTTP_11, + "HTTP/2" => Version::HTTP_2, + _ => unreachable!(), + }) + .method(match $method { + "GET" => Method::GET, + "POST" => Method::POST, + "CONNECT" => Method::CONNECT, + _ => unreachable!(), + }); + + $( + let req = req.header($header_name, $header_value); + )* + + $( + let req = req.extension($extension); + )* + + req.body(Body::empty()).unwrap() + } + }; + } + + fn assert_websocket_no_match(request: &Request, matcher: &WebSocketMatcher) { + assert!( + !matcher.matches(None, request), + "!({matcher:?}).matches({request:?})" + ); + } + + fn assert_websocket_match(request: &Request, matcher: &WebSocketMatcher) { + assert!( + matcher.matches(None, request), + "({matcher:?}).matches({request:?})" + ); + } + + #[test] + fn test_websocket_match_default_http_11() { + let matcher = WebSocketMatcher::default(); + + assert_websocket_no_match( + &request! { + "GET" "HTTP/1.1" "/" + }, + &matcher, + ); + assert_websocket_no_match( + &request! { + "GET" "HTTP/1.1" "/" + "Upgrade": "websocket" + }, + &matcher, + ); + assert_websocket_no_match( + &request! { + "GET" "HTTP/1.1" "/" + "Connection": "upgrade" + }, + &matcher, + ); + assert_websocket_match( + &request! { + "GET" "HTTP/1.1" "/" + "Connection": "upgrade" + "Upgrade": "websocket" + }, + &matcher, + ); + } + + #[test] + fn test_websocket_match_default_http_2() { + let matcher = WebSocketMatcher::default(); + + assert_websocket_no_match( + &request! { + "GET" "HTTP/2" "/" + "Connection": "upgrade" + "Upgrade": "websocket" + "Sec-WebSocket-Version": "13" + "Sec-WebSocket-Key": "foobar" + }, + &matcher, + ); + assert_websocket_match( + &request! { + "CONNECT" "HTTP/2" "/" + w/ [ + Protocol::from_static("websocket"), + ] + }, + &matcher, + ); + assert_websocket_no_match( + &request! { + "GET" "HTTP/2" "/" + w/ [ + Protocol::from_static("websocket"), + ] + }, + &matcher, + ); + } +} diff --git a/rama-ws/src/handshake/matcher/service.rs b/rama-ws/src/handshake/matcher/service.rs new file mode 100644 index 000000000..7d40c7682 --- /dev/null +++ b/rama-ws/src/handshake/matcher/service.rs @@ -0,0 +1,108 @@ +use std::convert::Infallible; + +use rama_core::matcher::service::{ServiceMatch, ServiceMatcher}; +use rama_http::{Request, Response, StatusCode}; +use rama_net::proxy::IoForwardService; + +#[derive(Debug, Clone)] +/// Default matcher that can be used for Http websocket relays. +/// +/// Request matches for an http websocket request return +/// a [`HttpWebSocketRelayServiceResponseMatcher`] instance which +/// will match on 101 status code responses... +pub struct HttpWebSocketRelayServiceRequestMatcher { + relay_svc: S, +} + +impl Default for HttpWebSocketRelayServiceRequestMatcher { + fn default() -> Self { + Self { + relay_svc: IoForwardService::new(), + } + } +} + +impl HttpWebSocketRelayServiceRequestMatcher { + #[inline(always)] + #[must_use] + /// Create a new [`HttpWebSocketRelayServiceRequestMatcher`]. + pub fn new(relay_svc: S) -> Self { + Self { relay_svc } + } +} + +impl ServiceMatcher> for HttpWebSocketRelayServiceRequestMatcher +where + S: Clone + Send + Sync + 'static, + Body: Send + 'static, +{ + type Service = HttpWebSocketRelayServiceResponseMatcher; + type Error = Infallible; + type ModifiedInput = Request; + + async fn match_service( + &self, + req: Request, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: super::is_http_req_websocket_handshake(&req).then(|| { + HttpWebSocketRelayServiceResponseMatcher { + relay_svc: self.relay_svc.clone(), + } + }), + input: req, + }) + } + + async fn into_match_service( + self, + req: Request, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: super::is_http_req_websocket_handshake(&req).then(|| { + HttpWebSocketRelayServiceResponseMatcher { + relay_svc: self.relay_svc, + } + }), + input: req, + }) + } +} + +#[derive(Debug, Clone)] +/// Created by [`HttpWebSocketRelayServiceRequestMatcher`] for a valid 101 Switching Protocol response, +/// following the websocket request which started the handshake, request match. +pub struct HttpWebSocketRelayServiceResponseMatcher { + relay_svc: S, +} + +impl ServiceMatcher> for HttpWebSocketRelayServiceResponseMatcher +where + S: Clone + Send + Sync + 'static, + Body: Send + 'static, +{ + type Service = S; + type Error = Infallible; + type ModifiedInput = Response; + + async fn match_service( + &self, + res: Response, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: (res.status() == StatusCode::SWITCHING_PROTOCOLS) + .then(|| self.relay_svc.clone()), + input: res, + }) + } + + async fn into_match_service( + self, + res: Response, + ) -> Result, Self::Error> { + Ok(ServiceMatch { + service: (res.status() == StatusCode::SWITCHING_PROTOCOLS).then_some(self.relay_svc), + input: res, + }) + } +} diff --git a/rama-ws/src/handshake/mod.rs b/rama-ws/src/handshake/mod.rs index a0507c830..0ccf2207b 100644 --- a/rama-ws/src/handshake/mod.rs +++ b/rama-ws/src/handshake/mod.rs @@ -2,3 +2,5 @@ pub mod client; pub mod server; + +pub mod matcher; diff --git a/rama-ws/src/handshake/server.rs b/rama-ws/src/handshake/server.rs index 3be0e1442..4e23be236 100644 --- a/rama-ws/src/handshake/server.rs +++ b/rama-ws/src/handshake/server.rs @@ -8,8 +8,7 @@ use std::{ use rama_core::{ Service, error::{BoxError, ErrorContext}, - extensions::{Extensions, ExtensionsMut, ExtensionsRef}, - matcher::Matcher, + extensions::{ExtensionsMut, ExtensionsRef}, rt::Executor, telemetry::tracing::{self, Instrument}, }; @@ -37,109 +36,6 @@ use crate::{ runtime::AsyncWebSocket, }; -#[derive(Debug, Clone, Default)] -#[non_exhaustive] -/// WebSocket [`Matcher`] to match on incoming WebSocket requests. -/// -/// The [`Default`] ws matcher does already out of the box the basic checks: -/// -/// - for http/1.1: require GET method and `Upgrade: websocket` + `Connection: upgrade` headers -/// - for h2: require CONNECT method and `:protocol: websocket` pseudo header -pub struct WebSocketMatcher; - -impl WebSocketMatcher { - #[inline] - /// Create a new default [`WebSocketMatcher`]. - #[must_use] - pub fn new() -> Self { - Default::default() - } -} - -impl Matcher> for WebSocketMatcher -where - Body: Send + 'static, -{ - fn matches(&self, _ext: Option<&mut Extensions>, req: &Request) -> bool { - match req.version() { - version @ (Version::HTTP_10 | Version::HTTP_11) => { - match req.method() { - &Method::GET => (), - method => { - tracing::debug!( - http.version = ?version, - http.request.method = %method, - "WebSocketMatcher: h1: unexpected method found: no match", - ); - return false; - } - } - - if !req - .headers() - .typed_get::() - .map(|u| u.is_websocket()) - .unwrap_or_default() - { - tracing::trace!( - http.version = ?version, - "WebSocketMatcher: h1: no websocket upgrade header found: no match" - ); - return false; - } - - if !req - .headers() - .typed_get::() - .map(|c| c.contains_upgrade()) - .unwrap_or_default() - { - tracing::trace!( - http.version = ?version, - "WebSocketMatcher: h1: no connection upgrade header found: no match", - ); - return false; - } - } - version @ Version::HTTP_2 => { - match req.method() { - &Method::CONNECT => (), - method => { - tracing::debug!( - http.version = ?version, - http.request.method = %method, - "WebSocketMatcher: h2: unexpected method found: no match", - ); - return false; - } - } - - if !req - .extensions() - .get::() - .map(|p| p.as_str().trim().eq_ignore_ascii_case("websocket")) - .unwrap_or_default() - { - tracing::trace!( - http.version = ?version, - "WebSocketMatcher: h2: no websocket protocol (pseudo ext) found", - ); - return false; - } - } - version => { - tracing::debug!( - http.version = ?version, - "WebSocketMatcher: unexpected http version found: no match", - ); - return false; - } - } - - true - } -} - #[derive(Debug)] /// Server error which can be triggered in case the request validation failed pub enum RequestValidateError { @@ -969,6 +865,49 @@ mod tests { use super::*; + async fn assert_websocket_acceptor_ok( + request: Request, + acceptor: &WebSocketAcceptor, + expected_accepted_protocol: Option, + ) { + let (resp, req) = acceptor.serve(request).await.unwrap(); + match req.version() { + Version::HTTP_10 | Version::HTTP_11 => { + assert_eq!(StatusCode::SWITCHING_PROTOCOLS, resp.status()) + } + Version::HTTP_2 => assert_eq!(StatusCode::OK, resp.status()), + _ => unreachable!(), + } + let accepted_protocol = resp + .headers() + .typed_get::() + .map(|p| p.accept_first_protocol()); + if let Some(expected_accepted_protocol) = expected_accepted_protocol { + assert_eq!( + accepted_protocol.as_ref(), + Some(&expected_accepted_protocol), + "request = {req:?}" + ); + assert_eq!( + req.extensions().get::(), + Some(&expected_accepted_protocol), + "request = {req:?}" + ); + } else { + assert!(accepted_protocol.is_none()); + assert!( + req.extensions() + .get::() + .is_none() + ); + } + } + + async fn assert_websocket_acceptor_bad_request(request: Request, acceptor: &WebSocketAcceptor) { + let resp = acceptor.serve(request).await.unwrap_err(); + assert_eq!(StatusCode::BAD_REQUEST, resp.status()); + } + macro_rules! request { ( $method:literal $version:literal $uri:literal @@ -1019,131 +958,6 @@ mod tests { }; } - fn assert_websocket_no_match(request: &Request, matcher: &WebSocketMatcher) { - assert!( - !matcher.matches(None, request), - "!({matcher:?}).matches({request:?})" - ); - } - - fn assert_websocket_match(request: &Request, matcher: &WebSocketMatcher) { - assert!( - matcher.matches(None, request), - "({matcher:?}).matches({request:?})" - ); - } - - #[test] - fn test_websocket_match_default_http_11() { - let matcher = WebSocketMatcher::default(); - - assert_websocket_no_match( - &request! { - "GET" "HTTP/1.1" "/" - }, - &matcher, - ); - assert_websocket_no_match( - &request! { - "GET" "HTTP/1.1" "/" - "Upgrade": "websocket" - }, - &matcher, - ); - assert_websocket_no_match( - &request! { - "GET" "HTTP/1.1" "/" - "Connection": "upgrade" - }, - &matcher, - ); - assert_websocket_match( - &request! { - "GET" "HTTP/1.1" "/" - "Connection": "upgrade" - "Upgrade": "websocket" - }, - &matcher, - ); - } - - #[test] - fn test_websocket_match_default_http_2() { - let matcher = WebSocketMatcher::default(); - - assert_websocket_no_match( - &request! { - "GET" "HTTP/2" "/" - "Connection": "upgrade" - "Upgrade": "websocket" - "Sec-WebSocket-Version": "13" - "Sec-WebSocket-Key": "foobar" - }, - &matcher, - ); - assert_websocket_match( - &request! { - "CONNECT" "HTTP/2" "/" - w/ [ - Protocol::from_static("websocket"), - ] - }, - &matcher, - ); - assert_websocket_no_match( - &request! { - "GET" "HTTP/2" "/" - w/ [ - Protocol::from_static("websocket"), - ] - }, - &matcher, - ); - } - - async fn assert_websocket_acceptor_ok( - request: Request, - acceptor: &WebSocketAcceptor, - expected_accepted_protocol: Option, - ) { - let (resp, req) = acceptor.serve(request).await.unwrap(); - match req.version() { - Version::HTTP_10 | Version::HTTP_11 => { - assert_eq!(StatusCode::SWITCHING_PROTOCOLS, resp.status()) - } - Version::HTTP_2 => assert_eq!(StatusCode::OK, resp.status()), - _ => unreachable!(), - } - let accepted_protocol = resp - .headers() - .typed_get::() - .map(|p| p.accept_first_protocol()); - if let Some(expected_accepted_protocol) = expected_accepted_protocol { - assert_eq!( - accepted_protocol.as_ref(), - Some(&expected_accepted_protocol), - "request = {req:?}" - ); - assert_eq!( - req.extensions().get::(), - Some(&expected_accepted_protocol), - "request = {req:?}" - ); - } else { - assert!(accepted_protocol.is_none()); - assert!( - req.extensions() - .get::() - .is_none() - ); - } - } - - async fn assert_websocket_acceptor_bad_request(request: Request, acceptor: &WebSocketAcceptor) { - let resp = acceptor.serve(request).await.unwrap_err(); - assert_eq!(StatusCode::BAD_REQUEST, resp.status()); - } - #[tokio::test] async fn test_websocket_acceptor_default_http_2() { let acceptor = WebSocketAcceptor::default(); diff --git a/rama-ws/src/runtime/handshake.rs b/rama-ws/src/runtime/handshake.rs index f34c74bc3..d1c139ce1 100644 --- a/rama-ws/src/runtime/handshake.rs +++ b/rama-ws/src/runtime/handshake.rs @@ -4,7 +4,7 @@ use std::{ task::{Context, Poll}, }; -use rama_core::stream::Stream; +use rama_core::io::Io; use rama_core::telemetry::tracing::trace; use crate::{ @@ -15,7 +15,7 @@ use crate::{ pub(crate) async fn without_handshake(stream: S, f: F) -> AsyncWebSocket where F: FnOnce(AllowStd) -> WebSocket> + Unpin, - S: Stream + Unpin, + S: Io + Unpin, { let start = SkippedHandshakeFuture(Some(SkippedHandshakeFutureInner { f, stream })); diff --git a/rama-ws/src/runtime/stream.rs b/rama-ws/src/runtime/stream.rs index ebacb051f..ca4d4c13d 100644 --- a/rama-ws/src/runtime/stream.rs +++ b/rama-ws/src/runtime/stream.rs @@ -4,7 +4,7 @@ use std::{ task::{Context, Poll, ready}, }; -use rama_core::stream::Stream; +use rama_core::io::Io; use rama_core::{ error::BoxError, extensions::{Extensions, ExtensionsMut, ExtensionsRef}, @@ -46,7 +46,7 @@ impl AsyncWebSocket { /// handshake. pub async fn from_raw_socket(stream: S, role: Role, config: Option) -> Self where - S: Stream + Unpin + ExtensionsMut, + S: Io + Unpin + ExtensionsMut, { without_handshake(stream, move |allow_std| { WebSocket::from_raw_socket(allow_std, role, config) @@ -63,7 +63,7 @@ impl AsyncWebSocket { config: Option, ) -> Self where - S: Stream + Unpin + ExtensionsMut, + S: Io + Unpin + ExtensionsMut, { without_handshake(stream, move |allow_std| { WebSocket::from_partially_read(allow_std, part, role, config) @@ -101,7 +101,7 @@ impl AsyncWebSocket { /// Returns a shared reference to the inner stream. pub fn get_ref(&self) -> &S where - S: Stream + Unpin, + S: Io + Unpin, { self.inner.get_ref().get_ref() } @@ -109,7 +109,7 @@ impl AsyncWebSocket { /// Returns a mutable reference to the inner stream. pub fn get_mut(&mut self) -> &mut S where - S: Stream + Unpin, + S: Io + Unpin, { self.inner.get_mut().get_mut() } @@ -122,7 +122,7 @@ impl AsyncWebSocket { /// Close the underlying web socket pub async fn close(&mut self, msg: Option) -> Result<(), ProtocolError> where - S: Stream + Unpin, + S: Io + Unpin, { self.send(Message::Close(msg)).await } @@ -140,7 +140,7 @@ impl ExtensionsMut for AsyncWebSocket { } } -impl AsyncWebSocket { +impl AsyncWebSocket { #[inline] /// Writes and immediately flushes a message. pub fn send_message( @@ -162,7 +162,7 @@ impl AsyncWebSocket { impl futures::Stream for AsyncWebSocket where - T: Stream + Unpin, + T: Io + Unpin, { type Item = Result; @@ -195,7 +195,7 @@ where impl futures::stream::FusedStream for AsyncWebSocket where - T: Stream + Unpin, + T: Io + Unpin, { fn is_terminated(&self) -> bool { self.ended @@ -204,7 +204,7 @@ where impl futures::Sink for AsyncWebSocket where - T: Stream + Unpin, + T: Io + Unpin, { type Error = ProtocolError; diff --git a/src/cli/service/echo.rs b/src/cli/service/echo.rs index 0b9157c42..165bc9887 100644 --- a/src/cli/service/echo.rs +++ b/src/cli/service/echo.rs @@ -28,7 +28,10 @@ use crate::{ proto::h2::PseudoHeaderOrder, server::{HttpServer, layer::upgrade::UpgradeLayer}, service::web::{extract::Json, response::IntoResponse}, - ws::handshake::server::{WebSocketAcceptor, WebSocketEchoService, WebSocketMatcher}, + ws::handshake::{ + matcher::WebSocketMatcher, + server::{WebSocketAcceptor, WebSocketEchoService}, + }, }, layer::limit::policy::UnlimitedPolicy, layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy}, @@ -279,14 +282,14 @@ where let http_transport_service = match self.http_version { Some(Version::HTTP_2) => Either3::A({ - let mut http = HttpServer::h2(exec); + let mut http = HttpServer::new_h2(exec); if self.ws_support { http.h2_mut().set_enable_connect_protocol(); } http.service(http_service) }), Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => { - Either3::B(HttpServer::http1(exec).service(http_service)) + Either3::B(HttpServer::new_http1(exec).service(http_service)) } Some(version) => { return Err(BoxError::from("unsupported http version") diff --git a/src/cli/service/fs.rs b/src/cli/service/fs.rs index 44cffbfea..bb47df36e 100644 --- a/src/cli/service/fs.rs +++ b/src/cli/service/fs.rs @@ -271,9 +271,9 @@ where ); let http_transport_service = match self.http_version { - Some(Version::HTTP_2) => Either3::A(HttpServer::h2(executor).service(http_service)), + Some(Version::HTTP_2) => Either3::A(HttpServer::new_h2(executor).service(http_service)), Some(Version::HTTP_11 | Version::HTTP_10 | Version::HTTP_09) => { - Either3::B(HttpServer::http1(executor).service(http_service)) + Either3::B(HttpServer::new_http1(executor).service(http_service)) } Some(version) => { return Err(BoxError::from("unsupported http version") diff --git a/src/cli/service/ip.rs b/src/cli/service/ip.rs index 32671472c..293a17da3 100644 --- a/src/cli/service/ip.rs +++ b/src/cli/service/ip.rs @@ -22,13 +22,13 @@ use crate::{ server::HttpServer, service::web::response::{Html, IntoResponse, Json, Redirect}, }, + io::Io, layer::limit::policy::UnlimitedPolicy, layer::{ConsumeErrLayer, LimitLayer, TimeoutLayer, limit::policy::ConcurrentPolicy}, net::forwarded::Forwarded, net::stream::{SocketInfo, layer::http::BodyLimitLayer}, proxy::haproxy::server::HaProxyLayer, rt::Executor, - stream::Stream, tcp::TcpStream, telemetry::tracing, }; @@ -253,7 +253,7 @@ struct TcpIpService; impl Service for TcpIpService where - Input: Stream + Unpin + ExtensionsRef, + Input: Io + Unpin + ExtensionsRef, { type Output = (); type Error = BoxError; @@ -322,7 +322,7 @@ impl IpServiceBuilder { } impl IpServiceBuilder { - fn build_tcp( + fn build_tcp( self, #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option< TlsAcceptorLayer, @@ -357,7 +357,7 @@ impl IpServiceBuilder { Ok(tcp_service_builder.into_layer(TcpIpService)) } - fn build_http( + fn build_http( self, executor: Executor, #[cfg(any(feature = "rustls", feature = "boring"))] maybe_tls_accept_layer: Option< diff --git a/src/http/client/proxy_connector.rs b/src/http/client/proxy_connector.rs index f7534f296..f617a1529 100644 --- a/src/http/client/proxy_connector.rs +++ b/src/http/client/proxy_connector.rs @@ -5,6 +5,7 @@ use crate::{ http::client::proxy::layer::{ HttpProxyConnector, HttpProxyConnectorLayer, MaybeHttpProxiedConnection, }, + io::Io, net::{ Protocol, address::ProxyAddress, @@ -12,7 +13,6 @@ use crate::{ transport::TryRefIntoTransportContext, }, proxy::socks5::{Socks5ProxyConnector, Socks5ProxyConnectorLayer}, - stream::Stream, telemetry::tracing, }; use pin_project_lite::pin_project; @@ -79,7 +79,7 @@ impl ProxyConnector { impl Service for ProxyConnector where - S: ConnectorService, + S: ConnectorService, Input: TryRefIntoTransportContext + Send + 'static> + Send + ExtensionsMut diff --git a/src/http/mod.rs b/src/http/mod.rs index 8b291cbb1..750c8e11b 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -40,6 +40,11 @@ pub mod client; #[doc(inline)] pub use ::rama_http_backend::server; +#[cfg(feature = "http-full")] +#[cfg_attr(docsrs, doc(cfg(feature = "http-full")))] +#[doc(inline)] +pub use ::rama_http_backend::proxy; + #[cfg(feature = "ws")] #[cfg_attr(docsrs, doc(cfg(feature = "ws")))] #[doc(inline)] diff --git a/src/lib.rs b/src/lib.rs index a6eb0aef8..976e07abc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,6 +176,7 @@ //! used by all other `rama` code, as well as some other _core_ utilities //! - [`rama-crypto`](https://crates.io/crates/rama-crypto): rama crypto primitives and dependencies //! - [`rama-net`](https://crates.io/crates/rama-net): rama network types and utilities +//! - [`rama-net-apple-networkextension`](https://crates.io/crates/rama-net-apple-networkextension): Apple Network Extension support for rama //! - [`rama-dns`](https://crates.io/crates/rama-dns): DNS support for rama //! - [`rama-unix`](https://crates.io/crates/rama-unix): Unix (domain) socket support for rama //! - [`rama-tcp`](https://crates.io/crates/rama-tcp): TCP support for rama @@ -224,7 +225,7 @@ //! - rama crates avoid `unsafe` Rust as much as possible and use it only where necessary. //! - Supply chain auditing is done with [`cargo vet`](https://github.com/mozilla/cargo-vet). //! - Tier 1 platforms include macOS, Linux and Windows on modern architectures. -//! - The minimum supported Rust version (MSRV) is `1.91`. +//! - The minimum supported Rust version (MSRV) is `1.93`. //! //! For details see the compatibility section in the README and the CI configuration in //! the repository. @@ -269,7 +270,7 @@ #[doc(inline)] pub use ::rama_core::{ Layer, Service, ServiceInput, bytes, combinators, conversion, error, extensions, futures, - graceful, layer, matcher, rt, service, stream, username, + graceful, io, layer, matcher, rt, service, stream, username, }; #[cfg(feature = "crypto")] @@ -307,9 +308,22 @@ pub mod tls; pub use ::rama_dns as dns; #[cfg(feature = "net")] -#[cfg_attr(docsrs, doc(cfg(feature = "net")))] -#[doc(inline)] -pub use ::rama_net as net; +pub mod net { + #[cfg_attr(docsrs, doc(cfg(feature = "net")))] + #[doc(inline)] + pub use ::rama_net::*; + + #[cfg(all(target_vendor = "apple", feature = "net-apple-networkextension"))] + #[cfg_attr( + docsrs, + doc(cfg(all(target_vendor = "apple", feature = "net-apple-networkextension"))) + )] + pub mod apple { + //! Apple (vendor) specific network modules + #[doc(inline)] + pub use ::rama_net_apple_networkextension as networkextension; + } +} #[cfg(feature = "http")] #[cfg_attr(docsrs, doc(cfg(feature = "http")))] diff --git a/tests/integration/cli/cli_tests/serve_discard.rs b/tests/integration/cli/cli_tests/serve_discard.rs index 889714f03..c2b040cc6 100644 --- a/tests/integration/cli/cli_tests/serve_discard.rs +++ b/tests/integration/cli/cli_tests/serve_discard.rs @@ -1,6 +1,6 @@ use rama::{ extensions::Extensions, net::address::SocketAddress, rt::Executor, - tcp::client::default_tcp_connect, telemetry::tracing, udp::bind_udp, + tcp::client::default_tcp_connect, telemetry::tracing, udp::bind_udp_with_address, }; use rama_net::address::HostWithPort; @@ -81,7 +81,7 @@ async fn test_tls_tcp_discard() { TlsConnectorDataBuilder::new().with_server_verify_mode(ServerVerifyMode::Disable), )); match connector - .connect(TcpRequest::new(([127, 0, 0, 1], 63115).into())) + .connect(TcpRequest::new(HostWithPort::local_ipv4(63115))) .await { Ok(EstablishedClientConnection { conn, .. }) => { @@ -121,7 +121,9 @@ async fn test_udp_discard() { utils::init_tracing(); let _guard = utils::RamaService::serve_discard(63116, "udp"); - let socket = bind_udp(SocketAddress::local_ipv4(63117)).await.unwrap(); + let socket = bind_udp_with_address(SocketAddress::local_ipv4(63117)) + .await + .unwrap(); for i in 0..5 { match socket diff --git a/tests/integration/cli/cli_tests/serve_echo.rs b/tests/integration/cli/cli_tests/serve_echo.rs index 82c19ee7c..ed7fc93c8 100644 --- a/tests/integration/cli/cli_tests/serve_echo.rs +++ b/tests/integration/cli/cli_tests/serve_echo.rs @@ -5,14 +5,13 @@ use rama::{ client::EasyHttpWebClient, headers::SecWebSocketProtocol, ws::handshake::client::HttpClientWebSocketExt, }, - net::address::SocketAddress, + net::address::{HostWithPort, SocketAddress}, rt::Executor, tcp::client::default_tcp_connect, telemetry::tracing, - udp::bind_udp, + udp::bind_udp_with_address, utils::str::non_empty_str, }; -use rama_net::address::HostWithPort; #[cfg(feature = "boring")] use ::{ @@ -155,7 +154,7 @@ async fn test_tls_tcp_echo() { TlsConnectorDataBuilder::new().with_server_verify_mode(ServerVerifyMode::Disable), )); match connector - .connect(TcpRequest::new(([127, 0, 0, 1], 63111).into())) + .connect(TcpRequest::new(HostWithPort::local_ipv4(63111))) .await { Ok(EstablishedClientConnection { conn, .. }) => { @@ -182,7 +181,9 @@ async fn test_udp_echo() { utils::init_tracing(); let _guard = utils::RamaService::serve_echo(63112, utils::EchoMode::Udp); - let socket = bind_udp(SocketAddress::local_ipv4(63113)).await.unwrap(); + let socket = bind_udp_with_address(SocketAddress::local_ipv4(63113)) + .await + .unwrap(); for i in 0..5 { match socket @@ -408,7 +409,7 @@ async fn test_https_with_remote_tls_cert_issuer() { utils::init_tracing(); let (ca_issuer_cert, ca_issuer_key) = - boring_server_utils::self_signed_server_ca(&SelfSignedData::default()).unwrap(); + boring_server_utils::self_signed_server_auth_gen_ca(&SelfSignedData::default()).unwrap(); let (issuer_server_cert, issuer_server_key) = boring_server_utils::self_signed_server_auth_gen_cert( &SelfSignedData { @@ -505,7 +506,7 @@ async fn test_https_with_remote_tls_cert_issuer() { tracing::info!("spawning tcp listener for remote tls issuer"); - let tpc_listener = TcpListener::bind("[::1]:63132", Executor::default()) + let tpc_listener = TcpListener::bind_address("[::1]:63132", Executor::default()) .await .unwrap(); diff --git a/tests/integration/cli/cli_tests/serve_ip.rs b/tests/integration/cli/cli_tests/serve_ip.rs index 662f6ca2c..0ea1e1429 100644 --- a/tests/integration/cli/cli_tests/serve_ip.rs +++ b/tests/integration/cli/cli_tests/serve_ip.rs @@ -110,7 +110,7 @@ async fn test_tls_tcp_ip() { TlsConnectorDataBuilder::new().with_server_verify_mode(ServerVerifyMode::Disable), )); match connector - .connect(TcpRequest::new(([127, 0, 0, 1], 63120).into())) + .connect(TcpRequest::new(HostWithPort::local_ipv4(63120))) .await { Ok(EstablishedClientConnection { conn, .. }) => { diff --git a/tests/integration/client/mod.rs b/tests/integration/client/mod.rs index 02f823b6e..369914c97 100644 --- a/tests/integration/client/mod.rs +++ b/tests/integration/client/mod.rs @@ -25,7 +25,7 @@ use std::{convert::Infallible, time::Duration}; #[tokio::test] async fn h2_with_connection_pooling() { let http_server = - HttpServer::h2(Executor::default()).service(service_fn(async |req: Request| { + HttpServer::new_h2(Executor::default()).service(service_fn(async |req: Request| { // We are actually quite forgiving when we receive a http1 request instead of H2, // if we see a Host header here it means we received http1, something we don't expect. assert_eq!(req.headers().get(HOST), None); @@ -75,7 +75,7 @@ async fn h2_with_connection_pooling() { #[tokio::test] async fn h1_with_connection_pooling_detects_closed_connections() { let http_server = - HttpServer::http1(Executor::default()).service(service_fn(async |_req: Request| { + HttpServer::new_http1(Executor::default()).service(service_fn(async |_req: Request| { let mut resp = Response::new(Body::empty()); resp.headers_mut() .insert(header::CONNECTION, HeaderValue::from_static("close")); diff --git a/tests/integration/examples/example_tests/http_https_socks5_and_socks5h_connect_proxy.rs b/tests/integration/examples/example_tests/http_https_socks5_and_socks5h_connect_proxy.rs index 21732eb98..d8ce9e57f 100644 --- a/tests/integration/examples/example_tests/http_https_socks5_and_socks5h_connect_proxy.rs +++ b/tests/integration/examples/example_tests/http_https_socks5_and_socks5h_connect_proxy.rs @@ -154,9 +154,10 @@ async fn test_http_client_over_socks5_proxy_connect( } async fn spawn_http_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63179), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63179), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() @@ -172,9 +173,10 @@ async fn spawn_http_server() -> SocketAddress { } async fn spawn_https_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63181), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63181), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() diff --git a/tests/integration/examples/example_tests/http_mitm_proxy_boring.rs b/tests/integration/examples/example_tests/http_mitm_proxy_boring.rs index 9f037cebb..0cff91e62 100644 --- a/tests/integration/examples/example_tests/http_mitm_proxy_boring.rs +++ b/tests/integration/examples/example_tests/http_mitm_proxy_boring.rs @@ -21,7 +21,7 @@ use rama::{ Router, response::{Headers, IntoResponse as _, Json}, }, - ws::handshake::server::{WebSocketAcceptor, WebSocketMatcher}, + ws::handshake::{matcher::WebSocketMatcher, server::WebSocketAcceptor}, }, layer::ConsumeErrLayer, net::{address::ProxyAddress, tls::ApplicationProtocol, tls::server::SelfSignedData}, @@ -64,7 +64,7 @@ async fn test_http_mitm_proxy() { }); tokio::spawn(async { - HttpServer::http1(Executor::default()) + HttpServer::new_http1(Executor::default()) .listen( "127.0.0.1:63013", Arc::new(( @@ -153,7 +153,7 @@ async fn test_http_mitm_proxy() { ); tokio::spawn(async { - TcpListener::bind("127.0.0.1:63004", Executor::default()) + TcpListener::bind_address("127.0.0.1:63004", Executor::default()) .await .unwrap_or_else(|e| panic!("bind TCP Listener: secure web service: {e}")) .serve(tcp_service) @@ -169,13 +169,13 @@ async fn test_http_mitm_proxy() { .expect("with env key logger") .build(); - let http_1_over_tls_server = HttpServer::http1(Executor::default()); + let http_1_over_tls_server = HttpServer::new_http1(Executor::default()); let http_1_over_tls_server_tcp = TlsAcceptorLayer::new(data_http1_no_alpn).into_layer( http_1_over_tls_server.service(Arc::new(Router::new().with_get("/ping", "pong"))), ); tokio::spawn(async { - TcpListener::bind("127.0.0.1:63008", Executor::default()) + TcpListener::bind_address("127.0.0.1:63008", Executor::default()) .await .unwrap_or_else(|e| { panic!("bind TCP Listener: secure web service (for h1 traffic): {e}") diff --git a/tests/integration/examples/example_tests/http_mitm_proxy_rustls.rs b/tests/integration/examples/example_tests/http_mitm_proxy_rustls.rs index 4e8054d2e..1a65c7ae9 100644 --- a/tests/integration/examples/example_tests/http_mitm_proxy_rustls.rs +++ b/tests/integration/examples/example_tests/http_mitm_proxy_rustls.rs @@ -54,7 +54,7 @@ async fn test_http_mitm_proxy() { )); tokio::spawn(async { - TcpListener::bind("127.0.0.1:63006", Executor::default()) + TcpListener::bind_address("127.0.0.1:63006", Executor::default()) .await .unwrap_or_else(|e| panic!("bind TCP Listener: secure web service: {e}")) .serve(tcp_service) diff --git a/tests/integration/examples/example_tests/http_mitm_relay_proxy_boring.rs b/tests/integration/examples/example_tests/http_mitm_relay_proxy_boring.rs new file mode 100644 index 000000000..6507cfaab --- /dev/null +++ b/tests/integration/examples/example_tests/http_mitm_relay_proxy_boring.rs @@ -0,0 +1,260 @@ +use std::{convert::Infallible, sync::Arc, time::Duration}; + +use super::utils; + +use rama::{ + Layer, + bytes::Bytes, + extensions::Extensions, + futures::{StreamExt as _, async_stream::stream_fn}, + http::{ + Body, BodyExtractExt, Request, StatusCode, Version, + client::EasyHttpWebClient, + client::proxy::layer::SetProxyAuthHttpHeaderLayer, + headers::ContentType, + layer::compression::{CompressionLayer, predicate::Always}, + layer::retry::{ManagedPolicy, RetryLayer}, + server::HttpServer, + service::client::HttpClientExt as _, + service::web::{ + Router, + response::{Headers, IntoResponse as _, Json}, + }, + }, + layer::ConsumeErrLayer, + net::{address::ProxyAddress, tls::ApplicationProtocol, tls::server::SelfSignedData}, + rt::Executor, + tcp::server::TcpListener, + tls::boring::client::TlsConnectorDataBuilder, + tls::rustls::server::{TlsAcceptorDataBuilder, TlsAcceptorLayer}, + utils::{backoff::ExponentialBackoff, rng::HasherRng}, +}; + +use serde_json::{Value, json}; + +#[tokio::test] +#[ignore] +async fn test_http_mitm_relay_proxy() { + utils::init_tracing(); + + tokio::spawn(async { + HttpServer::auto(Executor::default()) + .listen( + "127.0.0.1:63015", + Arc::new(Router::new().with_get("/{*any}", async |req: Request| { + Json(json!({ + "method": req.method().as_str(), + "path": req.uri().path(), + })) + })), + ) + .await + .unwrap(); + }); + + tokio::spawn(async { + HttpServer::new_http1(Executor::default()) + .listen( + "127.0.0.1:63016", + Arc::new(( + ConsumeErrLayer::default(), + CompressionLayer::new().with_compress_predicate(Always::new()), + ).into_layer(Router::new() + .with_get("/response-stream", async || { + Ok::<_, Infallible>( + ( + Headers::single(ContentType::html_utf8()), + Body::from_stream( + stream_fn(move |mut yielder| async move { + yielder + .yield_item(Bytes::from_static( + b" + + + + Chunked transfer encoding test + +

Chunked transfer encoding test

", + )) + .await; + + tokio::time::sleep(Duration::from_millis(100)).await; + + yielder + .yield_item(Bytes::from_static( + b"
This is a chunked response after 100 ms.
", + )) + .await; + + tokio::time::sleep(Duration::from_secs(1)).await; + + yielder + .yield_item(Bytes::from_static( + b"
This is a chunked response after 1 second. + The server should not close the stream before all chunks are sent to a client.
", + )) + .await; + }) + .map(Ok::<_, Infallible>), + ), + ) + .into_response(), + ) + })), + )) + .await + .unwrap(); + }); + + let data = TlsAcceptorDataBuilder::try_new_self_signed(SelfSignedData { + organisation_name: Some("Example Server Acceptor".to_owned()), + ..Default::default() + }) + .expect("self signed acceptor data") + .with_alpn_protocols_http_auto() + .try_with_env_key_logger() + .expect("with env key logger") + .build(); + + let executor = Executor::default(); + + let mut http_tp = HttpServer::auto(executor); + http_tp.h2_mut().set_enable_connect_protocol(); + let tcp_service = TlsAcceptorLayer::new(data).into_layer(http_tp.service(Arc::new( + Router::new().with_get("/{*any}", async |req: Request| { + Json(json!({ + "method": req.method().as_str(), + "path": req.uri().path(), + })) + }), + ))); + + tokio::spawn(async { + TcpListener::bind_address("127.0.0.1:63017", Executor::default()) + .await + .unwrap_or_else(|e| panic!("bind TCP Listener: secure web service: {e}")) + .serve(tcp_service) + .await; + }); + + let data_http1_no_alpn = TlsAcceptorDataBuilder::try_new_self_signed(SelfSignedData { + organisation_name: Some("Example h1 Server Acceptor".to_owned()), + ..Default::default() + }) + .expect("self signed acceptor data") + .try_with_env_key_logger() + .expect("with env key logger") + .build(); + + let http_1_over_tls_server = HttpServer::new_http1(Executor::default()); + let http_1_over_tls_server_tcp = TlsAcceptorLayer::new(data_http1_no_alpn).into_layer( + http_1_over_tls_server.service(Arc::new(Router::new().with_get("/ping", "pong"))), + ); + + tokio::spawn(async { + TcpListener::bind_address("127.0.0.1:63018", Executor::default()) + .await + .unwrap_or_else(|e| { + panic!("bind TCP Listener: secure web service (for h1 traffic): {e}") + }) + .serve(http_1_over_tls_server_tcp) + .await; + }); + + let runner = utils::ExampleRunner::interactive("http_mitm_relay_proxy_boring", Some("boring")); + + let proxy_address = ProxyAddress::try_from("http://john:secret@127.0.0.1:62049").unwrap(); + + // test http request proxy flow + let result = runner + .get("http://127.0.0.1:63015/foo/bar") + .extension(proxy_address.clone()) + .send() + .await + .unwrap() + .try_into_json::() + .await + .unwrap(); + let expected_value = json!({"method":"GET","path":"/foo/bar"}); + assert_eq!(expected_value, result); + + let mut extensions = Extensions::new(); + extensions.insert(proxy_address.clone()); + + // test transfer chunked encoding over MITM Proxy + for http_version in [Version::HTTP_10, Version::HTTP_11] { + let resp = ( + SetProxyAuthHttpHeaderLayer::default(), + RetryLayer::new( + ManagedPolicy::default().with_backoff( + ExponentialBackoff::new( + Duration::from_millis(100), + Duration::from_secs(60), + 0.01, + HasherRng::default, + ) + .unwrap(), + ), + ), + ) + .into_layer(EasyHttpWebClient::default()) + .get("http://127.0.0.1:63016/response-stream") + .version(http_version) + .extension(proxy_address.clone()) + .send() + .await + .unwrap(); + + assert_eq!(StatusCode::OK, resp.status()); + + assert!(!resp.headers().contains_key("content-length")); + + let payload = resp.try_into_string().await.unwrap(); + assert!(payload.contains("Chunked transfer encoding test")); + assert!(payload.contains("This is a chunked response after 100 ms")); + assert!(payload.contains("all chunks are sent to a client.")); + } + + // test https request proxy flow (without ALPN) + let result = runner + .get("https://127.0.0.1:63017/foo/bar") + .extension(proxy_address.clone()) + .send() + .await + .unwrap() + .try_into_json::() + .await + .unwrap(); + let expected_value = json!({"method":"GET","path":"/foo/bar"}); + assert_eq!(expected_value, result); + + // test https request proxy flow for the different http versions + for desired_app_protocol in [ + None, + Some(ApplicationProtocol::HTTP_10), + Some(ApplicationProtocol::HTTP_11), + Some(ApplicationProtocol::HTTP_2), + ] { + let builder = runner + .get("https://127.0.0.1:63018/ping") + .extension(proxy_address.clone()); + + let builder = if let Some(app_protocol) = desired_app_protocol { + let tls_config = TlsConnectorDataBuilder::new() + .try_with_rama_alpn_protos(&[app_protocol]) + .unwrap(); + builder.extension(tls_config) + } else { + builder + }; + + let pong = builder + .send() + .await + .unwrap() + .try_into_string() + .await + .unwrap(); + assert_eq!("pong", pong); + } +} diff --git a/tests/integration/examples/example_tests/mod.rs b/tests/integration/examples/example_tests/mod.rs index 38872ed8c..7d3b8414e 100644 --- a/tests/integration/examples/example_tests/mod.rs +++ b/tests/integration/examples/example_tests/mod.rs @@ -35,6 +35,8 @@ mod http_listener_hello; mod http_mitm_proxy_boring; #[cfg(all(feature = "http-full", feature = "rustls"))] mod http_mitm_proxy_rustls; +#[cfg(all(feature = "http-full", feature = "rustls", feature = "boring"))] +mod http_mitm_relay_proxy_boring; #[cfg(feature = "http-full")] mod http_nd_json; #[cfg(feature = "http-full")] diff --git a/tests/integration/examples/example_tests/socks5_and_http_proxy.rs b/tests/integration/examples/example_tests/socks5_and_http_proxy.rs index fb2a69db5..35c8878b4 100644 --- a/tests/integration/examples/example_tests/socks5_and_http_proxy.rs +++ b/tests/integration/examples/example_tests/socks5_and_http_proxy.rs @@ -87,9 +87,10 @@ async fn test_http_client_over_socks5_proxy_connect( } async fn spawn_http_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63007), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63007), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() diff --git a/tests/integration/examples/example_tests/socks5_connect_proxy.rs b/tests/integration/examples/example_tests/socks5_connect_proxy.rs index da380560c..e1001ecc8 100644 --- a/tests/integration/examples/example_tests/socks5_connect_proxy.rs +++ b/tests/integration/examples/example_tests/socks5_connect_proxy.rs @@ -77,9 +77,10 @@ async fn test_http_client_over_socks5_proxy_connect( } async fn spawn_http_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63008), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63008), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() diff --git a/tests/integration/examples/example_tests/socks5_connect_proxy_mitm_proxy.rs b/tests/integration/examples/example_tests/socks5_connect_proxy_mitm_proxy.rs index 011897fa0..a548e1d96 100644 --- a/tests/integration/examples/example_tests/socks5_connect_proxy_mitm_proxy.rs +++ b/tests/integration/examples/example_tests/socks5_connect_proxy_mitm_proxy.rs @@ -104,9 +104,10 @@ async fn test_http_client_over_socks5_proxy_connect_with_mitm_cap( } async fn spawn_http_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63009), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63009), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() @@ -122,9 +123,10 @@ async fn spawn_http_server() -> SocketAddress { } async fn spawn_https_server() -> SocketAddress { - let tcp_service = TcpListener::bind(SocketAddress::default_ipv4(63010), Executor::default()) - .await - .expect("bind HTTP server on open port"); + let tcp_service = + TcpListener::bind_address(SocketAddress::default_ipv4(63010), Executor::default()) + .await + .expect("bind HTTP server on open port"); let bind_addr = tcp_service .local_addr() diff --git a/tests/integration/examples/example_tests/tls_sni_proxy_mitm.rs b/tests/integration/examples/example_tests/tls_sni_proxy_mitm.rs index df322b04b..c63f87816 100644 --- a/tests/integration/examples/example_tests/tls_sni_proxy_mitm.rs +++ b/tests/integration/examples/example_tests/tls_sni_proxy_mitm.rs @@ -91,7 +91,7 @@ async fn spawn_test_egres_server() { HttpServer::default().service("tls-sni-proxy-mitm-example".into_endpoint_service()), ); - let listener = TcpListener::bind("127.0.0.1:63015", Executor::default()) + let listener = TcpListener::bind_address("127.0.0.1:63015", Executor::default()) .await .unwrap_or_else(|e| panic!("bind TCP Listener: secure web service: {e}")); diff --git a/tests/turmoil/http.rs b/tests/turmoil/http.rs index 0767e1a4c..45a3afd2f 100644 --- a/tests/turmoil/http.rs +++ b/tests/turmoil/http.rs @@ -39,7 +39,7 @@ async fn start_server( let (conn, _) = conn_result?; let conn = TcpStream::new(conn); - let server = HttpServer::http1(Executor::default()); + let server = HttpServer::new_http1(Executor::default()); server .serve( conn,