diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0e7910f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,119 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + merge_group: + +permissions: + contents: read + +env: + RUSTFLAGS: -Dwarnings + RUST_BACKTRACE: 1 + +jobs: + test: + name: Test (Rust ${{matrix.toolchain}}, target ${{matrix.target}}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + toolchain: ["nightly", "beta", "stable"] + target: ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl"] + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{matrix.toolchain}} + components: llvm-tools, clippy, rust-src + - uses: taiki-e/install-action@v2 + with: + tool: just,cargo-llvm-cov,cargo-nextest + - name: Enable type layout randomization + if: matrix.toolchain == 'nightly' + run: echo RUSTFLAGS=${RUSTFLAGS}\ -Zrandomize-layout >> $GITHUB_ENV + - run: sudo apt-get update && sudo apt-get install -y musl-tools + if: endsWith(matrix.target, 'musl') + - run: rustup target add ${{matrix.target}} + - run: just example client --target ${{matrix.target}} + - run: just build --tests --release --target ${{matrix.target}} + - run: just ci-test --target ${{matrix.target}} + + msrv: + name: MSRV + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.77.0 + - uses: taiki-e/install-action@v2 + with: + tool: just + - run: just build --package ktls + - run: just build --package ktls-sys + - run: just build --package ktls-test + - run: just build --package ktls-util + + doc: + name: Documentation + runs-on: ubuntu-latest + timeout-minutes: 45 + env: + RUSTDOCFLAGS: -Dwarnings + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/install@cargo-docs-rs + - run: cargo docs-rs --package ktls + + clippy: + name: Clippy + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.77.0 + components: clippy + - uses: taiki-e/install-action@v2 + with: + tool: just + - run: just clippy + + coverage: + name: Test Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.77.0 + components: llvm-tools, clippy, rust-src + - uses: taiki-e/install-action@v2 + with: + tool: just,cargo-llvm-cov,cargo-nextest + - run: just example client + - run: just build --tests --release + - run: just ci-test + - name: Upload coverage information + run: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + ./codecov diff --git a/.github/workflows/kernel-compatibility-test.yml b/.github/workflows/kernel-compatibility-test.yml new file mode 100644 index 0000000..ba55804 --- /dev/null +++ b/.github/workflows/kernel-compatibility-test.yml @@ -0,0 +1,187 @@ +# Credits: https://github.com/tokio-rs/io-uring/blob/master/.github/workflows/kernel-version-test.yml +# +# Tests kTLS functionality across multiple kernel versions. +# Default matrix: 6.12, 6.6, 6.1, 5.15, 5.10, 5.4 +# Manual trigger supports custom space-separated version list. + +name: Kernel Compatibility Test + +on: + push: + branches: ["main"] + pull_request: + merge_group: + workflow_dispatch: + inputs: + kernel_versions: + description: "Space-separated list of Linux kernel versions to test (e.g., '6.12 6.6 6.1.148 5.15.189 5.10.240 5.4.296')" + required: true + +permissions: + contents: read + +jobs: + prepare-matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Set matrix + id: set-matrix + run: | + if [ -n "${GITHUB_EVENT_INPUTS_KERNEL_VERSIONS}" ]; then + # Manual trigger with custom versions + versions="${GITHUB_EVENT_INPUTS_KERNEL_VERSIONS}" + echo "Using manual input versions: $versions" + else + # Default versions for push events + versions="6.12 6.6 6.1.148 5.15.189 5.10.240 5.4.296" + echo "Using default versions: $versions" + fi + + # Convert space-separated list to JSON array + json_array=$(echo "$versions" | tr ' ' '\n' | jq -R . | jq -s -c .) + echo "matrix={\"kernel_version\":$json_array}" >> $GITHUB_OUTPUT + echo "Generated matrix: {\"kernel_version\":$json_array}" + env: + GITHUB_EVENT_INPUTS_KERNEL_VERSIONS: ${{ github.event.inputs.kernel_versions }} + + build: + needs: prepare-matrix + runs-on: ubuntu-latest + strategy: + matrix: ${{fromJson(needs.prepare-matrix.outputs.matrix)}} + fail-fast: false + env: + KERNEL_VERSION: ${{ matrix.kernel_version }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + bison flex libelf-dev \ + qemu-system-x86 busybox-static cpio xz-utils wget e2fsprogs \ + musl-tools + + - name: Install Rust 1.77.0 + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.77.0 + targets: x86_64-unknown-linux-musl + + - name: Generate the test binary + run: | + cargo build --package ktls-test --example client --release --target x86_64-unknown-linux-musl + + - name: Cache Linux source + id: cache-kernel + uses: actions/cache@v4 + with: + path: linux-${{ env.KERNEL_VERSION }} + key: kernel-${{ env.KERNEL_VERSION }} + + - name: Download & build Linux kernel + if: steps.cache-kernel.outputs.cache-hit != 'true' + run: | + MAJOR=${KERNEL_VERSION%%.*} + wget https://cdn.kernel.org/pub/linux/kernel/v${MAJOR}.x/linux-${KERNEL_VERSION}.tar.xz + tar xf linux-${KERNEL_VERSION}.tar.xz + cd linux-${KERNEL_VERSION} + + # Generate the default config + make defconfig + + # Enable essentials as built-ins + scripts/config --enable CONFIG_DEVTMPFS + scripts/config --enable CONFIG_DEVTMPFS_MOUNT + + # Enable virtio drivers + scripts/config --enable CONFIG_VIRTIO + scripts/config --enable CONFIG_VIRTIO_PCI + scripts/config --enable CONFIG_VIRTIO_BLK + + # Enable kTLS support + scripts/config --enable CONFIG_TLS + scripts/config --enable CONFIG_TLS_DEVICE + + # Generate the updated config + make olddefconfig + + make -j$(nproc) + + - name: Prepare initramfs + tests binaries + run: | + rm -rf initramfs && mkdir -p initramfs/{bin,sbin,proc,sys,tmp} + + # Copy the test binary + cp target/x86_64-unknown-linux-musl/release/examples/client initramfs/bin/ktls-test + + # Add necessary binaries from busybox + cp /usr/bin/busybox initramfs/bin/ + for cmd in sh mount ip ifconfig cat; do ln -sf busybox initramfs/bin/$cmd; done + ln -sf ../bin/busybox initramfs/sbin/poweroff + + # Generate init script + cat > initramfs/init << 'EOF' + #!/bin/sh + set -e + + # Activating the loopback interface (it's required for some network tests) + ip link set lo up + + mkdir -p /dev + + # Enable necessary devices + # https://www.kernel.org/doc/Documentation/admin-guide/devices.txt + mknod /dev/port c 1 4 + mknod /dev/null c 1 3 + mknod /dev/zero c 1 5 + mknod /dev/tty c 5 0 + + mkdir -p /tmp && mount -t tmpfs -o mode=1777 tmpfs /tmp + + # Bring up ext4 test volume at /mnt + mount -t devtmpfs devtmpfs /dev + + exit_code=0 + + # Run the test binary + RUST_BACKTRACE=1 /bin/ktls-test || exit_code=1 + + # If the test binary exited with a non-zero code, write it to /dev/port. + # This lets QEMU exit with non-zero exit-code, triggering a CI error. + [ $exit_code -eq 0 ] || printf '\x01' \ + | dd of=/dev/port bs=1 seek=244 count=1 2>/dev/null + + /sbin/poweroff -f + + EOF + + chmod +x initramfs/init + + # Pack into a CPIO archive + (cd initramfs && find . -print0 \ + | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz) + + - name: Run tests in QEMU + run: | + qemu-system-x86_64 \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -kernel linux-${KERNEL_VERSION}/arch/x86/boot/bzImage \ + -initrd initramfs.cpio.gz \ + -netdev user,id=net0 \ + -device e1000,netdev=net0 \ + -append "console=ttyS0 rootfstype=ramfs panic=1" \ + -nographic -no-reboot -m 1024 -action panic=exit-failure + + if [ $? -ne 0 ]; then + echo "tests failed (QEMU exited abnormally)" + exit 1 + else + echo "all tests passed" + fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 751f67b..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: test - -on: - push: - branches: ["main"] - pull_request: - merge_group: - -jobs: - test: - name: test - runs-on: ubuntu-latest - env: - RUSTC_WRAPPER: sccache - SCCACHE_GHA_ENABLED: true - CARGO_INCREMENTAL: 0 - steps: - - name: Check out repository code - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - with: - components: llvm-tools, clippy, rust-src - - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.9 - - uses: taiki-e/install-action@v2 - with: - tool: just,cargo-llvm-cov,cargo-nextest - - name: Run tests - run: | - cd ${{ github.workspace }} - just clippy --all-targets - RUST_BACKTRACE=1 just ci-test - - name: Upload coverage information - run: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov - ./codecov - - msrv: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@master - with: - toolchain: 1.75.0 - components: llvm-tools, clippy, rust-src - - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.9 - - uses: taiki-e/install-action@v2 - with: - tool: just - - name: Check library code with minimum supported Rust version - run: | - just check --lib --locked diff --git a/Cargo.lock b/Cargo.lock index f34db36..87ea4ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" @@ -28,38 +28,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "aws-lc-rs" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.28.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f7720b74ed28ca77f90769a71fd8c637a0137f6fae4ae947e1050229cff57f" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -76,34 +53,11 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "itertools", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn", - "which", -] - [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bytes" @@ -113,29 +67,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.18" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ - "jobserver", - "libc", "shlex", ] -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -143,26 +86,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[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.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "deranged" version = "0.4.0" @@ -172,98 +95,27 @@ dependencies = [ "powerfmt", ] -[[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 = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", ] [[package]] @@ -273,52 +125,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "glob" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "home" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "indexmap" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itertools" -version = "0.12.1" +name = "io-uring" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "either", -] - -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.2", + "bitflags", + "cfg-if", "libc", ] @@ -326,25 +139,14 @@ dependencies = [ name = "ktls" version = "6.0.2" dependencies = [ - "futures-util", - "ktls-sys 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static", + "bitflags", "libc", - "memoffset", + "log", "nix", - "num_enum", - "oorandom", "pin-project-lite", - "rcgen", "rustls", - "smallvec", - "socket2", - "test-case", - "thiserror", "tokio", - "tokio-rustls", "tracing", - "tracing-subscriber", ] [[package]] @@ -352,10 +154,31 @@ name = "ktls-sys" version = "1.0.2" [[package]] -name = "ktls-sys" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ed84c81d133bc00e291085cff51c15cb07d3cede0aade1f2d82dd33df82d7e7" +name = "ktls-test" +version = "0.0.0" +dependencies = [ + "bitflags", + "ktls", + "ktls-util", + "nix", + "rand", + "rcgen", + "rustls", + "test-case", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "ktls-util" +version = "0.0.0" +dependencies = [ + "ktls", + "rustls", + "thiserror", + "tokio", +] [[package]] name = "lazy_static" @@ -363,39 +186,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" -version = "0.2.171" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" - -[[package]] -name = "libloading" -version = "0.8.6" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" -dependencies = [ - "cfg-if", - "windows-targets", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -418,9 +219,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -431,37 +232,31 @@ dependencies = [ "autocfg", ] -[[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.7" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags", "cfg-if", @@ -470,16 +265,6 @@ dependencies = [ "memoffset", ] -[[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 = "nu-ansi-term" version = "0.46.0" @@ -496,27 +281,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num_enum" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" -dependencies = [ - "num_enum_derive", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "object" version = "0.36.7" @@ -532,12 +296,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "overload" version = "0.1.1" @@ -546,9 +304,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -556,9 +314,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -583,12 +341,6 @@ 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 = "powerfmt" version = "0.2.0" @@ -596,29 +348,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "prettyplease" -version = "0.2.32" +name = "ppv-lite86" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", + "zerocopy", ] [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -634,15 +376,44 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7" dependencies = [ "pem", "ring", @@ -653,23 +424,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.11" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", ] [[package]] @@ -683,13 +454,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax 0.8.6", ] [[package]] @@ -700,9 +471,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "ring" @@ -712,7 +483,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -720,36 +491,16 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustc-hash" -version = "1.1.0" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -760,17 +511,19 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -819,36 +572,33 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.9" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -859,9 +609,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -903,18 +653,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -923,12 +673,11 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", - "once_cell", ] [[package]] @@ -952,20 +701,21 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "tokio" -version = "1.44.2" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", + "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -979,33 +729,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" - -[[package]] -name = "toml_edit" -version = "0.22.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - [[package]] name = "tracing" version = "0.1.41" @@ -1019,9 +742,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", @@ -1030,9 +753,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", "valuable", @@ -1058,6 +781,7 @@ dependencies = [ "matchers", "nu-ansi-term", "once_cell", + "parking_lot", "regex", "sharded-slab", "smallvec", @@ -1087,29 +811,17 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "which" -version = "4.4.2" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "either", - "home", - "once_cell", - "rustix", + "wit-bindgen", ] [[package]] @@ -1217,30 +929,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.7.6" +name = "wit-bindgen" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10" +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "memchr", + "time", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "zerocopy" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "bitflags", + "zerocopy-derive", ] [[package]] -name = "yasna" -version = "0.5.2" +name = "zerocopy-derive" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ - "time", + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a2572f3..56eb375 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,14 @@ [workspace] -members = ["ktls", "ktls-sys"] +members = ["ktls", "ktls-sys", "ktls-test", "ktls-util"] resolver = "2" + +[workspace.package] +edition = "2021" +rust-version = "1.77.0" +# === Publication info === +authors = ["Amos Wenger "] +categories = ["network-programming"] +keywords = ["ktls", "linux", "tls", "rustls"] +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/rustls/ktls" diff --git a/Justfile b/Justfile index 1c1059f..e87eb07 100644 --- a/Justfile +++ b/Justfile @@ -4,35 +4,45 @@ _default: just --list # Run all tests with nextest and cargo-llvm-cov -ci-test: +ci-test *args: #!/bin/bash -eux - for backend in ring aws_lc_rs; do - cargo llvm-cov nextest --no-default-features --features tls12,$backend --lcov --output-path coverage.lcov - done + cargo llvm-cov nextest {{args}} --locked --all-features --lcov --output-path coverage.lcov -# Show coverage locally -cov: - #!/bin/bash -eux - cargo llvm-cov nextest --hide-instantiations --html --output-dir coverage +# =========== LOCAL COMMANDS =========== -t *args: - just test {{args}} +build *args: + cargo build {{args}} --locked -# Run all tests -test *args: +b *args: + just build {{args}} + +# Show coverage locally +cov *args: #!/bin/bash -eux - export RUST_BACKTRACE=1 - for feature in ring aws_lc_rs; do - cargo nextest run --no-default-features --features tls12,$feature {{args}} - done + cargo llvm-cov nextest {{args}} --locked --all-features --hide-instantiations --html --output-dir coverage + +check *args: + cargo check {{args}} --locked --all-features c *args: - just check {{args}} + just check {{args}} clippy *args: - cargo clippy {{args}} --no-default-features --features tls12,ring - cargo clippy {{args}} --no-default-features --features tls12,aws_lc_rs + cargo clippy {{args}} --locked --all-features -- -Dclippy::all -Dclippy::pedantic -check *args: - cargo check {{args}} --no-default-features --features tls12,ring - cargo check {{args}} --no-default-features --features tls12,aws_lc_rs +example *args: + cargo run --example {{args}} + +e *args: + just example {{args}} + +msrv *args: + cargo +1.77.0 clippy {{args}} --locked --all-features -- -Dclippy::all -Dclippy::pedantic + +t *args: + just test {{args}} + +test *args: + #!/bin/bash -eux + export RUST_BACKTRACE=1 + cargo nextest run {{args}} --locked --all-features diff --git a/README.md b/README.md index f56866d..27ff580 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -[![test pipeline](https://github.com/hapsoc/ktls/actions/workflows/test.yml/badge.svg)](https://github.com/hapsoc/ktls/actions/workflows/test.yml?query=branch%3Amain) -[![Coverage Status (codecov.io)](https://codecov.io/gh/hapsoc/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/hapsoc/ktls/) -[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) +[![Test pipeline](https://github.com/rustls/ktls/actions/workflows/ci.yml/badge.svg)](https://github.com/rustls/ktls/actions/workflows/ci.yml?query=branch%3Amain) +[![Coverage Status (codecov.io)](https://codecov.io/gh/rustls/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/rustls/ktls/) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) # ktls This repository hosts both: - * [ktls](./ktls): higher-level, safe wrappers over kTLS - * [ktls-sys](./ktls-sys): the raw system interface for kTLS on Linux + * [ktls](./ktls): high-level APIs for configuring kTLS (kernel TLS offload) on top of [rustls]. + * [ktls-util](./ktls-util): utilities for crate `ktls`. + * [ktls-sys](./ktls-sys): the raw system interface for kTLS on Linux (deprecated). ## License diff --git a/ktls-sys/Cargo.lock b/ktls-sys/Cargo.lock deleted file mode 100644 index 5abe3bb..0000000 --- a/ktls-sys/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "ktls-sys" -version = "1.0.1" diff --git a/ktls-sys/Cargo.toml b/ktls-sys/Cargo.toml index b94ea41..bd6223c 100644 --- a/ktls-sys/Cargo.toml +++ b/ktls-sys/Cargo.toml @@ -1,15 +1,17 @@ [package] name = "ktls-sys" version = "1.0.2" -edition = "2021" -license = "MIT OR Apache-2.0" -repository = "https://github.com/rustls/ktls-sys" -documentation = "https://docs.rs/ktls-sys" -authors = ["Amos Wenger "] -readme = "README.md" +edition.workspace = true +rust-version = "1.75.0" +# === Publication info === +authors.workspace = true +categories.workspace = true description = """ FFI bindings for `linux/tls.h` """ -rust-version = "1.75" +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true [dependencies] diff --git a/ktls-sys/Justfile b/ktls-sys/Justfile deleted file mode 100644 index 034b5f0..0000000 --- a/ktls-sys/Justfile +++ /dev/null @@ -1,16 +0,0 @@ -# just manual: https://github.com/casey/just#readme - -_default: - just --list - -# Run all tests with nextest and cargo-llvm-cov -ci-test: - #!/bin/bash -eux - cargo llvm-cov nextest --lcov --output-path coverage.lcov - -# Run all tests with cargo nextest -test *args: - RUST_BACKTRACE=1 cargo nextest run {{args}} - -check: - cargo clippy --all-targets diff --git a/ktls-sys/README.md b/ktls-sys/README.md index 2cee2cd..7aa372e 100644 --- a/ktls-sys/README.md +++ b/ktls-sys/README.md @@ -1,15 +1,19 @@ -[![test pipeline](https://github.com/hapsoc/ktls/actions/workflows/test.yml/badge.svg)](https://github.com/hapsoc/ktls/actions/workflows/test.yml?query=branch%3Amain) -[![Coverage Status (codecov.io)](https://codecov.io/gh/hapsoc/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/hapsoc/ktls/) [![Crates.io](https://img.shields.io/crates/v/ktls-sys)](https://crates.io/crates/ktls-sys) -[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) +[![Docs.rs](https://docs.rs/ktls-sys/badge.svg)](https://docs.rs/ktls-sys) +[![Test pipeline](https://github.com/rustls/ktls/actions/workflows/ci.yml/badge.svg)](https://github.com/rustls/ktls/actions/workflows/ci.yml?query=branch%3Amain) +[![Coverage Status (codecov.io)](https://codecov.io/gh/rustls/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/rustls/ktls/) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) # ktls-sys +> [!WARNING] +> This crate is deprecated. + `linux/tls.h` bindings, for TLS kernel offload. Generated with `bindgen tls.h -o src/bindings.rs` -See for a higher-level / safer interface. +See for a higher-level / safer interface. ## License diff --git a/ktls-sys/src/lib.rs b/ktls-sys/src/lib.rs index 742fd1d..bf28466 100644 --- a/ktls-sys/src/lib.rs +++ b/ktls-sys/src/lib.rs @@ -1,4 +1,12 @@ +//! FFI bindings to the kernel TLS (kTLS) API. + +#![allow(clippy::items_after_statements)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::ptr_as_ptr)] +#![allow(clippy::too_many_lines)] #![allow(non_camel_case_types)] #![allow(non_snake_case)] +#[rustfmt::skip] pub mod bindings; diff --git a/ktls-test/Cargo.toml b/ktls-test/Cargo.toml new file mode 100644 index 0000000..fb7dd7b --- /dev/null +++ b/ktls-test/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ktls-test" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +publish = false + +[dependencies] +bitflags = "2.9" +ktls = { path = "../ktls", features = ["raw-api", "tracing", "probe-ktls-compatibility"] } +ktls-util = { path = "../ktls-util" } +nix = "0.30.1" +rand = "0.9.2" +rcgen = { version = "0.14.3" } +rustls = { version = "0.23.27", default-features = false, features = ["std", "ring"] } +tokio = { version = "1.40", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] } +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter", "parking_lot"] } + +[dev-dependencies] +test-case = "3.3.1" diff --git a/ktls-test/examples/client.rs b/ktls-test/examples/client.rs new file mode 100644 index 0000000..c672559 --- /dev/null +++ b/ktls-test/examples/client.rs @@ -0,0 +1,28 @@ +//! Example: TLS client using `ktls`. + +use std::error::Error; + +use ktls_test::common; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let (ret1, ret2, ret3, ret4) = tokio::join!( + common::run_echo_test(common::CloseParty::Client, common::TestOption::empty()), + common::run_echo_test( + common::CloseParty::Client, + common::TestOption::HANDLE_IO_RESULT + ), + common::run_echo_test(common::CloseParty::Server, common::TestOption::empty()), + common::run_echo_test( + common::CloseParty::Server, + common::TestOption::HANDLE_IO_RESULT + ), + ); + + ret1?; + ret2?; + ret3?; + ret4?; + + Ok(()) +} diff --git a/ktls-test/examples/server.rs b/ktls-test/examples/server.rs new file mode 100644 index 0000000..a0c31c3 --- /dev/null +++ b/ktls-test/examples/server.rs @@ -0,0 +1,30 @@ +//! Example: TLS server using `ktls`. + +use ktls_test::common; +use tokio::net::TcpListener; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::new("TRACE")) + .try_init(); + + let Some(compatible_cipher_suites) = common::compatible_cipher_suites() else { + return Ok(()); + }; + + let listener = TcpListener::bind("0.0.0.0:8443").await.expect("Bind error"); + let acceptor = common::server::get_ktls_acceptor(compatible_cipher_suites); + + tokio::select! { + biased; + _ = tokio::signal::ctrl_c() => { + tracing::info!("Received Ctrl + C..."); + } + result = common::echo_server_loop(listener, acceptor) => { + result?; + } + } + + Ok(()) +} diff --git a/ktls-test/src/common.rs b/ktls-test/src/common.rs new file mode 100644 index 0000000..53d47fe --- /dev/null +++ b/ktls-test/src/common.rs @@ -0,0 +1,376 @@ +// Shared code for client and server examples. + +use std::io; +use std::sync::OnceLock; +use std::time::Duration; + +use ktls::utils::CompatibleCipherSuites; +use ktls_util::server::KtlsAcceptor; +use nix::errno::Errno; +use rustls::pki_types::ServerName; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::oneshot; +use tokio::time::sleep; + +pub mod client; +pub mod server; + +#[allow(dead_code)] +#[tracing::instrument(err)] +/// Echo test, shared by examples and tests. +pub async fn run_echo_test(close_party: CloseParty, test_option: TestOption) -> io::Result<()> { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::new("TRACE")) + .pretty() + .try_init(); + + let Some(compatible_cipher_suites) = compatible_cipher_suites() else { + return Ok(()); + }; + + let server_name = ServerName::try_from("localhost").unwrap(); + let listener = TcpListener::bind("0.0.0.0:0").await.expect("Bind error"); + let server_addr = listener.local_addr().expect("Cannot get local_addr"); + + let acceptor = server::get_ktls_acceptor(compatible_cipher_suites); + let connector = client::get_ktls_connector(compatible_cipher_suites); + + // Start echo server + let (shutdown_signal_tx, shutdown_signal_rx) = oneshot::channel::(); + let server_handle = tokio::spawn(echo_server( + listener, + acceptor, + shutdown_signal_rx, + test_option, + )); + + let mut ktls_stream = connector + .try_connect( + TcpStream::connect(server_addr) + .await + .expect("Connect to server error"), + server_name, + ) + .await + .map_err(io::Error::other)?; + + let test_data = test_data(); + + { + // Test: poll_write + ktls_stream.write_all(&test_data[..16]).await?; + tracing::info!("Sent 16 bytes"); + + let mut buf = [0u8; 16]; + + ktls_stream.read_exact(&mut buf).await?; + + assert!(buf == test_data[..16]); + } + + { + // Test: poll_flush + ktls_stream.write_all(&test_data[..16]).await?; + ktls_stream.flush().await?; + tracing::info!("Sent 16 bytes and flushed"); + + let mut buf = [0u8; 16]; + + ktls_stream.read_exact(&mut buf).await?; + + assert!(buf == test_data[..16]); + } + + { + const CHUNK_SIZE: usize = 17; + const VECTORED_WRITE_COUNT: usize = 5; + const TOTAL: usize = CHUNK_SIZE * VECTORED_WRITE_COUNT; + + let bufs: Vec<_> = test_data.chunks(17).map(io::IoSlice::new).take(5).collect(); + + // Test: poll_write_vectored + let mut has_read = ktls_stream.write_vectored(&bufs).await?; + + if let Some(buf) = test_data.get(has_read..TOTAL) { + tracing::warn!( + "Not all data sent, sent {has_read} bytes, remaining {} bytes", + buf.len() + ); + + ktls_stream.write_all(buf).await?; + + has_read += buf.len(); + } + + assert_eq!(has_read, TOTAL); + + let mut buf = [0u8; TOTAL]; + + ktls_stream.read_exact(&mut buf).await?; + + assert!(buf == test_data[..TOTAL]); + } + + { + // Test: large data (> u16::MAX) + ktls_stream.write_all(test_data).await?; + tracing::info!("Sent {} bytes", test_data.len()); + + let mut buf = vec![0u8; test_data.len()]; + + ktls_stream.read_exact(&mut buf).await?; + + assert!(buf == test_data); + } + + match close_party { + CloseParty::Client => { + tracing::info!("Client performing active shutdown"); + ktls_stream.shutdown().await?; + + // Try write after shutdown, should write 0 bytes + let n = ktls_stream.write(b"after shutdown").await?; + assert_eq!(n, 0); + + // Try write vectored after shutdown, should write 0 bytes + let bufs = [io::IoSlice::new(b"after shutdown vectored")]; + let n = ktls_stream.write_vectored(&bufs).await?; + assert_eq!(n, 0); + + // Try flush after shutdown, should be ok + ktls_stream.flush().await?; + + server_handle.await??; + } + CloseParty::Server => { + tracing::info!("Client performing passive shutdown, waiting for server to close"); + + // Notify server to close + shutdown_signal_tx.send(false).unwrap(); + + // Try read after server closed, should read 0 bytes (EOF) + let mut buf = [0u8; 16]; + + let n = ktls_stream.read(&mut buf).await?; + + assert_eq!(n, 0); + } + } + + tracing::info!("Echo test completed: {:#?}", compatible_cipher_suites); + + Ok(()) +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +/// Which party will close the connection actively. +pub enum CloseParty { + /// The client + Client, + + /// The server + Server, +} + +fn test_data() -> &'static [u8] { + static BUFFER: OnceLock> = OnceLock::new(); + + BUFFER.get_or_init(|| { + let mut v = vec![0; u16::MAX as usize + 1]; + + v.fill_with(rand::random); + + v + }) +} + +bitflags::bitflags! { + #[derive(Debug, Clone, Copy)] + /// Options for echo test. + pub struct TestOption: u8 { + /// Whether to test vectored write. + const HANDLE_IO_RESULT = 0b0000_0001; + } +} + +#[allow(dead_code)] +#[tracing::instrument(err)] +/// A simple echo server. +pub async fn echo_server( + listener: TcpListener, + acceptor: KtlsAcceptor, + mut shutdown_signal_rx: oneshot::Receiver, + test_option: TestOption, +) -> io::Result<()> { + match listener.accept().await { + Ok((stream, remote_addr)) => { + tracing::info!("Accepted connection from {remote_addr}"); + + match acceptor.try_accept(stream).await { + Ok(mut ktls_stream) => { + tracing::info!("Connection established with kTLS"); + + let mut buf = [0u8; 1024]; + + loop { + if test_option.contains(TestOption::HANDLE_IO_RESULT) { + tracing::info!("Testing improper usage of `handle_io_result`"); + + // Try read application data with improper usage of handle_io_result + ktls_stream.handle_io_result(Err::<(), _>(Errno::EIO.into()))?; + + // Again + ktls_stream.handle_io_result(Err::<(), _>(Errno::EIO.into()))?; + } + + let ret = tokio::select! { + biased; + is_brutal = &mut shutdown_signal_rx => { + tracing::info!("Received shutdown signal, is_brutal: {:?}", is_brutal); + + if is_brutal.unwrap_or(false) { + // Drop the connection brutally + drop(ktls_stream); + } else { + ktls_stream.shutdown().await.unwrap(); + + // Try write after shutdown, should write 0 bytes + let n = ktls_stream.write(b"after shutdown").await.unwrap(); + + assert_eq!(n, 0); + } + + loop { + sleep(Duration::from_secs(1)).await; + } + } + ret = ktls_stream.read(&mut buf) => ret + }; + + match ret { + Ok(0) => { + tracing::info!("Read EOF, client closed connection"); + break; + } + Ok(n) => { + tracing::trace!("Received {n} bytes"); + + if let Err(e) = ktls_stream.write_all(&buf[..n]).await { + tracing::error!( + "Failed to write to stream from {remote_addr}: {e:#?}" + ); + + break; + } + } + Err(e) => { + tracing::error!( + "Failed to read from stream from {remote_addr}: {e:#?}" + ); + + break; + } + } + } + } + Err(e) => { + tracing::error!("Failed to accept connection from {remote_addr}: {e:#?}"); + + return Err(io::Error::other(e)); + } + } + } + Err(e) => { + tracing::error!("Failed to accept connection: {e:#?}"); + + return Err(e); + } + } + + tracing::info!("Server shutting down"); + + Ok(()) +} + +#[allow(dead_code)] +#[tracing::instrument(err)] +/// A simple echo server. +pub async fn echo_server_loop(listener: TcpListener, acceptor: KtlsAcceptor) -> io::Result<()> { + loop { + match listener.accept().await { + Ok((stream, remote_addr)) => { + tracing::info!("Accepted connection from {remote_addr}"); + + match acceptor.try_accept(stream).await { + Ok(mut ktls_stream) => { + tokio::spawn(async move { + tracing::info!("Connection established with kTLS"); + + let mut buf = [0u8; 1024]; + + loop { + match ktls_stream.read(&mut buf).await { + Ok(0) => { + tracing::info!("Read EOF, client closed connection"); + break; + } + Ok(n) => { + tracing::trace!("Received {n} bytes"); + + if let Err(e) = ktls_stream.write_all(&buf[..n]).await { + tracing::error!( + "Failed to write to stream from {remote_addr}: \ + {e:#?}" + ); + + break; + } + } + Err(e) => { + tracing::error!( + "Failed to read from stream from {remote_addr}: {e:#?}" + ); + + break; + } + } + } + }); + } + Err(e) => { + tracing::error!("Failed to accept connection from {remote_addr}: {e:#?}"); + + return Err(io::Error::other(e)); + } + } + } + Err(e) => { + tracing::error!("Failed to accept connection: {e:#?}"); + + return Err(e); + } + } + } +} + +#[allow(dead_code)] +/// Get the compatible cipher suites for the current kernel. +pub fn compatible_cipher_suites() -> Option<&'static CompatibleCipherSuites> { + static COMPATIBLE_CIPHER_SUITES: OnceLock> = OnceLock::new(); + + COMPATIBLE_CIPHER_SUITES + .get_or_init(|| { + let c = CompatibleCipherSuites::probe().expect("probe error"); + + if let Some(c) = &c { + tracing::info!("Compatible cipher suites: {c:#?}"); + } else { + tracing::info!("The current kernel does not support kTLS"); + } + + c + }) + .as_ref() +} diff --git a/ktls-test/src/common/client.rs b/ktls-test/src/common/client.rs new file mode 100644 index 0000000..c8fe0fb --- /dev/null +++ b/ktls-test/src/common/client.rs @@ -0,0 +1,36 @@ +//! Client + +#![allow(dead_code)] + +use std::sync::{Arc, OnceLock}; + +use ktls::utils::CompatibleCipherSuites; +use ktls_util::client::KtlsConnector; + +pub mod verifier; + +/// Example to get a `KtlsConnector`. +pub fn get_ktls_connector(compatible_cipher_suites: &CompatibleCipherSuites) -> KtlsConnector { + static KTLS_CONNECTOR: OnceLock = OnceLock::new(); + + KTLS_CONNECTOR + .get_or_init(|| { + let mut crypto_provider = rustls::crypto::ring::default_provider(); + + compatible_cipher_suites.filter(&mut crypto_provider.cipher_suites); + + let mut config = rustls::ClientConfig::builder_with_provider(Arc::new(crypto_provider)) + .with_protocol_versions(compatible_cipher_suites.protocol_versions) + .expect("invalid protocol versions") + .dangerous() + .with_custom_certificate_verifier(verifier::NoCertificateVerification::new()) + .with_no_client_auth(); + + config.enable_secret_extraction = true; + + tracing::info!("Client config: {config:#?}"); + + KtlsConnector::new(Arc::new(config)) + }) + .clone() +} diff --git a/ktls-test/src/common/client/verifier.rs b/ktls-test/src/common/client/verifier.rs new file mode 100644 index 0000000..9697cef --- /dev/null +++ b/ktls-test/src/common/client/verifier.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; + +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; +use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; +use rustls::DigitallySignedStruct; + +#[derive(Debug, Clone, Copy)] +pub struct NoCertificateVerification; + +impl NoCertificateVerification { + #[must_use] + pub fn new() -> Arc { + Arc::new(Self) + } +} + +impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _: &CertificateDer<'_>, + _: &[CertificateDer<'_>], + _: &ServerName<'_>, + _: &[u8], + _: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _: &[u8], + _: &CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _: &[u8], + _: &CertificateDer<'_>, + _: &DigitallySignedStruct, + ) -> Result { + Ok(HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + vec![ + rustls::SignatureScheme::RSA_PKCS1_SHA256, + rustls::SignatureScheme::RSA_PKCS1_SHA384, + rustls::SignatureScheme::RSA_PKCS1_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA512, + rustls::SignatureScheme::RSA_PSS_SHA384, + rustls::SignatureScheme::RSA_PSS_SHA256, + rustls::SignatureScheme::ECDSA_NISTP256_SHA256, + rustls::SignatureScheme::ECDSA_NISTP384_SHA384, + rustls::SignatureScheme::ED25519, + ] + } +} diff --git a/ktls-test/src/common/server.rs b/ktls-test/src/common/server.rs new file mode 100644 index 0000000..8a25906 --- /dev/null +++ b/ktls-test/src/common/server.rs @@ -0,0 +1,47 @@ +//! Server + +#![allow(dead_code)] + +use std::sync::{Arc, OnceLock}; + +use ktls::utils::CompatibleCipherSuites; +use ktls_util::server::KtlsAcceptor; +use rcgen::{generate_simple_self_signed, CertifiedKey}; +use rustls::pki_types::PrivateKeyDer; +use rustls::ServerConfig; + +/// Example to get a global `KtlsAcceptor`. +pub fn get_ktls_acceptor(compatible_cipher_suites: &CompatibleCipherSuites) -> KtlsAcceptor { + static KTLS_ACCEPTOR: OnceLock = OnceLock::new(); + + KTLS_ACCEPTOR + .get_or_init(|| { + let subject_alt_names = vec!["localhost".to_string()]; + + let CertifiedKey { cert, signing_key } = + generate_simple_self_signed(subject_alt_names).unwrap(); + + let mut crypto_provider = rustls::crypto::ring::default_provider(); + + compatible_cipher_suites.filter(&mut crypto_provider.cipher_suites); + + let mut config = ServerConfig::builder_with_provider(Arc::new(crypto_provider)) + .with_protocol_versions(compatible_cipher_suites.protocol_versions) + .expect("invalid protocol versions") + .with_no_client_auth() + .with_single_cert( + vec![cert.der().clone()], + PrivateKeyDer::try_from(signing_key.serialized_der()) + .expect("invalid key") + .clone_key(), + ) + .expect("invalid certificate/key"); + + config.enable_secret_extraction = true; + + tracing::info!("Server config: {config:#?}"); + + KtlsAcceptor::new(Arc::new(config)) + }) + .clone() +} diff --git a/ktls-test/src/lib.rs b/ktls-test/src/lib.rs new file mode 100644 index 0000000..c304aff --- /dev/null +++ b/ktls-test/src/lib.rs @@ -0,0 +1,6 @@ +//! For testing ktls only + +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::missing_errors_doc)] + +pub mod common; diff --git a/ktls-test/tests/client.rs b/ktls-test/tests/client.rs new file mode 100644 index 0000000..0d90665 --- /dev/null +++ b/ktls-test/tests/client.rs @@ -0,0 +1,178 @@ +//! Test: client connect to real world websites. + +use std::io; +use std::num::NonZeroUsize; +use std::time::Duration; + +use ktls::KtlsStream; +use ktls_test::common; +use rustls::pki_types::ServerName; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::time::timeout; + +#[test_case::test_matrix( + [ + "www.google.com", // Google CDN + "www.bing.com", // Azure CDN + // "github.com", // Azure CDN + "www.baidu.com", // Baidu CDN + "stackoverflow.com", // Cloudflare CDN + "fastly.com", // Fastly CDN + ] +)] +#[tokio::test] +async fn test_connecct_sites(server_name: &'static str) -> io::Result<()> { + timeout( + Duration::from_secs(10), + test_connecct_sites_impl(server_name), + ) + .await + .unwrap_or_else(|e| { + tracing::warn!("Test to {server_name} timed out?"); + + Err(io::Error::new(io::ErrorKind::TimedOut, e)) + }) +} + +async fn test_connecct_sites_impl(server_name: &'static str) -> io::Result<()> { + let _ = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::new("TRACE")) + .pretty() + .try_init(); + + let Some(compatible_cipher_suites) = common::compatible_cipher_suites() else { + return Ok(()); + }; + + let Ok(Ok(socket)) = timeout( + Duration::from_secs(1), + TcpStream::connect(format!("{server_name}:443")), + ) + .await + else { + tracing::warn!("Failed to connect to {server_name}, skipped."); + + return Ok(()); + }; + + let connector = common::client::get_ktls_connector(compatible_cipher_suites); + + let mut ktls_stream = connector + .try_connect(socket, ServerName::try_from(server_name).unwrap()) + .await + .map_err(io::Error::other)?; + + // Test 1 + tracing::info!("First request to {server_name}"); + http_request(&mut ktls_stream, server_name).await?; + + // Test 2 + tracing::info!("Second request to {server_name}"); + http_request(&mut ktls_stream, server_name).await?; + + Ok(()) +} + +async fn http_request( + ktls_stream: &mut KtlsStream, + server_name: &str, +) -> io::Result<()> { + // Write HTTP/1.1 request + { + ktls_stream + .write_all( + format!( + "GET / HTTP/1.1\r\nHost: {}\r\nconnection: keep-alive\r\naccept-encoding: \ + identity\r\ntransfer-encoding: identity\r\n\r\n", + server_name + ) + .as_bytes(), + ) + .await?; + + tracing::debug!("Request sent to {server_name}"); + + // Read response + let mut response = Vec::new(); + + let mut buf_stream = tokio::io::BufStream::new(ktls_stream); + + let mut content_length = None; + + loop { + let total_has_read = response.len(); + + let has_read = buf_stream.read_until(b'\n', &mut response).await?; + + if has_read == 0 || response.ends_with(b"\r\n\r\n") { + break; + } + + let has_read_bytes = &response[total_has_read..]; + tracing::trace!( + "Received from {server_name}: {}", + String::from_utf8_lossy(has_read_bytes) + ); + + const PREFIX: &[u8; 16] = b"content-length: "; + + if has_read_bytes + .get(..PREFIX.len()) + .map(|v| v.eq_ignore_ascii_case(PREFIX)) + == Some(true) + { + let v = std::str::from_utf8(&has_read_bytes[PREFIX.len()..]) + .expect("content length should be a number string") + .trim() + .parse::() + .expect("content length should be a number"); + + content_length = Some(v); + } + } + + // Read body + { + let Some(Some(content_length)) = content_length.map(NonZeroUsize::new) else { + tracing::warn!("No body found in response from {server_name}, skipped."); + + return Ok(()); + }; + + tracing::debug!( + "Headers received from {server_name}, reading body ({content_length} bytes)..." + ); + + response.reserve(content_length.get()); + + #[allow(unsafe_code)] + // Safety: we have reserved enough space above. + buf_stream + .read_exact(unsafe { + std::slice::from_raw_parts_mut( + response.as_mut_ptr().add(response.len()), + content_length.get(), + ) + }) + .await?; + + #[allow(unsafe_code)] + // Safety: we just initialized the buffer above. + unsafe { + response.set_len(response.len() + content_length.get()); + } + } + + let response = String::from_utf8_lossy(&response); + + tracing::info!("Got response from {server_name}"); + + tracing::trace!( + "Response from {server_name}: {:#?} (...)", + &response[..64.min(response.len())] + ); + } + + Ok(()) +} diff --git a/ktls-test/tests/echo.rs b/ktls-test/tests/echo.rs new file mode 100644 index 0000000..687b377 --- /dev/null +++ b/ktls-test/tests/echo.rs @@ -0,0 +1,23 @@ +//! Test: echo server + +use std::io; + +use ktls_test::common; + +#[test_case::test_matrix( + [ + common::CloseParty::Client, + common::CloseParty::Server, + ], + [ + common::TestOption::empty(), + common::TestOption::HANDLE_IO_RESULT, + ] +)] +#[tokio::test] +async fn test_echo( + close_party: common::CloseParty, + test_option: common::TestOption, +) -> io::Result<()> { + common::run_echo_test(close_party, test_option).await +} diff --git a/ktls-util/Cargo.toml b/ktls-util/Cargo.toml new file mode 100644 index 0000000..700e2a1 --- /dev/null +++ b/ktls-util/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ktls-util" +version = "0.0.0" +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +categories.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +publish = false # Currently for test only + +[dependencies] +ktls = { path = "../ktls", default-features = false } +rustls = { version = "0.23.27", default-features = false, features = ["std"] } +thiserror = "2.0.16" +tokio = { version = "1.40", features = ["io-util"] } diff --git a/ktls-util/README.md b/ktls-util/README.md new file mode 100644 index 0000000..13a6d72 --- /dev/null +++ b/ktls-util/README.md @@ -0,0 +1,21 @@ +[![Crates.io](https://img.shields.io/crates/v/ktls)](https://crates.io/crates/ktls-util) +[![Docs.rs](https://docs.rs/ktls/badge.svg)](https://docs.rs/ktls-util) +[![Test pipeline](https://github.com/rustls/ktls/actions/workflows/ci.yml/badge.svg)](https://github.com/rustls/ktls/actions/workflows/ci.yml?query=branch%3Amain) +[![Coverage Status (codecov.io)](https://codecov.io/gh/rustls/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/rustls/ktls/) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) + +# `ktls-util` - Utilities for crate `ktls` + +## MSRV + +1.77.0 + +## LICENSE + +This project is primarily distributed under the terms of both the MIT license +and the Apache License (Version 2.0). + +See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details. + +[kernel TLS offload]: https://www.kernel.org/doc/html/latest/networking/tls-offload.html +[rustls]: https://docs.rs/rustls/latest/rustls/kernel/index.html diff --git a/ktls-util/src/client.rs b/ktls-util/src/client.rs new file mode 100644 index 0000000..cc0d179 --- /dev/null +++ b/ktls-util/src/client.rs @@ -0,0 +1,127 @@ +//! A TLS connector with kTLS offload support. + +// TODO: Well, this should be included in `tokio-rustls`? Or we should provide +// both sync/async versions? + +use std::io; +use std::os::fd::AsFd; +use std::sync::Arc; + +use ktls::setup::{setup_ulp, SetupError}; +use ktls::stream::KtlsStream; +use rustls::client::UnbufferedClientConnection; +use rustls::pki_types::ServerName; +use rustls::unbuffered::{ConnectionState, EncodeError, UnbufferedStatus}; +use rustls::ClientConfig; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; + +use crate::error::Error; +use crate::read_record; + +#[derive(Debug, Clone)] +/// A TLS connector with kTLS offload support. +pub struct KtlsConnector { + config: Arc, +} + +impl KtlsConnector { + #[must_use] + /// Create a new [`KtlsConnector`] with the given [`ClientConfig`]. + pub const fn new(config: Arc) -> Self { + Self { config } + } + + /// Connects to a TLS server using the given socket and server name. + /// + /// ## Errors + /// + /// [`SetupError`]. This may contain the original socket if the setup failed + /// and the caller can fallback to normal TLS connector implementation. + pub async fn try_connect( + &self, + socket: S, + server_name: ServerName<'static>, + ) -> Result, SetupError> + where + S: AsyncRead + AsyncWrite + AsFd + Unpin, + { + let socket = setup_ulp(socket)?; + + self.internal_try_connect(socket, server_name) + .await + .map_err(|error| SetupError { + error: io::Error::other(error), + socket: None, + }) + } + + // `rustls` has poor support for async/await... + async fn internal_try_connect( + &self, + mut socket: S, + server_name: ServerName<'static>, + ) -> Result, Error> + where + S: AsyncRead + AsyncWrite + AsFd + Unpin, + { + let mut conn = UnbufferedClientConnection::new(self.config.clone(), server_name) + .map_err(Error::Config)?; + + let mut incoming = Vec::with_capacity(u16::MAX as usize + 5); + let mut outgoing = Vec::with_capacity(u16::MAX as usize + 5); + let mut outgoing_used = 0usize; + + loop { + let UnbufferedStatus { discard, state } = conn.process_tls_records(&mut incoming); + + let state = state.map_err(Error::Handshake)?; + + match state { + ConnectionState::BlockedHandshake => { + read_record(&mut socket, &mut incoming).await?; + } + ConnectionState::PeerClosed | ConnectionState::Closed => { + return Err(Error::ConnectionClosedBeforeHandshakeCompleted); + } + ConnectionState::EncodeTlsData(mut state) => { + match state.encode(&mut outgoing[outgoing_used..]) { + Ok(count) => outgoing_used += count, + Err(EncodeError::AlreadyEncoded) => unreachable!(), + Err(EncodeError::InsufficientSize(e)) => { + outgoing.resize(outgoing_used + e.required_size, 0u8); + + match state.encode(&mut outgoing[outgoing_used..]) { + Ok(count) => outgoing_used += count, + Err(e) => unreachable!("encode failed after resizing buffer: {e}"), + } + } + } + } + ConnectionState::TransmitTlsData(data) => { + // FIXME: may_encrypt_app_data to check if we can send early data? + + socket + .write_all(&outgoing[..outgoing_used]) + .await + .map_err(Error::IO)?; + outgoing_used = 0; + data.done(); + } + ConnectionState::WriteTraffic(_) => { + // Handshake is done + incoming.drain(..discard); + + break; + } + ConnectionState::ReadTraffic(_) => unreachable!( + "ReadTraffic should not be encountered during the handshake process" + ), + _ => unreachable!("unexpected connection state"), + } + + incoming.drain(..discard); + } + + KtlsStream::from_unbuffered_client_connnection(socket, conn).map_err(Error::Ktls) + } +} diff --git a/ktls-util/src/error.rs b/ktls-util/src/error.rs new file mode 100644 index 0000000..c942c1b --- /dev/null +++ b/ktls-util/src/error.rs @@ -0,0 +1,38 @@ +//! Errors + +use std::io; + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +/// Unified error type for this crate +pub(crate) enum Error { + #[error(transparent)] + /// General IO error. + IO(#[from] io::Error), + + #[error("The peer closed the connection before the TLS handshake could be completed")] + /// (Reserved) The peer closed the connection before the TLS handshake could + /// be completed. + ConnectionClosedBeforeHandshakeCompleted, + + #[error("Failed to create rustls unbuffered connection: {0}")] + /// (Reserved) + Config(#[source] rustls::Error), + + #[error("An error occurred during handshake: {0}")] + /// (Reserved) + Handshake(#[source] rustls::Error), + + #[error(transparent)] + /// Errors from ktls crate. + Ktls(#[from] ktls::Error), +} + +impl From for io::Error { + fn from(error: Error) -> Self { + match error { + Error::IO(error) => error, + _ => Self::other(error), + } + } +} diff --git a/ktls-util/src/lib.rs b/ktls-util/src/lib.rs new file mode 100644 index 0000000..3919a08 --- /dev/null +++ b/ktls-util/src/lib.rs @@ -0,0 +1,78 @@ +//! Utilities for ktls crate. + +#![warn( + unsafe_code, + unused_must_use, + clippy::alloc_instead_of_core, + clippy::exhaustive_enums, + clippy::exhaustive_structs, + clippy::manual_let_else, + clippy::use_self, + clippy::upper_case_acronyms, + elided_lifetimes_in_paths, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unreachable_pub, + unused_import_braces, + unused_extern_crates, + unused_qualifications +)] + +use std::{io, slice}; + +use tokio::io::{AsyncRead, AsyncReadExt}; + +pub mod client; +pub(crate) mod error; +pub mod server; + +pub(crate) async fn read_record(socket: &mut S, incoming: &mut Vec) -> io::Result<()> +where + S: AsyncRead + Unpin, +{ + const RECORD_HDR_SIZE: usize = 5; + + incoming.reserve(RECORD_HDR_SIZE); + + #[allow(unsafe_code)] + // Safety: We just reserved enough space for the header. + let record_hdr = unsafe { + slice::from_raw_parts_mut( + incoming.spare_capacity_mut().as_mut_ptr().cast(), + RECORD_HDR_SIZE, + ) + }; + + socket + .read_exact(record_hdr) + .await + .map_err(ktls::Error::IO)?; + + let payload_length = u16::from_be_bytes([record_hdr[3], record_hdr[4]]) as usize; + + incoming.reserve(payload_length); + + #[allow(unsafe_code)] + // Safety: We just reserved enough space for the payload. + let payload = unsafe { + slice::from_raw_parts_mut( + incoming + .spare_capacity_mut() + .as_mut_ptr() + .add(RECORD_HDR_SIZE) + .cast(), + payload_length, + ) + }; + + socket.read_exact(payload).await.map_err(ktls::Error::IO)?; + + #[allow(unsafe_code)] + // Safety: We have just read data into the space we reserved. + unsafe { + incoming.set_len(incoming.len() + RECORD_HDR_SIZE + payload_length); + } + + Ok(()) +} diff --git a/ktls-util/src/server.rs b/ktls-util/src/server.rs new file mode 100644 index 0000000..a5cb59f --- /dev/null +++ b/ktls-util/src/server.rs @@ -0,0 +1,124 @@ +//! A TLS acceptor with kTLS offload support. + +// TODO: Well, this should be included in `tokio-rustls`? Or we should provide +// both sync/async versions? + +use std::io; +use std::os::fd::AsFd; +use std::sync::Arc; + +use ktls::setup::{setup_ulp, SetupError}; +use ktls::stream::KtlsStream; +use rustls::server::UnbufferedServerConnection; +use rustls::unbuffered::{ConnectionState, EncodeError, UnbufferedStatus}; +use rustls::ServerConfig; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; + +use crate::error::Error; +use crate::read_record; + +#[derive(Debug, Clone)] +/// A TLS acceptor with kTLS offload support. +pub struct KtlsAcceptor { + config: Arc, +} + +impl KtlsAcceptor { + #[must_use] + /// Create a new [`KtlsAcceptor`] with the given [`ServerConfig`]. + pub const fn new(config: Arc) -> Self { + Self { config } + } + + /// Accepts a TLS connection on the given socket. + /// + /// ## Errors + /// + /// [`SetupError`]. This may contain the original socket if the setup failed + /// and the caller can fallback to normal TLS acceptor implementation. + pub async fn try_accept(&self, socket: S) -> Result, SetupError> + where + S: AsyncRead + AsyncWrite + AsFd + Unpin, + { + let socket = setup_ulp(socket)?; + + self.internal_try_accept(socket) + .await + .map_err(|error| SetupError { + error: io::Error::other(error), + socket: None, + }) + } + + async fn internal_try_accept(&self, mut socket: S) -> Result, Error> + where + S: AsyncWrite + AsyncRead + AsFd + Unpin, + { + let mut conn = + UnbufferedServerConnection::new(self.config.clone()).map_err(Error::Config)?; + + let mut incoming = Vec::with_capacity(u16::MAX as usize + 5); + let mut outgoing = Vec::with_capacity(u16::MAX as usize + 5); + let mut outgoing_used = 0usize; + let mut early_data_received = Vec::new(); + + loop { + let UnbufferedStatus { mut discard, state } = conn.process_tls_records(&mut incoming); + + let state = state.map_err(Error::Handshake)?; + + match state { + ConnectionState::BlockedHandshake => { + read_record(&mut socket, &mut incoming).await?; + } + ConnectionState::PeerClosed | ConnectionState::Closed => { + return Err(Error::ConnectionClosedBeforeHandshakeCompleted); + } + ConnectionState::ReadEarlyData(mut data) => { + while let Some(record) = data.next_record() { + let record = record.map_err(Error::Handshake)?; + + discard += record.discard; + + early_data_received.extend_from_slice(record.payload); + } + } + ConnectionState::EncodeTlsData(mut state) => { + match state.encode(&mut outgoing[outgoing_used..]) { + Ok(count) => outgoing_used += count, + Err(EncodeError::AlreadyEncoded) => unreachable!(), + Err(EncodeError::InsufficientSize(e)) => { + outgoing.resize(outgoing_used + e.required_size, 0u8); + + match state.encode(&mut outgoing[outgoing_used..]) { + Ok(count) => outgoing_used += count, + Err(e) => unreachable!("encode failed after resizing buffer: {e}"), + } + } + } + } + ConnectionState::TransmitTlsData(data) => { + socket + .write_all(&outgoing[..outgoing_used]) + .await + .map_err(Error::IO)?; + outgoing_used = 0; + data.done(); + } + ConnectionState::WriteTraffic(_) => { + incoming.drain(..discard); + break; + } + ConnectionState::ReadTraffic(_) => unreachable!( + "ReadTraffic should not be encountered during the handshake process" + ), + _ => unreachable!("unexpected connection state"), + } + + incoming.drain(..discard); + } + + KtlsStream::from_unbuffered_server_connnection(socket, conn, Some(early_data_received)) + .map_err(Error::Ktls) + } +} diff --git a/ktls/Cargo.toml b/ktls/Cargo.toml index d0656fa..cb358c6 100644 --- a/ktls/Cargo.toml +++ b/ktls/Cargo.toml @@ -1,44 +1,50 @@ [package] name = "ktls" version = "6.0.2" -edition = "2021" -license = "MIT OR Apache-2.0" -repository = "https://github.com/rustls/ktls" -documentation = "https://docs.rs/ktls" -authors = ["Amos Wenger "] -readme = "README.md" +edition.workspace = true +rust-version.workspace = true +# === Publication info === +authors.workspace = true +categories.workspace = true description = """ Configures kTLS for tokio-rustls client and server connections. """ -rust-version = "1.75" +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true + +[package.metadata.docs.rs] +features = ["tls12", "async-io-tokio", "raw-api", "probe-ktls-compatibility"] +targets = ["x86_64-unknown-linux-gnu"] [dependencies] -libc = { version = "0.2.155", features = ["const-extern-fn"] } -thiserror = "2" -tracing = "0.1.40" -tokio-rustls = { default-features = false, version = "0.26.0" } -rustls = { version = "0.23.12", default-features = false } -smallvec = "1.13.2" -memoffset = "0.9.1" -pin-project-lite = "0.2.14" -tokio = { version = "1.39.2", features = ["net", "macros", "io-util"] } -ktls-sys = "1.0.1" -num_enum = "0.7.3" -futures-util = "0.3.30" -nix = { version = "0.29.0", features = ["socket", "uio", "net"] } - -[dev-dependencies] -lazy_static = "1.5.0" -oorandom = "11.1.4" -rcgen = "0.13.1" -socket2 = "0.5.7" -test-case = "3.3.1" -tokio = { version = "1.39.2", features = ["full"] } -tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } +bitflags = "2.9" +libc = { version = "0.2.175", features = ["const-extern-fn"] } +log = { version = "0.4.27", optional = true } +nix = { version = "0.30.1", features = ["socket", "uio", "net"] } +pin-project-lite = "0.2.16" +rustls = { version = "0.23.31", default-features = false, features = ["std"] } +tokio = { version = "1.40", optional = true } +tracing = { version = "0.1.41", optional = true } [features] -default = ["aws_lc_rs", "tls12"] -aws_lc_rs = ["rustls/aws_lc_rs", "tokio-rustls/aws_lc_rs"] -aws-lc-rs = ["aws_lc_rs"] # Alias because Cargo features commonly use `-` -ring = ["rustls/ring", "tokio-rustls/ring"] -tls12 = ["rustls/tls12", "tokio-rustls/tls12"] +default = ["tls12", "async-io-tokio"] + +# Enable rustls TLS 1.2 support +tls12 = ["rustls/tls12"] + +# Expose some low-level APIs +raw-api = [] + +# Implements tokio's `AsyncRead` and `AsyncWrite` traits for `KtlsStream` +async-io-tokio = ["dep:tokio"] + +# Logging support with `log` crate +log = ["dep:log"] + +# Logging support with `tracing` crate +tracing = ["dep:tracing"] + +# Probes for the compatibility of the current kernel with kTLS +probe-ktls-compatibility = [] diff --git a/ktls/README.md b/ktls/README.md index 59c8897..dfb23ea 100644 --- a/ktls/README.md +++ b/ktls/README.md @@ -1,17 +1,24 @@ -[![test pipeline](https://github.com/hapsoc/ktls/actions/workflows/test.yml/badge.svg)](https://github.com/hapsoc/ktls/actions/workflows/test.yml?query=branch%3Amain) -[![Coverage Status (codecov.io)](https://codecov.io/gh/hapsoc/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/hapsoc/ktls/) [![Crates.io](https://img.shields.io/crates/v/ktls)](https://crates.io/crates/ktls) -[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) +[![Docs.rs](https://docs.rs/ktls/badge.svg)](https://docs.rs/ktls) +[![Test pipeline](https://github.com/rustls/ktls/actions/workflows/ci.yml/badge.svg)](https://github.com/rustls/ktls/actions/workflows/ci.yml?query=branch%3Amain) +[![Coverage Status (codecov.io)](https://codecov.io/gh/rustls/ktls/branch/main/graph/badge.svg)](https://codecov.io/gh/rustls/ktls/) +[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) -# ktls +# `ktls` - Kernel TLS offload (kTLS) support built on top of [rustls]. -Configures kTLS ([kernel TLS -offload](https://www.kernel.org/doc/html/latest/networking/tls-offload.html)) -for any type that implements `AsRawFd`, given a rustls `ServerConnection`. +This crate provides high-level APIs for configuring [kernel TLS offload] (kTLS), +extending the bare minimum functionality provided by [rustls]. -## License +## MSRV + +1.77.0 + +## LICENSE This project is primarily distributed under the terms of both the MIT license and the Apache License (Version 2.0). See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) for details. + +[kernel TLS offload]: https://www.kernel.org/doc/html/latest/networking/tls-offload.html +[rustls]: https://docs.rs/rustls/latest/rustls/kernel/index.html diff --git a/ktls/src/async_read_ready.rs b/ktls/src/async_read_ready.rs deleted file mode 100644 index b8d9a21..0000000 --- a/ktls/src/async_read_ready.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::{io, task}; - -pub trait AsyncReadReady { - /// cf. https://docs.rs/tokio/latest/tokio/net/struct.TcpStream.html#method.poll_read_ready - fn poll_read_ready(&self, cx: &mut task::Context<'_>) -> task::Poll>; -} - -impl AsyncReadReady for tokio::net::TcpStream { - fn poll_read_ready(&self, cx: &mut task::Context<'_>) -> task::Poll> { - tokio::net::TcpStream::poll_read_ready(self, cx) - } -} diff --git a/ktls/src/cork_stream.rs b/ktls/src/cork_stream.rs deleted file mode 100644 index 9d8271e..0000000 --- a/ktls/src/cork_stream.rs +++ /dev/null @@ -1,214 +0,0 @@ -use std::{io, pin::Pin, task}; - -use rustls::internal::msgs::codec::Codec; -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - -use crate::AsyncReadReady; - -enum State { - ReadHeader { header_buf: [u8; 5], offset: usize }, - ReadPayload { msg_size: usize, offset: usize }, - // we encountered EOF while reading, or saw an invalid header and we're just - // passing reads through without doing any sort of processing now. - Passthrough, -} - -/// This is a wrapper that reads TLS message headers so it knows when to start -/// doing empty reads at the message boundary when "draining" a rustls -/// connection before setting up kTLS for it. -/// -/// The short explanation is: rustls might have buffered one or more -/// ApplicationData messages (the last one might even be partial) by the time -/// "connect" / "accept" returns. -/// -/// We not only need to pop messages rustls has already deframed (that's done in -/// a drain function elsewhere), but also let rustls finish reading and -/// deframing any partial message it may have already buffered. -/// -/// Because this wrapper is trying very hard not to do any error handling, if it -/// encounters anything that doesn't look like a TLS header (unknown type, -/// nonsensical size, unexpected EOF), it'll quite easily fall back to a -/// "passthrough" mode with no internal buffering, letting rustls take care -/// reporting any errors. -pub struct CorkStream { - pub io: IO, - // if true, causes empty reads at the message boudnary - pub corked: bool, - state: State, -} - -impl CorkStream { - pub fn new(io: IO) -> Self { - Self { - io, - corked: false, - state: State::ReadHeader { - header_buf: Default::default(), - offset: 0, - }, - } - } -} - -impl AsyncRead for CorkStream -where - IO: AsyncRead, -{ - #[inline] - fn poll_read( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> task::Poll> { - let this = unsafe { self.get_unchecked_mut() }; - let mut io = unsafe { Pin::new_unchecked(&mut this.io) }; - - let state = &mut this.state; - - loop { - match state { - State::ReadHeader { header_buf, offset } => { - if *offset == 0 && this.corked { - tracing::trace!( - "corked, returning empty read (but waking to prevent stalls)" - ); - cx.waker().wake_by_ref(); - return task::Poll::Ready(Ok(())); - } - - let left = header_buf.len() - *offset; - tracing::trace!("reading header, {left}/{} bytes left", header_buf.len()); - - { - let mut rest = ReadBuf::new(&mut header_buf[*offset..]); - tracing::trace!("reading header: doing i/o"); - futures_util::ready!(io.as_mut().poll_read(cx, &mut rest)?); - tracing::trace!("reading header: io was ready"); - *offset += rest.filled().len(); - if rest.filled().is_empty() { - // that's an unexpected EOF for sure, but let's have - // rustls deal with the error reporting shall we? - tracing::trace!( - "unexpected EOF: header cut short after {} bytes", - *offset - ); - buf.put_slice(&header_buf[..*offset]); - *state = State::Passthrough; - - return task::Poll::Ready(Ok(())); - } - tracing::trace!("read {} bytes off of header", rest.filled().len()); - } - - if *offset == 5 { - // TODO: handle cases where buffer has less than 5 bytes - // remaining. I (fasterthanlime) bet this never happens in - // practice since the rustls deframer uses `copy_within` to - // get rid of the part of the buffer it's already decoded. - assert!(buf.remaining() >= 5, "you found an edge case in ktls!"); - buf.put_slice(&header_buf[..]); - - match decode_header(*header_buf) { - Some((typ, version, len)) => { - tracing::trace!( - "read header: typ={typ:?}, version={version:?}, len={len}" - ); - *state = State::ReadPayload { - msg_size: len as usize, - offset: 0, - }; - } - None => { - // we encountered an invalid header, let's bail out - tracing::warn!("encountered invalid header, bailing out"); - *state = State::Passthrough; - } - } - - return task::Poll::Ready(Ok(())); - } else { - // keep trying - } - } - State::ReadPayload { msg_size, offset } => { - let rest = *msg_size - *offset; - - let just_read = { - let mut rest = buf.take(rest); - futures_util::ready!(io.as_mut().poll_read(cx, &mut rest)?); - - tracing::trace!("read {} bytes off of payload", rest.filled().len()); - *offset += rest.filled().len(); - - if *offset == *msg_size { - tracing::trace!("read full payload (all {} bytes)", *offset); - *state = State::ReadHeader { - header_buf: Default::default(), - offset: 0, - }; - } - - rest.filled().len() - }; - - let new_filled = buf.filled().len() + just_read; - buf.set_filled(new_filled); - - return task::Poll::Ready(Ok(())); - } - State::Passthrough => { - // we encountered EOF while reading, or saw an invalid header and we're just - // passing reads through without doing any sort of processing now. - return io.poll_read(cx, buf); - } - } - } - } -} - -impl AsyncReadReady for CorkStream -where - IO: AsyncReadReady, -{ - fn poll_read_ready(&self, cx: &mut task::Context<'_>) -> task::Poll> { - self.io.poll_read_ready(cx) - } -} - -impl AsyncWrite for CorkStream -where - IO: AsyncWrite, -{ - #[inline] - fn poll_write( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &[u8], - ) -> task::Poll> { - let io = unsafe { self.map_unchecked_mut(|s| &mut s.io) }; - io.poll_write(cx, buf) - } - - #[inline] - fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll> { - let io = unsafe { self.map_unchecked_mut(|s| &mut s.io) }; - io.poll_flush(cx) - } - - #[inline] - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll> { - let io = unsafe { self.map_unchecked_mut(|s| &mut s.io) }; - io.poll_shutdown(cx) - } -} - -fn decode_header(b: [u8; 5]) -> Option<(rustls::ContentType, rustls::ProtocolVersion, u16)> { - let typ = rustls::ContentType::read_bytes(&b[0..1]).ok()?; - let version = rustls::ProtocolVersion::read_bytes(&b[1..3]).ok()?; - // this is dumb but it looks less scary than `.try_into().unwrap()`: - let len: u16 = u16::from_be_bytes([b[3], b[4]]); - Some((typ, version, len)) -} diff --git a/ktls/src/error.rs b/ktls/src/error.rs new file mode 100644 index 0000000..3e6f0b0 --- /dev/null +++ b/ktls/src/error.rs @@ -0,0 +1,94 @@ +//! Error types for the `ktls` crate + +use std::{fmt, io}; + +use rustls::SupportedCipherSuite; + +#[non_exhaustive] +#[derive(Debug)] +/// Unified error type for this crate +pub enum Error { + /// Invalid crypto material, e.g., wrong size key or IV. + InvalidCryptoInfo(InvalidCryptoInfo), + + /// Failed to extract connection secrets from rustls connection, e.g., not + /// have `config.enable_secret_extraction` set to true + ExtractSecrets(rustls::Error), + + /// General IO error. + IO(io::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidCryptoInfo(e) => e.fmt(f), + Self::ExtractSecrets(e) => { + write!(f, "failed to extract secrets from rustls connection: {e}") + } + Self::IO(e) => e.fmt(f), + } + } +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidCryptoInfo(_) => None, + Self::ExtractSecrets(e) => Some(e), + Self::IO(e) => Some(e), + } + } +} + +impl From for Error { + fn from(error: io::Error) -> Self { + Self::IO(error) + } +} + +impl From for io::Error { + fn from(error: Error) -> Self { + match error { + Error::IO(error) => error, + _ => Self::other(error), + } + } +} + +impl From for Error { + fn from(error: InvalidCryptoInfo) -> Self { + Self::InvalidCryptoInfo(error) + } +} + +#[non_exhaustive] +#[derive(Debug)] +/// Crypto material is invalid, e.g., wrong size key or IV. +pub enum InvalidCryptoInfo { + /// The provided key has an incorrect size (unlikely). + WrongSizeKey, + + /// The provided IV has an incorrect size (unlikely). + WrongSizeIv, + + /// The negotiated cipher suite is not supported for ktls by the running + /// kernel. + UnsupportedCipherSuite(SupportedCipherSuite), +} + +impl fmt::Display for InvalidCryptoInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::WrongSizeKey => write!(f, "wrong size key"), + Self::WrongSizeIv => write!(f, "wrong size iv"), + Self::UnsupportedCipherSuite(suite) => { + write!( + f, + "the negotiated cipher suite [{suite:?}] is not supported for ktls by the \ + running kernel" + ) + } + } + } +} diff --git a/ktls/src/ffi.rs b/ktls/src/ffi.rs index 77af19b..3ba8444 100644 --- a/ktls/src/ffi.rs +++ b/ktls/src/ffi.rs @@ -1,294 +1,62 @@ -use std::os::unix::prelude::RawFd; +//! Raw FFI wrappers. -use ktls_sys::bindings as ktls; -use rustls::{ - internal::msgs::{enums::AlertLevel, message::Message}, - AlertDescription, ConnectionTrafficSecrets, SupportedCipherSuite, -}; +// Since Rust 2021 doesn't have `size_of_val` included in prelude. +#![allow(unused_qualifications)] -pub(crate) const TLS_1_2_VERSION_NUMBER: u16 = (((ktls::TLS_1_2_VERSION_MAJOR & 0xFF) as u16) << 8) - | ((ktls::TLS_1_2_VERSION_MINOR & 0xFF) as u16); +use std::os::fd::RawFd; +use std::{io, mem, ptr}; -pub(crate) const TLS_1_3_VERSION_NUMBER: u16 = (((ktls::TLS_1_3_VERSION_MAJOR & 0xFF) as u16) << 8) - | ((ktls::TLS_1_3_VERSION_MINOR & 0xFF) as u16); - -/// `setsockopt` level constant: TCP -const SOL_TCP: libc::c_int = 6; - -/// `setsockopt` SOL_TCP name constant: "upper level protocol" -const TCP_ULP: libc::c_int = 31; - -/// `setsockopt` level constant: TLS -const SOL_TLS: libc::c_int = 282; - -/// `setsockopt` SOL_TLS level constant: transmit (write) -const TLS_TX: libc::c_int = 1; - -/// `setsockopt` SOL_TLS level constant: receive (read) -const TLX_RX: libc::c_int = 2; - -pub fn setup_ulp(fd: RawFd) -> std::io::Result<()> { - unsafe { - if libc::setsockopt( - fd, - SOL_TCP, - TCP_ULP, - "tls".as_ptr() as *const libc::c_void, - 3, - ) < 0 - { - return Err(std::io::Error::last_os_error()); - } - } - - Ok(()) -} - -#[derive(Clone, Copy, Debug)] -pub enum Direction { - // Transmit - Tx, - // Receive - Rx, -} - -impl From for libc::c_int { - fn from(val: Direction) -> Self { - match val { - Direction::Tx => TLS_TX, - Direction::Rx => TLX_RX, - } - } -} - -#[allow(dead_code)] -pub enum CryptoInfo { - AesGcm128(ktls::tls12_crypto_info_aes_gcm_128), - AesGcm256(ktls::tls12_crypto_info_aes_gcm_256), - AesCcm128(ktls::tls12_crypto_info_aes_ccm_128), - Chacha20Poly1305(ktls::tls12_crypto_info_chacha20_poly1305), - Sm4Gcm(ktls::tls12_crypto_info_sm4_gcm), - Sm4Ccm(ktls::tls12_crypto_info_sm4_ccm), -} - -impl CryptoInfo { - /// Return the system struct as a pointer. - pub fn as_ptr(&self) -> *const libc::c_void { - match self { - CryptoInfo::AesGcm128(info) => info as *const _ as *const libc::c_void, - CryptoInfo::AesGcm256(info) => info as *const _ as *const libc::c_void, - CryptoInfo::AesCcm128(info) => info as *const _ as *const libc::c_void, - CryptoInfo::Chacha20Poly1305(info) => info as *const _ as *const libc::c_void, - CryptoInfo::Sm4Gcm(info) => info as *const _ as *const libc::c_void, - CryptoInfo::Sm4Ccm(info) => info as *const _ as *const libc::c_void, - } - } - - /// Return the system struct size. - pub fn size(&self) -> usize { - match self { - CryptoInfo::AesGcm128(_) => std::mem::size_of::(), - CryptoInfo::AesGcm256(_) => std::mem::size_of::(), - CryptoInfo::AesCcm128(_) => std::mem::size_of::(), - CryptoInfo::Chacha20Poly1305(_) => { - std::mem::size_of::() - } - CryptoInfo::Sm4Gcm(_) => std::mem::size_of::(), - CryptoInfo::Sm4Ccm(_) => std::mem::size_of::(), - } - } -} - -#[derive(thiserror::Error, Debug)] -pub enum KtlsCompatibilityError { - #[error("cipher suite not supported with kTLS: {0:?}")] - UnsupportedCipherSuite(SupportedCipherSuite), - - #[error("wrong size key")] - WrongSizeKey, - - #[error("wrong size iv")] - WrongSizeIv, -} - -impl CryptoInfo { - /// Try to convert rustls cipher suite and secrets into a `CryptoInfo`. - pub fn from_rustls( - cipher_suite: SupportedCipherSuite, - (seq, secrets): (u64, ConnectionTrafficSecrets), - ) -> Result { - let version = match cipher_suite { - SupportedCipherSuite::Tls12(..) => TLS_1_2_VERSION_NUMBER, - SupportedCipherSuite::Tls13(..) => TLS_1_3_VERSION_NUMBER, - }; - - Ok(match secrets { - ConnectionTrafficSecrets::Aes128Gcm { key, iv } => { - // see https://github.com/rustls/rustls/issues/1833, between - // rustls 0.21 and 0.22, the extract_keys codepath was changed, - // so, for TLS 1.2, both GCM-128 and GCM-256 return the - // Aes128Gcm variant. - - match key.as_ref().len() { - 16 => CryptoInfo::AesGcm128(ktls::tls12_crypto_info_aes_gcm_128 { - info: ktls::tls_crypto_info { - version, - cipher_type: ktls::TLS_CIPHER_AES_GCM_128 as _, - }, - iv: iv - .as_ref() - .get(4..) - .expect("AES-GCM-128 iv is 8 bytes") - .try_into() - .expect("AES-GCM-128 iv is 8 bytes"), - key: key - .as_ref() - .try_into() - .expect("AES-GCM-128 key is 16 bytes"), - salt: iv - .as_ref() - .get(..4) - .expect("AES-GCM-128 salt is 4 bytes") - .try_into() - .expect("AES-GCM-128 salt is 4 bytes"), - rec_seq: seq.to_be_bytes(), - }), - 32 => CryptoInfo::AesGcm256(ktls::tls12_crypto_info_aes_gcm_256 { - info: ktls::tls_crypto_info { - version, - cipher_type: ktls::TLS_CIPHER_AES_GCM_256 as _, - }, - iv: iv - .as_ref() - .get(4..) - .expect("AES-GCM-256 iv is 8 bytes") - .try_into() - .expect("AES-GCM-256 iv is 8 bytes"), - key: key - .as_ref() - .try_into() - .expect("AES-GCM-256 key is 32 bytes"), - salt: iv - .as_ref() - .get(..4) - .expect("AES-GCM-256 salt is 4 bytes") - .try_into() - .expect("AES-GCM-256 salt is 4 bytes"), - rec_seq: seq.to_be_bytes(), - }), - _ => unreachable!("GCM key length is not 16 or 32"), - } - } - ConnectionTrafficSecrets::Aes256Gcm { key, iv } => { - CryptoInfo::AesGcm256(ktls::tls12_crypto_info_aes_gcm_256 { - info: ktls::tls_crypto_info { - version, - cipher_type: ktls::TLS_CIPHER_AES_GCM_256 as _, - }, - iv: iv - .as_ref() - .get(4..) - .expect("AES-GCM-256 iv is 8 bytes") - .try_into() - .expect("AES-GCM-256 iv is 8 bytes"), - key: key - .as_ref() - .try_into() - .expect("AES-GCM-256 key is 32 bytes"), - salt: iv - .as_ref() - .get(..4) - .expect("AES-GCM-256 salt is 4 bytes") - .try_into() - .expect("AES-GCM-256 salt is 4 bytes"), - rec_seq: seq.to_be_bytes(), - }) - } - ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv } => { - CryptoInfo::Chacha20Poly1305(ktls::tls12_crypto_info_chacha20_poly1305 { - info: ktls::tls_crypto_info { - version, - cipher_type: ktls::TLS_CIPHER_CHACHA20_POLY1305 as _, - }, - iv: iv - .as_ref() - .try_into() - .expect("Chacha20-Poly1305 iv is 12 bytes"), - key: key - .as_ref() - .try_into() - .expect("Chacha20-Poly1305 key is 32 bytes"), - salt: ktls::__IncompleteArrayField::new(), - rec_seq: seq.to_be_bytes(), - }) - } - _ => { - return Err(KtlsCompatibilityError::UnsupportedCipherSuite(cipher_suite)); - } - }) - } -} - -pub fn setup_tls_info(fd: RawFd, dir: Direction, info: CryptoInfo) -> Result<(), crate::Error> { - let ret = unsafe { libc::setsockopt(fd, SOL_TLS, dir.into(), info.as_ptr(), info.size() as _) }; - if ret < 0 { - return Err(crate::Error::TlsCryptoInfoError( - std::io::Error::last_os_error(), - )); - } - Ok(()) -} - -const TLS_SET_RECORD_TYPE: libc::c_int = 1; -const ALERT: u8 = 0x15; - -// Yes, really. cmsg components are aligned to [libc::c_long] -#[cfg_attr(target_pointer_width = "32", repr(C, align(4)))] -#[cfg_attr(target_pointer_width = "64", repr(C, align(8)))] -struct Cmsg { - hdr: libc::cmsghdr, +#[repr(C)] +pub(crate) struct Cmsg { + _hdr: libc::cmsghdr, data: [u8; N], } impl Cmsg { - fn new(level: i32, typ: i32, data: [u8; N]) -> Self { - Self { - hdr: libc::cmsghdr { - // on Linux this is a usize, on macOS this is a u32 - #[allow(clippy::unnecessary_cast)] - cmsg_len: (memoffset::offset_of!(Self, data) + N) as _, - cmsg_level: level, - cmsg_type: typ, - }, - data, - } - } -} - -pub fn send_close_notify(fd: RawFd) -> std::io::Result<()> { - let mut data = vec![]; - Message::build_alert(AlertLevel::Warning, AlertDescription::CloseNotify) - .payload - .encode(&mut data); - - let mut cmsg = Cmsg::new(SOL_TLS, TLS_SET_RECORD_TYPE, [ALERT]); - - let msg = libc::msghdr { - msg_name: std::ptr::null_mut(), - msg_namelen: 0, - msg_iov: &mut libc::iovec { - iov_base: data.as_mut_ptr() as _, - iov_len: data.len(), - }, - msg_iovlen: 1, - msg_control: &mut cmsg as *mut _ as *mut _, - msg_controllen: cmsg.hdr.cmsg_len, - msg_flags: 0, - }; - - let ret = unsafe { libc::sendmsg(fd, &msg, 0) }; - if ret < 0 { - return Err(std::io::Error::last_os_error()); + #[allow(trivial_numeric_casts)] + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_possible_wrap)] + pub(crate) fn new(level: i32, typ: i32, data: [u8; N]) -> Self { + #[allow(unsafe_code)] + // SAFETY: zeroed is fine for cmsghdr as we will set all the fields we use. + let mut hdr = unsafe { mem::zeroed::() }; + + hdr.cmsg_level = level; + hdr.cmsg_type = typ; + // For MUSL target, this is u32. + hdr.cmsg_len = (mem::offset_of!(Self, data) + N) as _; + + Self { _hdr: hdr, data } + } +} + +#[allow(trivial_numeric_casts)] +#[allow(clippy::cast_possible_truncation)] +#[allow(clippy::cast_possible_wrap)] +/// A wrapper around [`libc::sendmsg`]. +pub(crate) fn sendmsg( + fd: RawFd, + data: &mut [io::IoSlice<'_>], + cmsg: &mut Cmsg, + flags: i32, +) -> io::Result { + #[allow(unsafe_code)] + // SAFETY: zeroed is fine for msghdr as we will set all the fields we use. + let mut msghdr: libc::msghdr = unsafe { mem::zeroed() }; + + msghdr.msg_control = ptr::from_mut(cmsg).cast(); + msghdr.msg_controllen = mem::size_of_val(cmsg) as _; + msghdr.msg_iov = ptr::from_mut(data).cast(); + msghdr.msg_iovlen = data.len() as _; + + #[allow(unsafe_code)] + // SAFETY: syscall + let ret = unsafe { libc::sendmsg(fd, &msghdr, flags) }; + + if ret >= 0 { + #[allow(clippy::cast_sign_loss)] + Ok(ret as usize) + } else { + Err(io::Error::last_os_error()) } - Ok(()) } diff --git a/ktls/src/ktls_stream.rs b/ktls/src/ktls_stream.rs deleted file mode 100644 index 2d6b615..0000000 --- a/ktls/src/ktls_stream.rs +++ /dev/null @@ -1,308 +0,0 @@ -use nix::{ - errno::Errno, - sys::socket::{recvmsg, ControlMessageOwned, MsgFlags, SockaddrIn, TlsGetRecordType}, -}; -use num_enum::FromPrimitive; -use std::{ - io::{self, IoSliceMut}, - os::unix::prelude::AsRawFd, - pin::Pin, - task, -}; - -use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; - -use crate::AsyncReadReady; - -// A wrapper around `IO` that sends a `close_notify` when shut down or dropped. -pin_project_lite::pin_project! { - pub struct KtlsStream - where - IO: AsRawFd - { - #[pin] - inner: IO, - write_closed: bool, - read_closed: bool, - drained: Option<(usize, Vec)>, - } -} - -impl KtlsStream -where - IO: AsRawFd, -{ - pub fn new(inner: IO, drained: Option>) -> Self { - Self { - inner, - write_closed: false, - read_closed: false, - drained: drained.map(|drained| (0, drained)), - } - } - - /// Return the drained data + the original I/O - pub fn into_raw(self) -> (Option>, IO) { - (self.drained.map(|(_, drained)| drained), self.inner) - } - - /// Returns a reference to the original I/O - pub fn get_ref(&self) -> &IO { - &self.inner - } - - /// Returns a mut reference to the original I/O - pub fn get_mut(&mut self) -> &mut IO { - &mut self.inner - } -} - -#[derive(Debug, PartialEq, Clone, Copy, num_enum::FromPrimitive)] -#[repr(u8)] -enum TlsAlertLevel { - Warning = 1, - Fatal = 2, - #[num_enum(catch_all)] - Other(u8), -} - -#[derive(Debug, PartialEq, Clone, Copy, num_enum::FromPrimitive)] -#[repr(u8)] -enum TlsAlertDescription { - CloseNotify = 0, - #[num_enum(catch_all)] - Other(u8), -} - -impl AsyncRead for KtlsStream -where - IO: AsRawFd + AsyncRead + AsyncReadReady, -{ - fn poll_read( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> task::Poll> { - tracing::trace!(buf.remaining = %buf.remaining(), "KtlsStream::poll_read"); - - if self.read_closed { - return task::Poll::Ready(Ok(())); - } - - if buf.remaining() == 0 { - return task::Poll::Ready(Ok(())); - } - - let mut this = self.project(); - - if let Some((drain_index, drained)) = this.drained.as_mut() { - let drained = &drained[*drain_index..]; - let len = std::cmp::min(buf.remaining(), drained.len()); - - tracing::trace!(%len, "KtlsStream::poll_read, can take from drain"); - buf.put_slice(&drained[..len]); - - *drain_index += len; - if *drain_index >= drained.len() { - tracing::trace!("KtlsStream::poll_read, done draining"); - *this.drained = None; - } - cx.waker().wake_by_ref(); - - tracing::trace!("KtlsStream::poll_read, returning after drain"); - return task::Poll::Ready(Ok(())); - } - - let read_res = this.inner.as_mut().poll_read(cx, buf); - if let task::Poll::Ready(Err(e)) = &read_res { - // 5 is a generic "input/output error", it happens when - // using poll_read on a kTLS socket that just received - // a control message - if let Some(5) = e.raw_os_error() { - // could be a control message, let's check - let fd = this.inner.as_raw_fd(); - - // XXX: recvmsg wants a `&mut Vec` so it's able to resize it - // I guess? Or so there's a clear separation between uninitialized - // and initialized? We could probably get read of that heap alloc, idk. - - // let mut cmsgspace = - // [0u8; unsafe { libc::CMSG_SPACE(std::mem::size_of::() as _) as _ }]; - let mut cmsgspace = Vec::with_capacity(unsafe { - libc::CMSG_SPACE(std::mem::size_of::() as _) as _ - }); - - let mut iov = [IoSliceMut::new(buf.initialize_unfilled())]; - let flags = MsgFlags::empty(); - - let r = recvmsg::(fd, &mut iov, Some(&mut cmsgspace), flags); - let r = match r { - Ok(r) => r, - Err(Errno::EAGAIN) => { - unreachable!("expected a control message, got EAGAIN") - } - Err(e) => { - // ok I guess it really failed then - tracing::trace!(?e, "recvmsg failed"); - return Err(e.into()).into(); - } - }; - let cmsg = r - .cmsgs()? - .next() - .expect("we should've received exactly one control message"); - - let record_type = match cmsg { - ControlMessageOwned::TlsGetRecordType(t) => t, - _ => panic!("unexpected cmsg type: {cmsg:#?}"), - }; - - match record_type { - TlsGetRecordType::ChangeCipherSpec => { - panic!("change_cipher_spec isn't supported by the ktls crate") - } - TlsGetRecordType::Alert => { - // the alert level and description are in iovs - let iov = r.iovs().next().expect("expected data in iovs"); - - let (level, description) = match iov { - [] => { - // we have an early return case for that - unreachable!(); - } - &[level] => { - // https://github.com/facebookincubator/fizz/blob/fff6d9d49d3c554ab66b58822d1e1fe93e8d80f2/fizz/experimental/ktls/AsyncKTLSSocket.cpp#L144 - // - // Since all alerts (even warning-level alerts) - // signal the abort of a TLS session, we do not - // need to worry about additional application - // data. - // - // If we only have half the alert (because the - // user passed a buffer of size 1), just assume - // it's a close_notify - ( - TlsAlertLevel::from_primitive(level), - TlsAlertDescription::CloseNotify, - ) - } - &[level, description] => ( - TlsAlertLevel::from_primitive(level), - TlsAlertDescription::from_primitive(description), - ), - _ => { - unreachable!( - "TLS alerts are exactly 2 bytes, your kTLS is misbehaving" - ); - } - }; - - match (level, description) { - // https://datatracker.ietf.org/doc/html/rfc5246#section-7.2 - // alerts we should handle are ones with fatal level or a - // close_notify - (_, TlsAlertDescription::CloseNotify) | (TlsAlertLevel::Fatal, _) => { - tracing::trace!(?level, ?description, "got TLS alert"); - *this.read_closed = true; - *this.write_closed = true; - if let Err(e) = - crate::ffi::send_close_notify(this.inner.as_raw_fd()) - { - return Err(e).into(); - } - // the file descriptor will be closed when the stream is dropped, - // we already protect against writes-after-close_notify through - // the write_closed flag - return task::Poll::Ready(Ok(())); - } - _ => { - // we got something we probably can't handle - } - } - return task::Poll::Ready(Ok(())); - } - TlsGetRecordType::Handshake => { - // TODO: this is where we receive TLS 1.3 resumption tickets, - // should those be stored anywhere? I'm not even sure what - // format they have at this point - tracing::trace!( - "ignoring handshake message (probably a resumption ticket)" - ); - } - TlsGetRecordType::ApplicationData => { - unreachable!("received TLS application in recvmsg, this is supposed to happen in the poll_read codepath") - } - TlsGetRecordType::Unknown(t) => { - // just ignore the record? - tracing::trace!("received record_type {t:#?}"); - } - _ => { - tracing::trace!("received unsupported record type"); - } - }; - - // FIXME: this is hacky, but can we do better? - // after we handled (..ignored) the control message, we don't - // know whether the socket is still ready to be read or not. - // - // we could try looping (tricky code structure), but we can't, - // for example, just call `poll_read`, which might fail not - // with EAGAIN/EWOULDBLOCK, but because _another_ control - // message is available. - cx.waker().wake_by_ref(); - return task::Poll::Pending; - } - } - - read_res - } -} - -impl AsyncWrite for KtlsStream -where - IO: AsRawFd + AsyncWrite, -{ - fn poll_write( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &[u8], - ) -> task::Poll> { - if self.write_closed { - return task::Poll::Ready(Ok(0)); - } - - self.project().inner.poll_write(cx, buf) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll> { - self.project().inner.poll_flush(cx) - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll> { - let this = self.project(); - - if !*this.write_closed { - // they didn't hang up on us, we're nicely being asked to shut down, - // let's send a close_notify (and not wait for them to send it back) - *this.write_closed = true; - if let Err(e) = crate::ffi::send_close_notify(this.inner.as_raw_fd()) { - return Err(e).into(); - } - } - - // this ends up closing the inner file descriptor no matter what - this.inner.poll_shutdown(cx) - } -} - -impl AsRawFd for KtlsStream -where - IO: AsRawFd, -{ - fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd { - self.inner.as_raw_fd() - } -} diff --git a/ktls/src/lib.rs b/ktls/src/lib.rs index c428fe5..6885f1b 100644 --- a/ktls/src/lib.rs +++ b/ktls/src/lib.rs @@ -1,451 +1,33 @@ -use ffi::{setup_tls_info, setup_ulp, KtlsCompatibilityError}; -use futures_util::future::try_join_all; -use ktls_sys::bindings as sys; -use rustls::{Connection, SupportedCipherSuite, SupportedProtocolVersion}; - -#[cfg(all(not(feature = "ring"), not(feature = "aws_lc_rs")))] -compile_error!("This crate needs wither the 'ring' or 'aws_lc_rs' feature enabled"); -#[cfg(all(feature = "ring", feature = "aws_lc_rs"))] -compile_error!("The 'ring' and 'aws_lc_rs' features are mutually exclusive"); -#[cfg(feature = "aws_lc_rs")] -use rustls::crypto::aws_lc_rs::cipher_suite; -#[cfg(feature = "ring")] -use rustls::crypto::ring::cipher_suite; - -use smallvec::SmallVec; -use std::{ - future::Future, - io, - net::SocketAddr, - os::unix::prelude::{AsRawFd, RawFd}, -}; -use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite}, - net::{TcpListener, TcpStream}, -}; - +#![doc = include_str!("../README.md")] +#![warn( + unsafe_code, + unused_must_use, + clippy::alloc_instead_of_core, + clippy::exhaustive_enums, + clippy::exhaustive_structs, + clippy::manual_let_else, + clippy::use_self, + clippy::upper_case_acronyms, + elided_lifetimes_in_paths, + missing_docs, + trivial_casts, + trivial_numeric_casts, + unreachable_pub, + unused_import_braces, + unused_extern_crates, + unused_qualifications +)] + +#[cfg(not(target_os = "linux"))] +compile_error!("This crate only supports Linux"); + +pub mod error; mod ffi; -pub use crate::ffi::CryptoInfo; - -mod async_read_ready; -pub use async_read_ready::AsyncReadReady; - -mod ktls_stream; -pub use ktls_stream::KtlsStream; - -mod cork_stream; -pub use cork_stream::CorkStream; - -#[derive(Debug, Default)] -pub struct CompatibleCiphers { - pub tls12: CompatibleCiphersForVersion, - pub tls13: CompatibleCiphersForVersion, -} - -#[derive(Debug, Default)] -pub struct CompatibleCiphersForVersion { - pub aes_gcm_128: bool, - pub aes_gcm_256: bool, - pub chacha20_poly1305: bool, -} - -impl CompatibleCiphers { - /// List compatible ciphers. This listens on a TCP socket and blocks for a - /// little while. Do once at the very start of a program. Should probably be - /// behind a lazy_static / once_cell - pub async fn new() -> io::Result { - let mut ciphers = CompatibleCiphers::default(); - - let ln = TcpListener::bind("0.0.0.0:0").await?; - let local_addr = ln.local_addr()?; - - // Accepted conns of ln - let mut accepted_conns: SmallVec<[TcpStream; 12]> = SmallVec::new(); - - let accept_conns_fut = async { - loop { - if let Ok((conn, _addr)) = ln.accept().await { - accepted_conns.push(conn); - } - } - }; - - ciphers.test_ciphers(local_addr, accept_conns_fut).await?; - - Ok(ciphers) - } - - async fn test_ciphers( - &mut self, - local_addr: SocketAddr, - accept_conns_fut: impl Future, - ) -> io::Result<()> { - let ciphers: Vec<(SupportedCipherSuite, &mut bool)> = vec![ - ( - cipher_suite::TLS13_AES_128_GCM_SHA256, - &mut self.tls13.aes_gcm_128, - ), - ( - cipher_suite::TLS13_AES_256_GCM_SHA384, - &mut self.tls13.aes_gcm_256, - ), - ( - cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, - &mut self.tls13.chacha20_poly1305, - ), - ( - cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - &mut self.tls12.aes_gcm_128, - ), - ( - cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - &mut self.tls12.aes_gcm_256, - ), - ( - cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - &mut self.tls12.chacha20_poly1305, - ), - ]; - - let create_connections_fut = - try_join_all((0..ciphers.len()).map(|_| TcpStream::connect(local_addr))); - - let socks = tokio::select! { - // Use biased here to optimize performance. - // - // With biased, tokio::select! would first poll create_connections_fut, - // which would poll all `TcpStream::connect` futures and requests - // new connections to `ln` then returns `Poll::Pending`. - // - // Then accept_conns_fut would be polled, which accepts all pending - // connections, wake up create_connections_fut then returns - // `Poll::Pending`. - // - // Finally, create_connections_fut wakes up and all connections - // are ready, the result is collected into a Vec and ends - // the tokio::select!. - biased; - - res = create_connections_fut => res?, - _ = accept_conns_fut => unreachable!(), - }; - - assert_eq!(ciphers.len(), socks.len()); - - ciphers - .into_iter() - .zip(socks) - .for_each(|((cipher_suite, field), sock)| { - *field = sample_cipher_setup(&sock, cipher_suite).is_ok(); - }); - - Ok(()) - } - - /// Returns true if we're reasonably confident that functions like - /// [config_ktls_client] and [config_ktls_server] will succeed. - pub fn is_compatible(&self, suite: SupportedCipherSuite) -> bool { - let kcs = match KtlsCipherSuite::try_from(suite) { - Ok(kcs) => kcs, - Err(_) => return false, - }; - - let fields = match kcs.version { - KtlsVersion::TLS12 => &self.tls12, - KtlsVersion::TLS13 => &self.tls13, - }; - - match kcs.typ { - KtlsCipherType::AesGcm128 => fields.aes_gcm_128, - KtlsCipherType::AesGcm256 => fields.aes_gcm_256, - KtlsCipherType::Chacha20Poly1305 => fields.chacha20_poly1305, - } - } -} - -fn sample_cipher_setup(sock: &TcpStream, cipher_suite: SupportedCipherSuite) -> Result<(), Error> { - let kcs = match KtlsCipherSuite::try_from(cipher_suite) { - Ok(kcs) => kcs, - Err(_) => panic!("unsupported cipher suite"), - }; - - let ffi_version = match kcs.version { - KtlsVersion::TLS12 => ffi::TLS_1_2_VERSION_NUMBER, - KtlsVersion::TLS13 => ffi::TLS_1_3_VERSION_NUMBER, - }; - - let crypto_info = match kcs.typ { - KtlsCipherType::AesGcm128 => CryptoInfo::AesGcm128(sys::tls12_crypto_info_aes_gcm_128 { - info: sys::tls_crypto_info { - version: ffi_version, - cipher_type: sys::TLS_CIPHER_AES_GCM_128 as _, - }, - iv: Default::default(), - key: Default::default(), - salt: Default::default(), - rec_seq: Default::default(), - }), - KtlsCipherType::AesGcm256 => CryptoInfo::AesGcm256(sys::tls12_crypto_info_aes_gcm_256 { - info: sys::tls_crypto_info { - version: ffi_version, - cipher_type: sys::TLS_CIPHER_AES_GCM_256 as _, - }, - iv: Default::default(), - key: Default::default(), - salt: Default::default(), - rec_seq: Default::default(), - }), - KtlsCipherType::Chacha20Poly1305 => { - CryptoInfo::Chacha20Poly1305(sys::tls12_crypto_info_chacha20_poly1305 { - info: sys::tls_crypto_info { - version: ffi_version, - cipher_type: sys::TLS_CIPHER_CHACHA20_POLY1305 as _, - }, - iv: Default::default(), - key: Default::default(), - salt: Default::default(), - rec_seq: Default::default(), - }) - } - }; - let fd = sock.as_raw_fd(); - - setup_ulp(fd).map_err(Error::UlpError)?; - - setup_tls_info(fd, ffi::Direction::Tx, crypto_info)?; - - Ok(()) -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("failed to enable TLS ULP (upper level protocol): {0}")] - UlpError(#[source] std::io::Error), - - #[error("kTLS compatibility error: {0}")] - KtlsCompatibility(#[from] KtlsCompatibilityError), - - #[error("failed to export secrets")] - ExportSecrets(#[source] rustls::Error), - - #[error("failed to configure tx/rx (unsupported cipher?): {0}")] - TlsCryptoInfoError(#[source] std::io::Error), - - #[error("an I/O occured while draining the rustls stream: {0}")] - DrainError(#[source] std::io::Error), - - #[error("no negotiated cipher suite: call config_ktls_* only /after/ the handshake")] - NoNegotiatedCipherSuite, -} - -/// Configure kTLS for this socket. If this call succeeds, data can be written -/// and read from this socket, and the kernel takes care of encryption -/// transparently. I'm not clear how rekeying is handled (probably via control -/// messages, but can't find a code sample for it). -/// -/// The inner IO type must be wrapped in [CorkStream] since it's the only way -/// to drain a rustls stream cleanly. See its documentation for details. -pub async fn config_ktls_server( - mut stream: tokio_rustls::server::TlsStream>, -) -> Result, Error> -where - IO: AsRawFd + AsyncRead + AsyncReadReady + AsyncWrite + Unpin, -{ - stream.get_mut().0.corked = true; - let drained = drain(&mut stream).await.map_err(Error::DrainError)?; - let (io, conn) = stream.into_inner(); - let io = io.io; - - setup_inner(io.as_raw_fd(), Connection::Server(conn))?; - Ok(KtlsStream::new(io, drained)) -} - -/// Configure kTLS for this socket. If this call succeeds, data can be -/// written and read from this socket, and the kernel takes care of encryption -/// (and key updates, etc.) transparently. -/// -/// The inner IO type must be wrapped in [CorkStream] since it's the only way -/// to drain a rustls stream cleanly. See its documentation for details. -pub async fn config_ktls_client( - mut stream: tokio_rustls::client::TlsStream>, -) -> Result, Error> -where - IO: AsRawFd + AsyncRead + AsyncWrite + Unpin, -{ - stream.get_mut().0.corked = true; - let drained = drain(&mut stream).await.map_err(Error::DrainError)?; - let (io, conn) = stream.into_inner(); - let io = io.io; - - setup_inner(io.as_raw_fd(), Connection::Client(conn))?; - Ok(KtlsStream::new(io, drained)) -} - -/// Read all the bytes we can read without blocking. This is used to drained the -/// already-decrypted buffer from a tokio-rustls I/O type -async fn drain(stream: &mut (impl AsyncRead + Unpin)) -> std::io::Result>> { - tracing::trace!("Draining rustls stream"); - let mut drained = vec![0u8; 128 * 1024]; - let mut filled = 0; - - loop { - tracing::trace!("stream.read called"); - let n = match stream.read(&mut drained[filled..]).await { - Ok(n) => n, - Err(ref e) if e.kind() == std::io::ErrorKind::UnexpectedEof => { - // actually this is expected for us! - tracing::trace!("stream.read returned UnexpectedEof, that's expected for us"); - break; - } - Err(e) => { - tracing::trace!("stream.read returned error: {e}"); - return Err(e); - } - }; - tracing::trace!("stream.read returned {n}"); - if n == 0 { - // that's what CorkStream returns when it's at a message boundary - break; - } - filled += n; - } - - let maybe_drained = if filled == 0 { - None - } else { - tracing::trace!("Draining rustls stream done: drained {filled} bytes"); - drained.resize(filled, 0); - Some(drained) - }; - Ok(maybe_drained) -} - -fn setup_inner(fd: RawFd, conn: Connection) -> Result<(), Error> { - let cipher_suite = match conn.negotiated_cipher_suite() { - Some(cipher_suite) => cipher_suite, - None => { - return Err(Error::NoNegotiatedCipherSuite); - } - }; - - let secrets = match conn.dangerous_extract_secrets() { - Ok(secrets) => secrets, - Err(err) => return Err(Error::ExportSecrets(err)), - }; - - ffi::setup_ulp(fd).map_err(Error::UlpError)?; - - let tx = CryptoInfo::from_rustls(cipher_suite, secrets.tx)?; - setup_tls_info(fd, ffi::Direction::Tx, tx)?; - - let rx = CryptoInfo::from_rustls(cipher_suite, secrets.rx)?; - setup_tls_info(fd, ffi::Direction::Rx, rx)?; - - Ok(()) -} - -/// TLS versions supported by this crate -#[non_exhaustive] -#[derive(Debug, Clone, Copy)] -pub enum KtlsVersion { - TLS12, - TLS13, -} - -impl KtlsVersion { - /// Converts into the equivalent rustls [SupportedProtocolVersion] - pub fn as_supported_version(&self) -> &'static SupportedProtocolVersion { - match self { - KtlsVersion::TLS12 => &rustls::version::TLS12, - KtlsVersion::TLS13 => &rustls::version::TLS13, - } - } -} - -/// A TLS cipher suite. Used mostly internally. -#[derive(Clone, Copy)] -pub struct KtlsCipherSuite { - /// The TLS version - pub version: KtlsVersion, - - /// The cipher type - pub typ: KtlsCipherType, -} - -/// Cipher types supported by this crate -#[non_exhaustive] -#[derive(Debug, Clone, Copy)] -pub enum KtlsCipherType { - AesGcm128, - AesGcm256, - Chacha20Poly1305, -} - -#[derive(Debug, thiserror::Error)] -pub enum CipherSuiteError { - #[error("TLS 1.2 support not built in")] - Tls12NotBuiltIn, - - #[error("unsupported cipher suite")] - UnsupportedCipherSuite(SupportedCipherSuite), -} - -impl TryFrom for KtlsCipherSuite { - type Error = CipherSuiteError; - - fn try_from(#[allow(unused)] suite: SupportedCipherSuite) -> Result { - { - let version = match suite { - SupportedCipherSuite::Tls12(..) => { - if !cfg!(feature = "tls12") { - return Err(CipherSuiteError::Tls12NotBuiltIn); - } - KtlsVersion::TLS12 - } - SupportedCipherSuite::Tls13(..) => KtlsVersion::TLS13, - }; - - let family = { - if suite == cipher_suite::TLS13_AES_128_GCM_SHA256 { - KtlsCipherType::AesGcm128 - } else if suite == cipher_suite::TLS13_AES_256_GCM_SHA384 { - KtlsCipherType::AesGcm256 - } else if suite == cipher_suite::TLS13_CHACHA20_POLY1305_SHA256 { - KtlsCipherType::Chacha20Poly1305 - } else if suite == cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 { - KtlsCipherType::AesGcm128 - } else if suite == cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 { - KtlsCipherType::AesGcm256 - } else if suite == cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 { - KtlsCipherType::Chacha20Poly1305 - } else { - return Err(CipherSuiteError::UnsupportedCipherSuite(suite)); - } - }; - - Ok(Self { - typ: family, - version, - }) - } - } -} - -impl KtlsCipherSuite { - pub fn as_supported_cipher_suite(&self) -> SupportedCipherSuite { - match self.version { - KtlsVersion::TLS12 => match self.typ { - KtlsCipherType::AesGcm128 => cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - KtlsCipherType::AesGcm256 => cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - KtlsCipherType::Chacha20Poly1305 => { - cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 - } - }, - KtlsVersion::TLS13 => match self.typ { - KtlsCipherType::AesGcm128 => cipher_suite::TLS13_AES_128_GCM_SHA256, - KtlsCipherType::AesGcm256 => cipher_suite::TLS13_AES_256_GCM_SHA384, - KtlsCipherType::Chacha20Poly1305 => cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, - }, - } - } -} +pub mod log; +mod protocol; +pub mod setup; +pub mod stream; +pub mod utils; + +pub use error::Error; +pub use stream::KtlsStream; diff --git a/ktls/src/log.rs b/ktls/src/log.rs new file mode 100644 index 0000000..e52aa97 --- /dev/null +++ b/ktls/src/log.rs @@ -0,0 +1,61 @@ +//! Logger macros. + +#[macro_export] +#[doc(hidden)] +macro_rules! trace { + ($($tt:tt)*) => { + #[cfg(feature = "tracing")] + tracing::trace!($($tt)*); + + #[cfg(all(feature = "log", not(feature = "tracing")))] + log::trace!($($tt)*); + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! debug { + ($($tt:tt)*) => { + #[cfg(feature = "tracing")] + tracing::debug!($($tt)*); + + #[cfg(all(feature = "log", not(feature = "tracing")))] + log::debug!($($tt)*); + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! info { + ($($tt:tt)*) => { + #[cfg(feature = "tracing")] + tracing::info!($($tt)*); + + #[cfg(all(feature = "log", not(feature = "tracing")))] + log::info!($($tt)*); + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! warn { + ($($tt:tt)*) => { + #[cfg(feature = "tracing")] + tracing::warn!($($tt)*); + + #[cfg(all(feature = "log", not(feature = "tracing")))] + log::warn!($($tt)*); + }; +} + +#[macro_export] +#[doc(hidden)] +macro_rules! error { + ($($tt:tt)*) => { + #[cfg(feature = "tracing")] + tracing::error!($($tt)*); + + #[cfg(all(feature = "log", not(feature = "tracing")))] + log::error!($($tt)*); + }; +} diff --git a/ktls/src/protocol.rs b/ktls/src/protocol.rs new file mode 100644 index 0000000..fc5385a --- /dev/null +++ b/ktls/src/protocol.rs @@ -0,0 +1,80 @@ +//! TLS protocol enums that are not publically exposed by rustls. + +#![allow(non_upper_case_globals)] + +use std::fmt; + +macro_rules! c_enum { + { + $( #[$attr:meta] )* + $vis:vis enum $name:ident: $repr:ty { + $( + $( #[$vattr:meta] )* + $variant:ident = $value:expr + ),* $(,)? + } + } => { + $( #[$attr] )* + #[repr(transparent)] + $vis struct $name($vis $repr); + + impl $name { + $( + $( #[$vattr] )* + $vis const $variant: Self = Self($value); + )* + } + + impl fmt::Debug for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + $( const $variant: $repr = $name::$variant.0; )* + + let text = match self.0 { + $( $variant => concat!(stringify!($name), "::", stringify!($variant)), )* + _ => return f.debug_tuple(stringify!($name)).field(&self.0).finish() + }; + + f.write_str(text) + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + $( const $variant: $repr = $name::$variant.0; )* + + let text = match self.0 { + $( $variant => stringify!($variant), )* + _ => return <$repr as fmt::Display>::fmt(&self.0, f) + }; + + f.write_str(text) + } + } + + impl From<$repr> for $name { + fn from(value: $repr) -> Self { + Self(value) + } + } + + impl From<$name> for $repr { + fn from(value: $name) -> Self { + value.0 + } + } + } +} + +/// `KeyUpdate`, not requested +pub(crate) const KEY_UPDATE_NOT_REQUESTED: u8 = 0; + +/// `KeyUpdate`, requested +pub(crate) const KEY_UPDATE_REQUESTED: u8 = 1; + +c_enum! { + #[derive(Copy, Clone, Eq, PartialEq)] + pub(crate) enum KeyUpdateRequest: u8 { + UpdateNotRequested = KEY_UPDATE_NOT_REQUESTED, + UpdateRequested = KEY_UPDATE_REQUESTED + } +} diff --git a/ktls/src/setup.rs b/ktls/src/setup.rs new file mode 100644 index 0000000..dfc30b3 --- /dev/null +++ b/ktls/src/setup.rs @@ -0,0 +1,29 @@ +//! Transport Layer Security (TLS) is a Upper Layer Protocol (ULP) that runs +//! over TCP. TLS provides end-to-end data integrity and confidentiality. +//! +//! Once the TCP connection is established, sets the TLS ULP, which allows us to +//! set/get TLS socket options. +//! +//! This module provides the [`setup_ulp`] function, which sets the ULP (Upper +//! Layer Protocol) to TLS for a TCP socket. The user can also determine whether +//! the kernel supports kTLS with [`setup_ulp`]. +//! +//! After the TLS handshake is completed, we have all the parameters required to +//! move the data-path to the kernel. There is a separate socket option for +//! moving the transmit and the receive into the kernel. +//! +//! This module provides the low-level [`setup_tls_params`] function (when +//! feature `raw-api` is enabled), which sets the Kernel TLS parameters on the +//! TCP socket, allowing the kernel to handle encryption and decryption of the +//! TLS data. + +#![allow(clippy::module_name_repetitions)] + +pub(crate) mod tls; +pub(crate) mod ulp; + +#[cfg(feature = "raw-api")] +pub use tls::{ + setup_tls_params, setup_tls_params_rx, setup_tls_params_tx, TlsCryptoInfoRx, TlsCryptoInfoTx, +}; +pub use ulp::{setup_ulp, SetupError}; diff --git a/ktls/src/setup/tls.rs b/ktls/src/setup/tls.rs new file mode 100644 index 0000000..aab3466 --- /dev/null +++ b/ktls/src/setup/tls.rs @@ -0,0 +1,308 @@ +//! See the [module-level documentation](crate::setup) for more details. + +#![allow(rustdoc::private_intra_doc_links)] +#![allow(unreachable_pub)] + +use std::os::fd::{AsFd, AsRawFd}; +use std::{io, mem}; + +use libc::c_int; +use nix::errno::Errno; +use nix::sys::socket::{setsockopt, SetSockOpt}; +use rustls::crypto::cipher::NONCE_LEN; +use rustls::{ConnectionTrafficSecrets, ExtractedSecrets, SupportedCipherSuite}; + +use crate::error::{Error, InvalidCryptoInfo}; + +/// Sets the kTLS parameters on the socket after the TLS handshake is completed. +/// +/// ## Errors +/// +/// * Invalid crypto materials. +/// * Syscall error. +pub fn setup_tls_params( + socket: &S, + cipher_suite: SupportedCipherSuite, + secrets: ExtractedSecrets, +) -> Result<(), Error> { + let (tx, rx) = TlsCryptoInfo::extract_from(cipher_suite, secrets)?; + + setsockopt(socket, TcpTlsTx {}, &tx) + .and_then(|()| setsockopt(socket, TcpTlsRx {}, &rx)) + .map_err(io::Error::from) + .map_err(Error::IO) +} + +/// Like [`setup_tls_params`], but only sets up the transmit direction. +/// +/// This is useful when performing key update. +/// +/// ## Errors +/// +/// See [`setup_tls_params`]. +pub fn setup_tls_params_tx( + socket: &S, + cipher_suite: SupportedCipherSuite, + (seq, secrets): (u64, ConnectionTrafficSecrets), +) -> Result<(), Error> { + let tx = TlsCryptoInfoTx::extract_tx_from(cipher_suite, (seq, secrets))?; + + setsockopt(socket, TcpTlsTx {}, &tx) + .map_err(io::Error::from) + .map_err(Error::IO) +} + +/// Like [`setup_tls_params`], but only sets up the receive direction. +/// +/// This is useful when performing key update. +/// +/// ## Errors +/// +/// See [`setup_tls_params`]. +pub fn setup_tls_params_rx( + socket: &S, + cipher_suite: SupportedCipherSuite, + (seq, secrets): (u64, ConnectionTrafficSecrets), +) -> Result<(), Error> { + let rx = TlsCryptoInfoRx::extract_rx_from(cipher_suite, (seq, secrets))?; + + setsockopt(socket, TcpTlsRx {}, &rx) + .map_err(io::Error::from) + .map_err(Error::IO) +} + +#[derive(Debug, Clone, Copy)] +/// Sets the Kernel TLS read/write parameters on the TCP socket. +struct TcpTls {} + +/// See [`TcpTls`]. +type TcpTlsTx = TcpTls<{ libc::TLS_TX }>; + +/// See [`TcpTls`]. +type TcpTlsRx = TcpTls<{ libc::TLS_RX }>; + +impl SetSockOpt for TcpTls { + type Val = TlsCryptoInfo; + + fn set(&self, fd: &F, val: &Self::Val) -> nix::Result<()> { + let (ffi_ptr, ffi_len) = val.0.as_ffi_value(); + + #[allow(unsafe_code)] + // SAFETY: syscall + unsafe { + let res = libc::setsockopt( + fd.as_fd().as_raw_fd(), + libc::SOL_TLS, + DIRECTION, + ffi_ptr, + ffi_len, + ); + Errno::result(res)?; + } + + Ok(()) + } +} + +#[repr(transparent)] +/// Sets the Kernel TLS read/write parameters on the TCP socket. +pub struct TlsCryptoInfo(TlsCryptoInfoImpl); + +/// See [`TlsCryptoInfo`]. +pub type TlsCryptoInfoTx = TlsCryptoInfo<{ libc::TLS_TX }>; + +/// See [`TlsCryptoInfo`]. +pub type TlsCryptoInfoRx = TlsCryptoInfo<{ libc::TLS_RX }>; + +#[cfg(any(feature = "raw-api", feature = "probe-ktls-compatibility"))] +impl TlsCryptoInfo { + /// Create a custom [`TlsCryptoInfo`] from the given + /// [`libc::tls12_crypto_info_aes_gcm_128`]. + /// + /// This is for advanced usage only. + pub const fn custom_aes_128_gcm(inner: libc::tls12_crypto_info_aes_gcm_128) -> Self { + Self(TlsCryptoInfoImpl::AesGcm128(inner)) + } + + /// Create a custom [`TlsCryptoInfo`] from the given + /// [`libc::tls12_crypto_info_aes_gcm_256`]. + pub const fn custom_aes_256_gcm(inner: libc::tls12_crypto_info_aes_gcm_256) -> Self { + Self(TlsCryptoInfoImpl::AesGcm256(inner)) + } + + /// Create a custom [`TlsCryptoInfo`] from the given + /// [`libc::tls12_crypto_info_chacha20_poly1305`]. + pub const fn custom_chacha20_poly1305( + inner: libc::tls12_crypto_info_chacha20_poly1305, + ) -> Self { + Self(TlsCryptoInfoImpl::Chacha20Poly1305(inner)) + } + + /// Sets the kTLS parameters on the given file descriptor. + pub fn set(&self, fd: &Fd) -> Result<(), Error> { + setsockopt(fd, TcpTls {}, self) + .map_err(io::Error::from) + .map_err(Error::IO) + } +} + +impl TlsCryptoInfo { + /// Extract the bidirectional `TlsCryptoInfo` from the given + /// `SupportedCipherSuite` and `ExtractedSecrets`. + /// + /// ## Errors + /// + /// * Invalid crypto materials + fn extract_from( + cipher_suite: SupportedCipherSuite, + secrets: ExtractedSecrets, + ) -> Result<(TlsCryptoInfoTx, TlsCryptoInfoRx), InvalidCryptoInfo> { + Ok(( + TlsCryptoInfo(TlsCryptoInfoImpl::extract_from(cipher_suite, secrets.tx)?), + TlsCryptoInfo(TlsCryptoInfoImpl::extract_from(cipher_suite, secrets.rx)?), + )) + } +} + +impl TlsCryptoInfoTx { + fn extract_tx_from( + cipher_suite: SupportedCipherSuite, + (seq, secrets): (u64, ConnectionTrafficSecrets), + ) -> Result { + TlsCryptoInfoImpl::extract_from(cipher_suite, (seq, secrets)).map(TlsCryptoInfo) + } +} + +impl TlsCryptoInfoRx { + fn extract_rx_from( + cipher_suite: SupportedCipherSuite, + (seq, secrets): (u64, ConnectionTrafficSecrets), + ) -> Result { + TlsCryptoInfoImpl::extract_from(cipher_suite, (seq, secrets)).map(TlsCryptoInfo) + } +} + +#[repr(C)] +#[allow(unused)] +/// A wrapper around the system `tls12_crypto_info_*` structs, use with setting +/// up the kTLS r/w parameters on the TCP socket. +/// +/// This is originated from the `nix` crate, which currently does not support +/// `AES-128-CCM` and `SM4`, so we implement our own version here. +enum TlsCryptoInfoImpl { + AesGcm128(libc::tls12_crypto_info_aes_gcm_128), + AesGcm256(libc::tls12_crypto_info_aes_gcm_256), + Chacha20Poly1305(libc::tls12_crypto_info_chacha20_poly1305), + AesCcm128(libc::tls12_crypto_info_aes_ccm_128), + Sm4Gcm(libc::tls12_crypto_info_sm4_gcm), + Sm4Ccm(libc::tls12_crypto_info_sm4_ccm), +} + +impl TlsCryptoInfoImpl { + #[allow(unused_qualifications)] + #[allow(clippy::cast_possible_truncation)] // Since Rust 2021 doesn't have `size_of_val` included in prelude. + #[inline] + fn as_ffi_value(&self) -> (*const libc::c_void, libc::socklen_t) { + match self { + Self::AesGcm128(crypto_info) => ( + <*const _>::cast(crypto_info), + mem::size_of_val(crypto_info) as libc::socklen_t, + ), + Self::AesGcm256(crypto_info) => ( + <*const _>::cast(crypto_info), + mem::size_of_val(crypto_info) as libc::socklen_t, + ), + Self::AesCcm128(crypto_info) => ( + <*const _>::cast(crypto_info), + mem::size_of_val(crypto_info) as libc::socklen_t, + ), + Self::Chacha20Poly1305(crypto_info) => ( + <*const _>::cast(crypto_info), + mem::size_of_val(crypto_info) as libc::socklen_t, + ), + Self::Sm4Gcm(crypto_info) => ( + <*const _>::cast(crypto_info), + mem::size_of_val(crypto_info) as libc::socklen_t, + ), + Self::Sm4Ccm(crypto_info) => ( + <*const _>::cast(crypto_info), + mem::size_of_val(crypto_info) as libc::socklen_t, + ), + } + } + + /// Extract the `TlsCryptoInfoImpl` from the given + /// `SupportedCipherSuite` and `ConnectionTrafficSecrets`. + fn extract_from( + cipher_suite: SupportedCipherSuite, + (seq, secrets): (u64, ConnectionTrafficSecrets), + ) -> Result { + let version = match cipher_suite { + #[cfg(feature = "tls12")] + SupportedCipherSuite::Tls12(..) => libc::TLS_1_2_VERSION, + SupportedCipherSuite::Tls13(..) => libc::TLS_1_3_VERSION, + }; + + Ok(match secrets { + ConnectionTrafficSecrets::Aes128Gcm { key, iv } => { + // see https://github.com/rustls/rustls/issues/1833, between + // rustls 0.21 and 0.22, the extract_keys codepath was changed, + // so, for TLS 1.2, both GCM-128 and GCM-256 return the + // Aes128Gcm variant. + // + // This issue is fixed since rustls 0.23. + + let iv_and_salt: &[u8; NONCE_LEN] = iv.as_ref().try_into().unwrap(); + + Self::AesGcm128(libc::tls12_crypto_info_aes_gcm_128 { + info: libc::tls_crypto_info { + version, + cipher_type: libc::TLS_CIPHER_AES_GCM_128, + }, + iv: iv_and_salt[4..].try_into().unwrap(), + key: key + .as_ref() + .try_into() + .map_err(|_| InvalidCryptoInfo::WrongSizeKey)?, + salt: iv_and_salt[..4].try_into().unwrap(), + rec_seq: seq.to_be_bytes(), + }) + } + ConnectionTrafficSecrets::Aes256Gcm { key, iv } => { + let iv_and_salt: &[u8; NONCE_LEN] = iv.as_ref().try_into().unwrap(); + + Self::AesGcm256(libc::tls12_crypto_info_aes_gcm_256 { + info: libc::tls_crypto_info { + version, + cipher_type: libc::TLS_CIPHER_AES_GCM_256, + }, + iv: iv_and_salt[4..].try_into().unwrap(), + key: key + .as_ref() + .try_into() + .map_err(|_| InvalidCryptoInfo::WrongSizeKey)?, + salt: iv_and_salt[..4].try_into().unwrap(), + rec_seq: seq.to_be_bytes(), + }) + } + ConnectionTrafficSecrets::Chacha20Poly1305 { key, iv } => { + Self::Chacha20Poly1305(libc::tls12_crypto_info_chacha20_poly1305 { + info: libc::tls_crypto_info { + version, + cipher_type: libc::TLS_CIPHER_CHACHA20_POLY1305, + }, + iv: iv.as_ref().try_into().unwrap(), + key: key + .as_ref() + .try_into() + .map_err(|_| InvalidCryptoInfo::WrongSizeKey)?, + salt: [], + rec_seq: seq.to_be_bytes(), + }) + } + _ => { + return Err(InvalidCryptoInfo::UnsupportedCipherSuite(cipher_suite)); + } + }) + } +} diff --git a/ktls/src/setup/ulp.rs b/ktls/src/setup/ulp.rs new file mode 100644 index 0000000..96cf889 --- /dev/null +++ b/ktls/src/setup/ulp.rs @@ -0,0 +1,61 @@ +//! See the [module-level documentation](crate::setup) for more details. + +use std::os::fd::AsFd; +use std::{fmt, io}; + +use nix::errno::Errno; +use nix::sys::socket::{setsockopt, sockopt}; + +/// Sets the TLS Upper Layer Protocol (ULP). +/// +/// This should be called before performing any I/O operations on the +/// socket. +/// +/// # Errors +/// +/// [`SetupError`]. +/// +/// If the error is caused by the system not supporting kTLS, such as kernel +/// module `tls` not being enabled or the kernel version being too old, will +/// have the original socket returned, see [`SetupError::socket`]. +pub fn setup_ulp(socket: S) -> Result> { + match setsockopt(&socket, sockopt::TcpUlp::default(), b"tls") { + Ok(()) => Ok(socket), + Err(err) if err == Errno::ENOENT => Err(SetupError { + error: io::Error::from(err), + socket: Some(socket), + }), + Err(err) => Err(SetupError { + error: io::Error::from(err), + socket: None, + }), + } +} + +#[allow(clippy::exhaustive_structs)] +/// An error that occurred while configuring the ULP. +pub struct SetupError { + /// The I/O error that occurred while configuring the ULP. + pub error: io::Error, + + /// The original I/O socket. + pub socket: Option, +} + +impl fmt::Debug for SetupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.error.fmt(f) + } +} + +impl fmt::Display for SetupError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.error) + } +} + +impl std::error::Error for SetupError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.error) + } +} diff --git a/ktls/src/stream.rs b/ktls/src/stream.rs new file mode 100644 index 0000000..5d06222 --- /dev/null +++ b/ktls/src/stream.rs @@ -0,0 +1,251 @@ +//! See [`KtlsStream`]. + +#![allow(clippy::module_name_repetitions)] + +pub mod context; +pub mod error; +pub mod impl_std; +#[cfg(feature = "async-io-tokio")] +pub mod impl_tokio; + +#[cfg(feature = "raw-api")] +use std::io; +use std::os::fd::AsFd; + +use rustls::client::UnbufferedClientConnection; +use rustls::server::UnbufferedServerConnection; + +use crate::error::Error; +use crate::setup::tls::setup_tls_params; +#[cfg(feature = "raw-api")] +use crate::stream::context::Buffer; +use crate::stream::context::{Context, StreamState, TlsConnData}; + +const DEFAULT_SCRATCH_CAPACITY: usize = 64; + +pin_project_lite::pin_project! { + #[derive(Debug)] + #[project = KTlsStreamProject] + /// A thin wrapper around an inner socket with kernel TLS (kTLS) offload + /// configured. + /// + /// This implements traits [`Read`](std::io::Read) and + /// [`Write`](std::io::Write), [`AsyncRead`](tokio::io::AsyncRead) and + /// [`AsyncWrite`](tokio::io::AsyncWrite) (when feature `async-io-tokio` is + /// enabled). + /// + /// For those who may need low-level access to the inner socket, feature + /// `raw-api` provides an unsafe method [`as_raw`](Self::as_raw) to get a + /// mutable reference to the inner socket. + /// + /// ## Behaviours + /// + /// Once a TLS `close_notify` alert from the peer is received, all subsequent + /// read operations will return EOF. + /// + /// Once the caller explicitly calls `(poll_)shutdown` on the stream, all + /// subsequent write operations will return 0 bytes, indicating that the + /// stream is closed for writing. + /// + /// Once the stream is being dropped, a `close_notify` alert would be sent to + /// the peer automatically before shutting down the inner socket, according to + /// [RFC 8446, section 6.1]. + /// + /// The caller may call `(poll_)shutdown` on the stream to shutdown explicitly + /// both sides of the stream. Currently, there's no way provided by this crate + /// to shutdown the TLS stream write side only. For TLS 1.2, this is ideal since + /// once one party sends a `close_notify` alert, *the other party MUST respond + /// with a `close_notify` alert of its own and close down the connection + /// immediately*, according to [RFC 5246, section 7.2.1]; for TLS 1.3, *both + /// parties need not wait to receive a "`close_notify`" alert before + /// closing their read side of the connection*, according to [RFC 8446, section + /// 6.1]. + /// + /// [RFC 5246, section 7.2.1]: https://tools.ietf.org/html/rfc5246#section-7.2.1 + /// [RFC 8446, section 6.1]: https://tools.ietf.org/html/rfc8446#section-6.1 + pub struct KtlsStream { + #[pin] + inner: S, + ctx: Context, + } + + impl PinnedDrop for KtlsStream { + fn drop(this: Pin<&mut Self>) { + let this = this.project(); + + // TODO: No need to flush? It's a no-op anyway for TcpStream / UnixStream. + this.ctx.shutdown(&*this.inner); + } + } +} + +impl KtlsStream +where + S: AsFd, +{ + /// Attempts to construct a new [`KtlsStream`] from the provided socket and + /// [`UnbufferedClientConnection`]. + /// + /// ## Prerequisites + /// + /// - The provided [`UnbufferedClientConnection`] must meet the following + /// requirements: + /// + /// - TLS handshake must be completed + /// - [`enable_extract_secrets`](rustls::ClientConfig::enable_secret_extraction) must be set to `true` + /// + /// For detailed information about these prerequisites, see the + /// [`rustls::kernel`] module documentation. + /// + /// - The socket provided must have ULP configured with + /// [`setup_ktls_ulp`](crate::setup::setup_ulp) in advance. + /// + /// ## Errors + /// + /// Returns an error if the connection does not meet the prerequisites or if + /// the underlying kernel TLS setup fails. + pub fn from_unbuffered_client_connnection( + socket: S, + conn: UnbufferedClientConnection, + ) -> Result { + let (secrets, conn) = conn + .dangerous_into_kernel_connection() + .map_err(Error::ExtractSecrets)?; + + let supported_cipher_suite = conn.negotiated_cipher_suite(); + + let this = Self { + inner: socket, + ctx: Context::new(StreamState::empty(), Vec::new(), TlsConnData::Client(conn)), + }; + + setup_tls_params(&this.inner, supported_cipher_suite, secrets)?; + + Ok(this) + } + + /// Attempts to construct a new [`KtlsStream`] from the provided socket and + /// [`UnbufferedServerConnection`]. + /// + /// ## Prerequisites + /// + /// - The provided [`UnbufferedServerConnection`] must meet the following + /// requirements: + /// + /// - TLS handshake must be completed + /// - [`enable_extract_secrets`](rustls::ServerConfig::enable_secret_extraction) must be set to `true` + /// + /// For detailed information about these prerequisites, see the + /// [`rustls::kernel`] module documentation. + /// + /// - The socket provided must have ULP configured with + /// [`setup_ktls_ulp`](crate::setup::setup_ulp) in advance. + /// + /// ## Errors + /// + /// Returns an error if the connection does not meet the prerequisites or if + /// the underlying kernel TLS setup fails. + pub fn from_unbuffered_server_connnection( + socket: S, + conn: UnbufferedServerConnection, + early_data_received: Option>, + ) -> Result { + let (secrets, conn) = conn + .dangerous_into_kernel_connection() + .map_err(Error::ExtractSecrets)?; + + // From here, the connection is considered established and handshake is + // completed + + let supported_cipher_suite = conn.negotiated_cipher_suite(); + let (state, buffer) = match early_data_received { + Some(early_data_received) if !early_data_received.is_empty() => { + (StreamState::HAS_BUFFERED_DATA, early_data_received) + } + _ => ( + StreamState::empty(), + Vec::with_capacity(DEFAULT_SCRATCH_CAPACITY), + ), + }; + + let this = Self { + inner: socket, + ctx: Context::new(state, buffer, TlsConnData::Server(conn)), + }; + + setup_tls_params(&this.inner, supported_cipher_suite, secrets)?; + + Ok(this) + } + + #[allow(unsafe_code)] + #[cfg(feature = "raw-api")] + #[inline] + /// Returns a mutable reference to the inner socket if the TLS stream is not + /// closed (unidirectionally or bidirectionally). + /// + /// This requires a mutable reference to the [`KtlsStream`] to ensure a + /// exclusive access to the inner socket. + /// + /// ## Safety + /// + /// The caller must ensure that: + /// + /// * All buffered data **MUST** be retrieved using + /// [`Self::take_buffered_data`] and properly consumed before accessing + /// the inner socket. Buffered data typically consists of: + /// + /// - Early data received during handshake. + /// - Application data received due to improper usage of + /// [`Self::handle_io_result`]. + /// + /// * The caller **MAY** handle any [`io::Result`]s returned by I/O + /// operations on the inner socket with [`Self::handle_io_result`]. + /// + /// * The caller **MUST NOT** shutdown the inner socket directly, which will + /// lead to undefined behaviours. Instead, the caller **MAY** call + /// `(poll_)shutdown` explictly on the [`KtlsStream`] to gracefully + /// shutdown the TLS stream (with `close_notify` be sent) manually, or + /// just drop the stream to do automatic graceful shutdown. + /// + /// [RFC 8446, section 6.1]: https://tools.ietf.org/html/rfc8446#section-6.1 + pub unsafe fn as_raw(&mut self) -> Option<&mut S> { + debug_assert!( + !self.ctx.state().has_buffered_data(), + "Buffered data must be consumed before accessing the inner stream." + ); + + if self.ctx.state().is_partially_closed() { + return None; + } + + Some(&mut self.inner) + } + + #[cfg(feature = "raw-api")] + /// Inspects and handles the [`io::Result`] returned by a I/O operation on + /// the inner socket directly. + /// + /// - If the result is `Ok`, it returns `Some(T)`. + /// - If the errno is [`EIO`](libc::EIO), it tries to handle any TLS control + /// messages received, and returns `None` if succeeded. + /// - Otherwise, it aborts the connection with `internal_error` alert and + /// returns the error. + /// + /// ## Errors + /// + /// The unrecoverable original [`io::Error`]. + pub fn handle_io_result(&mut self, ret: io::Result) -> io::Result> { + self.ctx.handle_io_result(&self.inner, ret) + } + + #[cfg(feature = "raw-api")] + #[must_use = "The buffered data must be handled."] + /// Takes the buffered data, if any, and resets the buffer state. + /// + /// This method is useful and should be called before performing low-level + /// I/O operations on the inner socket. + pub fn take_buffered_data(&mut self) -> Option { + self.ctx.take_buffer() + } +} diff --git a/ktls/src/stream/context.rs b/ktls/src/stream/context.rs new file mode 100644 index 0000000..34c8a03 --- /dev/null +++ b/ktls/src/stream/context.rs @@ -0,0 +1,956 @@ +//! kTLS stream context + +use std::num::NonZeroUsize; +use std::os::fd::{AsFd, AsRawFd}; +use std::{fmt, io, mem, ops, slice}; + +use nix::errno::Errno; +use nix::sys::socket::{cmsg_space, recvmsg, ControlMessageOwned, MsgFlags, TlsGetRecordType}; +use rustls::client::ClientConnectionData; +use rustls::internal::msgs::codec::Reader; +use rustls::internal::msgs::enums::AlertLevel; +use rustls::kernel::KernelConnection; +use rustls::server::ServerConnectionData; +use rustls::{ + AlertDescription, ConnectionTrafficSecrets, ContentType, HandshakeType, InvalidMessage, + PeerMisbehaved, ProtocolVersion, SupportedCipherSuite, +}; + +use crate::protocol::{KeyUpdateRequest, KEY_UPDATE_NOT_REQUESTED, KEY_UPDATE_REQUESTED}; +use crate::setup::tls::{setup_tls_params_rx, setup_tls_params_tx}; +use crate::stream::error::KtlsStreamError; + +/// Helper macro to handle the return value of an I/O operation on the +/// kTLS stream. +macro_rules! handle_ret { + ($this:expr, $($tt:tt)+) => { + loop { + let ret = $($tt)+; + + if let Some(ret) = $this.ctx.handle_io_result(&$this.inner, ret).transpose() { + return ret.into(); + }; + } + }; +} + +#[allow(unused)] +/// `poll` version of `handle_ret` macro, for async I/O operations. +macro_rules! handle_ret_async { + ($this:expr, $($tt:tt)+) => { + loop { + let ret = std::task::ready!($($tt)+); + + if let Some(ret) = $this.ctx.handle_io_result(&*$this.inner, ret).transpose() { + return std::task::Poll::Ready(ret); + }; + } + }; +} + +#[allow(unused)] +pub(crate) use {handle_ret, handle_ret_async}; + +macro_rules! abort_and_return_error { + ($ctx:expr, $stream:expr, $desc:expr, $error:expr) => { + let _ = $ctx.abort($stream, Some($desc)); + + return Err($error); + }; + ($ctx:expr, $stream:expr, $error:expr) => { + let _ = $ctx.abort($stream, None); + + return Err($error); + }; +} + +#[derive(Debug)] +/// kTLS stream context. +pub(crate) struct Context { + /// The I/O state + state: StreamState, + + /// Shared buffer + buffer: Buffer, + + /// The TLS connection data, managing connection secrets and session + /// tickets. + data: TlsConnData, +} + +impl Context { + /// Creates a new context. + pub(crate) fn new(state: StreamState, buffer: Vec, data: TlsConnData) -> Self { + Self { + state, + buffer: Buffer { + inner: buffer, + offset: 0, + }, + data, + } + } + + /// Returns the current state. + pub(crate) fn state(&self) -> &StreamState { + &self.state + } + + /// Reads buffered data from the inner buffer into the provided one, and + /// returns the number of bytes read. + pub(crate) fn read_buffer(&mut self, buf: &mut [u8]) -> Option { + crate::trace!( + "Reading from internal buffer, remaining_len={}", + self.buffer.len() + ); + + // Read from the inner buffer into the provided buffer + let has_read = self.buffer.read(buf); + + if self.buffer.is_read_done() { + // Clear the buffer. + self.buffer.reset(); + self.state.set_has_buffered_data(false); + } + + has_read + } + + #[must_use = "The buffered data must be handled."] + #[cfg_attr(not(feature = "raw-api"), allow(unused))] + /// Takes the buffered data, if any. + pub(crate) fn take_buffer(&mut self) -> Option { + if self.state.has_buffered_data() { + self.state.set_has_buffered_data(false); + + Some(mem::take(&mut self.buffer)) + } else { + None + } + } + + // /// Returns the TLS connection data. + // pub(crate) fn data(&self) -> &TlsConnData { + // &self.data + // } + + /// Shuts down the TLS stream and sends a `close_notify` alert to the peer. + pub(crate) fn shutdown(&mut self, socket: &S) { + crate::trace!("Shutting down the TLS stream with `close_notify` alert"); + + #[allow(unused)] + if let Err(e) = self.set_closed_state_and_try_send_alert( + socket, + AlertLevel::Warning, + AlertDescription::CloseNotify, + ) { + crate::error!("Failed to send `close_notify` alert: {}", e); + } else { + crate::trace!("`close_notify` alert sent"); + }; + } + + /// Aborts the TLS stream and sends an `internal_error` alert to the peer. + pub(crate) fn abort(&mut self, socket: &S, desc: Option) { + crate::trace!("Aborting the TLS stream with `internal_error` alert"); + + #[allow(unused)] + if let Err(e) = self.set_closed_state_and_try_send_alert( + socket, + AlertLevel::Fatal, + desc.unwrap_or(AlertDescription::InternalError), + ) { + crate::error!("Failed to send `internal_error` alert: {}", e); + } else { + crate::trace!("`internal_error` alert sent"); + }; + } + + fn set_closed_state_and_try_send_alert( + &mut self, + socket: &S, + level: AlertLevel, + desc: AlertDescription, + ) -> io::Result<()> { + let ret = if self.state.is_write_closed() { + Ok(()) + } else { + Self::try_send_tls_control_message( + socket, + ContentType::Alert, + &[level.into(), desc.into()], + ) + .map(|_| ()) + }; + + self.state.set_closed(); + + ret + } + + #[inline] + #[cfg_attr( + feature = "tracing", + tracing::instrument( + level = "INFO", + name = "Context::handle_io_result", + skip(socket, ret), + err + ) + )] + /// Inspects and handles the [`io::Result`] returned by a I/O operation on + /// the inner socket directly. + /// + /// - If the result is `Ok`, it returns `Some(T)`. + /// - If the errno is `EIO`, it tries to handle any TLS control messages + /// received, and returns `None` if succeeded. + /// - If the error kind is `BrokenPipe`, it marks the stream as closed and + /// returns `None`. + /// - Otherwise, it aborts the connection with `internal_error` alert and + /// returns the error. + /// + /// ## Errors + /// + /// The unrecoverable original [`io::Error`]. + pub(crate) fn handle_io_result( + &mut self, + socket: &S, + ret: io::Result, + ) -> io::Result> { + match ret { + Ok(ret) => Ok(Some(ret)), + Err(e) if e.raw_os_error() == Some(libc::EIO) => { + crate::debug!("Received EIO, trying to receive TLS control message"); + + self.try_recv_tls_control_message(socket)?; + + Ok(None) + } + Err(e) if e.kind() == io::ErrorKind::BrokenPipe => { + // The peer may send a `close_notify` alert and close the connection + // immediately? + crate::debug!("The peer closed the connection: {}", e); + + self.state.set_closed(); + + Ok(None) + } + Err(e) => { + self.abort(socket, None); + + Err(e) + } + } + } + + // === Internal methods === + + /// Other than application data, TLS has control messages such as alert + /// messages (record type 21) and handshake messages (record type 22), etc. + /// These messages can be sent over the socket with this method. + /// + /// Control message data should be provided unencrypted, and will be + /// encrypted by the kernel. + fn try_send_tls_control_message( + socket: &S, + typ: ContentType, + encoded_payload: &[u8], + ) -> io::Result { + // Nix does not support sending control messages with `sendmsg` yet. + + // TODO: Should an error here abort the whole connection? + + crate::ffi::sendmsg( + socket.as_fd().as_raw_fd(), + &mut [io::IoSlice::new(encoded_payload)], + &mut crate::ffi::Cmsg::new(libc::SOL_TLS, libc::TLS_SET_RECORD_TYPE, [typ.into()]), + 0, + ) + } + + #[allow(clippy::too_many_lines)] + /// Handles TLS control messages received by kernel. + /// + /// The caller **SHOULD** first check if the raw os error returned were + /// `EIO`, which indicates that there is a TLS control message available. + /// But in fact, this method can be called even if there's no TLS control + /// message (not recommended to do so). + /// + /// Will abort the connection if the control message is invalid or + /// unexpected, and return an error. + fn try_recv_tls_control_message(&mut self, socket: &S) -> io::Result<()> { + // Reuse the existing buffer to avoid extra allocations. + self.buffer.inner.reserve(u16::MAX as usize + 5); + + #[allow(unsafe_code)] + // Safety: We have reserved enough space in the buffer above. + let mut buffer: &mut [u8] = unsafe { + slice::from_raw_parts_mut( + self.buffer + .inner + .as_mut_ptr() + .add(self.buffer.inner.len()) + .cast(), + self.buffer.inner.capacity() - self.buffer.inner.len(), + ) + }; + let buffer_capacity = buffer.len(); + + // Read the control message and the associated data into the buffer. + let content_type = { + let (content_type, recv_bytes) = { + // For Linux kernel <= 5.10, will read more cmsgs than one. + let cmsg_buffer = + &mut [mem::MaybeUninit::::zeroed(); cmsg_space::() * 24]; + + let iov = &mut [io::IoSliceMut::new(buffer)]; + + let recv_msg = match recvmsg::<()>( + socket.as_fd().as_raw_fd(), + iov, + { + #[allow(unsafe_code)] + // Safety: will access only the initialized part of the buffer below. + Some(unsafe { + slice::from_raw_parts_mut( + cmsg_buffer.as_mut_ptr().cast(), + cmsg_buffer.len(), + ) + }) + }, + MsgFlags::MSG_DONTWAIT, + ) { + Ok(recv_msg) => recv_msg, + Err(Errno::EAGAIN) => { + return Ok(()); + } + Err(e) => { + abort_and_return_error!( + self, + socket, + io::Error::other(format!("recvmsg failed: {e}")) + ); + } + }; + + if recv_msg.bytes > buffer_capacity { + abort_and_return_error!( + self, + socket, + io::Error::other(format!( + "recvmsg read more bytes ({}) than maximum ({})?", + recv_msg.bytes, buffer_capacity + )) + ); + } + + let mut cmsgs = recv_msg + .cmsgs() + .expect("should have 1..24 control message received"); + + match cmsgs.next().expect("should have at least one CMSG?") { + ControlMessageOwned::TlsGetRecordType(content_type) => { + // `recv` will never return data from mixed types of TLS records. + debug_assert!(cmsgs.all(|cmsg| { + matches!(cmsg, ControlMessageOwned::TlsGetRecordType(_)) + })); + + (content_type, recv_msg.bytes) + } + value => { + abort_and_return_error!( + self, + socket, + io::Error::other(format!( + "unknown control message received: {value:?}" + )) + ); + } + } + }; + + // Safety: We have just written `recv_msg.bytes` bytes to the spare capacity of + // the buffer. + buffer = &mut buffer[..recv_bytes]; + + content_type + }; + + match content_type { + TlsGetRecordType::Handshake => { + self.try_handle_tls_control_message_handshake(socket, buffer)?; + } + TlsGetRecordType::Alert => { + if let [level, desc] = buffer { + self.try_handle_tls_control_message_alert( + socket, + (*level).into(), + (*desc).into(), + )?; + } else { + // The peer sent an invalid alert. We send back an error + // and close the connection. + + crate::error!("Invalid alert message received: {:?}", &buffer); + + abort_and_return_error!( + self, + socket, + AlertDescription::DecodeError, + KtlsStreamError::InvalidMessage(InvalidMessage::MessageTooLarge).into() + ); + } + } + TlsGetRecordType::ChangeCipherSpec => { + // ChangeCipherSpec should only be sent under the following conditions: + // + // * TLS 1.2: during a handshake or a rehandshake + // * TLS 1.3: during a handshake + // + // We don't have to worry about handling messages during a handshake + // and rustls does not support TLS 1.2 rehandshakes so we just emit + // an error here and abort the connection. + + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::PeerMisbehaved( + PeerMisbehaved::IllegalMiddleboxChangeCipherSpec, + ) + .into() + ); + } + TlsGetRecordType::ApplicationData => { + // This shouldn't happen in normal operation. + + crate::warn!( + "Received {} bytes of application data when handling TLS control message", + buffer.len() + ); + + if !buffer.is_empty() { + #[allow(unsafe_code)] + // SAFETY: We have checked the buffer length above. + unsafe { + self.buffer + .inner + .set_len(self.buffer.inner.len() + buffer.len()); + } + + self.state.set_has_buffered_data(true); + } + } + _ => { + crate::error!( + "Received unexpected TLS control message: {content_type:?}, with data {:?}", + buffer + ); + + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::InvalidMessage(InvalidMessage::InvalidContentType).into() + ); + } + } + + Ok(()) + } + + /// Handles a TLS alert received from the peer. + fn try_handle_tls_control_message_alert( + &mut self, + socket: &S, + level: AlertLevel, + desc: AlertDescription, + ) -> io::Result<()> { + match desc { + AlertDescription::CloseNotify + if self.data.protocol_version() == ProtocolVersion::TLSv1_2 => + { + // RFC 5246, section 7.2.1: Unless some other fatal alert has been transmitted, + // each party is required to send a close_notify alert before closing the write + // side of the connection. The other party MUST respond with a close_notify + // alert of its own and close down the connection immediately, discarding any + // pending writes. + crate::trace!("Received `close_notify` alert, should shutdown the TLS stream"); + + self.shutdown(socket); + } + AlertDescription::CloseNotify => { + // RFC 8446, section 6.1: Each party MUST send a "close_notify" alert before + // closing its write side of the connection, unless it has already sent some + // error alert. This does not have any effect on its read side of the + // connection. Note that this is a change from versions of TLS prior to TLS 1.3 + // in which implementations were required to react to a "close_notify" by + // discarding pending writes and sending an immediate "close_notify" alert of + // their own. That previous requirement could cause truncation in the read + // side. Both parties need not wait to receive a "close_notify" alert before + // closing their read side of the connection, though doing so would introduce + // the possibility of truncation. + + crate::trace!( + "Received `close_notify` alert, should shutdown the read side of TLS stream" + ); + + self.state.set_read_closed(); + } + _ if self.data.protocol_version() == ProtocolVersion::TLSv1_2 + && level == AlertLevel::Warning => + { + // RFC 5246, section 7.2.2: If an alert with a level of warning + // is sent and received, generally the connection can continue + // normally. + + crate::warn!("Received alert, level={level:?}, desc: {desc:?}"); + } + _ => { + // All other alerts are treated as fatal and result in us immediately shutting + // down the connection and emitting an error. + + crate::error!("Received fatal alert, level={level:?}, desc: {desc:?}"); + + self.state.set_closed(); + + return Err(KtlsStreamError::Alert(desc).into()); + } + } + + Ok(()) + } + + #[allow(clippy::too_many_lines)] + /// Handles a TLS alert received from the peer. + fn try_handle_tls_control_message_handshake( + &mut self, + socket: &S, + payload: &[u8], + ) -> io::Result<()> { + fn read_message<'a>(reader: &mut Reader<'a>) -> Option<(HandshakeType, &'a [u8])> { + let &[typ, a, b, c] = reader.take(4)? else { + unreachable!() + }; + + let handshake_type = HandshakeType::from(typ); + let length = u32::from_be_bytes([0, a, b, c]) as usize; + + let payload = reader.take(length)?; + + Some((handshake_type, payload)) + } + + let mut reader = Reader::init(payload); + let mut sub_message_count = 0; + + loop { + let Some((handshake_type, payload)) = read_message(&mut reader) else { + crate::error!( + "Received truncated handshake message, payload: {:?}", + payload + ); + + abort_and_return_error!( + self, + socket, + AlertDescription::DecodeError, + KtlsStreamError::InvalidMessage(InvalidMessage::MessageTooShort).into() + ); + }; + + sub_message_count += 1; + + match handshake_type { + HandshakeType::KeyUpdate + if self.data.protocol_version() == ProtocolVersion::TLSv1_3 => + { + self.try_handle_tls_control_message_handshake_key_update( + socket, + payload, + &reader, + sub_message_count, + )?; + } + HandshakeType::NewSessionTicket + if self.data.protocol_version() == ProtocolVersion::TLSv1_3 => + { + let TlsConnData::Client(conn) = &mut self.data else { + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::InvalidMessage(InvalidMessage::UnexpectedMessage( + "TLS 1.2 peer sent a TLS 1.3 NewSessionTicket message", + )) + .into() + ); + }; + + match conn.handle_new_session_ticket(payload) { + Ok(()) => (), + // Convert some messages into their higher-level equivalents + Err(rustls::Error::InvalidMessage(err)) => { + abort_and_return_error!( + self, + socket, + AlertDescription::DecodeError, + KtlsStreamError::InvalidMessage(err).into() + ); + } + Err(rustls::Error::PeerMisbehaved(err)) => { + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::PeerMisbehaved(err).into() + ); + } + + // Other errors are not necessarily fatal + Err(_) => {} + } + } + _ if self.data.protocol_version() == ProtocolVersion::TLSv1_3 => { + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::InvalidMessage(InvalidMessage::UnexpectedMessage( + "expected KeyUpdate or NewSessionTicket handshake messages only", + )) + .into() + ); + } + _ => { + crate::error!( + "Unexpected handshake message: ver={:?}, typ={handshake_type:?}", + self.data.protocol_version() + ); + + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::InvalidMessage(InvalidMessage::UnexpectedMessage( + "handshake messages are not expected on TLS 1.2 connections", + )) + .into() + ); + } + } + + if reader.any_left() { + crate::trace!("Processing next sub messages."); + } else { + crate::trace!("All sub messages are processed."); + return Ok(()); + } + } + } + + fn try_handle_tls_control_message_handshake_key_update( + &mut self, + socket: &S, + payload: &[u8], + reader: &Reader<'_>, + sub_message_count: usize, + ) -> io::Result<()> { + if sub_message_count != 1 || reader.any_left() { + // RFC 8446, section 5.1: Handshake messages MUST NOT span key changes. + abort_and_return_error!( + self, + socket, + AlertDescription::UnexpectedMessage, + KtlsStreamError::PeerMisbehaved(PeerMisbehaved::KeyEpochWithPendingFragment).into() + ); + } + + let key_update_request = match payload { + [KEY_UPDATE_REQUESTED] => KeyUpdateRequest::UpdateRequested, + [KEY_UPDATE_NOT_REQUESTED] => KeyUpdateRequest::UpdateNotRequested, + _ => { + crate::error!("Received invalid KeyUpdateRequest: {:?}", payload); + + abort_and_return_error!( + self, + socket, + AlertDescription::DecodeError, + KtlsStreamError::InvalidMessage(InvalidMessage::InvalidKeyUpdate).into() + ); + } + }; + + { + let (seq, secrets) = match self.data.update_rx_secret() { + Ok(secrets) => secrets, + Err(e) => { + abort_and_return_error!( + self, + socket, + AlertDescription::InternalError, + KtlsStreamError::KeyUpdateFailed(e).into() + ); + } + }; + + if let Err(e) = + setup_tls_params_rx(socket, self.data.negotiated_cipher_suite(), (seq, secrets)) + { + abort_and_return_error!(self, socket, AlertDescription::InternalError, e.into()); + } + } + + match key_update_request { + KeyUpdateRequest::UpdateNotRequested => return Ok(()), + KeyUpdateRequest::UpdateRequested => { + let message = [ + HandshakeType::KeyUpdate.into(), // typ + 0, + 0, + 1, // length + KeyUpdateRequest::UpdateNotRequested.into(), + ]; + + if let Err(e) = + Self::try_send_tls_control_message(socket, ContentType::Handshake, &message) + { + abort_and_return_error!( + self, + socket, + AlertDescription::InternalError, + io::Error::other(format!("Failed to send KeyUpdate message: {e}")) + ); + } + + let (seq, secrets) = match self.data.update_tx_secret() { + Ok(secrets) => secrets, + Err(e) => { + abort_and_return_error!( + self, + socket, + AlertDescription::InternalError, + KtlsStreamError::KeyUpdateFailed(e).into() + ); + } + }; + + if let Err(e) = + setup_tls_params_tx(socket, self.data.negotiated_cipher_suite(), (seq, secrets)) + { + abort_and_return_error!( + self, + socket, + AlertDescription::InternalError, + e.into() + ); + } + } + _ => { + unreachable!( + "KeyUpdateRequest should only be UpdateNotRequested or UpdateRequested here" + ); + } + } + + Ok(()) + } +} + +/// [`KernelConnection`], client side or server side. +pub(crate) enum TlsConnData { + Client(KernelConnection), + Server(KernelConnection), +} + +impl fmt::Debug for TlsConnData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Client(_) => f.debug_struct("TlsConnData::Client").finish(), + Self::Server(_) => f.debug_struct("TlsConnData::Server").finish(), + } + } +} + +impl TlsConnData { + #[inline] + fn protocol_version(&self) -> ProtocolVersion { + match self { + Self::Client(data) => data.protocol_version(), + Self::Server(data) => data.protocol_version(), + } + } + + #[inline] + fn negotiated_cipher_suite(&self) -> SupportedCipherSuite { + match self { + Self::Client(conn) => conn.negotiated_cipher_suite(), + Self::Server(conn) => conn.negotiated_cipher_suite(), + } + } + + #[inline] + fn update_tx_secret(&mut self) -> Result<(u64, ConnectionTrafficSecrets), rustls::Error> { + match self { + Self::Client(conn) => conn.update_tx_secret(), + Self::Server(conn) => conn.update_tx_secret(), + } + } + + #[inline] + fn update_rx_secret(&mut self) -> Result<(u64, ConnectionTrafficSecrets), rustls::Error> { + match self { + Self::Client(conn) => conn.update_rx_secret(), + Self::Server(conn) => conn.update_rx_secret(), + } + } +} + +bitflags::bitflags! { + #[derive(Debug)] + pub(crate) struct StreamState : u8 { + /// The read side of the TLS stream is closed. + const READ_CLOSED = 0b0000_0001; + + /// The write side of the TLS stream is closed. + const WRITE_CLOSED = 0b0000_0010; + + /// The stream is closed, both read and write sides. + const CLOSED = StreamState::READ_CLOSED.bits() | StreamState::WRITE_CLOSED.bits(); + + /// Has buffered data that needs to be handled before reading from the inner stream. + const HAS_BUFFERED_DATA = 0b0001_0000; + } +} + +impl StreamState { + #[inline] + /// If the read side of the TLS stream were closed. + pub(crate) fn is_read_closed(&self) -> bool { + self.contains(Self::READ_CLOSED) + } + + #[inline] + /// Sets the read side of the TLS stream as closed. + pub(crate) fn set_read_closed(&mut self) { + self.insert(Self::READ_CLOSED); + } + + #[inline] + /// If the write side of the TLS stream were closed. + pub(crate) fn is_write_closed(&self) -> bool { + self.contains(Self::WRITE_CLOSED) + } + + // #[inline] + // /// Sets the write side of the TLS stream as closed. + // pub(crate) fn set_write_closed(&mut self) { + // self.insert(Self::WRITE_CLOSED); + // } + + #[cfg_attr(not(feature = "raw-api"), allow(unused))] + #[inline] + /// If the stream is partially closed, either read or write side. + pub(crate) fn is_partially_closed(&self) -> bool { + self.is_read_closed() || self.is_write_closed() + } + + // #[inline] + // /// If the stream is closed, both read and write sides. + // pub(crate) fn is_closed(self) -> bool { + // self.contains(Self::CLOSED) + // } + + #[inline] + /// Sets the stream as closed, both read and write sides. + pub(crate) fn set_closed(&mut self) { + self.insert(Self::CLOSED); + } + + #[inline] + /// If the stream has buffered data that needs to be handled before reading + /// from the inner stream. + pub(crate) fn has_buffered_data(&self) -> bool { + self.contains(Self::HAS_BUFFERED_DATA) + } + + #[inline] + /// Sets the stream as having buffered data that needs to be handled before + /// reading from the inner stream. + pub(crate) fn set_has_buffered_data(&mut self, val: bool) { + self.set(Self::HAS_BUFFERED_DATA, val); + } +} + +#[derive(Clone, Default)] +/// A simple buffer with a read offset. +pub struct Buffer { + /// The inner buffer data. + inner: Vec, + + /// Read offset of the buffer. + offset: usize, +} + +impl fmt::Debug for Buffer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Buffer") + .field("inner", &self.inner.len()) + .field("offset", &self.offset) + .finish() + } +} + +impl ops::Deref for Buffer { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + // TODO: Here can actually avoid panic check, since `self.offset` is always + // guaranteed to be less than or equal to `self.inner.len()`, but the MSRV + // limits so. + &self.inner[self.offset..] + } +} + +impl AsRef<[u8]> for Buffer { + fn as_ref(&self) -> &[u8] { + self + } +} + +impl Buffer { + #[inline] + /// Reads from the inner buffer into the provided buffer, and advances the + /// read offset. + /// + /// Returns the number of bytes read. + pub fn read(&mut self, buf: &mut [u8]) -> Option { + // Read zero: buffer is empty or offset is at the end + let to_read = NonZeroUsize::new(buf.len().min(self.len()))?; + + buf[..to_read.get()].copy_from_slice(&self[..to_read.get()]); + + self.offset += to_read.get(); + + Some(to_read) + } + + #[allow(clippy::must_use_candidate)] + #[inline] + /// Check if the read offset has reached the end of the inner buffer. + pub fn is_read_done(&self) -> bool { + self.offset >= self.inner.len() + } + + #[inline] + /// Resets the buffer, clearing the inner data and resetting the read + /// offset. + pub(crate) fn reset(&mut self) { + #[allow(unsafe_code)] + // SAFETY: We are setting length to 0, which is always valid. + unsafe { + self.inner.set_len(0); + } + self.offset = 0; + } +} diff --git a/ktls/src/stream/error.rs b/ktls/src/stream/error.rs new file mode 100644 index 0000000..a47b35d --- /dev/null +++ b/ktls/src/stream/error.rs @@ -0,0 +1,88 @@ +//! Error type of `KtlsStream` and related operations. + +use std::{fmt, io}; + +use rustls::{AlertDescription, InvalidMessage, PeerMisbehaved}; + +#[non_exhaustive] +#[derive(Debug)] +/// The error type for `KtlsStream` and related operations. +pub enum KtlsStreamError { + /// A corrupt message was received from the peer. + InvalidMessage(InvalidMessage), + + /// The peer misbehaved in some way. + PeerMisbehaved(PeerMisbehaved), + + /// Failed to handle a key update request. + KeyUpdateFailed(rustls::Error), + + /// Failed to handle a provided session ticket. + SessionTicketFailed(rustls::Error), + + /// The connection has been closed by the peer. + Closed, + + /// Cannot handle control messages while there is buffered data to read. + ControlMessageWithBufferedData, + + /// The connection peer closed the connection with an alert. + Alert(AlertDescription), +} + +impl fmt::Display for KtlsStreamError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidMessage(e) => { + write!(f, "Received corrupt message of type {e:?}") + } + Self::PeerMisbehaved(e) => write!(f, "Peer misbehaved: {e:?}"), + Self::KeyUpdateFailed(e) => { + write!(f, "Failed to handle a key update request: {e}") + } + Self::SessionTicketFailed(e) => { + write!(f, "Failed to handle a provided session ticket: {e}") + } + Self::Closed => write!(f, "The connection has been closed by the peer"), + Self::ControlMessageWithBufferedData => { + write!( + f, + "Cannot handle control messages while there is buffered data to read" + ) + } + Self::Alert(desc) => { + write!( + f, + "Connection peer closed the connection with an alert: {desc:?}", + ) + } + } + } +} + +impl std::error::Error for KtlsStreamError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::KeyUpdateFailed(e) | Self::SessionTicketFailed(e) => Some(e), + _ => None, + } + } +} + +impl From for KtlsStreamError { + fn from(error: InvalidMessage) -> Self { + Self::InvalidMessage(error) + } +} + +impl From for KtlsStreamError { + fn from(error: PeerMisbehaved) -> Self { + Self::PeerMisbehaved(error) + } +} + +impl From for io::Error { + fn from(value: KtlsStreamError) -> Self { + Self::other(value) + } +} diff --git a/ktls/src/stream/impl_std.rs b/ktls/src/stream/impl_std.rs new file mode 100644 index 0000000..16adba0 --- /dev/null +++ b/ktls/src/stream/impl_std.rs @@ -0,0 +1,64 @@ +//! `Read` / `Write` support for `KtlsStream`. + +use std::io::{self, Read, Write}; +use std::os::fd::AsFd; + +use crate::stream::context::handle_ret; +use crate::stream::KtlsStream; + +impl KtlsStream { + /// Shuts down both read and write sides of the TLS stream. + pub fn shutdown(&mut self) { + self.ctx.shutdown(&self.inner); + } +} + +impl Read for KtlsStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + handle_ret!(self, { + let state = self.ctx.state(); + + if state.is_read_closed() { + crate::trace!("Read closed, returning EOF"); + + // received a `close_notify` alert from the peer, return EOF. + return Ok(0); + } + + if state.has_buffered_data() { + // Unlikely path, actually. + + if let Some(has_read) = self.ctx.read_buffer(buf) { + return Ok(has_read.get()); + } + } + + self.inner.read(buf) + }) + } +} + +impl Write for KtlsStream { + fn write(&mut self, buf: &[u8]) -> io::Result { + handle_ret!(self, { + if self.ctx.state().is_write_closed() { + crate::trace!("Write closed, returning EOF"); + + return Ok(0); + } + + self.inner.write(buf) + }) + } + + fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } + + fn by_ref(&mut self) -> &mut Self + where + Self: Sized, + { + self + } +} diff --git a/ktls/src/stream/impl_tokio.rs b/ktls/src/stream/impl_tokio.rs new file mode 100644 index 0000000..c042064 --- /dev/null +++ b/ktls/src/stream/impl_tokio.rs @@ -0,0 +1,121 @@ +//! Optional: Tokio's `AsyncRead` / `AsyncWrite` support for `KtlsStream`. + +use std::os::fd::AsFd; +use std::pin::Pin; +use std::{io, ptr, task}; + +use tokio::io::{self as async_io, AsyncRead, AsyncWrite}; + +use crate::stream::context::handle_ret_async; +use crate::stream::KtlsStream; + +impl AsyncRead for KtlsStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &mut async_io::ReadBuf<'_>, + ) -> task::Poll> { + let mut this = self.project(); + + handle_ret_async!(this, { + let state = this.ctx.state(); + + if state.is_read_closed() { + // received a `close_notify` alert from the peer, return EOF. + + crate::trace!("Read closed, returning EOF"); + + return task::Poll::Ready(Ok(())); + } + + if state.has_buffered_data() { + // Unlikely path, actually. + + #[allow(unsafe_code)] + #[allow(trivial_casts)] + // Safety: will set the initialized part after reading. + if let Some(has_read) = this + .ctx + .read_buffer(unsafe { &mut *(ptr::from_mut(buf.unfilled_mut()) as *mut [u8]) }) + { + #[allow(unsafe_code)] + // Safety: has filled and written `has_read` bytes. + unsafe { + buf.assume_init(has_read.get()); + }; + buf.advance(has_read.get()); + + return task::Poll::Ready(Ok(())); + } + } + + this.inner.as_mut().poll_read(cx, buf) + }) + } +} + +impl AsyncWrite for KtlsStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &[u8], + ) -> task::Poll> { + let mut this = self.project(); + + handle_ret_async!(this, { + if this.ctx.state().is_write_closed() { + crate::trace!("Write closed, returning EOF"); + + return task::Poll::Ready(Ok(0)); + } + + this.inner.as_mut().poll_write(cx, buf) + }) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll> { + let mut this = self.project(); + + handle_ret_async!(this, { + if this.ctx.state().is_write_closed() { + return task::Poll::Ready(Ok(())); + } + + this.inner.as_mut().poll_flush(cx) + }) + } + + /// Shuts down both read and write sides of the TLS stream. + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + let this = self.project(); + + this.ctx.shutdown(&*this.inner); + + this.inner.poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> task::Poll> { + let mut this = self.project(); + + handle_ret_async!(this, { + if this.ctx.state().is_write_closed() { + crate::trace!("Write closed, returning EOF"); + + return task::Poll::Ready(Ok(0)); + } + + this.inner.as_mut().poll_write_vectored(cx, bufs) + }) + } + + fn is_write_vectored(&self) -> bool { + self.inner.is_write_vectored() + } +} diff --git a/ktls/src/utils.rs b/ktls/src/utils.rs new file mode 100644 index 0000000..0228ca8 --- /dev/null +++ b/ktls/src/utils.rs @@ -0,0 +1,7 @@ +//! Utilities + +#[cfg(feature = "probe-ktls-compatibility")] +mod suites; + +#[cfg(feature = "probe-ktls-compatibility")] +pub use suites::CompatibleCipherSuites; diff --git a/ktls/src/utils/suites.rs b/ktls/src/utils/suites.rs new file mode 100644 index 0000000..899d6ef --- /dev/null +++ b/ktls/src/utils/suites.rs @@ -0,0 +1,193 @@ +//! See [`CompatibleCipherSuites`] + +use std::collections::HashSet; +use std::io; +use std::net::{TcpListener, TcpStream}; + +use rustls::{CipherSuite, SupportedCipherSuite, SupportedProtocolVersion}; + +use crate::setup::tls::TlsCryptoInfoTx; +use crate::setup::ulp::{setup_ulp, SetupError}; + +#[allow(clippy::module_name_repetitions)] +#[derive(Debug, Clone)] +/// A collection of compatible cipher suites for current kernel. +pub struct CompatibleCipherSuites { + suites: HashSet, + + /// The supported protocol versions. + pub protocol_versions: &'static [&'static SupportedProtocolVersion], +} + +impl CompatibleCipherSuites { + /// Probes the current Linux kernel for kTLS cipher suites compatibility. + /// + /// Returns `None` if the kernel does not support kTLS. + /// + /// # Notes + /// + /// - The caller may enable feature `rustls/tls12` to include TLS 1.2 + /// support, or the protocol versions may be empty if only TLS 1.2 is + /// supported by current Linux kernel. + /// - The caller may cache the result, as probing is expensive. + /// + /// ## Errors + /// + /// [`io::Error`]. + pub fn probe() -> io::Result> { + let listener = TcpListener::bind("127.0.0.1:0")?; + + let local_addr = listener.local_addr()?; + + let mut inner = HashSet::new(); + + let mut tls12_supported = false; + let mut tls13_supported = false; + + macro_rules! test_param { + ($method:ident, $data:ident, $version:expr, $cipher_type:expr) => {{ + let stream = match setup_ulp(TcpStream::connect(local_addr)?) { + Ok(stream) => stream, + Err(SetupError { + socket: Some(_), .. + }) => { + // kTLS is not supported + return Ok(None); + } + Err(SetupError { error, .. }) => { + return Err(error); + } + }; + + #[allow(unsafe_code)] + // SAFETY: zeroed is fine for libc structs as we will set all the fields + let mut data: libc::$data = unsafe { std::mem::zeroed() }; + + data.info = libc::tls_crypto_info { + version: $version, + cipher_type: $cipher_type, + }; + + TlsCryptoInfoTx::$method(data).set(&stream).is_ok() + }}; + } + + // Test TLS 1.2, AES-GCM-128 + if test_param!( + custom_aes_128_gcm, + tls12_crypto_info_aes_gcm_128, + libc::TLS_1_2_VERSION, + libc::TLS_CIPHER_AES_GCM_128 + ) { + inner.insert(CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.into()); + inner.insert(CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256.into()); + + tls12_supported = true; + } + + // Test TLS 1.2, AES-GCM-256 + if test_param!( + custom_aes_256_gcm, + tls12_crypto_info_aes_gcm_256, + libc::TLS_1_2_VERSION, + libc::TLS_CIPHER_AES_GCM_256 + ) { + inner.insert(CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384.into()); + inner.insert(CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384.into()); + + tls12_supported = true; + } + + // Test TLS 1.2, ChaCha20-Poly1305 + if test_param!( + custom_chacha20_poly1305, + tls12_crypto_info_chacha20_poly1305, + libc::TLS_1_2_VERSION, + libc::TLS_CIPHER_CHACHA20_POLY1305 + ) { + inner.insert(CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256.into()); + inner.insert(CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256.into()); + + tls12_supported = true; + } + + // Test TLS 1.3, AES-GCM-128 + if test_param!( + custom_aes_128_gcm, + tls12_crypto_info_aes_gcm_128, + libc::TLS_1_3_VERSION, + libc::TLS_CIPHER_AES_GCM_128 + ) { + inner.insert(CipherSuite::TLS13_AES_128_GCM_SHA256.into()); + + tls13_supported = true; + } + + // Test TLS 1.3, AES-GCM-256 + if test_param!( + custom_aes_256_gcm, + tls12_crypto_info_aes_gcm_256, + libc::TLS_1_3_VERSION, + libc::TLS_CIPHER_AES_GCM_256 + ) { + inner.insert(CipherSuite::TLS13_AES_256_GCM_SHA384.into()); + + tls13_supported = true; + } + + // Test TLS 1.3, ChaCha20-Poly1305 + if test_param!( + custom_chacha20_poly1305, + tls12_crypto_info_chacha20_poly1305, + libc::TLS_1_3_VERSION, + libc::TLS_CIPHER_CHACHA20_POLY1305 + ) { + inner.insert(CipherSuite::TLS13_CHACHA20_POLY1305_SHA256.into()); + + tls13_supported = true; + } + + Ok(Some(Self { + suites: inner, + protocol_versions: match (tls12_supported, tls13_supported) { + (true, true) => rustls::DEFAULT_VERSIONS, + // The first element is TLS 1.3 + (false, true) => &rustls::DEFAULT_VERSIONS[..1], + // The first element is TLS 1.2 (maybe, but empty slice is OK, let the caller handle + // it) + (true, false) => &rustls::DEFAULT_VERSIONS[1..], + // No supported versions + (false, false) => return Ok(None), + }, + })) + } + + /// Filters the provided cipher suites list in place, removing suites + /// which is incompatible. + /// + /// ## Examples + /// + /// ```no_run + /// use std::sync::Arc; + /// + /// use ktls_util::suites::CompatibleCipherSuites; + /// use rustls::crypto::CryptoProvider; + /// + /// // Get a crypto provider, for example, the default ring provider: + /// let mut crypto_provider = rustls::crypto::ring::default_provider(); + /// + /// // Filter it: + /// let compatible_ciphers: &CompatibleCipherSuites = ...; + /// compatible_ciphers.filter(&mut crypto_provider.cipher_suites); + /// + /// // Create client/server configuration with`builder_with_provider`, for example: + /// let root_store = ...; + /// let config = rustls::ClientConfig::builder_with_provider(Arc::new(crypto_provider)) + /// .with_protocol_versions(compatible_ciphers.protocol_versions)? + /// .with_root_certificates(root_store) + /// .with_no_client_auth(); + /// ``` + pub fn filter(&self, suite: &mut Vec) { + suite.retain(|s| self.suites.contains(&s.suite().into())); + } +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..cc7a638 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,21 @@ +edition = "2021" +tab_spaces = 4 +unstable_features = true + +# imports +group_imports = "StdExternalCrate" +imports_granularity = "Module" +reorder_imports = true + +# comments +format_code_in_doc_comments = true +normalize_comments = true +wrap_comments = true + +# others +condense_wildcard_suffixes = true +format_strings = true +format_macro_matchers = true +merge_derives = false +reorder_impl_items = true +use_field_init_shorthand = true diff --git a/tests/integration_test.rs b/tests/integration_test.rs deleted file mode 100644 index c20c39d..0000000 --- a/tests/integration_test.rs +++ /dev/null @@ -1,554 +0,0 @@ -use std::{ - io, - os::fd::{AsRawFd, RawFd}, - sync::Arc, - task, - time::Duration, -}; - -use ktls::{AsyncReadReady, CorkStream, KtlsCipherSuite, KtlsCipherType, KtlsVersion}; -use lazy_static::lazy_static; -use rcgen::generate_simple_self_signed; -use rustls::{ - client::Resumption, crypto::CryptoProvider, ClientConfig, RootCertStore, ServerConfig, - SupportedCipherSuite, -}; - -#[cfg(feature = "aws_lc_rs")] -use rustls::crypto::aws_lc_rs::cipher_suite; -#[cfg(feature = "ring")] -use rustls::crypto::ring::cipher_suite; - -use tokio::{ - io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, - net::{TcpListener, TcpStream}, -}; -use tokio_rustls::TlsConnector; -use tracing::{debug, Instrument}; -use tracing_subscriber::EnvFilter; - -const RANDOM_SEED: u128 = 19873239487139847918274_u128; - -struct Payloads { - client: Vec, - server: Vec, -} - -impl Default for Payloads { - fn default() -> Self { - let mut prng = oorandom::Rand64::new(RANDOM_SEED); - let payload_len = 262_144; - let mut gen_payload = || { - (0..payload_len) - .map(|_| (prng.rand_u64() % 256) as u8) - .collect() - }; - - Self { - client: gen_payload(), - server: gen_payload(), - } - } -} - -lazy_static! { - static ref PAYLOADS: Payloads = Payloads::default(); -} - -fn all_suites() -> Vec { - vec![ - cipher_suite::TLS13_AES_128_GCM_SHA256, - cipher_suite::TLS13_AES_256_GCM_SHA384, - cipher_suite::TLS13_CHACHA20_POLY1305_SHA256, - #[cfg(feature = "tls12")] - cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - #[cfg(feature = "tls12")] - cipher_suite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - #[cfg(feature = "tls12")] - cipher_suite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - ] -} - -#[tokio::test] -async fn compatible_ciphers() { - let cc = ktls::CompatibleCiphers::new().await.unwrap(); - for suite in all_suites() { - assert!(cc.is_compatible(suite)); - } -} - -#[tokio::test(flavor = "current_thread")] -async fn compatible_ciphers_single_thread() { - let cc = ktls::CompatibleCiphers::new().await.unwrap(); - for suite in all_suites() { - assert!(cc.is_compatible(suite)); - } -} - -#[derive(Clone, Copy)] -enum ServerTestFlavor { - ClientCloses, - ServerCloses, -} - -#[test_case::test_matrix( - [ - KtlsVersion::TLS12, - KtlsVersion::TLS13, - ], - [ - KtlsCipherType::AesGcm128, - KtlsCipherType::AesGcm256, - KtlsCipherType::Chacha20Poly1305, - ], - [ - ServerTestFlavor::ClientCloses, - ServerTestFlavor::ServerCloses, - ] -)] -#[tokio::test] -async fn server_tests(version: KtlsVersion, cipher_type: KtlsCipherType, flavor: ServerTestFlavor) { - if matches!(version, KtlsVersion::TLS12) && !cfg!(feature = "tls12") { - println!("Skipping..."); - return; - } - - let cipher_suite = KtlsCipherSuite { - version, - typ: cipher_type, - }; - - server_test_inner(cipher_suite, flavor).await -} - -async fn server_test_inner(cipher_suite: KtlsCipherSuite, flavor: ServerTestFlavor) { - tracing_subscriber::fmt() - // .with_env_filter(EnvFilter::new("rustls=trace,debug")) - // .with_env_filter(EnvFilter::new("debug")) - .with_env_filter(EnvFilter::new("trace")) - .pretty() - .init(); - - let subject_alt_names = vec!["localhost".to_string()]; - - let ckey = generate_simple_self_signed(subject_alt_names).unwrap(); - - let mut server_config = - ServerConfig::builder_with_provider(single_suite_provider(cipher_suite)) - .with_protocol_versions(&[cipher_suite.version.as_supported_version()]) - .unwrap() - .with_no_client_auth() - .with_single_cert( - vec![ckey.cert.der().clone()], - rustls::pki_types::PrivatePkcs8KeyDer::from(ckey.key_pair.serialize_der()).into(), - ) - .unwrap(); - - server_config.enable_secret_extraction = true; - server_config.key_log = Arc::new(rustls::KeyLogFile::new()); - - let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config)); - let ln = TcpListener::bind("[::]:0").await.unwrap(); - let addr = ln.local_addr().unwrap(); - - let jh = tokio::spawn( - async move { - let (stream, addr) = ln.accept().await.unwrap(); - debug!("Accepted TCP conn from {}", addr); - let stream = SpyStream(stream, "server"); - let stream = CorkStream::new(stream); - - let stream = acceptor.accept(stream).await.unwrap(); - debug!("Completed TLS handshake"); - - // sleep for a bit to let client write more data and stress test - // the draining logic - tokio::time::sleep(Duration::from_millis(100)).await; - - let mut stream = ktls::config_ktls_server(stream).await.unwrap(); - debug!("Configured kTLS"); - - debug!("Server reading data (1/5)"); - let mut buf = vec![0u8; PAYLOADS.client.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.client); - - debug!("Server writing data (2/5)"); - stream.write_all(&PAYLOADS.server).await.unwrap(); - stream.flush().await.unwrap(); - - debug!("Server reading data (3/5)"); - let mut buf = vec![0u8; PAYLOADS.client.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.client); - - debug!("Server writing data (4/5)"); - stream.write_all(&PAYLOADS.server).await.unwrap(); - stream.flush().await.unwrap(); - - match flavor { - ServerTestFlavor::ClientCloses => { - debug!("Server reading from closed session (5/5)"); - assert!( - stream.read_exact(&mut buf[..1]).await.is_err(), - "Session still open?" - ); - } - ServerTestFlavor::ServerCloses => { - debug!("Server sending close notify (5/5)"); - stream.shutdown().await.unwrap(); - - debug!("Server trying to write after closing"); - stream.write_all(&PAYLOADS.server).await.unwrap_err(); - } - } - - assert_eq!(stream.get_ref().1, "server"); - assert_eq!(stream.get_mut().1, "server"); - assert_eq!(stream.into_raw().1 .1, "server"); - } - .instrument(tracing::info_span!("server")), - ); - - let mut root_store = RootCertStore::empty(); - root_store.add(ckey.cert.der().clone()).unwrap(); - - let client_config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let tls_connector = TlsConnector::from(Arc::new(client_config)); - - let stream = TcpStream::connect(addr).await.unwrap(); - let mut stream = tls_connector - .connect("localhost".try_into().unwrap(), stream) - .await - .unwrap(); - - debug!("Client writing data (1/5)"); - stream.write_all(&PAYLOADS.client).await.unwrap(); - debug!("Flushing"); - stream.flush().await.unwrap(); - - debug!("Client reading data (2/5)"); - let mut buf = vec![0u8; PAYLOADS.server.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.server); - - debug!("Client writing data (3/5)"); - stream.write_all(&PAYLOADS.client).await.unwrap(); - debug!("Flushing"); - stream.flush().await.unwrap(); - - debug!("Client reading data (4/5)"); - let mut buf = vec![0u8; PAYLOADS.server.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.server); - - match flavor { - ServerTestFlavor::ClientCloses => { - debug!("Client sending close notify (5/5)"); - stream.shutdown().await.unwrap(); - - debug!("Client trying to write after closing"); - stream.write_all(&PAYLOADS.client).await.unwrap_err(); - } - ServerTestFlavor::ServerCloses => { - debug!("Client reading from closed session (5/5)"); - assert!( - stream.read_exact(&mut buf[..1]).await.is_err(), - "Session still open?" - ); - } - } - - jh.await.unwrap(); -} - -#[test_case::test_matrix( - [ - KtlsVersion::TLS12, - KtlsVersion::TLS13, - ], - [ - KtlsCipherType::AesGcm128, - KtlsCipherType::AesGcm256, - KtlsCipherType::Chacha20Poly1305, - ], - [ - ClientTestFlavor::ShortLastBuffer, - ClientTestFlavor::LongLastBuffer, - ] -)] -#[tokio::test] -async fn client_tests(version: KtlsVersion, cipher_type: KtlsCipherType, flavor: ClientTestFlavor) { - if matches!(version, KtlsVersion::TLS12) && !cfg!(feature = "tls12") { - println!("Skipping..."); - return; - } - - let cipher_suite = KtlsCipherSuite { - version, - typ: cipher_type, - }; - - client_test_inner(cipher_suite, flavor).await -} - -enum ClientTestFlavor { - ShortLastBuffer, - LongLastBuffer, -} - -async fn client_test_inner(cipher_suite: KtlsCipherSuite, flavor: ClientTestFlavor) { - tracing_subscriber::fmt() - // .with_env_filter(EnvFilter::new("rustls=trace,debug")) - // .with_env_filter(EnvFilter::new("debug")) - .with_env_filter(EnvFilter::new("trace")) - .pretty() - .init(); - - let subject_alt_names = vec!["localhost".to_string()]; - - let ckey = generate_simple_self_signed(subject_alt_names).unwrap(); - - let mut server_config = - ServerConfig::builder_with_provider(single_suite_provider(cipher_suite)) - .with_protocol_versions(&[cipher_suite.version.as_supported_version()]) - .unwrap() - .with_no_client_auth() - .with_single_cert( - vec![ckey.cert.der().clone()], - rustls::pki_types::PrivatePkcs8KeyDer::from(ckey.key_pair.serialize_der()).into(), - ) - .unwrap(); - - server_config.key_log = Arc::new(rustls::KeyLogFile::new()); - // server_config.send_tls13_tickets = 0; - - let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(server_config)); - let ln = TcpListener::bind("[::]:0").await.unwrap(); - let addr = ln.local_addr().unwrap(); - - let jh = tokio::spawn( - async move { - let (stream, addr) = ln.accept().await.unwrap(); - - debug!("Accepted TCP conn from {}", addr); - let mut stream = acceptor.accept(stream).await.unwrap(); - debug!("Completed TLS handshake"); - - debug!("Server reading data (1/5)"); - let mut buf = vec![0u8; PAYLOADS.client.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.client); - - debug!("Server writing data (2/5)"); - stream.write_all(&PAYLOADS.server).await.unwrap(); - - debug!("Server reading data (3/5)"); - let mut buf = vec![0u8; PAYLOADS.client.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.client); - - for _i in 0..3 { - debug!("Making the client wait (to make busywaits REALLY obvious)"); - tokio::time::sleep(Duration::from_millis(250)).await; - } - - debug!("Server writing data (4/5)"); - stream.write_all(&PAYLOADS.server).await.unwrap(); - - debug!("Server sending close notify (5/5)"); - stream.shutdown().await.unwrap(); - - debug!("Server trying to write after close notify"); - stream.write_all(&PAYLOADS.server).await.unwrap_err(); - - debug!("Server is happy with the exchange"); - } - .instrument(tracing::info_span!("server")), - ); - - let mut root_store = RootCertStore::empty(); - root_store.add(ckey.cert.der().clone()).unwrap(); - - let mut client_config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_no_client_auth(); - - client_config.enable_secret_extraction = true; - client_config.resumption = Resumption::disabled(); - - let tls_connector = TlsConnector::from(Arc::new(client_config)); - - let stream = TcpStream::connect(addr).await.unwrap(); - let stream = CorkStream::new(stream); - - let stream = tls_connector - .connect("localhost".try_into().unwrap(), stream) - .await - .unwrap(); - - let stream = ktls::config_ktls_client(stream).await.unwrap(); - let mut stream = SpyStream(stream, "client"); - - debug!("Client writing data (1/5)"); - stream.write_all(&PAYLOADS.client).await.unwrap(); - debug!("Flushing"); - stream.flush().await.unwrap(); - - tokio::time::sleep(Duration::from_millis(250)).await; - - debug!("Client reading data (2/5)"); - let mut buf = vec![0u8; PAYLOADS.server.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.server); - - debug!("Client writing data (3/5)"); - stream.write_all(&PAYLOADS.client).await.unwrap(); - debug!("Flushing"); - stream.flush().await.unwrap(); - - debug!("Client reading data (4/5)"); - let mut buf = vec![0u8; PAYLOADS.server.len()]; - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(buf, PAYLOADS.server); - - let buf = match flavor { - ClientTestFlavor::ShortLastBuffer => &mut buf[..1], - ClientTestFlavor::LongLastBuffer => &mut buf[..2], - }; - debug!( - "Client reading from closed session (with buffer of size {})", - buf.len() - ); - assert!(stream.read_exact(buf).await.is_err(), "Session still open?"); - - jh.await.unwrap(); -} - -struct SpyStream(IO, &'static str); - -impl AsyncRead for SpyStream -where - IO: AsyncRead, -{ - fn poll_read( - self: std::pin::Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &mut tokio::io::ReadBuf<'_>, - ) -> task::Poll> { - let old_filled = buf.filled().len(); - let name = self.1; - let res = unsafe { - let io = self.map_unchecked_mut(|s| &mut s.0); - io.poll_read(cx, buf) - }; - - match &res { - task::Poll::Ready(res) => match res { - Ok(_) => { - let num_read = buf.filled().len() - old_filled; - tracing::debug!(%name, "SpyStream read {num_read} bytes",); - } - Err(e) => { - tracing::debug!(%name, "SpyStream read errored: {e}"); - } - }, - task::Poll::Pending => { - tracing::debug!(%name, "SpyStream read would've blocked") - } - } - res - } -} - -impl AsyncReadReady for SpyStream -where - IO: AsyncReadReady, -{ - fn poll_read_ready(&self, cx: &mut task::Context<'_>) -> task::Poll> { - self.0.poll_read_ready(cx) - } -} - -impl AsyncWrite for SpyStream -where - IO: AsyncWrite, -{ - fn poll_write( - self: std::pin::Pin<&mut Self>, - cx: &mut task::Context<'_>, - buf: &[u8], - ) -> task::Poll> { - let res = unsafe { - let io = self.map_unchecked_mut(|s| &mut s.0); - io.poll_write(cx, buf) - }; - - match &res { - task::Poll::Ready(res) => match res { - Ok(n) => { - tracing::debug!("SpyStream wrote {n} bytes"); - } - Err(e) => { - tracing::debug!("SpyStream writing errored: {e}"); - } - }, - task::Poll::Pending => { - tracing::debug!("SpyStream writing would've blocked") - } - } - res - } - - fn poll_flush( - self: std::pin::Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll> { - unsafe { - let io = self.map_unchecked_mut(|s| &mut s.0); - io.poll_flush(cx) - } - } - - fn poll_shutdown( - self: std::pin::Pin<&mut Self>, - cx: &mut task::Context<'_>, - ) -> task::Poll> { - unsafe { - let io = self.map_unchecked_mut(|s| &mut s.0); - io.poll_shutdown(cx) - } - } -} - -impl AsRawFd for SpyStream -where - IO: AsRawFd, -{ - fn as_raw_fd(&self) -> RawFd { - self.0.as_raw_fd() - } -} - -fn single_suite_provider(cipher_suite: KtlsCipherSuite) -> Arc { - let mut provider = { - #[cfg(feature = "aws_lc_rs")] - { - rustls::crypto::aws_lc_rs::default_provider() - } - - #[cfg(feature = "ring")] - { - rustls::crypto::ring::default_provider() - } - }; - provider.cipher_suites.clear(); - provider - .cipher_suites - .push(cipher_suite.as_supported_cipher_suite()); - - Arc::new(provider) -}