diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..408c0f3 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,4 @@ +[env] +# Each interface needs 1 IP allocated to the WireGuard peer IP. +# "8" = 7 tunnels per protocol. +SMOLTCP_IFACE_MAX_ADDR_COUNT = "8" diff --git a/.github/ci/macos-install-packages b/.github/ci/macos-install-packages index afb1102..a4ece81 100755 --- a/.github/ci/macos-install-packages +++ b/.github/ci/macos-install-packages @@ -1,6 +1,6 @@ #!/bin/sh -brew install asciidoctor +# brew install asciidoctor -brew install openssl@1.1 -cp /usr/local/opt/openssl@1.1/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig/ +# brew install openssl@1.1 +# cp /usr/local/opt/openssl@1.1/lib/pkgconfig/*.pc /usr/local/lib/pkgconfig/ diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..382be60 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + rebase-strategy: "disabled" diff --git a/.github/onetun.png b/.github/onetun.png new file mode 100644 index 0000000..24bfcc6 Binary files /dev/null and b/.github/onetun.png differ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb404a0..e216219 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,7 +10,7 @@ jobs: matrix: rust: - stable - - 1.55.0 + - 1.80.0 steps: - name: Checkout sources uses: actions/checkout@v2 @@ -26,6 +26,12 @@ jobs: with: command: check + - name: Run cargo check without default features + uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features + test: name: Test Suite runs-on: ubuntu-latest @@ -33,7 +39,7 @@ jobs: matrix: rust: - stable - - 1.55.0 + - 1.80.0 steps: - name: Checkout sources uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3917b6..39b9be6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: run: echo "${{ env.VERSION }}" > artifacts/release-version - name: Upload artifacts - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v4 with: name: artifacts path: artifacts @@ -75,20 +75,28 @@ jobs: RUST_BACKTRACE: 1 strategy: matrix: - build: [ linux-amd64, macos-intel, windows ] + build: [ linux-amd64, linux-aarch64, macos-aarch64, windows ] include: - build: linux-amd64 - os: ubuntu-18.04 + os: ubuntu-latest rust: stable target: x86_64-unknown-linux-musl - - build: macos-intel + cross: true + - build: linux-aarch64 + os: ubuntu-latest + rust: stable + target: aarch64-unknown-linux-musl + cross: true + - build: macos-aarch64 os: macos-latest rust: stable - target: x86_64-apple-darwin + target: aarch64-apple-darwin + cross: false - build: windows os: windows-2019 rust: stable target: x86_64-pc-windows-msvc + cross: false steps: - name: Checkout repository @@ -97,7 +105,7 @@ jobs: fetch-depth: 1 - name: Install packages (Ubuntu) - if: matrix.os == 'ubuntu-18.04' + if: matrix.os == 'ubuntu-latest' run: | .github/ci/ubuntu-install-packages - name: Install packages (macOS) @@ -113,7 +121,7 @@ jobs: target: ${{ matrix.target }} - name: Get release download URL - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: artifacts path: artifacts @@ -126,17 +134,24 @@ jobs: echo "release upload url: $release_upload_url" - name: Build onetun binary - run: cargo build --release + shell: bash + run: | + if [ "${{ matrix.cross }}" = "true" ]; then + cargo install cross + cross build --release --target ${{ matrix.target }} + else + cargo build --release --target ${{ matrix.target }} + fi - name: Prepare onetun binary shell: bash run: | mkdir -p ci/assets if [ "${{ matrix.build }}" = "windows" ]; then - cp "target/release/onetun.exe" "ci/assets/onetun.exe" + cp "target/${{ matrix.target }}/release/onetun.exe" "ci/assets/onetun.exe" echo "ASSET=onetun.exe" >> $GITHUB_ENV else - cp "target/release/onetun" "ci/assets/onetun-${{ matrix.build }}" + cp "target/${{ matrix.target }}/release/onetun" "ci/assets/onetun-${{ matrix.build }}" echo "ASSET=onetun-${{ matrix.build }}" >> $GITHUB_ENV fi diff --git a/.gitignore b/.gitignore index 911ee60..7a1777c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /.idea .envrc *.log +*.pcap +.DS_Store diff --git a/Cargo.lock b/Cargo.lock index 0d286ad..f8e12f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,80 +1,101 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" -version = "0.16.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61f2b7f93d2c7d2b08263acaa4a363b3e276806c68af6134c44f523bf1aacd" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" [[package]] -name = "ascii" -version = "0.9.3" +name = "async-recursion" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.61" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a905d892734eea339e896738c14b9afce22b5318f64b951e70bf3844419b01" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets", ] [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bitflags" @@ -83,60 +104,79 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "boringtun" -version = "0.3.0" -source = "git+https://github.com/cloudflare/boringtun?branch=master#fbcf2689e7776a5af805c5a38feb5c8988829980" +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "base64", - "clap", - "daemonize", - "hex", - "ip_network", - "ip_network_table", - "jni", - "libc", - "parking_lot", - "ring", - "slog", - "slog-term", - "untrusted", + "digest", ] [[package]] -name = "boxfnonce" -version = "0.1.1" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5988cb1d626264ac94100be357308f29ff7cbdd3b36bda27f450a4ee3f713426" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "boringtun" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751787b019c674b9ac353f4eaa285e6711c21badb421cd8c199bf2c83b727f29" +dependencies = [ + "aead", + "base64", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "libc", + "nix", + "parking_lot", + "rand_core", + "ring", + "tracing", + "untrusted 0.9.0", + "x25519-dalek", +] [[package]] name = "bumpalo" -version = "3.7.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.1.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.0.71" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -145,108 +185,197 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "chrono" -version = "0.4.19" +name = "chacha20" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ - "libc", - "num-integer", - "num-traits", - "time", - "winapi", + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", ] [[package]] name = "clap" -version = "2.33.3" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ - "bitflags", + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +dependencies = [ + "anstyle", + "clap_lex", "strsim", - "textwrap", - "unicode-width", + "terminal_size", ] [[package]] -name = "combine" -version = "3.8.1" +name = "clap_lex" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii", - "byteorder", - "either", - "memchr", - "unreachable", -] +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] -name = "daemonize" -version = "0.4.1" +name = "cpufeatures" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c24513e34f53b640819f0ac9f705b673fcf4006d7aab8778bee72ebfc89815" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ - "boxfnonce", "libc", ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "crypto-common" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7" dependencies = [ "cfg-if", - "dirs-sys-next", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "curve25519-dalek-derive" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "libc", - "redox_users", - "winapi", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "either" -version = "1.6.1" +name = "defmt" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3" +dependencies = [ + "thiserror", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] [[package]] name = "env_logger" -version = "0.6.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", ] [[package]] -name = "error-chain" -version = "0.12.4" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ - "backtrace", - "version_check", + "libc", + "windows-sys 0.59.0", ] [[package]] -name = "futures" -version = "0.3.17" +name = "fiat-crypto" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -259,9 +388,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -269,15 +398,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -286,18 +415,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -305,23 +432,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -331,16 +457,24 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] [[package]] -name = "getrandom" -version = "0.2.3" +name = "generic-array" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +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", @@ -349,18 +483,40 @@ dependencies = [ [[package]] name = "gimli" -version = "0.25.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a01e0497841a3b2db4f8afa483cce65f7e96a3498bd6c541734792aeac8fe7" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -369,34 +525,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "humantime" -version = "1.3.0" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "quick-error", + "digest", ] [[package]] -name = "instant" -version = "0.1.11" +name = "humantime" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "indexmap" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ - "cfg-if", + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", ] [[package]] name = "ip_network" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee15951c035f79eddbef745611ec962f63f4558f1dadf98ab723cc603487c6f" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" [[package]] name = "ip_network_table" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa59c26e61245c366c7d615b56524cdc7c9104ba15c0ebfc0f09ddb8cdf728be" +checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" dependencies = [ "ip_network", "ip_network_table-deps-treebitmap", @@ -409,72 +581,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" [[package]] -name = "jni" -version = "0.10.2" +name = "is-terminal" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecfa3b81afc64d9a6539c4eece96ac9a93c551c713a313800dade8e33d7b5c1" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "cesu8", - "combine", - "error-chain", - "jni-sys", - "log", - "walkdir", + "hermit-abi", + "libc", + "windows-sys 0.52.0", ] -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "a865e038f7f6ed956f788f0d7d60c541fff74c7bd74272c5d4cf15c63743e705" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libc" +version = "0.2.170" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" [[package]] -name = "libc" -version = "0.2.103" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.5" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ + "autocfg", "scopeguard", ] -[[package]] -name = "lockfree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ee94b5ad113c7cb98c5a040f783d0952ee4fe100993881d1673c2cb002dd23" -dependencies = [ - "owned-alloc", -] - [[package]] name = "log" -version = "0.4.14" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "managed" @@ -484,147 +637,129 @@ checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" [[package]] name = "memchr" -version = "2.4.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +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.4.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", - "autocfg", + "adler2", ] [[package]] name = "mio" -version = "0.7.13" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "log", - "miow", - "ntapi", - "winapi", + "wasi", + "windows-sys 0.52.0", ] [[package]] -name = "miow" -version = "0.3.7" +name = "nix" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", -] - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" -dependencies = [ - "hermit-abi", + "bitflags 1.3.2", + "cfg-if", "libc", ] +[[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.26.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.8.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "onetun" -version = "0.1.10" +version = "0.3.10" dependencies = [ "anyhow", + "async-recursion", + "async-trait", + "base64", "boringtun", + "bytes", "clap", "futures", - "lockfree", "log", + "nom", "pretty_env_logger", + "priority-queue", "rand", "smoltcp", "tokio", + "tracing", ] [[package]] -name = "owned-alloc" -version = "0.2.0" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30fceb411f9a12ff9222c5f824026be368ff15dc2f13468d850c7d3f502205d6" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ - "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", - "instant", "libc", "redox_syscall", "smallvec", - "winapi", + "windows-targets", ] [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -633,68 +768,101 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "ppv-lite86" -version = "0.2.14" +name = "platforms" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741" +checksum = "d43467300237085a4f9e864b937cf0bc012cef7740be12be1a48b10d2c8a3701" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_env_logger" -version = "0.3.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "717ee476b1690853d222af4634056d830b5197ffd747726a9a1eee6da9f49074" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" dependencies = [ - "chrono", "env_logger", "log", ] [[package]] -name = "proc-macro-hack" -version = "0.5.19" +name = "priority-queue" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - -[[package]] -name = "proc-macro-nested" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" - -[[package]] -name = "proc-macro2" -version = "1.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" dependencies = [ - "unicode-xid", + "autocfg", + "equivalent", + "indexmap", ] [[package]] -name = "quick-error" -version = "1.2.3" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] @@ -709,46 +877,39 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom", - "redox_syscall", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -757,9 +918,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" @@ -771,90 +932,117 @@ dependencies = [ "libc", "once_cell", "spin", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] name = "rustc-demangle" -version = "0.1.21" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "rustversion" -version = "1.0.5" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "winapi-util", + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "signal-hook-registry" -version = "1.4.0" +name = "semver" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ - "libc", + "serde_derive", ] [[package]] -name = "slab" -version = "0.4.5" +name = "serde_derive" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" - -[[package]] -name = "slog" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" - -[[package]] -name = "slog-term" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c1e7e5aab61ced6006149ea772770b84a0d16ce0f7885def313e4829946d76" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ - "atty", - "chrono", - "slog", - "term", - "thread_local", + "proc-macro2", + "quote", + "syn", +] + +[[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.7.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smoltcp" -version = "0.8.0" -source = "git+https://github.com/smoltcp-rs/smoltcp?branch=master#35e833e33dfd3e4efc3eb7d5de06bec17c54b011" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" dependencies = [ - "bitflags", + "bitflags 1.3.2", "byteorder", - "libc", + "cfg-if", + "defmt", + "heapless", "log", "managed", ] +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -862,96 +1050,67 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] -name = "strsim" -version = "0.8.0" +name = "stable_deref_trait" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[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 = "1.0.80" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", - "unicode-xid", -] - -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", + "unicode-ident", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "terminal_size" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" dependencies = [ - "unicode-width", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "thread_local" -version = "1.1.3" +name = "thiserror" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" dependencies = [ - "once_cell", + "thiserror-impl", ] [[package]] -name = "time" -version = "0.1.44" +name = "thiserror-impl" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi", - "winapi", -] - -[[package]] -name = "tokio" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "once_cell", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", @@ -959,24 +1118,84 @@ dependencies = [ ] [[package]] -name = "unicode-width" -version = "0.1.9" +name = "tokio" +version = "1.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" dependencies = [ - "void", + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", ] [[package]] @@ -985,54 +1204,44 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" - -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "d15e63b4482863c109d70a7b8706c1e364eb6ea449b201a76c5b89cedcec2d5c" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "8d36ef12e3aaca16ddd3f67922bc63e48e953f126de60bd33ccc0101ef9998cd" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -1041,9 +1250,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "705440e08b42d3e4b36de7d66c944be628d579796b8090bfa3471478a2260051" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1051,9 +1260,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d" dependencies = [ "proc-macro2", "quote", @@ -1064,15 +1273,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "6ee99da9c5ba11bd675621338ef6fa52296b76b83305e9b6e5c77d4c286d6d49" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "a98bc3c33f0fe7e59ad7cd041b89034fa82a7c2d4365ca538dda6cdaf513863c" dependencies = [ "js-sys", "wasm-bindgen", @@ -1096,11 +1305,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] [[package]] @@ -1108,3 +1317,138 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-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 = "x25519-dalek" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[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" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 15d5047..ffb8a64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,48 @@ [package] name = "onetun" -version = "0.1.10" -edition = "2018" +version = "0.3.10" +edition = "2021" +license = "MIT" +description = "A cross-platform, user-space WireGuard port-forwarder that requires no system network configurations." +authors = ["Aram Peres "] +repository = "https://github.com/aramperes/onetun" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", branch = "master" } -clap = { version = "2.33", default-features = false, features = ["suggestions"] } +# Required dependencies (bin and lib) +boringtun = { version = "0.6.0", default-features = false } log = "0.4" -pretty_env_logger = "0.3" anyhow = "1" -smoltcp = { git = "https://github.com/smoltcp-rs/smoltcp", branch = "master" } -tokio = { version = "1", features = ["full"] } -lockfree = "0.5.1" -futures = "0.3.17" -rand = "0.8.4" +tokio = { version = "1", features = [ "rt", "sync", "io-util", "net", "time", "fs", "macros" ] } +futures = "0.3" +rand = "0.8" +nom = "7" +async-trait = "0.1" +priority-queue = "2.1" +smoltcp = { version = "0.12", default-features = false, features = [ + "std", + "log", + "medium-ip", + "proto-ipv4", + "proto-ipv6", + "socket-udp", + "socket-tcp", +] } +bytes = "1" +base64 = "0.13" + +# forward boringtuns tracing events to log +tracing = { version = "0.1", default-features = false, features = ["log"] } + +# bin-only dependencies +clap = { version = "4.4.11", default-features = false, features = ["suggestions", "std", "env", "help", "wrap_help"], optional = true } +pretty_env_logger = { version = "0.5", optional = true } +async-recursion = "1.0" + +[features] +pcap = [] +default = [ "bin" ] +bin = [ "clap", "pretty_env_logger", "pcap", "tokio/rt-multi-thread" ] + +[lib] diff --git a/Dockerfile b/Dockerfile index 9edc22a..90f851d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,11 @@ -FROM rust:1.55 as cargo-build +FROM rust:1.82.0 as cargo-build WORKDIR /usr/src/onetun COPY Cargo.toml Cargo.toml # Placeholder to download dependencies and cache them using layering RUN mkdir src/ +RUN touch src/lib.rs RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs RUN cargo build --release RUN rm -f target/x86_64-unknown-linux-musl/release/deps/myapp* @@ -14,8 +15,9 @@ COPY . . RUN cargo build --release FROM debian:11-slim -RUN apt-get update -RUN apt-get install dumb-init -y +RUN apt-get update \ + && apt-get install dumb-init -y \ + && rm -rf /var/lib/apt/lists/* COPY --from=cargo-build /usr/src/onetun/target/release/onetun /usr/local/bin/onetun diff --git a/LICENSE b/LICENSE index 7479b85..d57c948 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Aram Peres +Copyright (c) 2025 Aram Peres Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index f652574..58204f6 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,67 @@ +onetun + # onetun -A cross-platform, user-space WireGuard port-forwarder that requires no system network configurations. +A cross-platform, user-space WireGuard port-forwarder that requires **no root-access or system network configurations**. +[![crates.io](https://img.shields.io/crates/v/onetun.svg)](https://crates.io/crates/onetun) +[![MIT licensed](https://img.shields.io/crates/l/onetun.svg)](./LICENSE) [![Build status](https://github.com/aramperes/onetun/actions/workflows/build.yml/badge.svg)](https://github.com/aramperes/onetun/actions) [![Latest Release](https://img.shields.io/github/v/tag/aramperes/onetun?label=release)](https://github.com/aramperes/onetun/releases/latest) ## Use-case -- You have an existing WireGuard endpoint (router), accessible using its UDP endpoint (typically port 51820); and -- You have a peer on the WireGuard network, running a TCP server on a port accessible to the WireGuard network; and -- You want to access this TCP service from a second computer, on which you can't install WireGuard because you - can't (no root access) or don't want to (polluting OS configs). +Access TCP or UDP services running on your WireGuard network, from devices that don't have WireGuard installed. -For example, this can be useful to forward a port from a Kubernetes cluster to a server behind WireGuard, -without needing to install WireGuard in a Pod. +For example, + +- Personal or shared computers where you can't install WireGuard (root) +- IoT and mobile devices +- Root-less containers + +## Download + +onetun is available to install from [crates.io](https://crates.io/crates/onetun) with Rust ≥1.80.0: + +```shell +cargo install onetun +``` + +You can also download the binary for Windows, macOS (Apple Silicon), and Linux (amd64, arm64) from +the [Releases](https://github.com/aramperes/onetun/releases) page. + +You can also run onetun using [Docker](https://hub.docker.com/r/aramperes/onetun): + +```shell +docker run --rm --name onetun --user 1000 -p 8080:8080 aramperes/onetun \ + 0.0.0.0:8080:192.168.4.2:8080 [...options...] +``` + +You can also build onetun locally, using Rust ≥1.80.0: + +```shell +git clone https://github.com/aramperes/onetun && cd onetun +cargo build --release +./target/release/onetun +``` ## Usage -**onetun** opens a TCP port on your local system, from which traffic is forwarded to a TCP port on a peer in your +**onetun** opens a TCP or UDP port on your local system, from which traffic is forwarded to a port on a peer in your WireGuard network. It requires no changes to your operating system's network interfaces: you don't need to have `root` access, or install any WireGuard tool on your local system for it to work. The only prerequisite is to register a peer IP and public key on the remote WireGuard endpoint; those are necessary for the WireGuard endpoint to trust the onetun peer and for packets to be routed. -``` -./onetun \ - --endpoint-addr \ - --endpoint-public-key \ - --private-key \ - --source-peer-ip \ - --keep-alive \ - --log ::[:TCP,UDP,...] [...] \ + --endpoint-addr \ + --endpoint-public-key \ + --private-key \ + --source-peer-ip \ + --keep-alive \ + --log ``` > Note: you can use environment variables for all of these flags. Use `onetun --help` for details. @@ -40,7 +70,7 @@ the WireGuard endpoint to trust the onetun peer and for packets to be routed. Suppose your WireGuard endpoint has the following configuration, and is accessible from `140.30.3.182:51820`: -``` +```shell # /etc/wireguard/wg0.conf [Interface] @@ -63,7 +93,7 @@ We want to access a web server on the friendly peer (`192.168.4.2`) on port `808 local port, say `127.0.0.1:8080`, that will tunnel through WireGuard to reach the peer web server: ```shell -./onetun 127.0.0.1:8080 192.168.4.2:8080 \ +onetun 127.0.0.1:8080:192.168.4.2:8080 \ --endpoint-addr 140.30.3.182:51820 \ --endpoint-public-key 'PUB_****************************************' \ --private-key 'PRIV_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB' \ @@ -73,58 +103,129 @@ local port, say `127.0.0.1:8080`, that will tunnel through WireGuard to reach th You'll then see this log: -``` -INFO onetun > Tunnelling [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +```shell +INFO onetun > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) ``` Which means you can now access the port locally! -``` -$ curl 127.0.0.1:8080 +```shell +curl 127.0.0.1:8080 Hello world! ``` -## Download +### Multiple tunnels in parallel -Normally I would publish `onetun` to crates.io. However, it depends on some features -in [smoltcp](https://github.com/smoltcp-rs/smoltcp) and -[boringtun](https://github.com/cloudflare/boringtun) that haven't been published yet, so I'm forced to use their Git -repos as dependencies for now. - -In the meantime, you can download the binary for Windows, macOS (Intel), and Linux (amd64) from -the [Releases](https://github.com/aramperes/onetun/releases) page. - -You can also run onetun using [Docker](https://hub.docker.com/r/aramperes/onetun): +**onetun** supports running multiple tunnels in parallel. For example: ```shell -docker run --rm --name onetun --user 1000 -p 8080:8080 aramperes/onetun \ - 0.0.0.0:8080 192.168.4.2:8080 [...options...] +onetun 127.0.0.1:8080:192.168.4.2:8080 127.0.0.1:8081:192.168.4.4:8081 +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8081]->[192.168.4.4:8081] (via [140.30.3.182:51820] as peer 192.168.4.3) ``` -You can also build onetun locally, using Rust: +... would open TCP ports 8080 and 8081 locally, which forward to their respective ports on the different peers. + +#### Maximum number of tunnels + +`smoltcp` imposes a compile-time limit on the number of IP addresses assigned to an interface. **onetun** increases +the default value to support most use-cases. In effect, the default limit on the number of **onetun** peers +is **7 per protocol** (TCP and UDP). + +Should you need more unique IP addresses to forward ports to, you can increase the limit in `.cargo/config.toml` and recompile **onetun**. + +### UDP Support + +**onetun** supports UDP forwarding. You can add `:UDP` at the end of the port-forward configuration, or `UDP,TCP` to support +both protocols on the same port (note that this opens 2 separate tunnels, just on the same port) ```shell -$ git clone https://github.com/aramperes/onetun && cd onetun -$ cargo build --release -$ ./target/release/onetun +onetun 127.0.0.1:8080:192.168.4.2:8080:UDP +INFO onetun::tunnel > Tunneling UDP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) + +onetun 127.0.0.1:8080:192.168.4.2:8080:UDP,TCP +INFO onetun::tunnel > Tunneling UDP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +``` + +Note: UDP support is totally experimental. You should read the UDP portion of the **Architecture** section before using +it in any production capacity. + +### IPv6 Support + +**onetun** supports both IPv4 and IPv6. In fact, you can use onetun to forward some IP version to another, e.g. 6-to-4: + +```shell +onetun [::1]:8080:192.168.4.2:8080 +INFO onetun::tunnel > Tunneling TCP [[::1]:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +``` + +Note that each tunnel can only support one "source" IP version and one "destination" IP version. If you want to support +both IPv4 and IPv6 on the same port, you should create a second port-forward: + +```shell +onetun [::1]:8080:192.168.4.2:8080 127.0.0.1:8080:192.168.4.2:8080 +INFO onetun::tunnel > Tunneling TCP [[::1]:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +``` + +### Packet Capture + +For debugging purposes, you can enable the capture of IP packets sent between onetun and the WireGuard peer. +The output is a libpcap capture file that can be viewed with Wireshark. + +```shell +onetun --pcap wg.pcap 127.0.0.1:8080:192.168.4.2:8080 +INFO onetun::pcap > Capturing WireGuard IP packets to wg.pcap +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:8080]->[192.168.4.2:8080] (via [140.30.3.182:51820] as peer 192.168.4.3) +``` + +To capture packets sent to and from the onetun local port, you must use an external tool like `tcpdump` with root access: + +```shell +sudo tcpdump -i lo -w local.pcap 'dst 127.0.0.1 && port 8080' +``` + +### WireGuard Options + +By default, onetun will create the UDP socket to communicate with the WireGuard endpoint on all interfaces and on a dynamic port, +i.e. `0.0.0.0:0` for IPv4 endpoints, or `[::]:0` for IPv6. +You can bind to a static address instead using `--endpoint-bind-addr`: + +```shell +onetun --endpoint-bind-addr 0.0.0.0:51820 --endpoint-addr 140.30.3.182:51820 [...] +``` + +The security of the WireGuard connection can be further enhanced with a **pre-shared key** (PSK). You can generate such a key with the `wg genpsk` command, and provide it using `--preshared-key`. +The peer must also have this key configured using the `PresharedKey` option. + +```shell +onetun --preshared-key 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' [...] ``` ## Architecture +**In short:** onetun uses [smoltcp's](https://github.com/smoltcp-rs/smoltcp) TCP/IP and UDP stack to generate IP packets +using its state machine ("virtual interface"). The generated IP packets are +encrypted by [boringtun](https://github.com/cloudflare/boringtun) and sent to the WireGuard endpoint. Encrypted IP packets received +from the WireGuard endpoint are decrypted using boringtun and sent through the smoltcp virtual interface state machine. +onetun creates "virtual sockets" in the virtual interface to forward data sent from inbound connections, +as well as to receive data from the virtual interface to forward back to the local client. + +--- + onetun uses [tokio](https://github.com/tokio-rs/tokio), the async runtime, to listen for new TCP connections on the given port. -When a client connects to the local TCP port, it uses [smoltcp](https://github.com/smoltcp-rs/smoltcp) to -create a "virtual interface", with a "virtual client" and a "virtual server" for the connection. These "virtual" -components are the crux of how onetun works. They essentially replace the host's TCP/IP stack with smoltcp's, which -fully runs inside onetun. An ephemeral "virtual port" is also assigned to the connection, in order to route packets -back to the right connection. +When a client connects to the onetun's TCP port, a "virtual client" is +created in a [smoltcp](https://github.com/smoltcp-rs/smoltcp) "virtual" TCP/IP interface, which runs fully inside the onetun +process. An ephemeral "virtual port" is assigned to the "virtual client", which maps back to the local client. -When the real client opens the connection, the virtual client socket opens a TCP connection to the virtual server. -The virtual interface (implemented by smoltcp) in turn crafts the `SYN` segment and wraps it in an IP packet. +When the real client opens the connection, the virtual client socket opens a TCP connection to the virtual server +(a dummy socket bound to the remote host/port). The virtual interface in turn crafts the `SYN` segment and wraps it in an IP packet. Because of how the virtual client and server are configured, the IP packet is crafted with a source address being the configured `source-peer-ip` (`192.168.4.3` in the example above), -and the destination address is the remote peer's (`192.168.4.2`). +and the destination address matches the port-forward's configured destination (`192.168.4.2`). By doing this, we let smoltcp handle the crafting of the IP packets, and the handling of the client's TCP states. Instead of actually sending those packets to the virtual server, @@ -135,8 +236,8 @@ Once the WireGuard endpoint receives an encrypted IP packet, it decrypts it usin It reads the destination address, re-encrypts the IP packet using the matching peer's public key, and sends it off to the peer's UDP endpoint. -The remote peer receives the encrypted IP and decrypts it. It can then read the inner payload (the TCP segment), -forward it to the server's port, which handles the TCP segment. The server responds with `SYN-ACK`, which goes back through +The peer receives the encrypted IP and decrypts it. It can then read the inner payload (the TCP segment), +forward it to the server's port, which handles the TCP segment. The TCP server responds with `SYN-ACK`, which goes back through the peer's local WireGuard interface, gets encrypted, forwarded to the WireGuard endpoint, and then finally back to onetun's UDP port. When onetun receives an encrypted packet from the WireGuard endpoint, it decrypts it using boringtun. @@ -150,6 +251,72 @@ the virtual client to read it. When the virtual client reads data, it simply pus This work is all made possible by [smoltcp](https://github.com/smoltcp-rs/smoltcp) and [boringtun](https://github.com/cloudflare/boringtun), so special thanks to the developers of those libraries. +### UDP + +UDP support is experimental. Since UDP messages are stateless, there is no perfect way for onetun to know when to release the +assigned virtual port back to the pool for a new peer to use. This would cause issues over time as running out of virtual ports +would mean new datagrams get dropped. To alleviate this, onetun will cap the amount of ports used by one peer IP address; +if another datagram comes in from a different port but with the same IP, the least recently used virtual port will be freed and assigned +to the new peer port. At that point, any datagram packets destined for the reused virtual port will be routed to the new peer, +and any datagrams received by the old peer will be dropped. + +In addition, in cases where many IPs are exhausting the UDP virtual port pool in tandem, and a totally new peer IP sends data, +onetun will have to pick the least recently used virtual port from _any_ peer IP and reuse it. However, this is only allowed +if the least recently used port hasn't been used for a certain amount of time. If all virtual ports are truly "active" +(with at least one transmission within that time limit), the new datagram gets dropped due to exhaustion. + +All in all, I would not recommend using UDP forwarding for public services, since it's most likely prone to simple DoS or DDoS. + +## HTTP/SOCKS Proxy + +**onetun** is a Transport-layer proxy (also known as port forwarding); it is not in scope to provide +a HTTP/SOCKS proxy server. However, you can easily chain **onetun** with a proxy server on a remote +that is locked down to your WireGuard network. + +For example, you could run [dante-server](https://www.inet.no/dante/) on a peer (ex. `192.168.4.2`) with the following configuration: + +``` +# /etc/danted.conf + +logoutput: syslog +user.privileged: root +user.unprivileged: nobody + +internal: 192.168.4.2 port=1080 +external: eth0 + +socksmethod: none +clientmethod: none + +# Locks down proxy use to WireGuard peers (192.168.4.x) +client pass { + from: 192.168.4.0/24 to: 0.0.0.0/0 +} +socks pass { + from: 192.168.4.0/24 to: 0.0.0.0/0 +} +``` + +Then use **onetun** to expose the SOCKS5 proxy locally: + +```shell +onetun 127.0.0.1:1080:192.168.4.2:1080 +INFO onetun::tunnel > Tunneling TCP [127.0.0.1:1080]->[192.168.4.2:1080] (via [140.30.3.182:51820] as peer 192.168.4.3) +``` + +Test with `curl` (or configure your browser): + +```shell +curl -x socks5://127.0.0.1:1080 https://ifconfig.me +``` + +## Contributing and Maintenance + +I will gladly accept contributions to onetun, and set aside time to review all pull-requests. +Please consider opening a GitHub issue if you are unsure if your contribution is within the scope of the project. + +**Disclaimer**: I do not have enough personal time to actively maintain onetun besides open-source contributions. + ## License -MIT. See `LICENSE` for details. +MIT License. See `LICENSE` for details. Copyright © 2025 Aram Peres. diff --git a/src/config.rs b/src/config.rs index 2c1a993..411efa3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,130 +1,339 @@ +use std::collections::HashSet; +use std::convert::TryFrom; +use std::fmt::{Display, Formatter}; +use std::fs::read_to_string; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::sync::Arc; -use anyhow::Context; -use boringtun::crypto::{X25519PublicKey, X25519SecretKey}; -use clap::{App, Arg}; +use anyhow::{bail, Context}; +pub use boringtun::x25519::{PublicKey, StaticSecret}; -#[derive(Clone, Debug)] +const DEFAULT_PORT_FORWARD_SOURCE: &str = "127.0.0.1"; + +#[derive(Clone)] pub struct Config { - pub(crate) source_addr: SocketAddr, - pub(crate) dest_addr: SocketAddr, - pub(crate) private_key: Arc, - pub(crate) endpoint_public_key: Arc, - pub(crate) endpoint_addr: SocketAddr, - pub(crate) source_peer_ip: IpAddr, - pub(crate) keepalive_seconds: Option, - pub(crate) log: String, + pub port_forwards: Vec, + #[allow(dead_code)] + pub remote_port_forwards: Vec, + pub private_key: Arc, + pub endpoint_public_key: Arc, + pub preshared_key: Option<[u8; 32]>, + pub endpoint_addr: SocketAddr, + pub endpoint_bind_addr: SocketAddr, + pub source_peer_ip: IpAddr, + pub keepalive_seconds: Option, + pub max_transmission_unit: usize, + pub log: String, + pub warnings: Vec, + pub pcap_file: Option, } impl Config { + #[cfg(feature = "bin")] pub fn from_args() -> anyhow::Result { - let matches = App::new("onetun") + use clap::{Arg, Command}; + + let mut warnings = vec![]; + + let matches = Command::new("onetun") .author("Aram Peres ") .version(env!("CARGO_PKG_VERSION")) .args(&[ - Arg::with_name("SOURCE_ADDR") - .required(true) - .takes_value(true) - .env("ONETUN_SOURCE_ADDR") - .help("The source address (IP + port) to forward from. Example: 127.0.0.1:2115"), - Arg::with_name("DESTINATION_ADDR") - .required(true) - .takes_value(true) - .env("ONETUN_DESTINATION_ADDR") - .help("The destination address (IP + port) to forward to. The IP should be a peer registered in the Wireguard endpoint. Example: 192.168.4.2:2116"), - Arg::with_name("private-key") - .required(true) - .takes_value(true) + Arg::new("PORT_FORWARD") + .required(false) + .num_args(1..) + .help("Port forward configurations. The format of each argument is [src_host:]::[:TCP,UDP,...], \ + where [src_host] is the local IP to listen on, is the local port to listen on, is the remote peer IP to forward to, and is the remote port to forward to. \ + Environment variables of the form 'ONETUN_PORT_FORWARD_[#]' are also accepted, where [#] starts at 1.\n\ + Examples:\n\ + \t127.0.0.1:8080:192.168.4.1:8081:TCP,UDP\n\ + \t127.0.0.1:8080:192.168.4.1:8081:TCP\n\ + \t0.0.0.0:8080:192.168.4.1:8081\n\ + \t[::1]:8080:192.168.4.1:8081\n\ + \t8080:192.168.4.1:8081\n\ + \t8080:192.168.4.1:8081:TCP\n\ + \tlocalhost:8080:192.168.4.1:8081:TCP\n\ + \tlocalhost:8080:peer.intranet:8081:TCP\ + "), + Arg::new("private-key") + .conflicts_with("private-key-file") + .num_args(1) .long("private-key") .env("ONETUN_PRIVATE_KEY") - .help("The private key of this peer. The corresponding public key should be registered in the Wireguard endpoint."), - Arg::with_name("endpoint-public-key") + .help("The private key of this peer. The corresponding public key should be registered in the WireGuard endpoint. \ + You can also use '--private-key-file' to specify a file containing the key instead."), + Arg::new("private-key-file") + .num_args(1) + .long("private-key-file") + .env("ONETUN_PRIVATE_KEY_FILE") + .help("The path to a file containing the private key of this peer. The corresponding public key should be registered in the WireGuard endpoint."), + Arg::new("endpoint-public-key") .required(true) - .takes_value(true) + .num_args(1) .long("endpoint-public-key") .env("ONETUN_ENDPOINT_PUBLIC_KEY") - .help("The public key of the Wireguard endpoint (remote)."), - Arg::with_name("endpoint-addr") + .help("The public key of the WireGuard endpoint (remote)."), + Arg::new("preshared-key") + .required(false) + .num_args(1) + .long("preshared-key") + .env("ONETUN_PRESHARED_KEY") + .help("The pre-shared key (PSK) as configured with the peer."), + Arg::new("endpoint-addr") .required(true) - .takes_value(true) + .num_args(1) .long("endpoint-addr") .env("ONETUN_ENDPOINT_ADDR") - .help("The address (IP + port) of the Wireguard endpoint (remote). Example: 1.2.3.4:51820"), - Arg::with_name("source-peer-ip") + .help("The address (IP + port) of the WireGuard endpoint (remote). Example: 1.2.3.4:51820"), + Arg::new("endpoint-bind-addr") + .required(false) + .num_args(1) + .long("endpoint-bind-addr") + .env("ONETUN_ENDPOINT_BIND_ADDR") + .help("The address (IP + port) used to bind the local UDP socket for the WireGuard tunnel. Example: 1.2.3.4:30000. Defaults to 0.0.0.0:0 for IPv4 endpoints, or [::]:0 for IPv6 endpoints."), + Arg::new("source-peer-ip") .required(true) - .takes_value(true) + .num_args(1) .long("source-peer-ip") .env("ONETUN_SOURCE_PEER_IP") .help("The source IP to identify this peer as (local). Example: 192.168.4.3"), - Arg::with_name("keep-alive") + Arg::new("keep-alive") .required(false) - .takes_value(true) + .num_args(1) .long("keep-alive") .env("ONETUN_KEEP_ALIVE") .help("Configures a persistent keep-alive for the WireGuard tunnel, in seconds."), - Arg::with_name("log") + Arg::new("max-transmission-unit") .required(false) - .takes_value(true) + .num_args(1) + .long("max-transmission-unit") + .env("ONETUN_MTU") + .default_value("1420") + .help("Configures the max-transmission-unit (MTU) of the WireGuard tunnel."), + Arg::new("log") + .required(false) + .num_args(1) .long("log") .env("ONETUN_LOG") .default_value("info") - .help("Configures the log level and format.") + .help("Configures the log level and format."), + Arg::new("pcap") + .required(false) + .num_args(1) + .long("pcap") + .env("ONETUN_PCAP") + .help("Decrypts and captures IP packets on the WireGuard tunnel to a given output file."), + Arg::new("remote") + .required(false) + .num_args(1..) + .long("remote") + .short('r') + .help("Remote port forward configurations. The format of each argument is ::[:TCP,UDP,...], \ + where is the port the other peers will reach the server with, is the IP to forward to, and is the port to forward to. \ + The will be bound on onetun's peer IP, as specified by --source-peer-ip. If you pass a different value for here, it will be rejected.\n\ + Note: : must be reachable by onetun. If referring to another WireGuard peer, use --bridge instead (not supported yet).\n\ + Environment variables of the form 'ONETUN_REMOTE_PORT_FORWARD_[#]' are also accepted, where [#] starts at 1.\n\ + Examples:\n\ + \t--remote 8080:localhost:8081:TCP,UDP\n\ + \t--remote 8080:[::1]:8081:TCP\n\ + \t--remote 8080:google.com:80\ + "), ]).get_matches(); + // Combine `PORT_FORWARD` arg and `ONETUN_PORT_FORWARD_#` envs + let mut port_forward_strings = HashSet::new(); + if let Some(values) = matches.get_many::("PORT_FORWARD") { + for value in values { + port_forward_strings.insert(value.to_owned()); + } + } + for n in 1.. { + if let Ok(env) = std::env::var(format!("ONETUN_PORT_FORWARD_{}", n)) { + port_forward_strings.insert(env); + } else { + break; + } + } + + // Parse `PORT_FORWARD` strings into `PortForwardConfig` + let port_forwards: anyhow::Result>> = port_forward_strings + .into_iter() + .map(|s| PortForwardConfig::from_notation(&s, DEFAULT_PORT_FORWARD_SOURCE)) + .collect(); + let port_forwards: Vec = port_forwards + .context("Failed to parse port forward config")? + .into_iter() + .flatten() + .collect(); + + // Read source-peer-ip + let source_peer_ip = parse_ip(matches.get_one::("source-peer-ip")) + .context("Invalid source peer IP")?; + + // Combined `remote` arg and `ONETUN_REMOTE_PORT_FORWARD_#` envs + let mut port_forward_strings = HashSet::new(); + if let Some(values) = matches.get_many::("remote") { + for value in values { + port_forward_strings.insert(value.to_owned()); + } + } + for n in 1.. { + if let Ok(env) = std::env::var(format!("ONETUN_REMOTE_PORT_FORWARD_{}", n)) { + port_forward_strings.insert(env); + } else { + break; + } + } + // Parse `PORT_FORWARD` strings into `PortForwardConfig` + let remote_port_forwards: anyhow::Result>> = + port_forward_strings + .into_iter() + .map(|s| { + PortForwardConfig::from_notation( + &s, + matches.get_one::("source-peer-ip").unwrap(), + ) + }) + .collect(); + let mut remote_port_forwards: Vec = remote_port_forwards + .context("Failed to parse remote port forward config")? + .into_iter() + .flatten() + .collect(); + for port_forward in remote_port_forwards.iter_mut() { + if port_forward.source.ip() != source_peer_ip { + bail!("Remote port forward config must match --source-peer-ip ({}), or be omitted.", source_peer_ip); + } + port_forward.source = SocketAddr::from((source_peer_ip, port_forward.source.port())); + port_forward.remote = true; + } + + if port_forwards.is_empty() && remote_port_forwards.is_empty() { + bail!("No port forward configurations given."); + } + + // Read private key from file or CLI argument + let (group_readable, world_readable) = matches + .get_one::("private-key-file") + .and_then(is_file_insecurely_readable) + .unwrap_or_default(); + if group_readable { + warnings.push("Private key file is group-readable. This is insecure.".into()); + } + if world_readable { + warnings.push("Private key file is world-readable. This is insecure.".into()); + } + + let private_key = if let Some(private_key_file) = + matches.get_one::("private-key-file") + { + read_to_string(private_key_file) + .map(|s| s.trim().to_string()) + .context("Failed to read private key file") + } else { + if std::env::var("ONETUN_PRIVATE_KEY").is_err() { + warnings.push("Private key was passed using CLI. This is insecure. \ + Use \"--private-key-file \", or the \"ONETUN_PRIVATE_KEY\" env variable instead.".into()); + } + matches + .get_one::("private-key") + .cloned() + .context("Missing private key") + }?; + + let endpoint_addr = parse_addr(matches.get_one::("endpoint-addr")) + .context("Invalid endpoint address")?; + + let endpoint_bind_addr = if let Some(addr) = matches.get_one::("endpoint-bind-addr") + { + let addr = parse_addr(Some(addr)).context("Invalid bind address")?; + // Make sure the bind address and endpoint address are the same IP version + if addr.ip().is_ipv4() != endpoint_addr.ip().is_ipv4() { + bail!("Endpoint and bind addresses must be the same IP version"); + } + addr + } else { + // Return the IP version of the endpoint address + match endpoint_addr { + SocketAddr::V4(_) => parse_addr(Some("0.0.0.0:0"))?, + SocketAddr::V6(_) => parse_addr(Some("[::]:0"))?, + } + }; + Ok(Self { - source_addr: parse_addr(matches.value_of("SOURCE_ADDR")) - .with_context(|| "Invalid source address")?, - dest_addr: parse_addr(matches.value_of("DESTINATION_ADDR")) - .with_context(|| "Invalid destination address")?, - private_key: Arc::new( - parse_private_key(matches.value_of("private-key")) - .with_context(|| "Invalid private key")?, - ), + port_forwards, + remote_port_forwards, + private_key: Arc::new(parse_private_key(&private_key).context("Invalid private key")?), endpoint_public_key: Arc::new( - parse_public_key(matches.value_of("endpoint-public-key")) - .with_context(|| "Invalid endpoint public key")?, + parse_public_key(matches.get_one::("endpoint-public-key")) + .context("Invalid endpoint public key")?, ), - endpoint_addr: parse_addr(matches.value_of("endpoint-addr")) - .with_context(|| "Invalid endpoint address")?, - source_peer_ip: parse_ip(matches.value_of("source-peer-ip")) - .with_context(|| "Invalid source peer IP")?, - keepalive_seconds: parse_keep_alive(matches.value_of("keep-alive")) - .with_context(|| "Invalid keep-alive value")?, - log: matches.value_of("log").unwrap_or_default().into(), + preshared_key: parse_preshared_key(matches.get_one::("preshared-key"))?, + endpoint_addr, + endpoint_bind_addr, + source_peer_ip, + keepalive_seconds: parse_keep_alive(matches.get_one::("keep-alive")) + .context("Invalid keep-alive value")?, + max_transmission_unit: parse_mtu(matches.get_one::("max-transmission-unit")) + .context("Invalid max-transmission-unit value")?, + log: matches + .get_one::("log") + .cloned() + .unwrap_or_default(), + pcap_file: matches.get_one::("pcap").cloned(), + warnings, }) } } -fn parse_addr(s: Option<&str>) -> anyhow::Result { - s.with_context(|| "Missing address")? +fn parse_addr>(s: Option) -> anyhow::Result { + s.context("Missing address")? + .as_ref() .to_socket_addrs() - .with_context(|| "Invalid address")? + .context("Invalid address")? .next() - .with_context(|| "Could not lookup address") + .context("Could not lookup address") } -fn parse_ip(s: Option<&str>) -> anyhow::Result { - s.with_context(|| "Missing IP")? +fn parse_ip(s: Option<&String>) -> anyhow::Result { + s.context("Missing IP address")? .parse::() - .with_context(|| "Invalid IP address") + .context("Invalid IP address") } -fn parse_private_key(s: Option<&str>) -> anyhow::Result { - s.with_context(|| "Missing private key")? - .parse::() - .map_err(|e| anyhow::anyhow!("{}", e)) - .with_context(|| "Invalid private key") +fn parse_private_key(s: &str) -> anyhow::Result { + let decoded = base64::decode(s).context("Failed to decode private key")?; + if let Ok::<[u8; 32], _>(bytes) = decoded.try_into() { + Ok(StaticSecret::from(bytes)) + } else { + bail!("Invalid private key") + } } -fn parse_public_key(s: Option<&str>) -> anyhow::Result { - s.with_context(|| "Missing public key")? - .parse::() - .map_err(|e| anyhow::anyhow!("{}", e)) - .with_context(|| "Invalid public key") +fn parse_public_key(s: Option<&String>) -> anyhow::Result { + let encoded = s.context("Missing public key")?; + let decoded = base64::decode(encoded).context("Failed to decode public key")?; + if let Ok::<[u8; 32], _>(bytes) = decoded.try_into() { + Ok(PublicKey::from(bytes)) + } else { + bail!("Invalid public key") + } } -fn parse_keep_alive(s: Option<&str>) -> anyhow::Result> { +fn parse_preshared_key(s: Option<&String>) -> anyhow::Result> { + if let Some(s) = s { + let decoded = base64::decode(s).context("Failed to decode preshared key")?; + if let Ok::<[u8; 32], _>(bytes) = decoded.try_into() { + Ok(Some(bytes)) + } else { + bail!("Invalid preshared key") + } + } else { + Ok(None) + } +} + +fn parse_keep_alive(s: Option<&String>) -> anyhow::Result> { if let Some(s) = s { let parsed: u16 = s.parse().with_context(|| { format!( @@ -137,3 +346,369 @@ fn parse_keep_alive(s: Option<&str>) -> anyhow::Result> { Ok(None) } } + +fn parse_mtu(s: Option<&String>) -> anyhow::Result { + s.context("Missing MTU")?.parse().context("Invalid MTU") +} + +#[cfg(unix)] +fn is_file_insecurely_readable(path: &String) -> Option<(bool, bool)> { + use std::fs::File; + use std::os::unix::fs::MetadataExt; + + let mode = File::open(path).ok()?.metadata().ok()?.mode(); + Some((mode & 0o40 > 0, mode & 0o4 > 0)) +} + +#[cfg(not(unix))] +fn is_file_insecurely_readable(_path: &String) -> Option<(bool, bool)> { + // No good way to determine permissions on non-Unix target + None +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct PortForwardConfig { + /// The source IP and port where the local server will run. + pub source: SocketAddr, + /// The destination IP and port to which traffic will be forwarded. + pub destination: SocketAddr, + /// The transport protocol to use for the port (Layer 4). + pub protocol: PortProtocol, + /// Whether this is a remote port forward. + pub remote: bool, +} + +impl PortForwardConfig { + /// Converts a string representation into `PortForwardConfig`. + /// + /// Sample formats: + /// - `127.0.0.1:8080:192.168.4.1:8081:TCP,UDP` + /// - `127.0.0.1:8080:192.168.4.1:8081:TCP` + /// - `0.0.0.0:8080:192.168.4.1:8081` + /// - `[::1]:8080:192.168.4.1:8081` + /// - `8080:192.168.4.1:8081` + /// - `8080:192.168.4.1:8081:TCP` + /// - `localhost:8080:192.168.4.1:8081:TCP` + /// - `localhost:8080:peer.intranet:8081:TCP` + /// + /// Implementation Notes: + /// - The format is formalized as `[src_host:]::[:PROTO1,PROTO2,...]` + /// - `src_host` is optional and defaults to `127.0.0.1`. + /// - `src_host` and `dst_host` may be specified as IPv4, IPv6, or a FQDN to be resolved by DNS. + /// - IPv6 addresses must be prefixed with `[` and suffixed with `]`. Example: `[::1]`. + /// - Any `u16` is accepted as `src_port` and `dst_port` + /// - Specifying protocols (`PROTO1,PROTO2,...`) is optional and defaults to `TCP`. Values must be separated by commas. + pub fn from_notation(s: &str, default_source: &str) -> anyhow::Result> { + mod parsers { + use nom::branch::alt; + use nom::bytes::complete::is_not; + use nom::character::complete::{alpha1, char, digit1}; + use nom::combinator::{complete, map, opt, success}; + use nom::error::ErrorKind; + use nom::multi::separated_list1; + use nom::sequence::{delimited, preceded, separated_pair, tuple}; + use nom::IResult; + + fn ipv6(s: &str) -> IResult<&str, &str> { + delimited(char('['), is_not("]"), char(']'))(s) + } + + fn ipv4_or_fqdn(s: &str) -> IResult<&str, &str> { + let s = is_not(":")(s)?; + if s.1.chars().all(|c| c.is_ascii_digit()) { + // If ipv4 or fqdn is all digits, it's not valid. + Err(nom::Err::Error(nom::error::ParseError::from_error_kind( + s.1, + ErrorKind::Fail, + ))) + } else { + Ok(s) + } + } + + fn port(s: &str) -> IResult<&str, &str> { + digit1(s) + } + + fn ip_or_fqdn(s: &str) -> IResult<&str, &str> { + alt((ipv6, ipv4_or_fqdn))(s) + } + + fn no_ip(s: &str) -> IResult<&str, Option<&str>> { + success(None)(s) + } + + fn src_addr(s: &str) -> IResult<&str, (Option<&str>, &str)> { + let with_ip = separated_pair(map(ip_or_fqdn, Some), char(':'), port); + let without_ip = tuple((no_ip, port)); + alt((with_ip, without_ip))(s) + } + + fn dst_addr(s: &str) -> IResult<&str, (&str, &str)> { + separated_pair(ip_or_fqdn, char(':'), port)(s) + } + + fn protocol(s: &str) -> IResult<&str, &str> { + alpha1(s) + } + + fn protocols(s: &str) -> IResult<&str, Option>> { + opt(preceded(char(':'), separated_list1(char(','), protocol)))(s) + } + + #[allow(clippy::type_complexity)] + pub fn port_forward( + s: &str, + ) -> IResult<&str, ((Option<&str>, &str), (), (&str, &str), Option>)> + { + complete(tuple(( + src_addr, + map(char(':'), |_| ()), + dst_addr, + protocols, + )))(s) + } + } + + // TODO: Could improve error management with custom errors, so that the messages are more helpful. + let (src_addr, _, dst_addr, protocols) = parsers::port_forward(s) + .map_err(|e| anyhow::anyhow!("Invalid port-forward definition: {}", e))? + .1; + + let source = ( + src_addr.0.unwrap_or(default_source), + src_addr.1.parse::().context("Invalid source port")?, + ) + .to_socket_addrs() + .context("Invalid source address")? + .next() + .context("Could not resolve source address")?; + + let destination = ( + dst_addr.0, + dst_addr.1.parse::().context("Invalid source port")?, + ) + .to_socket_addrs() // TODO: Pass this as given and use DNS config instead (issue #15) + .context("Invalid destination address")? + .next() + .context("Could not resolve destination address")?; + + // Parse protocols + let protocols = if let Some(protocols) = protocols { + let protocols: anyhow::Result> = + protocols.into_iter().map(PortProtocol::try_from).collect(); + protocols + } else { + Ok(vec![PortProtocol::Tcp]) + } + .context("Failed to parse protocols")?; + + // Returns an config for each protocol + Ok(protocols + .into_iter() + .map(|protocol| Self { + source, + destination, + protocol, + remote: false, + }) + .collect()) + } +} + +impl Display for PortForwardConfig { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.remote { + write!( + f, + "(remote){}:{}:{}", + self.source, self.destination, self.protocol + ) + } else { + write!(f, "{}:{}:{}", self.source, self.destination, self.protocol) + } + } +} + +/// Layer 7 protocols for ports. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub enum PortProtocol { + /// TCP + Tcp, + /// UDP + Udp, +} + +impl TryFrom<&str> for PortProtocol { + type Error = anyhow::Error; + + fn try_from(value: &str) -> anyhow::Result { + match value.to_uppercase().as_str() { + "TCP" => Ok(Self::Tcp), + "UDP" => Ok(Self::Udp), + _ => Err(anyhow::anyhow!("Invalid protocol specifier: {}", value)), + } + } +} + +impl Display for PortProtocol { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Tcp => "TCP", + Self::Udp => "UDP", + } + ) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_1() { + assert_eq!( + PortForwardConfig::from_notation( + "192.168.0.1:8080:192.168.4.1:8081:TCP,UDP", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![ + PortForwardConfig { + source: SocketAddr::from_str("192.168.0.1:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }, + PortForwardConfig { + source: SocketAddr::from_str("192.168.0.1:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Udp, + remote: false, + } + ] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_2() { + assert_eq!( + PortForwardConfig::from_notation( + "192.168.0.1:8080:192.168.4.1:8081:TCP", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: SocketAddr::from_str("192.168.0.1:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_3() { + assert_eq!( + PortForwardConfig::from_notation( + "0.0.0.0:8080:192.168.4.1:8081", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: SocketAddr::from_str("0.0.0.0:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_4() { + assert_eq!( + PortForwardConfig::from_notation( + "[::1]:8080:192.168.4.1:8081", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: SocketAddr::from_str("[::1]:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_5() { + assert_eq!( + PortForwardConfig::from_notation("8080:192.168.4.1:8081", DEFAULT_PORT_FORWARD_SOURCE) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: SocketAddr::from_str("127.0.0.1:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_6() { + assert_eq!( + PortForwardConfig::from_notation( + "8080:192.168.4.1:8081:TCP", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: SocketAddr::from_str("127.0.0.1:8080").unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_7() { + assert_eq!( + PortForwardConfig::from_notation( + "localhost:8080:192.168.4.1:8081", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: "localhost:8080".to_socket_addrs().unwrap().next().unwrap(), + destination: SocketAddr::from_str("192.168.4.1:8081").unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } + /// Tests the parsing of `PortForwardConfig`. + #[test] + fn test_parse_port_forward_config_8() { + assert_eq!( + PortForwardConfig::from_notation( + "localhost:8080:localhost:8081:TCP", + DEFAULT_PORT_FORWARD_SOURCE + ) + .expect("Failed to parse"), + vec![PortForwardConfig { + source: "localhost:8080".to_socket_addrs().unwrap().next().unwrap(), + destination: "localhost:8081".to_socket_addrs().unwrap().next().unwrap(), + protocol: PortProtocol::Tcp, + remote: false, + }] + ); + } +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..d6582ce --- /dev/null +++ b/src/events.rs @@ -0,0 +1,190 @@ +use bytes::Bytes; +use std::fmt::{Display, Formatter}; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; + +use crate::config::PortForwardConfig; +use crate::virtual_iface::VirtualPort; +use crate::PortProtocol; + +/// Events that go on the bus between the local server, smoltcp, and WireGuard. +#[derive(Debug, Clone)] +pub enum Event { + /// Dumb event with no data. + Dumb, + /// A new connection with the local server was initiated, and the given virtual port was assigned. + ClientConnectionInitiated(PortForwardConfig, VirtualPort), + /// A connection was dropped from the pool and should be closed in all interfaces. + ClientConnectionDropped(VirtualPort), + /// Data received by the local server that should be sent to the virtual server. + LocalData(PortForwardConfig, VirtualPort, Bytes), + /// Data received by the remote server that should be sent to the local client. + RemoteData(VirtualPort, Bytes), + /// IP packet received from the WireGuard tunnel that should be passed through the corresponding virtual device. + InboundInternetPacket(PortProtocol, Bytes), + /// IP packet to be sent through the WireGuard tunnel as crafted by the virtual device. + OutboundInternetPacket(Bytes), + /// Notifies that a virtual device read an IP packet. + VirtualDeviceFed(PortProtocol), +} + +impl Display for Event { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Event::Dumb => { + write!(f, "Dumb{{}}") + } + Event::ClientConnectionInitiated(pf, vp) => { + write!(f, "ClientConnectionInitiated{{ pf={} vp={} }}", pf, vp) + } + Event::ClientConnectionDropped(vp) => { + write!(f, "ClientConnectionDropped{{ vp={} }}", vp) + } + Event::LocalData(pf, vp, data) => { + let size = data.len(); + write!(f, "LocalData{{ pf={} vp={} size={} }}", pf, vp, size) + } + Event::RemoteData(vp, data) => { + let size = data.len(); + write!(f, "RemoteData{{ vp={} size={} }}", vp, size) + } + Event::InboundInternetPacket(proto, data) => { + let size = data.len(); + write!( + f, + "InboundInternetPacket{{ proto={} size={} }}", + proto, size + ) + } + Event::OutboundInternetPacket(data) => { + let size = data.len(); + write!(f, "OutboundInternetPacket{{ size={} }}", size) + } + Event::VirtualDeviceFed(proto) => { + write!(f, "VirtualDeviceFed{{ proto={} }}", proto) + } + } + } +} + +#[derive(Clone)] +pub struct Bus { + counter: Arc, + bus: Arc>, +} + +impl Bus { + /// Creates a new event bus. + pub fn new() -> Self { + let (bus, _) = tokio::sync::broadcast::channel(1000); + let bus = Arc::new(bus); + let counter = Arc::new(AtomicU32::default()); + Self { bus, counter } + } + + /// Creates a new endpoint on the event bus. + pub fn new_endpoint(&self) -> BusEndpoint { + let id = self.counter.fetch_add(1, Ordering::Relaxed); + let tx = (*self.bus).clone(); + let rx = self.bus.subscribe(); + + let tx = BusSender { id, tx }; + BusEndpoint { id, tx, rx } + } +} + +impl Default for Bus { + fn default() -> Self { + Self::new() + } +} + +pub struct BusEndpoint { + id: u32, + tx: BusSender, + rx: tokio::sync::broadcast::Receiver<(u32, Event)>, +} + +impl BusEndpoint { + /// Sends the event on the bus. Note that the messages sent by this endpoint won't reach itself. + pub fn send(&self, event: Event) { + self.tx.send(event) + } + + /// Returns the unique sequential ID of this endpoint. + pub fn id(&self) -> u32 { + self.id + } + + /// Awaits the next `Event` on the bus to be read. + pub async fn recv(&mut self) -> Event { + loop { + match self.rx.recv().await { + Ok((id, event)) => { + if id == self.id { + // If the event was sent by this endpoint, it is skipped + continue; + } else { + return event; + } + } + Err(_) => { + error!("Failed to read event bus from endpoint #{}", self.id); + return futures::future::pending().await; + } + } + } + } + + /// Creates a new sender for this endpoint that can be cloned. + pub fn sender(&self) -> BusSender { + self.tx.clone() + } +} + +#[derive(Clone)] +pub struct BusSender { + id: u32, + tx: tokio::sync::broadcast::Sender<(u32, Event)>, +} + +impl BusSender { + /// Sends the event on the bus. Note that the messages sent by this endpoint won't reach itself. + pub fn send(&self, event: Event) { + trace!("#{} -> {}", self.id, event); + match self.tx.send((self.id, event)) { + Ok(_) => {} + Err(_) => error!("Failed to send event to bus from endpoint #{}", self.id), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_bus() { + let bus = Bus::new(); + + let mut endpoint_1 = bus.new_endpoint(); + let mut endpoint_2 = bus.new_endpoint(); + let mut endpoint_3 = bus.new_endpoint(); + + assert_eq!(endpoint_1.id(), 0); + assert_eq!(endpoint_2.id(), 1); + assert_eq!(endpoint_3.id(), 2); + + endpoint_1.send(Event::Dumb); + let recv_2 = endpoint_2.recv().await; + let recv_3 = endpoint_3.recv().await; + assert!(matches!(recv_2, Event::Dumb)); + assert!(matches!(recv_3, Event::Dumb)); + + endpoint_2.send(Event::Dumb); + let recv_1 = endpoint_1.recv().await; + let recv_3 = endpoint_3.recv().await; + assert!(matches!(recv_1, Event::Dumb)); + assert!(matches!(recv_3, Event::Dumb)); + } +} diff --git a/src/ip_sink.rs b/src/ip_sink.rs deleted file mode 100644 index f17eb21..0000000 --- a/src/ip_sink.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::virtual_device::VirtualIpDevice; -use crate::wg::WireGuardTunnel; -use smoltcp::iface::InterfaceBuilder; -use smoltcp::socket::SocketSet; -use std::sync::Arc; -use tokio::time::Duration; - -/// A repeating task that processes unroutable IP packets. -pub async fn run_ip_sink_interface(wg: Arc) -> ! { - // Initialize interface - let device = VirtualIpDevice::new_sink(wg) - .await - .expect("Failed to initialize VirtualIpDevice for sink interface"); - - // No sockets on sink interface - let mut socket_set_entries: [_; 0] = Default::default(); - let mut socket_set = SocketSet::new(&mut socket_set_entries[..]); - let mut virtual_interface = InterfaceBuilder::new(device).ip_addrs([]).finalize(); - - loop { - let loop_start = smoltcp::time::Instant::now(); - match virtual_interface.poll(&mut socket_set, loop_start) { - Ok(processed) if processed => { - trace!("[SINK] Virtual interface polled some packets to be processed",); - tokio::time::sleep(Duration::from_millis(1)).await; - } - Err(e) => { - error!("[SINK] Virtual interface poll error: {:?}", e); - } - _ => { - tokio::time::sleep(Duration::from_millis(5)).await; - } - } - } -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a76fa18 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,122 @@ +#[macro_use] +extern crate log; + +use std::sync::Arc; + +use anyhow::Context; + +use crate::config::{Config, PortProtocol}; +use crate::events::Bus; +use crate::tunnel::tcp::TcpPortPool; +use crate::tunnel::udp::UdpPortPool; +use crate::virtual_device::VirtualIpDevice; +use crate::virtual_iface::tcp::TcpVirtualInterface; +use crate::virtual_iface::udp::UdpVirtualInterface; +use crate::virtual_iface::VirtualInterfacePoll; +use crate::wg::WireGuardTunnel; + +pub mod config; +pub mod events; +#[cfg(feature = "pcap")] +pub mod pcap; +pub mod tunnel; +pub mod virtual_device; +pub mod virtual_iface; +pub mod wg; + +/// Starts the onetun tunnels in separate tokio tasks. +/// +/// Note: This future completes immediately. +pub async fn start_tunnels(config: Config, bus: Bus) -> anyhow::Result<()> { + // Initialize the port pool for each protocol + let tcp_port_pool = TcpPortPool::new(); + let udp_port_pool = UdpPortPool::new(); + + #[cfg(feature = "pcap")] + if let Some(pcap_file) = config.pcap_file.clone() { + // Start packet capture + let bus = bus.clone(); + tokio::spawn(async move { pcap::capture(pcap_file, bus).await }); + } + + let wg = WireGuardTunnel::new(&config, bus.clone()) + .await + .context("Failed to initialize WireGuard tunnel")?; + let wg = Arc::new(wg); + + { + // Start routine task for WireGuard + let wg = wg.clone(); + tokio::spawn(async move { wg.routine_task().await }); + } + + { + // Start consumption task for WireGuard + let wg = wg.clone(); + tokio::spawn(Box::pin(async move { wg.consume_task().await })); + } + + { + // Start production task for WireGuard + let wg = wg.clone(); + tokio::spawn(async move { wg.produce_task().await }); + } + + if config + .port_forwards + .iter() + .any(|pf| pf.protocol == PortProtocol::Tcp) + { + // TCP device + let bus = bus.clone(); + let device = + VirtualIpDevice::new(PortProtocol::Tcp, bus.clone(), config.max_transmission_unit); + + // Start TCP Virtual Interface + let port_forwards = config.port_forwards.clone(); + let iface = TcpVirtualInterface::new(port_forwards, bus, config.source_peer_ip); + tokio::spawn(async move { iface.poll_loop(device).await }); + } + + if config + .port_forwards + .iter() + .any(|pf| pf.protocol == PortProtocol::Udp) + { + // UDP device + let bus = bus.clone(); + let device = + VirtualIpDevice::new(PortProtocol::Udp, bus.clone(), config.max_transmission_unit); + + // Start UDP Virtual Interface + let port_forwards = config.port_forwards.clone(); + let iface = UdpVirtualInterface::new(port_forwards, bus, config.source_peer_ip); + tokio::spawn(async move { iface.poll_loop(device).await }); + } + + { + let port_forwards = config.port_forwards; + let source_peer_ip = config.source_peer_ip; + + port_forwards + .into_iter() + .map(|pf| { + ( + pf, + wg.clone(), + tcp_port_pool.clone(), + udp_port_pool.clone(), + bus.clone(), + ) + }) + .for_each(move |(pf, wg, tcp_port_pool, udp_port_pool, bus)| { + tokio::spawn(async move { + tunnel::port_forward(pf, source_peer_ip, tcp_port_pool, udp_port_pool, wg, bus) + .await + .unwrap_or_else(|e| error!("Port-forward failed for {} : {}", pf, e)) + }); + }); + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 8384d3e..4ff2954 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,463 +1,36 @@ +#[cfg(feature = "bin")] #[macro_use] extern crate log; -use std::net::{IpAddr, SocketAddr}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::Context; -use smoltcp::iface::InterfaceBuilder; -use smoltcp::socket::{SocketSet, TcpSocket, TcpSocketBuffer}; -use smoltcp::wire::{IpAddress, IpCidr}; -use tokio::net::{TcpListener, TcpStream}; - -use crate::config::Config; -use crate::port_pool::PortPool; -use crate::virtual_device::VirtualIpDevice; -use crate::wg::WireGuardTunnel; - -pub mod config; -pub mod ip_sink; -pub mod port_pool; -pub mod virtual_device; -pub mod wg; - -pub const MAX_PACKET: usize = 65536; - +#[cfg(feature = "bin")] #[tokio::main] async fn main() -> anyhow::Result<()> { - let config = Config::from_args().with_context(|| "Failed to read config")?; + use anyhow::Context; + use onetun::{config::Config, events::Bus}; + + let config = Config::from_args().context("Configuration has errors")?; init_logger(&config)?; - let port_pool = Arc::new(PortPool::new()); - let wg = WireGuardTunnel::new(&config) - .await - .with_context(|| "Failed to initialize WireGuard tunnel")?; - let wg = Arc::new(wg); - - { - // Start routine task for WireGuard - let wg = wg.clone(); - tokio::spawn(async move { wg.routine_task().await }); + for warning in &config.warnings { + warn!("{}", warning); } - { - // Start consumption task for WireGuard - let wg = wg.clone(); - tokio::spawn(async move { wg.consume_task().await }); - } + let bus = Bus::default(); + onetun::start_tunnels(config, bus).await?; - { - // Start IP sink task for incoming IP packets - let wg = wg.clone(); - tokio::spawn(async move { ip_sink::run_ip_sink_interface(wg).await }); - } - - info!( - "Tunnelling [{}]->[{}] (via [{}] as peer {})", - &config.source_addr, &config.dest_addr, &config.endpoint_addr, &config.source_peer_ip - ); - - tcp_proxy_server( - config.source_addr, - config.source_peer_ip, - config.dest_addr, - port_pool.clone(), - wg, - ) - .await + futures::future::pending().await } -/// Starts the server that listens on TCP connections. -async fn tcp_proxy_server( - listen_addr: SocketAddr, - source_peer_ip: IpAddr, - dest_addr: SocketAddr, - port_pool: Arc, - wg: Arc, -) -> anyhow::Result<()> { - let listener = TcpListener::bind(listen_addr) - .await - .with_context(|| "Failed to listen on TCP proxy server")?; - - loop { - let wg = wg.clone(); - let port_pool = port_pool.clone(); - let (socket, peer_addr) = listener - .accept() - .await - .with_context(|| "Failed to accept connection on TCP proxy server")?; - - // Assign a 'virtual port': this is a unique port number used to route IP packets - // received from the WireGuard tunnel. It is the port number that the virtual client will - // listen on. - let virtual_port = match port_pool.next() { - Ok(port) => port, - Err(e) => { - error!( - "Failed to assign virtual port number for connection [{}]: {:?}", - peer_addr, e - ); - continue; - } - }; - - info!("[{}] Incoming connection from {}", virtual_port, peer_addr); - - tokio::spawn(async move { - let port_pool = Arc::clone(&port_pool); - let result = handle_tcp_proxy_connection( - socket, - virtual_port, - source_peer_ip, - dest_addr, - wg.clone(), - ) - .await; - - if let Err(e) = result { - error!( - "[{}] Connection dropped un-gracefully: {:?}", - virtual_port, e - ); - } else { - info!("[{}] Connection closed by client", virtual_port); - } - - // Release port when connection drops - wg.release_virtual_interface(virtual_port); - port_pool.release(virtual_port); - }); - } +#[cfg(not(feature = "bin"))] +fn main() -> anyhow::Result<()> { + Err(anyhow::anyhow!("Binary compiled without 'bin' feature")) } -/// Handles a new TCP connection with its assigned virtual port. -async fn handle_tcp_proxy_connection( - socket: TcpStream, - virtual_port: u16, - source_peer_ip: IpAddr, - dest_addr: SocketAddr, - wg: Arc, -) -> anyhow::Result<()> { - // Abort signal for stopping the Virtual Interface - let abort = Arc::new(AtomicBool::new(false)); +#[cfg(feature = "bin")] +fn init_logger(config: &onetun::config::Config) -> anyhow::Result<()> { + use anyhow::Context; - // Signals that the Virtual Client is ready to send data - let (virtual_client_ready_tx, virtual_client_ready_rx) = tokio::sync::oneshot::channel::<()>(); - - // data_to_real_client_(tx/rx): This task reads the data from this mpsc channel to send back - // to the real client. - let (data_to_real_client_tx, mut data_to_real_client_rx) = tokio::sync::mpsc::channel(1_000); - - // data_to_real_server_(tx/rx): This task sends the data received from the real client to the - // virtual interface (virtual server socket). - let (data_to_virtual_server_tx, data_to_virtual_server_rx) = tokio::sync::mpsc::channel(1_000); - - // Spawn virtual interface - { - let abort = abort.clone(); - tokio::spawn(async move { - virtual_tcp_interface( - virtual_port, - source_peer_ip, - dest_addr, - wg, - abort, - data_to_real_client_tx, - data_to_virtual_server_rx, - virtual_client_ready_tx, - ) - .await - }); - } - - // Wait for virtual client to be ready. - virtual_client_ready_rx - .await - .expect("failed to wait for virtual client to be ready"); - trace!("[{}] Virtual client is ready to send data", virtual_port); - - loop { - tokio::select! { - readable_result = socket.readable() => { - match readable_result { - Ok(_) => { - // Buffer for the individual TCP segment. - let mut buffer = Vec::with_capacity(MAX_PACKET); - match socket.try_read_buf(&mut buffer) { - Ok(size) if size > 0 => { - let data = &buffer[..size]; - debug!( - "[{}] Read {} bytes of TCP data from real client", - virtual_port, size - ); - if let Err(e) = data_to_virtual_server_tx.send(data.to_vec()).await { - error!( - "[{}] Failed to dispatch data to virtual interface: {:?}", - virtual_port, e - ); - } - } - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { - continue; - } - Err(e) => { - error!( - "[{}] Failed to read from client TCP socket: {:?}", - virtual_port, e - ); - break; - } - _ => { - break; - } - } - } - Err(e) => { - error!("[{}] Failed to check if readable: {:?}", virtual_port, e); - break; - } - } - } - data_recv_result = data_to_real_client_rx.recv() => { - match data_recv_result { - Some(data) => match socket.try_write(&data) { - Ok(size) => { - debug!( - "[{}] Wrote {} bytes of TCP data to real client", - virtual_port, size - ); - } - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { - if abort.load(Ordering::Relaxed) { - break; - } else { - continue; - } - } - Err(e) => { - error!( - "[{}] Failed to write to client TCP socket: {:?}", - virtual_port, e - ); - } - }, - None => { - if abort.load(Ordering::Relaxed) { - break; - } else { - continue; - } - }, - } - } - } - } - - trace!("[{}] TCP socket handler task terminated", virtual_port); - abort.store(true, Ordering::Relaxed); - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -async fn virtual_tcp_interface( - virtual_port: u16, - source_peer_ip: IpAddr, - dest_addr: SocketAddr, - wg: Arc, - abort: Arc, - data_to_real_client_tx: tokio::sync::mpsc::Sender>, - mut data_to_virtual_server_rx: tokio::sync::mpsc::Receiver>, - virtual_client_ready_tx: tokio::sync::oneshot::Sender<()>, -) -> anyhow::Result<()> { - let mut virtual_client_ready_tx = Some(virtual_client_ready_tx); - - // Create a device and interface to simulate IP packets - // In essence: - // * TCP packets received from the 'real' client are 'sent' to the 'virtual server' via the 'virtual client' - // * Those TCP packets generate IP packets, which are captured from the interface and sent to the WireGuardTunnel - // * IP packets received by the WireGuardTunnel (from the endpoint) are fed into this 'virtual interface' - // * The interface processes those IP packets and routes them to the 'virtual client' (the rest is discarded) - // * The TCP data read by the 'virtual client' is sent to the 'real' TCP client - - // Consumer for IP packets to send through the virtual interface - // Initialize the interface - let device = VirtualIpDevice::new(virtual_port, wg) - .with_context(|| "Failed to initialize VirtualIpDevice")?; - let mut virtual_interface = InterfaceBuilder::new(device) - .ip_addrs([ - // Interface handles IP packets for the sender and recipient - IpCidr::new(IpAddress::from(source_peer_ip), 32), - IpCidr::new(IpAddress::from(dest_addr.ip()), 32), - ]) - .finalize(); - - // Server socket: this is a placeholder for the interface to route new connections to. - // TODO: Determine if we even need buffers here. - let server_socket: anyhow::Result = { - static mut TCP_SERVER_RX_DATA: [u8; MAX_PACKET] = [0; MAX_PACKET]; - static mut TCP_SERVER_TX_DATA: [u8; MAX_PACKET] = [0; MAX_PACKET]; - let tcp_rx_buffer = TcpSocketBuffer::new(unsafe { &mut TCP_SERVER_RX_DATA[..] }); - let tcp_tx_buffer = TcpSocketBuffer::new(unsafe { &mut TCP_SERVER_TX_DATA[..] }); - let mut socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); - - socket - .listen((IpAddress::from(dest_addr.ip()), dest_addr.port())) - .with_context(|| "Virtual server socket failed to listen")?; - - Ok(socket) - }; - - let client_socket: anyhow::Result = { - static mut TCP_SERVER_RX_DATA: [u8; MAX_PACKET] = [0; MAX_PACKET]; - static mut TCP_SERVER_TX_DATA: [u8; MAX_PACKET] = [0; MAX_PACKET]; - let tcp_rx_buffer = TcpSocketBuffer::new(unsafe { &mut TCP_SERVER_RX_DATA[..] }); - let tcp_tx_buffer = TcpSocketBuffer::new(unsafe { &mut TCP_SERVER_TX_DATA[..] }); - let mut socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); - - socket - .connect( - (IpAddress::from(dest_addr.ip()), dest_addr.port()), - (IpAddress::from(source_peer_ip), virtual_port), - ) - .with_context(|| "Virtual server socket failed to listen")?; - - Ok(socket) - }; - - // Socket set: there are always 2 sockets: 1 virtual client and 1 virtual server. - let mut socket_set_entries: [_; 2] = Default::default(); - let mut socket_set = SocketSet::new(&mut socket_set_entries[..]); - let _server_handle = socket_set.add(server_socket?); - let client_handle = socket_set.add(client_socket?); - - // Instructs that this is the last poll, after which the connection is closed. - let mut graceful_shutdown = false; - - // Any data that wasn't sent because it was over the sending buffer limit - let mut tx_extra = Vec::new(); - - loop { - let loop_start = smoltcp::time::Instant::now(); - let forceful_shutdown = abort.load(Ordering::Relaxed); - - if forceful_shutdown { - // Un-graceful shutdown: sends a RST packet. - trace!( - "[{}] Forcefully shutting down virtual interface", - virtual_port - ); - let mut client_socket = socket_set.get::(client_handle); - client_socket.abort(); - } - - match virtual_interface.poll(&mut socket_set, loop_start) { - Ok(processed) if processed => { - trace!( - "[{}] Virtual interface polled some packets to be processed", - virtual_port - ); - } - Err(e) => { - error!("[{}] Virtual interface poll error: {:?}", virtual_port, e); - } - _ => {} - } - - { - let mut client_socket = socket_set.get::(client_handle); - if client_socket.can_recv() { - match client_socket.recv(|buffer| (buffer.len(), buffer.to_vec())) { - Ok(data) => { - trace!( - "[{}] Virtual client received {} bytes of data", - virtual_port, - data.len() - ); - // Send it to the real client - if let Err(e) = data_to_real_client_tx.send(data).await { - error!("[{}] Failed to dispatch data from virtual client to real client: {:?}", virtual_port, e); - } - } - Err(e) => { - error!( - "[{}] Failed to read from virtual client socket: {:?}", - virtual_port, e - ); - } - } - } - if client_socket.can_send() { - if let Some(virtual_client_ready_tx) = virtual_client_ready_tx.take() { - virtual_client_ready_tx - .send(()) - .expect("Failed to notify real client that virtual client is ready"); - } - - let mut to_transfer = None; - - if tx_extra.is_empty() { - // We can read the next data in the queue - if let Ok(data) = data_to_virtual_server_rx.try_recv() { - to_transfer = Some(data); - } - } - - let to_transfer_slice = to_transfer.as_ref().unwrap_or(&tx_extra).as_slice(); - if !to_transfer_slice.is_empty() { - let total = to_transfer_slice.len(); - match client_socket.send_slice(to_transfer_slice) { - Ok(sent) => { - trace!( - "[{}] Sent {}/{} bytes via virtual client socket", - virtual_port, - sent, - total, - ); - tx_extra = Vec::from(&to_transfer_slice[sent..total]); - } - Err(e) => { - error!( - "[{}] Failed to send slice via virtual client socket: {:?}", - virtual_port, e - ); - } - } - } - } - if !graceful_shutdown - && !forceful_shutdown - && !client_socket.is_active() - && !client_socket.can_recv() - { - // Graceful shutdown - client_socket.close(); - trace!( - "[{}] Gracefully shutting down virtual interface", - virtual_port - ); - // We don't break the loop right away so that the FIN segment can be sent in the next poll. - graceful_shutdown = true; - continue; - } - } - - if graceful_shutdown || forceful_shutdown { - break; - } - - tokio::time::sleep(Duration::from_millis(1)).await; - } - trace!("[{}] Virtual interface task terminated", virtual_port); - abort.store(true, Ordering::Relaxed); - Ok(()) -} - -fn init_logger(config: &Config) -> anyhow::Result<()> { - let mut builder = pretty_env_logger::formatted_builder(); + let mut builder = pretty_env_logger::formatted_timed_builder(); builder.parse_filters(&config.log); - builder - .try_init() - .with_context(|| "Failed to initialize logger") + builder.try_init().context("Failed to initialize logger") } diff --git a/src/pcap.rs b/src/pcap.rs new file mode 100644 index 0000000..487fbe0 --- /dev/null +++ b/src/pcap.rs @@ -0,0 +1,113 @@ +use crate::events::Event; +use crate::Bus; +use anyhow::Context; +use smoltcp::time::Instant; +use tokio::fs::File; +use tokio::io::{AsyncWriteExt, BufWriter}; + +struct Pcap { + writer: BufWriter, +} + +/// libpcap file writer +/// This is mostly taken from `smoltcp`, but rewritten to be async. +impl Pcap { + async fn flush(&mut self) -> anyhow::Result<()> { + self.writer + .flush() + .await + .context("Failed to flush pcap writer") + } + + async fn write(&mut self, data: &[u8]) -> anyhow::Result { + self.writer + .write(data) + .await + .with_context(|| format!("Failed to write {} bytes to pcap writer", data.len())) + } + + async fn write_u16(&mut self, value: u16) -> anyhow::Result<()> { + self.writer + .write_u16(value) + .await + .context("Failed to write u16 to pcap writer") + } + + async fn write_u32(&mut self, value: u32) -> anyhow::Result<()> { + self.writer + .write_u32(value) + .await + .context("Failed to write u32 to pcap writer") + } + + async fn global_header(&mut self) -> anyhow::Result<()> { + self.write_u32(0xa1b2c3d4).await?; // magic number + self.write_u16(2).await?; // major version + self.write_u16(4).await?; // minor version + self.write_u32(0).await?; // timezone (= UTC) + self.write_u32(0).await?; // accuracy (not used) + self.write_u32(65535).await?; // maximum packet length + self.write_u32(101).await?; // link-layer header type (101 = IP) + self.flush().await + } + + async fn packet_header(&mut self, timestamp: Instant, length: usize) -> anyhow::Result<()> { + assert!(length <= 65535); + + self.write_u32(timestamp.secs() as u32).await?; // timestamp seconds + self.write_u32(timestamp.micros() as u32).await?; // timestamp microseconds + self.write_u32(length as u32).await?; // captured length + self.write_u32(length as u32).await?; // original length + Ok(()) + } + + async fn packet(&mut self, timestamp: Instant, packet: &[u8]) -> anyhow::Result<()> { + self.packet_header(timestamp, packet.len()) + .await + .context("Failed to write packet header to pcap writer")?; + self.write(packet) + .await + .context("Failed to write packet to pcap writer")?; + self.writer + .flush() + .await + .context("Failed to flush pcap writer")?; + self.flush().await + } +} + +/// Listens on the event bus for IP packets sent from and to the WireGuard tunnel. +pub async fn capture(pcap_file: String, bus: Bus) -> anyhow::Result<()> { + let mut endpoint = bus.new_endpoint(); + let file = File::create(&pcap_file) + .await + .context("Failed to create pcap file")?; + let writer = BufWriter::new(file); + + let mut writer = Pcap { writer }; + writer + .global_header() + .await + .context("Failed to write global header to pcap writer")?; + + info!("Capturing WireGuard IP packets to {}", &pcap_file); + loop { + match endpoint.recv().await { + Event::InboundInternetPacket(_proto, ip) => { + let instant = Instant::now(); + writer + .packet(instant, &ip) + .await + .context("Failed to write inbound IP packet to pcap writer")?; + } + Event::OutboundInternetPacket(ip) => { + let instant = Instant::now(); + writer + .packet(instant, &ip) + .await + .context("Failed to write output IP packet to pcap writer")?; + } + _ => {} + } + } +} diff --git a/src/port_pool.rs b/src/port_pool.rs deleted file mode 100644 index 7bff712..0000000 --- a/src/port_pool.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::ops::Range; - -use anyhow::Context; -use rand::seq::SliceRandom; -use rand::thread_rng; - -const MIN_PORT: u16 = 32768; -const MAX_PORT: u16 = 60999; -const PORT_RANGE: Range = MIN_PORT..MAX_PORT; - -/// A pool of virtual ports available. -/// This structure is thread-safe and lock-free; you can use it safely in an `Arc`. -pub struct PortPool { - /// Remaining ports - inner: lockfree::queue::Queue, - /// Ports in use, with their associated IP channel sender. - taken: lockfree::set::Set, -} - -impl Default for PortPool { - fn default() -> Self { - Self::new() - } -} - -impl PortPool { - /// Initializes a new pool of virtual ports. - pub fn new() -> Self { - let inner = lockfree::queue::Queue::default(); - let mut ports: Vec = PORT_RANGE.collect(); - ports.shuffle(&mut thread_rng()); - ports.into_iter().for_each(|p| inner.push(p) as ()); - Self { - inner, - taken: lockfree::set::Set::new(), - } - } - - /// Requests a free port from the pool. An error is returned if none is available (exhaused max capacity). - pub fn next(&self) -> anyhow::Result { - let port = self - .inner - .pop() - .with_context(|| "Virtual port pool is exhausted")?; - self.taken - .insert(port) - .ok() - .with_context(|| "Failed to insert taken")?; - Ok(port) - } - - /// Releases a port back into the pool. - pub fn release(&self, port: u16) { - self.inner.push(port); - self.taken.remove(&port); - } - - /// Whether the given port is in use by a virtual interface. - pub fn is_in_use(&self, port: u16) -> bool { - self.taken.contains(&port) - } -} diff --git a/src/tunnel/mod.rs b/src/tunnel/mod.rs new file mode 100644 index 0000000..eadf8b0 --- /dev/null +++ b/src/tunnel/mod.rs @@ -0,0 +1,34 @@ +use std::net::IpAddr; +use std::sync::Arc; + +use crate::config::{PortForwardConfig, PortProtocol}; +use crate::events::Bus; +use crate::tunnel::tcp::TcpPortPool; +use crate::tunnel::udp::UdpPortPool; +use crate::wg::WireGuardTunnel; + +pub mod tcp; +pub mod udp; + +pub async fn port_forward( + port_forward: PortForwardConfig, + source_peer_ip: IpAddr, + tcp_port_pool: TcpPortPool, + udp_port_pool: UdpPortPool, + wg: Arc, + bus: Bus, +) -> anyhow::Result<()> { + info!( + "Tunneling {} [{}]->[{}] (via [{}] as peer {})", + port_forward.protocol, + port_forward.source, + port_forward.destination, + &wg.endpoint, + source_peer_ip + ); + + match port_forward.protocol { + PortProtocol::Tcp => tcp::tcp_proxy_server(port_forward, tcp_port_pool, bus).await, + PortProtocol::Udp => udp::udp_proxy_server(port_forward, udp_port_pool, bus).await, + } +} diff --git a/src/tunnel/tcp.rs b/src/tunnel/tcp.rs new file mode 100644 index 0000000..47b0197 --- /dev/null +++ b/src/tunnel/tcp.rs @@ -0,0 +1,211 @@ +use std::collections::VecDeque; +use std::ops::Range; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use bytes::BytesMut; +use rand::seq::SliceRandom; +use rand::thread_rng; +use tokio::io::AsyncWriteExt; +use tokio::net::{TcpListener, TcpStream}; + +use crate::config::{PortForwardConfig, PortProtocol}; +use crate::events::{Bus, Event}; +use crate::virtual_iface::VirtualPort; + +const MAX_PACKET: usize = 65536; +const MIN_PORT: u16 = 1000; +const MAX_PORT: u16 = 60999; +const PORT_RANGE: Range = MIN_PORT..MAX_PORT; + +/// Starts the server that listens on TCP connections. +pub async fn tcp_proxy_server( + port_forward: PortForwardConfig, + port_pool: TcpPortPool, + bus: Bus, +) -> anyhow::Result<()> { + let listener = TcpListener::bind(port_forward.source) + .await + .context("Failed to listen on TCP proxy server")?; + + loop { + let port_pool = port_pool.clone(); + let (socket, peer_addr) = listener + .accept() + .await + .context("Failed to accept connection on TCP proxy server")?; + + // Assign a 'virtual port': this is a unique port number used to route IP packets + // received from the WireGuard tunnel. It is the port number that the virtual client will + // listen on. + let virtual_port = match port_pool.next().await { + Ok(port) => port, + Err(e) => { + error!( + "Failed to assign virtual port number for connection [{}]: {:?}", + peer_addr, e + ); + continue; + } + }; + + info!("[{}] Incoming connection from {}", virtual_port, peer_addr); + + let bus = bus.clone(); + tokio::spawn(async move { + let port_pool = port_pool.clone(); + let result = handle_tcp_proxy_connection(socket, virtual_port, port_forward, bus).await; + + if let Err(e) = result { + error!( + "[{}] Connection dropped un-gracefully: {:?}", + virtual_port, e + ); + } else { + info!("[{}] Connection closed by client", virtual_port); + } + + tokio::time::sleep(Duration::from_millis(100)).await; // Make sure the other tasks have time to process the event + port_pool.release(virtual_port).await; + }); + } +} + +/// Handles a new TCP connection with its assigned virtual port. +async fn handle_tcp_proxy_connection( + mut socket: TcpStream, + virtual_port: VirtualPort, + port_forward: PortForwardConfig, + bus: Bus, +) -> anyhow::Result<()> { + let mut endpoint = bus.new_endpoint(); + endpoint.send(Event::ClientConnectionInitiated(port_forward, virtual_port)); + + let mut buffer = BytesMut::with_capacity(MAX_PACKET); + loop { + tokio::select! { + readable_result = socket.readable() => { + match readable_result { + Ok(_) => { + match socket.try_read_buf(&mut buffer) { + Ok(size) if size > 0 => { + let data = Vec::from(&buffer[..size]); + endpoint.send(Event::LocalData(port_forward, virtual_port, data.into())); + // Reset buffer + buffer.clear(); + } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + continue; + } + Err(e) => { + error!( + "[{}] Failed to read from client TCP socket: {:?}", + virtual_port, e + ); + break; + } + _ => { + break; + } + } + } + Err(e) => { + error!("[{}] Failed to check if readable: {:?}", virtual_port, e); + break; + } + } + } + event = endpoint.recv() => { + match event { + Event::ClientConnectionDropped(e_vp) if e_vp == virtual_port => { + // This connection is supposed to be closed, stop the task. + break; + } + Event::RemoteData(e_vp, data) if e_vp == virtual_port => { + // Have remote data to send to the local client + if let Err(e) = socket.writable().await { + error!("[{}] Failed to check if writable: {:?}", virtual_port, e); + } + let expected = data.len(); + let mut sent = 0; + loop { + if sent >= expected { + break; + } + match socket.write(&data[sent..expected]).await { + Ok(written) => { + debug!("[{}] Sent {} (expected {}) bytes to local client", virtual_port, written, expected); + sent += written; + if sent < expected { + debug!("[{}] Will try to resend remaining {} bytes to local client", virtual_port, (expected - written)); + } + }, + Err(e) => { + error!("[{}] Failed to send {} bytes to local client: {:?}", virtual_port, expected, e); + break; + } + } + } + } + _ => {} + } + } + } + } + + // Notify other endpoints that this task has closed and no more data is to be sent to the local client + endpoint.send(Event::ClientConnectionDropped(virtual_port)); + + Ok(()) +} + +/// A pool of virtual ports available for TCP connections. +#[derive(Clone)] +pub struct TcpPortPool { + inner: Arc>, +} + +impl Default for TcpPortPool { + fn default() -> Self { + Self::new() + } +} + +impl TcpPortPool { + /// Initializes a new pool of virtual ports. + pub fn new() -> Self { + let mut inner = TcpPortPoolInner::default(); + let mut ports: Vec = PORT_RANGE.collect(); + ports.shuffle(&mut thread_rng()); + ports + .into_iter() + .for_each(|p| inner.queue.push_back(p) as ()); + Self { + inner: Arc::new(tokio::sync::RwLock::new(inner)), + } + } + + /// Requests a free port from the pool. An error is returned if none is available (exhausted max capacity). + pub async fn next(&self) -> anyhow::Result { + let mut inner = self.inner.write().await; + let port = inner + .queue + .pop_front() + .context("TCP virtual port pool is exhausted")?; + Ok(VirtualPort::new(port, PortProtocol::Tcp)) + } + + /// Releases a port back into the pool. + pub async fn release(&self, port: VirtualPort) { + let mut inner = self.inner.write().await; + inner.queue.push_back(port.num()); + } +} + +/// Non thread-safe inner logic for TCP port pool. +#[derive(Debug, Default)] +struct TcpPortPoolInner { + /// Remaining ports in the pool. + queue: VecDeque, +} diff --git a/src/tunnel/udp.rs b/src/tunnel/udp.rs new file mode 100644 index 0000000..ab52dc7 --- /dev/null +++ b/src/tunnel/udp.rs @@ -0,0 +1,257 @@ +use std::collections::{HashMap, VecDeque}; +use std::net::{IpAddr, SocketAddr}; +use std::ops::Range; +use std::sync::Arc; +use std::time::Instant; + +use anyhow::Context; +use bytes::Bytes; +use priority_queue::double_priority_queue::DoublePriorityQueue; +use rand::seq::SliceRandom; +use rand::thread_rng; +use tokio::net::UdpSocket; + +use crate::config::{PortForwardConfig, PortProtocol}; +use crate::events::{Bus, Event}; +use crate::virtual_iface::VirtualPort; + +const MAX_PACKET: usize = 65536; +const MIN_PORT: u16 = 1000; +const MAX_PORT: u16 = 60999; +const PORT_RANGE: Range = MIN_PORT..MAX_PORT; + +/// How long to keep the UDP peer address assigned to its virtual specified port, in seconds. +/// TODO: Make this configurable by the CLI +const UDP_TIMEOUT_SECONDS: u64 = 60; + +/// To prevent port-flooding, we set a limit on the amount of open ports per IP address. +/// TODO: Make this configurable by the CLI +const PORTS_PER_IP: usize = 100; + +/// Starts the server that listens on UDP datagrams. +pub async fn udp_proxy_server( + port_forward: PortForwardConfig, + port_pool: UdpPortPool, + bus: Bus, +) -> anyhow::Result<()> { + let mut endpoint = bus.new_endpoint(); + let socket = UdpSocket::bind(port_forward.source) + .await + .context("Failed to bind on UDP proxy address")?; + + let mut buffer = [0u8; MAX_PACKET]; + loop { + tokio::select! { + to_send_result = next_udp_datagram(&socket, &mut buffer, port_pool.clone()) => { + match to_send_result { + Ok(Some((port, data))) => { + endpoint.send(Event::LocalData(port_forward, port, data)); + } + Ok(None) => { + continue; + } + Err(e) => { + error!( + "Failed to read from client UDP socket: {:?}", + e + ); + break; + } + } + } + event = endpoint.recv() => { + if let Event::RemoteData(virtual_port, data) = event { + if let Some(peer) = port_pool.get_peer_addr(virtual_port).await { + // Have remote data to send to the local client + if let Err(e) = socket.writable().await { + error!("[{}] Failed to check if writable: {:?}", virtual_port, e); + } + let expected = data.len(); + let mut sent = 0; + loop { + if sent >= expected { + break; + } + match socket.send_to(&data[sent..expected], peer).await { + Ok(written) => { + debug!("[{}] Sent {} (expected {}) bytes to local client", virtual_port, written, expected); + sent += written; + if sent < expected { + debug!("[{}] Will try to resend remaining {} bytes to local client", virtual_port, (expected - written)); + } + }, + Err(e) => { + error!("[{}] Failed to send {} bytes to local client: {:?}", virtual_port, expected, e); + break; + } + } + } + port_pool.update_last_transmit(virtual_port).await; + } + } + } + } + } + Ok(()) +} + +async fn next_udp_datagram( + socket: &UdpSocket, + buffer: &mut [u8], + port_pool: UdpPortPool, +) -> anyhow::Result> { + let (size, peer_addr) = socket + .recv_from(buffer) + .await + .context("Failed to accept incoming UDP datagram")?; + + // Assign a 'virtual port': this is a unique port number used to route IP packets + // received from the WireGuard tunnel. It is the port number that the virtual client will + // listen on. + let port = match port_pool.next(peer_addr).await { + Ok(port) => port, + Err(e) => { + error!( + "Failed to assign virtual port number for UDP datagram from [{}]: {:?}", + peer_addr, e + ); + return Ok(None); + } + }; + + debug!( + "[{}] Received datagram of {} bytes from {}", + port, size, peer_addr + ); + + port_pool.update_last_transmit(port).await; + + let data = buffer[..size].to_vec(); + Ok(Some((port, data.into()))) +} + +/// A pool of virtual ports available for TCP connections. +#[derive(Clone)] +pub struct UdpPortPool { + inner: Arc>, +} + +impl Default for UdpPortPool { + fn default() -> Self { + Self::new() + } +} + +impl UdpPortPool { + /// Initializes a new pool of virtual ports. + pub fn new() -> Self { + let mut inner = UdpPortPoolInner::default(); + let mut ports: Vec = PORT_RANGE.collect(); + ports.shuffle(&mut thread_rng()); + ports + .into_iter() + .for_each(|p| inner.queue.push_back(p) as ()); + Self { + inner: Arc::new(tokio::sync::RwLock::new(inner)), + } + } + + /// Requests a free port from the pool. An error is returned if none is available (exhausted max capacity). + pub async fn next(&self, peer_addr: SocketAddr) -> anyhow::Result { + // A port found to be reused. This is outside of the block because the read lock cannot be upgraded to a write lock. + let mut port_reuse: Option = None; + + { + let inner = self.inner.read().await; + if let Some(port) = inner.port_by_peer_addr.get(&peer_addr) { + return Ok(VirtualPort::new(*port, PortProtocol::Udp)); + } + + // Count how many ports are being used by the peer IP + let peer_ip = peer_addr.ip(); + let peer_port_count = inner + .peer_port_usage + .get(&peer_ip) + .map(|v| v.len()) + .unwrap_or_default(); + + if peer_port_count >= PORTS_PER_IP { + // Return least recently used port in this IP's pool + port_reuse = Some( + *(inner + .peer_port_usage + .get(&peer_ip) + .unwrap() + .peek_min() + .unwrap() + .0), + ); + warn!( + "Peer [{}] is re-using active virtual port {} due to self-exhaustion.", + peer_addr, + port_reuse.unwrap() + ); + } + } + + let mut inner = self.inner.write().await; + + let port = port_reuse + .or_else(|| inner.queue.pop_front()) + .or_else(|| { + // If there is no port to reuse, and the port pool is exhausted, take the last recently used port overall, + // as long as the last transmission exceeds the deadline + let last: (&u16, &Instant) = inner.port_usage.peek_min().unwrap(); + if Instant::now().duration_since(*last.1).as_secs() > UDP_TIMEOUT_SECONDS { + warn!( + "Peer [{}] is re-using inactive virtual port {} due to global exhaustion.", + peer_addr, last.0 + ); + Some(*last.0) + } else { + None + } + }) + .context("Virtual port pool is exhausted")?; + + inner.port_by_peer_addr.insert(peer_addr, port); + inner.peer_addr_by_port.insert(port, peer_addr); + Ok(VirtualPort::new(port, PortProtocol::Udp)) + } + + /// Notify that the given virtual port has received or transmitted a UDP datagram. + pub async fn update_last_transmit(&self, port: VirtualPort) { + let mut inner = self.inner.write().await; + if let Some(peer) = inner.peer_addr_by_port.get(&port.num()).copied() { + let pq: &mut DoublePriorityQueue = inner + .peer_port_usage + .entry(peer.ip()) + .or_insert_with(Default::default); + pq.push(port.num(), Instant::now()); + } + let pq: &mut DoublePriorityQueue = &mut inner.port_usage; + pq.push(port.num(), Instant::now()); + } + + pub async fn get_peer_addr(&self, port: VirtualPort) -> Option { + let inner = self.inner.read().await; + inner.peer_addr_by_port.get(&port.num()).copied() + } +} + +/// Non thread-safe inner logic for UDP port pool. +#[derive(Debug, Default)] +struct UdpPortPoolInner { + /// Remaining ports in the pool. + queue: VecDeque, + /// The port assigned by peer IP/port. This is used to lookup an existing virtual port + /// for an incoming UDP datagram. + port_by_peer_addr: HashMap, + /// The socket address assigned to a peer IP/port. This is used to send a UDP datagram to + /// the real peer address, given the virtual port. + peer_addr_by_port: HashMap, + /// Keeps an ordered map of the most recently used virtual ports by a peer (client) IP. + peer_port_usage: HashMap>, + /// Keeps an ordered map of the most recently used virtual ports in general. + port_usage: DoublePriorityQueue, +} diff --git a/src/virtual_device.rs b/src/virtual_device.rs index b8a61ce..28d8751 100644 --- a/src/virtual_device.rs +++ b/src/virtual_device.rs @@ -1,102 +1,136 @@ -use crate::wg::WireGuardTunnel; -use anyhow::Context; -use smoltcp::phy::{Device, DeviceCapabilities, Medium}; -use smoltcp::time::Instant; -use std::sync::Arc; +use crate::config::PortProtocol; +use crate::events::{BusSender, Event}; +use crate::Bus; +use bytes::{BufMut, Bytes, BytesMut}; +use smoltcp::{ + phy::{DeviceCapabilities, Medium}, + time::Instant, +}; +use std::{ + collections::VecDeque, + sync::{Arc, Mutex}, +}; -/// A virtual device that processes IP packets. IP packets received from the WireGuard endpoint -/// are made available to this device using a channel receiver. IP packets sent from this device -/// are asynchronously sent out to the WireGuard tunnel. +/// A virtual device that processes IP packets through smoltcp and WireGuard. pub struct VirtualIpDevice { - /// Tunnel to send IP packets to. - wg: Arc, + /// Max transmission unit (bytes) + max_transmission_unit: usize, /// Channel receiver for received IP packets. - ip_dispatch_rx: tokio::sync::mpsc::Receiver>, + bus_sender: BusSender, + /// Local queue for packets received from the bus that need to go through the smoltcp interface. + process_queue: Arc>>, } impl VirtualIpDevice { - pub fn new(virtual_port: u16, wg: Arc) -> anyhow::Result { - let ip_dispatch_rx = wg - .register_virtual_interface(virtual_port) - .with_context(|| "Failed to register IP dispatch for virtual interface")?; + /// Initializes a new virtual IP device. + pub fn new(protocol: PortProtocol, bus: Bus, max_transmission_unit: usize) -> Self { + let mut bus_endpoint = bus.new_endpoint(); + let bus_sender = bus_endpoint.sender(); + let process_queue = Arc::new(Mutex::new(VecDeque::new())); - Ok(Self { wg, ip_dispatch_rx }) - } + { + let process_queue = process_queue.clone(); + tokio::spawn(async move { + loop { + match bus_endpoint.recv().await { + Event::InboundInternetPacket(ip_proto, data) if ip_proto == protocol => { + let mut queue = process_queue + .lock() + .expect("Failed to acquire process queue lock"); + queue.push_back(data); + bus_endpoint.send(Event::VirtualDeviceFed(ip_proto)); + } + _ => {} + } + } + }); + } - pub async fn new_sink(wg: Arc) -> anyhow::Result { - let ip_dispatch_rx = wg - .register_sink_interface() - .await - .with_context(|| "Failed to register IP dispatch for sink virtual interface")?; - Ok(Self { wg, ip_dispatch_rx }) + Self { + bus_sender, + process_queue, + max_transmission_unit, + } } } -impl<'a> Device<'a> for VirtualIpDevice { - type RxToken = RxToken; - type TxToken = TxToken; +impl smoltcp::phy::Device for VirtualIpDevice { + type RxToken<'a> + = RxToken + where + Self: 'a; + type TxToken<'a> + = TxToken + where + Self: 'a; - fn receive(&'a mut self) -> Option<(Self::RxToken, Self::TxToken)> { - match self.ip_dispatch_rx.try_recv() { - Ok(buffer) => Some(( - Self::RxToken { buffer }, + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + let next = { + let mut queue = self + .process_queue + .lock() + .expect("Failed to acquire process queue lock"); + queue.pop_front() + }; + match next { + Some(buffer) => Some(( + Self::RxToken { + buffer: { + let mut buf = BytesMut::new(); + buf.put(buffer); + buf + }, + }, Self::TxToken { - wg: self.wg.clone(), + sender: self.bus_sender.clone(), }, )), - Err(_) => None, + None => None, } } - fn transmit(&'a mut self) -> Option { + fn transmit(&mut self, _timestamp: Instant) -> Option> { Some(TxToken { - wg: self.wg.clone(), + sender: self.bus_sender.clone(), }) } fn capabilities(&self) -> DeviceCapabilities { let mut cap = DeviceCapabilities::default(); cap.medium = Medium::Ip; - cap.max_transmission_unit = 1420; + cap.max_transmission_unit = self.max_transmission_unit; cap } } #[doc(hidden)] pub struct RxToken { - buffer: Vec, + buffer: BytesMut, } impl smoltcp::phy::RxToken for RxToken { - fn consume(mut self, _timestamp: Instant, f: F) -> smoltcp::Result + fn consume(self, f: F) -> R where - F: FnOnce(&mut [u8]) -> smoltcp::Result, + F: FnOnce(&[u8]) -> R, { - f(&mut self.buffer) + f(&self.buffer) } } #[doc(hidden)] pub struct TxToken { - wg: Arc, + sender: BusSender, } impl smoltcp::phy::TxToken for TxToken { - fn consume(self, _timestamp: Instant, len: usize, f: F) -> smoltcp::Result + fn consume(self, len: usize, f: F) -> R where - F: FnOnce(&mut [u8]) -> smoltcp::Result, + F: FnOnce(&mut [u8]) -> R, { - let mut buffer = Vec::new(); - buffer.resize(len, 0); + let mut buffer = vec![0; len]; let result = f(&mut buffer); - tokio::spawn(async move { - match self.wg.send_ip_packet(&buffer).await { - Ok(_) => {} - Err(e) => { - error!("Failed to send IP packet to WireGuard endpoint: {:?}", e); - } - } - }); + self.sender + .send(Event::OutboundInternetPacket(buffer.into())); result } } diff --git a/src/virtual_iface/mod.rs b/src/virtual_iface/mod.rs new file mode 100644 index 0000000..4fd6f47 --- /dev/null +++ b/src/virtual_iface/mod.rs @@ -0,0 +1,65 @@ +pub mod tcp; +pub mod udp; + +use crate::config::PortProtocol; +use crate::VirtualIpDevice; +use async_trait::async_trait; +use std::fmt::{Display, Formatter}; + +#[async_trait] +pub trait VirtualInterfacePoll { + /// Initializes the virtual interface and processes incoming data to be dispatched + /// to the WireGuard tunnel and to the real client. + async fn poll_loop(mut self, device: VirtualIpDevice) -> anyhow::Result<()>; +} + +/// Virtual port. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub struct VirtualPort(u16, PortProtocol); + +impl VirtualPort { + /// Create a new `VirtualPort` instance, with the given port number and associated protocol. + pub fn new(port: u16, proto: PortProtocol) -> Self { + VirtualPort(port, proto) + } + + /// The port number + pub fn num(&self) -> u16 { + self.0 + } + + /// The protocol of this port. + pub fn proto(&self) -> PortProtocol { + self.1 + } +} + +impl From for u16 { + fn from(port: VirtualPort) -> Self { + port.num() + } +} + +impl From<&VirtualPort> for u16 { + fn from(port: &VirtualPort) -> Self { + port.num() + } +} + +impl From for PortProtocol { + fn from(port: VirtualPort) -> Self { + port.proto() + } +} + +impl From<&VirtualPort> for PortProtocol { + fn from(port: &VirtualPort) -> Self { + port.proto() + } +} + +impl Display for VirtualPort { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}:{}]", self.num(), self.proto()) + } +} diff --git a/src/virtual_iface/tcp.rs b/src/virtual_iface/tcp.rs new file mode 100644 index 0000000..3a3fd8d --- /dev/null +++ b/src/virtual_iface/tcp.rs @@ -0,0 +1,257 @@ +use crate::config::{PortForwardConfig, PortProtocol}; +use crate::events::Event; +use crate::virtual_device::VirtualIpDevice; +use crate::virtual_iface::{VirtualInterfacePoll, VirtualPort}; +use crate::Bus; +use anyhow::Context; +use async_trait::async_trait; +use bytes::Bytes; +use smoltcp::iface::PollResult; +use smoltcp::{ + iface::{Config, Interface, SocketHandle, SocketSet}, + socket::tcp, + time::Instant, + wire::{HardwareAddress, IpAddress, IpCidr, IpVersion}, +}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + net::IpAddr, + time::Duration, +}; + +const MAX_PACKET: usize = 65536; + +/// A virtual interface for proxying Layer 7 data to Layer 3 packets, and vice-versa. +pub struct TcpVirtualInterface { + source_peer_ip: IpAddr, + port_forwards: Vec, + bus: Bus, + sockets: SocketSet<'static>, +} + +impl TcpVirtualInterface { + /// Initialize the parameters for a new virtual interface. + /// Use the `poll_loop()` future to start the virtual interface poll loop. + pub fn new(port_forwards: Vec, bus: Bus, source_peer_ip: IpAddr) -> Self { + Self { + port_forwards: port_forwards + .into_iter() + .filter(|f| matches!(f.protocol, PortProtocol::Tcp)) + .collect(), + source_peer_ip, + bus, + sockets: SocketSet::new([]), + } + } + + fn new_server_socket(port_forward: PortForwardConfig) -> anyhow::Result> { + static mut TCP_SERVER_RX_DATA: [u8; 0] = []; + static mut TCP_SERVER_TX_DATA: [u8; 0] = []; + + let tcp_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut TCP_SERVER_RX_DATA[..] }); + let tcp_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut TCP_SERVER_TX_DATA[..] }); + let mut socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer); + + socket + .listen(( + IpAddress::from(port_forward.destination.ip()), + port_forward.destination.port(), + )) + .context("Virtual server socket failed to listen")?; + + Ok(socket) + } + + fn new_client_socket() -> anyhow::Result> { + let rx_data = vec![0u8; MAX_PACKET]; + let tx_data = vec![0u8; MAX_PACKET]; + let tcp_rx_buffer = tcp::SocketBuffer::new(rx_data); + let tcp_tx_buffer = tcp::SocketBuffer::new(tx_data); + let socket = tcp::Socket::new(tcp_rx_buffer, tcp_tx_buffer); + Ok(socket) + } + + fn addresses(&self) -> Vec { + let mut addresses = HashSet::new(); + addresses.insert(IpAddress::from(self.source_peer_ip)); + for config in self.port_forwards.iter() { + addresses.insert(IpAddress::from(config.destination.ip())); + } + addresses + .into_iter() + .map(|addr| IpCidr::new(addr, addr_length(&addr))) + .collect() + } +} + +#[async_trait] +impl VirtualInterfacePoll for TcpVirtualInterface { + async fn poll_loop(mut self, mut device: VirtualIpDevice) -> anyhow::Result<()> { + // Create CIDR block for source peer IP + each port forward IP + let addresses = self.addresses(); + let config = Config::new(HardwareAddress::Ip); + + // Create virtual interface (contains smoltcp state machine) + let mut iface = Interface::new(config, &mut device, Instant::now()); + iface.update_ip_addrs(|ip_addrs| { + addresses.into_iter().for_each(|addr| { + ip_addrs + .push(addr) + .expect("maximum number of IPs in TCP interface reached"); + }); + }); + + // Create virtual server for each port forward + for port_forward in self.port_forwards.iter() { + let server_socket = TcpVirtualInterface::new_server_socket(*port_forward)?; + self.sockets.add(server_socket); + } + + // The next time to poll the interface. Can be None for instant poll. + let mut next_poll: Option = None; + + // Bus endpoint to read events + let mut endpoint = self.bus.new_endpoint(); + + // Maps virtual port to its client socket handle + let mut port_client_handle_map: HashMap = HashMap::new(); + + // Data packets to send from a virtual client + let mut send_queue: HashMap> = HashMap::new(); + + loop { + tokio::select! { + _ = match (next_poll, port_client_handle_map.len()) { + (None, 0) => tokio::time::sleep(Duration::MAX), + (None, _) => tokio::time::sleep(Duration::ZERO), + (Some(until), _) => tokio::time::sleep_until(until), + } => { + let loop_start = smoltcp::time::Instant::now(); + + // Find closed sockets + port_client_handle_map.retain(|virtual_port, client_handle| { + let client_socket = self.sockets.get_mut::(*client_handle); + if client_socket.state() == tcp::State::Closed { + endpoint.send(Event::ClientConnectionDropped(*virtual_port)); + send_queue.remove(virtual_port); + self.sockets.remove(*client_handle); + false + } else { + // Not closed, retain + true + } + }); + + if iface.poll(loop_start, &mut device, &mut self.sockets) == PollResult::SocketStateChanged { + log::trace!("TCP virtual interface polled some packets to be processed"); + } + + for (virtual_port, client_handle) in port_client_handle_map.iter() { + let client_socket = self.sockets.get_mut::(*client_handle); + if client_socket.can_send() { + if let Some(send_queue) = send_queue.get_mut(virtual_port) { + let to_transfer = send_queue.pop_front(); + if let Some(to_transfer_slice) = to_transfer.as_deref() { + let total = to_transfer_slice.len(); + match client_socket.send_slice(to_transfer_slice) { + Ok(sent) => { + if sent < total { + // Sometimes only a subset is sent, so the rest needs to be sent on the next poll + let tx_extra = Vec::from(&to_transfer_slice[sent..total]); + send_queue.push_front(tx_extra.into()); + } + } + Err(e) => { + error!( + "Failed to send slice via virtual client socket: {:?}", e + ); + } + } + } else if client_socket.state() == tcp::State::CloseWait { + client_socket.close(); + } + } + } + if client_socket.can_recv() { + match client_socket.recv(|buffer| (buffer.len(), Bytes::from(buffer.to_vec()))) { + Ok(data) => { + debug!("[{}] Received {} bytes from virtual server", virtual_port, data.len()); + if !data.is_empty() { + endpoint.send(Event::RemoteData(*virtual_port, data)); + } + } + Err(e) => { + error!( + "Failed to read from virtual client socket: {:?}", e + ); + } + } + } + } + + // The virtual interface determines the next time to poll (this is to reduce unnecessary polls) + next_poll = match iface.poll_delay(loop_start, &self.sockets) { + Some(smoltcp::time::Duration::ZERO) => None, + Some(delay) => { + trace!("TCP Virtual interface delayed next poll by {}", delay); + Some(tokio::time::Instant::now() + Duration::from_millis(delay.total_millis())) + }, + None => None, + }; + } + event = endpoint.recv() => { + match event { + Event::ClientConnectionInitiated(port_forward, virtual_port) => { + let client_socket = TcpVirtualInterface::new_client_socket()?; + let client_handle = self.sockets.add(client_socket); + + // Add handle to map + port_client_handle_map.insert(virtual_port, client_handle); + send_queue.insert(virtual_port, VecDeque::new()); + + let client_socket = self.sockets.get_mut::(client_handle); + let context = iface.context(); + + client_socket + .connect( + context, + ( + IpAddress::from(port_forward.destination.ip()), + port_forward.destination.port(), + ), + (IpAddress::from(self.source_peer_ip), virtual_port.num()), + ) + .context("Virtual server socket failed to listen")?; + + next_poll = None; + } + Event::ClientConnectionDropped(virtual_port) => { + if let Some(client_handle) = port_client_handle_map.get(&virtual_port) { + let client_socket = self.sockets.get_mut::(*client_handle); + client_socket.close(); + next_poll = None; + } + } + Event::LocalData(_, virtual_port, data) if send_queue.contains_key(&virtual_port) => { + if let Some(send_queue) = send_queue.get_mut(&virtual_port) { + send_queue.push_back(data); + next_poll = None; + } + } + Event::VirtualDeviceFed(PortProtocol::Tcp) => { + next_poll = None; + } + _ => {} + } + } + } + } + } +} + +const fn addr_length(addr: &IpAddress) -> u8 { + match addr.version() { + IpVersion::Ipv4 => 32, + IpVersion::Ipv6 => 128, + } +} diff --git a/src/virtual_iface/udp.rs b/src/virtual_iface/udp.rs new file mode 100644 index 0000000..cb643c9 --- /dev/null +++ b/src/virtual_iface/udp.rs @@ -0,0 +1,227 @@ +use crate::config::PortForwardConfig; +use crate::events::Event; +use crate::virtual_device::VirtualIpDevice; +use crate::virtual_iface::{VirtualInterfacePoll, VirtualPort}; +use crate::{Bus, PortProtocol}; +use anyhow::Context; +use async_trait::async_trait; +use bytes::Bytes; +use smoltcp::iface::PollResult; +use smoltcp::{ + iface::{Config, Interface, SocketHandle, SocketSet}, + socket::udp::{self, UdpMetadata}, + time::Instant, + wire::{HardwareAddress, IpAddress, IpCidr, IpVersion}, +}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + net::IpAddr, + time::Duration, +}; + +const MAX_PACKET: usize = 65536; + +pub struct UdpVirtualInterface { + source_peer_ip: IpAddr, + port_forwards: Vec, + bus: Bus, + sockets: SocketSet<'static>, +} + +impl UdpVirtualInterface { + /// Initialize the parameters for a new virtual interface. + /// Use the `poll_loop()` future to start the virtual interface poll loop. + pub fn new(port_forwards: Vec, bus: Bus, source_peer_ip: IpAddr) -> Self { + Self { + port_forwards: port_forwards + .into_iter() + .filter(|f| matches!(f.protocol, PortProtocol::Udp)) + .collect(), + source_peer_ip, + bus, + sockets: SocketSet::new([]), + } + } + + fn new_server_socket(port_forward: PortForwardConfig) -> anyhow::Result> { + static mut UDP_SERVER_RX_META: [udp::PacketMetadata; 0] = []; + static mut UDP_SERVER_RX_DATA: [u8; 0] = []; + static mut UDP_SERVER_TX_META: [udp::PacketMetadata; 0] = []; + static mut UDP_SERVER_TX_DATA: [u8; 0] = []; + let udp_rx_buffer = + udp::PacketBuffer::new(unsafe { &mut UDP_SERVER_RX_META[..] }, unsafe { + &mut UDP_SERVER_RX_DATA[..] + }); + let udp_tx_buffer = + udp::PacketBuffer::new(unsafe { &mut UDP_SERVER_TX_META[..] }, unsafe { + &mut UDP_SERVER_TX_DATA[..] + }); + let mut socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer); + socket + .bind(( + IpAddress::from(port_forward.destination.ip()), + port_forward.destination.port(), + )) + .context("UDP virtual server socket failed to bind")?; + Ok(socket) + } + + fn new_client_socket( + source_peer_ip: IpAddr, + client_port: VirtualPort, + ) -> anyhow::Result> { + let rx_meta = vec![udp::PacketMetadata::EMPTY; 10]; + let tx_meta = vec![udp::PacketMetadata::EMPTY; 10]; + let rx_data = vec![0u8; MAX_PACKET]; + let tx_data = vec![0u8; MAX_PACKET]; + let udp_rx_buffer = udp::PacketBuffer::new(rx_meta, rx_data); + let udp_tx_buffer = udp::PacketBuffer::new(tx_meta, tx_data); + let mut socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer); + socket + .bind((IpAddress::from(source_peer_ip), client_port.num())) + .context("UDP virtual client failed to bind")?; + Ok(socket) + } + + fn addresses(&self) -> Vec { + let mut addresses = HashSet::new(); + addresses.insert(IpAddress::from(self.source_peer_ip)); + for config in self.port_forwards.iter() { + addresses.insert(IpAddress::from(config.destination.ip())); + } + addresses + .into_iter() + .map(|addr| IpCidr::new(addr, addr_length(&addr))) + .collect() + } +} + +#[async_trait] +impl VirtualInterfacePoll for UdpVirtualInterface { + async fn poll_loop(mut self, mut device: VirtualIpDevice) -> anyhow::Result<()> { + // Create CIDR block for source peer IP + each port forward IP + let addresses = self.addresses(); + let config = Config::new(HardwareAddress::Ip); + + // Create virtual interface (contains smoltcp state machine) + let mut iface = Interface::new(config, &mut device, Instant::now()); + iface.update_ip_addrs(|ip_addrs| { + addresses.into_iter().for_each(|addr| { + ip_addrs + .push(addr) + .expect("maximum number of IPs in UDP interface reached"); + }); + }); + + // Create virtual server for each port forward + for port_forward in self.port_forwards.iter() { + let server_socket = UdpVirtualInterface::new_server_socket(*port_forward)?; + self.sockets.add(server_socket); + } + + // The next time to poll the interface. Can be None for instant poll. + let mut next_poll: Option = None; + + // Bus endpoint to read events + let mut endpoint = self.bus.new_endpoint(); + + // Maps virtual port to its client socket handle + let mut port_client_handle_map: HashMap = HashMap::new(); + + // Data packets to send from a virtual client + let mut send_queue: HashMap> = + HashMap::new(); + + loop { + tokio::select! { + _ = match (next_poll, port_client_handle_map.len()) { + (None, 0) => tokio::time::sleep(Duration::MAX), + (None, _) => tokio::time::sleep(Duration::ZERO), + (Some(until), _) => tokio::time::sleep_until(until), + } => { + let loop_start = smoltcp::time::Instant::now(); + + if iface.poll(loop_start, &mut device, &mut self.sockets) == PollResult::SocketStateChanged { + log::trace!("UDP virtual interface polled some packets to be processed"); + } + + for (virtual_port, client_handle) in port_client_handle_map.iter() { + let client_socket = self.sockets.get_mut::(*client_handle); + if client_socket.can_send() { + if let Some(send_queue) = send_queue.get_mut(virtual_port) { + let to_transfer = send_queue.pop_front(); + if let Some((port_forward, data)) = to_transfer { + client_socket + .send_slice( + &data, + UdpMetadata::from(port_forward.destination), + ) + .unwrap_or_else(|e| { + error!( + "[{}] Failed to send data to virtual server: {:?}", + virtual_port, e + ); + }); + } + } + } + if client_socket.can_recv() { + match client_socket.recv() { + Ok((data, _peer)) => { + if !data.is_empty() { + endpoint.send(Event::RemoteData(*virtual_port, data.to_vec().into())); + } + } + Err(e) => { + error!( + "Failed to read from virtual client socket: {:?}", e + ); + } + } + } + } + + // The virtual interface determines the next time to poll (this is to reduce unnecessary polls) + next_poll = match iface.poll_delay(loop_start, &self.sockets) { + Some(smoltcp::time::Duration::ZERO) => None, + Some(delay) => { + trace!("UDP Virtual interface delayed next poll by {}", delay); + Some(tokio::time::Instant::now() + Duration::from_millis(delay.total_millis())) + }, + None => None, + }; + } + event = endpoint.recv() => { + match event { + Event::LocalData(port_forward, virtual_port, data) => { + if let Some(send_queue) = send_queue.get_mut(&virtual_port) { + // Client socket already exists + send_queue.push_back((port_forward, data)); + } else { + // Client socket does not exist + let client_socket = UdpVirtualInterface::new_client_socket(self.source_peer_ip, virtual_port)?; + let client_handle = self.sockets.add(client_socket); + + // Add handle to map + port_client_handle_map.insert(virtual_port, client_handle); + send_queue.insert(virtual_port, VecDeque::from(vec![(port_forward, data)])); + } + next_poll = None; + } + Event::VirtualDeviceFed(PortProtocol::Udp) => { + next_poll = None; + } + _ => {} + } + } + } + } + } +} + +const fn addr_length(addr: &IpAddress) -> u8 { + match addr.version() { + IpVersion::Ipv4 => 32, + IpVersion::Ipv6 => 128, + } +} diff --git a/src/wg.rs b/src/wg.rs index 7fc5559..f114c43 100644 --- a/src/wg.rs +++ b/src/wg.rs @@ -1,54 +1,54 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::{IpAddr, SocketAddr}; use std::time::Duration; +use crate::Bus; use anyhow::Context; +use async_recursion::async_recursion; +use boringtun::noise::errors::WireGuardError; use boringtun::noise::{Tunn, TunnResult}; use log::Level; -use smoltcp::wire::{IpProtocol, IpVersion, Ipv4Packet, Ipv6Packet, TcpPacket}; +use smoltcp::wire::{IpProtocol, IpVersion, Ipv4Packet, Ipv6Packet}; use tokio::net::UdpSocket; -use tokio::sync::RwLock; +use tokio::sync::Mutex; -use crate::config::Config; -use crate::MAX_PACKET; +use crate::config::{Config, PortProtocol}; +use crate::events::Event; /// The capacity of the channel for received IP packets. -const DISPATCH_CAPACITY: usize = 1_000; +pub const DISPATCH_CAPACITY: usize = 1_000; +const MAX_PACKET: usize = 65536; /// A WireGuard tunnel. Encapsulates and decapsulates IP packets /// to be sent to and received from a remote UDP endpoint. /// This tunnel supports at most 1 peer IP at a time, but supports simultaneous ports. pub struct WireGuardTunnel { - source_peer_ip: IpAddr, + pub(crate) source_peer_ip: IpAddr, /// `boringtun` peer/tunnel implementation, used for crypto & WG protocol. - peer: Box, + peer: Mutex>, /// The UDP socket for the public WireGuard endpoint to connect to. udp: UdpSocket, /// The address of the public WireGuard endpoint (UDP). - endpoint: SocketAddr, - /// Maps virtual ports to the corresponding IP packet dispatcher. - virtual_port_ip_tx: lockfree::map::Map>>, - /// IP packet dispatcher for unroutable packets. `None` if not initialized. - sink_ip_tx: RwLock>>>, + pub(crate) endpoint: SocketAddr, + /// Event bus + bus: Bus, } impl WireGuardTunnel { /// Initialize a new WireGuard tunnel. - pub async fn new(config: &Config) -> anyhow::Result { + pub async fn new(config: &Config, bus: Bus) -> anyhow::Result { let source_peer_ip = config.source_peer_ip; - let peer = Self::create_tunnel(config)?; - let udp = UdpSocket::bind("0.0.0.0:0") - .await - .with_context(|| "Failed to create UDP socket for WireGuard connection")?; + let peer = Mutex::new(Box::new(Self::create_tunnel(config)?)); let endpoint = config.endpoint_addr; - let virtual_port_ip_tx = lockfree::map::Map::new(); + let udp = UdpSocket::bind(config.endpoint_bind_addr) + .await + .context("Failed to create UDP socket for WireGuard connection")?; Ok(Self { source_peer_ip, peer, udp, endpoint, - virtual_port_ip_tx, - sink_ip_tx: RwLock::new(None), + bus, }) } @@ -56,12 +56,16 @@ impl WireGuardTunnel { pub async fn send_ip_packet(&self, packet: &[u8]) -> anyhow::Result<()> { trace_ip_packet("Sending IP packet", packet); let mut send_buf = [0u8; MAX_PACKET]; - match self.peer.encapsulate(packet, &mut send_buf) { + let encapsulate_result = { + let mut peer = self.peer.lock().await; + peer.encapsulate(packet, &mut send_buf) + }; + match encapsulate_result { TunnResult::WriteToNetwork(packet) => { self.udp .send_to(packet, self.endpoint) .await - .with_context(|| "Failed to send encrypted IP packet to WireGuard endpoint.")?; + .context("Failed to send encrypted IP packet to WireGuard endpoint.")?; debug!( "Sent {} bytes to WireGuard endpoint (encrypted IP packet)", packet.len() @@ -83,81 +87,84 @@ impl WireGuardTunnel { Ok(()) } - /// Register a virtual interface (using its assigned virtual port) with the given IP packet `Sender`. - pub fn register_virtual_interface( - &self, - virtual_port: u16, - ) -> anyhow::Result>> { - let existing = self.virtual_port_ip_tx.get(&virtual_port); - if existing.is_some() { - Err(anyhow::anyhow!("Cannot register virtual interface with virtual port {} because it is already registered", virtual_port)) - } else { - let (sender, receiver) = tokio::sync::mpsc::channel(DISPATCH_CAPACITY); - self.virtual_port_ip_tx.insert(virtual_port, sender); - Ok(receiver) + pub async fn produce_task(&self) -> ! { + trace!("Starting WireGuard production task"); + let mut endpoint = self.bus.new_endpoint(); + + loop { + if let Event::OutboundInternetPacket(data) = endpoint.recv().await { + match self.send_ip_packet(&data).await { + Ok(_) => {} + Err(e) => { + error!("{:?}", e); + } + } + } } } - /// Register a virtual interface (using its assigned virtual port) with the given IP packet `Sender`. - pub async fn register_sink_interface( - &self, - ) -> anyhow::Result>> { - let (sender, receiver) = tokio::sync::mpsc::channel(DISPATCH_CAPACITY); - - let mut sink_ip_tx = self.sink_ip_tx.write().await; - *sink_ip_tx = Some(sender); - - Ok(receiver) - } - - /// Releases the virtual interface from IP dispatch. - pub fn release_virtual_interface(&self, virtual_port: u16) { - self.virtual_port_ip_tx.remove(&virtual_port); - } - /// WireGuard Routine task. Handles Handshake, keep-alive, etc. pub async fn routine_task(&self) -> ! { trace!("Starting WireGuard routine task"); loop { let mut send_buf = [0u8; MAX_PACKET]; - match self.peer.update_timers(&mut send_buf) { - TunnResult::WriteToNetwork(packet) => { - debug!( - "Sending routine packet of {} bytes to WireGuard endpoint", - packet.len() - ); - match self.udp.send_to(packet, self.endpoint).await { - Ok(_) => {} - Err(e) => { - error!( - "Failed to send routine packet to WireGuard endpoint: {:?}", - e - ); - } - }; - } - TunnResult::Err(e) => { - error!( - "Failed to prepare routine packet for WireGuard endpoint: {:?}", - e - ); - } - TunnResult::Done => { - // Sleep for a bit - tokio::time::sleep(Duration::from_millis(1)).await; - } - other => { - warn!("Unexpected WireGuard routine task state: {:?}", other); - } - } + let tun_result = { self.peer.lock().await.update_timers(&mut send_buf) }; + self.handle_routine_tun_result(tun_result).await; } } + #[async_recursion] + async fn handle_routine_tun_result<'a: 'async_recursion>(&self, result: TunnResult<'a>) -> () { + match result { + TunnResult::WriteToNetwork(packet) => { + debug!( + "Sending routine packet of {} bytes to WireGuard endpoint", + packet.len() + ); + match self.udp.send_to(packet, self.endpoint).await { + Ok(_) => {} + Err(e) => { + error!( + "Failed to send routine packet to WireGuard endpoint: {:?}", + e + ); + } + }; + } + TunnResult::Err(WireGuardError::ConnectionExpired) => { + warn!("Wireguard handshake has expired!"); + + let mut buf = vec![0u8; MAX_PACKET]; + let result = self + .peer + .lock() + .await + .format_handshake_initiation(&mut buf[..], false); + + self.handle_routine_tun_result(result).await + } + TunnResult::Err(e) => { + error!( + "Failed to prepare routine packet for WireGuard endpoint: {:?}", + e + ); + } + TunnResult::Done => { + // Sleep for a bit + tokio::time::sleep(Duration::from_millis(1)).await; + } + other => { + warn!("Unexpected WireGuard routine task state: {:?}", other); + } + }; + } + /// WireGuard consumption task. Receives encrypted packets from the WireGuard endpoint, /// decapsulates them, and dispatches newly received IP packets. pub async fn consume_task(&self) -> ! { trace!("Starting WireGuard consumption task"); + let endpoint = self.bus.new_endpoint(); loop { let mut recv_buf = [0u8; MAX_PACKET]; @@ -174,7 +181,11 @@ impl WireGuardTunnel { }; let data = &recv_buf[..size]; - match self.peer.decapsulate(None, data, &mut send_buf) { + let decapsulate_result = { + let mut peer = self.peer.lock().await; + peer.decapsulate(None, data, &mut send_buf) + }; + match decapsulate_result { TunnResult::WriteToNetwork(packet) => { match self.udp.send_to(packet, self.endpoint).await { Ok(_) => {} @@ -183,9 +194,10 @@ impl WireGuardTunnel { continue; } }; + let mut peer = self.peer.lock().await; loop { let mut send_buf = [0u8; MAX_PACKET]; - match self.peer.decapsulate(None, &[], &mut send_buf) { + match peer.decapsulate(None, &[], &mut send_buf) { TunnResult::WriteToNetwork(packet) => { match self.udp.send_to(packet, self.endpoint).await { Ok(_) => {} @@ -210,38 +222,8 @@ impl WireGuardTunnel { // For debugging purposes: parse packet trace_ip_packet("Received IP packet", packet); - match self.route_ip_packet(packet) { - RouteResult::Dispatch(port) => { - let sender = self.virtual_port_ip_tx.get(&port); - if let Some(sender_guard) = sender { - let sender = sender_guard.val(); - match sender.send(packet.to_vec()).await { - Ok(_) => { - trace!( - "Dispatched received IP packet to virtual port {}", - port - ); - } - Err(e) => { - error!( - "Failed to dispatch received IP packet to virtual port {}: {}", - port, e - ); - } - } - } else { - warn!("[{}] Race condition: failed to get virtual port sender after it was dispatched", port); - } - } - RouteResult::Sink => { - trace!("Sending unroutable IP packet received from WireGuard endpoint to sink interface"); - self.route_ip_sink(packet).await.unwrap_or_else(|e| { - error!("Failed to send unroutable IP packet to sink: {:?}", e) - }); - } - RouteResult::Drop => { - trace!("Dropped unroutable IP packet received from WireGuard endpoint"); - } + if let Some(proto) = self.route_protocol(packet) { + endpoint.send(Event::InboundInternetPacket(proto, packet.to_vec().into())); } } _ => {} @@ -249,78 +231,46 @@ impl WireGuardTunnel { } } - fn create_tunnel(config: &Config) -> anyhow::Result> { + fn create_tunnel(config: &Config) -> anyhow::Result { + let private = config.private_key.as_ref().clone(); + let public = *config.endpoint_public_key.as_ref(); + Tunn::new( - config.private_key.clone(), - config.endpoint_public_key.clone(), - None, + private, + public, + config.preshared_key, config.keepalive_seconds, 0, None, ) .map_err(|s| anyhow::anyhow!("{}", s)) - .with_context(|| "Failed to initialize boringtun Tunn") + .context("Failed to initialize boringtun Tunn") } - /// Makes a decision on the handling of an incoming IP packet. - fn route_ip_packet(&self, packet: &[u8]) -> RouteResult { + /// Determine the inner protocol of the incoming IP packet (TCP/UDP). + fn route_protocol(&self, packet: &[u8]) -> Option { match IpVersion::of_packet(packet) { Ok(IpVersion::Ipv4) => Ipv4Packet::new_checked(&packet) .ok() // Only care if the packet is destined for this tunnel - .filter(|packet| Ipv4Addr::from(packet.dst_addr()) == self.source_peer_ip) - .map(|packet| match packet.protocol() { - IpProtocol::Tcp => Some(self.route_tcp_segment(packet.payload())), + .filter(|packet| packet.dst_addr() == self.source_peer_ip) + .and_then(|packet| match packet.next_header() { + IpProtocol::Tcp => Some(PortProtocol::Tcp), + IpProtocol::Udp => Some(PortProtocol::Udp), // Unrecognized protocol, so we cannot determine where to route - _ => Some(RouteResult::Drop), - }) - .flatten() - .unwrap_or(RouteResult::Drop), + _ => None, + }), Ok(IpVersion::Ipv6) => Ipv6Packet::new_checked(&packet) .ok() // Only care if the packet is destined for this tunnel - .filter(|packet| Ipv6Addr::from(packet.dst_addr()) == self.source_peer_ip) - .map(|packet| match packet.next_header() { - IpProtocol::Tcp => Some(self.route_tcp_segment(packet.payload())), + .filter(|packet| packet.dst_addr() == self.source_peer_ip) + .and_then(|packet| match packet.next_header() { + IpProtocol::Tcp => Some(PortProtocol::Tcp), + IpProtocol::Udp => Some(PortProtocol::Udp), // Unrecognized protocol, so we cannot determine where to route - _ => Some(RouteResult::Drop), - }) - .flatten() - .unwrap_or(RouteResult::Drop), - _ => RouteResult::Drop, - } - } - - /// Makes a decision on the handling of an incoming TCP segment. - fn route_tcp_segment(&self, segment: &[u8]) -> RouteResult { - TcpPacket::new_checked(segment) - .ok() - .map(|tcp| { - if self.virtual_port_ip_tx.get(&tcp.dst_port()).is_some() { - RouteResult::Dispatch(tcp.dst_port()) - } else if tcp.rst() { - RouteResult::Drop - } else { - RouteResult::Sink - } - }) - .unwrap_or(RouteResult::Drop) - } - - /// Route a packet to the IP sink interface. - async fn route_ip_sink(&self, packet: &[u8]) -> anyhow::Result<()> { - let ip_sink_tx = self.sink_ip_tx.read().await; - - if let Some(ip_sink_tx) = &*ip_sink_tx { - ip_sink_tx - .send(packet.to_vec()) - .await - .with_context(|| "Failed to dispatch IP packet to sink interface") - } else { - warn!( - "Could not dispatch unroutable IP packet to sink because interface is not active." - ); - Ok(()) + _ => None, + }), + _ => None, } } } @@ -344,12 +294,3 @@ fn trace_ip_packet(message: &str, packet: &[u8]) { } } } - -enum RouteResult { - /// Dispatch the packet to the virtual port. - Dispatch(u16), - /// The packet is not routable, and should be sent to the sink interface. - Sink, - /// The packet is not routable, and can be safely ignored. - Drop, -}