diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5400d93 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,176 @@ +### Rust ### +target/ +# Libraries shouldn't lock their dependencies +Cargo.lock + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Kotlin ### +# Compiled class file +*.class + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +build/ +/*/local.properties +out/ +production/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iws + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!clients/android/gradle/wrapper/gradle-wrapper.jar + +### Apple ### +.DS_Store +.build/ +DerivedData/ +xcuserdata/ +*.xcuserstate + +Firezone/Developer.xcconfig diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01f6983..5d25eb8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,27 +34,21 @@ jobs: - macos-12 - windows-2019 - windows-2022 + # FIXME: There's this weird cargo thing where it stops finding the webrtc's related crates + # probably has to do with the cache + some cargo bug. + exclude: + - rust: nightly + runs-on: ubuntu-20.04 runs-on: ${{ matrix.runs-on }} steps: - name: Checkout uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/.crates.toml - ~/.cargo/.crates2.json - ~/.cargo/.package-cache - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Update toolchain run: rustup update --no-self-update ${{ matrix.rust }} && rustup default ${{ matrix.rust }} && rustup component add clippy + - uses: Swatinem/rust-cache@v2 - name: Run cargo static analysis checks run: | - cargo check + cargo check --workspace cargo clippy -- -D clippy::all cargo test @@ -70,18 +64,7 @@ jobs: rust: [stable] steps: - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/.crates.toml - ~/.cargo/.crates2.json - ~/.cargo/.package-cache - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: Swatinem/rust-cache@v2 - name: Setup toolchain run: | rustup update --no-self-update ${{ matrix.rust }} \ @@ -92,8 +75,8 @@ jobs: - uses: actions/cache@v3 with: path: | - ~/.gradle/caches - ~/.gradle/wrapper + ~/clients/android/.gradle/caches + ~/clients/android/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- @@ -107,10 +90,10 @@ jobs: uses: gradle/gradle-build-action@v2 with: arguments: build assembleRelease - build-root-directory: android + build-root-directory: clients/android - name: Move artifact run: | - mv ./android/lib/build/outputs/aar/lib-release.aar ./connlib-${{ needs.draft-release.outputs.tag_name }}.aar + mv ./clients/android/lib/build/outputs/aar/lib-release.aar ./connlib-${{ needs.draft-release.outputs.tag_name }}.aar - uses: actions/upload-artifact@v3 with: name: connlib-android @@ -129,18 +112,7 @@ jobs: rust: [stable] steps: - uses: actions/checkout@v3 - - uses: actions/cache@v3 - with: - path: | - ~/.cargo/.crates.toml - ~/.cargo/.crates2.json - ~/.cargo/.package-cache - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - target/ - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: Swatinem/rust-cache@v2 - name: Setup toolchain run: | rustup update --no-self-update ${{ matrix.rust }} \ @@ -160,8 +132,8 @@ jobs: env: CONFIGURATION: Release PROJECT_DIR: . + working-directory: ./clients/apple run: | - cd apple # build-xcframework.sh calls build-rust.sh indirectly via `xcodebuild`, but it pollutes the environment # to the point that it causes the `ring` build to fail for the aarch64-apple-darwin target. So, explicitly # build first. See https://github.com/briansmith/ring/issues/1332 diff --git a/.gitignore b/.gitignore index 0bcad19..5400d93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,12 @@ ### Rust ### -/target +target/ # Libraries shouldn't lock their dependencies -/Cargo.lock +Cargo.lock ### Android ### # Gradle files .gradle/ build/ -android/target/ # Local configuration file (sdk path, etc) local.properties @@ -101,11 +100,10 @@ proguard/ # Log Files # Android Studio -/*/build/ +build/ /*/local.properties -/*/out -/*/*/build -/*/*/production +out/ +production/ .navigation/ *.ipr *~ @@ -126,7 +124,6 @@ obj/ # IntelliJ IDEA *.iws -/out/ # User-specific configurations .idea/caches/ @@ -167,14 +164,13 @@ fabric.properties ### AndroidStudio Patch ### -!/gradle/wrapper/gradle-wrapper.jar +!clients/android/gradle/wrapper/gradle-wrapper.jar ### Apple ### .DS_Store .build/ -build/ DerivedData/ xcuserdata/ -**/*.xcuserstate +*.xcuserstate Firezone/Developer.xcconfig diff --git a/Cargo.toml b/Cargo.toml index fe3de85..aa60248 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,12 @@ -[package] -name = "firezone-connlib" -version = "0.1.6" -edition = "2021" - -[dependencies] -# Apple tunnel dependencies -[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", features = ["device"] } - -# Linux tunnel dependencies -[target.'cfg(target_os = "linux")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", features = ["device"] } - -# Android tunnel dependencies -[target.'cfg(target_os = "android")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", features = ["jni-bindings"] } -android_logger = "0.13" -log = "0.4.14" - -# Windows tunnel dependencies -[target.'cfg(target_os = "windows")'.dependencies] -boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f" } -wintun = "0.2.1" +[workspace] +members = [ + "clients/android", + "clients/apple", + "clients/headless", + "libs/tunnel", + "libs/client", + "libs/gateway", + "libs/common", + "gateway", + "macros", +] diff --git a/Dockerfile-gateway b/Dockerfile-gateway new file mode 100644 index 0000000..0466fc1 --- /dev/null +++ b/Dockerfile-gateway @@ -0,0 +1,11 @@ +FROM rust:1.70-slim as BUILDER +WORKDIR /build/ +COPY . ./ +RUN cargo build -p gateway --release + +# TODO: Change to musl + alpine +FROM debian:bullseye-slim +WORKDIR /app/ +COPY --from=BUILDER /build/target/release/gateway . +ENV PATH "/app:$PATH" +CMD ["gateway"] diff --git a/Dockerfile-headless b/Dockerfile-headless new file mode 100644 index 0000000..bb5e180 --- /dev/null +++ b/Dockerfile-headless @@ -0,0 +1,11 @@ +FROM rust:1.70-slim as BUILDER +WORKDIR /build/ +COPY . ./ +RUN cargo build -p headless --release + +# TODO: Change to musl + alpine +FROM debian:bullseye-slim +WORKDIR /app/ +COPY --from=BUILDER /build/target/release/headless . +ENV PATH "/app:$PATH" +CMD ["headless"] diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..c34202b --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,20 @@ +NOTICES AND INFORMATION +Do Not Translate or Localize + +This software incorporates material from third parties. + +Please refer to this document for the license terms of the components that this product depends and use. + +=== + +This product depends on and uses Boringtun source code: + +Copyright (c) 2019 Cloudflare, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/apple/Cargo.lock b/apple/Cargo.lock deleted file mode 100644 index 14afc01..0000000 --- a/apple/Cargo.lock +++ /dev/null @@ -1,1240 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "aead" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c192eb8f11fc081b0fe4259ba5af04217d4e0faddd02417310a927911abd7c8" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_log-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f0fc03f560e1aebde41c2398b691cb98b5ea5996a6184a7a67bbbb77448969" - -[[package]] -name = "android_logger" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa490e751f3878eb9accb9f18988eca52c2337ce000a8bf31ef50d4c723ca9e" -dependencies = [ - "android_log-sys", - "env_logger", - "log", - "once_cell", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "boringtun" -version = "0.5.2" -source = "git+https://github.com/cloudflare/boringtun?rev=878385f#878385f171d60effac4ad1a9d4dee41e777528b8" -dependencies = [ - "aead", - "base64", - "blake2", - "chacha20poly1305", - "hex", - "hmac", - "ip_network", - "ip_network_table", - "jni", - "libc", - "nix", - "parking_lot", - "rand_core", - "ring", - "socket2", - "thiserror", - "tracing", - "tracing-subscriber", - "untrusted 0.9.0", - "x25519-dalek", -] - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chacha20" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fc89c7c5b9e7a02dfe45cd2367bae382f9ed31c61ca8debe5f827c420a2f08" -dependencies = [ - "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 = "combine" -version = "4.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "connlib-apple" -version = "0.1.6" -dependencies = [ - "firezone-connlib", - "libc", - "swift-bridge", - "swift-bridge-build", -] - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "rand_core", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.0.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" -dependencies = [ - "cfg-if", - "fiat-crypto", - "packed_simd_2", - "platforms", - "subtle", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - -[[package]] -name = "firezone-connlib" -version = "0.1.6" -dependencies = [ - "android_logger", - "boringtun", - "log", - "wintun", -] - -[[package]] -name = "generic-array" -version = "0.14.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "ip_network" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" - -[[package]] -name = "ip_network_table" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4099b7cfc5c5e2fe8c5edf3f6f7adf7a714c9cc697534f63a5a5da30397cb2c0" -dependencies = [ - "ip_network", - "ip_network_table-deps-treebitmap", -] - -[[package]] -name = "ip_network_table-deps-treebitmap" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e537132deb99c0eb4b752f0346b6a836200eaaa3516dd7e5514b63930a09e5d" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[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.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" - -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "nix" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" -dependencies = [ - "autocfg", - "bitflags", - "cfg-if", - "libc", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "packed_simd_2" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" -dependencies = [ - "cfg-if", - "libm", -] - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.45.0", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "platforms" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" - -[[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.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro2" -version = "1.0.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "rustix" -version = "0.36.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.45.0", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "serde" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.159" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.12", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "swift-bridge" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa07c7cd2b2d7ca48d96f5abd159e3fd3eee3457e7bd03adc1994bfbdabd2f" -dependencies = [ - "swift-bridge-build", - "swift-bridge-macro", -] - -[[package]] -name = "swift-bridge-build" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286f727dc922736a1ed74c06bebf43d08b8295a7ba38c77326c74e2b9dfd43df" -dependencies = [ - "proc-macro2", - "swift-bridge-ir", - "syn 1.0.109", - "tempfile", -] - -[[package]] -name = "swift-bridge-ir" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4de97e9abde20abc1c01f6d4faa8072d723c73aba288264481a83a1e2787dc" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "swift-bridge-macro" -version = "0.1.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fabad38a0fc643ceeafefed79e08408c30eeec325b629e426a15b13d055d0a" -dependencies = [ - "proc-macro2", - "quote", - "swift-bridge-ir", - "syn 1.0.109", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - -[[package]] -name = "tempfile" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.42.0", -] - -[[package]] -name = "thiserror" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.12", -] - -[[package]] -name = "thread_local" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "typenum" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" - -[[package]] -name = "unicode-ident" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" - -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - -[[package]] -name = "universal-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "wintun" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "094235c21dfb805c6870b00d0d80f65b727d8296ab88ae6b506e827e4b4116de" -dependencies = [ - "itertools", - "libloading", - "log", - "once_cell", - "rand", - "widestring", - "winapi", -] - -[[package]] -name = "x25519-dalek" -version = "2.0.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95" -dependencies = [ - "curve25519-dalek", - "rand_core", - "serde", - "zeroize", -] - -[[package]] -name = "zeroize" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] diff --git a/apple/Cargo.toml b/apple/Cargo.toml deleted file mode 100644 index a3be9d3..0000000 --- a/apple/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "connlib-apple" -version = "0.1.6" -edition = "2021" - -build = "build.rs" - -[build-dependencies] -swift-bridge-build = "0.1" - -[dependencies] -libc = "0.2" -swift-bridge = "0.1" -firezone-connlib = { path = "../" } - -[lib] -name = "connlib" -crate-type = ["staticlib"] diff --git a/apple/src/lib.rs b/apple/src/lib.rs deleted file mode 100644 index 13f8905..0000000 --- a/apple/src/lib.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Swift bridge generated code triggers this below -#![allow(improper_ctypes)] - -use firezone_connlib::Session; -use std::sync::Arc; - -#[swift_bridge::bridge] -mod ffi { - // TODO: Allegedly not FFI safe, but works - #[swift_bridge(swift_repr = "struct")] - struct ResourceList { - resources: String, - } - - // TODO: Allegedly not FFI safe, but works - #[swift_bridge(swift_repr = "struct")] - struct TunnelAddresses { - address4: String, - address6: String, - } - - extern "Rust" { - type WrappedSession; - - #[swift_bridge(associated_to = WrappedSession)] - fn connect( - portal_url: String, - token: String, - callback_handler: CallbackHandler, - ) -> WrappedSession; - - #[swift_bridge(swift_name = "bumpSockets")] - fn bump_sockets(&self) -> bool; - - #[swift_bridge(swift_name = "disableSomeRoamingForBrokenMobileSemantics")] - fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool; - - fn disconnect(&self) -> bool; - } - - extern "Swift" { - type CallbackHandler; - - #[swift_bridge(swift_name = "onUpdateResources")] - fn on_update_resources(&self, resourceList: ResourceList) -> bool; - - #[swift_bridge(swift_name = "onSetTunnelAddresses")] - fn on_set_tunnel_addresses(&self, tunnelAddresses: TunnelAddresses) -> bool; - } -} - -pub struct WrappedSession { - session: Session, -} - -impl WrappedSession { - fn connect(portal_url: String, token: String, callback_handler: ffi::CallbackHandler) -> Self { - let session = Session::connect(portal_url, token); - - let resources = "[]".to_string(); - let cb = Arc::new(callback_handler); - let callback_handler = Arc::clone(&cb); - - callback_handler.on_update_resources(ffi::ResourceList { resources }); - callback_handler.on_set_tunnel_addresses(ffi::TunnelAddresses { - address4: "100.100.1.1".to_string(), - address6: "fd00:0222:2021:1111:0000:0000:0001:0002".to_string(), - }); - - WrappedSession { - session: session.unwrap(), - } - } - - fn bump_sockets(&self) -> bool { - // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#L177 - return true; - } - - fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { - // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 - return true; - } - - fn disconnect(&self) -> bool { - self.session.disconnect() - } -} diff --git a/android/.gitignore b/clients/android/.gitignore similarity index 100% rename from android/.gitignore rename to clients/android/.gitignore diff --git a/android/Cargo.toml b/clients/android/Cargo.toml similarity index 79% rename from android/Cargo.toml rename to clients/android/Cargo.toml index 85b9a56..6526daa 100644 --- a/android/Cargo.toml +++ b/clients/android/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] jni = { version = "0.21.1", features = ["invocation"] } -firezone-connlib = { path = "../" } +firezone-client-connlib = { path = "../../libs/client" } log = "0.4" android_logger = "0.13" diff --git a/android/build.gradle.kts b/clients/android/build.gradle.kts similarity index 100% rename from android/build.gradle.kts rename to clients/android/build.gradle.kts diff --git a/android/consumer-rules.pro b/clients/android/consumer-rules.pro similarity index 100% rename from android/consumer-rules.pro rename to clients/android/consumer-rules.pro diff --git a/android/gradle.properties b/clients/android/gradle.properties similarity index 100% rename from android/gradle.properties rename to clients/android/gradle.properties diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/clients/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.jar rename to clients/android/gradle/wrapper/gradle-wrapper.jar diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/clients/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to clients/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/gradlew b/clients/android/gradlew similarity index 100% rename from android/gradlew rename to clients/android/gradlew diff --git a/android/gradlew.bat b/clients/android/gradlew.bat similarity index 100% rename from android/gradlew.bat rename to clients/android/gradlew.bat diff --git a/android/lib/build.gradle.kts b/clients/android/lib/build.gradle.kts similarity index 100% rename from android/lib/build.gradle.kts rename to clients/android/lib/build.gradle.kts diff --git a/android/lib/src/main/AndroidManifest.xml b/clients/android/lib/src/main/AndroidManifest.xml similarity index 100% rename from android/lib/src/main/AndroidManifest.xml rename to clients/android/lib/src/main/AndroidManifest.xml diff --git a/android/lib/src/main/java/dev/firezone/connlib/Logger.kt b/clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt similarity index 100% rename from android/lib/src/main/java/dev/firezone/connlib/Logger.kt rename to clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt diff --git a/android/lib/src/main/java/dev/firezone/connlib/Session.kt b/clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt similarity index 100% rename from android/lib/src/main/java/dev/firezone/connlib/Session.kt rename to clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt diff --git a/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt b/clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt similarity index 100% rename from android/lib/src/main/java/dev/firezone/connlib/VpnService.kt rename to clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt diff --git a/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt b/clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt similarity index 100% rename from android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt rename to clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt diff --git a/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt b/clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt similarity index 100% rename from android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt rename to clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt diff --git a/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt b/clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt similarity index 100% rename from android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt rename to clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt diff --git a/android/proguard-rules.pro b/clients/android/proguard-rules.pro similarity index 100% rename from android/proguard-rules.pro rename to clients/android/proguard-rules.pro diff --git a/android/settings.gradle.kts b/clients/android/settings.gradle.kts similarity index 100% rename from android/settings.gradle.kts rename to clients/android/settings.gradle.kts diff --git a/android/src/lib.rs b/clients/android/src/lib.rs similarity index 70% rename from android/src/lib.rs rename to clients/android/src/lib.rs index 443ba43..1a80f8e 100644 --- a/android/src/lib.rs +++ b/clients/android/src/lib.rs @@ -4,7 +4,9 @@ extern crate android_logger; extern crate jni; use self::jni::JNIEnv; use android_logger::Config; -use firezone_connlib::Session; +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; use jni::objects::{JClass, JObject, JString, JValue}; use log::LevelFilter; @@ -25,19 +27,38 @@ pub extern "system" fn Java_dev_firezone_connlib_Logger_init(_: JNIEnv, _: JClas ) } +pub enum CallbackHandler {} +impl Callbacks for CallbackHandler { + fn on_update_resources(_resource_list: ResourceList) { + todo!() + } + + fn on_set_tunnel_adresses(_tunnel_addresses: TunnelAddresses) { + todo!() + } + + fn on_error(_error: &Error, _error_type: ErrorType) { + todo!() + } +} + +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] -pub extern "system" fn Java_dev_firezone_connlib_Session_connect( +pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_connect( mut env: JNIEnv, _class: JClass, portal_url: JString, portal_token: JString, callback: JObject, -) -> *const Session { +) -> *const Session { let portal_url: String = env.get_string(&portal_url).unwrap().into(); let portal_token: String = env.get_string(&portal_token).unwrap().into(); - let session = Session::connect(portal_url, portal_token).expect("Failed to connect to portal"); + let session = Box::new( + Session::connect::(portal_url.as_str(), portal_token).expect("TODO!"), + ); // TODO: Get actual IPs returned from portal based on this device let tunnelAddressesJSON = "[{\"tunnel_ipv4\": \"100.100.1.1\", \"tunnel_ipv6\": \"fd00:0222:2011:1111:6def:1001:fe67:0012\"}]"; @@ -52,42 +73,32 @@ pub extern "system" fn Java_dev_firezone_connlib_Session_connect( Err(e) => error!("Failed to call setTunnelAddresses: {:?}", e), } - // TODO: Fix callback ref copy - // let resourcesJSON = "[{\"id\": \"342b8565-5de2-4289-877c-751d924518e9\", \"label\": \"GitLab\", \"address\": \"gitlab.com\", \"tunnel_ipv4\": \"100.71.55.101\", \"tunnel_ipv6\": \"fd00:0222:2011:1111:6def:1001:fe67:0012\"}]"; - // let resources = env.new_string(resourcesJSON).unwrap(); - // match env.call_method( - // callback, - // "onUpdateResources", - // "(Ljava/lang/String;)Z", - // &[JValue::from(&resources)], - // ) { - // Ok(res) => trace!("onUpdateResources returned {:?}", res), - // Err(e) => error!("Failed to call setResources: {:?}", e), - // } - - let session_ptr = Box::into_raw(Box::new(session)); - - session_ptr + Box::into_raw(session) } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_disconnect( _env: JNIEnv, _: JClass, - session_ptr: *mut Session, + session_ptr: *mut Session, ) -> bool { if session_ptr.is_null() { return false; } - unsafe { Box::from_raw(session_ptr).disconnect() } + let session = unsafe { &mut *session_ptr }; + session.disconnect() } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_bump_sockets( - session_ptr: *const Session, + session_ptr: *const Session, ) -> bool { if session_ptr.is_null() { return false; @@ -96,13 +107,15 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_Session_bump_sockets( unsafe { (*session_ptr).bump_sockets() }; // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 - return true; + true } +/// # Safety +/// Pointers must be valid #[allow(non_snake_case)] #[no_mangle] pub unsafe extern "system" fn Java_dev_firezone_connlib_disable_some_roaming_for_broken_mobile_semantics( - session_ptr: *const Session, + session_ptr: *const Session, ) -> bool { if session_ptr.is_null() { return false; @@ -111,5 +124,5 @@ pub unsafe extern "system" fn Java_dev_firezone_connlib_disable_some_roaming_for unsafe { (*session_ptr).disable_some_roaming_for_broken_mobile_semantics() }; // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 - return true; + true } diff --git a/apple/.gitignore b/clients/apple/.gitignore similarity index 100% rename from apple/.gitignore rename to clients/apple/.gitignore diff --git a/clients/apple/Cargo.toml b/clients/apple/Cargo.toml new file mode 100644 index 0000000..80469bb --- /dev/null +++ b/clients/apple/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "connlib-apple" +version = "0.1.6" +edition = "2021" + +build = "build.rs" + +[build-dependencies] +swift-bridge-build = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } + +[dependencies] +libc = "0.2" +swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } +firezone-client-connlib = { path = "../../libs/client" } + +[lib] +name = "connlib" +crate-type = ["staticlib"] diff --git a/apple/README.md b/clients/apple/README.md similarity index 100% rename from apple/README.md rename to clients/apple/README.md diff --git a/apple/Sources/Connlib/Adapter.swift b/clients/apple/Sources/Connlib/Adapter.swift similarity index 100% rename from apple/Sources/Connlib/Adapter.swift rename to clients/apple/Sources/Connlib/Adapter.swift diff --git a/apple/Sources/Connlib/BridgingHeader.h b/clients/apple/Sources/Connlib/BridgingHeader.h similarity index 100% rename from apple/Sources/Connlib/BridgingHeader.h rename to clients/apple/Sources/Connlib/BridgingHeader.h diff --git a/apple/Sources/Connlib/CallbackHandler.swift b/clients/apple/Sources/Connlib/CallbackHandler.swift similarity index 100% rename from apple/Sources/Connlib/CallbackHandler.swift rename to clients/apple/Sources/Connlib/CallbackHandler.swift diff --git a/apple/Sources/Connlib/Generated/.gitignore b/clients/apple/Sources/Connlib/Generated/.gitignore similarity index 100% rename from apple/Sources/Connlib/Generated/.gitignore rename to clients/apple/Sources/Connlib/Generated/.gitignore diff --git a/apple/Sources/Connlib/connlib.h b/clients/apple/Sources/Connlib/connlib.h similarity index 100% rename from apple/Sources/Connlib/connlib.h rename to clients/apple/Sources/Connlib/connlib.h diff --git a/apple/Tests/connlibTests/.gitkeep b/clients/apple/Tests/connlibTests/.gitkeep similarity index 100% rename from apple/Tests/connlibTests/.gitkeep rename to clients/apple/Tests/connlibTests/.gitkeep diff --git a/apple/build-rust.sh b/clients/apple/build-rust.sh similarity index 100% rename from apple/build-rust.sh rename to clients/apple/build-rust.sh diff --git a/apple/build-xcframework.sh b/clients/apple/build-xcframework.sh similarity index 100% rename from apple/build-xcframework.sh rename to clients/apple/build-xcframework.sh diff --git a/apple/build.rs b/clients/apple/build.rs similarity index 85% rename from apple/build.rs rename to clients/apple/build.rs index b25e626..ddc9a9c 100644 --- a/apple/build.rs +++ b/clients/apple/build.rs @@ -1,4 +1,4 @@ -const XCODE_CONFIGURATION_ENV: &'static str = "CONFIGURATION"; +const XCODE_CONFIGURATION_ENV: &str = "CONFIGURATION"; fn main() { let out_dir = "Sources/Connlib/Generated"; diff --git a/apple/connlib.xcodeproj/project.pbxproj b/clients/apple/connlib.xcodeproj/project.pbxproj similarity index 100% rename from apple/connlib.xcodeproj/project.pbxproj rename to clients/apple/connlib.xcodeproj/project.pbxproj diff --git a/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme b/clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme similarity index 100% rename from apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme rename to clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme diff --git a/clients/apple/src/lib.rs b/clients/apple/src/lib.rs new file mode 100644 index 0000000..c539b79 --- /dev/null +++ b/clients/apple/src/lib.rs @@ -0,0 +1,115 @@ +// Swift bridge generated code triggers this below +#![allow(improper_ctypes)] +#![cfg(any(target_os = "macos", target_os = "ios"))] + +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, SwiftConnlibError, SwiftErrorType, + TunnelAddresses, +}; + +#[swift_bridge::bridge] +mod ffi { + #[swift_bridge(swift_repr = "struct")] + struct ResourceList { + resources: String, + } + + // TODO: Allegedly not FFI safe, but works + #[swift_bridge(swift_repr = "struct")] + struct TunnelAddresses { + address4: String, + address6: String, + } + + #[swift_bridge(already_declared)] + enum SwiftConnlibError {} + + #[swift_bridge(already_declared)] + enum SwiftErrorType {} + + extern "Rust" { + type WrappedSession; + + #[swift_bridge(associated_to = WrappedSession)] + fn connect(portal_url: String, token: String) -> Result; + + #[swift_bridge(swift_name = "bumpSockets")] + fn bump_sockets(&self) -> bool; + + #[swift_bridge(swift_name = "disableSomeRoamingForBrokenMobileSemantics")] + fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool; + + fn disconnect(&mut self) -> bool; + } + + extern "Swift" { + type Opaque; + #[swift_bridge(swift_name = "onUpdateResources")] + fn on_update_resources(resourceList: ResourceList); + + #[swift_bridge(swift_name = "onSetTunnelAddresses")] + fn on_set_tunnel_addresses(tunnelAddresses: TunnelAddresses); + + #[swift_bridge(swift_name = "onError")] + fn on_error(error: SwiftConnlibError, error_type: SwiftErrorType); + } +} + +impl From for ffi::ResourceList { + fn from(value: ResourceList) -> Self { + Self { + resources: value.resources.join(","), + } + } +} + +impl From for ffi::TunnelAddresses { + fn from(value: TunnelAddresses) -> Self { + Self { + address4: value.address4.to_string(), + address6: value.address6.to_string(), + } + } +} + +/// This is used by the apple client to interact with our code. +pub struct WrappedSession { + session: Session, +} + +struct CallbackHandler; + +impl Callbacks for CallbackHandler { + fn on_update_resources(resource_list: ResourceList) { + ffi::on_update_resources(resource_list.into()); + } + + fn on_set_tunnel_adresses(tunnel_addresses: TunnelAddresses) { + ffi::on_set_tunnel_addresses(tunnel_addresses.into()); + } + + fn on_error(error: &Error, error_type: ErrorType) { + ffi::on_error(error.into(), error_type.into()); + } +} + +impl WrappedSession { + fn connect(portal_url: String, token: String) -> Result { + let session = Session::connect::(portal_url.as_str(), token)?; + Ok(Self { session }) + } + + fn bump_sockets(&self) -> bool { + // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#L177 + todo!() + } + + fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { + // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 + todo!() + } + + fn disconnect(&mut self) -> bool { + self.session.disconnect() + } +} diff --git a/clients/headless/Cargo.toml b/clients/headless/Cargo.toml new file mode 100644 index 0000000..4ff21e6 --- /dev/null +++ b/clients/headless/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "headless" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +firezone-client-connlib = { path = "../../libs/client" } +clap = { version = "4.2", features = ["derive"] } +url = { version = "2.3.1", default-features = false } +tracing-subscriber = { version = "0.3" } +tracing = { version = "0.1" } diff --git a/clients/headless/src/main.rs b/clients/headless/src/main.rs new file mode 100644 index 0000000..fc96d7d --- /dev/null +++ b/clients/headless/src/main.rs @@ -0,0 +1,46 @@ +use clap::Parser; +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; +use url::Url; + +enum CallbackHandler {} + +impl Callbacks for CallbackHandler { + fn on_update_resources(_resource_list: ResourceList) { + todo!() + } + + fn on_set_tunnel_adresses(_tunnel_addresses: TunnelAddresses) { + todo!() + } + + fn on_error(error: &Error, error_type: ErrorType) { + match error_type { + ErrorType::Recoverable => tracing::warn!("Encountered error: {error}"), + ErrorType::Fatal => panic!("Encountered fatal error: {error}"), + } + } +} + +fn main() { + tracing_subscriber::fmt::init(); + // TODO: read args from env instead + let args = Args::parse(); + // TODO: This is disgusting + let mut session = + Session::::connect::(args.url, args.secret).unwrap(); + tracing::info!("Started new session"); + session.wait_for_ctrl_c().unwrap(); + session.disconnect(); +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + #[arg(long)] + pub secret: String, + #[arg(long)] + pub url: Url, +} diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml new file mode 100644 index 0000000..e24b817 --- /dev/null +++ b/gateway/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gateway" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +firezone-gateway-connlib = { path = "../libs/gateway" } +clap = { version = "4.2", features = ["derive"] } +url = { version = "2.3.1", default-features = false } +tracing-subscriber = { version = "0.3" } +tracing = { version = "0.1" } diff --git a/gateway/src/main.rs b/gateway/src/main.rs new file mode 100644 index 0000000..8d60914 --- /dev/null +++ b/gateway/src/main.rs @@ -0,0 +1,45 @@ +use clap::Parser; +use firezone_gateway_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; +use url::Url; + +enum CallbackHandler {} + +impl Callbacks for CallbackHandler { + fn on_update_resources(_resource_list: ResourceList) { + todo!() + } + + fn on_set_tunnel_adresses(_tunnel_addresses: TunnelAddresses) { + todo!() + } + + fn on_error(error: &Error, error_type: ErrorType) { + match error_type { + ErrorType::Recoverable => tracing::warn!("Encountered error: {error}"), + ErrorType::Fatal => panic!("Encountered fatal error: {error}"), + } + } +} + +fn main() { + tracing_subscriber::fmt::init(); + // TODO: read args from env instead + let args = Args::parse(); + // TODO: This is disgusting + let mut session = + Session::::connect::(args.url, args.secret).unwrap(); + session.wait_for_ctrl_c().unwrap(); + session.disconnect(); +} + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Args { + #[arg(long)] + pub secret: String, + #[arg(long)] + pub url: Url, +} diff --git a/libs/client/Cargo.toml b/libs/client/Cargo.toml new file mode 100644 index 0000000..c81bbc1 --- /dev/null +++ b/libs/client/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "firezone-client-connlib" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1.27", default-features = false, features = ["sync"] } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +async-trait = { version = "0.1", default-features = false } +libs-common = { path = "../common" } +firezone-tunnel = { path = "../tunnel" } +serde = { version = "1.0", default-features = false, features = ["std", "derive"] } + +[dev-dependencies] +serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/libs/client/src/control.rs b/libs/client/src/control.rs new file mode 100644 index 0000000..18215e2 --- /dev/null +++ b/libs/client/src/control.rs @@ -0,0 +1,191 @@ +use std::{marker::PhantomData, sync::Arc, time::Duration}; + +use crate::messages::{Connect, EgressMessages, InitClient, Messages, Relays}; +use libs_common::{ + boringtun::x25519::StaticSecret, + error_type::ErrorType::{Fatal, Recoverable}, + messages::{Id, ResourceDescription}, + Callbacks, ControlSession, Result, +}; + +use async_trait::async_trait; +use firezone_tunnel::{ControlSignal, Tunnel}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; + +const INTERNAL_CHANNEL_SIZE: usize = 256; + +#[async_trait] +impl ControlSignal for ControlSignaler { + async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()> { + self.internal_sender + .send(EgressMessages::ListRelays { + resource_id: resource.id(), + }) + .await?; + Ok(()) + } +} + +/// Implementation of [ControlSession] for clients. +pub struct ControlPlane { + tunnel: Arc>, + control_signaler: ControlSignaler, + _phantom: PhantomData, +} + +#[derive(Clone)] +struct ControlSignaler { + internal_sender: Arc>, +} + +impl ControlPlane +where + C: Send + Sync + 'static, +{ + #[tracing::instrument(level = "trace", skip(self))] + async fn start(mut self, mut receiver: Receiver) { + let mut interval = tokio::time::interval(Duration::from_secs(10)); + loop { + tokio::select! { + Some(msg) = receiver.recv() => self.handle_message(msg).await, + _ = interval.tick() => self.stats_event().await, + else => break + } + } + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn init( + &mut self, + InitClient { + interface, + resources, + }: InitClient, + ) { + if let Err(e) = self.tunnel.set_interface(&interface).await { + tracing::error!("Couldn't intialize interface: {e}"); + C::on_error(&e, Fatal); + return; + } + + for resource_description in resources { + self.add_resource(resource_description).await + } + + tracing::info!("Firezoned Started!"); + } + + #[tracing::instrument(level = "trace", skip(self))] + async fn connect( + &mut self, + Connect { + rtc_sdp, + resource_id, + gateway_public_key, + }: Connect, + ) { + if let Err(e) = self + .tunnel + .recieved_offer_response(resource_id, rtc_sdp, gateway_public_key.0.into()) + .await + { + C::on_error(&e, Recoverable); + } + } + + #[tracing::instrument(level = "trace", skip(self))] + async fn add_resource(&self, resource_description: ResourceDescription) { + self.tunnel.add_resource(resource_description).await; + } + + #[tracing::instrument(level = "trace", skip(self))] + fn remove_resource(&self, id: Id) { + todo!() + } + + #[tracing::instrument(level = "trace", skip(self))] + fn update_resource(&self, resource_description: ResourceDescription) { + todo!() + } + + #[tracing::instrument(level = "trace", skip(self))] + fn relays( + &self, + Relays { + resource_id, + relays, + }: Relays, + ) { + let tunnel = Arc::clone(&self.tunnel); + let control_signaler = self.control_signaler.clone(); + tokio::spawn(async move { + match tunnel.request_connection(resource_id, relays).await { + Ok(connection_request) => { + if let Err(err) = control_signaler + .internal_sender + .send(EgressMessages::RequestConnection(connection_request)) + .await + { + tunnel.cleanup_connection(resource_id); + C::on_error(&err.into(), Recoverable); + } + } + Err(err) => { + tunnel.cleanup_connection(resource_id); + C::on_error(&err, Recoverable); + } + } + }); + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn handle_message(&mut self, msg: Messages) { + match msg { + Messages::Init(init) => self.init(init).await, + Messages::Relays(connection_details) => self.relays(connection_details), + Messages::Connect(connect) => self.connect(connect).await, + Messages::ResourceAdded(resource) => self.add_resource(resource).await, + Messages::ResourceRemoved(resource) => self.remove_resource(resource.id), + Messages::ResourceUpdated(resource) => self.update_resource(resource), + } + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn stats_event(&mut self) { + // TODO + } +} + +#[async_trait] +impl ControlSession + for ControlPlane +{ + #[tracing::instrument(level = "trace", skip(private_key))] + async fn start( + private_key: StaticSecret, + ) -> Result<(Sender, Receiver)> { + // This is kinda hacky, the buffer size is 1 so that we make sure that we + // process one message at a time, blocking if a previous message haven't been processed + // to force queue ordering. + let (sender, receiver) = channel::(1); + + let (internal_sender, internal_receiver) = channel(INTERNAL_CHANNEL_SIZE); + let internal_sender = Arc::new(internal_sender); + let control_signaler = ControlSignaler { internal_sender }; + let tunnel = Arc::new(Tunnel::new(private_key, control_signaler.clone()).await?); + + let control_plane = ControlPlane:: { + tunnel, + control_signaler, + _phantom: PhantomData, + }; + + tokio::spawn(async move { control_plane.start(receiver).await }); + + Ok((sender, internal_receiver)) + } + + fn socket_path() -> &'static str { + "device" + } +} diff --git a/libs/client/src/lib.rs b/libs/client/src/lib.rs new file mode 100644 index 0000000..1a04c75 --- /dev/null +++ b/libs/client/src/lib.rs @@ -0,0 +1,21 @@ +//! Main connlib library for clients. +use control::ControlPlane; +use messages::EgressMessages; +use messages::IngressMessages; + +mod control; +mod messages; + +/// Session type for clients. +/// +/// For more information see libs_common docs on [Session][libs_common::Session]. +pub type Session = + libs_common::Session, IngressMessages, EgressMessages, ReplyMessages, Messages>; + +pub use libs_common::{ + error::SwiftConnlibError, + error_type::{ErrorType, SwiftErrorType}, + Callbacks, Error, ResourceList, TunnelAddresses, +}; +use messages::Messages; +use messages::ReplyMessages; diff --git a/libs/client/src/messages.rs b/libs/client/src/messages.rs new file mode 100644 index 0000000..67e2a48 --- /dev/null +++ b/libs/client/src/messages.rs @@ -0,0 +1,274 @@ +use firezone_tunnel::RTCSessionDescription; +use serde::{Deserialize, Serialize}; + +use libs_common::messages::{Id, Interface, Key, Relay, RequestConnection, ResourceDescription}; + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct InitClient { + pub interface: Interface, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub resources: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct RemoveResource { + pub id: Id, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Connect { + pub rtc_sdp: RTCSessionDescription, + pub resource_id: Id, + pub gateway_public_key: Key, +} + +// Just because RTCSessionDescription doesn't implement partialeq +impl PartialEq for Connect { + fn eq(&self, other: &Self) -> bool { + self.resource_id == other.resource_id && self.gateway_public_key == other.gateway_public_key + } +} + +impl Eq for Connect {} + +/// List of relays +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Relays { + /// Resource id corresponding to the relay + pub resource_id: Id, + /// The actual list of relays + pub relays: Vec, +} + +// These messages are the messages that can be recieved +// by a client. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +// TODO: We will need to re-visit webrtc-rs +#[allow(clippy::large_enum_variant)] +pub enum IngressMessages { + Init(InitClient), + Connect(Connect), + + // Resources: arrive in an orderly fashion + ResourceAdded(ResourceDescription), + ResourceRemoved(RemoveResource), + ResourceUpdated(ResourceDescription), +} + +/// The replies that can arrive from the channel by a client +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +pub enum ReplyMessages { + Relays(Relays), +} + +/// The totality of all messages (might have a macro in the future to derive the other types) +#[derive(Debug, Clone, PartialEq, Eq)] +#[allow(clippy::large_enum_variant)] +pub enum Messages { + Init(InitClient), + Relays(Relays), + Connect(Connect), + + // Resources: arrive in an orderly fashion + ResourceAdded(ResourceDescription), + ResourceRemoved(RemoveResource), + ResourceUpdated(ResourceDescription), +} + +impl From for Messages { + fn from(value: IngressMessages) -> Self { + match value { + IngressMessages::Init(m) => Self::Init(m), + IngressMessages::Connect(m) => Self::Connect(m), + IngressMessages::ResourceAdded(m) => Self::ResourceAdded(m), + IngressMessages::ResourceRemoved(m) => Self::ResourceRemoved(m), + IngressMessages::ResourceUpdated(m) => Self::ResourceUpdated(m), + } + } +} + +impl From for Messages { + fn from(value: ReplyMessages) -> Self { + match value { + ReplyMessages::Relays(m) => Self::Relays(m), + } + } +} + +// These messages can be sent from a client to a control pane +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +// TODO: We will need to re-visit webrtc-rs +#[allow(clippy::large_enum_variant)] +pub enum EgressMessages { + ListRelays { resource_id: Id }, + RequestConnection(RequestConnection), +} + +#[cfg(test)] +mod test { + use libs_common::{ + control::PhoenixMessage, + messages::{ + Interface, Relay, ResourceDescription, ResourceDescriptionCidr, ResourceDescriptionDns, + Stun, Turn, + }, + }; + + use crate::messages::{EgressMessages, Relays, ReplyMessages}; + + use super::{IngressMessages, InitClient}; + + // TODO: request_connection tests + + #[test] + fn init_phoenix_message() { + let m = PhoenixMessage::new( + "device", + IngressMessages::Init(InitClient { + interface: Interface { + ipv4: "100.72.112.111".parse().unwrap(), + ipv6: "fd00:2011:1111::13:efb9".parse().unwrap(), + upstream_dns: vec![], + }, + resources: vec![ + ResourceDescription::Cidr(ResourceDescriptionCidr { + id: "73037362-715d-4a83-a749-f18eadd970e6".parse().unwrap(), + address: "172.172.0.0/16".parse().unwrap(), + name: "172.172.0.0/16".to_string(), + }), + ResourceDescription::Dns(ResourceDescriptionDns { + id: "03000143-e25e-45c7-aafb-144990e57dcd".parse().unwrap(), + address: "gitlab.mycorp.com".to_string(), + ipv4: "100.126.44.50".parse().unwrap(), + ipv6: "fd00:2011:1111::e:7758".parse().unwrap(), + name: "gitlab.mycorp.com".to_string(), + }), + ], + }), + ); + println!("{}", serde_json::to_string(&m).unwrap()); + let message = r#"{ + "event": "init", + "payload": { + "interface": { + "ipv4": "100.72.112.111", + "ipv6": "fd00:2011:1111::13:efb9", + "upstream_dns": [] + }, + "resources": [ + { + "address": "172.172.0.0/16", + "id": "73037362-715d-4a83-a749-f18eadd970e6", + "name": "172.172.0.0/16", + "type": "cidr" + }, + { + "address": "gitlab.mycorp.com", + "id": "03000143-e25e-45c7-aafb-144990e57dcd", + "ipv4": "100.126.44.50", + "ipv6": "fd00:2011:1111::e:7758", + "name": "gitlab.mycorp.com", + "type": "dns" + } + ] + }, + "ref": null, + "topic": "device" + }"#; + let ingress_message: PhoenixMessage = + serde_json::from_str(message).unwrap(); + assert_eq!(m, ingress_message); + } + + #[test] + fn list_relays_message() { + let m = PhoenixMessage::::new( + "device", + EgressMessages::ListRelays { + resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(), + }, + ); + let message = r#" + { + "event": "list_relays", + "payload": { + "resource_id": "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3" + }, + "ref":null, + "topic": "device" + } + "#; + let egress_message = serde_json::from_str(&message).unwrap(); + assert_eq!(m, egress_message); + } + + #[test] + fn list_relays_reply() { + let m = PhoenixMessage::::new_reply( + "device", + ReplyMessages::Relays(Relays { + resource_id: "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3".parse().unwrap(), + relays: vec![ + Relay::Stun(Stun { + uri: "stun:189.172.73.111:3478".to_string(), + }), + Relay::Turn(Turn { + expires_at: 1686629954, + uri: "turn:189.172.73.111:3478".to_string(), + username: "1686629954:C7I74wXYFdFugMYM".to_string(), + password: "OXXRDJ7lJN1cm+4+2BWgL87CxDrvpVrn5j3fnJHye98".to_string(), + }), + Relay::Stun(Stun { + uri: "stun:::1:3478".to_string(), + }), + Relay::Turn(Turn { + expires_at: 1686629954, + uri: "turn:::1:3478".to_string(), + username: "1686629954:dpHxHfNfOhxPLfMG".to_string(), + password: "8Wtb+3YGxO6ia23JUeSEfZ2yFD6RhGLkbgZwqjebyKY".to_string(), + }), + ], + }), + ); + let message = r#" + { + "ref":null, + "topic":"device", + "event": "phx_reply", + "payload": { + "response": { + "relays": [ + { + "type":"stun", + "uri":"stun:189.172.73.111:3478" + }, + { + "expires_at": 1686629954, + "password": "OXXRDJ7lJN1cm+4+2BWgL87CxDrvpVrn5j3fnJHye98", + "type": "turn", + "uri": "turn:189.172.73.111:3478", + "username":"1686629954:C7I74wXYFdFugMYM" + }, + { + "type": "stun", + "uri": "stun:::1:3478" + }, + { + "expires_at": 1686629954, + "password": "8Wtb+3YGxO6ia23JUeSEfZ2yFD6RhGLkbgZwqjebyKY", + "type": "turn", + "uri": "turn:::1:3478", + "username": "1686629954:dpHxHfNfOhxPLfMG" + }], + "resource_id": "f16ecfa0-a94f-4bfd-a2ef-1cc1f2ef3da3" + }, + "status":"ok" + } + }"#; + let reply_message = serde_json::from_str(&message).unwrap(); + assert_eq!(m, reply_message); + } +} diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml new file mode 100644 index 0000000..eda1210 --- /dev/null +++ b/libs/common/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "libs-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +jni-bindings = ["boringtun/jni-bindings"] + +[dependencies] +base64 = { version = "0.21", default-features = false, features = ["std"] } +serde = { version = "1.0", default-features = false, features = ["derive", "std"] } +futures = { version = "0.3", default-features = false, features = ["std", "async-await", "executor"] } +futures-util = { version = "0.3", default-features = false, features = ["std", "async-await", "async-await-macro"] } +tokio-tungstenite = { version = "0.18", default-features = false, features = ["connect", "handshake"] } +webrtc = { version = "0.7" } +uuid = { version = "1.3", default-features = false, features = ["std", "v4", "serde"] } +thiserror = { version = "1.0", default-features = false } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +serde_json = { version = "1.0", default-features = false, features = ["std"] } +tokio = { version = "1.28", default-features = false, features = ["rt", "rt-multi-thread"]} +url = { version = "2.3.1", default-features = false } +rand_core = { version = "0.6.4", default-features = false, features = ["std"] } +async-trait = { version = "0.1", default-features = false } +backoff = { version = "0.4", default-features = false } +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false } +ip_network = { version = "0.4", default-features = false, features = ["serde"] } + +macros = { path = "../../macros" } + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +swift-bridge = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } + +[target.'cfg(target_os = "linux")'.dependencies] +rtnetlink = { version = "0.12", default-features = false, features = ["tokio_socket"] } diff --git a/libs/common/src/control.rs b/libs/common/src/control.rs new file mode 100644 index 0000000..6058379 --- /dev/null +++ b/libs/common/src/control.rs @@ -0,0 +1,334 @@ +//! Control protocol related module. +//! +//! This modules contains the logic for handling in and out messages through the control plane. +//! Handling of the message itself can be found in the other lib crates. +//! +//! Entrypoint for this module is [PhoenixChannel]. +use std::{marker::PhantomData, time::Duration}; + +use base64::Engine; +use futures::{ + channel::mpsc::{channel, Receiver, Sender}, + TryStreamExt, +}; +use futures_util::{Future, SinkExt, StreamExt}; +use rand_core::{OsRng, RngCore}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use tokio_tungstenite::{ + connect_async, + tungstenite::{self, handshake::client::Request}, +}; +use tungstenite::Message; +use url::Url; + +use crate::{Error, Result}; + +const CHANNEL_SIZE: usize = 1_000; + +/// Main struct to interact with the control-protocol channel. +/// +/// After creating a new `PhoenixChannel` using [PhoenixChannel::new] you need to +/// use [start][PhoenixChannel::start] for the channel to do anything. +/// +/// If you want to send something through the channel you need to obtain a [PhoenixSender] through +/// [PhoenixChannel::sender], this will already clone the sender so no need to clone it after you obtain it. +/// +/// When [PhoenixChannel::start] is called a new websocket is created that will listen message from the control plane +/// based on the parameters passed on [new][PhoenixChannel::new], from then on any messages sent with a sender +/// obtained by [PhoenixChannel::sender] will be forwarded to the websocket up to the control plane. Ingress messages +/// will be passed on to the `handler` provided in [PhoenixChannel::new]. +/// +/// The future returned by [PhoenixChannel::start] will finish when the websocket closes (by an error), meaning that if you +/// `await` it, it will block until you use `close` in a [PhoenixSender], the portal close the connection or something goes wrong. +pub struct PhoenixChannel { + uri: Url, + handler: F, + sender: Sender, + receiver: Receiver, + _phantom: PhantomData<(I, R, M)>, +} + +// This is basically the same as tungstenite does but we add some new headers (namely user-agent) +fn make_request(uri: &Url) -> Result { + let host = uri.host().ok_or(Error::UriError)?; + let host = if let Some(port) = uri.port() { + format!("{host}:{port}") + } else { + host.to_string() + }; + + let mut r = [0u8; 16]; + OsRng.fill_bytes(&mut r); + let key = base64::engine::general_purpose::STANDARD.encode(r); + + let req = Request::builder() + .method("GET") + .header("Host", host) + .header("Connection", "Upgrade") + .header("Upgrade", "websocket") + .header("Sec-WebSocket-Version", "13") + .header("Sec-WebSocket-Key", key) + // TODO: Get OS Info here (os_info crate) + .header("User-Agent", "MacOs/13.3 (Mac) connlib/0.1.0") + .uri(uri.as_str()) + .body(())?; + Ok(req) +} + +impl PhoenixChannel +where + I: DeserializeOwned, + R: DeserializeOwned, + M: From + From, + F: Fn(M) -> Fut, + Fut: Future + Send + 'static, +{ + /// Starts the tunnel with the parameters given in [Self::new]. + /// + // (Note: we could add a generic list of messages but this is easier) + /// Additionally, you can add a list of topic to join after connection ASAP. + /// + /// See [struct-level docs][PhoenixChannel] for more info. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn start(&mut self, topics: Vec) -> Result<()> { + tracing::trace!("Trying to connect to the portal..."); + + let (ws_stream, _) = connect_async(make_request(&self.uri)?).await?; + + tracing::trace!("Successfully connected to portal"); + + let (mut write, read) = ws_stream.split(); + + let mut sender = self.sender(); + let Self { + handler, receiver, .. + } = self; + + let process_messages = read.try_for_each(|message| async { + Self::message_process(handler, message).await; + Ok(()) + }); + + // Would we like to do write.send_all(futures::stream(Message::text(...))) ? + // yes. + // but since write is taken by reference rust doesn't believe this future is sendable anymore + // so this works for now, since we only use it with 1 topic. + for topic in topics { + write + .send(Message::Text( + // We don't care about the reply type when serializing + serde_json::to_string(&PhoenixMessage::<_, ()>::new( + topic, + EgressControlMessage::PhxJoin(Empty {}), + )) + .expect("we should always be able to serialize a join topic message"), + )) + .await?; + } + + // TODO: is Forward cancel safe? + // I would assume it is and that's the advantage over + // while let Some(item) = reciever.next().await { write.send(item) } ... + // but double check this! + // If it's not cancel safe this means an item can be consumed and never sent. + // Furthermore can this also happen if write errors out? *that* I'd assume is possible... + // What option is left? write a new future to forward items. + // For now we should never assume that an item arrived the portal because we sent it! + let send_messages = receiver.map(Ok).forward(write); + + let phoenix_heartbeat = tokio::spawn(async move { + let mut timer = tokio::time::interval(Duration::from_secs(30)); + loop { + timer.tick().await; + let Ok(_) = sender.send("phoenix", EgressControlMessage::Heartbeat(Empty {})).await else { break }; + } + }); + + futures_util::pin_mut!(process_messages, send_messages); + // processing messages should be quick otherwise it'd block sending messages. + // we could remove this limitation by spawning a separate taks for each of these. + let result = futures::future::select(process_messages, send_messages) + .await + .factor_first() + .0; + phoenix_heartbeat.abort(); + result?; + + Ok(()) + } + + #[tracing::instrument(level = "trace", skip(handler))] + async fn message_process(handler: &F, message: tungstenite::Message) { + tracing::trace!("{message:?}"); + + match message.into_text() { + Ok(m_str) => match serde_json::from_str::>(&m_str) { + Ok(m) => match m.payload { + Payload::Message(m) => handler(m.into()).await, + Payload::Reply(status) => match status { + ReplyMessage::PhxReply(phx_reply) => match phx_reply { + // TODO: Here we should pass error info to a subscriber + PhxReply::Error(info) => tracing::error!("Portal error: {info:?}"), + PhxReply::Ok(reply) => match reply { + OkReply::NoMessage(Empty {}) => { + tracing::trace!("Phoenix status message") + } + OkReply::Message(m) => handler(m.into()).await, + }, + }, + ReplyMessage::PhxError(Empty {}) => tracing::error!("Phoenix error"), + }, + }, + Err(e) => { + tracing::error!("Error deserializing message {m_str}: {e:?}"); + } + }, + _ => tracing::error!("Recieved message that is not text"), + } + } + + /// Obtains a new sender that can be used to send message with this [PhoenixChannel] to the portal. + /// + /// Note that for the sender to relay any message will need the future returned [PhoenixChannel::start] to be polled (await it), + /// and [PhoenixChannel::start] takes `&mut self`, meaning you need to get the sender before running [PhoenixChannel::start]. + pub fn sender(&self) -> PhoenixSender { + PhoenixSender { + sender: self.sender.clone(), + } + } + + /// Creates a new [PhoenixChannel] not started yet. + /// + /// # Parameters: + /// - `uri`: Portal's websocket uri + /// - `handler`: The handle that will be called for each recieved message. + /// + /// For more info see [struct-level docs][PhoenixChannel]. + pub fn new(uri: Url, handler: F) -> Self { + let (sender, receiver) = channel(CHANNEL_SIZE); + + Self { + sender, + receiver, + uri, + handler, + _phantom: PhantomData, + } + } +} + +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +#[serde(untagged)] +enum Payload { + // We might want other type for the reply message + // but that makes everything even more convoluted! + // and we need to think how to make this whole mess less convoluted. + Reply(ReplyMessage), + Message(T), +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub struct PhoenixMessage { + topic: String, + #[serde(flatten)] + payload: Payload, + #[serde(rename = "ref")] + reference: Option, +} + +impl PhoenixMessage { + pub fn new(topic: impl Into, payload: T) -> Self { + Self { + topic: topic.into(), + payload: Payload::Message(payload), + reference: None, + } + } + + pub fn new_reply(topic: impl Into, payload: R) -> Self { + Self { + topic: topic.into(), + // There has to be a better way :\ + payload: Payload::Reply(ReplyMessage::PhxReply(PhxReply::Ok(OkReply::Message( + payload, + )))), + reference: None, + } + } +} + +// Awful hack to get serde_json to generate an empty "{}" instead of using "null" +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)] +#[serde(deny_unknown_fields)] +struct Empty {} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +enum EgressControlMessage { + PhxJoin(Empty), + Heartbeat(Empty), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +enum ReplyMessage { + PhxReply(PhxReply), + PhxError(Empty), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(untagged)] +enum OkReply { + Message(T), + NoMessage(Empty), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +enum ErrorInfo { + Reason(String), + Offline, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "status", content = "response")] +enum PhxReply { + Ok(OkReply), + Error(ErrorInfo), +} + +/// You can use this sender to send messages through a `PhoenixChannel`. +/// +/// Messages won't be sent unless [PhoenixChannel::start] is running, internally +/// this sends messages through a future channel that are forwrarded then in [PhoenixChannel] event loop +pub struct PhoenixSender { + sender: Sender, +} + +impl PhoenixSender { + /// Sends a message upstream to a connected [PhoenixChannel]. + /// + /// # Parameters + /// - topic: Phoenix topic + /// - payload: Message's payload + pub async fn send(&mut self, topic: impl Into, payload: impl Serialize) -> Result<()> { + // We don't care about the reply type when serializing + let str = serde_json::to_string(&PhoenixMessage::<_, ()>::new(topic, payload))?; + self.sender.send(Message::text(str)).await?; + Ok(()) + } + + /// Join a phoenix topic, meaning that after this method is invoked [PhoenixChannel] will + /// recieve messages from that topic, given that upstream accepts you into the given topic. + pub async fn join_topic(&mut self, topic: impl Into) -> Result<()> { + self.send(topic, EgressControlMessage::PhxJoin(Empty {})) + .await + } + + /// Closes the [PhoenixChannel] + pub async fn close(&mut self) -> Result<()> { + self.sender.send(Message::Close(None)).await?; + self.sender.close().await?; + Ok(()) + } +} diff --git a/libs/common/src/error.rs b/libs/common/src/error.rs new file mode 100644 index 0000000..7e0682b --- /dev/null +++ b/libs/common/src/error.rs @@ -0,0 +1,103 @@ +//! Error module. +use base64::{DecodeError, DecodeSliceError}; +use boringtun::noise::errors::WireGuardError; +use macros::SwiftEnum; +use thiserror::Error; + +/// Unified Result type to use across connlib. +pub type Result = std::result::Result; + +/// Unified error type to use across connlib. +#[derive(Error, Debug, SwiftEnum)] +pub enum ConnlibError { + /// Standard IO error. + #[error(transparent)] + Io(#[from] std::io::Error), + /// Error while decoding a base64 value. + #[error("There was an error while decoding a base64 value: {0}")] + Base64DecodeError(#[from] DecodeError), + /// Error while decoding a base64 value from a slice. + #[error("There was an error while decoding a base64 value: {0}")] + Base64DecodeSliceError(#[from] DecodeSliceError), + /// Request error for websocket connection. + #[error("Error forming request: {0}")] + RequestError(#[from] tokio_tungstenite::tungstenite::http::Error), + /// Error during websocket connection. + #[error("Portal connection error: {0}")] + PortalConnectionError(#[from] tokio_tungstenite::tungstenite::error::Error), + /// Provided string was not formatted as a URL. + #[error("Badly formatted URI")] + UriError, + /// Serde's serialize error. + #[error(transparent)] + SerializeError(#[from] serde_json::Error), + /// Webrtc errror + #[error("ICE-related error: {0}")] + IceError(#[from] webrtc::Error), + /// Webrtc error regarding data channel. + #[error("ICE-data error: {0}")] + IceDataError(#[from] webrtc::data::Error), + /// Error while sending through an async channelchannel. + #[error("Error sending message through an async channel")] + SendChannelError, + /// Error when trying to establish connection between peers. + #[error("Error while establishing connection between peers")] + ConnectionEstablishError, + /// Error related to wireguard protocol. + #[error("Wireguard error")] + WireguardError(WireGuardError), + /// Expected an initialized runtime but there was none. + #[error("Expected runtime to be initialized")] + NoRuntime, + /// Tried to access a resource which didn't exists. + #[error("Tried to access an undefined resource")] + UnknownResource, + /// Error regarding our own control protocol. + #[error("Control plane protocol error. Unexpected messages or message order.")] + ControlProtocolError, + /// Error when reading system's interface + #[error("Error while reading system's interface")] + IfaceRead(std::io::Error), + /// Glob for errors without a type. + #[error("Other error: {0}")] + Other(&'static str), + /// Invalid tunnel name + #[error("Invalid tunnel name")] + InvalidTunnelName, + #[error(transparent)] + NetlinkError(#[from] rtnetlink::Error), + /// No iface found + #[error("No iface found")] + NoIface, + /// No MTU found + #[error("No MTU found")] + NoMtu, +} + +/// Type auto-generated by [SwiftEnum] intended to be used with rust-swift-bridge. +/// All the variants come from [ConnlibError], reference that for documentaiton. +pub use swift_ffi::SwiftConnlibError; + +impl From for ConnlibError { + fn from(e: WireGuardError) -> Self { + ConnlibError::WireguardError(e) + } +} + +impl From<&'static str> for ConnlibError { + fn from(e: &'static str) -> Self { + ConnlibError::Other(e) + } +} + +impl From> for ConnlibError { + fn from(_: tokio::sync::mpsc::error::SendError) -> Self { + ConnlibError::SendChannelError + } +} + +impl From for ConnlibError { + fn from(_: futures::channel::mpsc::SendError) -> Self { + ConnlibError::SendChannelError + } +} diff --git a/libs/common/src/error_type.rs b/libs/common/src/error_type.rs new file mode 100644 index 0000000..7f411c8 --- /dev/null +++ b/libs/common/src/error_type.rs @@ -0,0 +1,20 @@ +//! Module that contains the Error-Type that hints how to handle an error to upper layers. +use macros::SwiftEnum; +/// This indicates whether the produced error is something recoverable or fatal. +/// Fata/Recoverable only indicates how to handle the error for the client. +/// +/// Any of the errors in [ConnlibError][crate::error::ConnlibError] could be of any [ErrorType] depending the circumstance. +#[derive(Debug, Clone, Copy, SwiftEnum)] +pub enum ErrorType { + /// Recoverable means that the session can continue + /// e.g. Failed to send an SDP + Recoverable, + /// Fatal error means that the session should stop and start again, + /// generally after user input, such as clicking connect once more. + /// e.g. Max number of retries was reached when trying to connect to the portal. + Fatal, +} + +/// Auto generated enum by [SwiftEnum], all variants come from [ErrorType] +/// reference that for docs. +pub use swift_ffi::SwiftErrorType; diff --git a/libs/common/src/lib.rs b/libs/common/src/lib.rs new file mode 100644 index 0000000..2d4c0fe --- /dev/null +++ b/libs/common/src/lib.rs @@ -0,0 +1,18 @@ +//! This crates contains shared types and behavior between all the other libraries. +//! +//! This includes types provided by external crates, i.e. [boringtun] to make sure that +//! we are using the same version across our own crates. + +pub mod error; +pub mod error_type; + +mod session; + +pub mod control; +pub mod messages; + +pub use boringtun; +pub use error::ConnlibError as Error; +pub use error::Result; + +pub use session::{Callbacks, ControlSession, ResourceList, Session, TunnelAddresses}; diff --git a/libs/common/src/messages.rs b/libs/common/src/messages.rs new file mode 100644 index 0000000..f2c1470 --- /dev/null +++ b/libs/common/src/messages.rs @@ -0,0 +1,160 @@ +//! Message types that are used by both the gateway and client. +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use ip_network::IpNetwork; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; + +mod key; + +pub use key::Key; + +/// General type for handling portal's id (UUID v4) +pub type Id = Uuid; + +/// Represents a wireguard peer. +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct Peer { + /// Keepalive: How often to send a keep alive message. + pub persistent_keepalive: Option, + /// Peer's public key. + pub public_key: Key, + /// Peer's Ipv4 (only 1 ipv4 per peer for now and mandatory). + pub ipv4: Ipv4Addr, + /// Peer's Ipv6 (only 1 ipv6 per peer for now and mandatory). + pub ipv6: Ipv6Addr, + /// Preshared key for the given peer. + pub preshared_key: Key, +} + +/// Represent a connection request from a client to a given resource. +/// +/// While this is a client-only message it's hosted in common since the tunnel +/// make use of this message type. +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RequestConnection { + /// Resource id the request is for. + pub resource_id: Id, + /// The preshared key the client generated for the connection that it is trying to establish. + pub device_preshared_key: Key, + /// Client's local RTC Session Description that the client will use for this connection. + pub device_rtc_session_description: RTCSessionDescription, +} + +// Custom implementation of partial eq to ignore client_rtc_sdp +impl PartialEq for RequestConnection { + fn eq(&self, other: &Self) -> bool { + self.resource_id == other.resource_id + && self.device_preshared_key == other.device_preshared_key + } +} + +impl Eq for RequestConnection {} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum ResourceDescription { + Dns(ResourceDescriptionDns), + Cidr(ResourceDescriptionCidr), +} + +/// Description of a resource that maps to a DNS record. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ResourceDescriptionDns { + /// Resource's id. + pub id: Id, + /// Internal resource's domain name. + pub address: String, + /// Resource's ipv4 mapping. + /// + /// Note that this is not the actual ipv4 for the resource not even wireguard's ipv4 for the resource. + /// This is just the mapping we use internally between a resource and its ip for intercepting packets. + pub ipv4: Ipv4Addr, + /// Resource's ipv6 mapping. + /// + /// Note that this is not the actual ipv6 for the resource not even wireguard's ipv6 for the resource. + /// This is just the mapping we use internally between a resource and its ip for intercepting packets. + pub ipv6: Ipv6Addr, + /// Name of the resource. + /// + /// Used only for display. + pub name: String, +} + +impl ResourceDescription { + pub fn ips(&self) -> Vec { + match self { + ResourceDescription::Dns(r) => vec![r.ipv4.into(), r.ipv6.into()], + ResourceDescription::Cidr(r) => vec![r.address], + } + } + + pub fn id(&self) -> Id { + match self { + ResourceDescription::Dns(r) => r.id, + ResourceDescription::Cidr(r) => r.id, + } + } +} + +/// Description of a resource that maps to a CIDR. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct ResourceDescriptionCidr { + /// Resource's id. + pub id: Id, + /// CIDR that this resource points to. + pub address: IpNetwork, + /// Name of the resource. + /// + /// Used only for display. + pub name: String, +} + +/// Represents a wireguard interface configuration. +/// +/// Note that the ips are /32 for ipv4 and /128 for ipv6. +/// This is done to minimize collisions and we update the routing table manually. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Interface { + /// Interface's Ipv4. + pub ipv4: Ipv4Addr, + /// Interface's Ipv6. + pub ipv6: Ipv6Addr, + /// DNS that will be used to query for DNS that aren't within our resource list. + #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default)] + pub upstream_dns: Vec, +} + +/// A single relay +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum Relay { + /// STUN type of relay + Stun(Stun), + /// TURN type of relay + Turn(Turn), +} + +/// Represent a TURN relay +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Turn { + // TODO: DateTIme + //// Expire time of the username/password in unix millisecond timestamp UTC + pub expires_at: u64, + /// URI of the relay + pub uri: String, + /// Username for the relay + pub username: String, + // TODO: SecretString + /// Password for the relay + pub password: String, +} + +/// Stun kind of relay +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Stun { + /// URI for the relay + pub uri: String, +} diff --git a/libs/common/src/messages/key.rs b/libs/common/src/messages/key.rs new file mode 100644 index 0000000..9499ce8 --- /dev/null +++ b/libs/common/src/messages/key.rs @@ -0,0 +1,54 @@ +use base64::{display::Base64Display, engine::general_purpose::STANDARD, Engine}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use std::{fmt, str::FromStr}; + +use crate::Error; + +const KEY_SIZE: usize = 32; + +/// A `Key` struct to hold interface or peer keys as bytes. This type is +/// deserialized from a base64 encoded string. It can also be serialized back +/// into an encoded string. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Key(pub [u8; KEY_SIZE]); + +impl FromStr for Key { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut key_bytes = [0u8; KEY_SIZE]; + let bytes_decoded = STANDARD.decode_slice(s, &mut key_bytes)?; + + if bytes_decoded != KEY_SIZE { + Err(base64::DecodeError::InvalidLength)?; + } + + Ok(Self(key_bytes)) + } +} + +impl fmt::Display for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", Base64Display::new(&self.0, &STANDARD)) + } +} + +impl<'de> Deserialize<'de> for Key { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + +impl Serialize for Key { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(&self) + } +} diff --git a/libs/common/src/session.rs b/libs/common/src/session.rs new file mode 100644 index 0000000..975d368 --- /dev/null +++ b/libs/common/src/session.rs @@ -0,0 +1,241 @@ +use async_trait::async_trait; +use backoff::{backoff::Backoff, ExponentialBackoffBuilder}; +use boringtun::x25519::{PublicKey, StaticSecret}; +use rand_core::OsRng; +use std::{ + marker::PhantomData, + net::{Ipv4Addr, Ipv6Addr}, +}; +use tokio::{ + runtime::Runtime, + sync::mpsc::{Receiver, Sender}, +}; +use url::Url; + +use crate::{control::PhoenixChannel, error_type::ErrorType, messages::Key, Error, Result}; + +// TODO: Not the most tidy trait for a control-plane. +/// Trait that represents a control-plane. +#[async_trait] +pub trait ControlSession { + /// Start control-plane with the given private-key in the background. + async fn start(private_key: StaticSecret) -> Result<(Sender, Receiver)>; + + /// Either "gateway" or "client" used to get the control-plane URL. + fn socket_path() -> &'static str; +} + +// TODO: Currently I'm using Session for both gateway and clients +// however, gateway could use the runtime directly and could make things easier +// so revisit this. +/// A session is the entry-point for connlib, mantains the runtime and the tunnel. +/// +/// A session is created using [Session::connect], then to stop a session we use [Session::disconnect]. +pub struct Session { + runtime: Option, + _phantom: PhantomData<(T, U, V, R, M)>, +} + +/// Resource list that will be displayed to the users. +pub struct ResourceList { + pub resources: Vec, +} + +/// Tunnel addresses to be surfaced to the client apps. +pub struct TunnelAddresses { + /// IPv4 Address. + pub address4: Ipv4Addr, + /// IPv6 Address. + pub address6: Ipv6Addr, +} + +// Evaluate doing this not static +/// Traits that will be used by connlib to callback the client upper layers. +pub trait Callbacks { + /// Called when there's a change in the resource list. + fn on_update_resources(resource_list: ResourceList); + /// Called when the tunnel address is set. + fn on_set_tunnel_adresses(tunnel_addresses: TunnelAddresses); + /// Called when there's an error. + /// + /// # Parameters + /// - `error`: The actual error that happened. + /// - `error_type`: Wether the error should terminate the session or not. + fn on_error(error: &Error, error_type: ErrorType); +} + +macro_rules! fatal_error { + ($result:expr, $c:ty) => { + match $result { + Ok(res) => res, + Err(e) => { + <$c>::on_error(&e, ErrorType::Fatal); + return; + } + } + }; +} + +impl Session +where + T: ControlSession, + U: for<'de> serde::Deserialize<'de> + std::fmt::Debug + Send + 'static, + R: for<'de> serde::Deserialize<'de> + std::fmt::Debug + Send + 'static, + V: serde::Serialize + Send + 'static, + M: From + From + Send + 'static + std::fmt::Debug, +{ + /// Block on waiting for ctrl+c to terminate the runtime. + /// (Used for the gateways). + pub fn wait_for_ctrl_c(&mut self) -> Result<()> { + self.runtime + .as_ref() + .ok_or(Error::NoRuntime)? + .block_on(async { + tokio::signal::ctrl_c().await?; + Ok(()) + }) + } + + /// Starts a session in the background. + /// + /// This will: + /// 1. Create and start a tokio runtime + /// 2. Connect to the control plane to the portal + /// 3. Start the tunnel in the background and forward control plane messages to it. + /// + /// The generic parameter `C` should implement all the handlers and that's how errors will be surfaced. + /// + /// On a fatal error you should call `[Session::disconnect]` and start a new one. + // TODO: token should be something like SecretString but we need to think about FFI compatibiltiy + pub fn connect(portal_url: impl TryInto, token: String) -> Result { + // TODO: We could use tokio::runtime::current() to get the current runtime + // which could work with swif-rust that already runs a runtime. But IDK if that will work + // in all pltaforms, a couple of new threads shouldn't bother none. + // Big question here however is how do we get the result? We could block here await the result and spawn a new task. + // but then platforms should know that this function is blocking. + + let portal_url = portal_url.try_into().map_err(|_| Error::UriError)?; + + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + + runtime.spawn(async move { + let private_key = StaticSecret::random_from_rng(OsRng); + let self_id = uuid::Uuid::new_v4(); + + let connect_url = fatal_error!(get_websocket_path(portal_url, token, T::socket_path(), &Key(PublicKey::from(&private_key).to_bytes()), &self_id.to_string()), C); + + let (sender, mut receiver) = fatal_error!(T::start(private_key).await, C); + + let mut connection = PhoenixChannel::<_, U, R, M>::new(connect_url, move |msg| { + let sender = sender.clone(); + async move { + tracing::trace!("Recieved message: {msg:?}"); + if let Err(e) = sender.send(msg).await { + tracing::warn!("Recieved a message after handler already closed: {e}. Probably message recieved during session clean up."); + } + } + }); + + // Used to send internal messages + let mut internal_sender = connection.sender(); + let topic = T::socket_path().to_string(); + let topic_send = topic.clone(); + + tokio::spawn(async move { + let mut exponential_backoff = ExponentialBackoffBuilder::default().build(); + loop { + let result = connection.start(vec![topic.clone()]).await; + if let Some(t) = exponential_backoff.next_backoff() { + tracing::warn!("Error during connection to the portal, retrying in {} seconds", t.as_secs()); + match result { + Ok(()) => C::on_error(&tokio_tungstenite::tungstenite::Error::ConnectionClosed.into(), ErrorType::Recoverable), + Err(e) => C::on_error(&e, ErrorType::Recoverable) + } + tokio::time::sleep(t).await; + } else { + tracing::error!("Connection to the portal error, check your internet or the status of the portal.\nDisconnecting interface."); + match result { + Ok(()) => C::on_error(&crate::Error::PortalConnectionError(tokio_tungstenite::tungstenite::Error::ConnectionClosed), ErrorType::Fatal), + Err(e) => C::on_error(&e, ErrorType::Fatal) + } + break; + } + } + + }); + + // TODO: Implement Sink for PhoenixEvent (created from a PhoenixSender event + topic) + // that way we can simply do receiver.forward(sender) + tokio::spawn(async move { + while let Some(message) = receiver.recv().await { + if let Err(err) = internal_sender.send(&topic_send, message).await { + tracing::error!("Channel already closed when trying to send message: {err}. Probably trying to send a message during session clean up."); + } + } + }); + }); + + Ok(Self { + runtime: Some(runtime), + _phantom: PhantomData, + }) + } + + /// Cleanup a [Session]. + /// + /// For now this just drops the runtime, which should drop all pending tasks. + /// Further cleanup should be done here. (Otherwise we can just drop [Session]). + pub fn disconnect(&mut self) -> bool { + // 1. Close the websocket connection + // 2. Free the device handle (UNIX) + // 3. Close the file descriptor (UNIX) + // 4. Remove the mapping + + // The way we cleanup the tasks is we drop the runtime + // this means we don't need to keep track of different tasks + // but if any of the tasks never yields this will block forever! + // So always yield and if you spawn a blocking tasks rewrite this. + // Furthermore, we will depend on Drop impls to do the list above so, + // implement them :) + self.runtime = None; + true + } + + /// TODO + pub fn bump_sockets(&self) -> bool { + true + } + + /// TODO + pub fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { + true + } +} + +fn get_websocket_path( + mut url: Url, + secret: String, + mode: &str, + public_key: &Key, + external_id: &str, +) -> Result { + { + let mut paths = url.path_segments_mut().map_err(|_| Error::UriError)?; + paths.pop_if_empty(); + paths.push(mode); + paths.push("websocket"); + } + + { + let mut query_pairs = url.query_pairs_mut(); + query_pairs.clear(); + query_pairs.append_pair("token", &secret); + query_pairs.append_pair("public_key", &public_key.to_string()); + query_pairs.append_pair("external_id", external_id); + query_pairs.append_pair("name_suffix", "todo"); + } + + Ok(url) +} diff --git a/libs/gateway/Cargo.toml b/libs/gateway/Cargo.toml new file mode 100644 index 0000000..32245ad --- /dev/null +++ b/libs/gateway/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "firezone-gateway-connlib" +version = "0.1.0" +edition = "2021" + +[dependencies] +libs-common = { path = "../common" } +async-trait = { version = "0.1", default-features = false } +firezone-tunnel = { path = "../tunnel" } +tokio = { version = "1.27", default-features = false, features = ["sync"] } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +serde = { version = "1.0", default-features = false, features = ["std", "derive"] } + +[dev-dependencies] +serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/libs/gateway/src/control.rs b/libs/gateway/src/control.rs new file mode 100644 index 0000000..06b4e21 --- /dev/null +++ b/libs/gateway/src/control.rs @@ -0,0 +1,159 @@ +use std::{sync::Arc, time::Duration}; + +use firezone_tunnel::{ControlSignal, Tunnel}; +use libs_common::{ + boringtun::x25519::StaticSecret, + error_type::ErrorType::{Fatal, Recoverable}, + messages::ResourceDescription, + Callbacks, ControlSession, Result, +}; +use tokio::sync::mpsc::{channel, Receiver, Sender}; + +use super::messages::{ + ConnectionReady, EgressMessages, IngressMessages, InitGateway, RequestConnection, +}; + +use async_trait::async_trait; + +const INTERNAL_CHANNEL_SIZE: usize = 256; + +pub struct ControlPlane { + tunnel: Arc>, + control_signaler: ControlSignaler, +} + +#[derive(Clone)] +struct ControlSignaler { + internal_sender: Arc>, +} + +#[async_trait] +impl ControlSignal for ControlSignaler { + async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()> { + tracing::warn!("A message to network resource: {resource:?} was discarded, gateways aren't meant to be used as clients."); + Ok(()) + } +} + +impl ControlPlane +where + C: Send + Sync + 'static, +{ + #[tracing::instrument(level = "trace", skip(self))] + async fn start(mut self, mut receiver: Receiver) { + let mut interval = tokio::time::interval(Duration::from_secs(10)); + loop { + tokio::select! { + Some(msg) = receiver.recv() => self.handle_message(msg).await, + _ = interval.tick() => self.stats_event().await, + else => break + } + } + } + + #[tracing::instrument(level = "trace", skip_all)] + async fn init(&mut self, init: InitGateway) { + if let Err(e) = self.tunnel.set_interface(&init.interface).await { + tracing::error!("Couldn't initialize interface: {e}"); + C::on_error(&e, Fatal); + return; + } + + // TODO: Enable masquerading here. + tracing::info!("Firezoned Started!"); + } + + #[tracing::instrument(level = "trace", skip(self))] + fn connection_request(&self, connection_request: RequestConnection) { + let tunnel = Arc::clone(&self.tunnel); + let control_signaler = self.control_signaler.clone(); + tokio::spawn(async move { + match tunnel + .set_peer_connection_request( + connection_request.device.rtc_session_description, + connection_request.device.peer.into(), + connection_request.relays, + connection_request.device.id, + ) + .await + { + Ok(gateway_rtc_sdp) => { + if let Err(err) = control_signaler + .internal_sender + .send(EgressMessages::ConnectionReady(ConnectionReady { + client_id: connection_request.device.id, + gateway_rtc_sdp, + })) + .await + { + tunnel.cleanup_peer_connection(connection_request.device.id); + C::on_error(&err.into(), Recoverable); + } + } + Err(err) => { + tunnel.cleanup_peer_connection(connection_request.device.id); + C::on_error(&err, Recoverable); + } + } + }); + } + + #[tracing::instrument(level = "trace", skip(self))] + fn add_resource(&self, resource: ResourceDescription) { + todo!() + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn handle_message(&mut self, msg: IngressMessages) { + match msg { + IngressMessages::Init(init) => self.init(init).await, + IngressMessages::RequestConnection(connection_request) => { + self.connection_request(connection_request) + } + IngressMessages::AddResource(resource) => self.add_resource(resource), + IngressMessages::RemoveResource(_) => todo!(), + IngressMessages::UpdateResource(_) => todo!(), + } + } + + #[tracing::instrument(level = "trace", skip(self))] + pub(super) async fn stats_event(&mut self) { + tracing::debug!("TODO: STATS EVENT"); + } +} + +#[async_trait] +impl ControlSession for ControlPlane +where + C: Send + Sync + 'static, +{ + #[tracing::instrument(level = "trace", skip(private_key))] + async fn start( + private_key: StaticSecret, + ) -> Result<(Sender, Receiver)> { + // This is kinda hacky, the buffer size is 1 so that we make sure that we + // process one message at a time, blocking if a previous message haven't been processed + // to force queue ordering. + // (couldn't find any other guarantee of the ordering of message) + let (sender, receiver) = channel::(1); + + let (internal_sender, internal_receiver) = channel(INTERNAL_CHANNEL_SIZE); + let internal_sender = Arc::new(internal_sender); + let control_signaler = ControlSignaler { internal_sender }; + let tunnel = Arc::new(Tunnel::<_, C>::new(private_key, control_signaler.clone()).await?); + + let control_plane = ControlPlane { + tunnel, + control_signaler, + }; + + // TODO: We should have some kind of callback from clients to surface errors here + tokio::spawn(async move { control_plane.start(receiver).await }); + + Ok((sender, internal_receiver)) + } + + fn socket_path() -> &'static str { + "gateway" + } +} diff --git a/libs/gateway/src/lib.rs b/libs/gateway/src/lib.rs new file mode 100644 index 0000000..6fa63b7 --- /dev/null +++ b/libs/gateway/src/lib.rs @@ -0,0 +1,21 @@ +//! Main connlib library for gateway. +use control::ControlPlane; +use messages::EgressMessages; +use messages::IngressMessages; + +mod control; +mod messages; + +/// Session type for gateway. +/// +/// For more information see libs_common docs on [Session][libs_common::Session]. +// TODO: Still working on gateway messages +pub type Session = libs_common::Session< + ControlPlane, + IngressMessages, + EgressMessages, + IngressMessages, + IngressMessages, +>; + +pub use libs_common::{error_type::ErrorType, Callbacks, Error, ResourceList, TunnelAddresses}; diff --git a/libs/gateway/src/messages.rs b/libs/gateway/src/messages.rs new file mode 100644 index 0000000..18d1038 --- /dev/null +++ b/libs/gateway/src/messages.rs @@ -0,0 +1,138 @@ +use std::net::IpAddr; + +use firezone_tunnel::RTCSessionDescription; +use libs_common::messages::{Id, Interface, Peer, Relay, ResourceDescription}; +use serde::{Deserialize, Serialize}; + +// TODO: Should this have a resource? +#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)] +pub struct InitGateway { + pub interface: Interface, + pub ipv4_masquerade_enabled: bool, + pub ipv6_masquerade_enabled: bool, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Actor { + pub id: Id, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Device { + pub id: Id, + pub rtc_session_description: RTCSessionDescription, + pub peer: Peer, +} + +// rtc_sdp is ignored from eq since RTCSessionDescription doesn't implement this +// this will probably be changed in the future. +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.peer == other.peer + } +} + +impl Eq for Device {} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct RequestConnection { + pub actor: Actor, + pub relays: Vec, + pub resource: ResourceDescription, + pub device: Device, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub enum Destination { + DnsName(String), + Ip(Vec), +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Metrics { + peers_metrics: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Metric { + pub client_id: Id, + pub resource_id: Id, + pub rx_bytes: u32, + pub tx_bytes: u32, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct RemoveResource { + pub id: Id, +} + +// These messages are the messages that can be recieved +// either by a client or a gateway by the client. +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +// TODO: We will need to re-visit webrtc-rs +#[allow(clippy::large_enum_variant)] +pub enum IngressMessages { + Init(InitGateway), + RequestConnection(RequestConnection), + AddResource(ResourceDescription), + RemoveResource(RemoveResource), + UpdateResource(ResourceDescription), +} + +// These messages can be sent from a gateway +// to a control pane. +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "snake_case", tag = "event", content = "payload")] +// TODO: We will need to re-visit webrtc-rs +#[allow(clippy::large_enum_variant)] +pub enum EgressMessages { + ConnectionReady(ConnectionReady), + Metrics(Metrics), +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ConnectionReady { + pub client_id: Id, + pub gateway_rtc_sdp: RTCSessionDescription, +} + +#[cfg(test)] +mod test { + use libs_common::{control::PhoenixMessage, messages::Interface}; + + use super::{IngressMessages, InitGateway}; + + #[test] + fn init_phoenix_message() { + let m = PhoenixMessage::new( + "gateway:83d28051-324e-48fe-98ed-19690899b3b6", + IngressMessages::Init(InitGateway { + interface: Interface { + ipv4: "100.115.164.78".parse().unwrap(), + ipv6: "fd00:2011:1111::2c:f6ab".parse().unwrap(), + upstream_dns: vec![], + }, + ipv4_masquerade_enabled: true, + ipv6_masquerade_enabled: true, + }), + ); + + let message = r#"{ + "event": "init", + "payload": { + "interface": { + "ipv4": "100.115.164.78", + "ipv6": "fd00:2011:1111::2c:f6ab" + }, + "ipv4_masquerade_enabled": true, + "ipv6_masquerade_enabled": true + }, + "ref": null, + "topic": "gateway:83d28051-324e-48fe-98ed-19690899b3b6" + }"#; + let ingress_message: PhoenixMessage = + serde_json::from_str(message).unwrap(); + assert_eq!(m, ingress_message); + } +} diff --git a/libs/tunnel/Cargo.toml b/libs/tunnel/Cargo.toml new file mode 100644 index 0000000..9c2420b --- /dev/null +++ b/libs/tunnel/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "firezone-tunnel" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-trait = { version = "0.1", default-features = false } +tokio = { version = "1.27", default-features = false, features = ["rt", "rt-multi-thread", "sync"] } +thiserror = { version = "1.0", default-features = false } +rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +serde = { version = "1.0", default-features = false, features = ["derive", "std"] } +futures = { version = "0.3", default-features = false, features = ["std", "async-await", "executor"] } +futures-util = { version = "0.3", default-features = false, features = ["std", "async-await", "async-await-macro"] } +tracing = { version = "0.1", default-features = false, features = ["std", "attributes"] } +parking_lot = { version = "0.12", default-features = false } +bytes = { version = "1.4", default-features = false, features = ["std"] } +itertools = { version = "0.10", default-features = false, features = ["use_std"] } +libs-common = { path = "../common" } +libc = { version = "0.2", default-features = false, features = ["std", "const-extern-fn", "extra_traits"] } +ip_network = { version = "0.4", default-features = false } +ip_network_table = { version = "0.2", default-features = false } + +# TODO: research replacing for https://github.com/algesten/str0m +webrtc = { version = "0.7" } + +# Linux tunnel dependencies +[target.'cfg(target_os = "linux")'.dependencies] +netlink-packet-route = { version = "0.15", default-features = false } +netlink-packet-core = { version = "0.5", default-features = false } +rtnetlink = { version = "0.12", default-features = false, features = ["tokio_socket"] } + +# Android tunnel dependencies +[target.'cfg(target_os = "android")'.dependencies] +android_logger = "0.13" +log = "0.4.14" + +# Windows tunnel dependencies +[target.'cfg(target_os = "windows")'.dependencies] +wintun = "0.2.1" diff --git a/libs/tunnel/src/control_protocol.rs b/libs/tunnel/src/control_protocol.rs new file mode 100644 index 0000000..6cd7b55 --- /dev/null +++ b/libs/tunnel/src/control_protocol.rs @@ -0,0 +1,314 @@ +use std::sync::Arc; + +use libs_common::{ + boringtun::{ + noise::Tunn, + x25519::{PublicKey, StaticSecret}, + }, + error_type::ErrorType::Recoverable, + messages::{Id, Key, Relay, RequestConnection}, + Callbacks, Error, Result, +}; +use rand_core::OsRng; +use webrtc::{ + data_channel::RTCDataChannel, + ice_transport::{ice_credential_type::RTCIceCredentialType, ice_server::RTCIceServer}, + peer_connection::{ + configuration::RTCConfiguration, peer_connection_state::RTCPeerConnectionState, + sdp::session_description::RTCSessionDescription, RTCPeerConnection, + }, +}; + +use crate::{peer::Peer, ControlSignal, PeerConfig, Tunnel}; + +impl Tunnel +where + C: Send + Sync + 'static, + CB: Send + Sync + 'static, +{ + async fn handle_channel_open( + self: &Arc, + data_channel: Arc, + index: u32, + peer_config: PeerConfig, + ) -> Result<()> { + let channel = data_channel.detach().await.expect("TODO"); + let tunn = Tunn::new( + self.private_key.clone(), + peer_config.public_key, + Some(peer_config.preshared_key.to_bytes()), + peer_config.persistent_keepalive, + index, + None, + )?; + + let peer = Arc::new(Peer::from_config( + tunn, + index, + &peer_config, + Arc::clone(&channel), + )); + + { + let mut peers_by_ip = self.peers_by_ip.write(); + for ip in peer_config.ips { + peers_by_ip.insert(ip, Arc::clone(&peer)); + } + } + + self.start_peer_handler(Arc::clone(&peer)); + Ok(()) + } + + #[tracing::instrument(level = "trace", skip(self))] + async fn initialize_peer_request( + self: &Arc, + relays: Vec, + ) -> Result> { + let config = RTCConfiguration { + ice_servers: relays + .into_iter() + .map(|srv| match srv { + Relay::Stun(stun) => RTCIceServer { + urls: vec![stun.uri], + ..Default::default() + }, + Relay::Turn(turn) => RTCIceServer { + urls: vec![turn.uri], + username: turn.username, + credential: turn.password, + // TODO: check what this is used for + credential_type: RTCIceCredentialType::Password, + }, + }) + .collect(), + ..Default::default() + }; + let peer_connection = Arc::new(self.webrtc_api.new_peer_connection(config).await?); + + peer_connection.on_peer_connection_state_change(Box::new(|_s| { + Box::pin(async { + // Respond with failure to control plane and remove peer + }) + })); + + Ok(peer_connection) + } + + #[tracing::instrument(level = "trace", skip(self))] + fn handle_connection_state_update(self: &Arc, state: RTCPeerConnectionState) { + tracing::trace!("Peer Connection State has changed: {state}"); + if state == RTCPeerConnectionState::Failed { + // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. + // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. + // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. + tracing::warn!("Peer Connection has gone to failed exiting"); + } + } + + #[tracing::instrument(level = "trace", skip(self))] + fn set_connection_state_update(self: &Arc, peer_connection: &Arc) { + let tunnel = Arc::clone(self); + peer_connection.on_peer_connection_state_change(Box::new( + move |state: RTCPeerConnectionState| { + let tunnel = Arc::clone(&tunnel); + Box::pin(async move { tunnel.handle_connection_state_update(state) }) + }, + )); + } + + /// Initiate an ice connection request. + /// + /// Given a resource id and a list of relay creates a [RequestConnection] + /// and prepares the tunnel to handle the connection once initiated. + /// + /// # Note + /// This function blocks until all ICE candidates are gathered so it might block for a long time. + /// + /// # Parameters + /// - `resource_id`: Id of the resource we are going to request the connection to. + /// - `relays`: The list of relays used for that connection. + /// + /// # Returns + /// A [RequestConnection] that should be sent to the gateway through the control-plane. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn request_connection( + self: &Arc, + resource_id: Id, + relays: Vec, + ) -> Result { + let peer_connection = self.initialize_peer_request(relays).await?; + self.set_connection_state_update(&peer_connection); + + let data_channel = peer_connection.create_data_channel("data", None).await?; + let d = Arc::clone(&data_channel); + + let tunnel = Arc::clone(self); + + let preshared_key = StaticSecret::random_from_rng(OsRng); + let p_key = preshared_key.clone(); + let resource_description = tunnel + .resources + .read() + .get_by_id(&resource_id) + .expect("TODO") + .clone(); + data_channel.on_open(Box::new(move || { + tracing::trace!("new data channel opened!"); + Box::pin(async move { + let index = tunnel.next_index(); + let Some(gateway_public_key) = tunnel.gateway_public_keys.lock().remove(&resource_id) else { + tunnel.cleanup_connection(resource_id); + tracing::warn!("Opened ICE channel with gateway without ever recieving public key"); + CB::on_error(&Error::ControlProtocolError, Recoverable); + return; + }; + let peer_config = PeerConfig { + persistent_keepalive: None, + public_key: gateway_public_key, + ips: resource_description.ips(), + preshared_key: p_key, + }; + + if let Err(e) = tunnel.handle_channel_open(d, index, peer_config).await { + tracing::error!("Couldn't establish wireguard link after channel was opened: {e}"); + CB::on_error(&e, Recoverable); + tunnel.cleanup_connection(resource_id); + } + tunnel.awaiting_connection.lock().remove(&resource_id); + }) + })); + + let offer = peer_connection.create_offer(None).await?; + let mut gather_complete = peer_connection.gathering_complete_promise().await; + peer_connection.set_local_description(offer).await?; + + // FIXME: timeout here! (but probably don't even bother because we need to implement ICE trickle) + let _ = gather_complete.recv().await; + let local_description = peer_connection + .local_description() + .await + .expect("set_local_description was just called above"); + + self.peer_connections + .lock() + .insert(resource_id, peer_connection); + + Ok(RequestConnection { + resource_id, + device_preshared_key: Key(preshared_key.to_bytes()), + device_rtc_session_description: local_description, + }) + } + + /// Called when a response to [Tunnel::request_connection] is ready. + /// + /// Once this is called if everything goes fine a new tunnel should be started between the 2 peers. + /// + /// # Parameters + /// - `resource_id`: Id of the resource that responded. + /// - `rtc_sdp`: Remote SDP. + /// - `gateway_public_key`: Public key of the gateway that is handling that resource for this connection. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn recieved_offer_response( + self: &Arc, + resource_id: Id, + rtc_sdp: RTCSessionDescription, + gateway_public_key: PublicKey, + ) -> Result<()> { + let peer_connection = self + .peer_connections + .lock() + .get(&resource_id) + .ok_or(Error::UnknownResource)? + .clone(); + self.gateway_public_keys + .lock() + .insert(resource_id, gateway_public_key); + peer_connection.set_remote_description(rtc_sdp).await?; + Ok(()) + } + + /// Removes client's id from connections we are expecting. + pub fn cleanup_peer_connection(self: &Arc, client_id: Id) { + self.peer_connections.lock().remove(&client_id); + } + + /// Accept a connection request from a client. + /// + /// Sets a connection to a remote SDP, creates the local SDP + /// and returns it. + /// + /// # Note + /// + /// This function blocks until it gathers all the ICE candidates + /// so it might block for a long time. + /// + /// # Parameters + /// - `sdp_session`: Remote session description. + /// - `peer`: Configuration for the remote peer. + /// - `relays`: List of relays to use with this connection. + /// - `client_id`: UUID of the remote client. + /// + /// # Returns + /// An [RTCSessionDescription] of the local sdp, with candidates gathered. + pub async fn set_peer_connection_request( + self: &Arc, + sdp_session: RTCSessionDescription, + peer: PeerConfig, + relays: Vec, + client_id: Id, + ) -> Result { + let peer_connection = self.initialize_peer_request(relays).await?; + let index = self.next_index(); + let tunnel = Arc::clone(self); + self.peer_connections + .lock() + .insert(client_id, Arc::clone(&peer_connection)); + + self.set_connection_state_update(&peer_connection); + + peer_connection.on_data_channel(Box::new(move |d| { + tracing::trace!("data channel created!"); + let data_channel = Arc::clone(&d); + let peer = peer.clone(); + let tunnel = Arc::clone(&tunnel); + Box::pin(async move { + d.on_open(Box::new(move || { + tracing::trace!("new data channel opened!"); + Box::pin(async move { + if let Err(e) = tunnel.handle_channel_open(data_channel, index, peer).await + { + CB::on_error(&e, Recoverable); + tracing::error!( + "Couldn't establish wireguard link after opening channel: {e}" + ); + // Note: handle_channel_open can only error out before insert to peers_by_ip + // otherwise we would need to clean that up too! + tunnel.peer_connections.lock().remove(&client_id); + } + }) + })) + }) + })); + + peer_connection.set_remote_description(sdp_session).await?; + + let mut gather_complete = peer_connection.gathering_complete_promise().await; + let answer = peer_connection.create_answer(None).await?; + peer_connection.set_local_description(answer).await?; + let _ = gather_complete.recv().await; + let local_desc = peer_connection + .local_description() + .await + .ok_or(Error::ConnectionEstablishError)?; + + Ok(local_desc) + } + + /// Clean up a connection to a resource. + pub fn cleanup_connection(&self, resource_id: Id) { + self.awaiting_connection.lock().remove(&resource_id); + self.peer_connections.lock().remove(&resource_id); + } +} diff --git a/libs/tunnel/src/device_channel_unix.rs b/libs/tunnel/src/device_channel_unix.rs new file mode 100644 index 0000000..2168a4c --- /dev/null +++ b/libs/tunnel/src/device_channel_unix.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use libs_common::{Error, Result}; +use tokio::io::unix::AsyncFd; + +use crate::tun::{IfaceConfig, IfaceDevice}; + +#[derive(Debug)] +pub(crate) struct DeviceChannel(AsyncFd>); + +impl DeviceChannel { + pub(crate) async fn mtu(&self) -> Result { + self.0.get_ref().mtu().await + } + + pub(crate) async fn read(&self, out: &mut [u8]) -> std::io::Result { + loop { + let mut guard = self.0.readable().await?; + + match guard.try_io(|inner| { + inner.get_ref().read(out).map_err(|err| match err { + Error::IfaceRead(e) => e, + _ => panic!("Unexpected error while trying to read network interface"), + }) + }) { + Ok(result) => break result.map(|e| e.len()), + Err(_would_block) => continue, + } + } + } + + pub(crate) async fn write4(&self, buf: &[u8]) -> std::io::Result { + loop { + let mut guard = self.0.writable().await?; + + // write4 and write6 does the same + match guard.try_io(|inner| match inner.get_ref().write4(buf) { + 0 => Err(std::io::Error::last_os_error()), + i => Ok(i), + }) { + Ok(result) => break result, + Err(_would_block) => continue, + } + } + } + + pub(crate) async fn write6(&self, buf: &[u8]) -> std::io::Result { + loop { + let mut guard = self.0.writable().await?; + + // write4 and write6 does the same + match guard.try_io(|inner| match inner.get_ref().write6(buf) { + 0 => Err(std::io::Error::last_os_error()), + i => Ok(i), + }) { + Ok(result) => break result, + Err(_would_block) => continue, + } + } + } +} + +pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { + let dev = Arc::new(IfaceDevice::new("utun").await?.set_non_blocking()?); + let async_dev = Arc::clone(&dev); + let device_channel = DeviceChannel(AsyncFd::new(async_dev)?); + let iface_config = IfaceConfig(dev); + + Ok((iface_config, device_channel)) +} diff --git a/libs/tunnel/src/device_channel_win.rs b/libs/tunnel/src/device_channel_win.rs new file mode 100644 index 0000000..2edf498 --- /dev/null +++ b/libs/tunnel/src/device_channel_win.rs @@ -0,0 +1,27 @@ +use crate::tun::IfaceConfig; +use libs_common::Result; + +#[derive(Debug)] +pub(crate) struct DeviceChannel; + +impl DeviceChannel { + pub(crate) async fn mtu(&self) -> Result { + todo!() + } + + pub(crate) async fn read(&self, _out: &mut [u8]) -> std::io::Result { + todo!() + } + + pub(crate) async fn write4(&self, _buf: &[u8]) -> std::io::Result { + todo!() + } + + pub(crate) async fn write6(&self, _buf: &[u8]) -> std::io::Result { + todo!() + } +} + +pub(crate) async fn create_iface() -> Result<(IfaceConfig, DeviceChannel)> { + todo!() +} diff --git a/libs/tunnel/src/index.rs b/libs/tunnel/src/index.rs new file mode 100644 index 0000000..f58fcb9 --- /dev/null +++ b/libs/tunnel/src/index.rs @@ -0,0 +1,61 @@ +use rand_core::{OsRng, RngCore}; + +// A basic linear-feedback shift register implemented as xorshift, used to +// distribute peer indexes across the 24-bit address space reserved for peer +// identification. +// The purpose is to obscure the total number of peers using the system and to +// ensure it requires a non-trivial amount of processing power and/or samples +// to guess other peers' indices. Anything more ambitious than this is wasted +// with only 24 bits of space. +pub(crate) struct IndexLfsr { + initial: u32, + lfsr: u32, + mask: u32, +} + +impl IndexLfsr { + /// Generate a random 24-bit nonzero integer + fn random_index() -> u32 { + const LFSR_MAX: u32 = 0xffffff; // 24-bit seed + loop { + let i = OsRng.next_u32() & LFSR_MAX; + if i > 0 { + // LFSR seed must be non-zero + break i; + } + } + } + + /// Generate the next value in the pseudorandom sequence + pub(crate) fn next(&mut self) -> u32 { + // 24-bit polynomial for randomness. This is arbitrarily chosen to + // inject bitflips into the value. + const LFSR_POLY: u32 = 0xd80000; // 24-bit polynomial + debug_assert_ne!(self.lfsr, 0); + let value = self.lfsr - 1; // lfsr will never have value of 0 + self.lfsr = (self.lfsr >> 1) ^ ((0u32.wrapping_sub(self.lfsr & 1u32)) & LFSR_POLY); + assert!(self.lfsr != self.initial, "Too many peers created"); + value ^ self.mask + } +} + +impl Default for IndexLfsr { + fn default() -> Self { + let seed = Self::random_index(); + IndexLfsr { + initial: seed, + lfsr: seed, + mask: Self::random_index(), + } + } +} + +// Checks that a packet has the index we expect +pub(crate) fn check_packet_index(recv_idx: u32, expected_idx: u32) -> bool { + if (recv_idx >> 8) == expected_idx { + true + } else { + tracing::warn!("receiver index doesn't match peer index, something fishy is going on"); + false + } +} diff --git a/libs/tunnel/src/lib.rs b/libs/tunnel/src/lib.rs new file mode 100644 index 0000000..1047244 --- /dev/null +++ b/libs/tunnel/src/lib.rs @@ -0,0 +1,511 @@ +//! Connlib tunnel implementation. +//! +//! This is both the wireguard and ICE implementation that should work in tandem. +//! [Tunnel] is the main entry-point for this crate. +use ip_network::IpNetwork; +use ip_network_table::IpNetworkTable; +use libs_common::{ + boringtun::{ + noise::{ + errors::WireGuardError, handshake::parse_handshake_anon, rate_limiter::RateLimiter, + Packet, Tunn, TunnResult, + }, + x25519::{PublicKey, StaticSecret}, + }, + error_type::ErrorType::{Fatal, Recoverable}, + Callbacks, +}; + +use async_trait::async_trait; +use bytes::Bytes; +use itertools::Itertools; +use parking_lot::{Mutex, RwLock}; +use peer::Peer; +use resource_table::ResourceTable; +use tokio::time::MissedTickBehavior; +use webrtc::{ + api::{ + interceptor_registry::register_default_interceptors, media_engine::MediaEngine, + setting_engine::SettingEngine, APIBuilder, API, + }, + interceptor::registry::Registry, + peer_connection::RTCPeerConnection, +}; + +use std::{ + collections::{HashMap, HashSet}, + marker::PhantomData, + net::IpAddr, + sync::Arc, + time::Duration, +}; + +use libs_common::{ + messages::{Id, Interface as InterfaceConfig, ResourceDescription}, + Result, +}; + +use device_channel::{create_iface, DeviceChannel}; +use tun::IfaceConfig; + +pub use webrtc::peer_connection::sdp::session_description::RTCSessionDescription; + +use index::{check_packet_index, IndexLfsr}; + +mod control_protocol; +mod index; +mod peer; +mod resource_table; + +// TODO: For now all tunnel implementations are the same +// will divide when we start introducing differences. +#[cfg(target_os = "windows")] +#[path = "tun_win.rs"] +mod tun; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +#[path = "tun_darwin.rs"] +mod tun; + +#[cfg(target_os = "linux")] +#[path = "tun_linux.rs"] +mod tun; + +#[cfg(target_os = "android")] +#[path = "tun_android.rs"] +mod tun; + +#[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "linux", + target_os = "android" +))] +#[path = "device_channel_unix.rs"] +mod device_channel; + +#[cfg(target_os = "windows")] +#[path = "device_channel_win.rs"] +mod device_channel; + +const RESET_PACKET_COUNT_INTERVAL: Duration = Duration::from_secs(1); +const REFRESH_PEERS_TIEMRS_INTERVAL: Duration = Duration::from_secs(1); + +// Note: Taken from boringtun +const HANDSHAKE_RATE_LIMIT: u64 = 100; +const MAX_UDP_SIZE: usize = (1 << 16) - 1; + +/// Represent's the tunnel actual peer's config +/// Obtained from libs_common's Peer +#[derive(Clone)] +pub struct PeerConfig { + pub(crate) persistent_keepalive: Option, + pub(crate) public_key: PublicKey, + pub(crate) ips: Vec, + pub(crate) preshared_key: StaticSecret, +} + +impl From for PeerConfig { + fn from(value: libs_common::messages::Peer) -> Self { + Self { + persistent_keepalive: value.persistent_keepalive, + public_key: value.public_key.0.into(), + ips: vec![value.ipv4.into(), value.ipv6.into()], + preshared_key: value.preshared_key.0.into(), + } + } +} + +/// Trait used for out-going signals to control plane that are **required** to be made from inside the tunnel. +/// +/// Generally, we try to return from the functions here rather than using this callback. +#[async_trait] +pub trait ControlSignal { + /// Signals to the control plane an intent to initiate a connection to the given resource. + /// + /// Used when a packet is found to a resource we have no connection stablished but is within the list of resources available for the client. + async fn signal_connection_to(&self, resource: &ResourceDescription) -> Result<()>; +} + +/// Tunnel is a wireguard state machine that uses webrtc's ICE channels instead of UDP sockets +/// to communicate between peers. +pub struct Tunnel { + next_index: Mutex, + // We use a tokio's mutex here since it makes things easier and we only need it + // during init, so the performance hit is neglibile + iface_config: tokio::sync::Mutex, + device_channel: Arc, + rate_limiter: Arc, + private_key: StaticSecret, + public_key: PublicKey, + peers_by_ip: RwLock>>, + peer_connections: Mutex>>, + awaiting_connection: Mutex>, + webrtc_api: API, + resources: RwLock, + control_signaler: C, + gateway_public_keys: Mutex>, + _phantom: PhantomData, +} + +impl Tunnel +where + C: Send + Sync + 'static, + CB: Send + Sync + 'static, +{ + /// Creates a new tunnel. + /// + /// # Parameters + /// - `private_key`: wireguard's private key. + /// - `control_signaler`: this is used to send SDP from the tunnel to the control plane. + #[tracing::instrument(level = "trace", skip(private_key, control_signaler))] + pub async fn new(private_key: StaticSecret, control_signaler: C) -> Result { + let public_key = (&private_key).into(); + let rate_limiter = Arc::new(RateLimiter::new(&public_key, HANDSHAKE_RATE_LIMIT)); + let peers_by_ip = RwLock::new(IpNetworkTable::new()); + let next_index = Default::default(); + let (iface_config, device_channel) = create_iface().await?; + let iface_config = tokio::sync::Mutex::new(iface_config); + let device_channel = Arc::new(device_channel); + let peer_connections = Default::default(); + let resources = Default::default(); + let awaiting_connection = Default::default(); + let gateway_public_keys = Default::default(); + + // ICE + let mut media_engine = MediaEngine::default(); + + // Register default codecs (TODO: We need this?) + media_engine.register_default_codecs()?; + let mut registry = Registry::new(); + registry = register_default_interceptors(registry, &mut media_engine)?; + let mut setting_engine = SettingEngine::default(); + setting_engine.detach_data_channels(); + // TODO: Enable UDPMultiplex (had some problems before) + + let webrtc_api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .with_setting_engine(setting_engine) + .build(); + + Ok(Self { + gateway_public_keys, + rate_limiter, + private_key, + peer_connections, + public_key, + peers_by_ip, + next_index, + webrtc_api, + iface_config, + device_channel, + resources, + awaiting_connection, + control_signaler, + _phantom: PhantomData, + }) + } + + /// Adds a the given resource to the tunnel. + /// + /// Once added, when a packet for the resource is intercepted a new data channel will be created + /// and packets will be wrapped with wireguard and sent through it. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn add_resource(&self, resource_description: ResourceDescription) { + { + let mut iface_config = self.iface_config.lock().await; + for ip in resource_description.ips() { + if let Err(err) = iface_config.add_route(ip).await { + CB::on_error(&err, Fatal); + } + } + } + self.resources.write().insert(resource_description); + } + + /// Sets the interface configuration and starts background tasks. + #[tracing::instrument(level = "trace", skip(self))] + pub async fn set_interface(self: &Arc, config: &InterfaceConfig) -> Result<()> { + { + let mut iface_config = self.iface_config.lock().await; + iface_config + .set_iface_config(config) + .await + .expect("Couldn't initiate interface"); + iface_config + .up() + .await + .expect("Couldn't initiate interface"); + } + + self.start_timers(); + self.start_iface_handler(); + + tracing::trace!("Started background loops"); + + Ok(()) + } + + async fn peer_refresh(peer: &Peer, dst_buf: &mut [u8; MAX_UDP_SIZE]) { + let update_timers_result = peer.update_timers(&mut dst_buf[..]); + + match update_timers_result { + TunnResult::Done => {} + TunnResult::Err(WireGuardError::ConnectionExpired) => { + tracing::error!("Connection expired"); + } + TunnResult::Err(e) => tracing::error!(message = "Timer error", error = ?e), + TunnResult::WriteToNetwork(packet) => peer.send_infallible::(packet).await, + _ => panic!("Unexpected result from update_timers"), + }; + } + + fn start_rate_limiter_refresh_timer(self: &Arc) { + let rate_limiter = self.rate_limiter.clone(); + tokio::spawn(async move { + let mut interval = tokio::time::interval(RESET_PACKET_COUNT_INTERVAL); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + loop { + rate_limiter.reset_count(); + interval.tick().await; + } + }); + } + + fn start_peers_refresh_timer(self: &Arc) { + let tunnel = self.clone(); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(REFRESH_PEERS_TIEMRS_INTERVAL); + interval.set_missed_tick_behavior(MissedTickBehavior::Delay); + let mut dst_buf = [0u8; MAX_UDP_SIZE]; + + loop { + let peers: Vec<_> = tunnel + .peers_by_ip + .read() + .iter() + .map(|p| p.1) + .unique_by(|p| p.index) + .cloned() + .collect(); + + for peer in peers { + Self::peer_refresh(&peer, &mut dst_buf).await; + } + + interval.tick().await; + } + }); + } + + fn start_timers(self: &Arc) { + self.start_rate_limiter_refresh_timer(); + self.start_peers_refresh_timer(); + } + + fn is_wireguard_packet_ok(&self, parsed_packet: &Packet, peer: &Peer) -> bool { + match &parsed_packet { + Packet::HandshakeInit(p) => { + parse_handshake_anon(&self.private_key, &self.public_key, p).is_ok() + } + Packet::HandshakeResponse(p) => check_packet_index(p.receiver_idx, peer.index), + Packet::PacketCookieReply(p) => check_packet_index(p.receiver_idx, peer.index), + Packet::PacketData(p) => check_packet_index(p.receiver_idx, peer.index), + } + } + + fn start_peer_handler(self: &Arc, peer: Arc) { + let tunnel = Arc::clone(self); + tokio::spawn(async move { + let mut src_buf = [0u8; MAX_UDP_SIZE]; + let mut dst_buf = [0u8; MAX_UDP_SIZE]; + // Loop while we have packets on the anonymous connection + while let Ok(size) = peer.channel.read(&mut src_buf[..]).await { + tracing::trace!("read {size} bytes from peer"); + // The rate limiter initially checks mac1 and mac2, and optionally asks to send a cookie + let parsed_packet = match tunnel.rate_limiter.verify_packet( + // TODO: Some(addr.ip()) webrtc doesn't expose easily the underlying data channel remote ip + // so for now we don't use it. but we need it for rate limiter although we probably not need it since the data channel + // will only be established to authenticated peers, so the portal could already prevent being ddos'd + // but maybe in that cased we can drop this rate_limiter all together and just use decapsulate + None, + &src_buf[..size], + &mut dst_buf, + ) { + Ok(packet) => packet, + Err(TunnResult::WriteToNetwork(cookie)) => { + peer.send_infallible::(cookie).await; + continue; + } + Err(_) => continue, + }; + + if !tunnel.is_wireguard_packet_ok(&parsed_packet, &peer) { + continue; + } + + let decapsulate_result = peer.tunnel.lock().decapsulate( + // TODO: See comment above + None, + &src_buf[..size], + &mut dst_buf[..], + ); + + // We found a peer, use it to decapsulate the message+ + let mut flush = false; + match decapsulate_result { + TunnResult::Done => {} + TunnResult::Err(_) => continue, + TunnResult::WriteToNetwork(packet) => { + flush = true; + peer.send_infallible::(packet).await; + } + TunnResult::WriteToTunnelV4(packet, addr) => { + if peer.is_allowed(addr) { + tunnel.write4_device_infallible(packet).await; + } + } + TunnResult::WriteToTunnelV6(packet, addr) => { + if peer.is_allowed(addr) { + tunnel.write6_device_infallible(packet).await; + } + } + }; + + if flush { + // Flush pending queue + while let TunnResult::WriteToNetwork(packet) = { + let res = peer.tunnel.lock().decapsulate(None, &[], &mut dst_buf[..]); + res + } { + peer.send_infallible::(packet).await; + } + } + } + }); + } + + async fn write4_device_infallible(&self, packet: &[u8]) { + if let Err(e) = self.device_channel.write4(packet).await { + CB::on_error(&e.into(), Recoverable); + } + } + + async fn write6_device_infallible(&self, packet: &[u8]) { + if let Err(e) = self.device_channel.write6(packet).await { + CB::on_error(&e.into(), Recoverable); + } + } + + fn get_resource(&self, buff: &[u8]) -> Option { + // TODO: Check if DNS packet, in that case parse and get dns + let addr = Tunn::dst_address(buff)?; + let resources = self.resources.read(); + match addr { + IpAddr::V4(ipv4) => resources.get_by_ip(ipv4).cloned(), + IpAddr::V6(ipv6) => resources.get_by_ip(ipv6).cloned(), + } + } + + fn start_iface_handler(self: &Arc) { + let dev = self.clone(); + tokio::spawn(async move { + loop { + let mut src = [0u8; MAX_UDP_SIZE]; + let mut dst = [0u8; MAX_UDP_SIZE]; + let res = { + // TODO: We should check here if what we read is a whole packet + // there's no docs on tun device on when a whole packet is read, is it \n or another thing? + // found some comments saying that a single read syscall represents a single packet but no docs on that + // See https://stackoverflow.com/questions/18461365/how-to-read-packet-by-packet-from-linux-tun-tap + match dev.device_channel.mtu().await { + Ok(mtu) => match dev.device_channel.read(&mut src[..mtu]).await { + Ok(res) => res, + Err(err) => { + tracing::error!("Couldn't read packet from interface: {err}"); + CB::on_error(&err.into(), Recoverable); + continue; + } + }, + Err(err) => { + tracing::error!("Couldn't obtain iface mtu: {err}"); + CB::on_error(&err, Recoverable); + continue; + } + } + }; + + let dst_addr = match Tunn::dst_address(&src[..res]) { + Some(addr) => addr, + None => continue, + }; + + let (encapsulate_result, channel) = { + let peers_by_ip = dev.peers_by_ip.read(); + match peers_by_ip.longest_match(dst_addr).map(|p| p.1) { + Some(peer) => ( + peer.tunnel.lock().encapsulate(&src[..res], &mut dst[..]), + peer.channel.clone(), + ), + None => { + // We can buffer requests here but will drop them for now and let the upper layer reliability protocol handle this + if let Some(resource) = dev.get_resource(&src[..res]) { + // We have awaiting connection to prevent a race condition where + // create_peer_connection hasn't added the thing to peer_connections + // and we are finding another packet to the same address (otherwise we would just use peer_connections here) + let mut awaiting_connection = dev.awaiting_connection.lock(); + let id = resource.id(); + if !awaiting_connection.contains(&id) { + tracing::trace!("Found new intent to send packets to resource with resource-ip: {dst_addr}, initializing connection..."); + + awaiting_connection.insert(id); + let dev = Arc::clone(&dev); + + tokio::spawn(async move { + if let Err(e) = dev + .control_signaler + .signal_connection_to(&resource) + .await + { + // Not a deadlock because this is a different task + dev.awaiting_connection.lock().remove(&id); + tracing::error!("couldn't start protocol for new connection to resource: {e}"); + CB::on_error(&e, Recoverable); + } + }); + } + } + continue; + } + } + }; + + match encapsulate_result { + TunnResult::Done => { + tracing::trace!( + "tunnel for resource corresponding to {dst_addr} was finalized" + ); + } + TunnResult::Err(e) => { + tracing::error!(message = "Encapsulate error for resource corresponding to {dst_addr}", error = ?e); + CB::on_error(&e.into(), Recoverable); + } + TunnResult::WriteToNetwork(packet) => { + tracing::trace!("writing iface packet to peer: {dst_addr}"); + if let Err(e) = channel.write(&Bytes::copy_from_slice(packet)).await { + tracing::error!("Couldn't write packet to channel: {e}"); + CB::on_error(&e.into(), Recoverable); + } + } + _ => panic!("Unexpected result from encapsulate"), + }; + } + }); + } + + fn next_index(&self) -> u32 { + self.next_index.lock().next() + } +} diff --git a/libs/tunnel/src/peer.rs b/libs/tunnel/src/peer.rs new file mode 100644 index 0000000..00b25e9 --- /dev/null +++ b/libs/tunnel/src/peer.rs @@ -0,0 +1,65 @@ +use std::{net::IpAddr, sync::Arc}; + +use bytes::Bytes; +use ip_network::IpNetwork; +use ip_network_table::IpNetworkTable; +use libs_common::{ + boringtun::noise::{Tunn, TunnResult}, + error_type::ErrorType, + Callbacks, +}; +use parking_lot::Mutex; +use webrtc::data::data_channel::DataChannel; + +use super::PeerConfig; + +pub(crate) struct Peer { + pub tunnel: Mutex, + pub index: u32, + pub allowed_ips: IpNetworkTable<()>, + pub channel: Arc, +} + +impl Peer { + pub(crate) async fn send_infallible(&self, data: &[u8]) { + if let Err(e) = self.channel.write(&Bytes::copy_from_slice(data)).await { + tracing::error!("Couldn't send packet to connected peer: {e}"); + CB::on_error(&e.into(), ErrorType::Recoverable); + } + } + + pub(crate) fn from_config( + tunnel: Tunn, + index: u32, + config: &PeerConfig, + channel: Arc, + ) -> Self { + Self::new(Mutex::new(tunnel), index, config.ips.clone(), channel) + } + + pub(crate) fn new( + tunnel: Mutex, + index: u32, + ips: Vec, + channel: Arc, + ) -> Peer { + let mut allowed_ips = IpNetworkTable::new(); + for ip in ips { + allowed_ips.insert(ip, ()); + } + Peer { + tunnel, + index, + allowed_ips, + channel, + } + } + + pub(crate) fn update_timers<'a>(&self, dst: &'a mut [u8]) -> TunnResult<'a> { + self.tunnel.lock().update_timers(dst) + } + + pub(crate) fn is_allowed(&self, addr: impl Into) -> bool { + self.allowed_ips.longest_match(addr).is_some() + } +} diff --git a/libs/tunnel/src/resource_table.rs b/libs/tunnel/src/resource_table.rs new file mode 100644 index 0000000..72e5818 --- /dev/null +++ b/libs/tunnel/src/resource_table.rs @@ -0,0 +1,151 @@ +//! A resource table is a custom type that allows us to store a resource under an id and possibly multiple ips or even network ranges +use std::{collections::HashMap, net::IpAddr, ptr::NonNull}; + +use ip_network_table::IpNetworkTable; +use libs_common::messages::{Id, ResourceDescription}; + +// Oh boy... here we go +/// The resource table type +/// +/// This is specifically crafted for our use case, so the API is particularly made for us and not generic +pub(crate) struct ResourceTable { + id_table: HashMap, + network_table: IpNetworkTable>, + dns_name: HashMap>, +} + +// SAFETY: We actually hold a `Vec` internally that the poitners points to +unsafe impl Send for ResourceTable {} +// SAFETY: we don't allow interior mutability of the pointers we hold, in fact we don't allow ANY mutability! +// (this is part of the reason why the API is so limiting, it is easier to reason about. +unsafe impl Sync for ResourceTable {} + +impl Default for ResourceTable { + fn default() -> ResourceTable { + ResourceTable::new() + } +} + +impl ResourceTable { + /// Creates a new `ResourceTable` + pub fn new() -> ResourceTable { + ResourceTable { + network_table: IpNetworkTable::new(), + id_table: HashMap::new(), + dns_name: HashMap::new(), + } + } + + /// Gets the resource by ip + pub fn get_by_ip(&self, ip: impl Into) -> Option<&ResourceDescription> { + // SAFETY: if we found the pointer, due to our internal consistency rules it is in the id_table + self.network_table + .longest_match(ip) + .map(|m| unsafe { m.1.as_ref() }) + } + + /// Gets the resource by id + pub fn get_by_id(&self, id: &Id) -> Option<&ResourceDescription> { + self.id_table.get(id) + } + + // SAFETY: resource_description must still be in storage since we are going to reference it. + unsafe fn remove_resource(&mut self, resource_description: NonNull) { + let id = { + let res = resource_description.as_ref(); + match res { + ResourceDescription::Dns(r) => { + self.dns_name.remove(&r.address); + self.network_table.remove(r.ipv4); + self.network_table.remove(r.ipv6); + r.id + } + ResourceDescription::Cidr(r) => { + self.network_table.remove(r.address); + r.id + } + } + }; + self.id_table.remove(&id); + } + + fn cleaup_resource(&mut self, resource_description: &ResourceDescription) { + match resource_description { + ResourceDescription::Dns(r) => { + if let Some(res) = self.id_table.get(&r.id) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res.into()); + } + // Don't use res after here + } + + if let Some(res) = self.dns_name.remove(&r.address) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + // Don't use res after here + } + + if let Some(res) = self.network_table.remove(r.ipv4) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + } + + if let Some(res) = self.network_table.remove(r.ipv6) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + } + } + ResourceDescription::Cidr(r) => { + if let Some(res) = self.id_table.get(&r.id) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res.into()); + } + // Don't use res after here + } + + if let Some(res) = self.network_table.remove(r.address) { + // SAFETY: We are consistent that if the item exists on any of the containers it still exists in the storage + unsafe { + self.remove_resource(res); + } + } + } + } + } + + // For soundness it's very important that this API only takes a resource_description + // doing this, we can assume that when removing a resource from the id table we have all the info + // about all the o + /// Inserts a new resource_description + /// + /// If the id was used previously the old value will be deleted. + /// Same goes if any of the ip matches exactly an old ip or dns name. + /// This means that a match in IP or dns name will discard all old values. + /// + /// This is done so that we don't have dangling values. + pub fn insert(&mut self, resource_description: ResourceDescription) { + self.cleaup_resource(&resource_description); + let id = resource_description.id(); + self.id_table.insert(id, resource_description); + // we just inserted it we can unwrap + let res = self.id_table.get(&id).unwrap(); + match res { + ResourceDescription::Dns(r) => { + self.network_table.insert(r.ipv4, res.into()); + self.network_table.insert(r.ipv6, res.into()); + self.dns_name.insert(r.address.clone(), res.into()); + } + ResourceDescription::Cidr(r) => { + self.network_table.insert(r.address, res.into()); + } + } + } +} diff --git a/libs/tunnel/src/tun_android.rs b/libs/tunnel/src/tun_android.rs new file mode 100644 index 0000000..d47d94a --- /dev/null +++ b/libs/tunnel/src/tun_android.rs @@ -0,0 +1,20 @@ +use super::InterfaceConfig; +use libs_common::Result; + +#[derive(Debug)] +pub(crate) struct IfaceConfig(pub(crate) Arc); + +#[derive(Debug)] +pub(crate) struct IfaceDevice; + +impl IfaceConfig { + // It's easier to not make these functions async, setting these should not block the thread for too long + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, _config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub fn up(&mut self) -> Result<()> { + todo!() + } +} diff --git a/libs/tunnel/src/tun_darwin.rs b/libs/tunnel/src/tun_darwin.rs new file mode 100644 index 0000000..21e4ecc --- /dev/null +++ b/libs/tunnel/src/tun_darwin.rs @@ -0,0 +1,279 @@ +use libc::{ + close, connect, ctl_info, fcntl, getsockopt, ioctl, iovec, msghdr, recvmsg, sendmsg, sockaddr, + sockaddr_ctl, sockaddr_in, socket, socklen_t, AF_INET, AF_INET6, AF_SYSTEM, AF_SYS_CONTROL, + CTLIOCGINFO, F_GETFL, F_SETFL, IF_NAMESIZE, IPPROTO_IP, O_NONBLOCK, PF_SYSTEM, SOCK_DGRAM, + SOCK_STREAM, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, +}; +use libs_common::{Error, Result}; +use std::{ + ffi::{c_int, c_short, c_uchar}, + io, + mem::{size_of, size_of_val}, + os::fd::{AsRawFd, RawFd}, + sync::Arc, +}; + +use super::InterfaceConfig; + +const CTRL_NAME: &[u8] = b"com.apple.net.utun_control"; +const SIOCGIFMTU: u64 = 0x0000_0000_c020_6933; + +#[derive(Debug)] +pub(crate) struct IfaceConfig(pub(crate) Arc); + +#[derive(Debug)] +pub(crate) struct IfaceDevice { + fd: RawFd, +} + +impl AsRawFd for IfaceDevice { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl Drop for IfaceDevice { + fn drop(&mut self) { + unsafe { close(self.fd) }; + } +} +// For some reason this is not available in libc for darwin :c +#[allow(non_camel_case_types)] +#[repr(C)] +pub struct ifreq { + ifr_name: [c_uchar; IF_NAMESIZE], + ifr_ifru: IfrIfru, +} + +#[repr(C)] +union IfrIfru { + ifru_addr: sockaddr, + ifru_addr_v4: sockaddr_in, + ifru_addr_v6: sockaddr_in, + ifru_dstaddr: sockaddr, + ifru_broadaddr: sockaddr, + ifru_flags: c_short, + ifru_metric: c_int, + ifru_mtu: c_int, + ifru_phys: c_int, + ifru_media: c_int, + ifru_intval: c_int, + ifru_wake_flags: u32, + ifru_route_refcnt: u32, + ifru_cap: [c_int; 2], + ifru_functional_type: u32, +} + +// On Darwin tunnel can only be named utunXXX +pub fn parse_utun_name(name: &str) -> Result { + if !name.starts_with("utun") { + return Err(Error::InvalidTunnelName); + } + + match name.get(4..) { + None | Some("") => { + // The name is simply "utun" + Ok(0) + } + Some(idx) => { + // Everything past utun should represent an integer index + idx.parse::() + .map_err(|_| Error::InvalidTunnelName) + .map(|x| x + 1) + } + } +} + +impl IfaceDevice { + fn write(&self, src: &[u8], af: u8) -> usize { + let mut hdr = [0, 0, 0, af]; + let mut iov = [ + iovec { + iov_base: hdr.as_mut_ptr() as _, + iov_len: hdr.len(), + }, + iovec { + iov_base: src.as_ptr() as _, + iov_len: src.len(), + }, + ]; + + let msg_hdr = msghdr { + msg_name: std::ptr::null_mut(), + msg_namelen: 0, + msg_iov: &mut iov[0], + msg_iovlen: iov.len() as _, + msg_control: std::ptr::null_mut(), + msg_controllen: 0, + msg_flags: 0, + }; + + match unsafe { sendmsg(self.fd, &msg_hdr, 0) } { + -1 => 0, + n => n as usize, + } + } + + pub fn new(name: &str) -> Result { + let idx = parse_utun_name(name)?; + + let fd = match unsafe { socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL) } { + -1 => return Err(get_last_error()), + fd => fd, + }; + + let mut info = ctl_info { + ctl_id: 0, + ctl_name: [0; 96], + }; + info.ctl_name[..CTRL_NAME.len()] + // SAFETY: We only care about maintaining the same byte value not the same value, + // meaning that the slice &[u8] here is just a blob of bytes for us, we need this conversion + // just because `c_char` is i8 (for some reason). + // One thing I don't like about this is that `ctl_name` is actually a nul-terminated string, + // which we are only getting because `CTRL_NAME` is less than 96 bytes long and we are 0-value + // initializing the array we should be using a CStr to be explicit... but this is slightly easier. + .copy_from_slice(unsafe { &*(CTRL_NAME as *const [u8] as *const [i8]) }); + + if unsafe { ioctl(fd, CTLIOCGINFO, &mut info as *mut ctl_info) } < 0 { + unsafe { close(fd) }; + return Err(get_last_error()); + } + + let addr = sockaddr_ctl { + sc_len: size_of::() as u8, + sc_family: AF_SYSTEM as u8, + ss_sysaddr: AF_SYS_CONTROL as u16, + sc_id: info.ctl_id, + sc_unit: idx, + sc_reserved: Default::default(), + }; + + if unsafe { + connect( + fd, + &addr as *const sockaddr_ctl as _, + size_of_val(&addr) as _, + ) + } < 0 + { + unsafe { close(fd) }; + return Err(get_last_error()); + } + + Ok(Self { fd }) + } + + pub fn set_non_blocking(self) -> Result { + match unsafe { fcntl(self.fd, F_GETFL) } { + -1 => Err(get_last_error()), + flags => match unsafe { fcntl(self.fd, F_SETFL, flags | O_NONBLOCK) } { + -1 => Err(get_last_error()), + _ => Ok(self), + }, + } + } + + pub fn name(&self) -> Result { + let mut tunnel_name = [0u8; 256]; + let mut tunnel_name_len = tunnel_name.len() as socklen_t; + if unsafe { + getsockopt( + self.fd, + SYSPROTO_CONTROL, + UTUN_OPT_IFNAME, + tunnel_name.as_mut_ptr() as _, + &mut tunnel_name_len, + ) + } < 0 + || tunnel_name_len == 0 + { + return Err(get_last_error()); + } + + Ok(String::from_utf8_lossy(&tunnel_name[..(tunnel_name_len - 1) as usize]).to_string()) + } + + /// Get the current MTU value + pub fn mtu(&self) -> Result { + let fd = match unsafe { socket(AF_INET, SOCK_STREAM, IPPROTO_IP) } { + -1 => return Err(get_last_error()), + fd => fd, + }; + + let name = self.name()?; + let iface_name: &[u8] = name.as_ref(); + let mut ifr = ifreq { + ifr_name: [0; IF_NAMESIZE], + ifr_ifru: IfrIfru { ifru_mtu: 0 }, + }; + + ifr.ifr_name[..iface_name.len()].copy_from_slice(iface_name); + + if unsafe { ioctl(fd, SIOCGIFMTU, &ifr) } < 0 { + return Err(get_last_error()); + } + + unsafe { close(fd) }; + + Ok(unsafe { ifr.ifr_ifru.ifru_mtu } as _) + } + + pub fn write4(&self, src: &[u8]) -> usize { + self.write(src, AF_INET as u8) + } + + pub fn write6(&self, src: &[u8]) -> usize { + self.write(src, AF_INET6 as u8) + } + + pub fn read<'a>(&self, dst: &'a mut [u8]) -> Result<&'a mut [u8]> { + let mut hdr = [0u8; 4]; + + let mut iov = [ + iovec { + iov_base: hdr.as_mut_ptr() as _, + iov_len: hdr.len(), + }, + iovec { + iov_base: dst.as_mut_ptr() as _, + iov_len: dst.len(), + }, + ]; + + let mut msg_hdr = msghdr { + msg_name: std::ptr::null_mut(), + msg_namelen: 0, + msg_iov: &mut iov[0], + msg_iovlen: iov.len() as _, + msg_control: std::ptr::null_mut(), + msg_controllen: 0, + msg_flags: 0, + }; + + match unsafe { recvmsg(self.fd, &mut msg_hdr, 0) } { + -1 => Err(Error::IfaceRead(io::Error::last_os_error())), + 0..=4 => Ok(&mut dst[..0]), + n => Ok(&mut dst[..(n - 4) as usize]), + } + } +} + +// So, these functions take a mutable &self, this is not neccesary in theory but it's correct! +impl IfaceConfig { + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + // TODO + + Ok(()) + } + + pub fn up(&mut self) -> Result<()> { + // TODO + Ok(()) + } +} + +fn get_last_error() -> Error { + Error::Io(io::Error::last_os_error()) +} diff --git a/libs/tunnel/src/tun_linux.rs b/libs/tunnel/src/tun_linux.rs new file mode 100644 index 0000000..65e6aad --- /dev/null +++ b/libs/tunnel/src/tun_linux.rs @@ -0,0 +1,271 @@ +use futures::TryStreamExt; +use ip_network::IpNetwork; +use libc::{ + close, fcntl, ioctl, open, read, sockaddr, sockaddr_in, write, F_GETFL, F_SETFL, + IFF_MULTI_QUEUE, IFF_NO_PI, IFF_TUN, IFNAMSIZ, O_NONBLOCK, O_RDWR, +}; +use libs_common::{Error, Result}; +use netlink_packet_route::rtnl::link::nlas::Nla; +use rtnetlink::{new_connection, Handle}; +use std::{ + ffi::{c_int, c_short, c_uchar}, + io, + os::fd::{AsRawFd, RawFd}, + sync::Arc, +}; + +use super::InterfaceConfig; + +#[derive(Debug)] +pub(crate) struct IfaceConfig(pub(crate) Arc); + +const TUNSETIFF: u64 = 0x4004_54ca; +const TUN_FILE: &[u8] = b"/dev/net/tun\0"; +const RT_SCOPE_LINK: u8 = 253; +const RT_PROT_UNSPEC: u8 = 0; + +#[repr(C)] +union IfrIfru { + ifru_addr: sockaddr, + ifru_addr_v4: sockaddr_in, + ifru_addr_v6: sockaddr_in, + ifru_dstaddr: sockaddr, + ifru_broadaddr: sockaddr, + ifru_flags: c_short, + ifru_metric: c_int, + ifru_mtu: c_int, + ifru_phys: c_int, + ifru_media: c_int, + ifru_intval: c_int, + ifru_wake_flags: u32, + ifru_route_refcnt: u32, + ifru_cap: [c_int; 2], + ifru_functional_type: u32, +} + +#[repr(C)] +pub struct ifreq { + ifr_name: [c_uchar; IFNAMSIZ], + ifr_ifru: IfrIfru, +} + +#[derive(Debug)] +pub struct IfaceDevice { + fd: RawFd, + handle: Handle, + connection: tokio::task::JoinHandle<()>, + interface_index: u32, +} + +impl Drop for IfaceDevice { + fn drop(&mut self) { + self.connection.abort(); + unsafe { close(self.fd) }; + } +} + +impl AsRawFd for IfaceDevice { + fn as_raw_fd(&self) -> RawFd { + self.fd + } +} + +impl IfaceDevice { + fn write(&self, buf: &[u8]) -> usize { + match unsafe { write(self.fd, buf.as_ptr() as _, buf.len() as _) } { + -1 => 0, + n => n as usize, + } + } + + pub async fn new(name: &str) -> Result { + let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } { + -1 => return Err(get_last_error()), + fd => fd, + }; + + let iface_name = name.as_bytes(); + let mut ifr = ifreq { + ifr_name: [0; IFNAMSIZ], + ifr_ifru: IfrIfru { + ifru_flags: (IFF_TUN | IFF_NO_PI | IFF_MULTI_QUEUE) as _, + }, + }; + + if iface_name.len() >= ifr.ifr_name.len() { + return Err(Error::InvalidTunnelName); + } + + ifr.ifr_name[..iface_name.len()].copy_from_slice(iface_name); + + if unsafe { ioctl(fd, TUNSETIFF as _, &ifr) } < 0 { + return Err(get_last_error()); + } + + let name = name.to_string(); + + let (connection, handle, _) = new_connection()?; + let join_handle = tokio::spawn(connection); + let interface_index = handle + .link() + .get() + .match_name(name.clone()) + .execute() + .try_next() + .await? + .ok_or(Error::NoIface)? + .header + .index; + + Ok(Self { + fd, + handle, + connection: join_handle, + interface_index, + }) + } + + pub fn set_non_blocking(self) -> Result { + match unsafe { fcntl(self.fd, F_GETFL) } { + -1 => Err(get_last_error()), + flags => match unsafe { fcntl(self.fd, F_SETFL, flags | O_NONBLOCK) } { + -1 => Err(get_last_error()), + _ => Ok(self), + }, + } + } + + /// Get the current MTU value + pub async fn mtu(&self) -> Result { + while let Ok(Some(msg)) = self + .handle + .link() + .get() + .match_index(self.interface_index) + .execute() + .try_next() + .await + { + for nla in msg.nlas { + if let Nla::Mtu(mtu) = nla { + return Ok(mtu as usize); + } + } + } + + Err(Error::NoMtu) + } + + pub fn write4(&self, src: &[u8]) -> usize { + self.write(src) + } + + pub fn write6(&self, src: &[u8]) -> usize { + self.write(src) + } + + pub fn read<'a>(&self, dst: &'a mut [u8]) -> Result<&'a mut [u8]> { + match unsafe { read(self.fd, dst.as_mut_ptr() as _, dst.len()) } { + -1 => Err(Error::IfaceRead(io::Error::last_os_error())), + n => Ok(&mut dst[..n as usize]), + } + } +} + +fn get_last_error() -> Error { + Error::Io(io::Error::last_os_error()) +} + +impl IfaceConfig { + pub async fn add_route(&mut self, route: IpNetwork) -> Result<()> { + let req = self + .0 + .handle + .route() + .add() + .output_interface(self.0.interface_index) + .protocol(RT_PROT_UNSPEC) + .scope(RT_SCOPE_LINK); + match route { + IpNetwork::V4(ipnet) => { + req.v4() + .source_prefix(ipnet.network_address(), ipnet.netmask()) + .destination_prefix(ipnet.network_address(), ipnet.netmask()) + .execute() + .await? + } + IpNetwork::V6(ipnet) => { + req.v6() + .source_prefix(ipnet.network_address(), ipnet.netmask()) + .destination_prefix(ipnet.network_address(), ipnet.netmask()) + .execute() + .await? + } + } + /* + TODO: This works for ignoring the error but the route isn't added afterwards + let's try removing all routes on init for the given interface I think that will work. + match res { + Ok(_) + | Err(rtnetlink::Error::NetlinkError(netlink_packet_core::error::ErrorMessage { + code: NETLINK_ERROR_FILE_EXISTS, + .. + })) => Ok(()), + + Err(err) => Err(err.into()), + } + */ + + Ok(()) + } + #[tracing::instrument(level = "trace", skip(self))] + pub async fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + let ips = self + .0 + .handle + .address() + .get() + .set_link_index_filter(self.0.interface_index) + .execute(); + + ips.try_for_each(|ip| self.0.handle.address().del(ip).execute()) + .await?; + + self.0 + .handle + .address() + .add(self.0.interface_index, config.ipv4.into(), 32) + .execute() + .await?; + + self.0 + .handle + .address() + .add(self.0.interface_index, config.ipv6.into(), 128) + .execute() + .await?; + + //TODO! + /* + let name: String = self.name.clone().try_into()?; + for dns in &config.dns { + //resolvconf::set_dns(&name, dns).await?; + } + */ + + //nftables::enable_masquerade((config.ipv4_masquerade, config.ipv6_masquerade)).await?; + + Ok(()) + } + + pub async fn up(&mut self) -> Result<()> { + self.0 + .handle + .link() + .set(self.0.interface_index) + .up() + .execute() + .await?; + Ok(()) + } +} diff --git a/libs/tunnel/src/tun_win.rs b/libs/tunnel/src/tun_win.rs new file mode 100644 index 0000000..66fcf4e --- /dev/null +++ b/libs/tunnel/src/tun_win.rs @@ -0,0 +1,17 @@ +use super::InterfaceConfig; +use libs_common::Result; + +#[derive(Debug)] +pub(crate) struct IfaceConfig; + +impl IfaceConfig { + // It's easier to not make these functions async, setting these should not block the thread for too long + #[tracing::instrument(level = "trace", skip(self))] + pub fn set_iface_config(&mut self, _config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub fn up(&mut self) -> Result<()> { + todo!() + } +} diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 0000000..5326335 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0" } +proc-macro2 = { version = "1.0" } +quote = { version = "1.0" } diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 0000000..d765549 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,108 @@ +#![recursion_limit = "128"] + +extern crate proc_macro; +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{Data, DeriveInput, Fields}; + +/// Macro that generates a new enum with only the discriminants of another enum within a module that implements swift_bridge. +/// +/// This is a workaround to create an error type compatible with swift that can be converted from the original error type. +/// it implements `From` so the idea is that you can call a swift ffi function `handle_error(err.into());` +/// +/// This makes a lot of assumption about the types it's being implemented on since we're controling the type it is not meant +/// to be a public macro. (However be careful if you reuse it somewhere else! this is based in strum's EnumDiscrminant so you can +/// check there for an actual propper implementation). +/// +/// IMPORTANT!: You need to include swift_bridge::bridge for macos and ios target so this doesn't error out. +#[proc_macro_derive(SwiftEnum)] +pub fn swift_enum(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse_macro_input!(input as DeriveInput); + + let toks = swift_enum_inner(&ast).unwrap_or_else(|err| err.to_compile_error()); + toks.into() +} + +fn swift_enum_inner(ast: &DeriveInput) -> syn::Result { + let name = &ast.ident; + let vis = &ast.vis; + + let variants = match &ast.data { + Data::Enum(v) => &v.variants, + _ => { + return Err(syn::Error::new( + Span::call_site(), + "This macro only support enums.", + )) + } + }; + + let discriminants: Vec<_> = variants + .into_iter() + .map(|v| { + let ident = &v.ident; + quote! {#ident} + }) + .collect(); + + let enum_name = syn::Ident::new(&format!("Swift{}", name), Span::call_site()); + let mod_name = syn::Ident::new("swift_ffi", Span::call_site()); + + let arms = variants + .iter() + .map(|variant| { + let ident = &variant.ident; + let params = match &variant.fields { + Fields::Unit => quote! {}, + Fields::Unnamed(_fields) => { + quote! { (..) } + } + Fields::Named(_fields) => { + quote! { { .. } } + } + }; + + quote! { #name::#ident #params => #mod_name::#enum_name::#ident } + }) + .collect::>(); + + let from_fn_body = quote! { match val { #(#arms),* } }; + + let impl_from_ref = { + quote! { + impl<'a> ::core::convert::From<&'a #name> for #mod_name::#enum_name { + fn from(val: &'a #name) -> Self { + #from_fn_body + } + } + } + }; + + let impl_from = { + quote! { + impl ::core::convert::From<#name> for #mod_name::#enum_name { + fn from(val: #name) -> Self { + #from_fn_body + } + } + } + }; + + // If we wanted to expose this function we should have another crate that actually also includes + // swift_bridge. but since we are only using this inside our crates we can just make sure we include it. + Ok(quote! { + #[cfg_attr(any(target_os = "macos", target_os = "ios"), swift_bridge::bridge)] + #vis mod #mod_name { + pub enum #enum_name { + #(#discriminants),* + } + + } + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #impl_from_ref + + #[cfg(any(target_os = "macos", target_os = "ios"))] + #impl_from + }) +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 1ddbcb2..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,33 +0,0 @@ -use platform::tunnel::Tunnel; - -mod platform; - -#[allow(dead_code)] -pub struct Session { - tunnel: Tunnel, -} - -impl Session { - pub fn connect(_portal_url: String, _token: String) -> Result { - match Tunnel::new() { - Ok(tunnel) => Ok(Session { tunnel }), - Err(e) => Err(e), - } - } - - pub fn disconnect(&self) -> bool { - // 1. Close the websocket connection - // 2. Free the device handle (UNIX) - // 3. Close the file descriptor (UNIX) - // 4. Remove the mapping - true - } - - pub fn bump_sockets(&self) -> bool { - true - } - - pub fn disable_some_roaming_for_broken_mobile_semantics(&self) -> bool { - true - } -} diff --git a/src/platform.rs b/src/platform.rs deleted file mode 100644 index 43547b8..0000000 --- a/src/platform.rs +++ /dev/null @@ -1,19 +0,0 @@ -// Tunnel management for Linux -#[cfg(target_os = "linux")] -#[path = "platform/linux.rs"] -pub mod tunnel; - -// Tunnel management for macOS and iOS -#[cfg(any(target_os = "ios", target_os = "macos"))] -#[path = "platform/apple.rs"] -pub mod tunnel; - -// Tunnel management for Windows -#[cfg(target_os = "windows")] -#[path = "platform/windows.rs"] -pub mod tunnel; - -// Tunnel management for Android -#[cfg(target_os = "android")] -#[path = "platform/android.rs"] -pub mod tunnel; diff --git a/src/platform/android.rs b/src/platform/android.rs deleted file mode 100644 index a20b00b..0000000 --- a/src/platform/android.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[allow(dead_code)] -pub struct Tunnel { - fd: i32, -} - -impl Tunnel { - pub fn new() -> Result { - // On android, the file descriptor is passed from the VPN service. We'll need to accept it - // or set it later. - Ok(Self { fd: -1 }) - } -} diff --git a/src/platform/apple.rs b/src/platform/apple.rs deleted file mode 100644 index 4bb89c4..0000000 --- a/src/platform/apple.rs +++ /dev/null @@ -1,22 +0,0 @@ -use boringtun::device::tun::TunSocket; - -#[allow(dead_code)] -pub struct Tunnel { - socket: TunSocket, -} - -impl Tunnel { - pub fn new() -> Result { - // Loop through all utun interfaces and try to find an unused one - for index in 0..255 { - let utun_name = format!("utun{index}"); - if let Ok(socket) = TunSocket::new(utun_name.as_str()) { - return Ok(Tunnel { socket }); - } - } - Err(std::io::Error::new( - std::io::ErrorKind::Other, - "No more utun interfaces available", - )) - } -} diff --git a/src/platform/linux.rs b/src/platform/linux.rs deleted file mode 100644 index 6d29680..0000000 --- a/src/platform/linux.rs +++ /dev/null @@ -1,20 +0,0 @@ -use boringtun::device::tun::TunSocket; - -const TUN_NAME: &str = "wg-firezone"; - -#[allow(dead_code)] -pub struct Tunnel { - socket: TunSocket, -} - -impl Tunnel { - pub fn new() -> Result { - match TunSocket::new(TUN_NAME) { - Ok(socket) => Ok(Self { socket }), - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "TunSocket::new() failed", - )), - } - } -} diff --git a/src/platform/windows.rs b/src/platform/windows.rs deleted file mode 100644 index 5ea15d0..0000000 --- a/src/platform/windows.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub struct Tunnel { - // TODO: Windows virtual adapter? -} - -impl Tunnel { - pub fn new() -> Result { - Ok(Tunnel {}) - } -}