From a564dccd67af2c046462758949f3bb38318947cc Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Wed, 21 Aug 2024 14:06:03 +0800 Subject: [PATCH] init: all --- .github/workflows/release.yml | 235 +++ .gitignore | 6 + Cargo.lock | 1489 +++++++++++++++++ Cargo.toml | 3 + LICENSE | 674 ++++++++ QUICK_START.md | 95 ++ README.md | 78 + SPEC.md | 263 +++ asport-client/Cargo.toml | 42 + asport-client/README.md | 16 + asport-client/src/config.rs | 372 ++++ asport-client/src/connection/authenticated.rs | 63 + asport-client/src/connection/handle_stream.rs | 153 ++ asport-client/src/connection/handle_task.rs | 240 +++ asport-client/src/connection/mod.rs | 421 +++++ asport-client/src/connection/udp_session.rs | 206 +++ asport-client/src/error.rs | 53 + asport-client/src/main.rs | 119 ++ asport-client/src/utils.rs | 346 ++++ asport-quinn/Cargo.toml | 19 + asport-quinn/README.md | 20 + asport-quinn/src/lib.rs | 662 ++++++++ asport-server/Cargo.toml | 44 + asport-server/README.md | 16 + asport-server/src/config.rs | 409 +++++ asport-server/src/connection/authenticated.rs | 64 + asport-server/src/connection/handle_bind.rs | 148 ++ asport-server/src/connection/handle_stream.rs | 153 ++ asport-server/src/connection/handle_task.rs | 250 +++ asport-server/src/connection/mod.rs | 284 ++++ asport-server/src/connection/udp_sessions.rs | 138 ++ asport-server/src/error.rs | 59 + asport-server/src/main.rs | 116 ++ asport-server/src/server.rs | 151 ++ asport-server/src/utils.rs | 288 ++++ asport/Cargo.toml | 30 + asport/README.md | 28 + asport/src/lib.rs | 35 + asport/src/marshal.rs | 108 ++ asport/src/model/client_hello.rs | 130 ++ asport/src/model/connect.rs | 75 + asport/src/model/dissociate.rs | 69 + asport/src/model/heartbeat.rs | 57 + asport/src/model/mod.rs | 532 ++++++ asport/src/model/packet.rs | 278 +++ asport/src/model/server_hello.rs | 70 + asport/src/protocol/client_hello.rs | 137 ++ asport/src/protocol/connect.rs | 47 + asport/src/protocol/dissociate.rs | 48 + asport/src/protocol/heartbeat.rs | 31 + asport/src/protocol/mod.rs | 165 ++ asport/src/protocol/packet.rs | 105 ++ asport/src/protocol/server_hello.rs | 65 + asport/src/unmarshal.rs | 315 ++++ client.example.toml | 146 ++ client.quick.example.toml | 70 + release/systemd/system/asport-client.service | 18 + release/systemd/system/asport-client@.service | 18 + release/systemd/system/asport-server.service | 18 + release/systemd/system/asport-server@.service | 18 + server.example.toml | 117 ++ server.quick.example.toml | 58 + 62 files changed, 10483 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 QUICK_START.md create mode 100644 README.md create mode 100644 SPEC.md create mode 100644 asport-client/Cargo.toml create mode 100644 asport-client/README.md create mode 100644 asport-client/src/config.rs create mode 100644 asport-client/src/connection/authenticated.rs create mode 100644 asport-client/src/connection/handle_stream.rs create mode 100644 asport-client/src/connection/handle_task.rs create mode 100644 asport-client/src/connection/mod.rs create mode 100644 asport-client/src/connection/udp_session.rs create mode 100644 asport-client/src/error.rs create mode 100644 asport-client/src/main.rs create mode 100644 asport-client/src/utils.rs create mode 100644 asport-quinn/Cargo.toml create mode 100644 asport-quinn/README.md create mode 100644 asport-quinn/src/lib.rs create mode 100644 asport-server/Cargo.toml create mode 100644 asport-server/README.md create mode 100644 asport-server/src/config.rs create mode 100644 asport-server/src/connection/authenticated.rs create mode 100644 asport-server/src/connection/handle_bind.rs create mode 100644 asport-server/src/connection/handle_stream.rs create mode 100644 asport-server/src/connection/handle_task.rs create mode 100644 asport-server/src/connection/mod.rs create mode 100644 asport-server/src/connection/udp_sessions.rs create mode 100644 asport-server/src/error.rs create mode 100644 asport-server/src/main.rs create mode 100644 asport-server/src/server.rs create mode 100644 asport-server/src/utils.rs create mode 100644 asport/Cargo.toml create mode 100644 asport/README.md create mode 100644 asport/src/lib.rs create mode 100644 asport/src/marshal.rs create mode 100644 asport/src/model/client_hello.rs create mode 100644 asport/src/model/connect.rs create mode 100644 asport/src/model/dissociate.rs create mode 100644 asport/src/model/heartbeat.rs create mode 100644 asport/src/model/mod.rs create mode 100644 asport/src/model/packet.rs create mode 100644 asport/src/model/server_hello.rs create mode 100644 asport/src/protocol/client_hello.rs create mode 100644 asport/src/protocol/connect.rs create mode 100644 asport/src/protocol/dissociate.rs create mode 100644 asport/src/protocol/heartbeat.rs create mode 100644 asport/src/protocol/mod.rs create mode 100644 asport/src/protocol/packet.rs create mode 100644 asport/src/protocol/server_hello.rs create mode 100644 asport/src/unmarshal.rs create mode 100644 client.example.toml create mode 100644 client.quick.example.toml create mode 100644 release/systemd/system/asport-client.service create mode 100644 release/systemd/system/asport-client@.service create mode 100644 release/systemd/system/asport-server.service create mode 100644 release/systemd/system/asport-server@.service create mode 100644 server.example.toml create mode 100644 server.quick.example.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..562f9d5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,235 @@ +name: Release +on: + workflow_dispatch: + release: + types: [prereleased] + push: + branches: + - main + - master + - dev* + - feat* + - fix* + - chore* + - test* + paths: + - "**/*.rs" + - "**/Cargo.toml" + - "release/" + - "Cargo.lock" + - ".github/workflows/release.yml" + pull_request: + types: [ opened, synchronize, reopened, ready_for_review ] + paths: + - "**/*.rs" + - "**/Cargo.toml" + - "release/" + - "Cargo.lock" + - ".github/workflows/release.yml" + +jobs: + release: + strategy: + fail-fast: false + matrix: + include: + - arch-name: x86_64-unknown-linux-gnu + os: ubuntu-latest + target: x86_64-unknown-linux-gnu + cross: true + file-ext: + platform: linux + + - arch-name: x86_64-unknown-linux-musl + os: ubuntu-latest + target: x86_64-unknown-linux-musl + cross: true + file-ext: + platform: linux + + - arch-name: x86_64-unknown-freebsd + os: ubuntu-latest + target: x86_64-unknown-freebsd + cross: true + file-ext: + platform: freebsd + + - arch-name: x86_64-pc-windows-msvc + os: windows-latest + target: x86_64-pc-windows-msvc + cross: false + file-ext: .exe + platform: windows + + - arch-name: x86_64-pc-windows-gnu + os: ubuntu-latest + target: x86_64-pc-windows-gnu + cross: true + file-ext: .exe + platform: windows + + - arch-name: x86_64-apple-darwin + os: macos-latest + target: x86_64-apple-darwin + cross: false + file-ext: + platform: darwin + + - arch-name: i686-unknown-linux-gnu + os: ubuntu-latest + target: i686-unknown-linux-gnu + cross: true + file-ext: + platform: linux + + - arch-name: i686-unknown-linux-musl + os: ubuntu-latest + target: i686-unknown-linux-musl + cross: true + file-ext: + platform: linux + + - arch-name: i686-pc-windows-msvc + os: windows-latest + target: i686-pc-windows-msvc + cross: true + file-ext: .exe + platform: windows + + - arch-name: aarch64-unknown-linux-gnu + os: ubuntu-latest + target: aarch64-unknown-linux-gnu + cross: true + file-ext: + platform: linux + + - arch-name: aarch64-unknown-linux-musl + os: ubuntu-latest + target: aarch64-unknown-linux-musl + cross: true + file-ext: + platform: linux + + - arch-name: aarch64-pc-windows-msvc + os: windows-latest + target: aarch64-pc-windows-msvc + cross: true + file-ext: .exe + platform: windows + + - arch-name: aarch64-apple-darwin + os: macos-latest + target: aarch64-apple-darwin + cross: true + file-ext: + platform: darwin + + - arch-name: armv7-unknown-linux-gnueabi + os: ubuntu-latest + target: armv7-unknown-linux-gnueabi + cross: true + file-ext: + platform: linux + + - arch-name: armv7-unknown-linux-gnueabihf + os: ubuntu-latest + target: armv7-unknown-linux-gnueabihf + cross: true + file-ext: + platform: linux + + - arch-name: armv7-unknown-linux-musleabi + os: ubuntu-latest + target: armv7-unknown-linux-musleabi + cross: true + file-ext: + platform: linux + + - arch-name: armv7-unknown-linux-musleabihf + os: ubuntu-latest + target: armv7-unknown-linux-musleabihf + cross: true + file-ext: + platform: linux + + - arch-name: riscv64gc-unknown-linux-gnu + os: ubuntu-latest + target: riscv64gc-unknown-linux-gnu + cross: true + file-ext: + platform: linux + + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + override: true + + - name: Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.cross }} + command: build + args: --release --target ${{ matrix.target }} + + - name: Move binaries + run: | + mkdir artifacts/ + mv target/${{ matrix.target }}/release/asport-client${{ matrix.file-ext }} artifacts/ + mv target/${{ matrix.target }}/release/asport-server${{ matrix.file-ext }} artifacts/ + + - name: Prepare package + run: cp *.example.toml artifacts/ + + - name: Add systemd service for Linux + if: matrix.platform == 'linux' + run: cp -r release/systemd artifacts/ + + - name: Archive artifacts + id: archive + shell: bash + run: | + pushd artifacts + if ${{ matrix.platform == 'windows' }}; then + 7z a ../asport-${{ matrix.arch-name }}.zip . + FILE=asport-${{ matrix.arch-name }}.zip + else + tar -cJf ../asport-${{ matrix.arch-name }}.tar.xz . + FILE=asport-${{ matrix.arch-name }}.tar.xz + fi + popd + + echo "FILE=$FILE" >> $GITHUB_OUTPUT + + - name: Calculate digest + shell: bash + run: | + DGST=${{ steps.archive.outputs.FILE }}.dgst + openssl dgst -md5 $FILE | sed 's/([^)]*)//g' >>$DGST + openssl dgst -sha1 $FILE | sed 's/([^)]*)//g' >>$DGST + openssl dgst -sha256 $FILE | sed 's/([^)]*)//g' >>$DGST + openssl dgst -sha512 $FILE | sed 's/([^)]*)//g' >>$DGST + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: asport-${{ matrix.arch-name }} + path: asport-${{ matrix.arch-name }}.* + + - name: Upload to GitHub release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: | + asport-${{ matrix.arch-name }}.* + file_glob: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d397cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target/ + +.idea +.vscode + +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3d35825 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1489 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "asport" +version = "0.1.0" +dependencies = [ + "asport", + "bytes", + "futures-util", + "parking_lot", + "register-count", + "thiserror", + "uuid", +] + +[[package]] +name = "asport-client" +version = "0.1.0" +dependencies = [ + "asport", + "asport-quinn", + "bytes", + "clap", + "config", + "crossbeam-utils", + "env_logger", + "humantime", + "log", + "once_cell", + "parking_lot", + "ppp", + "quinn", + "register-count", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "socket2", + "thiserror", + "tokio", + "tokio-util", + "uuid", + "xdg", +] + +[[package]] +name = "asport-quinn" +version = "0.1.0" +dependencies = [ + "asport", + "bytes", + "futures-util", + "quinn", + "thiserror", + "uuid", +] + +[[package]] +name = "asport-server" +version = "0.1.0" +dependencies = [ + "asport", + "asport-quinn", + "bimap", + "bytes", + "clap", + "config", + "crossbeam-utils", + "env_logger", + "humantime", + "log", + "parking_lot", + "quinn", + "register-count", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "socket2", + "sysctl", + "thiserror", + "tokio", + "tokio-util", + "uuid", + "xdg", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "enum-as-inner" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.157" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374af5f94e54fa97cf75e945cce8a6b201e88a1a07e688b47dfd2a59c66dbd86" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[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.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[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 = "object" +version = "0.36.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +dependencies = [ + "parking_lot_core", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pest" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "ppp" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d901d7dd743c478e14af9518bdbc33e53e50be56429233f812537f29dbf0d1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +dependencies = [ + "bytes", + "futures-io", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "register-count" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6d8b2af7d3e6675306d6757f10b4cf0b218a9fa6a0b44d668f2132684ae4893" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags", + "serde", + "serde_derive", +] + +[[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 = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.208" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysctl" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" +dependencies = [ + "bitflags", + "byteorder", + "enum-as-inner", + "libc", + "thiserror", + "walkdir", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.39.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "xdg" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ffc96d9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = [ "asport", "asport-client", "asport-quinn","asport-server" ] +resolver = "2" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e72bfdd --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..de5ed51 --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,95 @@ +# Quick Start + +## Installation + +### Cargo + +#### Pre-requisite + +You need to have [Rust](https://www.rust-lang.org/tools/install) installed on your system. + +#### Install + +```bash +# Install Server +cargo install asport-server + +# Install Client +cargo install asport-client +``` + +### Package Manager + +Not available yet. If you can help to package this projects, I would be grateful. + +### Manual + +#### Download + +Download the latest release from the [release page](https://github.com/AkinoKaede/asport/releases). + +Here is the suggested target for common platforms: + +| Operating System | Architecture | Target | +|-----------------------------------------------------|-----------------------------|----------------------------| +| Linux (most distros) | x86_64 (aka. x86-64, amd64) | x86_64-unknown-linux-gnu | +| Linux (Alpine Linux, OpenWrt, and some old distros) | x86_64 | x86_64-unknown-linux-musl | +| Linux (most distros) | aarch64 | aarch64-unknown-linux-gnu | +| Linux (Alpine Linux, OpenWrt, and some old distros) | aarch64 | aarch64-unknown-linux-musl | +| macOS | x86_64 (Intel) | x86_64-apple-darwin | +| macOS | aarch64 (Apple Silicon) | aarch64-apple-darwin | +| Windows | x86_64 | x86_64-pc-windows-msvc | +| Windows | aarch64 | aarch64-pc-windows-msvc | + +Your target may vary, please check +the [Rust Platform Support](https://doc.rust-lang.org/nightly/rustc/platform-support.html) for more information. + +### Extract + +On Linux and macOS, you can extract the tarball with the following command: + +```bash +tar -xvf asport-*.tar.xz +``` + +On Windows, you can extract the zip file with the following command: + +```powershell +tar -xvf asport-*.zip +``` + +You can also use a graphical tool to extract the archive, such as built-in File Explorer on Windows, Finder on +macOS, or any third-party tools like 7-Zip. + +### Install + +After extracting the archive on Linux and macOS, you can install the binaries to your system with the following command: + +```bash +cp asport-server /usr/local/bin +cp asport-client /usr/local/bin +``` + +## Configuration + +### Server + +You can copy the quick start configuration from the [client.quick.example.toml](./client.quick.example.toml). + +If you want to learn more about the configuration, please refer to the [client.example.toml](./client.example.toml). + +### Client + +You can copy the quick start configuration from the [server.quick.example.toml](./server.quick.example.toml). + +If you want to learn more about the configuration, please refer to the [server.example.toml](./server.example.toml). + +## Run + +```bash +# Run Server +asport-server -c server.toml + +# Run Client +asport-client -c client.toml +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..304ce8e --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Asport + +A quick and secure reverse proxy based on QUIC for NAT traversal. + +## Introduction + +Asport is a project that aims to provide an implementation for ASPORT. ASPORT is a reverse proxy protocol that uses QUIC +as its transport layer. + +ASPORT is designed on the top of [QUIC](https://www.rfc-editor.org/rfc/rfc9000.html) protocol, which is a multiplexed, +secure, and reliable transport protocol. + +When paired with QUIC, ASPORT can achieve: + +- Fully multiplexed. All streams and datagrams are multiplexed in a single QUIC connection. +- Two UDP proxying modes: + - `native`: Having characteristics of native UDP mechanism, transferring UDP packets lossy using QUIC unreliable datagram. + - `quic`: Transferring UDP packets lossless using QUIC unidirectional streams. +- All the advantages of QUIC, including but not limited to: + - Bidirectional user-space congestion control. + - Optional 0-RTT connection handshake. + - Connection migration. + +The specification of ASPORT can be found in [SPEC.md](./SPEC.md). + +## Features + +Why should you choose Asport? + +- Secure. ASPORT uses QUIC as its transport layer, which uses TLS 1.3 for encryption. +- Low latency. ASPORT uses QUIC's stream multiplexing to reduce the latency caused by the additional handshake. +- Higher transfer speed than traditional multiplexed TCP-based proxies. Many ISP limits the speed of a single TCP connection, +but QUIC can bypass this limitation. +- Awesome UDP forwarding. Many similar projects use stream-based connection to forward UDP packets (e.g. UDP over TCP), when +loss a packet, subsequent packets will be delayed. ASPORT uses QUIC's unidirectional stream and unreliable datagram to +forward UDP packets, which can avoid this problem. +- User-space congestion control. You can use BBR on any platform, even if the platform does not support it, such as macOS. +- [PROXY protocol](https://www.haproxy.org/download/2.4/doc/proxy-protocol.txt) support in Client. +- Some simple censorship circumvention features. You can bypass some DPI and probing by setting some options in configuration. +You can bypass firewall in some companies, schools, and etc. (I don't encourage you to do this, but it's a feature.) +The design of it is based my experience in developing some anti-censorship software. + +## Quick Start + +Please refer to the [Quick Start](./QUICK_START.md) guide. + +## Project Structure + +This repository contains the following crates: + +- **[asport](./asport)** - Library. The protocol itself, protocol & model abstraction, synchronous / asynchronous marshalling. +- **[asport-quinn](./asport-quinn)** - Library. A wrapper around [quinn](https://github.com/quinn-rs/quinn) to provide functions of ASPORT. +- **[asport-server](./asport-server)** - Binary. A simple ASPORT server implementation as a reference. +- **[asport-client](./asport-client)** - Binary. A simple ASPORT client implementation as a reference. + +## Roadmap + +- [ ] Better documentation. +- [ ] Mock tests. +- [ ] [PROXY protocol](https://www.haproxy.org/download/2.4/doc/proxy-protocol.txt) for `asport-server`. + +### Long-term Goals + +- [ ] REST/RPC interface for `asport-server`. +- [ ] Web status monitor for `asport-server`. +- [ ] Web console for `asport-server`. +- [ ] Full-featured implementation of ASPORT in Go. + +## Credits + +This project is highly inspired by [TUIC](https://github.com/EAimTY/tuic). Many ideas and code snippets are borrowed from +[TUIC](https://github.com/EAimTY/tuic). Thanks to the authors and contributors of TUIC for providing such a great project. + +## License + +This repository is licensed under [GNU General Public License v3.0 or later](./LICENSE). + +SPDX-License-Identifier: [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) \ No newline at end of file diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..53c8265 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,263 @@ +# ASPORT Protocol Specification + +## Version + +`0x00` _DRAFT_ + +## Conventions +The key terms "MUST", "SHOULD" and "SHOULD NOT" in this protocol specification are to be interpreted as described +in [RFC2119](https://datatracker.ietf.org/doc/html/rfc2119). + +## Overview + +ASPORT uses QUIC as its transport layer, which is a multiplexed, secure, and reliable transport protocol. + +Any stream or unreliable datagram sent from the client and server side will sent a `Command` header before the payload. +The `Command` header contains the type of the command and the command-specific data. + +All fields are in Big Endian unless otherwise noted. + +## Command +The Definition of the `Command` header is as follows: + +```p4 +enum bit<8> CmdType { + ClientHello = 0; + ServerHello = 1; + Connect = 2; + Packet = 3; + Dissociate = 4; + Heartbeat = 5; +}; + +header command_t { + bit<8> version; + CmdType cmd_type; +} + +header_union command_body { + client_hello_h client_hello; + server_hello_h server_hello; + connect_h connect; + packet_h packet; + dissociate_h dissociate; + heartbeat_h heartbeat; +}; +``` + +`version` is the version of the ASPORT protocol. The current version is `0x00`. + +### Address + +Before we dive into the details of each command, we need to define the `Address` header. + +Address is a header that contains the address family, the address itself, and the port. It's Socks5-like, but +without the domain name and add a none type. + +```p4 +enum bit<8> AddressFamily { + Ipv4 = 1; + Ipv6 = 4; + None = 255; +}; + +header_union address_t { + bit<32> ipv4; + bit<128> ipv6; + void none; +}; + +header_union port_t { + bit<16> port; + void none; +}; + +header address_h { + AddressFamily family; + address_t address; + port_t port; +}; +``` + +### ClientHello + +- Command Type Code: `0x00` +- Transport: Unidirectional Stream +- Direction: Client -> Server + +After the QUIC handshake, the client sends a `ClientHello` command to the server. The `ClientHello` command contains +the UUID, the token, the forward mode, and the expected port range. + +The `token` is a 256-bit hash of the user's password using [TLS Keying Material Exporter](https://www.rfc-editor.org/rfc/rfc5705) on current TLS session. The server will verify the token to authenticate the user. + +The `ForwardMode` is a combination of two options: forward network and UDP forward mode. And UDP forward mode will +be defined in [`Packet`](#packet). + +The expected port range is a header that contains the start and end port of the expected port range. + +```p4 +header expected_port_range_h { + bit<16> start; + bit<16> end; +}; + +enum bit<8> ForwardMode { + Tcp = 0; + UdpNative = 1; + UdpQuic = 2; + TcpUdpNative = 3; + TcpUdpQuic = 4; +}; + +header client_hello_h { + bit<128> uuid; + bit<256> token; + ForwardMode forward_mode; + expected_port_range_h expected_port_range; +}; +``` + +### ServerHello + +- Command Type Code: `0x01` +- Transport: Unidirectional Stream +- Direction: Server -> Client + +After the server receives the `ClientHello` command, server SHOULD authenticate the user and bind the port, then send +a `ServerHello` command to the client. + +The `ServerHello` command contains the code and the body. The code is the result of the authentication and binding +process. + +If authentication is successful and the port is bound, the server MUST send `Success` and the port that is bound. +The port MUST in the expected port range. + +If UUID is not found or the token is invalid, the server can send `AuthFailed` and close the connection. For bypass +some probing, the server can also close the connection directly without sending any `ServerHello` command. + +If not any port can be bound, the server SHOULD send `BindFailed` and close the connection. + +If server not allow any port in the expected port range, the server SHOULD send `PortDenied` and close the connection. + +And if the server not allow all the network that the client want to forward, the server SHOULD send `NetworkDenied` and +close the connection. + +```p4 +enum bit<8> ServerHelloCode { + Success = 0; + AuthFailed = 1; + BindFailed = 2; + PortDenied = 3; + NetworkDenied = 4; +}; + +header_union server_hello_body { + bit<16> port; + void none; +}; + +header server_hello_h { + ServerHelloCode code; + server_hello_body body; +}; +``` + +### Connect + +- Command Type Code: `0x02` +- Transport: Bidirectional Stream +- Direction: Server -> Client + +When the server receives a TCP connection, the server SHOULD send a `Connect` command to the client on the +bidirectional stream. The `Connect` command contains the address of the source server. + +After the client receives the `Connect` command, the client SHOULD open a TCP connection to the target that the +client wants to forward. + +The client and server SHOULD forwarding data between two TCP connections and the bidirectional stream. + +```p4 +header connect_h { + address_h address; +}; +``` + +### Packet + +- Command Type Code: `0x03` +- Transport: Unreliable Datagram / Unreliable Datagram +- Direction: Client -> Server / Server -> Client + +ASPORT achieves 0-RTT UDP forwarding by syncing UDP session ID (associate ID) between the client and the server. + +Client SHOULD create a UDP session table for each QUIC connection, mapping every associate ID to an associated UDP socket. + +Server SHOULD crate a UDP session table for each QUIC connection, mapping every associate ID to a source address. + +The associate ID is a 16-bit unsigned integer generated by the server. + +When receiving a UDP packet, the server SHOULD check the source address is already associated with an associate ID. +If not, the server SHOULD allocate an associate ID for the source address and prefix the UDP packet with the `Packet` +command header then sends to the client. + +When receiving a `Packet` command, the client SHOULD check whether the associate ID is already associated with a UDP socket. +If not, the client SHOULD allocate a UDP socket for the associate ID and send the UDP packet to the target that the +client wants to forward and accept UDP packets from any destination at the same time, prefixing them with the `Packet` +command header then sends back to the server. The server should check the associate ID and the target address before +forwarding the UDP packet. If the associate ID is not found or the target address is not the same as the source address, +the server SHOULD drop the packet. + +For performance, the client can remove the UDP socket in the session table after a period of inactivity. + +`Packet` command can be send through: + +- QUIC unreliable datagram (Native Mode). +- QUIC uni-directional stream (QUIC Mode). + +In native mode, the size of prefixed UDP packet may large than MTU, so the client SHOULD fragment the UDP packet and +prefix them with the `Packet` command header. In QUIC mode, the packet SHOULD be sent in one piece. + +The server MUST send the `Packet` command in the way that the client requested in the `ClientHello` command. + +For the fragmented UDP packet, the first fragment SHOULD contain the source address or the target address, and other +fragments SHOULD use the `None` address type. + +```p4 +header packet_h { + bit<16> assoc_id; + bit<16> pkt_id; + bit<8> frag_total; + bit<8> frag_id; + bit<16> size; + address_h address; +}; +``` + +### Dissociate + +- Command Type Code: `0x04` +- Transport: Unidirectional Stream +- Direction: Server -> Client + +The server can dissociate a UDP session by sending a `Dissociate` command to the client. The client SHOULD remove the +UDP socket in the session table after receiving the `Dissociate` command. + +```p4 +header dissociate_h { + bit<16> assoc_id; +}; +``` + +### Heartbeat + +- Command Type Code: `0x05` +- Transport: Unreliable Datagram +- Direction: Client -> Server + +Heartbeat is a command that is used to keep the connection alive. The client SHOULD send it using the unreliable datagram in a interval. +The payload of the `Heartbeat` command is empty. + +```p4 +header heartbeat_h { +}; +``` \ No newline at end of file diff --git a/asport-client/Cargo.toml b/asport-client/Cargo.toml new file mode 100644 index 0000000..5c04063 --- /dev/null +++ b/asport-client/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "asport-client" +version = "0.1.0" +authors = ["Kaede Akino "] +description = "A simple Asport client implementation." +categories = ["network-programming"] +keywords = ["network", "proxy", "reverse-proxy", "quic", "asport"] +edition = "2021" +readme = "README.md" +license = "GPL-3.0-or-later" +repository = "https://github.com/AkinoKaede/asport" + +[dependencies] +asport = { path = "../asport", version = "0.1.0" } +asport-quinn = { path = "../asport-quinn", version = "0.1.0" } +bytes = { version = "1.6.1", default-features = false, features = ["std"] } +clap = { version = "4.5.8", default-features = false, features = ["color", "derive", "error-context", "help", "std", "suggestions", "usage"] } +config = { version = "0.14.0", default-features = false, features = ["async", "convert-case", "json", "json5", "ron", "toml", "yaml"] } +crossbeam-utils = { version = "0.8.20", default-features = false, features = ["std"] } +env_logger = { version = "0.10.2", default-features = false, features = ["auto-color", "humantime"] } +humantime = { version = "2.1.0", default-features = false } +log = { version = "0.4.22", default-features = false, features = ["serde", "std"] } +once_cell = { version = "1.19.0", default-features = false, features = ["parking_lot", "std"] } +parking_lot = { version = "0.12.3", default-features = false, features = ["send_guard"] } +ppp = { version = "2.2.0", default-features = false } +quinn = { version = "0.11.2", default-features = false, features = ["futures-io", "runtime-tokio", "rustls"] } +register-count = { version = "0.1.0", default-features = false, features = ["std"] } +rustls = { version = "0.23.11", default-features = false } +rustls-native-certs = { version = "0.7.1", default-features = false } +rustls-pemfile = { version = "2.1.2", default-features = false } +serde = { version = "1.0.204", default-features = false, features = ["derive", "std"] } +socket2 = { version = "0.5.7", default-features = false } +thiserror = { version = "1.0.62", default-features = false } +tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "parking_lot", "rt-multi-thread", "time"] } +tokio-util = { version = "0.7.11", default-features = false, features = ["compat"] } +uuid = { version = "1.10.0", default-features = false, features = ["serde", "std"] } + +[dev-dependencies] +serde_json = { version = "1.0.120", default-features = false, features = ["std"] } + +[target.'cfg(unix)'.dependencies] +xdg = { version = "2.5.2", default-features = false } diff --git a/asport-client/README.md b/asport-client/README.md new file mode 100644 index 0000000..9101b99 --- /dev/null +++ b/asport-client/README.md @@ -0,0 +1,16 @@ +# asport-client + +A simple Asport client implementation. + +## Overview + +This crate provides a minimal Asport client implementation in Rust. It is designed to be a reference for the ASPORT protocol client implementation. + +## Quick Start + +Please refer to the [Quick Start](https://github.com/AkinoKaede/asport/blob/main/QUICK_START.md) guide. + +## License +This crate is licensed under [GNU General Public License v3.0 or later](https://github.com/AkinoKaede/asport/blob/main/LICENSE). + +SPDX-License-Identifier: [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) \ No newline at end of file diff --git a/asport-client/src/config.rs b/asport-client/src/config.rs new file mode 100644 index 0000000..c9d9018 --- /dev/null +++ b/asport-client/src/config.rs @@ -0,0 +1,372 @@ +use std::{ + fmt::Display, + ops::RangeInclusive, + path::PathBuf, + str::FromStr, + sync::{Arc, OnceLock}, + time::Duration, +}; + +use humantime::Duration as HumanDuration; +use log::LevelFilter; +use rustls::RootCertStore; +use serde::{de::Error as DeError, Deserialize, Deserializer}; +use uuid::Uuid; + +use crate::utils::{Address, CongestionControl, load_certs, Network, ProxyProtocol, UdpForwardMode}; + +// TODO: need a better way to do this +static CONFIG_BASE_PATH: OnceLock = OnceLock::new(); + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub server: Address, + + pub local: Address, + + pub uuid: Uuid, + + #[serde(deserialize_with = "deserialize_password")] + pub password: Arc<[u8]>, + + #[serde( + default = "default::network", + deserialize_with = "deserialize_from_str" + )] + pub network: Network, + + #[serde( + default = "default::udp_forward_mode", + deserialize_with = "deserialize_from_str" + )] + pub udp_forward_mode: UdpForwardMode, + + #[serde( + default = "default::udp_timeout", + deserialize_with = "deserialize_duration" + )] + pub udp_timeout: Duration, + + #[serde( + alias = "port", + default = "default::expected_port_range", + deserialize_with = "deserialize_expected_port_range" + )] + pub expected_port_range: RangeInclusive, + + #[serde(alias = "sni")] + pub server_name: Option, + + #[serde( + default = "default::certificates::default", + deserialize_with = "deserialize_certificates" + )] + pub certificates: RootCertStore, + + #[serde(default = "default::disable_sni")] + pub disable_sni: bool, + + #[serde( + default = "default::congestion_control", + deserialize_with = "deserialize_from_str" + )] + pub congestion_control: CongestionControl, + + #[serde( + default = "default::alpn", + deserialize_with = "deserialize_alpn" + )] + pub alpn: Vec>, + + #[serde(default = "default::zero_rtt_handshake")] + pub zero_rtt_handshake: bool, + + #[serde( + default = "default::healthy_check", + deserialize_with = "deserialize_duration" + )] + pub healthy_check: Duration, + + #[serde( + default = "default::timeout", + deserialize_with = "deserialize_duration" + )] + pub timeout: Duration, + + #[serde( + default = "default::handshake_timeout", + deserialize_with = "deserialize_duration" + )] + pub handshake_timeout: Duration, + + #[serde( + default = "default::task_negotiation_timeout", + deserialize_with = "deserialize_duration" + )] + pub task_negotiation_timeout: Duration, + + #[serde( + default = "default::heartbeat", + deserialize_with = "deserialize_duration" + )] + pub heartbeat: Duration, + + #[serde(default = "default::max_packet_size")] + pub max_packet_size: usize, + + #[serde(default = "default::send_window")] + pub send_window: u64, + + #[serde(default = "default::receive_window")] + pub receive_window: u32, + + #[serde( + default = "default::gc_interval", + deserialize_with = "deserialize_duration" + )] + pub gc_interval: Duration, + + #[serde( + default = "default::gc_lifetime", + deserialize_with = "deserialize_duration" + )] + pub gc_lifetime: Duration, + + #[serde( + default = "default::proxy_protocol", + deserialize_with = "deserialize_from_str" + )] + pub proxy_protocol: ProxyProtocol, + + #[serde(default = "default::log_level")] + pub log_level: LevelFilter, +} + +impl Config { + pub fn build(path: PathBuf) -> Result { + let base_path = path.parent(); + match base_path { + Some(base_path) => { + CONFIG_BASE_PATH.set(base_path.to_path_buf()).map_err( + |e| config::ConfigError::custom( + format!("failed to set config path: {:?}", e) + ) + )?; + } + None => { + return Err(config::ConfigError::custom("config path is not a file")); + } + } + + let cfg = config::Config::builder() + .add_source(config::File::from(path)) + .build()?; + + match cfg.try_deserialize::() { + Ok(cfg) => { + Ok(cfg) + } + Err(err) => Err(config::ConfigError::custom(err)), + } + } +} + +mod default { + use std::ops::RangeInclusive; + use std::time::Duration; + + use log::LevelFilter; + + use crate::utils::{CongestionControl, Network, ProxyProtocol, UdpForwardMode}; + + pub mod certificates { + use std::path::PathBuf; + + use rustls::RootCertStore; + + use crate::utils::load_certs; + + pub fn paths() -> Vec { + Vec::new() + } + + pub fn disable_native() -> bool { + false + } + + pub fn default() -> RootCertStore { + let paths: Vec = Vec::new(); + match load_certs(paths, false) { + Ok(certs) => certs, + Err(err) => { + log::error!("failed to load certificates: {}", err); + std::process::exit(1); + } + } + } + } + + pub fn network() -> Network { + Network::Both + } + + pub fn udp_forward_mode() -> UdpForwardMode { + UdpForwardMode::Native + } + + pub fn udp_timeout() -> Duration { + Duration::from_secs(60) + } + + pub fn expected_port_range() -> RangeInclusive { + 1..=65535 + } + + pub fn disable_sni() -> bool { + false + } + + pub fn congestion_control() -> CongestionControl { + CongestionControl::Cubic + } + + pub fn alpn() -> Vec> { + vec![b"asport".to_vec()] + } + + pub fn zero_rtt_handshake() -> bool { + false + } + + pub fn healthy_check() -> Duration { + Duration::from_secs(20) + } + + pub fn timeout() -> Duration { + Duration::from_secs(8) + } + + pub fn handshake_timeout() -> Duration { + Duration::from_secs(3) + } + + pub fn task_negotiation_timeout() -> Duration { + Duration::from_secs(3) + } + + pub fn heartbeat() -> Duration { + Duration::from_secs(3) + } + + pub fn max_packet_size() -> usize { + 1350 + } + + pub fn send_window() -> u64 { + 8 * 1024 * 1024 * 2 + } + + pub fn receive_window() -> u32 { + 8 * 1024 * 1024 + } + + pub fn gc_interval() -> Duration { + Duration::from_secs(3) + } + + pub fn gc_lifetime() -> Duration { + Duration::from_secs(15) + } + + pub fn proxy_protocol() -> ProxyProtocol { + ProxyProtocol::None + } + + pub fn log_level() -> LevelFilter { + LevelFilter::Warn + } +} + +pub fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result +where + T: FromStr, + ::Err: Display, + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + T::from_str(&s).map_err(DeError::custom) +} + +pub fn deserialize_password<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Ok(Arc::from(s.into_bytes().into_boxed_slice())) +} + +pub fn deserialize_alpn<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let s = Vec::::deserialize(deserializer)?; + Ok(s.into_iter().map(|alpn| alpn.into_bytes()).collect()) +} + +pub fn deserialize_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + + s.parse::() + .map(|d| *d) + .map_err(DeError::custom) +} + +pub fn deserialize_expected_port_range<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum PortRange { + Single(u16), + Range(u16, u16), + RangeInclusive(RangeInclusive), + } + + let range = PortRange::deserialize(deserializer)?; + + match range { + PortRange::Single(port) => Ok(port..=port), + PortRange::Range(start, end) => Ok(start..=end), + PortRange::RangeInclusive(range) => Ok(range), + } +} + +pub fn deserialize_certificates<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + struct Certificates { + #[serde(default = "default::certificates::paths")] + paths: Vec, + #[serde(default = "default::certificates::disable_native")] + disable_native: bool, + } + + let certs_cfg = Certificates::deserialize(deserializer)?; + + let base_path = CONFIG_BASE_PATH.get().unwrap(); + + let paths = certs_cfg.paths.iter().map(|path| base_path.join(path)).collect(); + + match load_certs(paths, certs_cfg.disable_native) { + Ok(certs) => Ok(certs), + Err(err) => Err(DeError::custom(err)), + } +} \ No newline at end of file diff --git a/asport-client/src/connection/authenticated.rs b/asport-client/src/connection/authenticated.rs new file mode 100644 index 0000000..8bfdab7 --- /dev/null +++ b/asport-client/src/connection/authenticated.rs @@ -0,0 +1,63 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + future::Future, + pin::Pin, + sync::Arc, + task::{Context, Poll, Waker}, +}; + +use crossbeam_utils::atomic::AtomicCell; +use parking_lot::Mutex; + +struct AuthenticatedInner { + port: AtomicCell>, + broadcast: Mutex>, +} + +#[derive(Clone)] +pub struct Authenticated(Arc); + +impl Authenticated { + pub fn new() -> Self { + Self(Arc::new(AuthenticatedInner { + port: AtomicCell::new(None), + broadcast: Mutex::new(Vec::new()), + })) + } + + pub fn set(&self, port: u16) { + self.0.port.store(Some(port)); + + + for waker in self.0.broadcast.lock().drain(..) { + waker.wake(); + } + } + + pub fn get(&self) -> Option { + self.0.port.load() + } +} + +impl Future for Authenticated { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.get().is_some() { + Poll::Ready(()) + } else { + self.0.broadcast.lock().push(cx.waker().clone()); + Poll::Pending + } + } +} + +impl Display for Authenticated { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + if let Some(port) = self.get() { + write!(f, "{port}") + } else { + write!(f, "unauthenticated") + } + } +} diff --git a/asport-client/src/connection/handle_stream.rs b/asport-client/src/connection/handle_stream.rs new file mode 100644 index 0000000..c00bbf2 --- /dev/null +++ b/asport-client/src/connection/handle_stream.rs @@ -0,0 +1,153 @@ +use std::sync::atomic::Ordering; + +use bytes::Bytes; +use quinn::{RecvStream, SendStream, VarInt}; +use register_count::Register; +use tokio::time; + +use asport_quinn::Task; + +use crate::{ + error::Error, + utils::{Network, UdpForwardMode}, +}; + +use super::Connection; + +impl Connection { + pub async fn accept_uni_stream(&self) -> Result<(RecvStream, Register), Error> { + let max = self.max_concurrent_uni_streams.load(Ordering::Relaxed); + + if self.remote_uni_stream_cnt.count() as u32 == max { + self.max_concurrent_uni_streams + .store(max * 2, Ordering::Relaxed); + + self.inner + .set_max_concurrent_uni_streams(VarInt::from(max * 2)); + } + + let recv = self.inner.accept_uni().await?; + let reg = self.remote_uni_stream_cnt.reg(); + Ok((recv, reg)) + } + + pub async fn accept_bi_stream(&self) -> Result<((SendStream, RecvStream), Register), Error> { + let max = self.max_concurrent_bi_streams.load(Ordering::Relaxed); + + if self.remote_bi_stream_cnt.count() as u32 == max { + self.max_concurrent_bi_streams + .store(max * 2, Ordering::Relaxed); + + self.inner + .set_max_concurrent_bi_streams(VarInt::from(max * 2)); + } + + let stream = self.inner.accept_bi().await?; + let reg = self.remote_bi_stream_cnt.reg(); + Ok((stream, reg)) + } + + pub async fn accept_datagram(&self) -> Result { + Ok(self.inner.read_datagram().await?) + } + + pub async fn handle_uni_stream(self, recv: RecvStream, _reg: Register) { + let pre_process = async { + let task = time::timeout( + self.task_negotiation_timeout, + self.model.accept_uni_stream(recv), + ) + .await + .map_err(|_| Error::TaskNegotiationTimeout)??; + + if let Task::ServerHello(server_hello) = &task { + self.handshake(server_hello).await?; + } + + tokio::select! { + () = self.auth.clone() => {} + err = self.inner.closed() => return Err(Error::from(err)), + }; + + Ok(task) + }; + + let res = match pre_process.await { + Ok(Task::ServerHello(server_hello)) => Ok(self.handle_server_hello(server_hello).await), + Ok(Task::Packet(pkt)) => if self.network.udp() { + match self.udp_forward_mode { + UdpForwardMode::Quic => { + self.handle_packet(pkt).await; + Ok(()) + } + UdpForwardMode::Native => Err(Error::WrongPacketSource), + } + } else { + Err(Error::NetworkDenied(Network::Udp)) + }, + Ok(Task::Dissociate(assoc_id)) => Ok(self.handle_dissociate(assoc_id).await), + Ok(_) => unreachable!(), + Err(err) => Err(err), + }; + + if let Err(err) = res { + log::warn!("incoming unidirectional stream error: {err}"); + } + } + + pub async fn handle_bi_stream(self, (send, recv): (SendStream, RecvStream), _reg: Register) { + let pre_process = async { + let task = time::timeout( + self.task_negotiation_timeout, + self.model.accept_bi_stream(send, recv), + ) + .await + .map_err(|_| Error::TaskNegotiationTimeout)??; + + tokio::select! { + () = self.auth.clone() => {} + err = self.inner.closed() => return Err(Error::from(err)), + }; + + Ok(task) + }; + + let res = match pre_process.await { + Ok(Task::Connect(connect)) => if self.network.tcp() { + self.handle_connect(connect).await; + Ok(()) + } else { + Err(Error::NetworkDenied(Network::Tcp)) + } + Ok(_) => unreachable!(), + Err(err) => Err(err), + }; + + if let Err(err) = res { + log::warn!("incoming bidirectional stream error: {err}"); + } + } + + pub async fn handle_datagram(self, dg: Bytes) { + log::debug!("incoming datagram"); + let res = match self.model.accept_datagram(dg) { + Err(err) => Err::<(), Error>(Error::Model(err)), + Ok(Task::Packet(pkt)) => if self.network.udp() { + match self.udp_forward_mode { + UdpForwardMode::Native => { + self.handle_packet(pkt).await; + Ok(()) + } + UdpForwardMode::Quic => Err(Error::WrongPacketSource), + } + } else { + Err(Error::NetworkDenied(Network::Udp)) + }, + _ => unreachable!(), + }; + + if let Err(err) = res { + log::warn!("incoming datagram error: {err}"); + } + } +} \ No newline at end of file diff --git a/asport-client/src/connection/handle_task.rs b/asport-client/src/connection/handle_task.rs new file mode 100644 index 0000000..d14d84b --- /dev/null +++ b/asport-client/src/connection/handle_task.rs @@ -0,0 +1,240 @@ +use std::{ + collections::hash_map::Entry, + io::{Error as IoError, ErrorKind}, + time::Duration, +}; + +use bytes::Bytes; +use quinn::ZeroRttAccepted; +use tokio::{ + io::{self, AsyncWriteExt}, + net::TcpStream, + time, +}; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +use asport::Address; +use asport_quinn::{Connect, Packet, ServerHello}; + +use crate::{ + error::Error, + utils::{NetworkUdpForwardModeCombine, ProxyProtocol, UdpForwardMode, union_proxy_protocol_addresses}, +}; + +use super::{ + Connection, + ERROR_CODE, + udp_session::UdpSession, +}; + +impl Connection { + pub async fn client_hello(self, zero_rtt_accepted: Option) { + if let Some(zero_rtt_accepted) = zero_rtt_accepted { + log::debug!("[client_hello] waiting for connection to be fully established"); + zero_rtt_accepted.await; + } + + log::debug!("[client_hello] sending client hello"); + + match self.model.client_hello( + self.uuid, + self.password.clone(), + NetworkUdpForwardModeCombine::new(self.network, self.udp_forward_mode), + self.expected_port_range, + ).await { + Ok(()) => log::info!("[client_hello] {uuid}", uuid = self.uuid), + Err(err) => log::warn!("[client_hello] client hello sending error: {err}"), + } + } + + pub async fn heartbeat(self, heartbeat: Duration) { + loop { + time::sleep(heartbeat).await; + + if self.is_closed() { + break; + } + + match self.model.heartbeat().await { + Ok(()) => log::debug!("[heartbeat]"), + Err(err) => log::warn!("[heartbeat] {err}"), + } + } + } + + pub async fn handle_server_hello(&self, server_hello: ServerHello) { + log::info!("[server_hello] remote port: {port}", + port = server_hello.port().unwrap()); // safe to unwrap + } + + pub async fn handle_connect(&self, conn: Connect) { + let source_addr_string = conn.addr().to_string(); + log::info!("[connect] {source_addr_string}"); + + let source_addr = match conn.addr() { + Address::SocketAddress(addr) => Some(addr.clone()), + Address::None => None, + }; + + let process = async { + let mut stream = None; + let mut last_err = None; + let mut local_addr = None; + match self.local.resolve().await { + Ok(addrs) => { + for addr in addrs { + match TcpStream::connect(addr).await { + Ok(s) => { + stream = Some(s); + local_addr = Some(addr); + break; + } + Err(err) => last_err = Some(Error::from(err)), + } + } + } + Err(err) => last_err = Some(err), + } + + if let Some(mut stream) = stream { + let addresses = union_proxy_protocol_addresses(source_addr, local_addr.unwrap()); + + let proxy_protocol_header = match (self.proxy_protocol, addresses) { + (ProxyProtocol::None, _) => None, + (ProxyProtocol::V1, Some(addresses)) => { + let v1 = ppp::v1::Addresses::from(addresses).to_string(); + Some(Bytes::from(v1)) + } + (ProxyProtocol::V2, Some(addresses)) => { + let v2 = ppp::v2::Builder::with_addresses( + ppp::v2::Version::Two | ppp::v2::Command::Proxy, + ppp::v2::Protocol::Stream, + addresses, + ).build().unwrap(); + Some(Bytes::from(v2)) + } + _ => { + return Err(Error::MissingAddress); + } + }; + + if let Some(header) = proxy_protocol_header { + let _ = stream.write(&header).await; + } + + let mut conn = conn.compat(); + let res = io::copy_bidirectional(&mut conn, &mut stream).await; + let _ = conn.get_mut().reset(ERROR_CODE); + let _ = stream.shutdown().await; + res?; + Ok::<_, Error>(()) + } else { + let _ = conn.compat().shutdown().await; + Err(last_err + .unwrap_or_else(|| Error::from(IoError::new(ErrorKind::NotFound, "no address resolved"))))? + } + }; + + match process.await { + Ok(()) => {} + Err(err) => log::warn!("[connect] {source_addr_string}: {err}"), + } + } + + + pub async fn handle_packet(&self, pkt: Packet) { + let assoc_id = pkt.assoc_id(); + let pkt_id = pkt.pkt_id(); + let frag_id = pkt.frag_id(); + let frag_total = pkt.frag_total(); + + log::info!( + "[packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] fragment {frag_id}/{frag_total}", + mode = self.udp_forward_mode, + frag_id = frag_id + 1, + ); + + let (pkt, addr, assoc_id) = match pkt.accept().await { + Ok(None) => return, + Ok(Some(res)) => res, + Err(err) => { + log::warn!( + "[packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] fragment {frag_id}/{frag_total}: {err}", + mode = self.udp_forward_mode, + frag_id = frag_id + 1, + ); + return; + } + }; + + let process = async { + log::info!( + "[packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] to {src_addr}", + mode = self.udp_forward_mode, + src_addr = addr, + ); + + let Some(local_addr) = self.local.resolve().await?.next() else { + return Err(Error::from(IoError::new(ErrorKind::NotFound, "no address resolved"))); + }; + + let session = match self.udp_sessions.lock().entry(assoc_id) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => { + let session = UdpSession::new( + self.clone(), + assoc_id, + self.max_packet_size, + self.udp_timeout, + local_addr, + addr.clone(), + self.proxy_protocol, + )?; + entry.insert(session.clone()); + session + } + }; + + session.send(pkt).await + }; + + if let Err(err) = process.await { + log::warn!( + "[packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] from {src_addr}: {err}", + mode = self.udp_forward_mode, + src_addr = addr, + ); + } + } + + pub async fn handle_dissociate(&self, assoc_id: u16) { + log::info!("[dissociate] [{assoc_id:#06x}]"); + + if let Some(session) = self.udp_sessions.lock().remove(&assoc_id) { + session.close(); + } + } + + pub async fn forward_packet(self, pkt: Bytes, addr: Address, assoc_id: u16) { + let addr_display = addr.to_string(); + + log::info!( + "[packet] [{assoc_id:#06x}] [to-{mode}] to {dst_addr}", + mode = self.udp_forward_mode, + dst_addr = addr_display, + ); + + let res = match self.udp_forward_mode { + UdpForwardMode::Native => self.model.packet_native(pkt, addr, assoc_id), + UdpForwardMode::Quic => self.model.packet_quic(pkt, addr, assoc_id).await, + }; + + if let Err(err) = res { + log::warn!( + "[packet] [{assoc_id:#06x}] [to-{mode}] to {dst_addr}: {err}", + mode = self.udp_forward_mode, + dst_addr = addr_display, + ); + } + } +} \ No newline at end of file diff --git a/asport-client/src/connection/mod.rs b/asport-client/src/connection/mod.rs new file mode 100644 index 0000000..6371293 --- /dev/null +++ b/asport-client/src/connection/mod.rs @@ -0,0 +1,421 @@ +use std::{ + collections::HashMap, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, + ops::RangeInclusive, + sync::{Arc, atomic::AtomicU32}, + time::Duration, +}; + +use crossbeam_utils::atomic::AtomicCell; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use quinn::{ClientConfig, congestion::{BbrConfig, CubicConfig, NewRenoConfig}, + Connection as QuinnConnection, crypto::rustls::QuicClientConfig, Endpoint as QuinnEndpoint, + EndpointConfig, TokioRuntime, TransportConfig, VarInt, ZeroRttAccepted}; +use register_count::Counter; +use rustls::ClientConfig as RustlsClientConfig; +use tokio::{ + sync::{Mutex as AsyncMutex, OnceCell as AsyncOnceCell}, + time, +}; +use uuid::Uuid; + +use asport::ServerHello as ServerHelloHeader; +use asport_quinn::{Connection as Model, ServerHello, side}; + +use crate::{ + config::Config, + error::Error, + utils::{Address, CongestionControl, Network, ProxyProtocol, ServerAddress, UdpForwardMode}, +}; + +use self::{authenticated::Authenticated, udp_session::UdpSession}; + +mod handle_task; +mod handle_stream; +mod authenticated; +mod udp_session; + +static ENDPOINT: OnceCell> = OnceCell::new(); +static CONNECTION: AsyncOnceCell> = AsyncOnceCell::const_new(); +static HEALTHY_CHECK: AtomicCell = AtomicCell::new(Duration::from_secs(0)); +static TIMEOUT: AtomicCell = AtomicCell::new(Duration::from_secs(0)); + + +pub const ERROR_CODE: VarInt = VarInt::from_u32(0); +const DEFAULT_CONCURRENT_STREAMS: u32 = 32; + +#[derive(Clone)] +pub struct Connection { + inner: QuinnConnection, + model: Model, + local: Address, + uuid: Uuid, + password: Arc<[u8]>, + network: Network, + udp_sessions: Arc>>, + udp_timeout: Duration, + udp_forward_mode: UdpForwardMode, + expected_port_range: RangeInclusive, + auth: Authenticated, + task_negotiation_timeout: Duration, + max_packet_size: usize, + proxy_protocol: ProxyProtocol, + remote_uni_stream_cnt: Counter, + remote_bi_stream_cnt: Counter, + max_concurrent_uni_streams: Arc, + max_concurrent_bi_streams: Arc, +} + +impl Connection { + pub fn set_config(cfg: Config) -> Result<(), Error> { + let mut crypto = RustlsClientConfig::builder() + .with_root_certificates(cfg.certificates).with_no_client_auth(); + + crypto.alpn_protocols = cfg.alpn; + crypto.enable_early_data = true; + crypto.enable_sni = !cfg.disable_sni; + + let mut config = ClientConfig::new( + Arc::new(QuicClientConfig::try_from(crypto)?) + ); + let mut tp_cfg = TransportConfig::default(); + + tp_cfg + .max_concurrent_bidi_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS)) + .max_concurrent_uni_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS)) + .send_window(cfg.send_window) + .stream_receive_window(VarInt::from_u32(cfg.receive_window)) + .max_idle_timeout(None); + + match cfg.congestion_control { + CongestionControl::Cubic => { + tp_cfg.congestion_controller_factory(Arc::new(CubicConfig::default())) + } + CongestionControl::NewReno => { + tp_cfg.congestion_controller_factory(Arc::new(NewRenoConfig::default())) + } + CongestionControl::Bbr => { + tp_cfg.congestion_controller_factory(Arc::new(BbrConfig::default())) + } + }; + + config.transport_config(Arc::new(tp_cfg)); + + // Try to create an IPv4 socket as the placeholder first, if it fails, try IPv6. + let socket = UdpSocket::bind(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))) + .or_else(|err| { + UdpSocket::bind(SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0))).map_err(|_| err) + }) + .map_err(|err| Error::Socket("failed to create endpoint UDP socket", err))?; + + + let mut ep = QuinnEndpoint::new( + EndpointConfig::default(), + None, + socket, + Arc::new(TokioRuntime), + )?; + + ep.set_default_client_config(config); + + let ep = Endpoint { + ep, + server: ServerAddress::new(cfg.server, cfg.server_name), + local: cfg.local, + uuid: cfg.uuid, + password: cfg.password, + network: cfg.network, + udp_forward_mode: cfg.udp_forward_mode, + udp_timeout: cfg.udp_timeout, + expected_port_range: cfg.expected_port_range, + zero_rtt_handshake: cfg.zero_rtt_handshake, + heartbeat: cfg.heartbeat, + handshake_timeout: cfg.handshake_timeout, + task_negotiation_timeout: cfg.task_negotiation_timeout, + max_packet_size: cfg.max_packet_size, + gc_interval: cfg.gc_interval, + gc_lifetime: cfg.gc_lifetime, + proxy_protocol: cfg.proxy_protocol, + }; + + + ENDPOINT + .set(Mutex::new(ep)) + .map_err(|_| "endpoint already initialized") + .unwrap(); + + HEALTHY_CHECK.store(cfg.healthy_check); + TIMEOUT.store(cfg.timeout); + + Ok(()) + } + + pub async fn check() -> Result<(), Error> { + let try_init_conn = async { + ENDPOINT + .get() + .unwrap() + .lock() + .connect() + .await + .map(AsyncMutex::new) + }; + + let check_and_reconnect_conn = async { + let mut conn = CONNECTION + .get_or_try_init(|| try_init_conn) + .await? + .lock() + .await; + + if conn.is_closed() { + let new_conn = ENDPOINT.get().unwrap().lock().connect().await?; + *conn = new_conn; + } + + Ok::<_, Error>(()) + }; + + time::timeout(TIMEOUT.load(), check_and_reconnect_conn) + .await + .map_err(|_| Error::Timeout)??; + + Ok(()) + } + + pub async fn start() { + let mut interval = time::interval(HEALTHY_CHECK.load()); + + loop { + interval.tick().await; + + if let Err(err) = Self::check().await { + log::warn!("[check] {err}", err = err); + } + } + } + + #[allow(clippy::too_many_arguments)] + fn new( + conn: QuinnConnection, + zero_rtt_accepted: Option, + local: Address, + uuid: Uuid, + password: Arc<[u8]>, + network: Network, + udp_forward_mode: UdpForwardMode, + udp_timeout: Duration, + expected_port_range: RangeInclusive, + heartbeat: Duration, + handshake_timeout: Duration, + task_negotiation_timeout: Duration, + max_packet_size: usize, + gc_interval: Duration, + gc_lifetime: Duration, + proxy_protocol: ProxyProtocol, + ) -> Self { + let conn = Self { + inner: conn.clone(), + model: Model::::new(conn), + local, + uuid, + password, + network, + udp_sessions: Arc::new(Mutex::new(HashMap::new())), + udp_forward_mode, + udp_timeout, + expected_port_range, + auth: Authenticated::new(), + task_negotiation_timeout, + max_packet_size, + proxy_protocol, + remote_uni_stream_cnt: Counter::new(), + remote_bi_stream_cnt: Counter::new(), + max_concurrent_uni_streams: Arc::new(AtomicU32::new(DEFAULT_CONCURRENT_STREAMS)), + max_concurrent_bi_streams: Arc::new(AtomicU32::new(DEFAULT_CONCURRENT_STREAMS)), + }; + + tokio::spawn( + conn.clone() + .init(zero_rtt_accepted, heartbeat, handshake_timeout, gc_interval, gc_lifetime), + ); + + conn + } + + async fn init( + self, + zero_rtt_accepted: Option, + heartbeat: Duration, + handshake_timeout: Duration, + gc_interval: Duration, + gc_lifetime: Duration, + ) { + log::info!("connection established"); + + tokio::spawn(self.clone().client_hello(zero_rtt_accepted)); + tokio::spawn(self.clone().timeout_handshake(handshake_timeout)); + tokio::spawn(self.clone().heartbeat(heartbeat)); + tokio::spawn(self.clone().collect_garbage(gc_interval, gc_lifetime)); + + let err = loop { + tokio::select! { + res = self.accept_uni_stream() => match res { + Ok((recv, reg)) => tokio::spawn(self.clone().handle_uni_stream(recv, reg)), + Err(err) => break err, + }, + res = self.accept_bi_stream() => match res { + Ok((stream, reg)) => tokio::spawn(self.clone().handle_bi_stream(stream, reg)), + Err(err) => break err, + }, + res = self.accept_datagram() => match res { + Ok(dg) => tokio::spawn(self.clone().handle_datagram(dg)), + Err(err) => break err, + }, + }; + }; + + log::warn!("connection error: {err}"); + } + + async fn timeout_handshake(self, timeout: Duration) { + time::sleep(timeout).await; + + if self.auth.get().is_none() { + log::warn!("[authenticate] timeout"); + self.close(); + } + } + + async fn handshake(&self, hello: &ServerHello) -> Result<(), Error> { + if self.auth.get().is_some() { + return Err(Error::DuplicatedHello); + } + + match hello.handshake_code() { + ServerHelloHeader::HANDSHAKE_CODE_SUCCESS => { + self.auth.set(hello.port().unwrap()); + Ok(()) + } + ServerHelloHeader::HANDSHAKE_CODE_AUTH_FAILED => Err(Error::AuthFailed), + ServerHelloHeader::HANDSHAKE_CODE_BIND_FAILED => Err(Error::RemoteBindFailed), + ServerHelloHeader::HANDSHAKE_CODE_PORT_DENIED => Err(Error::PortDenied), + ServerHelloHeader::HANDSHAKE_CODE_NETWORK_DENIED => Err(Error::NetworkDenied(self.network)), + _ => unreachable!(), + } + } + + async fn collect_garbage(self, gc_interval: Duration, gc_lifetime: Duration) { + loop { + time::sleep(gc_interval).await; + + if self.is_closed() { + break; + } + + log::debug!("packet fragment garbage collecting event"); + self.model.collect_garbage(gc_lifetime); + } + } + + fn is_closed(&self) -> bool { + self.inner.close_reason().is_some() + } + + fn close(&self) { + self.inner.close(ERROR_CODE, &[]); + } +} + +struct Endpoint { + ep: QuinnEndpoint, + server: ServerAddress, + local: Address, + uuid: Uuid, + password: Arc<[u8]>, + network: Network, + udp_forward_mode: UdpForwardMode, + udp_timeout: Duration, + expected_port_range: RangeInclusive, + zero_rtt_handshake: bool, + heartbeat: Duration, + handshake_timeout: Duration, + task_negotiation_timeout: Duration, + max_packet_size: usize, + gc_interval: Duration, + gc_lifetime: Duration, + proxy_protocol: ProxyProtocol, +} + +impl Endpoint { + async fn connect(&mut self) -> Result { + let mut last_err = None; + + for addr in self.server.resolve().await? { + let connect_to = async { + let match_ipv4 = + addr.is_ipv4() && self.ep.local_addr().map_or(false, |addr| addr.is_ipv4()); + let match_ipv6 = + addr.is_ipv6() && self.ep.local_addr().map_or(false, |addr| addr.is_ipv6()); + + if !match_ipv4 && !match_ipv6 { + let bind_addr = if addr.is_ipv4() { + SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0)) + } else { + SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0)) + }; + + self.ep + .rebind(UdpSocket::bind(bind_addr).map_err(|err| { + Error::Socket("failed to create endpoint UDP socket", err) + })?) + .map_err(|err| { + Error::Socket("failed to rebind endpoint UDP socket", err) + })?; + } + + let conn = self.ep.connect(addr, self.server.server_name())?; + + let (conn, zero_rtt_accepted) = if self.zero_rtt_handshake { + match conn.into_0rtt() { + Ok((conn, zero_rtt_accepted)) => (conn, Some(zero_rtt_accepted)), + Err(conn) => (conn.await?, None), + } + } else { + (conn.await?, None) + }; + + Ok((conn, zero_rtt_accepted)) + }; + + + match connect_to.await { + Ok((conn, zero_rtt_accepted)) => { + return Ok(Connection::new( + conn, + zero_rtt_accepted, + self.local.clone(), + self.uuid, + self.password.clone(), + self.network, + self.udp_forward_mode, + self.udp_timeout, + self.expected_port_range.clone(), + self.heartbeat, + self.handshake_timeout, + self.task_negotiation_timeout, + self.max_packet_size, + self.gc_interval, + self.gc_lifetime, + self.proxy_protocol, + )); + } + Err(err) => last_err = Some(err), + } + } + + + Err(last_err.unwrap_or(Error::DnsResolve)) + } +} \ No newline at end of file diff --git a/asport-client/src/connection/udp_session.rs b/asport-client/src/connection/udp_session.rs new file mode 100644 index 0000000..65f841f --- /dev/null +++ b/asport-client/src/connection/udp_session.rs @@ -0,0 +1,206 @@ +use std::{ + io::Error as IoError, + net::{IpAddr, Ipv6Addr, SocketAddr, UdpSocket as StdUdpSocket}, + sync::Arc, + time::Duration, +}; + +use bytes::{BufMut, Bytes, BytesMut}; +use parking_lot::Mutex; +use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use tokio::{ + net::UdpSocket, + sync::{ + mpsc::{self, Sender as MpscSender}, + oneshot::{self, Sender as OneshotSender}, + }, + time, +}; + +use asport::Address; + +use crate::error::Error; +use crate::utils::{ProxyProtocol, union_proxy_protocol_addresses}; + +use super::Connection; + +#[derive(Clone)] +pub struct UdpSession(Arc); + +struct UdpSessionInner { + assoc_id: u16, + conn: Connection, + socket: UdpSocket, + max_pkt_size: usize, + local: SocketAddr, + remote: Option, + proxy_protocol: ProxyProtocol, + update: MpscSender<()>, + close: Mutex>>, +} + +impl UdpSession { + pub fn new( + conn: Connection, + assoc_id: u16, + max_pkt_size: usize, + udp_timeout: Duration, + local: SocketAddr, + remote: Address, + proxy_protocol: ProxyProtocol, + ) -> Result { + let socket = { + let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP)) + .map_err(|err| Error::Socket("failed to create UDP associate IPv6 socket", err))?; + + socket.set_nonblocking(true).map_err(|err| { + Error::Socket( + "failed setting UDP associate IPv6 socket as non-blocking", + err, + ) + })?; + + socket + .bind(&SockAddr::from(SocketAddr::from(( + Ipv6Addr::UNSPECIFIED, + 0, + )))) + .map_err(|err| Error::Socket("failed to bind UDP associate IPv6 socket", err))?; + UdpSocket::from_std(StdUdpSocket::from(socket))? + }; + + let (close_tx, close_rx) = oneshot::channel(); + + let (update_tx, mut update_rx) = mpsc::channel(1); + + let remote_socket_address = match remote { + Address::SocketAddress(addr) => Some(addr), + Address::None => None, + }; + + let session = Self(Arc::new(UdpSessionInner { + conn, + assoc_id, + socket, + max_pkt_size, + local, + proxy_protocol, + remote: remote_socket_address, + close: Mutex::new(Some(close_tx)), + update: update_tx, + })); + + let session_listening = session.clone(); + + let listen = async move { + loop { + let pkt = match session_listening.recv().await { + Ok(res) => res, + Err(err) => { + log::warn!("[packet] [{assoc_id:#06x}] outbound listening error: {err}",); + continue; + } + }; + + tokio::spawn(session_listening.0.conn.clone().forward_packet( + pkt, + remote.clone(), + session_listening.0.assoc_id, + )); + } + }; + + tokio::spawn(async move { + tokio::select! { + _ = listen => unreachable!(), + _ = close_rx => {}, + } + }); + + // GC like NAT table + // If this session is inactive, close itself. + let gc_session = session.clone(); + tokio::spawn(async move { + loop { + tokio::select! { + Some(_) = update_rx.recv() => {}, + _ = time::sleep(udp_timeout) => { + log::debug!("UDP session [{assoc_id:#06x}] timeout"); + + if let Some(session)= gc_session.0.conn.udp_sessions.lock().remove(&assoc_id) { + session.close(); + }; + + return; + }, + } + } + }); + + Ok(session) + } + + pub async fn send(&self, pkt: Bytes) -> Result<(), Error> { + self.send_to(pkt, self.0.local).await + } + + pub async fn send_to(&self, pkt: Bytes, addr: SocketAddr) -> Result<(), Error> { + let addresses = union_proxy_protocol_addresses(self.0.remote, addr); + + let packet = if matches!(self.0.proxy_protocol, ProxyProtocol::V2) { + let addresses = if let Some(addresses) = addresses { + addresses + } else { + return Err(Error::MissingAddress); + }; + + let v2 = ppp::v2::Builder::with_addresses( + ppp::v2::Version::Two | ppp::v2::Command::Proxy, + ppp::v2::Protocol::Datagram, + addresses, + ).build().unwrap(); + + let mut buf = BytesMut::with_capacity(v2.len() + pkt.len()); + buf.put(v2.as_slice()); + buf.put(pkt); + buf.freeze() + } else { + pkt + }; + + // Because self.0.socket is bind on [::], we need to convert the IPv4 address to IPv6. + let addr = match addr { + SocketAddr::V4(v4) => SocketAddr::new(IpAddr::from(v4.ip().to_ipv6_mapped()), v4.port()), + addr => addr, + }; + + self.0.socket.send_to(&packet, addr).await?; + + self.update().await; + + Ok(()) + } + + + async fn recv(&self) -> Result { + self.recv_from().await.map(|(pkt, _)| pkt) + } + + async fn recv_from(&self) -> Result<(Bytes, SocketAddr), IoError> { + let mut buf = vec![0u8; self.0.max_pkt_size]; + let (n, addr) = self.0.socket.recv_from(&mut buf).await?; + buf.truncate(n); + + self.update().await; + + Ok((Bytes::from(buf), addr)) + } + + async fn update(&self) { + self.0.update.send(()).await.unwrap(); + } + + pub fn close(&self) { + let _ = self.0.close.lock().take().unwrap().send(()); + } +} diff --git a/asport-client/src/error.rs b/asport-client/src/error.rs new file mode 100644 index 0000000..9acaaa8 --- /dev/null +++ b/asport-client/src/error.rs @@ -0,0 +1,53 @@ +use std::io::Error as IoError; + +use quinn::{ConnectError, ConnectionError, crypto::rustls::NoInitialCipherSuite}; +use rustls::Error as RustlsError; +use thiserror::Error; + +use asport_quinn::Error as ModelError; + +use crate::utils::Network; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] IoError), + #[error(transparent)] + Connect(#[from] ConnectError), + #[error("load native certificates error: {0}")] + LoadNativeCerts(IoError), + #[error(transparent)] + Rustls(#[from] RustlsError), + #[error(transparent)] + NoInitialCipherSuite(#[from] NoInitialCipherSuite), + #[error("{0}: {1}")] + Socket(&'static str, IoError), + #[error("timeout establishing connection")] + Timeout, + #[error("duplicated authentication")] + DuplicatedHello, + #[error("authentication failed")] + AuthFailed, + #[error("remote bind failed")] + RemoteBindFailed, + #[error("network denied: {0}")] + NetworkDenied(Network), + #[error("port denied")] + PortDenied, + #[error(transparent)] + Model(#[from] ModelError), + #[error("cannot resolve the server name")] + DnsResolve, + #[error("task negotiation timed out")] + TaskNegotiationTimeout, + #[error("invalid packet source")] + WrongPacketSource, + #[error("missing address")] + MissingAddress, +} + +impl From for Error { + fn from(err: ConnectionError) -> Self { + Self::Io(IoError::from(err)) + } +} diff --git a/asport-client/src/main.rs b/asport-client/src/main.rs new file mode 100644 index 0000000..756b193 --- /dev/null +++ b/asport-client/src/main.rs @@ -0,0 +1,119 @@ +/* +* Asport, a quick and secure reverse proxy based on QUIC for NAT traversal. +* Copyright (C) 2024 Kaede Akino +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +use std::{ + cell::LazyCell, + path::PathBuf, + process, +}; + +use clap::Parser; +use env_logger::Builder as LoggerBuilder; + +use crate::connection::Connection; + +mod config; +mod utils; +mod error; +mod connection; + +#[derive(Parser)] +#[command(about, author, version)] +struct Arguments { + #[clap(short, long)] + config: Option, +} + +#[tokio::main] +async fn main() { + let args = Arguments::parse(); + + let config_path = args.config.unwrap_or_else(|| { + if let Some(path) = find_config() { + path + } else { + eprintln!("No configuration file found, please specify one with --config"); + process::exit(1); + } + }); + + let cfg = match config::Config::build(config_path) { + Ok(cfg) => cfg, + Err(err) => { + eprintln!("{err}"); + process::exit(1); + } + }; + + LoggerBuilder::new() + .filter_level(cfg.log_level) + .format_module_path(false) + .format_target(false) + .init(); + + + match Connection::set_config(cfg) { + Ok(()) => {} + Err(err) => { + eprintln!("{err}"); + process::exit(1); + } + } + + Connection::start().await; +} + +const CONFIG_EXTENSIONS: [&str; 6] = ["json", "jsonc", "ron", "toml", "yaml", "yml"]; +const CONFIG_NAMES: LazyCell> = LazyCell::new(|| { + CONFIG_EXTENSIONS.iter() + .map(|ext| PathBuf::from(format!("client.{}", ext))).collect::>() +}); + +#[cfg(unix)] +fn find_config() -> Option { + for config in CONFIG_NAMES.iter() { + if config.exists() { + return Some(config.clone()); + } + } + + let xdg_dirs = if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("asport") { + xdg_dirs + } else { + return None; + }; + + for config in CONFIG_NAMES.iter() { + if let Some(path) = xdg_dirs.find_config_file(config) { + return Some(path); + } + } + + None +} + +#[cfg(not(unix))] +fn find_config() -> Option { + for config in CONFIG_NAMES.iter() { + if config.exists() { + return Some(config.clone()); + } + } + + None +} diff --git a/asport-client/src/utils.rs b/asport-client/src/utils.rs new file mode 100644 index 0000000..8eef411 --- /dev/null +++ b/asport-client/src/utils.rs @@ -0,0 +1,346 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + fs::{self, File}, + io::BufReader, + net::{IpAddr, SocketAddr}, + path::Path, + str::FromStr, +}; + +use rustls::{pki_types::CertificateDer, RootCertStore}; +use rustls_pemfile::Item; +use serde::{de::Error as DeError, Deserialize, Deserializer}; +use tokio::net; + +use asport::ForwardMode; + +use crate::error::Error; + +pub fn load_certs>(paths: Vec

, disable_native: bool) -> Result { + let mut certs = RootCertStore::empty(); + + for path in &paths { + let mut file = BufReader::new(File::open(path)?); + + while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) { + if let Item::X509Certificate(cert) = item { + certs.add(cert)?; + } + } + } + + if certs.is_empty() { + for path in &paths { + certs.add(CertificateDer::from(fs::read(path)?))?; + } + } + + if !disable_native { + for cert in rustls_native_certs::load_native_certs().map_err(Error::LoadNativeCerts)? { + let _ = certs.add(cert); + } + } + + Ok(certs) +} + +pub fn union_proxy_protocol_addresses(source: Option, destination: SocketAddr) + -> Option<(SocketAddr, SocketAddr)> { + match (source, destination) { + // If destination is an IPv6 address and source is an IPv4 address, convert source to an IPv6-mapped-IPv4 address + // Avoid to be UNKNOWN or AF_UNSPEC + // See also: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + (Some(SocketAddr::V4(source_v4)), destination @ SocketAddr::V6(_)) => { + let source = SocketAddr::new(IpAddr::from(source_v4.ip().to_ipv6_mapped()), source_v4.port()); + Some((source, destination)) + } + // If destination is an IPv4 address and source is an IPv6 address, try to convert source to an IPv4-mapped-IPv6 address + (Some(source @ SocketAddr::V6(source_v6)), destination @ SocketAddr::V4(_)) => { + match source_v6.ip().to_ipv4_mapped() { + Some(ipv4) => { + let source = SocketAddr::new(IpAddr::from(ipv4), source_v6.port()); + Some((source, destination)) + } + // Finally, it will be convert to UNKNOWN (v1) or AF_UNSPEC (v2). + None => Some((source, destination)), + } + } + (Some(source), destination) => Some((source, destination)), + _ => None, + } +} + +pub enum CongestionControl { + Cubic, + NewReno, + Bbr, +} + +impl FromStr for CongestionControl { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("cubic") { + Ok(Self::Cubic) + } else if s.eq_ignore_ascii_case("new_reno") || s.eq_ignore_ascii_case("newreno") { + Ok(Self::NewReno) + } else if s.eq_ignore_ascii_case("bbr") { + Ok(Self::Bbr) + } else { + Err("invalid congestion control") + } + } +} + +#[derive(Debug, PartialEq, Copy, Clone)] +pub enum Network { + Tcp, + Udp, + Both, +} + +impl Network { + pub(crate) fn is_tcp(&self) -> bool { + matches!(self, Self::Tcp) + } + + pub(crate) fn is_udp(&self) -> bool { + matches!(self, Self::Udp) + } + + pub(crate) fn is_both(&self) -> bool { + matches!(self, Self::Both) + } + + pub(crate) fn tcp(&self) -> bool { + self.is_both() || self.is_tcp() + } + + pub(crate) fn udp(&self) -> bool { + self.is_both() || self.is_udp() + } +} + +impl Display for Network { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Tcp => write!(f, "tcp"), + Self::Udp => write!(f, "udp"), + Self::Both => write!(f, "both"), + } + } +} + +impl FromStr for Network { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("tcp") { + Ok(Self::Tcp) + } else if s.eq_ignore_ascii_case("udp") { + Ok(Self::Udp) + } else if vec!["both", "tcpudp", "tcp_udp", "tcp-udp", "all"].iter() + .any(|&x| s.eq_ignore_ascii_case(x)) { + Ok(Self::Both) + } else { + Err("invalid network") + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum UdpForwardMode { + Native, + Quic, +} + +impl FromStr for UdpForwardMode { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("native") { + Ok(Self::Native) + } else if s.eq_ignore_ascii_case("quic") { + Ok(Self::Quic) + } else { + Err("invalid UDP relay mode") + } + } +} + +impl Display for UdpForwardMode { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Native => write!(f, "native"), + Self::Quic => write!(f, "quic"), + } + } +} + +#[derive(Clone, Copy, Debug)] +pub enum ProxyProtocol { + None, + V1, + V2, +} + +impl FromStr for ProxyProtocol { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("v1") { + Ok(Self::V1) + } else if s.eq_ignore_ascii_case("v2") { + Ok(Self::V2) + } else if vec!["none", "disable", "disabled", "off"].iter() + .any(|&x| s.eq_ignore_ascii_case(x)) { + Ok(Self::None) + } else { + Err("invalid proxy protocol version") + } + } +} + +impl Display for ProxyProtocol { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::None => write!(f, "none"), + Self::V1 => write!(f, "v1"), + Self::V2 => write!(f, "v2"), + } + } +} + + +pub struct NetworkUdpForwardModeCombine(Network, UdpForwardMode); + +impl NetworkUdpForwardModeCombine { + pub fn new(network: Network, mode: UdpForwardMode) -> Self { + Self(network, mode) + } +} + +impl From for ForwardMode { + fn from(value: NetworkUdpForwardModeCombine) -> Self { + let (network, mode) = (value.0, value.1); + match (network, mode) { + (Network::Tcp, _) => ForwardMode::Tcp, + (Network::Udp, UdpForwardMode::Native) => ForwardMode::UdpNative, + (Network::Udp, UdpForwardMode::Quic) => ForwardMode::UdpQuic, + (Network::Both, UdpForwardMode::Native) => ForwardMode::TcpUdpNative, + (Network::Both, UdpForwardMode::Quic) => ForwardMode::TcpUdpQuic, + } + } +} + +impl From<(Network, UdpForwardMode)> for NetworkUdpForwardModeCombine { + fn from(value: (Network, UdpForwardMode)) -> Self { + NetworkUdpForwardModeCombine::new(value.0, value.1) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Address { + SocketAddress(SocketAddr), + DomainAddress(String, u16), +} + +impl Address { + pub fn new(host: String, port: u16) -> Self { + match host.parse::() { + Ok(ip) => Self::SocketAddress(SocketAddr::from((ip, port))), + Err(_) => Self::DomainAddress(host, port), + } + } + + pub async fn resolve(&self) -> Result, Error> { + match self { + Self::SocketAddress(addr) => Ok(vec![*addr].into_iter()), + Self::DomainAddress(host, port) => { + Ok(net::lookup_host((host.as_str(), *port)) + .await? + .collect::>() + .into_iter()) + } + } + } +} + +impl<'de> Deserialize<'de> for Address { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + let (host, port) = s + .rsplit_once(':') + .ok_or(DeError::custom("invalid server address"))?; + + // remove first and last brackets for IPv6 address + let host = if host.starts_with('[') && host.ends_with(']') { + host[1..host.len() - 1].to_string() + } else { + host.to_string() + }; + + let port = port.parse().map_err(DeError::custom)?; + + Ok(Address::new(host, port)) + } +} + +pub struct ServerAddress { + addr: Address, + server_name: String, +} + +impl ServerAddress { + pub fn new(addr: Address, server_name: Option) -> Self { + let server_name = match (server_name, &addr) { + (Some(name), _) => name, + // Use IP address as server name if no server name is provided + (None, Address::SocketAddress(addr)) => addr.ip().to_string(), + (None, Address::DomainAddress(domain, _)) => domain.clone(), + }; + + Self { addr, server_name } + } + + pub fn server_name(&self) -> &str { + &self.server_name + } + + pub async fn resolve(&self) -> Result, Error> { + self.addr.resolve().await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_address() { + let s = r#""127.0.0.1:8080""#; + let addr: Address = serde_json::from_str(s).unwrap(); + assert_eq!(addr, Address::SocketAddress(SocketAddr::from(([127, 0, 0, 1], 8080)))); + + let s = r#""[::1]:8080""#; + let addr: Address = serde_json::from_str(s).unwrap(); + assert_eq!(addr, Address::SocketAddress(SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 8080)))); + + let s = r#""asport.akinokaede.com:8080""#; + let addr: Address = serde_json::from_str(s).unwrap(); + assert_eq!(addr, Address::DomainAddress("asport.akinokaede.com".to_string(), 8080)); + + // Invalid address + let s = r#""127.0.0.1""#; + let addr: Result = serde_json::from_str(s); + assert!(addr.is_err()); + + let s = r#""127.0.0.1:test""#; + let addr: Result = serde_json::from_str(s); + assert!(addr.is_err()); + } +} \ No newline at end of file diff --git a/asport-quinn/Cargo.toml b/asport-quinn/Cargo.toml new file mode 100644 index 0000000..e9ed16b --- /dev/null +++ b/asport-quinn/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "asport-quinn" +version = "0.1.0" +authors = ["Kaede Akino "] +description = "A wrapped quinn to implement ASPORT protocol." +categories = ["network-programming"] +keywords = ["network", "proxy", "reverse-proxy", "quic", "asport"] +edition = "2021" +readme = "README.md" +license = "GPL-3.0-or-later" +repository = "https://github.com/AkinoKaede/asport" + +[dependencies] +asport = { path = "../asport", version = "0.1.0", features = ["async_marshal", "marshal", "model"] } +bytes = { version = "1.6.1", default-features = false, features = ["std"] } +futures-util = { version = "0.3.30", default-features = false, features = ["io", "std"] } +quinn = { version = "0.11.2", default-features = false, features = ["futures-io"] } +thiserror = { version = "1.0.62", default-features = false } +uuid = { version = "1.10.0", default-features = false, features = ["std"] } \ No newline at end of file diff --git a/asport-quinn/README.md b/asport-quinn/README.md new file mode 100644 index 0000000..8f030a2 --- /dev/null +++ b/asport-quinn/README.md @@ -0,0 +1,20 @@ +# asport-quinn + +A wrapped [quinn](https://github.com/quinn-rs/quinn) to implement ASPORT protocol. + +## Overview + +This crate provides a wrapper [`Connection`](https://docs.rs/asport-quinn/latest/asport_quinn/struct.Connection.html) around [`quinn::Connection`](https://docs.rs/quinn/latest/quinn/struct.Connection.html). + +## Usage + +Run the following command to add this crate as a dependency: + +```bash +cargo add asport-quinn +``` + +## License +This crate is licensed under [GNU General Public License v3.0 or later](https://github.com/AkinoKaede/asport/blob/main/LICENSE). + +SPDX-License-Identifier: [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) \ No newline at end of file diff --git a/asport-quinn/src/lib.rs b/asport-quinn/src/lib.rs new file mode 100644 index 0000000..bc00be9 --- /dev/null +++ b/asport-quinn/src/lib.rs @@ -0,0 +1,662 @@ +/* +* Asport, a quick and secure reverse proxy based on QUIC for NAT traversal. +* Copyright (C) 2024 Kaede Akino +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +use std::{ + fmt::{Debug, Formatter, Result as FmtResult}, + io::{Cursor, Error as IoError}, + ops::RangeInclusive, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +use bytes::{BufMut, Bytes, BytesMut}; +use futures_util::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use quinn::{ClosedStream, Connection as QuinnConnection, ConnectionError, RecvStream, + SendDatagramError, SendStream, VarInt}; +use thiserror::Error; +use uuid::Uuid; + +use asport::{ + Address, ForwardMode, Header, + model::{ + AssembleError, + ClientHello as ClientHelloModel, + Connect as ConnectModel, + Connection as ConnectionModel, + KeyingMaterialExporter as KeyingMaterialExporterImpl, + Packet as PacketModel, ServerHello as ServerHelloModel, + side::{Rx, Tx}, + }, + ServerHello as ServerHelloHeader, UnmarshalError, +}; + +use self::side::Side; + +pub mod side { + #[derive(Clone, Debug)] + pub struct Client; + + #[derive(Clone, Debug)] + pub struct Server; + + #[derive(Debug)] + pub(super) enum Side { + Client(C), + Server(S), + } +} + +/// The ASPORT Connection. +/// +/// This struct takes a clone of `quinn::Connection` for performing ASPORT operations. +#[derive(Clone)] +pub struct Connection { + conn: QuinnConnection, + model: ConnectionModel, + _marker: Side, +} + +impl Connection { + /// Sends a `Packet` using UDP relay mode `native`. + pub fn packet_native( + &self, + pkt: impl AsRef<[u8]>, + addr: Address, + assoc_id: u16, + ) -> Result<(), Error> { + let Some(max_pkt_size) = self.conn.max_datagram_size() else { + return Err(Error::SendDatagram(SendDatagramError::Disabled)); + }; + + let model = self.model.send_packet(assoc_id, addr, max_pkt_size); + + for (header, frag) in model.into_fragments(pkt) { + let mut buf = BytesMut::with_capacity(header.len() + frag.len()); + header.write(&mut buf); + buf.put_slice(frag); + self.conn.send_datagram(Bytes::from(buf))?; + } + + Ok(()) + } + + /// Sends a `Packet` using UDP relay mode `quic`. + pub async fn packet_quic( + &self, + pkt: impl AsRef<[u8]>, + addr: Address, + assoc_id: u16, + ) -> Result<(), Error> { + let model = self.model.send_packet(assoc_id, addr, u16::MAX as usize); + + for (header, frag) in model.into_fragments(pkt) { + let mut send = self.conn.open_uni().await?; + header.async_marshal(&mut send).await?; + AsyncWriteExt::write_all(&mut send, frag).await?; + send.close().await?; + } + + Ok(()) + } + + /// Returns the number of `Connect` tasks + pub fn task_connect_count(&self) -> usize { + self.model.task_connect_count() + } + + /// Returns the number of active UDP sessions + pub fn task_associate_count(&self) -> usize { + self.model.task_associate_count() + } + + /// Removes packet fragments that can not be reassembled within the specified timeout + pub fn collect_garbage(&self, timeout: Duration) { + self.model.collect_garbage(timeout); + } + + fn keying_material_exporter(&self) -> KeyingMaterialExporter { + KeyingMaterialExporter(self.conn.clone()) + } +} + +impl Connection { + /// Creates a new client side `Connection`. + pub fn new(conn: QuinnConnection) -> Self { + Self { + conn, + model: ConnectionModel::new(), + _marker: side::Client, + } + } + + /// Sends a `ClientHello` command. + pub async fn client_hello( + &self, + uuid: Uuid, + password: impl AsRef<[u8]>, + forward_mode: impl Into, + expected_port_range: RangeInclusive, + ) -> Result<(), Error> { + let model = self + .model + .send_client_hello(uuid, password, &self.keying_material_exporter(), forward_mode, expected_port_range); + + let mut send = self.conn.open_uni().await?; + model.header().async_marshal(&mut send).await?; + send.close().await?; + Ok(()) + } + + + /// Sends a `Heartbeat` command. + pub async fn heartbeat(&self) -> Result<(), Error> { + let model = self.model.send_heartbeat(); + let mut buf = Vec::with_capacity(model.header().len()); + model.header().async_marshal(&mut buf).await.unwrap(); + self.conn.send_datagram(Bytes::from(buf))?; + Ok(()) + } + + /// Try to parse a `quinn::RecvStream` as a ASPORT command. + /// + /// The `quinn::RecvStream` should be accepted by `quinn::Connection::accept_uni()` from the same `quinn::Connection`. + pub async fn accept_uni_stream(&self, mut recv: RecvStream) -> Result { + let header = match Header::async_unmarshal(&mut recv).await { + Ok(header) => header, + Err(err) => return Err(Error::UnmarshalUniStream(err, recv)), + }; + + match header { + Header::ClientHello(_) => Err(Error::BadCommandUniStream("clienthello", recv)), + Header::ServerHello(server_hello) => { + let model = self.model.recv_server_hello(server_hello); + Ok(Task::ServerHello(ServerHello::new( + model + ))) + } + Header::Packet(pkt) => { + let model = self.model.recv_packet_unrestricted(pkt); + Ok(Task::Packet(Packet::new(model, PacketSource::Quic(recv)))) + } + Header::Dissociate(dissoc) => { + let model = self.model.recv_dissociate(dissoc); + Ok(Task::Dissociate(model.assoc_id())) + } + Header::Connect(_) => Err(Error::BadCommandUniStream("connect", recv)), + Header::Heartbeat(_) => Err(Error::BadCommandUniStream("heartbeat", recv)), + _ => unreachable!(), + } + } + + /// Try to parse a pair of `quinn::SendStream` and `quinn::RecvStream` as a ASPORT command. + /// + /// The pair of stream should be accepted by `quinn::Connection::accept_bi()` from the same `quinn::Connection`. + pub async fn accept_bi_stream( + &self, + send: SendStream, + mut recv: RecvStream, + ) -> Result { + let header = match Header::async_unmarshal(&mut recv).await { + Ok(header) => header, + Err(err) => return Err(Error::UnmarshalBiStream(err, send, recv)), + }; + + match header { + Header::ClientHello(_) => Err(Error::BadCommandBiStream("clienthello", send, recv)), + Header::ServerHello(_) => Err(Error::BadCommandBiStream("serverhello", send, recv)), + Header::Connect(connect) => { + let model = self.model.recv_connect(connect); + Ok(Task::Connect(Connect::new(Side::Client(model), send, recv))) + } + Header::Packet(_) => Err(Error::BadCommandBiStream("packet", send, recv)), + Header::Dissociate(_) => Err(Error::BadCommandBiStream("dissociate", send, recv)), + Header::Heartbeat(_) => Err(Error::BadCommandBiStream("heartbeat", send, recv)), + _ => unreachable!(), + } + } + + /// Try to parse a QUIC Datagram as a ASPORT command. + /// + /// The Datagram should be accepted by `quinn::Connection::read_datagram()` from the same `quinn::Connection`. + pub fn accept_datagram(&self, dg: Bytes) -> Result { + let mut dg = Cursor::new(dg); + + let header = match Header::unmarshal(&mut dg) { + Ok(header) => header, + Err(err) => return Err(Error::UnmarshalDatagram(err, dg.into_inner())), + }; + + match header { + Header::ClientHello(_) => Err(Error::BadCommandDatagram("clienthello", dg.into_inner())), + Header::ServerHello(_) => Err(Error::BadCommandDatagram("serverhello", dg.into_inner())), + Header::Connect(_) => Err(Error::BadCommandDatagram("connect", dg.into_inner())), + Header::Packet(pkt) => { + let model = self.model.recv_packet_unrestricted(pkt); + let pos = dg.position() as usize; + let buf = dg.into_inner().slice(pos..pos + model.size() as usize); + Ok(Task::Packet(Packet::new(model, PacketSource::Native(buf)))) + } + Header::Dissociate(_) => Err(Error::BadCommandDatagram("dissociate", dg.into_inner())), + Header::Heartbeat(_) => Err(Error::BadCommandDatagram("heartbeat", dg.into_inner())), + _ => unreachable!(), + } + } +} + +impl Connection { + /// Creates a new server side `Connection`. + pub fn new(conn: QuinnConnection) -> Self { + Self { + conn, + model: ConnectionModel::new(), + _marker: side::Server, + } + } + + /// Sends a `ServerHello` command. + pub async fn server_hello(&self, result: ServerHelloHeader) -> Result<(), Error> { + let model = self + .model + .send_server_hello(result); + let mut send = self.conn.open_uni().await?; + model.header().async_marshal(&mut send).await?; + send.close().await?; + Ok(()) + } + + /// Sends a `Connect` command. + pub async fn connect(&self, addr: Address) -> Result { + let model = self.model.send_connect(addr); + let (mut send, recv) = self.conn.open_bi().await?; + model.header().async_marshal(&mut send).await?; + Ok(Connect::new(Side::Server(model), send, recv)) + } + + /// Sends a `Dissociate` command. + pub async fn dissociate(&self, assoc_id: u16) -> Result<(), Error> { + let model = self.model.send_dissociate(assoc_id); + let mut send = self.conn.open_uni().await?; + model.header().async_marshal(&mut send).await?; + send.close().await?; + Ok(()) + } + + + /// Try to parse a `quinn::RecvStream` as a ASPORT command. + /// + /// The `quinn::RecvStream` should be accepted by `quinn::Connection::accept_uni()` from the same `quinn::Connection`. + pub async fn accept_uni_stream(&self, mut recv: RecvStream) -> Result { + let header = match Header::async_unmarshal(&mut recv).await { + Ok(header) => header, + Err(err) => return Err(Error::UnmarshalUniStream(err, recv)), + }; + + match header { + Header::ClientHello(client_hello) => { + let model = self.model.recv_client_hello(client_hello); + Ok(Task::ClientHello(ClientHello::new( + model, + self.keying_material_exporter(), + ))) + } + Header::ServerHello(_) => Err(Error::BadCommandUniStream("serverhello", recv)), + Header::Connect(_) => Err(Error::BadCommandUniStream("connect", recv)), + Header::Packet(pkt) => { + let assoc_id = pkt.assoc_id(); + let pkt_id = pkt.pkt_id(); + self.model + .recv_packet(pkt) + .map_or(Err(Error::InvalidUdpSession(assoc_id, pkt_id)), |pkt| { + Ok(Task::Packet(Packet::new(pkt, PacketSource::Quic(recv)))) + }) + } + Header::Dissociate(_) => Err(Error::BadCommandUniStream("dissociate", recv)), + Header::Heartbeat(_) => Err(Error::BadCommandUniStream("heartbeat", recv)), + _ => unreachable!(), + } + } + + /// Try to parse a pair of `quinn::SendStream` and `quinn::RecvStream` as a ASPORT command. + /// + /// The pair of stream should be accepted by `quinn::Connection::accept_bi()` from the same `quinn::Connection`. + pub async fn accept_bi_stream( + &self, + send: SendStream, + mut recv: RecvStream, + ) -> Result { + let header = match Header::async_unmarshal(&mut recv).await { + Ok(header) => header, + Err(err) => return Err(Error::UnmarshalBiStream(err, send, recv)), + }; + + match header { + Header::ClientHello(_) => Err(Error::BadCommandUniStream("clienthello", recv)), + Header::ServerHello(_) => Err(Error::BadCommandBiStream("serverhello", send, recv)), + Header::Connect(_) => Err(Error::BadCommandBiStream("connect", send, recv)), + Header::Packet(_) => Err(Error::BadCommandBiStream("packet", send, recv)), + Header::Dissociate(_) => Err(Error::BadCommandBiStream("dissociate", send, recv)), + Header::Heartbeat(_) => Err(Error::BadCommandBiStream("heartbeat", send, recv)), + _ => unreachable!(), + } + } + + /// Try to parse a QUIC Datagram as a ASPORT command. + /// + /// The Datagram should be accepted by `quinn::Connection::read_datagram()` from the same `quinn::Connection`. + pub fn accept_datagram(&self, dg: Bytes) -> Result { + let mut dg = Cursor::new(dg); + + let header = match Header::unmarshal(&mut dg) { + Ok(header) => header, + Err(err) => return Err(Error::UnmarshalDatagram(err, dg.into_inner())), + }; + + match header { + Header::ClientHello(_) => Err(Error::BadCommandDatagram("clienthello", dg.into_inner())), + Header::ServerHello(_) => Err(Error::BadCommandDatagram("serverhello", dg.into_inner())), + Header::Connect(_) => Err(Error::BadCommandDatagram("connect", dg.into_inner())), + Header::Packet(pkt) => { + let assoc_id = pkt.assoc_id(); + let pkt_id = pkt.pkt_id(); + if let Some(pkt) = self.model.recv_packet(pkt) { + let pos = dg.position() as usize; + let mut buf = dg.into_inner(); + if (pos + pkt.size() as usize) <= buf.len() { + buf = buf.slice(pos..pos + pkt.size() as usize); + Ok(Task::Packet(Packet::new(pkt, PacketSource::Native(buf)))) + } else { + Err(Error::PayloadLength(pkt.size() as usize, buf.len() - pos)) + } + } else { + Err(Error::InvalidUdpSession(assoc_id, pkt_id)) + } + } + Header::Dissociate(_) => Err(Error::BadCommandDatagram("dissociate", dg.into_inner())), + Header::Heartbeat(hb) => { + let _ = self.model.recv_heartbeat(hb); + Ok(Task::Heartbeat) + } + _ => unreachable!(), + } + } +} + +/// A received `ClientHello` command. +#[derive(Debug)] +pub struct ClientHello { + model: ClientHelloModel, + exporter: KeyingMaterialExporter, +} + +impl ClientHello { + fn new(model: ClientHelloModel, exporter: KeyingMaterialExporter) -> Self { + Self { model, exporter } + } + + /// The UUID of the client. + pub fn uuid(&self) -> Uuid { + self.model.uuid() + } + + /// The hashed token. + pub fn token(&self) -> [u8; 32] { + self.model.token() + } + + pub fn forward_mode(&self) -> ForwardMode { + self.model.forward_mode() + } + + pub fn expected_port_range(&self) -> RangeInclusive { + self.model.expected_port_range() + } + + /// Validates if the given password is matching the hashed token. + pub fn validate(&self, password: impl AsRef<[u8]>) -> bool { + self.model.is_valid(password, &self.exporter) + } +} + +/// A received `ServerHello` command. +#[derive(Debug)] +pub struct ServerHello { + model: ServerHelloModel, +} + +impl ServerHello { + fn new(model: ServerHelloModel) -> Self { + Self { model } + } + + /// The handshake code of the `ServerHello` command. + pub fn handshake_code(&self) -> u8 { + self.model.handshake_code() + } + + /// The port of the `ServerHello` command. + pub fn port(&self) -> Option { + self.model.port() + } +} + +/// A received `Connect` command. +pub struct Connect { + model: Side, ConnectModel>, + send: SendStream, + recv: RecvStream, +} + +impl Connect { + fn new( + model: Side, ConnectModel>, + send: SendStream, + recv: RecvStream, + ) -> Self { + Self { model, send, recv } + } + + /// Returns the `Connect` address + pub fn addr(&self) -> &Address { + match &self.model { + Side::Server(model) => { + let Header::Connect(conn) = model.header() else { unreachable!() }; + conn.addr() + } + Side::Client(model) => model.addr(), + } + } + + /// Immediately closes the `Connect` streams with the given error code. Returns the result of closing the send and receive streams, respectively. + pub fn reset( + &mut self, + error_code: VarInt, + ) -> (Result<(), ClosedStream>, Result<(), ClosedStream>) { + let send_res = self.send.reset(error_code); + let recv_res = self.recv.stop(error_code); + (send_res, recv_res) + } +} + +impl AsyncRead for Connect { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + AsyncRead::poll_read(Pin::new(&mut self.get_mut().recv), cx, buf) + } +} + +impl AsyncWrite for Connect { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + AsyncWrite::poll_write(Pin::new(&mut self.get_mut().send), cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_flush(Pin::new(&mut self.get_mut().send), cx) + } + + fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + AsyncWrite::poll_close(Pin::new(&mut self.get_mut().send), cx) + } +} + +impl Debug for Connect { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let model = match &self.model { + Side::Client(model) => model as &dyn Debug, + Side::Server(model) => model as &dyn Debug, + }; + + f.debug_struct("Connect") + .field("model", model) + .field("send", &self.send) + .field("recv", &self.recv) + .finish() + } +} + + +/// A received `Packet` command. +#[derive(Debug)] +pub struct Packet { + model: PacketModel, + src: PacketSource, +} + +#[derive(Debug)] +enum PacketSource { + Quic(RecvStream), + Native(Bytes), +} + +impl Packet { + fn new(model: PacketModel, src: PacketSource) -> Self { + Self { src, model } + } + + /// Returns the UDP session ID + pub fn assoc_id(&self) -> u16 { + self.model.assoc_id() + } + + /// Returns the packet ID + pub fn pkt_id(&self) -> u16 { + self.model.pkt_id() + } + + /// Returns the fragment ID + pub fn frag_id(&self) -> u8 { + self.model.frag_id() + } + + /// Returns the total number of fragments + pub fn frag_total(&self) -> u8 { + self.model.frag_total() + } + + /// Whether the packet is from UDP relay mode `quic` + pub fn is_from_quic(&self) -> bool { + matches!(self.src, PacketSource::Quic(_)) + } + + /// Whether the packet is from UDP relay mode `native` + pub fn is_from_native(&self) -> bool { + matches!(self.src, PacketSource::Native(_)) + } + + /// Accepts the packet payload. If the packet is fragmented and not yet fully assembled, `Ok(None)` is returned. + pub async fn accept(self) -> Result, Error> { + let pkt = match self.src { + PacketSource::Quic(mut recv) => { + let mut buf = vec![0; self.model.size() as usize]; + AsyncReadExt::read_exact(&mut recv, &mut buf).await?; + Bytes::from(buf) + } + PacketSource::Native(pkt) => pkt, + }; + + let mut asm = Vec::new(); + + Ok(self + .model + .assemble(pkt)? + .map(|pkt| pkt.assemble(&mut asm)) + .map(|(addr, assoc_id)| (Bytes::from(asm), addr, assoc_id))) + } +} + + +#[non_exhaustive] +#[derive(Debug)] +pub enum Task { + ClientHello(ClientHello), + ServerHello(ServerHello), + Connect(Connect), + Packet(Packet), + Dissociate(u16), + Heartbeat, +} + +#[derive(Debug)] +struct KeyingMaterialExporter(QuinnConnection); + +impl KeyingMaterialExporterImpl for KeyingMaterialExporter { + fn export_keying_material(&self, label: &[u8], context: &[u8]) -> [u8; 32] { + let mut buf = [0; 32]; + self.0 + .export_keying_material(&mut buf, label, context) + .unwrap(); + buf + } +} + + +/// Errors that can occur when processing a task. +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] IoError), + #[error(transparent)] + Connection(#[from] ConnectionError), + #[error(transparent)] + SendDatagram(#[from] SendDatagramError), + #[error("expecting payload length {0} but got {1}")] + PayloadLength(usize, usize), + #[error("packet {1:#06x} on invalid udp session {0:#06x}")] + InvalidUdpSession(u16, u16), + #[error(transparent)] + Assemble(#[from] AssembleError), + #[error("error unmarshalling uni_stream: {0}")] + UnmarshalUniStream(UnmarshalError, RecvStream), + #[error("error unmarshalling bi_stream: {0}")] + UnmarshalBiStream(UnmarshalError, SendStream, RecvStream), + #[error("error unmarshalling datagram: {0}")] + UnmarshalDatagram(UnmarshalError, Bytes), + #[error("bad command `{0}` from uni_stream")] + BadCommandUniStream(&'static str, RecvStream), + #[error("bad command `{0}` from bi_stream")] + BadCommandBiStream(&'static str, SendStream, RecvStream), + #[error("bad command `{0}` from datagram")] + BadCommandDatagram(&'static str, Bytes), +} \ No newline at end of file diff --git a/asport-server/Cargo.toml b/asport-server/Cargo.toml new file mode 100644 index 0000000..194fcf1 --- /dev/null +++ b/asport-server/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "asport-server" +version = "0.1.0" +authors = ["Kaede Akino "] +description = "A simple Asport server implementation." +categories = ["network-programming"] +keywords = ["network", "proxy", "reverse-proxy", "quic", "asport"] +edition = "2021" +readme = "README.md" +license = "GPL-3.0-or-later" +repository = "https://github.com/AkinoKaede/asport" + +[dependencies] +asport = { path = "../asport", version = "0.1.0" } +asport-quinn = { path = "../asport-quinn", version = "0.1.0" } +bimap = { version = "0.6.3", default-features = false, features = ["std"] } +bytes = { version = "1.6.1", default-features = false, features = ["std"] } +clap = { version = "4.5.8", default-features = false, features = ["color", "derive", "error-context", "help", "std", "suggestions", "usage"] } +config = { version = "0.14.0", default-features = false, features = ["async", "convert-case", "json", "json5", "ron", "toml", "yaml"] } +crossbeam-utils = { version = "0.8.20", default-features = false, features = ["std"] } +env_logger = { version = "0.10.2", default-features = false, features = ["auto-color", "humantime"] } +humantime = { version = "2.1.0", default-features = false } +log = { version = "0.4.22", default-features = false, features = ["serde", "std"] } +parking_lot = { version = "0.12.3", default-features = false, features = ["send_guard"] } +quinn = { version = "0.11.2", default-features = false, features = ["futures-io", "runtime-tokio", "rustls"] } +register-count = { version = "0.1.0", default-features = false, features = ["std"] } +rustls = { version = "0.23.11", default-features = false } +rustls-pemfile = { version = "2.1.2", default-features = false, features = ["std"] } +serde = { version = "1.0.204", default-features = false, features = ["derive", "std"] } +socket2 = { version = "0.5.7", default-features = false } +thiserror = { version = "1.0.62", default-features = false } +tokio = { version = "1.38.0", default-features = false, features = ["io-util", "macros", "net", "parking_lot", "rt-multi-thread", "time"] } +tokio-util = { version = "0.7.11", default-features = false, features = ["compat"] } +uuid = { version = "1.10.0", default-features = false, features = ["serde", "std"] } +xdg = { version = "2.5.2", default-features = false } + +[dev-dependencies] +serde_json = { version = "1.0.120", default-features = false, features = ["std"] } + +[target.'cfg(unix)'.dependencies] +xdg = { version = "2.5.2", default-features = false } + +[target.'cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "linux", target_os = "android"))'.dependencies] +sysctl = { version = "0.5.5", default-features = false } \ No newline at end of file diff --git a/asport-server/README.md b/asport-server/README.md new file mode 100644 index 0000000..2ad73bf --- /dev/null +++ b/asport-server/README.md @@ -0,0 +1,16 @@ +# asport-server + +A simple Asport server implementation. + +## Overview + +This crate provides a minimal Asport server implementation in Rust. It is designed to be a reference for the ASPORT protocol server implementation. + +## Quick Start + +Please refer to the [Quick Start](https://github.com/AkinoKaede/asport/blob/main/QUICK_START.md) guide. + +## License +This crate is licensed under [GNU General Public License v3.0 or later](https://github.com/AkinoKaede/asport/blob/main/LICENSE). + +SPDX-License-Identifier: [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) \ No newline at end of file diff --git a/asport-server/src/config.rs b/asport-server/src/config.rs new file mode 100644 index 0000000..046ac5c --- /dev/null +++ b/asport-server/src/config.rs @@ -0,0 +1,409 @@ +use std::{ + collections::{BTreeSet, HashMap}, fmt::Display, net::SocketAddr, ops::RangeInclusive, + path::PathBuf, str::FromStr, sync::OnceLock, time::Duration, +}; +use std::net::IpAddr; + +use humantime::Duration as HumanDuration; +use log::LevelFilter; +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use serde::{de::Error as DeError, Deserialize, Deserializer}; +use uuid::Uuid; + +use crate::utils::{CongestionControl, load_certs, load_priv_key, Network}; + +// TODO: need a better way to do this +static CONFIG_BASE_PATH: OnceLock = OnceLock::new(); + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Config { + pub server: SocketAddr, + + #[serde(deserialize_with = "deserialize_certs")] + pub certificate: Vec>, + + #[serde(deserialize_with = "deserialize_priv_key")] + pub private_key: PrivateKeyDer<'static>, + + pub proxies: Vec, + + #[serde( + default = "default::congestion_control", + deserialize_with = "deserialize_from_str" + )] + pub congestion_control: CongestionControl, + + #[serde( + default = "default::alpn", + deserialize_with = "deserialize_alpn" + )] + pub alpn: Vec>, + + #[serde(default = "default::zero_rtt_handshake")] + pub zero_rtt_handshake: bool, + + pub only_v6: Option, + + #[serde( + default = "default::handshake_timeout", + deserialize_with = "deserialize_duration" + )] + pub handshake_timeout: Duration, + + #[serde(default = "default::authentication_failed_reply")] + pub authentication_failed_reply: bool, + + #[serde( + default = "default::task_negotiation_timeout", + deserialize_with = "deserialize_duration" + )] + pub task_negotiation_timeout: Duration, + + #[serde( + default = "default::max_idle_time", + deserialize_with = "deserialize_duration" + )] + pub max_idle_time: Duration, + + #[serde(default = "default::max_packet_size")] + pub max_packet_size: usize, + + #[serde(default = "default::send_window")] + pub send_window: u64, + + #[serde(default = "default::receive_window")] + pub receive_window: u32, + + #[serde(default = "default::log_level")] + pub log_level: LevelFilter, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Proxy { + #[serde(deserialize_with = "deserialize_users")] + pub users: HashMap>, + + #[serde(default = "default::proxy::bind_ip")] + pub bind_ip: IpAddr, + + #[serde( + default = "default::proxy::allow_ports", + deserialize_with = "deserialize_ports" + )] + pub allow_ports: BTreeSet, + + pub only_v6: Option, + + #[serde( + default = "default::proxy::network", + deserialize_with = "deserialize_network" + )] + pub allow_network: Network, +} + +impl Config { + pub fn build(path: PathBuf) -> Result { + let base_path = path.parent(); + match base_path { + Some(base_path) => { + CONFIG_BASE_PATH.set(base_path.to_path_buf()).map_err( + |e| config::ConfigError::custom( + format!("failed to set config path: {:?}", e) + ) + )?; + } + None => { + return Err(config::ConfigError::custom("config path is not a file")); + } + } + + let cfg = config::Config::builder() + .add_source(config::File::from(path)) + .build()?; + + cfg.try_deserialize::() + } +} + +mod default { + use std::time::Duration; + + use log::LevelFilter; + + use crate::utils::CongestionControl; + + pub fn congestion_control() -> CongestionControl { + CongestionControl::Cubic + } + + pub fn alpn() -> Vec> { + vec![b"asport".to_vec()] + } + + pub fn zero_rtt_handshake() -> bool { + false + } + + pub fn handshake_timeout() -> Duration { + Duration::from_secs(3) + } + + pub fn authentication_failed_reply() -> bool { + return true + } + + pub fn task_negotiation_timeout() -> Duration { + Duration::from_secs(3) + } + + pub fn max_idle_time() -> Duration { + Duration::from_secs(10) + } + + pub fn max_packet_size() -> usize { + 1350 + } + + pub fn send_window() -> u64 { + 8 * 1024 * 1024 * 2 + } + + pub fn receive_window() -> u32 { + 8 * 1024 * 1024 + } + + pub fn log_level() -> LevelFilter { + LevelFilter::Warn + } + + + pub(crate) mod proxy { + use std::collections::BTreeSet; + use std::net::{IpAddr, Ipv6Addr}; + + use crate::utils::{ephemeral_port_range, Network}; + + pub fn bind_ip() -> IpAddr { + IpAddr::V6(Ipv6Addr::UNSPECIFIED) + } + + pub fn allow_ports() -> BTreeSet { + ephemeral_port_range().collect() + } + + pub fn network() -> Network { + Network::Both + } + } +} + +pub fn deserialize_alpn<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let s = Vec::::deserialize(deserializer)?; + Ok(s.into_iter().map(|alpn| alpn.into_bytes()).collect()) +} + +pub fn deserialize_users<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let users = HashMap::::deserialize(deserializer)?; + + if users.is_empty() { + return Err(DeError::custom("users cannot be empty")); + } + + Ok(users + .into_iter() + .map(|(k, v)| (k, v.into_bytes().into_boxed_slice())) + .collect()) +} + +pub fn deserialize_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + + s.parse::() + .map(|d| *d) + .map_err(DeError::custom) +} + + +pub fn deserialize_ports<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum PortRange { + Range(RangeInclusive), + Single(u16), + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum PortRanges { + Array(Vec), + Range(RangeInclusive), + Single(u16), + } + + let ranges = PortRanges::deserialize(deserializer)?; + + let mut set = BTreeSet::::new(); + + match ranges { + PortRanges::Array(array) => { + for range in array { + match range { + PortRange::Range(range) => { + set.extend(range); + } + PortRange::Single(port) => { + set.insert(port); + } + } + } + } + PortRanges::Range(range) => { + set.extend(range); + } + PortRanges::Single(port) => { + set.insert(port); + } + } + + Ok(set) +} + +pub fn deserialize_network<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum Middle { + #[serde(deserialize_with = "deserialize_from_str")] + Single(Network), + + Double(#[serde(deserialize_with = "deserialize_from_str")]Network, + #[serde(deserialize_with = "deserialize_from_str")]Network), + } + + let middle = Middle::deserialize(deserializer)?; + match middle { + Middle::Single(network) => Ok(network), + Middle::Double(a, b) => { + match (a, b) { + (Network::Tcp, Network::Udp) | (Network::Udp, Network::Tcp) | + (_, Network::Both) | (Network::Both, _) => Ok(Network::Both), + (Network::Tcp, Network::Tcp) => Ok(Network::Tcp), + (Network::Udp, Network::Udp) => Ok(Network::Udp), + } + } + } +} + +pub fn deserialize_certs<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + let path = PathBuf::deserialize(deserializer)?; + let path = CONFIG_BASE_PATH.get().unwrap().join(path); + + load_certs(path).map_err(DeError::custom) +} + +pub fn deserialize_priv_key<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let path = PathBuf::deserialize(deserializer)?; + let path = CONFIG_BASE_PATH.get().unwrap().join(path); + + load_priv_key(path).map_err(DeError::custom) +} + +pub fn deserialize_from_str<'de, T, D>(deserializer: D) -> Result +where + T: FromStr, + ::Err: Display, + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + T::from_str(&s).map_err(DeError::custom) +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_deserialize_ports() { + let s = r#"1"#; + let ports: BTreeSet = deserialize_ports(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(ports, vec![1].into_iter().collect()); + + let s = r#"{"start": 1, "end": 5}"#; + let ports: BTreeSet = deserialize_ports(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(ports, vec![1, 2, 3, 4, 5].into_iter().collect()); + + let s = r#"[1, 2, 3]"#; + let ports: BTreeSet = deserialize_ports(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(ports, vec![1, 2, 3].into_iter().collect()); + + let s = r#"[{"start": 1, "end": 5}, 8, {"start": 2, "end": 3}]"#; + let ports: BTreeSet = deserialize_ports(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(ports, vec![1, 2, 3, 4, 5, 8].into_iter().collect()); + + let s = r#"[]"#; + let ports: BTreeSet = deserialize_ports(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(ports, vec![].into_iter().collect()); + } + + #[test] + fn test_deserialize_network() { + let s = r#""tcp""#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Tcp); + + let s = r#""udp""#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Udp); + + let s = r#""tcpudp""#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Both); + + let s = r#""tcp_udp""#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Both); + + let s = r#""tcp-udp""#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Both); + + let s = r#""all""#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Both); + + let s = r#"["tcp", "tcp"]"#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Tcp); + + let s = r#"["tcp", "all"]"#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Both); + + let s = r#"["tcp", "udp"]"#; + let network: Network = deserialize_network(&mut serde_json::Deserializer::from_str(s)).unwrap(); + assert_eq!(network, Network::Both); + } +} \ No newline at end of file diff --git a/asport-server/src/connection/authenticated.rs b/asport-server/src/connection/authenticated.rs new file mode 100644 index 0000000..9021580 --- /dev/null +++ b/asport-server/src/connection/authenticated.rs @@ -0,0 +1,64 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + future::Future, + pin::Pin, + sync::Arc, + task::{Context, Poll, Waker}, +}; + +use crossbeam_utils::atomic::AtomicCell; +use parking_lot::Mutex; +use uuid::Uuid; + +struct AuthenticatedInner { + auth: AtomicCell>, + broadcast: Mutex>, +} + +#[derive(Clone)] +pub struct Authenticated(Arc); + +impl Authenticated { + pub fn new() -> Self { + Self(Arc::new(AuthenticatedInner { + auth: AtomicCell::new(None), + broadcast: Mutex::new(Vec::new()), + })) + } + + pub fn set(&self, uuid: Uuid, port: u16) { + self.0.auth.store(Some((uuid, port))); + + + for waker in self.0.broadcast.lock().drain(..) { + waker.wake(); + } + } + + pub fn get(&self) -> Option<(Uuid, u16)> { + self.0.auth.load() + } +} + +impl Future for Authenticated { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.get().is_some() { + Poll::Ready(()) + } else { + self.0.broadcast.lock().push(cx.waker().clone()); + Poll::Pending + } + } +} + +impl Display for Authenticated { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + if let Some((uuid, port)) = self.get() { + write!(f, "{uuid} {port}") + } else { + write!(f, "unauthenticated") + } + } +} diff --git a/asport-server/src/connection/handle_bind.rs b/asport-server/src/connection/handle_bind.rs new file mode 100644 index 0000000..7be0c86 --- /dev/null +++ b/asport-server/src/connection/handle_bind.rs @@ -0,0 +1,148 @@ +use std::{ + cell::LazyCell, + collections::BTreeSet, + net::{IpAddr, SocketAddr}, +}; + +use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use tokio::{ + io::Result as IoResult, + net::{TcpListener, UdpSocket}, +}; + +use crate::{ + error::Error, + utils::{ephemeral_port_range, Network}, +}; + +use super::{Connection, udp_sessions::UdpSessions}; + +const EPHEMERAL_PORTS: LazyCell> = LazyCell::new(|| { + ephemeral_port_range().collect() +}); + +impl Connection { + async fn bind_tcp(&self, bind_ip: IpAddr, port: u16, only_v6: Option) -> IoResult { + let domain = match bind_ip { + IpAddr::V4(_) => Domain::IPV4, + IpAddr::V6(_) => Domain::IPV6, + }; + + let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; + + if let Some(only_v6) = only_v6 { + socket.set_only_v6(only_v6)?; + } + + socket.set_nonblocking(true)?; + + socket.bind(&SockAddr::from( + SocketAddr::new(bind_ip, port) + ))?; + + // backlog is the queue length for pending connections + socket.listen(128)?; + + TcpListener::from_std(socket.into()) + } + + async fn bind_udp(&self, bind_ip: IpAddr, port: u16, only_v6: Option) -> IoResult { + let domain = match bind_ip { + IpAddr::V4(_) => Domain::IPV4, + IpAddr::V6(_) => Domain::IPV6, + }; + + let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP))?; + + if let Some(only_v6) = only_v6 { + socket.set_only_v6(only_v6)?; + } + + socket.set_nonblocking(true)?; + + socket.bind(&SockAddr::from( + SocketAddr::new(bind_ip, port) + ))?; + + UdpSocket::from_std(socket.into()) + } + + pub(crate) async fn bind(&self, + bind_ip: IpAddr, + bind_ports: BTreeSet, + only_v6: Option, + network: &Network) -> Result { + let ports = { + match network { + Network::Tcp | Network::Udp => { + match bind_ports == *EPHEMERAL_PORTS { + // workaround for ephemeral ports, make it chosen by OS. + true => vec![0], + false => bind_ports.into_iter().collect(), + } + } + Network::Both => bind_ports.into_iter().collect(), + } + }; + + // NOTICE: const_btree_len is still unstable, so we have to use this workaround. + if ports.is_empty() { + return Err(Error::PortDenied); + } + + for port in ports { + let tcp_listener = async { + if network.tcp() { + return Some(self.bind_tcp(bind_ip, port, only_v6).await); + } + + None + }.await; + + let udp_socket = async { + if network.udp() { + return Some(self.bind_udp(bind_ip, port, only_v6).await); + } + + None + }.await; + + match (tcp_listener, udp_socket) { + (Some(Ok(tcp_listener)), Some(Ok(udp_socket))) => { + self.clone().handle_tcp_listener(tcp_listener).await; + let mut udp_sessions = self.udp_sessions.lock().await; + *udp_sessions = Some(UdpSessions::new( + self.clone(), + udp_socket, + self.max_packet_size, + )); + + return Ok(port); + } + + (Some(Ok(tcp_listener)), None) => { + let port = tcp_listener.local_addr().unwrap().port(); // Get actual port when port is 0 + self.clone().handle_tcp_listener(tcp_listener).await; + return Ok(port); + } + + (None, Some(Ok(udp_socket))) => { + let port = udp_socket.local_addr().unwrap().port(); // Get actual port when port is 0 + let mut udp_sessions = self.udp_sessions.lock().await; + *udp_sessions = Some(UdpSessions::new( + self.clone(), + udp_socket, + self.max_packet_size, + )); + + return Ok(port); + } + + _ => {} + } + } + + + Err(Error::BindFailed) + } +} \ No newline at end of file diff --git a/asport-server/src/connection/handle_stream.rs b/asport-server/src/connection/handle_stream.rs new file mode 100644 index 0000000..b78ac6e --- /dev/null +++ b/asport-server/src/connection/handle_stream.rs @@ -0,0 +1,153 @@ +use std::sync::atomic::Ordering; + +use bytes::Bytes; +use quinn::{RecvStream, SendStream, VarInt}; +use register_count::Register; +use tokio::time; + +use asport_quinn::Task; + +use crate::error::Error; + +use super::Connection; + +impl Connection { + pub async fn handle_uni_stream(self, recv: RecvStream, _reg: Register) { + log::debug!( + "[{id:#010x}] [{addr}] [{user}] incoming unidirectional stream", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + + let max = self.max_concurrent_uni_streams.load(Ordering::Relaxed); + + if self.remote_uni_stream_cnt.count() as u32 == max { + self.max_concurrent_uni_streams + .store(max * 2, Ordering::Relaxed); + + self.inner + .set_max_concurrent_uni_streams(VarInt::from(max * 2)); + } + + let pre_process = async { + let task = time::timeout( + self.task_negotiation_timeout, + self.model.accept_uni_stream(recv), + ) + .await + .map_err(|_| Error::TaskNegotiationTimeout)??; + + + if let Task::ClientHello(client_hello) = &task { + self.handshake(client_hello).await?; + }; + + tokio::select! { + () = self.auth.clone() => {} + err = self.inner.closed() => return Err(Error::from(err)), + }; + + Ok(task) + }; + + match pre_process.await { + Ok(Task::ClientHello(client_hello)) => self.handle_client_hello(client_hello).await, + Ok(Task::Packet(pkt)) => self.handle_packet(pkt).await, + Ok(_) => unreachable!(), + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] handling incoming unidirectional stream error: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + self.close(); + } + } + } + + pub async fn handle_bi_stream(self, (send, recv): (SendStream, RecvStream), _reg: Register) { + log::debug!( + "[{id:#010x}] [{addr}] [{user}] incoming bidirectional stream", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + + let max = self.max_concurrent_bi_streams.load(Ordering::Relaxed); + + if self.remote_bi_stream_cnt.count() as u32 == max { + self.max_concurrent_bi_streams + .store(max * 2, Ordering::Relaxed); + + self.inner + .set_max_concurrent_bi_streams(VarInt::from(max * 2)); + } + + let pre_process = async { + let task = time::timeout( + self.task_negotiation_timeout, + self.model.accept_bi_stream(send, recv), + ) + .await + .map_err(|_| Error::TaskNegotiationTimeout)??; + + tokio::select! { + () = self.auth.clone() => {} + err = self.inner.closed() => return Err(Error::from(err)), + }; + + Ok(task) + }; + + match pre_process.await { + Ok(_) => unreachable!(), + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] handling incoming bidirectional stream error: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + self.close(); + } + } + } + + + pub async fn handle_datagram(self, dg: Bytes) { + log::debug!( + "[{id:#010x}] [{addr}] [{user}] incoming datagram", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + + let pre_process = async { + let task = self.model.accept_datagram(dg)?; + + tokio::select! { + () = self.auth.clone() => {} + err = self.inner.closed() => return Err(Error::from(err)), + }; + + Ok(task) + }; + + match pre_process.await { + Ok(Task::Heartbeat) => self.handle_heartbeat().await, + Ok(Task::Packet(pkt)) => self.handle_packet(pkt).await, + Ok(_) => unreachable!(), + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] handling incoming datagram error: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + self.close(); + } + } + } +} \ No newline at end of file diff --git a/asport-server/src/connection/handle_task.rs b/asport-server/src/connection/handle_task.rs new file mode 100644 index 0000000..b9910ef --- /dev/null +++ b/asport-server/src/connection/handle_task.rs @@ -0,0 +1,250 @@ +use std::{ + io::{Error as IoError, ErrorKind}, + net::SocketAddr, +}; + +use bytes::Bytes; +use tokio::{ + io::{self, AsyncWriteExt}, + net::TcpStream, +}; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +use asport::{Address, ServerHello}; +use asport_quinn::{ClientHello, Connect, Packet}; + +use crate::error::Error; +use crate::utils::UdpForwardMode; + +use super::{Connection, ERROR_CODE}; + +impl Connection { + pub async fn server_hello(&self, server_hello: ServerHello) { + match self.model.server_hello(server_hello).await { + Ok(()) => log::info!( + "[{id:#010x}] [{addr}] [{user}] [server_hello]", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ), + Err(err) => log::warn!( + "[{id:#010x}] [{addr}] [{user}] [server_hello] sending server hello error: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ), + } + } + + pub async fn connect(&self, addr: SocketAddr) -> Result { + let addr_display = addr.to_string(); + + // IPv4-mapped IPv6 convert to IPv4 + /* + let addr = match addr { + original_addr @ SocketAddr::V4(_) => original_addr, + original_addr @ SocketAddr::V6(v6_addr) => { + match v6_addr.ip().to_ipv4_mapped() { + Some(ipv4) => SocketAddr::V4(SocketAddrV4::new(ipv4, original_addr.port())), + None => original_addr, + } + }, + }; + */ + + match self.model.connect(Address::SocketAddress(addr)).await { + Ok(conn) => Ok(conn), + Err(err) => { + log::warn!("[connect] failed initializing forwarding from {addr_display}: {err}"); + Err(Error::Model(err)) + } + } + } + + pub async fn handle_packet(&self, pkt: Packet) { + let assoc_id = pkt.assoc_id(); + let pkt_id = pkt.pkt_id(); + let frag_id = pkt.frag_id(); + let frag_total = pkt.frag_total(); + + let mode = self.udp_forward_mode.load().unwrap(); + + log::info!( + "[{id:#010x}] [{addr}] [{user}] [packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] fragment {frag_id}/{frag_total}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + frag_id = frag_id + 1, + ); + + let (pkt, addr, assoc_id) = match pkt.accept().await { + Ok(None) => return, + Ok(Some(res)) => res, + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] [packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] fragment {frag_id}/{frag_total}: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + frag_id = frag_id + 1, + ); + return; + } + }; + + let process = async { + log::info!( + "[{id:#010x}] [{addr}] [{user}] [packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] to {dst_addr}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + dst_addr = addr, + ); + + + let sessions = match self.udp_sessions.lock().await.clone() { + Some(sessions) => sessions, + None => { return Err(Error::from(IoError::new(ErrorKind::NotFound, "no UDP sessions"))); } + }; + + let socket_addr = match addr { + Address::None => { + return Err(Error::from(IoError::new(ErrorKind::NotFound, "no address"))); + } + Address::SocketAddress(addr) => addr, + }; + + // Validate destination address + // Because client can send packet with any address, we need to validate it. + // If not, it can be used for proxy. + if !self.udp_sessions.lock().await.clone().unwrap().validate(assoc_id, socket_addr) { // unwrap() is safe because of it's checked. + return Err(Error::from(IoError::new(ErrorKind::InvalidInput, "destination address is not valid"))); + } + + sessions.send_to(pkt, socket_addr).await + }; + + if let Err(err) = process.await { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] [packet] [{assoc_id:#06x}] [from-{mode}] [{pkt_id:#06x}] to {dst_addr}: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + dst_addr = addr, + ); + } + } + + + pub async fn dissociate(&self, assoc_id: u16) -> Result<(), Error> { + log::info!( + "[{id:#010x}] [{addr}] [{auth}] [dissociate] [{assoc_id:#06x}]", + id = self.id(), + addr = self.inner.remote_address(), + auth = self.auth, + ); + match self.model.dissociate(assoc_id).await { + Ok(()) => Ok(()), + Err(err) => { + log::warn!("[{id:#010x}] [{addr}] [{auth}] [dissociate] [{assoc_id:#06x}] {err}", + id = self.id(), + addr = self.inner.remote_address(), + auth = self.auth, + ); + Err(Error::Model(err)) + } + } + } + + pub async fn handle_client_hello(&self, client_hello: ClientHello) { + log::info!( + "[{id:#010x}] [{addr}] [{auth}] [client_hello] {auth_uuid}", + id = self.id(), + addr = self.inner.remote_address(), + auth = self.auth, + auth_uuid = client_hello.uuid(), + ); + } + + pub async fn handle_heartbeat(&self) { + log::info!( + "[{id:#010x}] [{addr}] [{user}] [heartbeat]", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + ); + } + + pub async fn forward_tcp(self, (mut stream, addr): (TcpStream, SocketAddr)) { + match self.connect(addr).await { + Ok(conn) => { + log::info!( + "[{id:#010x}] [{addr}] [{user}] [connect]", + id = self.id(), + addr = addr, + user = self.auth, + ); + + let mut conn = conn.compat(); + match io::copy_bidirectional(&mut conn, &mut stream).await { + Ok(_) => {} + Err(err) => { + let _ = stream.shutdown().await; + let _ = conn.get_mut().reset(ERROR_CODE); + log::warn!( + "[{id:#010x}] [{addr}] [{user}] [connect] TCP stream forwarding error: {err}", + id = self.id(), + addr = addr, + user = self.auth, + err = err, + ); + } + } + } + + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] [connect] unable to forward: {err}", + id = self.id(), + addr = addr, + user = self.auth, + err = err, + ); + return; + } + }; + } + + pub async fn forward_packet(self, pkt: Bytes, addr: Address, assoc_id: u16, dissociate_before_forward: bool) { + if dissociate_before_forward { + let _ = self.dissociate(assoc_id).await; + } + + let addr_display = addr.to_string(); + + let udp_forward_mode = self.udp_forward_mode.load().unwrap(); + + log::info!( + "[{id:#010x}] [{addr}] [{user}] [packet] [{assoc_id:#06x}] [to-{udp_forward_mode}] to {src_addr}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + src_addr = addr_display, + ); + + let res = match udp_forward_mode { + UdpForwardMode::Native => self.model.packet_native(pkt, addr, assoc_id), + UdpForwardMode::Quic => self.model.packet_quic(pkt, addr, assoc_id).await, + }; + + if let Err(err) = res { + log::warn!( + "[{id:#010x}] [{addr}] [{user}] [packet] [{assoc_id:#06x}] [to-{udp_forward_mode}] to {src_addr}: {err}", + id = self.id(), + addr = self.inner.remote_address(), + user = self.auth, + src_addr = addr_display, + ); + } + } +} \ No newline at end of file diff --git a/asport-server/src/connection/mod.rs b/asport-server/src/connection/mod.rs new file mode 100644 index 0000000..92abbe1 --- /dev/null +++ b/asport-server/src/connection/mod.rs @@ -0,0 +1,284 @@ +use std::{ + collections::HashMap, + sync::{ + Arc, + atomic::AtomicU32, + }, + time::Duration, +}; + +use crossbeam_utils::atomic::AtomicCell; +use quinn::{Connecting, Connection as QuinnConnection, VarInt}; +use register_count::Counter; +use tokio::{ + net::TcpListener, + sync::Mutex, + time, +}; +use uuid::Uuid; + +use asport::{ForwardMode, ServerHello}; +use asport_quinn::{ClientHello, Connection as Model, side}; + +use crate::{ + error::Error, + utils::{self, Network, NetworkUdpForwardModeCombine, UdpForwardMode, User}, +}; + +use self::{ + authenticated::Authenticated, + udp_sessions::UdpSessions, +}; + +mod authenticated; +mod handle_stream; +mod handle_task; +mod handle_bind; +mod udp_sessions; + +pub const ERROR_CODE: VarInt = VarInt::from_u32(0); +pub const DEFAULT_CONCURRENT_STREAMS: u32 = 32; + +#[derive(Clone)] +pub struct Connection { + inner: QuinnConnection, + model: Model, + users: Arc>, + auth: Authenticated, + udp_forward_mode: Arc>>, + + udp_sessions: Arc>>, + + task_negotiation_timeout: Duration, + authentication_failed_reply: bool, + max_packet_size: usize, + + remote_uni_stream_cnt: Counter, + remote_bi_stream_cnt: Counter, + max_concurrent_uni_streams: Arc, + max_concurrent_bi_streams: Arc, +} + +impl Connection { + fn new( + conn: QuinnConnection, + users: Arc>, + task_negotiation_timeout: Duration, + authentication_failed_reply: bool, + max_packet_size: usize, + ) -> Self { + Self { + inner: conn.clone(), + model: Model::::new(conn), + users, + auth: Authenticated::new(), + udp_forward_mode: Arc::new(AtomicCell::new(None)), + udp_sessions: Arc::new(Mutex::new(None)), + task_negotiation_timeout, + authentication_failed_reply, + max_packet_size, + remote_uni_stream_cnt: Counter::new(), + remote_bi_stream_cnt: Counter::new(), + max_concurrent_uni_streams: Arc::new(AtomicU32::new(DEFAULT_CONCURRENT_STREAMS)), + max_concurrent_bi_streams: Arc::new(AtomicU32::new(DEFAULT_CONCURRENT_STREAMS)), + } + } + + pub async fn handle( + conn: Connecting, + users: Arc>, + zero_rtt_handshake: bool, + auth_timeout: Duration, + task_negotiation_timeout: Duration, + authentication_failed_reply: bool, + max_packet_size: usize, + ) { + let addr = conn.remote_address(); + + let init = async { + let conn = if zero_rtt_handshake { + match conn.into_0rtt() { + Ok((conn, _)) => conn, + Err(conn) => conn.await?, + } + } else { + conn.await? + }; + + Ok::<_, Error>(Self::new( + conn, + users, + task_negotiation_timeout, + authentication_failed_reply, + max_packet_size, + )) + }; + + match init.await { + Ok(conn) => { + log::info!( + "[{id:#010x}] [{addr}] [{user}] connection established", + id = conn.id(), + user = conn.auth, + ); + + tokio::spawn(conn.clone().timeout_handshake(auth_timeout)); + tokio::spawn(conn.clone().release_udp_sessions()); + + loop { + if conn.is_closed() { + break; + } + + let handle_incoming = async { + tokio::select! { + res = conn.inner.accept_uni() => + tokio::spawn(conn.clone().handle_uni_stream(res?, conn.remote_uni_stream_cnt.reg())), + res = conn.inner.accept_bi() => + tokio::spawn(conn.clone().handle_bi_stream(res?, conn.remote_bi_stream_cnt.reg())), + res = conn.inner.read_datagram() => + tokio::spawn(conn.clone().handle_datagram(res?)), + }; + + Ok::<_, Error>(()) + }; + + match handle_incoming.await { + Ok(()) => {} + Err(err) if err.is_trivial() => { + log::debug!( + "[{id:#010x}] [{addr}] [{user}] {err}", + id = conn.id(), + user = conn.auth, + ); + } + Err(err) => log::warn!( + "[{id:#010x}] [{addr}] [{user}] connection error: {err}", + id = conn.id(), + user = conn.auth, + ), + } + } + } + Err(err) if err.is_trivial() => { + log::debug!( + "[{id:#010x}] [{addr}] [unauthenticated] {err}", + id = u32::MAX, + ); + } + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [unauthenticated] {err}", + id = u32::MAX, + ) + } + } + } + + async fn handshake(&self, hello: &ClientHello) -> Result<(), Error> { + if self.auth.get().is_some() { + return Err(Error::DuplicatedHello); + } + + let (network, udp_forward_mode) = + >::into(hello.forward_mode()) + .into(); + self.udp_forward_mode.store(Some(udp_forward_mode)); + + let res = self.process_handshake(hello, network).await; + + match res { + Ok(listen_port) => { + self.server_hello(ServerHello::Success(listen_port)).await; + self.auth.set(hello.uuid(), listen_port); + } + Err(Error::AuthFailed(_)) => if self.authentication_failed_reply { + self.server_hello(ServerHello::AuthFailed).await; + }, + Err(Error::BindFailed) => self.server_hello(ServerHello::BindFailed).await, + Err(Error::PortDenied) => self.server_hello(ServerHello::PortDenied).await, + Err(Error::NetworkDenied(_)) => self.server_hello(ServerHello::NetworkDenied).await, + _ => unreachable!(), + }; + + res.map(|_| ()) + } + + async fn process_handshake(&self, hello: &ClientHello, network: Network) -> Result { + let user = self.users.get(&hello.uuid()).ok_or(Error::AuthFailed(hello.uuid()))?; + if !hello.validate(user.password()) { + return Err(Error::AuthFailed(hello.uuid())); + } + + let range = hello.expected_port_range(); + + let ports = user.allow_ports().into_iter() + .filter(|port| range.contains(port)) + .collect(); + + let network = utils::merge_network(user.allow_network().clone(), network)?; + + + self.bind(user.listen_ip(), ports, + user.only_v6(), &network).await + } + + async fn handle_tcp_listener(self, listener: TcpListener) { + tokio::spawn(async move { + // Prevent send `Connect` before `ServerHello` + // If not, can cause client to close connection, and it can be used for DoS attack + self.auth.clone().await; + + loop { + tokio::select! { + res = listener.accept() => { + tokio::spawn(self.clone().forward_tcp(res?)); + }, + _ = self.closed() => { + return Ok::<(), Error>(()); // Connection closed + } + }; + } + }); + } + + async fn release_udp_sessions(self) { + self.closed().await; + + let mut udp_sessions = self.udp_sessions.lock().await; + if let Some(udp_sessions) = udp_sessions.as_mut() { + udp_sessions.close(); + } + + *udp_sessions = None; + } + + async fn timeout_handshake(self, timeout: Duration) { + time::sleep(timeout).await; + + if self.auth.get().is_none() { + log::warn!( + "[{id:#010x}] [{addr}] [unauthenticated] [authenticate] timeout", + id = self.id(), + addr = self.inner.remote_address(), + ); + self.close(); + } + } + + async fn closed(&self) { + self.inner.closed().await; + } + + fn id(&self) -> u32 { + self.inner.stable_id() as u32 + } + + fn is_closed(&self) -> bool { + self.inner.close_reason().is_some() + } + + fn close(&self) { + self.inner.close(ERROR_CODE, &[]); + } +} \ No newline at end of file diff --git a/asport-server/src/connection/udp_sessions.rs b/asport-server/src/connection/udp_sessions.rs new file mode 100644 index 0000000..e5d3007 --- /dev/null +++ b/asport-server/src/connection/udp_sessions.rs @@ -0,0 +1,138 @@ +use std::{ + io::Error as IoError, + net::{IpAddr, SocketAddr}, + sync::{ + Arc, + atomic::{AtomicU16, Ordering}, + }, +}; + +use bytes::Bytes; +use parking_lot::Mutex; +use tokio::{ + net::UdpSocket, + sync::oneshot::{self, Sender}, +}; + +use asport::Address; + +use crate::error::Error; + +use super::Connection; + +#[derive(Clone)] +pub struct UdpSessions(Arc); + +struct UdpSessionsInner { + conn: Connection, + socket: UdpSocket, + local_addr: SocketAddr, + max_pkt_size: usize, + assoc_id_addr_map: Arc>>, + close: Mutex>>, +} + +impl UdpSessions { + pub fn new(conn: Connection, socket: UdpSocket, max_pkt_size: usize) -> Self { + let (tx, rx) = oneshot::channel(); + let assoc_id_addr_map = Arc::new(Mutex::new(bimap::BiMap::new())); + let assoc_id_addr_map_listening = assoc_id_addr_map.clone(); + + let local_addr = socket.local_addr().unwrap(); + + let sessions = Self(Arc::new(UdpSessionsInner { + conn, + socket, + local_addr, + max_pkt_size, + assoc_id_addr_map, + close: Mutex::new(Some(tx)), + })); + + let session_listening = sessions.clone(); + + + let listen = async move { + // Prevent send `Packet` before `ServerHello` + // If not, can cause client to close connection, and it can be used for DoS attack + session_listening.0.conn.auth.clone().await; + + let next_assoc_id = AtomicU16::new(0); + + loop { + let (pkt, addr) = match session_listening.recv_from().await { + Ok(res) => res, + Err(err) => { + log::warn!( + "[{id:#010x}] [{addr}] [{auth}] [packet] outbound listening error: {err}", + id = session_listening.0.conn.id(), + addr = session_listening.0.conn.inner.remote_address(), + auth = session_listening.0.conn.auth, + ); + continue; + } + }; + + let mut dissociate_before_forward = false; + let mut lock = assoc_id_addr_map_listening.lock(); + let assoc_id = match lock.get_by_right(&addr) { + Some(assoc_id) => *assoc_id, + None => { + let assoc_id = next_assoc_id.fetch_add(1, Ordering::Relaxed); + + if let Some(_) = lock.remove_by_left(&assoc_id) { + dissociate_before_forward = true; + } + + assoc_id + } + }; + + lock.insert(assoc_id, addr); + + tokio::spawn(session_listening.0.conn.clone().forward_packet( + pkt, + Address::SocketAddress(addr), + assoc_id, + dissociate_before_forward, + )); + } + }; + + tokio::spawn(async move { + tokio::select! { + _ = listen => unreachable!(), + _ = rx => {}, + } + }); + + sessions + } + + + pub async fn send_to(&self, pkt: Bytes, addr: SocketAddr) -> Result<(), Error> { + // map ipv4 to ipv6-mapped-ipv4 + let addr = match (addr, self.0.local_addr) { + (SocketAddr::V4(v4), SocketAddr::V6(_)) => SocketAddr::new(IpAddr::from(v4.ip().to_ipv6_mapped()), v4.port()), + (addr, _) => addr, + }; + + self.0.socket.send_to(&pkt, addr).await?; + Ok(()) + } + + async fn recv_from(&self) -> Result<(Bytes, SocketAddr), IoError> { + let mut buf = vec![0u8; self.0.max_pkt_size]; + let (n, addr) = self.0.socket.recv_from(&mut buf).await?; + buf.truncate(n); + Ok((Bytes::from(buf), addr)) + } + + pub fn validate(&self, assoc_id: u16, socket_addr: SocketAddr) -> bool { + matches!(self.0.assoc_id_addr_map.lock().get_by_left(&assoc_id), Some(addr) if *addr == socket_addr) + } + + pub fn close(&self) { + let _ = self.0.close.lock().take().unwrap().send(()); + } +} \ No newline at end of file diff --git a/asport-server/src/error.rs b/asport-server/src/error.rs new file mode 100644 index 0000000..e477b2d --- /dev/null +++ b/asport-server/src/error.rs @@ -0,0 +1,59 @@ +use std::io::Error as IoError; + +use quinn::{ConnectionError, crypto::rustls::NoInitialCipherSuite}; +use rustls::Error as RustlsError; +use thiserror::Error; +use uuid::Uuid; + +use asport_quinn::Error as ModelError; +use crate::utils::Network; + +#[derive(Debug, Error)] +pub enum Error { + #[error(transparent)] + Io(#[from] IoError), + #[error(transparent)] + Rustls(#[from] RustlsError), + #[error(transparent)] + NoInitialCipherSuite(#[from] NoInitialCipherSuite), + #[error("invalid max idle time")] + InvalidMaxIdleTime, + #[error("connection timed out")] + TimedOut, + #[error("connection locally closed")] + LocallyClosed, + #[error(transparent)] + Model(#[from] ModelError), + #[error("duplicated authentication")] + DuplicatedHello, + #[error("authentication failed: {0}")] + AuthFailed(Uuid), + #[error("bind failed")] + BindFailed, + #[error("network denied: {0}")] + NetworkDenied(Network), + #[error("port denied")] + PortDenied, + #[error("{0}: {1}")] + Socket(&'static str, IoError), + #[error("task negotiation timed out")] + TaskNegotiationTimeout, + #[error("invalid private key: {0}")] + InvalidPrivateKey(&'static str), +} + +impl Error { + pub fn is_trivial(&self) -> bool { + matches!(self, Self::TimedOut | Self::LocallyClosed) + } +} + +impl From for Error { + fn from(err: ConnectionError) -> Self { + match err { + ConnectionError::TimedOut => Self::TimedOut, + ConnectionError::LocallyClosed => Self::LocallyClosed, + _ => Self::Io(IoError::from(err)), + } + } +} diff --git a/asport-server/src/main.rs b/asport-server/src/main.rs new file mode 100644 index 0000000..292d3ff --- /dev/null +++ b/asport-server/src/main.rs @@ -0,0 +1,116 @@ +/* +* Asport, a quick and secure reverse proxy based on QUIC for NAT traversal. +* Copyright (C) 2024 Kaede Akino +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +use std::{ + cell::LazyCell, + path::PathBuf, + process, +}; + +use clap::Parser; +use env_logger::Builder as LoggerBuilder; + +use crate::server::Server; + +mod config; +mod utils; +mod server; +mod error; +mod connection; + +#[derive(Parser)] +#[command(about, author, version)] +struct Arguments { + #[clap(short, long)] + config: Option, +} + +#[tokio::main] +async fn main() { + let args = Arguments::parse(); + + let config_path = args.config.unwrap_or_else(|| { + if let Some(path) = find_config() { + path + } else { + eprintln!("No configuration file found, please specify one with --config"); + process::exit(1); + } + }); + + let cfg = match config::Config::build(config_path) { + Ok(cfg) => cfg, + Err(err) => { + eprintln!("{err}"); + process::exit(1); + } + }; + + LoggerBuilder::new() + .filter_level(cfg.log_level) + .format_module_path(false) + .format_target(false) + .init(); + + match Server::init(cfg) { + Ok(server) => server.start().await, + Err(err) => { + eprintln!("{err}"); + process::exit(1); + } + } +} +const CONFIG_EXTENSIONS: [&str; 6] = ["json", "jsonc", "ron", "toml", "yaml", "yml"]; +const CONFIG_NAMES: LazyCell> = LazyCell::new(|| { + CONFIG_EXTENSIONS.iter() + .map(|ext| PathBuf::from(format!("server.{}", ext))).collect::>() +}); + +#[cfg(unix)] +fn find_config() -> Option { + for config in CONFIG_NAMES.iter() { + if config.exists() { + return Some(config.clone()); + } + } + + let xdg_dirs = if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("asport") { + xdg_dirs + } else { + return None; + }; + + for config in CONFIG_NAMES.iter() { + if let Some(path) = xdg_dirs.find_config_file(config) { + return Some(path); + } + } + + None +} + +#[cfg(not(unix))] +fn find_config() -> Option { + for config in CONFIG_NAMES.iter() { + if config.exists() { + return Some(config.clone()); + } + } + + None +} \ No newline at end of file diff --git a/asport-server/src/server.rs b/asport-server/src/server.rs new file mode 100644 index 0000000..22c6e37 --- /dev/null +++ b/asport-server/src/server.rs @@ -0,0 +1,151 @@ +use std::{ + collections::HashMap, + net::{SocketAddr, UdpSocket as StdUdpSocket}, + sync::Arc, + time::Duration, +}; + +use quinn::{congestion::{BbrConfig, CubicConfig, NewRenoConfig}, Endpoint, EndpointConfig, + IdleTimeout, ServerConfig, TokioRuntime, TransportConfig, VarInt}; +use quinn::crypto::rustls::QuicServerConfig; +use rustls::ServerConfig as RustlsServerConfig; +use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use uuid::Uuid; + +use crate::{ + config::Config, + connection::{Connection, DEFAULT_CONCURRENT_STREAMS}, + error::Error, + utils::{CongestionControl, User}, +}; + +pub struct Server { + ep: Endpoint, + users: Arc>, + zero_rtt_handshake: bool, + hello_timeout: Duration, + task_negotiation_timeout: Duration, + authentication_failed_reply: bool, + max_packet_size: usize, +} + +impl Server { + pub fn init(cfg: Config) -> Result { + // CryptoProvider::get_default(); + let mut crypto = RustlsServerConfig::builder() + .with_no_client_auth() + .with_single_cert(cfg.certificate, cfg.private_key)?; + + crypto.alpn_protocols = cfg.alpn; + crypto.max_early_data_size = u32::MAX; + crypto.send_half_rtt_data = cfg.zero_rtt_handshake; + + + let mut config = ServerConfig::with_crypto(Arc::new( + QuicServerConfig::try_from(crypto)?)); + let mut tp_cfg = TransportConfig::default(); + + tp_cfg + .max_concurrent_bidi_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS)) + .max_concurrent_uni_streams(VarInt::from(DEFAULT_CONCURRENT_STREAMS)) + .send_window(cfg.send_window) + .stream_receive_window(VarInt::from_u32(cfg.receive_window)) + .max_idle_timeout(Some( + IdleTimeout::try_from(cfg.max_idle_time).map_err(|_| Error::InvalidMaxIdleTime)?, + )); + + match cfg.congestion_control { + CongestionControl::Cubic => { + tp_cfg.congestion_controller_factory(Arc::new(CubicConfig::default())) + } + CongestionControl::NewReno => { + tp_cfg.congestion_controller_factory(Arc::new(NewRenoConfig::default())) + } + CongestionControl::Bbr => { + tp_cfg.congestion_controller_factory(Arc::new(BbrConfig::default())) + } + }; + + config.transport_config(Arc::new(tp_cfg)); + + let mut users = HashMap::new(); + for proxy in cfg.proxies { + for (uuid, password) in proxy.users { + users.insert(uuid, User::new(password, proxy.bind_ip, proxy.allow_ports.clone(), + proxy.only_v6, proxy.allow_network)); + } + } + + + let socket = { + let domain = match cfg.server { + SocketAddr::V4(_) => Domain::IPV4, + SocketAddr::V6(_) => Domain::IPV6, + }; + + let socket = Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)) + .map_err(|err| Error::Socket("failed to create endpoint UDP socket", err))?; + + + if let Some(only_v6) = cfg.only_v6 { + socket.set_only_v6(only_v6).map_err(|err| { + Error::Socket("endpoint dual-stack socket setting error", err) + })?; + } + + socket + .bind(&SockAddr::from(cfg.server)) + .map_err(|err| Error::Socket("failed to bind endpoint UDP socket", err))?; + + StdUdpSocket::from(socket) + }; + + + let ep = Endpoint::new( + EndpointConfig::default(), + Some(config), + socket, + Arc::new(TokioRuntime), + )?; + + Ok(Self { + ep, + users: Arc::new(users), + zero_rtt_handshake: cfg.zero_rtt_handshake, + hello_timeout: cfg.handshake_timeout, + task_negotiation_timeout: cfg.task_negotiation_timeout, + authentication_failed_reply: cfg.authentication_failed_reply, + max_packet_size: cfg.max_packet_size, + }) + } + + pub async fn start(&self) { + log::warn!( + "Server started on {}", + self.ep.local_addr().unwrap() + ); + + loop { + let Some(incoming) = self.ep.accept().await else { + return; + }; + + match incoming.accept() { + Ok(conn) => { + tokio::spawn(Connection::handle( + conn, + self.users.clone(), + self.zero_rtt_handshake, + self.hello_timeout, + self.task_negotiation_timeout, + self.authentication_failed_reply, + self.max_packet_size, + )); + } + Err(err) => { + log::warn!("Failed to accept incoming connection: {}", err); + } + } + } + } +} \ No newline at end of file diff --git a/asport-server/src/utils.rs b/asport-server/src/utils.rs new file mode 100644 index 0000000..ba8b12d --- /dev/null +++ b/asport-server/src/utils.rs @@ -0,0 +1,288 @@ +use std::{ + collections::BTreeSet, + fmt::{Display, Formatter, Result as FmtResult}, + fs::{self, File}, + io::BufReader, + iter, + net::IpAddr, + ops::RangeInclusive, + path::Path, + str::FromStr, +}; + +use rustls::pki_types::{CertificateDer, PrivateKeyDer}; +use rustls_pemfile::Item; +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))] +use sysctl::CtlValue; +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd", + target_os = "linux", target_os = "android"))] +use sysctl::Sysctl; + +use asport::ForwardMode; + +use crate::error::Error; + +pub fn load_certs>(path: P) -> Result>, Error> { + let mut file = BufReader::new(File::open(&path) + .map_err(|e| Error::Io(e))?); + let mut certs = Vec::new(); + + while let Ok(Some(item)) = rustls_pemfile::read_one(&mut file) { + if let Item::X509Certificate(cert) = item { + certs.push(cert); + } + } + + // Der format + if certs.is_empty() { + certs = vec![CertificateDer::from(fs::read(&path) + .map_err(|e| Error::Io(e))?)]; + } + + Ok(certs) +} + +pub fn load_priv_key>(path: P) -> Result, Error> { + let mut file = BufReader::new( + File::open(&path).map_err(|e| Error::Io(e))? + ); + let mut priv_key: Option = None; + + for item in iter::from_fn(|| rustls_pemfile::read_one(&mut file).transpose()) { + match item { + Ok(Item::Pkcs1Key(key)) => { priv_key = Some(PrivateKeyDer::from(key)) } + Ok(Item::Pkcs8Key(key)) => { priv_key = Some(PrivateKeyDer::from(key)) } + Ok(Item::Sec1Key(key)) => { priv_key = Some(PrivateKeyDer::from(key)) } + _ => {} + } + } + + match priv_key { + Some(key) => Ok(key), + None => // Der format + fs::read(&path).map(PrivateKeyDer::try_from).map_err( + |e| Error::Io(e) + )?.map_err(|e| Error::InvalidPrivateKey(e)), + } +} +#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))] +pub fn ephemeral_port_range() -> RangeInclusive { + let first_ctl = sysctl::Ctl::new("net.inet.ip.portrange.first"); + let last_ctl = sysctl::Ctl::new("net.inet.ip.portrange.last"); + + if let (Ok(first_ctl), Ok(last_ctl)) = (first_ctl, last_ctl) { + // Actually, the first and last values should be u16, but sysctl crate returns them as i32. + if let (Ok(CtlValue::Int(first)), Ok(CtlValue::Int(last))) = (first_ctl.value(), last_ctl.value()) { + return (first as u16)..=(last as u16); + } + } + + // Default value for macOS, iOS and FreeBSD is 49152..=65535. + 49152..=65535 +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn ephemeral_port_range() -> RangeInclusive { + let ctl = sysctl::Ctl::new("net.ipv4.ip_local_port_range"); + if let Ok(ctl) = ctl { + if let Ok(value_str) = ctl.value_string() { + let mut iter = value_str.split_whitespace(); + if let (Some(start), Some(end)) = (iter.next(), iter.next()) { + if let (Ok(start), Ok(end)) = (start.parse::(), end.parse::()) { + return start..=end; + } + } + } + } + + // Default value for Linux is 32768..=60999. + // See also: https://www.kernel.org/doc/html/latest//networking/ip-sysctl.html#ip-variables + 32768..=60999 +} + + +#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "freebsd", + target_os = "linux", target_os = "android")))] +pub fn ephemeral_port_range() -> RangeInclusive { + // The suggested range by RFC6335 and IANA. + // See also: https://tools.ietf.org/html/rfc6335 + // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml + // Since Windows Vista/Server 2008 (6.0), the default dynamic port range is 49152..65535. + // And earlier versions had been dropped from support. + // See also: https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/default-dynamic-port-range-tcpip-chang + // https://doc.rust-lang.org/stable/rustc/platform-support.html + 49152..=65535 +} + + +pub enum CongestionControl { + Cubic, + NewReno, + Bbr, +} + +impl FromStr for CongestionControl { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("cubic") { + Ok(Self::Cubic) + } else if s.eq_ignore_ascii_case("new_reno") || s.eq_ignore_ascii_case("newreno") { + Ok(Self::NewReno) + } else if s.eq_ignore_ascii_case("bbr") { + Ok(Self::Bbr) + } else { + Err("invalid congestion control") + } + } +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum Network { + Tcp, + Udp, + Both, +} + +impl Network { + pub(crate) fn is_tcp(&self) -> bool { + matches!(self, Self::Tcp) + } + + pub(crate) fn is_udp(&self) -> bool { + matches!(self, Self::Udp) + } + + pub(crate) fn is_both(&self) -> bool { + matches!(self, Self::Both) + } + + pub(crate) fn tcp(&self) -> bool { + self.is_both() || self.is_tcp() + } + + pub(crate) fn udp(&self) -> bool { + self.is_both() || self.is_udp() + } +} + +impl FromStr for Network { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if s.eq_ignore_ascii_case("tcp") { + Ok(Self::Tcp) + } else if s.eq_ignore_ascii_case("udp") { + Ok(Self::Udp) + } else if vec!["both", "tcpudp", "tcp_udp", "tcp-udp", "all"].iter() + .any(|&x| s.eq_ignore_ascii_case(x)) { + Ok(Self::Both) + } else { + Err("invalid network") + } + } +} + +impl Display for Network { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Tcp => write!(f, "tcp"), + Self::Udp => write!(f, "udp"), + Self::Both => write!(f, "both"), + } + } +} + +pub fn merge_network(allow_network: Network, expected_network: Network) -> Result { + match (allow_network, expected_network) { + (Network::Both, _) => Ok(expected_network), + (Network::Tcp, Network::Tcp) => Ok(Network::Tcp), + (Network::Udp, Network::Udp) => Ok(Network::Udp), + _ => Err(Error::NetworkDenied(expected_network)), + } +} + +#[derive(Clone, Copy)] +pub enum UdpForwardMode { + Native, + Quic, +} + +impl Display for UdpForwardMode { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::Native => write!(f, "native"), + Self::Quic => write!(f, "quic"), + } + } +} + +pub struct NetworkUdpForwardModeCombine(Network, UdpForwardMode); + +impl NetworkUdpForwardModeCombine { + pub fn new(network: Network, mode: UdpForwardMode) -> Self { + Self(network, mode) + } +} + +impl From for NetworkUdpForwardModeCombine { + fn from(mode: ForwardMode) -> Self { + match mode { + ForwardMode::Tcp => Self::new(Network::Tcp, UdpForwardMode::Native), + ForwardMode::UdpNative => Self::new(Network::Udp, UdpForwardMode::Native), + ForwardMode::UdpQuic => Self::new(Network::Udp, UdpForwardMode::Quic), + ForwardMode::TcpUdpNative => Self::new(Network::Both, UdpForwardMode::Native), + ForwardMode::TcpUdpQuic => Self::new(Network::Both, UdpForwardMode::Quic), + } + } +} + +impl From for (Network, UdpForwardMode) { + fn from(value: NetworkUdpForwardModeCombine) -> Self { + (value.0, value.1) + } +} + +pub struct User { + password: Box<[u8]>, + bind_ip: IpAddr, + only_v6: Option, + allow_ports: BTreeSet, + allow_network: Network, +} + +impl User { + pub fn new(password: Box<[u8]>, + bind_ip: IpAddr, + allow_ports: BTreeSet, + only_v6: Option, + allow_network: Network) -> Self { + Self { + password, + bind_ip, + only_v6, + allow_ports, + allow_network, + } + } + + pub fn password(&self) -> &[u8] { + &self.password + } + + pub fn listen_ip(&self) -> IpAddr { + self.bind_ip + } + + pub fn allow_ports(&self) -> BTreeSet { + self.allow_ports.clone() + } + + pub fn only_v6(&self) -> Option { + self.only_v6 + } + + pub fn allow_network(&self) -> &Network { + &self.allow_network + } +} \ No newline at end of file diff --git a/asport/Cargo.toml b/asport/Cargo.toml new file mode 100644 index 0000000..18466db --- /dev/null +++ b/asport/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "asport" +version = "0.1.0" +authors = ["Kaede Akino "] +description = "An implementation of ASPORT protocol." +categories = ["network-programming"] +keywords = ["network", "proxy", "reverse-proxy", "quic", "asport"] +edition = "2021" +readme = "README.md" +license = "GPL-3.0-or-later" +repository = "https://github.com/AkinoKaede/asport" + +[features] +async_marshal = ["bytes", "futures-util"] +marshal = ["bytes"] +model = ["parking_lot", "register-count"] + +[dependencies] +bytes = { version = "1.6.1", default-features = false, features = ["std"], optional = true } +futures-util = { version = "0.3.28", default-features = false, features = ["io", "std"], optional = true } +parking_lot = { version = "0.12.1", default-features = false, optional = true } +register-count = { version = "0.1.0", default-features = false, features = ["std"], optional = true } +thiserror = { version = "1.0.62", default-features = false } +uuid = { version = "1.10.0", default-features = false, features = ["std"] } + +[dev-dependencies] +asport = { path = ".", features = ["async_marshal", "marshal", "model"] } + +[package.metadata.docs.rs] +all-features = true \ No newline at end of file diff --git a/asport/README.md b/asport/README.md new file mode 100644 index 0000000..27224dc --- /dev/null +++ b/asport/README.md @@ -0,0 +1,28 @@ +# asport + +An implementation of ASPORT protocol. + +## Overview + +The ASPORT protocol specification can be found in [SPEC.md](https://github.com/AkinoKaede/asport/blob/main/SPEC.md). This crate provides an implementation of the ASPORT protocol in Rust as a reference. + +Here is a list of optional features that can be enabled: + +- `model` - Provides a connection model abstraction of the ASPORT protocol, with packet fragmentation and task counter built-in. No I/O operation is involved. +- `marshal` - Provides methods for marshalling and unmarshalling the protocol in sync flavor. +- `async_marshal` - Provides methods for marshalling and unmarshalling the protocol in async flavor. +- +The root of the protocol abstraction is the [`Header`](https://docs.rs/asport/latest/asport/enum.Header.html). + +## Usage + +Run the following command to add this crate as a dependency: + +```bash +cargo add asport +``` + +## License +This crate is licensed under [GNU General Public License v3.0 or later](https://github.com/AkinoKaede/asport/blob/main/LICENSE). + +SPDX-License-Identifier: [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) \ No newline at end of file diff --git a/asport/src/lib.rs b/asport/src/lib.rs new file mode 100644 index 0000000..bb879f6 --- /dev/null +++ b/asport/src/lib.rs @@ -0,0 +1,35 @@ +/* +* Asport, a quick and secure reverse proxy based on QUIC for NAT traversal. +* Copyright (C) 2024 Kaede Akino +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +pub use self::protocol::{ + Address, ClientHello, Connect, Dissociate, ForwardMode, Header, Heartbeat, Packet, ServerHello, VERSION, +}; +#[cfg(any(feature = "async_marshal", feature = "marshal"))] +pub use self::unmarshal::UnmarshalError; + +mod protocol; + +#[cfg(feature = "model")] +pub mod model; + +#[cfg(any(feature = "async_marshal", feature = "marshal"))] +mod marshal; + +#[cfg(any(feature = "async_marshal", feature = "marshal"))] +mod unmarshal; + diff --git a/asport/src/marshal.rs b/asport/src/marshal.rs new file mode 100644 index 0000000..5c95516 --- /dev/null +++ b/asport/src/marshal.rs @@ -0,0 +1,108 @@ +use std::{ + io::{Error as IoError, Write}, + net::SocketAddr, +}; + +use bytes::{BufMut, BytesMut}; +use futures_util::{AsyncWrite, AsyncWriteExt}; + +use crate::{Address, ClientHello, Connect, Dissociate, Header, Heartbeat, Packet, ServerHello, VERSION}; + +impl Header { + /// Marshals the header into an `AsyncWrite` stream + #[cfg(feature = "async_marshal")] + pub async fn async_marshal(&self, s: &mut (impl AsyncWrite + Unpin)) -> Result<(), IoError> { + let mut buf = BytesMut::with_capacity(self.len()); + self.write(&mut buf); + s.write_all(&buf).await + } + + /// Marshals the header into a `Write` stream + #[cfg(feature = "marshal")] + pub fn marshal(&self, s: &mut impl Write) -> Result<(), IoError> { + let mut buf = BytesMut::with_capacity(self.len()); + self.write(&mut buf); + s.write_all(&buf) + } + + /// Writes the header into a `BufMut` + pub fn write(&self, buf: &mut impl BufMut) { + buf.put_u8(VERSION); + buf.put_u8(self.type_code()); + + match self { + Self::ClientHello(client_hello) => client_hello.write(buf), + Self::ServerHello(server_hello) => server_hello.write(buf), + Self::Connect(connect) => connect.write(buf), + Self::Packet(packet) => packet.write(buf), + Self::Dissociate(dissociate) => dissociate.write(buf), + Self::Heartbeat(heartbeat) => heartbeat.write(buf), + } + } +} + +impl Address { + fn write(&self, buf: &mut impl BufMut) { + buf.put_u8(self.type_code()); + + match self { + Self::None => {} + Self::SocketAddress(SocketAddr::V4(addr)) => { + buf.put_slice(&addr.ip().octets()); + buf.put_u16(addr.port()); + } + Self::SocketAddress(SocketAddr::V6(addr)) => { + for seg in addr.ip().segments() { + buf.put_u16(seg); + } + buf.put_u16(addr.port()); + } + } + } +} + +impl ClientHello { + fn write(&self, buf: &mut impl BufMut) { + buf.put_slice(self.uuid().as_ref()); + buf.put_slice(&self.token()); + buf.put_slice(self.forward_mode().into()); + buf.put_u16(*self.expected_port_range().start()); + buf.put_u16(*self.expected_port_range().end()); + } +} + +impl ServerHello { + fn write(&self, buf: &mut impl BufMut) { + buf.put_u8(self.handshake_code()); + if let Some(port) = self.port() { + buf.put_u16(port); + } + } +} + +impl Connect { + fn write(&self, buf: &mut impl BufMut) { + self.addr().write(buf); + } +} + +impl Packet { + fn write(&self, buf: &mut impl BufMut) { + buf.put_u16(self.assoc_id()); + buf.put_u16(self.pkt_id()); + buf.put_u8(self.frag_total()); + buf.put_u8(self.frag_id()); + buf.put_u16(self.size()); + self.addr().write(buf); + } +} + +impl Dissociate { + fn write(&self, buf: &mut impl BufMut) { + buf.put_u16(self.assoc_id()); + } +} + +impl Heartbeat { + fn write(&self, _buf: &mut impl BufMut) {} +} diff --git a/asport/src/model/client_hello.rs b/asport/src/model/client_hello.rs new file mode 100644 index 0000000..4398945 --- /dev/null +++ b/asport/src/model/client_hello.rs @@ -0,0 +1,130 @@ +use std::{ + fmt::{Debug, Formatter, Result as FmtResult}, + ops::RangeInclusive +}; + +use uuid::Uuid; + +use crate::{ClientHello as ClientHelloHeader, ForwardMode, Header}; + +use super::side::{self, Side}; + +/// The model of the `ClientHello` command +pub struct ClientHello { + inner: Side, + _marker: M, +} + +struct Tx { + header: Header, +} + +impl ClientHello { + pub(super) fn new( + uuid: Uuid, + password: impl AsRef<[u8]>, + exporter: &impl KeyingMaterialExporter, + forward_mode: impl Into, + expected_port_range: RangeInclusive, + ) -> Self { + Self { + inner: Side::Tx(Tx { + header: Header::ClientHello(ClientHelloHeader::new( + uuid, + exporter.export_keying_material(uuid.as_ref(), password.as_ref()), + forward_mode.into(), + expected_port_range, + )), + }), + _marker: side::Tx, + } + } + + /// Returns the header of the `ClientHello` command + pub fn header(&self) -> &Header { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + &tx.header + } +} + +impl Debug for ClientHello { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + f.debug_struct("Authenticate") + .field("header", &tx.header) + .finish() + } +} + +struct Rx { + uuid: Uuid, + token: [u8; 32], + forward_mode: ForwardMode, + expected_port_range: RangeInclusive, +} + +impl ClientHello { + pub(super) fn new( + uuid: Uuid, + token: [u8; 32], + forward_mode: ForwardMode, + expected_port_range: RangeInclusive, + ) -> Self { + Self { + inner: Side::Rx(Rx { uuid, token, forward_mode, expected_port_range }), + _marker: side::Rx, + } + } + + /// Returns the UUID of the peer + pub fn uuid(&self) -> Uuid { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.uuid + } + + /// Returns the token of the peer + pub fn token(&self) -> [u8; 32] { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.token + } + + /// Returns whether the token is valid + pub fn is_valid( + &self, + password: impl AsRef<[u8]>, + exporter: &impl KeyingMaterialExporter, + ) -> bool { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.token == exporter.export_keying_material(rx.uuid.as_ref(), password.as_ref()) + } + + /// Returns the forward mode of the peer + pub fn forward_mode(&self) -> ForwardMode { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.forward_mode + } + + /// Returns the expected port range of the peer + pub fn expected_port_range(&self) -> RangeInclusive { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.expected_port_range.clone() + } +} + +impl Debug for ClientHello { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + f.debug_struct("Authenticate") + .field("uuid", &rx.uuid) + .field("token", &rx.token) + .field("forward_mode", &rx.forward_mode) + .field("expected_port_range", &rx.expected_port_range) + .finish() + } +} + +/// The trait for exporting keying material +pub trait KeyingMaterialExporter { + /// Exports keying material + fn export_keying_material(&self, label: &[u8], context: &[u8]) -> [u8; 32]; +} diff --git a/asport/src/model/connect.rs b/asport/src/model/connect.rs new file mode 100644 index 0000000..14def97 --- /dev/null +++ b/asport/src/model/connect.rs @@ -0,0 +1,75 @@ +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +use register_count::Register; + +use crate::{Address, Connect as ConnectHeader, Header}; + +use super::side::{self, Side}; + +pub struct Connect { + inner: Side, + _marker: M, +} +struct Tx { + header: Header, + _task_reg: Register, +} + +impl Connect { + pub(super) fn new(task_reg: Register, addr: Address) -> Self { + Self { + inner: Side::Tx(Tx { + header: Header::Connect(ConnectHeader::new(addr)), + _task_reg: task_reg, + }), + _marker: side::Tx, + } + } + + /// Returns the header of the `Connect` command + pub fn header(&self) -> &Header { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + &tx.header + } +} + +impl Debug for Connect { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + f.debug_struct("Connect") + .field("header", &tx.header) + .finish() + } +} + + +struct Rx { + addr: Address, + _task_reg: Register, +} + + +impl Connect { + pub(super) fn new(task_reg: Register, addr: Address) -> Self { + Self { + inner: Side::Rx(Rx { + addr, + _task_reg: task_reg, + }), + _marker: side::Rx, + } + } + + /// Returns the address + pub fn addr(&self) -> &Address { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + &rx.addr + } +} + +impl Debug for Connect { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + f.debug_struct("Connect").field("addr", &rx.addr).finish() + } +} \ No newline at end of file diff --git a/asport/src/model/dissociate.rs b/asport/src/model/dissociate.rs new file mode 100644 index 0000000..fa32077 --- /dev/null +++ b/asport/src/model/dissociate.rs @@ -0,0 +1,69 @@ +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +use crate::{Dissociate as DissociateHeader, Header}; + +use super::side::{self, Side}; + +/// The model of the `Dissociate` command +pub struct Dissociate { + inner: Side, + _marker: M, +} + +struct Tx { + header: Header, +} + +impl Dissociate { + pub(super) fn new(assoc_id: u16) -> Self { + Self { + inner: Side::Tx(Tx { + header: Header::Dissociate(DissociateHeader::new(assoc_id)), + }), + _marker: side::Tx, + } + } + + /// Returns the header of the `Dissociate` command + pub fn header(&self) -> &Header { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + &tx.header + } +} + +impl Debug for Dissociate { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + f.debug_struct("Dissociate") + .field("header", &tx.header) + .finish() + } +} + +struct Rx { + assoc_id: u16, +} + +impl Dissociate { + pub(super) fn new(assoc_id: u16) -> Self { + Self { + inner: Side::Rx(Rx { assoc_id }), + _marker: side::Rx, + } + } + + /// Returns the UDP session ID + pub fn assoc_id(&self) -> u16 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.assoc_id + } +} + +impl Debug for Dissociate { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + f.debug_struct("Dissociate") + .field("assoc_id", &rx.assoc_id) + .finish() + } +} diff --git a/asport/src/model/heartbeat.rs b/asport/src/model/heartbeat.rs new file mode 100644 index 0000000..99f9427 --- /dev/null +++ b/asport/src/model/heartbeat.rs @@ -0,0 +1,57 @@ +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +use crate::{Header, Heartbeat as HeartbeatHeader}; + +use super::side::{self, Side}; + +pub struct Heartbeat { + inner: Side, + _marker: M, +} + +struct Tx { + header: Header, +} + +impl Heartbeat { + pub(super) fn new() -> Self { + Self { + inner: Side::Tx(Tx { + header: Header::Heartbeat(HeartbeatHeader::new()), + }), + _marker: side::Tx, + } + } + + /// Returns the header of the `Heartbeat` command + pub fn header(&self) -> &Header { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + &tx.header + } +} + +impl Debug for Heartbeat { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + f.debug_struct("Heartbeat") + .field("header", &tx.header) + .finish() + } +} + +struct Rx; + +impl Heartbeat { + pub(super) fn new() -> Self { + Self { + inner: Side::Rx(Rx), + _marker: side::Rx, + } + } +} + +impl Debug for Heartbeat { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("Heartbeat").finish() + } +} diff --git a/asport/src/model/mod.rs b/asport/src/model/mod.rs new file mode 100644 index 0000000..1fa76f0 --- /dev/null +++ b/asport/src/model/mod.rs @@ -0,0 +1,532 @@ +use std::{ + collections::HashMap, + fmt::{Debug, Formatter, Result as FmtResult}, + mem, + ops::RangeInclusive, + sync::{Arc, atomic::{AtomicU16, Ordering}}, + time::{Duration, Instant}, +}; + +use parking_lot::Mutex; +use register_count::{Counter, Register}; +use thiserror::Error; +use uuid::Uuid; + +use crate::{ + Address, ClientHello as ClientHelloHeader, Connect as ConnectHeader, + Dissociate as DissociateHeader, ForwardMode, Heartbeat as HeartbeatHeader, + Packet as PacketHeader, ServerHello as ServerHelloHeader, +}; + +pub use self::{ + client_hello::{ClientHello, KeyingMaterialExporter}, + connect::Connect, + dissociate::Dissociate, + heartbeat::Heartbeat, + packet::Packet, + server_hello::ServerHello, +}; + +mod client_hello; +mod heartbeat; +mod server_hello; +mod connect; +mod packet; +mod dissociate; + +#[derive(Clone)] +pub struct Connection { + udp_sessions: Arc>>, + task_connect_count: Counter, + task_associate_count: Counter, +} + +impl Connection +where + B: AsRef<[u8]>, +{ + pub fn new() -> Self { + let task_associate_count = Counter::new(); + + Self { + udp_sessions: Arc::new(Mutex::new(UdpSessions::new(task_associate_count.clone()))), + task_connect_count: Counter::new(), + task_associate_count, + } + } + + /// Sends an `ClientHello` + pub fn send_client_hello( + &self, + uuid: Uuid, + password: impl AsRef<[u8]>, + exporter: &impl KeyingMaterialExporter, + forward_mode: impl Into, + expected_port_range: RangeInclusive, + ) -> ClientHello { + ClientHello::::new(uuid, password, exporter, forward_mode, expected_port_range) + } + + /// Receives an `ClientHello` + pub fn recv_client_hello(&self, header: ClientHelloHeader) -> ClientHello { + let (uuid, token, forward_mode, expected_port_range) = header.into(); + ClientHello::::new(uuid, token, forward_mode, expected_port_range) + } + + /// Sends a `ServerHello` + pub fn send_server_hello(&self, server_hello: ServerHelloHeader) -> ServerHello { + ServerHello::::new(server_hello) + } + + /// Receives a `ServerHello` + pub fn recv_server_hello(&self, header: ServerHelloHeader) -> ServerHello { + let (handshake_code, port) = header.into(); + ServerHello::::new(handshake_code, port) + } + + /// Sends a `Connect` + pub fn send_connect(&self, addr: Address) -> Connect { + Connect::::new(self.task_connect_count.reg(), addr) + } + + /// Receives a `Connect` + pub fn recv_connect(&self, header: ConnectHeader) -> Connect { + let (addr, ) = header.into(); + Connect::::new(self.task_connect_count.reg(), addr) + } + + /// Sends a `Packet` + pub fn send_packet( + &self, + assoc_id: u16, + addr: Address, + max_pkt_size: usize, + ) -> Packet { + self.udp_sessions + .lock() + .send_packet(assoc_id, addr, max_pkt_size) + } + + /// Receives a `Packet`. If the association ID is not found, returns `None` + pub fn recv_packet(&self, header: PacketHeader) -> Option> { + let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into(); + self.udp_sessions.lock().recv_packet( + self.udp_sessions.clone(), + assoc_id, + pkt_id, + frag_total, + frag_id, + size, + addr, + ) + } + + /// Receives a `Packet` without checking the association ID + pub fn recv_packet_unrestricted(&self, header: PacketHeader) -> Packet { + let (assoc_id, pkt_id, frag_total, frag_id, size, addr) = header.into(); + self.udp_sessions.lock().recv_packet_unrestricted( + self.udp_sessions.clone(), + assoc_id, + pkt_id, + frag_total, + frag_id, + size, + addr, + ) + } + + /// Sends a `Dissociate` + pub fn send_dissociate(&self, assoc_id: u16) -> Dissociate { + self.udp_sessions.lock().send_dissociate(assoc_id) + } + + /// Receives a `Dissociate` + pub fn recv_dissociate(&self, header: DissociateHeader) -> Dissociate { + let (assoc_id, ) = header.into(); + self.udp_sessions.lock().recv_dissociate(assoc_id) + } + + /// Sends a `Heartbeat` + pub fn send_heartbeat(&self) -> Heartbeat { + Heartbeat::::new() + } + + /// Receives a `Heartbeat` + pub fn recv_heartbeat(&self, header: HeartbeatHeader) -> Heartbeat { + let () = header.into(); + Heartbeat::::new() + } + + /// Returns the number of `Connect` tasks + pub fn task_connect_count(&self) -> usize { + self.task_connect_count.count() + } + + /// Returns the number of active UDP sessions + pub fn task_associate_count(&self) -> usize { + self.task_associate_count.count() + } + + /// Removes fragments that can not be reassembled within the specified timeout + pub fn collect_garbage(&self, timeout: Duration) { + self.udp_sessions.lock().collect_garbage(timeout); + } +} + +impl Debug for Connection +where + B: AsRef<[u8]> + Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("Connection") + .field("udp_sessions", &self.udp_sessions) + .field("task_connect_count", &self.task_connect_count()) + .field("task_associate_count", &self.task_associate_count()) + .finish() + } +} + +/// Abstracts the side of a task +pub mod side { + /// The side of a task that sends data + pub struct Tx; + /// The side of a task that receives data + pub struct Rx; + + pub(super) enum Side { + Tx(T), + Rx(R), + } +} + +struct UdpSessions { + sessions: HashMap>, + task_associate_count: Counter, +} + +impl UdpSessions +where + B: AsRef<[u8]>, +{ + fn new(task_associate_count: Counter) -> Self { + Self { + sessions: HashMap::new(), + task_associate_count, + } + } + + fn send_packet( + &mut self, + assoc_id: u16, + addr: Address, + max_pkt_size: usize, + ) -> Packet { + self.sessions + .entry(assoc_id) + .or_insert_with(|| UdpSession::new(self.task_associate_count.reg())) + .send_packet(assoc_id, addr, max_pkt_size) + } + + #[allow(clippy::too_many_arguments)] + fn recv_packet( + &mut self, + sessions: Arc>, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + ) -> Option> { + self.sessions.get_mut(&assoc_id).map(|session| { + session.recv_packet(sessions, assoc_id, pkt_id, frag_total, frag_id, size, addr) + }) + } + + #[allow(clippy::too_many_arguments)] + fn recv_packet_unrestricted( + &mut self, + sessions: Arc>, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + ) -> Packet { + self.sessions + .entry(assoc_id) + .or_insert_with(|| UdpSession::new(self.task_associate_count.reg())) + .recv_packet(sessions, assoc_id, pkt_id, frag_total, frag_id, size, addr) + } + + fn send_dissociate(&mut self, assoc_id: u16) -> Dissociate { + self.sessions.remove(&assoc_id); + Dissociate::::new(assoc_id) + } + + fn recv_dissociate(&mut self, assoc_id: u16) -> Dissociate { + self.sessions.remove(&assoc_id); + Dissociate::::new(assoc_id) + } + + #[allow(clippy::too_many_arguments)] + fn insert( + &mut self, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + data: B, + ) -> Result>, AssembleError> { + self.sessions + .entry(assoc_id) + .or_insert_with(|| UdpSession::new(self.task_associate_count.reg())) + .insert(assoc_id, pkt_id, frag_total, frag_id, size, addr, data) + } + + fn collect_garbage(&mut self, timeout: Duration) { + for (_, session) in self.sessions.iter_mut() { + session.collect_garbage(timeout); + } + } +} + +impl Debug for UdpSessions +where + B: AsRef<[u8]> + Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("UdpSessions") + .field("sessions", &self.sessions) + .finish() + } +} + + +struct UdpSession { + pkt_buf: HashMap>, + next_pkt_id: AtomicU16, + _task_reg: Register, +} + +impl UdpSession +where + B: AsRef<[u8]>, +{ + fn new(task_reg: Register) -> Self { + Self { + pkt_buf: HashMap::new(), + next_pkt_id: AtomicU16::new(0), + _task_reg: task_reg, + } + } + + fn send_packet( + &self, + assoc_id: u16, + addr: Address, + max_pkt_size: usize, + ) -> Packet { + Packet::::new( + assoc_id, + self.next_pkt_id.fetch_add(1, Ordering::AcqRel), + addr, + max_pkt_size, + ) + } + + #[allow(clippy::too_many_arguments)] + fn recv_packet( + &self, + sessions: Arc>>, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + ) -> Packet { + Packet::::new(sessions, assoc_id, pkt_id, frag_total, frag_id, size, addr) + } + + #[allow(clippy::too_many_arguments)] + fn insert( + &mut self, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + data: B, + ) -> Result>, AssembleError> { + let res = self + .pkt_buf + .entry(pkt_id) + .or_insert_with(|| PacketBuffer::new(frag_total)) + .insert(assoc_id, frag_total, frag_id, size, addr, data)?; + + if res.is_some() { + self.pkt_buf.remove(&pkt_id); + } + + Ok(res) + } + + fn collect_garbage(&mut self, timeout: Duration) { + self.pkt_buf.retain(|_, buf| buf.c_time.elapsed() < timeout); + } +} + +impl Debug for UdpSession +where + B: AsRef<[u8]> + Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + f.debug_struct("UdpSession") + .field("pkt_buf", &self.pkt_buf) + .field("next_pkt_id", &self.next_pkt_id) + .finish() + } +} + +#[derive(Debug)] +struct PacketBuffer { + buf: Vec>, + frag_total: u8, + frag_received: u8, + addr: Address, + c_time: Instant, +} + +impl PacketBuffer +where + B: AsRef<[u8]>, +{ + fn new(frag_total: u8) -> Self { + let mut buf = Vec::with_capacity(frag_total as usize); + buf.resize_with(frag_total as usize, || None); + + Self { + buf, + frag_total, + frag_received: 0, + addr: Address::None, + c_time: Instant::now(), + } + } + + fn insert( + &mut self, + assoc_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + data: B, + ) -> Result>, AssembleError> { + assert_eq!(data.as_ref().len(), size as usize); + + if frag_id >= frag_total { + return Err(AssembleError::InvalidFragmentId(frag_total, frag_id)); + } + + if frag_id == 0 && addr.is_none() { + return Err(AssembleError::InvalidAddress( + "no address in first fragment", + )); + } + + if frag_id != 0 && !addr.is_none() { + return Err(AssembleError::InvalidAddress( + "address in non-first fragment", + )); + } + + if self.buf[frag_id as usize].is_some() { + return Err(AssembleError::DuplicatedFragment(frag_id)); + } + + self.buf[frag_id as usize] = Some(data); + self.frag_received += 1; + + if frag_id == 0 { + self.addr = addr; + } + + if self.frag_received == self.frag_total { + Ok(Some(Assemblable::new( + mem::take(&mut self.buf), + self.addr.take(), + assoc_id, + ))) + } else { + Ok(None) + } + } +} + +/// A complete packet that can be assembled +#[derive(Debug)] +pub struct Assemblable { + buf: Vec>, + addr: Address, + assoc_id: u16, +} + +impl Assemblable +where + B: AsRef<[u8]>, +{ + fn new(buf: Vec>, addr: Address, assoc_id: u16) -> Self { + Self { + buf, + addr, + assoc_id, + } + } + + pub fn assemble(self, buf: &mut A) -> (Address, u16) + where + A: Assembler, + { + let data = self.buf.into_iter().map(|b| b.unwrap()); + buf.assemble(data); + (self.addr, self.assoc_id) + } +} + +/// A trait for assembling a packet +pub trait Assembler +where + Self: Sized, + B: AsRef<[u8]>, +{ + fn assemble(&mut self, data: impl IntoIterator); +} + +impl Assembler for Vec +where + B: AsRef<[u8]>, +{ + fn assemble(&mut self, data: impl IntoIterator) { + for d in data { + self.extend_from_slice(d.as_ref()); + } + } +} + +/// An error that can occur when assembling a packet +#[derive(Debug, Error)] +pub enum AssembleError { + #[error("invalid fragment id {1} in total {0} fragments")] + InvalidFragmentId(u8, u8), + #[error("{0}")] + InvalidAddress(&'static str), + #[error("duplicated fragment: {0}")] + DuplicatedFragment(u8), +} + diff --git a/asport/src/model/packet.rs b/asport/src/model/packet.rs new file mode 100644 index 0000000..5b069d1 --- /dev/null +++ b/asport/src/model/packet.rs @@ -0,0 +1,278 @@ +use std::{ + fmt::{Debug, Formatter, Result as FmtResult}, + marker::PhantomData, + slice, + sync::Arc, +}; + +use parking_lot::Mutex; + +use crate::{Address, Header, Packet as PacketHeader}; + +use super::{Assemblable, AssembleError, side::{self, Side}, UdpSessions}; + +pub struct Packet { + inner: Side>, + _marker: M, +} + +struct Tx { + assoc_id: u16, + pkt_id: u16, + addr: Address, + max_pkt_size: usize, +} + +impl Packet { + pub(super) fn new(assoc_id: u16, pkt_id: u16, addr: Address, max_pkt_size: usize) -> Self { + Self { + inner: Side::Tx(Tx { + assoc_id, + pkt_id, + addr, + max_pkt_size, + }), + _marker: side::Tx, + } + } + + /// Fragment the payload into multiple packets + pub fn into_fragments<'a, P>(self, payload: P) -> Fragments<'a, P> + where + P: AsRef<[u8]> + 'a, + { + let Side::Tx(tx) = self.inner else { unreachable!() }; + Fragments::new(tx.assoc_id, tx.pkt_id, tx.addr, tx.max_pkt_size, payload) + } + + /// Returns the UDP session ID + pub fn assoc_id(&self) -> u16 { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + tx.assoc_id + } + + /// Returns the packet ID + pub fn pkt_id(&self) -> u16 { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + tx.pkt_id + } + + /// Returns the address + pub fn addr(&self) -> &Address { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + &tx.addr + } +} + +impl Debug for Packet { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + f.debug_struct("Packet") + .field("assoc_id", &tx.assoc_id) + .field("pkt_id", &tx.pkt_id) + .field("addr", &tx.addr) + .field("max_pkt_size", &tx.max_pkt_size) + .finish() + } +} + +struct Rx { + sessions: Arc>>, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, +} + +impl Packet +where + B: AsRef<[u8]>, +{ + pub(super) fn new( + sessions: Arc>>, + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + ) -> Self { + Self { + inner: Side::Rx(Rx { + sessions, + assoc_id, + pkt_id, + frag_total, + frag_id, + size, + addr, + }), + _marker: side::Rx, + } + } + + /// Reassembles the packet. If the packet is not complete yet, `None` is returned. + pub fn assemble(self, data: B) -> Result>, AssembleError> { + let Side::Rx(rx) = self.inner else { unreachable!() }; + let mut sessions = rx.sessions.lock(); + + sessions.insert( + rx.assoc_id, + rx.pkt_id, + rx.frag_total, + rx.frag_id, + rx.size, + rx.addr, + data, + ) + } + + /// Returns the UDP session ID + pub fn assoc_id(&self) -> u16 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.assoc_id + } + + /// Returns the packet ID + pub fn pkt_id(&self) -> u16 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.pkt_id + } + + /// Returns the fragment ID + pub fn frag_id(&self) -> u8 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.frag_id + } + + /// Returns the total number of fragments + pub fn frag_total(&self) -> u8 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.frag_total + } + + /// Returns the address + pub fn addr(&self) -> &Address { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + &rx.addr + } + + /// Returns the size of the (fragmented) packet + pub fn size(&self) -> u16 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.size + } +} + +impl Debug for Packet { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + f.debug_struct("Packet") + .field("assoc_id", &rx.assoc_id) + .field("pkt_id", &rx.pkt_id) + .field("frag_total", &rx.frag_total) + .field("frag_id", &rx.frag_id) + .field("size", &rx.size) + .field("addr", &rx.addr) + .finish() + } +} + +/// Iterator over fragments of a packet +#[derive(Debug)] +pub struct Fragments<'a, P> { + assoc_id: u16, + pkt_id: u16, + addr: Address, + max_pkt_size: usize, + frag_total: u8, + next_frag_id: u8, + next_frag_start: usize, + payload: P, + _marker: PhantomData<&'a P>, +} + +impl<'a, P> Fragments<'a, P> +where + P: AsRef<[u8]> + 'a, +{ + fn new(assoc_id: u16, pkt_id: u16, addr: Address, max_pkt_size: usize, payload: P) -> Self { + let header_addr_ref = Header::Packet(PacketHeader::new(0, 0, 0, 0, 0, addr)); + let header_addr_none_ref = Header::Packet(PacketHeader::new(0, 0, 0, 0, 0, Address::None)); + + let first_frag_size = max_pkt_size - header_addr_ref.len(); + let frag_size_addr_none = max_pkt_size - header_addr_none_ref.len(); + + let Header::Packet(pkt) = header_addr_ref else { unreachable!() }; + let (_, _, _, _, _, addr) = pkt.into(); + + let frag_total = if first_frag_size < payload.as_ref().len() { + (1 + (payload.as_ref().len() - first_frag_size) / frag_size_addr_none + 1) as u8 + } else { + 1u8 + }; + + Self { + assoc_id, + pkt_id, + addr, + max_pkt_size, + frag_total, + next_frag_id: 0, + next_frag_start: 0, + payload, + _marker: PhantomData, + } + } +} + +impl<'a, P> Iterator for Fragments<'a, P> +where + P: AsRef<[u8]> + 'a, +{ + type Item = (Header, &'a [u8]); + + fn next(&mut self) -> Option { + if self.next_frag_id < self.frag_total { + let header_ref = Header::Packet(PacketHeader::new(0, 0, 0, 0, 0, self.addr.take())); + + let payload_size = self.max_pkt_size - header_ref.len(); + let next_frag_end = + (self.next_frag_start + payload_size).min(self.payload.as_ref().len()); + + let Header::Packet(pkt) = header_ref else { unreachable!() }; + let (_, _, _, _, _, addr) = pkt.into(); + + let header = Header::Packet(PacketHeader::new( + self.assoc_id, + self.pkt_id, + self.frag_total, + self.next_frag_id, + (next_frag_end - self.next_frag_start) as u16, + addr, + )); + + let payload_ptr = &(self.payload.as_ref()[self.next_frag_start]) as *const u8; + let payload = + unsafe { slice::from_raw_parts(payload_ptr, next_frag_end - self.next_frag_start) }; + + self.next_frag_id += 1; + self.next_frag_start = next_frag_end; + + Some((header, payload)) + } else { + None + } + } +} + +impl

ExactSizeIterator for Fragments<'_, P> +where + P: AsRef<[u8]>, +{ + fn len(&self) -> usize { + self.frag_total as usize + } +} diff --git a/asport/src/model/server_hello.rs b/asport/src/model/server_hello.rs new file mode 100644 index 0000000..32b2eca --- /dev/null +++ b/asport/src/model/server_hello.rs @@ -0,0 +1,70 @@ +use std::fmt::{Debug, Formatter, Result as FmtResult}; + +use crate::{Header, ServerHello as ServerHelloHeader}; + +use super::side::{self, Side}; + +/// The model of the `ServerHello` command +pub struct ServerHello { + inner: Side, + _marker: M, +} + +struct Tx { + header: Header, +} + +impl ServerHello { + pub(super) fn new(server_hello: ServerHelloHeader) -> Self { + Self { + inner: Side::Tx(Tx { + header: Header::ServerHello(server_hello), + }), + _marker: side::Tx, + } + } + + /// Returns the header of the `ServerHello` command + pub fn header(&self) -> &Header { + let Side::Tx(tx) = &self.inner else { unreachable!() }; + &tx.header + } +} + +struct Rx { + handshake_code: u8, + port: Option, +} + +impl ServerHello { + pub(super) fn new(handshake_code: u8, port: Option) -> Self { + Self { + inner: Side::Rx(Rx { handshake_code, port }), + _marker: side::Rx, + } + } + + /// Returns the handshake code of the `ServerHello` command + pub fn handshake_code(&self) -> u8 { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.handshake_code + } + + /// Returns the port of the `ServerHello` command + pub fn port(&self) -> Option { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + rx.port + } +} + +impl Debug for ServerHello { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let Side::Rx(rx) = &self.inner else { unreachable!() }; + f.debug_struct("ServerHello") + .field("handshake_code", &rx.handshake_code) + .field("port", &rx.port) + .finish() + } +} + + diff --git a/asport/src/protocol/client_hello.rs b/asport/src/protocol/client_hello.rs new file mode 100644 index 0000000..09997b9 --- /dev/null +++ b/asport/src/protocol/client_hello.rs @@ -0,0 +1,137 @@ +use std::ops::RangeInclusive; + +use thiserror::Error; +use uuid::Uuid; + +/// Command `ClientHello` +/// ```plain +/// +------+-------+-------+-------+-------+ +/// | UUID | TOKEN | FM | EPRS | EPRE | +/// +------+-------+-------+-------+-------+ +/// | 16 | 32 | 1 | 2 | 2 | +/// +------+-------+-------+-------+-------+ +/// ``` +/// +/// where: +/// +/// - `UUID` - client UUID +/// - `TOKEN` - client token. The client raw password is hashed into a 256-bit long token using [TLS Keying Material Exporter](https://www.rfc-editor.org/rfc/rfc5705) on current TLS session. While exporting, the `label` should be the client UUID and the `context` should be the raw password. +/// - `FM` - forward mode. The forward mode of the client. It can be: `0x00` for TCP, `0x01` for UDP native, `0x02` for UDP QUIC, `0x03` for TCP + UDP native, `0x04` for TCP + UDP QUIC. +/// - `EPRS` - expected port range start. The start of the port range that the client expects to be forwarded. +/// - `EPRE` - expected port range end. The end of the port range that the client expects to be forwarded. It must be greater than or equal to `EPRS`. +#[derive(Clone, Debug)] +pub struct ClientHello { + uuid: Uuid, + token: [u8; 32], + forward_mode: ForwardMode, + expected_port_range: RangeInclusive, +} + +impl ClientHello { + const TYPE_CODE: u8 = 0x00; + + pub const fn new( + uuid: Uuid, + token: [u8; 32], + forward_mode: ForwardMode, + expected_port_range: RangeInclusive, + ) -> Self { + Self { uuid, token, forward_mode, expected_port_range } + } + + pub fn uuid(&self) -> Uuid { + self.uuid + } + + pub fn token(&self) -> [u8; 32] { + self.token + } + + pub fn forward_mode(&self) -> ForwardMode { + self.forward_mode + } + + pub fn expected_port_range(&self) -> RangeInclusive { + self.expected_port_range.clone() + } + + pub const fn type_code() -> u8 { + Self::TYPE_CODE + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 16 + 32 + 1 + 2 + 2 + } +} + +impl From for (Uuid, [u8; 32], ForwardMode, RangeInclusive) { + fn from(hello: ClientHello) -> Self { + (hello.uuid, hello.token, hello.forward_mode, hello.expected_port_range) + } +} + +#[derive(Copy, Clone, Debug)] +#[repr(u8)] +pub enum ForwardMode { + Tcp = 0x00, + UdpNative = 0x01, // UDP forward with QUIC unreliable datagram + UdpQuic = 0x02, // UDP forward with QUIC unidirectional stream + // Combine + TcpUdpNative = 0x03, // Tcp + UdpNative + TcpUdpQuic = 0x04, // Tcp + UdpQuic +} + +impl ForwardMode { + pub fn tcp(&self) -> bool { + match self { + ForwardMode::Tcp | ForwardMode::TcpUdpNative | ForwardMode::TcpUdpQuic => true, + _ => false, + } + } + + pub fn udp(&self) -> bool { + match self { + ForwardMode::UdpNative | ForwardMode::UdpQuic | ForwardMode::TcpUdpNative | ForwardMode::TcpUdpQuic => true, + _ => false, + } + } + + pub fn both(&self) -> bool { + match self { + ForwardMode::TcpUdpNative | ForwardMode::TcpUdpQuic => true, + _ => false, + } + } +} + +#[derive(Debug, Error)] +#[error("invalid forward mode: {0}")] +pub struct InvalidForwardMode(u8); + +impl TryFrom for ForwardMode { + type Error = InvalidForwardMode; + + fn try_from(value: u8) -> Result { + match value { + 0x00 => Ok(ForwardMode::Tcp), + 0x01 => Ok(ForwardMode::UdpNative), + 0x02 => Ok(ForwardMode::UdpQuic), + 0x03 => Ok(ForwardMode::TcpUdpNative), + 0x04 => Ok(ForwardMode::TcpUdpQuic), + _ => Err(InvalidForwardMode(value)), + } + } +} + +impl From for &[u8] { + fn from(mode: ForwardMode) -> Self { + match mode { + ForwardMode::Tcp => &[0x00], + ForwardMode::UdpNative => &[0x01], + ForwardMode::UdpQuic => &[0x02], + ForwardMode::TcpUdpNative => &[0x03], + ForwardMode::TcpUdpQuic => &[0x04], + } + } +} \ No newline at end of file diff --git a/asport/src/protocol/connect.rs b/asport/src/protocol/connect.rs new file mode 100644 index 0000000..d9df025 --- /dev/null +++ b/asport/src/protocol/connect.rs @@ -0,0 +1,47 @@ +use super::Address; + +/// Command `Connect` +/// ```plain +/// +----------+ +/// | ADDR | +/// +----------+ +/// | Variable | +/// +----------+ +/// ``` +/// +/// where: +/// +/// - `ADDR` - source address +#[derive(Clone, Debug)] +pub struct Connect { + addr: Address, +} + +impl Connect { + const TYPE_CODE: u8 = 0x02; + pub const fn new(addr: Address) -> Self { + Self { addr } + } + + /// Returns the address + pub fn addr(&self) -> &Address { + &self.addr + } + + /// Returns the command type code + pub const fn type_code() -> u8 { + Self::TYPE_CODE + } + + /// Returns the serialized length of the command + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.addr.len() + } +} + +impl From for (Address,) { + fn from(conn: Connect) -> Self { + (conn.addr,) + } +} diff --git a/asport/src/protocol/dissociate.rs b/asport/src/protocol/dissociate.rs new file mode 100644 index 0000000..d2a2feb --- /dev/null +++ b/asport/src/protocol/dissociate.rs @@ -0,0 +1,48 @@ +/// Command `Dissociate` +/// +/// ```plain +/// +----------+ +/// | ASSOC_ID | +/// +----------+ +/// | 2 | +/// +----------+ +/// ``` +/// +/// where: +/// +/// - `ASSOC_ID` - UDP froward session ID +#[derive(Clone, Debug)] +pub struct Dissociate { + assoc_id: u16, +} + +impl Dissociate { + const TYPE_CODE: u8 = 0x04; + + /// Creates a new `Dissociate` command + pub const fn new(assoc_id: u16) -> Self { + Self { assoc_id } + } + + /// Returns the UDP forward session ID + pub fn assoc_id(&self) -> u16 { + self.assoc_id + } + + /// Returns the command type code + pub const fn type_code() -> u8 { + Self::TYPE_CODE + } + + /// Returns the serialized length of the command + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 2 + } +} + +impl From for (u16,) { + fn from(dissoc: Dissociate) -> Self { + (dissoc.assoc_id,) + } +} diff --git a/asport/src/protocol/heartbeat.rs b/asport/src/protocol/heartbeat.rs new file mode 100644 index 0000000..1cbc48a --- /dev/null +++ b/asport/src/protocol/heartbeat.rs @@ -0,0 +1,31 @@ +/// Command `Heartbeat` +/// ```plain +/// +-+ +/// | | +/// +-+ +/// | | +/// +-+ +/// ``` +#[derive(Clone, Debug)] +pub struct Heartbeat; + +impl Heartbeat { + const TYPE_CODE: u8 = 0x05; + + pub const fn new() -> Self { + Self + } + + pub const fn type_code() -> u8 { + Self::TYPE_CODE + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 0 + } +} + +impl From for () { + fn from(_: Heartbeat) -> Self {} +} diff --git a/asport/src/protocol/mod.rs b/asport/src/protocol/mod.rs new file mode 100644 index 0000000..3afaf47 --- /dev/null +++ b/asport/src/protocol/mod.rs @@ -0,0 +1,165 @@ +use std::{ + fmt::{Display, Formatter, Result as FmtResult}, + mem, + net::SocketAddr, +}; + +#[allow(unused_imports)] +pub use self::{ + client_hello::{ClientHello, ForwardMode, InvalidForwardMode}, + connect::Connect, + dissociate::Dissociate, + heartbeat::Heartbeat, + packet::Packet, + server_hello::ServerHello, +}; + +mod client_hello; +mod heartbeat; +mod server_hello; +mod connect; +mod packet; +mod dissociate; + +pub const VERSION: u8 = 0x00; + +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum Header { + ClientHello(ClientHello), + ServerHello(ServerHello), + Connect(Connect), + Packet(Packet), + Dissociate(Dissociate), + Heartbeat(Heartbeat), +} + +impl Header { + pub const TYPE_CODE_CLIENT_HELLO: u8 = ClientHello::type_code(); + pub const TYPE_CODE_SERVER_HELLO: u8 = ServerHello::type_code(); + pub const TYPE_CODE_CONNECT: u8 = Connect::type_code(); + pub const TYPE_CODE_PACKET: u8 = Packet::type_code(); + pub const TYPE_CODE_DISSOCIATE: u8 = Dissociate::type_code(); + pub const TYPE_CODE_HEARTBEAT: u8 = Heartbeat::type_code(); + + /// Returns the command type code + pub const fn type_code(&self) -> u8 { + match self { + Self::ClientHello(_) => Self::TYPE_CODE_CLIENT_HELLO, + Self::ServerHello(_) => Self::TYPE_CODE_SERVER_HELLO, + Self::Connect(_) => Self::TYPE_CODE_CONNECT, + Self::Packet(_) => Self::TYPE_CODE_PACKET, + Self::Dissociate(_) => Self::TYPE_CODE_DISSOCIATE, + Self::Heartbeat(_) => Self::TYPE_CODE_HEARTBEAT, + } + } + + /// Returns the serialized length of the command + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 2 + match self { + Self::ClientHello(client_hello) => client_hello.len(), + Self::ServerHello(server_hello) => server_hello.len(), + Self::Connect(connect) => connect.len(), + Self::Packet(packet) => packet.len(), + Self::Dissociate(dissociate) => dissociate.len(), + Self::Heartbeat(heartbeat) => heartbeat.len(), + } + } +} + + +/// Socks5-like variable-length field that encodes the network address +/// Domain is not supported because it not possible that the remote address is a domain. +/// +/// ```plain +/// +------+----------+----------+ +/// | ATYP | ADDR | PORT | +/// +------+----------+----------+ +/// | 1 | Variable | 2 | +/// +------+----------+----------+ +/// ``` +/// +/// where: +/// +/// - `ATYP` - the address type +/// - `ADDR` - the address +/// - `PORT` - the port +/// +/// The address type can be one of the following: +/// +/// - `0xff`: None +/// - `0x01`: IPv4 address +/// - `0x04`: IPv6 address +/// +/// Address type `None` is used in `Packet` commands that is not the first fragment of a UDP packet. +/// +/// The port number is encoded in 2 bytes after the Domain name / IP address. +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +pub enum Address { + None, + SocketAddress(SocketAddr), +} + +impl Address { + pub const TYPE_CODE_NONE: u8 = 0xff; + pub const TYPE_CODE_IPV4: u8 = 0x01; + pub const TYPE_CODE_IPV6: u8 = 0x04; + + /// Returns the address type code + pub const fn type_code(&self) -> u8 { + match self { + Self::None => Self::TYPE_CODE_NONE, + Self::SocketAddress(addr) => match addr { + SocketAddr::V4(_) => Self::TYPE_CODE_IPV4, + SocketAddr::V6(_) => Self::TYPE_CODE_IPV6, + }, + } + } + + /// Returns the serialized length of the address + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 1 + match self { + Address::None => 0, + Address::SocketAddress(SocketAddr::V4(_)) => 4 + 2, + Address::SocketAddress(SocketAddr::V6(_)) => 16 + 2, + } + } + + /// Takes the address out, leaving a `None` in its place + pub fn take(&mut self) -> Self { + mem::take(self) + } + + /// Returns `true` if the address is `None` + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + + /// Returns `true` if the address is an IPv4 address + pub fn is_ipv4(&self) -> bool { + matches!(self, Self::SocketAddress(SocketAddr::V4(_))) + } + + /// Returns `true` if the address is an IPv6 address + pub fn is_ipv6(&self) -> bool { + matches!(self, Self::SocketAddress(SocketAddr::V6(_))) + } +} + +impl Display for Address { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match self { + Self::None => write!(f, "none"), + Self::SocketAddress(addr) => write!(f, "{addr}"), + } + } +} + +impl Default for Address { + fn default() -> Self { + Self::None + } +} diff --git a/asport/src/protocol/packet.rs b/asport/src/protocol/packet.rs new file mode 100644 index 0000000..4461078 --- /dev/null +++ b/asport/src/protocol/packet.rs @@ -0,0 +1,105 @@ +use super::Address; + +/// Command `Packet` +/// ```plain +/// +----------+--------+------------+---------+------+----------+ +/// | ASSOC_ID | PKT_ID | FRAG_TOTAL | FRAG_ID | SIZE | ADDR | +/// +----------+--------+------------+---------+------+----------+ +/// | 2 | 2 | 1 | 1 | 2 | Variable | +/// +----------+--------+------------+---------+------+----------+ +/// ``` +/// +/// where: +/// +/// - `ASSOC_ID` - UDP forward session ID +/// - `PKT_ID` - UDP packet ID +/// - `FRAG_TOTAL` - total number of fragments of the UDP packet +/// - `FRAG_ID` - fragment ID of the UDP packet +/// - `SIZE` - length of the (fragmented) UDP packet +/// - `ADDR` - target (from client) or source (from server) address +#[derive(Clone, Debug)] +pub struct Packet { + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, +} + +impl Packet { + const TYPE_CODE: u8 = 0x03; + + /// Creates a new `Packet` command + pub const fn new( + assoc_id: u16, + pkt_id: u16, + frag_total: u8, + frag_id: u8, + size: u16, + addr: Address, + ) -> Self { + Self { + assoc_id, + pkt_id, + frag_total, + frag_id, + size, + addr, + } + } + + /// Returns the UDP forward session ID + pub fn assoc_id(&self) -> u16 { + self.assoc_id + } + + /// Returns the packet ID + pub fn pkt_id(&self) -> u16 { + self.pkt_id + } + + /// Returns the total number of fragments of the UDP packet + pub fn frag_total(&self) -> u8 { + self.frag_total + } + + /// Returns the fragment ID of the UDP packet + pub fn frag_id(&self) -> u8 { + self.frag_id + } + + /// Returns the length of the (fragmented) UDP packet + pub fn size(&self) -> u16 { + self.size + } + + /// Returns the target (from client) or source (from server) address + pub fn addr(&self) -> &Address { + &self.addr + } + + /// Returns the command type code + pub const fn type_code() -> u8 { + Self::TYPE_CODE + } + + /// Returns the serialized length of the command + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 2 + 2 + 1 + 1 + 2 + self.addr.len() + } +} + +impl From for (u16, u16, u8, u8, u16, Address) { + fn from(pkt: Packet) -> Self { + ( + pkt.assoc_id, + pkt.pkt_id, + pkt.frag_total, + pkt.frag_id, + pkt.size, + pkt.addr, + ) + } +} diff --git a/asport/src/protocol/server_hello.rs b/asport/src/protocol/server_hello.rs new file mode 100644 index 0000000..21bf6a1 --- /dev/null +++ b/asport/src/protocol/server_hello.rs @@ -0,0 +1,65 @@ +/// Command `ServerHello` +/// ```plain +/// +------+------+ +/// | CODE | PORT | +/// +------+------+ +/// | 1 | 2 | +/// +------+------+ +/// ``` +/// +/// where: +/// - `CODE` - the result of Handshake +/// - `PORT` - the port that the server listens on for this client +#[derive(Clone, Debug)] +pub enum ServerHello { + Success(u16), + AuthFailed, + BindFailed, + PortDenied, + NetworkDenied, +} + +impl ServerHello { + pub const TYPE_CODE: u8 = 0x01; + + pub const HANDSHAKE_CODE_SUCCESS: u8 = 0x00; + pub const HANDSHAKE_CODE_AUTH_FAILED: u8 = 0x01; + pub const HANDSHAKE_CODE_BIND_FAILED: u8 = 0x02; + pub const HANDSHAKE_CODE_PORT_DENIED: u8 = 0x03; + pub const HANDSHAKE_CODE_NETWORK_DENIED: u8 = 0x04; + + pub const fn type_code() -> u8 { + Self::TYPE_CODE + } + + pub fn handshake_code(&self) -> u8 { + match self { + Self::Success(_) => Self::HANDSHAKE_CODE_SUCCESS, + Self::AuthFailed => Self::HANDSHAKE_CODE_AUTH_FAILED, + Self::BindFailed => Self::HANDSHAKE_CODE_BIND_FAILED, + Self::PortDenied => Self::HANDSHAKE_CODE_PORT_DENIED, + Self::NetworkDenied => Self::HANDSHAKE_CODE_NETWORK_DENIED, + } + } + + pub fn port(&self) -> Option { + match self { + Self::Success(port) => Some(*port), + _ => None, + } + } + + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + 1 + match self { + Self::Success(_) => 2, + _ => 0, + } + } +} + +impl From for (u8, Option) { + fn from(server_hello: ServerHello) -> (u8, Option) { + (server_hello.handshake_code(), server_hello.port()) + } +} diff --git a/asport/src/unmarshal.rs b/asport/src/unmarshal.rs new file mode 100644 index 0000000..24141a3 --- /dev/null +++ b/asport/src/unmarshal.rs @@ -0,0 +1,315 @@ +use std::{ + io::{Error as IoError, Read}, + net::SocketAddr, + string::FromUtf8Error, +}; + +use futures_util::{AsyncRead, AsyncReadExt}; +use thiserror::Error; +use uuid::{Error as UuidError, Uuid}; + +use crate::{Address, ClientHello, Connect, Dissociate, ForwardMode, Header, Heartbeat, Packet, + protocol::InvalidForwardMode, ServerHello, VERSION, +}; + +impl Header { + /// Unmarshals a header from an `AsyncRead` stream + #[cfg(feature = "async_marshal")] + pub async fn async_unmarshal(s: &mut (impl AsyncRead + Unpin)) -> Result { + let mut buf = [0; 1]; + s.read_exact(&mut buf).await?; + let ver = buf[0]; + + if ver != VERSION { + return Err(UnmarshalError::InvalidVersion(ver)); + } + + let mut buf = [0; 1]; + s.read_exact(&mut buf).await?; + let cmd = buf[0]; + + match cmd { + Header::TYPE_CODE_CLIENT_HELLO => ClientHello::async_read(s).await.map(Self::ClientHello), + Header::TYPE_CODE_SERVER_HELLO => ServerHello::async_read(s).await.map(Self::ServerHello), + Header::TYPE_CODE_CONNECT => Connect::async_read(s).await.map(Self::Connect), + Header::TYPE_CODE_PACKET => Packet::async_read(s).await.map(Self::Packet), + Header::TYPE_CODE_DISSOCIATE => Dissociate::async_read(s).await.map(Self::Dissociate), + Header::TYPE_CODE_HEARTBEAT => Heartbeat::async_read(s).await.map(Self::Heartbeat), + _ => Err(UnmarshalError::InvalidCommand(cmd)), + } + } + + /// Unmarshals a header from a `Read` stream + #[cfg(feature = "marshal")] + pub fn unmarshal(s: &mut impl Read) -> Result { + let mut buf = [0; 1]; + s.read_exact(&mut buf)?; + let ver = buf[0]; + + if ver != VERSION { + return Err(UnmarshalError::InvalidVersion(ver)); + } + + let mut buf = [0; 1]; + s.read_exact(&mut buf)?; + let cmd = buf[0]; + + match cmd { + Header::TYPE_CODE_CLIENT_HELLO => ClientHello::read(s).map(Self::ClientHello), + Header::TYPE_CODE_SERVER_HELLO => ServerHello::read(s).map(Self::ServerHello), + Header::TYPE_CODE_CONNECT => Connect::read(s).map(Self::Connect), + Header::TYPE_CODE_PACKET => Packet::read(s).map(Self::Packet), + Header::TYPE_CODE_DISSOCIATE => Dissociate::read(s).map(Self::Dissociate), + Header::TYPE_CODE_HEARTBEAT => Heartbeat::read(s).map(Self::Heartbeat), + _ => Err(UnmarshalError::InvalidCommand(cmd)), + } + } +} + +impl Address { + #[cfg(feature = "async_marshal")] + async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { + let mut buf = [0; 1]; + s.read_exact(&mut buf).await?; + let type_code = buf[0]; + + match type_code { + Address::TYPE_CODE_NONE => Ok(Self::None), + Address::TYPE_CODE_IPV4 => { + let mut buf = [0; 6]; + s.read_exact(&mut buf).await?; + let ip = [buf[0], buf[1], buf[2], buf[3]]; + let port = u16::from_be_bytes([buf[4], buf[5]]); + Ok(Self::SocketAddress(SocketAddr::from((ip, port)))) + } + Address::TYPE_CODE_IPV6 => { + let mut buf = [0; 18]; + s.read_exact(&mut buf).await?; + let ip = [ + u16::from_be_bytes([buf[0], buf[1]]), + u16::from_be_bytes([buf[2], buf[3]]), + u16::from_be_bytes([buf[4], buf[5]]), + u16::from_be_bytes([buf[6], buf[7]]), + u16::from_be_bytes([buf[8], buf[9]]), + u16::from_be_bytes([buf[10], buf[11]]), + u16::from_be_bytes([buf[12], buf[13]]), + u16::from_be_bytes([buf[14], buf[15]]), + ]; + let port = u16::from_be_bytes([buf[16], buf[17]]); + + Ok(Self::SocketAddress(SocketAddr::from((ip, port)))) + } + _ => Err(UnmarshalError::InvalidAddressType(type_code)), + } + } + + #[cfg(feature = "marshal")] + fn read(s: &mut impl Read) -> Result { + let mut buf = [0; 1]; + s.read_exact(&mut buf)?; + let type_code = buf[0]; + + match type_code { + Address::TYPE_CODE_NONE => Ok(Self::None), + Address::TYPE_CODE_IPV4 => { + let mut buf = [0; 6]; + s.read_exact(&mut buf)?; + let ip = [buf[0], buf[1], buf[2], buf[3]]; + let port = u16::from_be_bytes([buf[4], buf[5]]); + Ok(Self::SocketAddress(SocketAddr::from((ip, port)))) + } + Address::TYPE_CODE_IPV6 => { + let mut buf = [0; 18]; + s.read_exact(&mut buf)?; + let ip = [ + u16::from_be_bytes([buf[0], buf[1]]), + u16::from_be_bytes([buf[2], buf[3]]), + u16::from_be_bytes([buf[4], buf[5]]), + u16::from_be_bytes([buf[6], buf[7]]), + u16::from_be_bytes([buf[8], buf[9]]), + u16::from_be_bytes([buf[10], buf[11]]), + u16::from_be_bytes([buf[12], buf[13]]), + u16::from_be_bytes([buf[14], buf[15]]), + ]; + let port = u16::from_be_bytes([buf[16], buf[17]]); + + Ok(Self::SocketAddress(SocketAddr::from((ip, port)))) + } + _ => Err(UnmarshalError::InvalidAddressType(type_code)), + } + } +} + +impl ClientHello { + #[cfg(feature = "async_marshal")] + async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { + let mut buf = [0; 53]; + s.read_exact(&mut buf).await?; + let uuid = Uuid::from_slice(&buf[..16])?; + let token = TryFrom::try_from(&buf[16..48]).unwrap(); + let forward_mode = ForwardMode::try_from(buf[48]) + .map_err(UnmarshalError::InvalidForwardMode)?; + let start = u16::from_be_bytes([buf[49], buf[50]]); + let end = u16::from_be_bytes([buf[51], buf[52]]); + let expected_port_range = start..=end; + + Ok(Self::new(uuid, token, forward_mode, expected_port_range)) + } + + #[cfg(feature = "marshal")] + fn read(s: &mut impl Read) -> Result { + let mut buf = [0; 53]; + s.read_exact(&mut buf)?; + let uuid = Uuid::from_slice(&buf[..16])?; + let token = TryFrom::try_from(&buf[16..48]).unwrap(); + let forward_mode = ForwardMode::try_from(buf[48]) + .map_err(UnmarshalError::InvalidForwardMode)?; + let start = u16::from_be_bytes([buf[49], buf[50]]); + let end = u16::from_be_bytes([buf[51], buf[52]]); + let expected_port_range = start..=end; + + Ok(Self::new(uuid, token, forward_mode, expected_port_range)) + } +} + +impl ServerHello { + #[cfg(feature = "async_marshal")] + async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { + let mut buf = [0; 1]; + s.read_exact(&mut buf).await?; + let handshake_code = buf[0]; + + match handshake_code { + ServerHello::HANDSHAKE_CODE_SUCCESS => { + let mut buf = [0; 2]; + s.read_exact(&mut buf).await?; + let port = u16::from_be_bytes([buf[0], buf[1]]); + + Ok(Self::Success(port)) + } + ServerHello::HANDSHAKE_CODE_AUTH_FAILED => Ok(Self::AuthFailed), + ServerHello::HANDSHAKE_CODE_BIND_FAILED => Ok(Self::BindFailed), + ServerHello::HANDSHAKE_CODE_PORT_DENIED => Ok(Self::PortDenied), + ServerHello::HANDSHAKE_CODE_NETWORK_DENIED => Ok(Self::NetworkDenied), + _ => Err(UnmarshalError::InvalidHandshakeCode(handshake_code)), + } + } + + #[cfg(feature = "marshal")] + fn read(s: &mut impl Read) -> Result { + let mut buf = [0; 1]; + s.read_exact(&mut buf)?; + let handshake_code = buf[0]; + + match handshake_code { + ServerHello::HANDSHAKE_CODE_SUCCESS => { + let mut buf = [0; 2]; + s.read_exact(&mut buf)?; + let port = u16::from_be_bytes([buf[0], buf[1]]); + + Ok(Self::Success(port)) + } + ServerHello::HANDSHAKE_CODE_AUTH_FAILED => Ok(Self::AuthFailed), + ServerHello::HANDSHAKE_CODE_BIND_FAILED => Ok(Self::BindFailed), + ServerHello::HANDSHAKE_CODE_PORT_DENIED => Ok(Self::PortDenied), + ServerHello::HANDSHAKE_CODE_NETWORK_DENIED => Ok(Self::NetworkDenied), + _ => Err(UnmarshalError::InvalidHandshakeCode(handshake_code)), + } + } +} + +impl Connect { + #[cfg(feature = "async_marshal")] + async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { + Ok(Self::new(Address::async_read(s).await?)) + } + + #[cfg(feature = "marshal")] + fn read(s: &mut impl Read) -> Result { + Ok(Self::new(Address::read(s)?)) + } +} + +impl Packet { + #[cfg(feature = "async_marshal")] + async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { + let mut buf = [0; 8]; + s.read_exact(&mut buf).await?; + + let assoc_id = u16::from_be_bytes([buf[0], buf[1]]); + let pkt_id = u16::from_be_bytes([buf[2], buf[3]]); + let frag_total = buf[4]; + let frag_id = buf[5]; + let size = u16::from_be_bytes([buf[6], buf[7]]); + let addr = Address::async_read(s).await?; + + Ok(Self::new(assoc_id, pkt_id, frag_total, frag_id, size, addr)) + } + + #[cfg(feature = "marshal")] + fn read(s: &mut impl Read) -> Result { + let mut buf = [0; 8]; + s.read_exact(&mut buf)?; + + let assoc_id = u16::from_be_bytes([buf[0], buf[1]]); + let pkt_id = u16::from_be_bytes([buf[2], buf[3]]); + let frag_total = buf[4]; + let frag_id = buf[5]; + let size = u16::from_be_bytes([buf[6], buf[7]]); + let addr = Address::read(s)?; + + Ok(Self::new(assoc_id, pkt_id, frag_total, frag_id, size, addr)) + } +} + +impl Dissociate { + #[cfg(feature = "async_marshal")] + async fn async_read(s: &mut (impl AsyncRead + Unpin)) -> Result { + let mut buf = [0; 2]; + s.read_exact(&mut buf).await?; + let assoc_id = u16::from_be_bytes(buf); + Ok(Self::new(assoc_id)) + } + + #[cfg(feature = "marshal")] + fn read(s: &mut impl Read) -> Result { + let mut buf = [0; 2]; + s.read_exact(&mut buf)?; + let assoc_id = u16::from_be_bytes(buf); + Ok(Self::new(assoc_id)) + } +} + + +impl Heartbeat { + #[cfg(feature = "async_marshal")] + async fn async_read(_s: &mut (impl AsyncRead + Unpin)) -> Result { + Ok(Self::new()) + } + + #[cfg(feature = "marshal")] + fn read(_s: &mut impl Read) -> Result { + Ok(Self::new()) + } +} + +/// Errors that can occur when unmarshalling a packet +#[derive(Debug, Error)] +pub enum UnmarshalError { + #[error(transparent)] + Io(#[from] IoError), + #[error("invalid version: {0}")] + InvalidVersion(u8), + #[error("invalid command: {0}")] + InvalidCommand(u8), + #[error("invalid UUID: {0}")] + InvalidUuid(#[from] UuidError), + #[error("invalid address type: {0}")] + InvalidAddressType(u8), + #[error("invalid handshake code: {0}")] + InvalidHandshakeCode(u8), + #[error(transparent)] + InvalidForwardMode(#[from] InvalidForwardMode), + #[error("address parsing error: {0}")] + AddressParse(#[from] FromUtf8Error), +} diff --git a/client.example.toml b/client.example.toml new file mode 100644 index 0000000..3afe30b --- /dev/null +++ b/client.example.toml @@ -0,0 +1,146 @@ +# Asport Client Configuration + +# Asport server's address. +# Format: : +# Note: If host is an IPv6 address, it should be enclosed in square brackets. +server = "[2001:db8::1]:443" + +# Forwarding destination address. +# Format: : +# Note: If host is an IPv6 address, it should be enclosed in square brackets. +local = "[::1]:2024" + +# UUID of the user. +# Note: This UUID is only an example. You should replace it with your own UUID. +uuid = "00000000-0000-0000-0000-000000000000" + +# Password of the user. +# Note: This password is only an example. You should replace it with your own password. +password = "password" + +# Network type for forwarding. +# Default: "both" +# Options: "tcp", "udp", "both" +network = "both" + +# The method of forwarding UDP packets. +# Default: "native" +# Options: "native", "quic" +# Note: Native mode use QUIC unreliable datagram for forwarding. It's low latency and overhead but may lose packets (unreliable). +# QUIC mode use QUIC uniderectional stream for forwarding. It's reliable but has higher latency and overhead. +udp_forward_mode = "native" + +# Inactive timeout for local UDP sockets. +# Default: "60s" +# Note: This behavior is similar to the NAT. If a socket is inactive for a long time, it will be closed. +udp_timeout = "60s" + +# Expected port range for the server. +# Default: [1, 65535] +# Format 1: +# Format 2: [, ] +# Format 3: { start = , end = } +# Alias: port +# Note: If server don't have any available port in this range, the handshake will fail. +expected_port_range = [1, 65535] + +# SNI for QUIC handshake. +# Default: in `server` +# Note: If the host of `server` is an IP address you will also need to set this. +server_name = "asport.akinokaede.com" + +# Disable SNI for QUIC handshake. +# Default: false +disable_sni = false + +# Congestion control algorithm. +# Default: "cubic" +# Options: "cubic", "reno", "bbr" +# Note: BBR may incrase transmission rate. +congestion_control = "cubic" + +# ALPN for QUIC handshake. +# Default: ["asport"] +# Format: ["", "", ...] +# Note: If you want to bypass some DPI, you can change this to ["h3"]. And you should also change the server's ALPN to ["h3"]. +alpn = ["asport"] + +# Enable 0-RTT handshake. +# Default: false +zero_rtt_handshake = false + +# Health check interval. +# Default: "20s" +# Note: Client will check the connection is not closed in this interval. If the connection is closed, it will try to reconnect. +healthy_check = "20s" + +# Timeout for connection establishment. +# Default: "8s" +timeout = "8s" + +# Handshake timeout. +# Default: "3s" +# Note: Connection will be closed if the handshake is not completed in this timeout. +handshake_timeout = "3s" + +# Task negotiation timeout. +# Default: "3s" +# Note: Accepting stream tasks timeout. +task_negotiation_timeout = "3s" + +# Heartbeat interval. +# Default: "3s" +# Note: Client will send a heartbeat packet to the server in this interval. +heartbeat = "3s" + +# Maximum packet size. +# Default: 1350 +# Note: It just make impact on Native mode. This value should be less than the MTU of the network. +# Default value (1350) is conservative and should work in most cases. If you want to get better performance, you can +# increase this value. In most cases, 1500 is a good choice. If you use PPPoE, you can set it to 1492. And 9000 is the +# common value for Ethernet jumbo frame. +max_packet_size = 1350 + +# Send window size. +# Default: 16_777_216 +send_window = 16_777_216 + +# Receive window size. +# Default: 8_388_608 +receive_window = 8_388_608 + +# Interval for removing packet fragments that can not be reassembled within the specified timeout. +# Default: "3s" +gc_interval = "3s" + +# Timeout for packet fragments that can not be reassembled, and it will be removed. +gc_lifetime = "15s" + +# Send PROXY protocol to local socket. +# Default: "disable" +# Options: "disable", "v1", "v2" +# Note: PROXY protocol is a protocol used to send original source address and port to the local socket. It's designed +# by HAProxy. V1 is human-readable and only support TCP. V2 is binary and support both TCP and UDP. +# Warning: Local socket cannon get true source address if it's listening on IPv4, and source address is an IPv6 address. +# See also: https://www.haproxy.org/download/2.4/doc/proxy-protocol.txt +proxy_protocol = "disable" + +# Log level. +# Default: "warn" +# Options: "trace", "debug", "info", "warn", "error", "off" +# Note: If you want sumbit a bug report, you should set this to "trace" or "debug" +log_level = "warn" + +# Certificate verification. +# Note: You may need to set this if you use a self-signed certificate. +[certificates] + +# Path to the root certificates. +# Format: ["", "", ...] +# Note: DER and PEM format are supported. +paths = ["path/to/exmaple.crt"] + +# Disable system root certificates. +# Default: false +# Note: If you set this to true, the system root certificates will not be used. +disable_native = false \ No newline at end of file diff --git a/client.quick.example.toml b/client.quick.example.toml new file mode 100644 index 0000000..e454a12 --- /dev/null +++ b/client.quick.example.toml @@ -0,0 +1,70 @@ +# Asport Client Quick-start Configuration + +# Asport server's address. +# Format: : +# Note: If host is an IPv6 address, it should be enclosed in square brackets. +# The port must be the same as the server's port. +server = "asport.akinokae.de:443" + +# Forwarding destination address. +# Format: : +# Note: If the destination listens on an IPv6 address, you should use it, for example, "[::1]:2024". +local = "127.0.0.1:2024" + +# UUID of the user. +# Note: This UUID is only an example. You should replace it with your own UUID. +# This UUID must also be in the server's configuration. +uuid = "00000000-0000-0000-0000-000000000000" + +# Password of the user. +# Note: This password is only an example. You should replace it with your own password. +password = "password" + +# Network type for forwarding. +# Default: "both" +# Options: "tcp", "udp", "both" +# Note: ["tcp", "udp"] is also supported, it's equivalent to "both". +# For example, HTTP, TLS, SSH, and Minecraft: Java Edition use TCP. DNS, NTP, QUIC, and Minecraft: Bedrock Edition use UDP. +# RDP and VNC use both TCP and UDP. +network = "both" + +# The method of forwarding UDP packets. +# Default: "native" +# Options: "native", "quic" +# Note: Native mode use QUIC unreliable datagram for forwarding. It's low latency and overhead but may lose packets (unreliable). +# QUIC mode use QUIC uniderectional stream for forwarding. It's reliable but has higher latency and overhead. +# If your network has a high packet loss rate, you can try QUIC mode. +udp_forward_mode = "native" + +# Expected port range for the server. +# Default: [1, 65535] +# Format 1: +# Format 2: [, ] +# Format 3: { start = , end = } +# Alias: port +# Note: If server don't have any available port in this range, the handshake will fail. +# Use single port if you want to use a fixed port. +port = 2024 + +# SNI for QUIC handshake. +# Default: in `server` +# Note: If the host of `server` is an IP address you will also need to set this. +# You don't need to set this if the host of `server` is its domain name. +server_name = "asport.akinokaede.com" + +# Congestion control algorithm. +# Default: "cubic" +# Options: "cubic", "reno", "bbr" +# Note: BBR may incrase transmission rate. +congestion_control = "bbr" + +# ALPN for QUIC handshake. +# Default: ["asport"] +# Note: set to ["h3"] to bypass some DPI. +alpn = ["h3"] + +# Log level. +# Default: "warn" +# Options: "trace", "debug", "info", "warn", "error", "off" +# Note: If you want sumbit a bug report, you should set this to "trace" or "debug" +log_level = "warn" \ No newline at end of file diff --git a/release/systemd/system/asport-client.service b/release/systemd/system/asport-client.service new file mode 100644 index 0000000..73eb415 --- /dev/null +++ b/release/systemd/system/asport-client.service @@ -0,0 +1,18 @@ +[Unit] +Description=Asport Client +Documentation=https://github.com/AkinoKaede/asport +After=network-online.target + +[Service] +User=asport +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +ExecStart=/usr/local/bin/asport-client --config /usr/local/etc/asport/client.toml +Restart=on-failure +RestartPreventExitStatus=23 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/release/systemd/system/asport-client@.service b/release/systemd/system/asport-client@.service new file mode 100644 index 0000000..6f0e94a --- /dev/null +++ b/release/systemd/system/asport-client@.service @@ -0,0 +1,18 @@ +[Unit] +Description=Asport Client +Documentation=https://github.com/AkinoKaede/asport +After=network-online.target + +[Service] +User=asport +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +ExecStart=/usr/local/bin/asport-client --config /usr/local/etc/asport/%i.toml +Restart=on-failure +RestartPreventExitStatus=23 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/release/systemd/system/asport-server.service b/release/systemd/system/asport-server.service new file mode 100644 index 0000000..195f2f0 --- /dev/null +++ b/release/systemd/system/asport-server.service @@ -0,0 +1,18 @@ +[Unit] +Description=Asport Server +Documentation=https://github.com/AkinoKaede/asport +After=network-online.target + +[Service] +User=asport +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +ExecStart=/usr/local/bin/asport-server --config /usr/local/etc/asport/server.toml +Restart=on-failure +RestartPreventExitStatus=23 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/release/systemd/system/asport-server@.service b/release/systemd/system/asport-server@.service new file mode 100644 index 0000000..bc73888 --- /dev/null +++ b/release/systemd/system/asport-server@.service @@ -0,0 +1,18 @@ +[Unit] +Description=Asport Server +Documentation=https://github.com/AkinoKaede/asport +After=network-online.target + +[Service] +User=asport +CapabilityBoundingSet=CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_BIND_SERVICE +NoNewPrivileges=true +ExecStart=/usr/local/bin/asport-server --config /usr/local/etc/asport/%i.toml +Restart=on-failure +RestartPreventExitStatus=23 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/server.example.toml b/server.example.toml new file mode 100644 index 0000000..f802a5c --- /dev/null +++ b/server.example.toml @@ -0,0 +1,117 @@ +# Asport Server Configuration + +# Listen address. +# Format: : +# Note: We recommend use [::] for public servers. If you want to listen on IPv4 only, use 0.0.0.0. +server = "[::]:443" + +# Path to the certificate file. +# Note: DER and PEM format are supported. +certificate = "path/to/cert.pem" + +# Path to the private key file. +# Note: DER and PEM format are supported. +private_key = "path/to/key.pem" + +# Congestion control algorithm. +# Default: "cubic" +# Options: "cubic", "reno", "bbr" +# Note: BBR may incrase transmission rate. +congestion_control = "cubic" + +# ALPN for QUIC handshake. +# Default: ["asport"] +# Format: ["", "", ...] +# Note: If you want to bypass some DPI, you can change this to ["h3"]. And you should also change the client's ALPN to ["h3"]. +alpn = ["asport"] + +# Enable 0-RTT handshake. +# Default: false +zero_rtt_handshake = false + +# Listen on IPv6 only. +# Default: NOT SET +# Note: Even if you set it to false, and the IP in `server` is an IPv4, software will PANIC. +# It's recommended to NOT SET this option. +only_v6 = false + +# Handshake timeout. +# Default: "3s" +# Note: Connection will be closed if the handshake is not completed in this timeout. +handshake_timeout = "3s" + +# Authentication failed reply. +# Default: true +# Note: If set to true, server will send reply to client when authentication failed. Otherwise, server will close the connection. +# If you want to hide your server from probing, you can set it to false. +authentication_failed_reply = true + +# Task negotiation timeout. +# Default: "3s" +# Note: Accepting stream tasks timeout. +task_negotiation_timeout = "3s" + +# Maximum idle time. +# Default: "10s" +# Note: Connection will be closed if it's idle for this time. +max_idle_time = "10s" + +# Maximum packet size. +# Default: 1350 +# Note: It just make impact on Native mode. This value should be less than the MTU of the network. +# Default value (1350) is conservative and should work in most cases. If you want to get better performance, you can +# increase this value. In most cases, 1500 is a good choice. If you use PPPoE, you can set it to 1492. And 9000 is the +# common value for Ethernet jumbo frame. +max_packet_size = 1350 + +# Send window size. +# Default: 16_777_216 +send_window = 16_777_216 + +# Receive window size. +# Default: 8_388_608 +receive_window = 8_388_608 + +# Log level. +# Default: "warn" +# Options: "trace", "debug", "info", "warn", "error", "off" +# Note: If you want sumbit a bug report, you should set this to "trace" or "debug" +log_level = "warn" + +# Reverse proxies configuration. +# Note: Multiple proxies are supported. +[[proxies]] + +# Bind address for reverse proxy. +# Format: +# Note: We recommend use [::] for public servers. If you want to listen on IPv4 only, use 0.0.0.0. +bind_ip = "[::]" + +# Allow ports. +# Default: +# Linux and Android: software will get ephemeral ports range from system. If failed, it will use 32768-60999. +# macOS, iOS and FreeBSD: software will get ephemeral ports range from system. If failed, it will use 49152-65535. +# Windows and other systems: software will use 49152-65535. +# Format 1: +# Format 2: { start = , end = } +# Format 3: [, , { start = , end = }, ...] +allow_ports = { start = 49152, end = 65535 } + +# Listen on IPv6 only. +# Default: NOT SET +# Note: Even if you set it to false, and `bind_ip` is an IPv4, software will PANIC. +# It's recommended to NOT SET this option. +only_v6 = false + +# Allow network. +# Default: "both" +# Options: "tcp", "udp", "both" +# Note: ["tcp", "udp"] is also supported, it's equivalent to "both". +allow_network = "both" + +# Users configuration. +# Format: = "" +# Note: UUID must be unique in all proxies. +[proxies.users] +00000000-0000-0000-0000-000000000000 = "password" +00000000-0000-0000-0000-000000000001 = "password" \ No newline at end of file diff --git a/server.quick.example.toml b/server.quick.example.toml new file mode 100644 index 0000000..60945c6 --- /dev/null +++ b/server.quick.example.toml @@ -0,0 +1,58 @@ +# Asport Server Quick-start Configuration + +# Listen address. +# Format: : +# Note: If you want to listen on localhost, use "[::1]:443". +# Port 443 may be blocked by some ISPs. You can use other ports such as 4443. +server = "[::]:443" + +# Path to the certificate file. +# Note: DER and PEM format are supported. +# You can use acme.sh to get a free certificate. +# See also: https://acme.sh +certificate = "path/to/cert.pem" + +# Path to the private key file. +# Note: DER and PEM format are supported. +private_key = "path/to/key.pem" + +# Congestion control algorithm. +# Default: "cubic" +# Options: "cubic", "reno", "bbr" +# Note: BBR may incrase transmission rate. +congestion_control = "bbr" + +# ALPN for QUIC handshake. +# Default: ["asport"] +# Format: ["", "", ...] +# Note: Set it to ["h3"] if you want to bypass some DPI. And you should also change the client's ALPN to ["h3"]. +alpn = ["h3"] + +# Authentication failed reply. +# Default: true +# Note: Set it to true for hiding your server from probing. +authentication_failed_reply = false + +# Log level. +# Default: "warn" +# Options: "trace", "debug", "info", "warn", "error", "off" +# Note: If you want sumbit a bug report, you should set this to "trace" or "debug" +log_level = "warn" + +# Reverse proxies configuration. +# Note: Multiple proxies are supported. +[[proxies]] + +# Allow ports. +# Note: If you want to allow all ports, you can set it to { start = 1, end = 65535 }. +# See more details in full exmaple configuration. +allow_ports = { start = 1, end = 63335 } + +# Users configuration. +# Format: = "" +# Note: UUID must be unique in all proxies. +# You can use `uuidgen` tools to generate a UUID. +# The example UUID and password are only examples. You MUST replace them with your own UUID and password. +[proxies.users] +00000000-0000-0000-0000-000000000000 = "password" +00000000-0000-0000-0000-000000000001 = "password" \ No newline at end of file