From e9be4b9ef5bf085a3e405f71ebd3604829e65a5d Mon Sep 17 00:00:00 2001 From: Gabi Date: Fri, 23 Jun 2023 19:39:58 -0300 Subject: [PATCH] connlib: moves it to the main firezone library This brindgs connlib from its own separated repo to firezone's monorepo. On top of bringing connlib we also add and unify the Dockerfile for all rust binaries and add a docker-compose that can run a headless client, a relay and a gateway which eventually will test the whole flow between a client and a resource. For this to work we also incorporated some elixir scripts to generate portal tokens for those components. --- .github/workflows/publish_connlib.yml | 46 + .github/workflows/rust.yml | 140 ++- NOTICE.txt | 20 + docker-compose.yml | 66 ++ elixir/.gitignore | 6 - gateway_variables.env | 1 + headless_variables.env | 1 + relay_variables.env | 1 + rust/Cargo.lock | 1038 +++++++++++++---- rust/Cargo.toml | 18 +- rust/Dockerfile | 21 + rust/connlib/.gitignore | 170 +++ rust/connlib/README.md | 31 + rust/connlib/clients/android/Cargo.toml | 14 + rust/connlib/clients/android/build.gradle.kts | 9 + .../clients/android/consumer-rules.pro | 0 .../connlib/clients/android/gradle.properties | 3 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 61608 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + rust/connlib/clients/android/gradlew | 244 ++++ rust/connlib/clients/android/gradlew.bat | 92 ++ .../clients/android/lib/build.gradle.kts | 94 ++ .../android/lib/src/main/AndroidManifest.xml | 4 + .../main/java/dev/firezone/connlib/Logger.kt | 5 + .../main/java/dev/firezone/connlib/Session.kt | 8 + .../java/dev/firezone/connlib/VpnService.kt | 19 + .../java/dev/firezone/connlib/ConnlibTest.kt | 9 + .../java/dev/firezone/connlib/SessionTest.kt | 9 + .../dev/firezone/connlib/VpnServiceTest.kt | 10 + .../clients/android/proguard-rules.pro | 21 + .../clients/android/settings.gradle.kts | 17 + rust/connlib/clients/android/src/lib.rs | 128 ++ rust/connlib/clients/apple/.gitignore | 31 + rust/connlib/clients/apple/Cargo.toml | 16 + rust/connlib/clients/apple/README.md | 19 + .../apple/Sources/Connlib/Adapter.swift | 298 +++++ .../apple/Sources/Connlib/BridgingHeader.h | 7 + .../Sources/Connlib/CallbackHandler.swift | 93 ++ .../Sources/Connlib/Generated/.gitignore | 2 + .../clients/apple/Sources/Connlib/connlib.h | 18 + .../clients/apple/Tests/connlibTests/.gitkeep | 0 rust/connlib/clients/apple/build-rust.sh | 60 + .../clients/apple/build-xcframework.sh | 33 + rust/connlib/clients/apple/build.rs | 14 + .../apple/connlib.xcodeproj/project.pbxproj | 466 ++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Connlib.xcscheme | 66 ++ rust/connlib/clients/apple/src/lib.rs | 115 ++ rust/connlib/clients/headless/Cargo.toml | 14 + rust/connlib/clients/headless/src/main.rs | 70 ++ rust/connlib/gateway/Cargo.toml | 13 + rust/connlib/gateway/src/main.rs | 54 + rust/connlib/libs/client/Cargo.toml | 16 + rust/connlib/libs/client/src/control.rs | 191 +++ rust/connlib/libs/client/src/lib.rs | 21 + rust/connlib/libs/client/src/messages.rs | 274 +++++ rust/connlib/libs/common/Cargo.toml | 36 + rust/connlib/libs/common/src/control.rs | 334 ++++++ rust/connlib/libs/common/src/error.rs | 119 ++ rust/connlib/libs/common/src/error_type.rs | 20 + rust/connlib/libs/common/src/lib.rs | 29 + rust/connlib/libs/common/src/messages.rs | 160 +++ rust/connlib/libs/common/src/messages/key.rs | 54 + rust/connlib/libs/common/src/session.rs | 241 ++++ rust/connlib/libs/gateway/Cargo.toml | 16 + rust/connlib/libs/gateway/src/control.rs | 159 +++ rust/connlib/libs/gateway/src/lib.rs | 21 + rust/connlib/libs/gateway/src/messages.rs | 138 +++ rust/connlib/libs/tunnel/Cargo.toml | 40 + .../libs/tunnel/src/control_protocol.rs | 314 +++++ .../libs/tunnel/src/device_channel_unix.rs | 70 ++ .../libs/tunnel/src/device_channel_win.rs | 27 + rust/connlib/libs/tunnel/src/index.rs | 61 + rust/connlib/libs/tunnel/src/lib.rs | 511 ++++++++ rust/connlib/libs/tunnel/src/peer.rs | 65 ++ .../connlib/libs/tunnel/src/resource_table.rs | 151 +++ rust/connlib/libs/tunnel/src/tun_android.rs | 20 + rust/connlib/libs/tunnel/src/tun_darwin.rs | 284 +++++ rust/connlib/libs/tunnel/src/tun_linux.rs | 272 +++++ rust/connlib/libs/tunnel/src/tun_win.rs | 22 + rust/connlib/macros/Cargo.toml | 12 + rust/connlib/macros/src/lib.rs | 108 ++ rust/relay.Dockerfile | 23 - rust/relay/Cargo.toml | 2 +- rust/relay/src/main.rs | 33 +- rust/rust-toolchain.toml | 14 +- 87 files changed, 7218 insertions(+), 295 deletions(-) create mode 100644 .github/workflows/publish_connlib.yml create mode 100644 NOTICE.txt create mode 100644 gateway_variables.env create mode 100644 headless_variables.env create mode 100644 relay_variables.env create mode 100644 rust/Dockerfile create mode 100644 rust/connlib/.gitignore create mode 100644 rust/connlib/README.md create mode 100644 rust/connlib/clients/android/Cargo.toml create mode 100644 rust/connlib/clients/android/build.gradle.kts create mode 100644 rust/connlib/clients/android/consumer-rules.pro create mode 100644 rust/connlib/clients/android/gradle.properties create mode 100644 rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 rust/connlib/clients/android/gradlew create mode 100644 rust/connlib/clients/android/gradlew.bat create mode 100644 rust/connlib/clients/android/lib/build.gradle.kts create mode 100644 rust/connlib/clients/android/lib/src/main/AndroidManifest.xml create mode 100644 rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt create mode 100644 rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt create mode 100644 rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt create mode 100644 rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt create mode 100644 rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt create mode 100644 rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt create mode 100644 rust/connlib/clients/android/proguard-rules.pro create mode 100644 rust/connlib/clients/android/settings.gradle.kts create mode 100644 rust/connlib/clients/android/src/lib.rs create mode 100644 rust/connlib/clients/apple/.gitignore create mode 100644 rust/connlib/clients/apple/Cargo.toml create mode 100644 rust/connlib/clients/apple/README.md create mode 100644 rust/connlib/clients/apple/Sources/Connlib/Adapter.swift create mode 100644 rust/connlib/clients/apple/Sources/Connlib/BridgingHeader.h create mode 100644 rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift create mode 100644 rust/connlib/clients/apple/Sources/Connlib/Generated/.gitignore create mode 100644 rust/connlib/clients/apple/Sources/Connlib/connlib.h create mode 100644 rust/connlib/clients/apple/Tests/connlibTests/.gitkeep create mode 100755 rust/connlib/clients/apple/build-rust.sh create mode 100755 rust/connlib/clients/apple/build-xcframework.sh create mode 100644 rust/connlib/clients/apple/build.rs create mode 100644 rust/connlib/clients/apple/connlib.xcodeproj/project.pbxproj create mode 100644 rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 rust/connlib/clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme create mode 100644 rust/connlib/clients/apple/src/lib.rs create mode 100644 rust/connlib/clients/headless/Cargo.toml create mode 100644 rust/connlib/clients/headless/src/main.rs create mode 100644 rust/connlib/gateway/Cargo.toml create mode 100644 rust/connlib/gateway/src/main.rs create mode 100644 rust/connlib/libs/client/Cargo.toml create mode 100644 rust/connlib/libs/client/src/control.rs create mode 100644 rust/connlib/libs/client/src/lib.rs create mode 100644 rust/connlib/libs/client/src/messages.rs create mode 100644 rust/connlib/libs/common/Cargo.toml create mode 100644 rust/connlib/libs/common/src/control.rs create mode 100644 rust/connlib/libs/common/src/error.rs create mode 100644 rust/connlib/libs/common/src/error_type.rs create mode 100644 rust/connlib/libs/common/src/lib.rs create mode 100644 rust/connlib/libs/common/src/messages.rs create mode 100644 rust/connlib/libs/common/src/messages/key.rs create mode 100644 rust/connlib/libs/common/src/session.rs create mode 100644 rust/connlib/libs/gateway/Cargo.toml create mode 100644 rust/connlib/libs/gateway/src/control.rs create mode 100644 rust/connlib/libs/gateway/src/lib.rs create mode 100644 rust/connlib/libs/gateway/src/messages.rs create mode 100644 rust/connlib/libs/tunnel/Cargo.toml create mode 100644 rust/connlib/libs/tunnel/src/control_protocol.rs create mode 100644 rust/connlib/libs/tunnel/src/device_channel_unix.rs create mode 100644 rust/connlib/libs/tunnel/src/device_channel_win.rs create mode 100644 rust/connlib/libs/tunnel/src/index.rs create mode 100644 rust/connlib/libs/tunnel/src/lib.rs create mode 100644 rust/connlib/libs/tunnel/src/peer.rs create mode 100644 rust/connlib/libs/tunnel/src/resource_table.rs create mode 100644 rust/connlib/libs/tunnel/src/tun_android.rs create mode 100644 rust/connlib/libs/tunnel/src/tun_darwin.rs create mode 100644 rust/connlib/libs/tunnel/src/tun_linux.rs create mode 100644 rust/connlib/libs/tunnel/src/tun_win.rs create mode 100644 rust/connlib/macros/Cargo.toml create mode 100644 rust/connlib/macros/src/lib.rs delete mode 100644 rust/relay.Dockerfile diff --git a/.github/workflows/publish_connlib.yml b/.github/workflows/publish_connlib.yml new file mode 100644 index 000000000..7f876f958 --- /dev/null +++ b/.github/workflows/publish_connlib.yml @@ -0,0 +1,46 @@ +name: Publish packages to GitHub Packages +on: + release: + types: [published] +jobs: + # Noop: XCFramework is attached to release already in build workflow + # publish-apple: + publish-android: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./rust + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + - uses: Swatinem/rust-cache@v2 + with: + workspaces: ./rust + - name: Setup toolchain + run: rustup show + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + - name: Sanity check tag equals AAR version + run: | + pkg_version=$(awk -F ' = ' '$1 ~ /version/ { gsub(/[\"]/, "", $2); printf("%s",$2); exit; }' connlib/android/lib/build.gradle.kts) + if [[ "${{ github.ref_name }}" = "$pkg_version" ]]; then + echo "Github ref name ${{ github.ref_name }} equals parsed package version $pkg_version. Continuing..." + else + echo "Github ref name ${{ github.ref_name }} differs from parsed package version $pkg_version! Aborting..." + exit 1 + fi + - name: Publish package + uses: gradle/gradle-build-action@v2 + with: + build-root-directory: android + arguments: publish + env: + GITHUB_ACTOR: ${{ secrets.GITHUB_ACTOR }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 43c68d624..5731f1d6a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,8 +12,18 @@ concurrency: cancel-in-progress: true jobs: - test: - name: Test all crates + draft-release: + runs-on: ubuntu-latest + outputs: + tag_name: ${{ steps.release_drafter.outputs.tag_name }} + steps: + - uses: release-drafter/release-drafter@v5 + id: release_drafter + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + test-relay: + name: Test relay runs-on: ubuntu-latest defaults: run: @@ -28,15 +38,130 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: ./rust - - run: cargo fmt -- --check - - run: cargo doc --no-deps --document-private-items + - run: cargo fmt -p relay -- --check + - run: cargo doc -p relay --no-deps --document-private-items env: RUSTDOCFLAGS: "-D warnings" - - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo clippy -p relay --all-targets --all-features -- -D warnings - run: cargo test - cross: # cross is separate from test because cross-compiling yields different artifacts and we cannot reuse the cache. - name: Cross compile all crates + test-connlib: + needs: draft-release + name: Connlib checks + strategy: + matrix: + runs-on: + - ubuntu-20.04 + - ubuntu-22.04 + - macos-11 + - macos-12 + - windows-2019 + - windows-2022 + runs-on: ${{ matrix.runs-on }} + defaults: + run: + working-directory: ./rust + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Update toolchain + run: rustup show + - uses: Swatinem/rust-cache@v2 + - name: Run connlib checks and tests + run: | + cargo check --workspace --exclude relay + cargo clippy --workspace --exclude relay -- -D clippy::all + cargo test --workspace --exclude relay + + build-android: + needs: + - test-connlib + - draft-release + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + matrix: + rust: [stable] + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + - name: Update toolchain + run: rustup show + - uses: actions/cache@v3 + with: + path: | + ~/rust/connlib/clients/android/.gradle/caches + ~/rust/connlib/clients/android/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'adopt' + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v1 + - name: Assemble Release + uses: gradle/gradle-build-action@v2 + with: + arguments: build assembleRelease + build-root-directory: rust/connlib/clients/android + - name: Move artifact + run: | + mv ./rust/connlib/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 + path: | + ./connlib-${{ needs.draft-release.outputs.tag_name }}.aar + + build-apple: + needs: + - test-connlib + - draft-release + runs-on: macos-latest + permissions: + contents: read + strategy: + matrix: + rust: [stable] + steps: + - uses: actions/checkout@v3 + - uses: Swatinem/rust-cache@v2 + - name: Update toolchain + run: rustup show + - name: Setup lipo + run: cargo install cargo-lipo + - uses: actions/cache@v3 + with: + path: apple/.build + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Build Connlib.xcframework.zip + env: + CONFIGURATION: Release + PROJECT_DIR: . + working-directory: ./rust/connlib/clients/apple + run: | + # 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 + ./build-rust.sh + ./build-xcframework.sh + mv Connlib.xcframework.zip ../../../Connlib-${{ needs.draft-release.outputs.tag_name }}.xcframework.zip + mv Connlib.xcframework.zip.checksum.txt ../../../Connlib-${{ needs.draft-release.outputs.tag_name }}.xcframework.zip.checksum.txt + - uses: actions/upload-artifact@v3 + with: + name: connlib-apple + path: | + ./Connlib-${{ needs.draft-release.outputs.tag_name }}.xcframework.zip + ./Connlib-${{ needs.draft-release.outputs.tag_name }}.xcframework.zip.checksum.txt + + cross-relay: # cross is separate from test because cross-compiling yields different artifacts and we cannot reuse the cache. + name: Cross compile relay runs-on: ubuntu-latest defaults: run: @@ -66,7 +191,6 @@ jobs: # This implicitly triggers installation of the toolchain in the `rust-toolchain.toml` file. # If we don't do this here, our cache action will compute a cache key based on the Rust version shipped on GitHub's runner which might differ from the one we use. - run: rustup show - - uses: Swatinem/rust-cache@v2 with: workspaces: ./rust diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 000000000..9e0597091 --- /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. diff --git a/docker-compose.yml b/docker-compose.yml index c639a8437..f4226263f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -90,12 +90,78 @@ services: OUTBOUND_EMAIL_ADAPTER_OPTS: "{\"api_key\":\"7da7d1cd-111c-44a7-b5ac-4027b9d230e5\"}" # Seeds STATIC_SEEDS: "true" + # Client info + USER_AGENT: "iOS/12.5 (iPhone) connlib/0.7.412" depends_on: postgres: condition: 'service_healthy' networks: - app + client: + environment: + FZ_URL: "ws://api:8081/" + FZ_SECRET: "SFMyNTY.g2gDaANkAAhpZGVudGl0eW0AAAAkN2RhN2QxY2QtMTExYy00NGE3LWI1YWMtNDAyN2I5ZDIzMGU1bQAAACDZI3ehOZSu3JOSMREkvzrtKjs8jkrW6fpbVw9opDYmi24GANjCD-qIAWIB4TOA.XhoLEDjIzuv1SXEVUV6lfIHW12n5-J5aBDUKCl8ovMk" + build: + context: rust + args: + PACKAGE: headless + image: firezone-headless + cap_add: + - NET_ADMIN + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 + devices: + - "/dev/net/tun:/dev/net/tun" + depends_on: + - api + networks: + app: + ipv4_address: 172.28.0.100 + + gateway: + environment: + FZ_URL: "ws://api:8081/" + FZ_SECRET: "SFMyNTY.g2gDaAJtAAAAJDNjZWYwNTY2LWFkZmQtNDhmZS1hMGYxLTU4MDY3OTYwOGY2Zm0AAABAamp0enhSRkpQWkdCYy1vQ1o5RHkyRndqd2FIWE1BVWRwenVScjJzUnJvcHg3NS16bmhfeHBfNWJUNU9uby1yYm4GAJXr4emIAWIAAVGA.jz0s-NohxgdAXeRMjIQ9kLBOyd7CmKXWi2FHY-Op8GM" + build: + context: rust + args: + PACKAGE: gateway + image: firezone-gateway + cap_add: + - NET_ADMIN + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + devices: + - "/dev/net/tun:/dev/net/tun" + depends_on: + - api + networks: + - app + + relay: + environment: + PUBLIC_IP4_ADDR: 172.28.0.101 + LISTEN_IP4_ADDR: 172.28.0.101 + PORTAL_WS_URL: "ws://api:8081/" + PORTAL_TOKEN: "SFMyNTY.g2gDaAJtAAAAJDcyODZiNTNkLTA3M2UtNGM0MS05ZmYxLWNjODQ1MWRhZDI5OW0AAABARVg3N0dhMEhLSlVWTGdjcE1yTjZIYXRkR25mdkFEWVFyUmpVV1d5VHFxdDdCYVVkRVUzbzktRmJCbFJkSU5JS24GAMDq4emIAWIAAVGA.fLlZsUMS0VJ4RCN146QzUuINmGubpsxoyIf3uhRHdiQ" + ports: + - "3478/udp" + - "49152-65535/udp" + build: + context: rust + args: + PACKAGE: relay + image: firezone-relay + depends_on: + - api + networks: + app: + ipv4_address: 172.28.0.101 + command: "--allow-insecure-ws" + api: build: context: elixir diff --git a/elixir/.gitignore b/elixir/.gitignore index 20e619c08..f0b785e33 100644 --- a/elixir/.gitignore +++ b/elixir/.gitignore @@ -1,6 +1,3 @@ -# macOS cruft -.DS_Store - # HTTPS dev certs priv/pki/authorities/local/ @@ -16,9 +13,6 @@ deps/ # If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump -# If NPM crashes, it generates a log, let's ignore it too. -npm-debug.log - # The directory NPM downloads your dependencies sources to. /assets/node_modules/ diff --git a/gateway_variables.env b/gateway_variables.env new file mode 100644 index 000000000..6858554d4 --- /dev/null +++ b/gateway_variables.env @@ -0,0 +1 @@ +FZ_SECRET=SFMyNTY.g2gDaAJtAAAAJGFkMTBjNTVhLTNiYTUtNDdjYy04YTZkLWJlNmE1NWJlN2FlN20AAABAcUpCSGNzRGtkVVdUUjJyaGQ0c2dGSFZ3U0d1ZnJncWFIV0dwNXFsOU5nUWR4RVRTbk9ycW1GbnN6cDVZWk50N24GAJzge-WIAWIAAVGA.au7yCBGyycngufVpdgPEGf4OtjzIx01k4-JSXVEALtk \ No newline at end of file diff --git a/headless_variables.env b/headless_variables.env new file mode 100644 index 000000000..4b0d80f60 --- /dev/null +++ b/headless_variables.env @@ -0,0 +1 @@ +FZ_SECRET=SFMyNTY.g2gDaANkAAhpZGVudGl0eW0AAAAkN2VhMGVkYzQtZjlkNy00NzlhLWE2OWQtYWM3NTZkN2QyYzk4bQAAACBzHhbd6lPr9SMx_HaE6mqiHZqvmV2wEcmmYUdtbi6xjW4GALGZe-WIAWIACTqA.zsVviEWg7VlEjHBf5krjxQ1wvj-TzrYdPBJfDbY3NnE \ No newline at end of file diff --git a/relay_variables.env b/relay_variables.env new file mode 100644 index 000000000..db8038413 --- /dev/null +++ b/relay_variables.env @@ -0,0 +1 @@ +PORTAL_TOKEN=SFMyNTY.g2gDaAJtAAAAJDQ0NWNkMWY2LThkMzMtNDJlOC1hMDQ0LWMzYTVlMmEyNTU0NW0AAABAWmk4c1JMX2FWWjdyZUZoUll5b3BVZ09qRV85aHVjajF2ZGlCSjg4Q0RXOUw4MzBOdmVCU3pSdXg0MFhtazlEcG4GAGD1e-WIAWIAAVGA.BMTyT0jfmPvn_WEWn9AjxvVuv5BhdGNWslaHgzuCATA \ No newline at end of file diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8fc9b409e..007975969 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -48,7 +48,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cipher 0.3.0", "cpufeatures", "opaque-debug", @@ -56,40 +56,26 @@ dependencies = [ [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cipher 0.4.4", "cpufeatures", ] [[package]] name = "aes-gcm" -version = "0.9.4" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" -dependencies = [ - "aead 0.4.3", - "aes 0.7.5", - "cipher 0.3.0", - "ctr 0.8.0", - "ghash 0.4.4", - "subtle", -] - -[[package]] -name = "aes-gcm" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e1366e0c69c9f927b1fa5ce2c7bf9eafc8f9268c0b9800729e8b267612447c" +checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237" dependencies = [ "aead 0.5.2", - "aes 0.8.2", + "aes 0.8.3", "cipher 0.4.4", "ctr 0.9.2", - "ghash 0.5.0", + "ghash", "subtle", ] @@ -115,13 +101,31 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" 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 = "anstream" version = "0.3.2" @@ -139,15 +143,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anstyle-parse" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" dependencies = [ "utf8parse", ] @@ -258,7 +262,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -273,6 +277,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "getrandom 0.2.10", + "instant", + "rand", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -328,12 +343,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "block-buffer" -version = "0.9.0" +name = "blake2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "generic-array", + "digest 0.10.7", ] [[package]] @@ -361,11 +376,36 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +[[package]] +name = "boringtun" +version = "0.5.2" +source = "git+https://github.com/cloudflare/boringtun?rev=878385f#878385f171d60effac4ad1a9d4dee41e777528b8" +dependencies = [ + "aead 0.5.2", + "base64 0.13.1", + "blake2", + "chacha20poly1305", + "hex", + "hmac", + "ip_network", + "ip_network_table", + "jni 0.19.0", + "libc", + "nix 0.25.1", + "parking_lot", + "rand_core 0.6.4", + "ring", + "tracing", + "tracing-subscriber", + "untrusted 0.9.0", + "x25519-dalek", +] + [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytecodec" @@ -406,12 +446,48 @@ dependencies = [ "subtle", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.4.4", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + [[package]] name = "cipher" version = "0.2.5" @@ -438,13 +514,14 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] name = "clap" -version = "4.3.4" +version = "4.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80672091db20273a15cf9fdd4e47ed43b5091ec9841bf4c6145c9dfbbcae09ed" +checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc" dependencies = [ "clap_builder", "clap_derive", @@ -453,9 +530,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.4" +version = "4.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1458a1df40e1e2afebb7ab60ce55c1fa8f431146205aa5f4887e0b111c27636" +checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae" dependencies = [ "anstream", "anstyle", @@ -473,7 +550,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -502,6 +579,26 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "connlib-android" +version = "0.1.6" +dependencies = [ + "android_logger", + "firezone-client-connlib", + "jni 0.21.1", + "log", +] + +[[package]] +name = "connlib-apple" +version = "0.1.6" +dependencies = [ + "firezone-client-connlib", + "libc", + "swift-bridge", + "swift-bridge-build 0.1.51 (git+https://github.com/conectado/swift-bridge.git?branch=fix-already-declared)", +] + [[package]] name = "const-oid" version = "0.9.2" @@ -532,9 +629,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -577,16 +674,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "ctr" version = "0.8.0" @@ -624,7 +711,7 @@ version = "4.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fiat-crypto", "packed_simd_2", "platforms", @@ -669,9 +756,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "der" @@ -777,7 +864,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "crypto-common", "subtle", ] @@ -790,7 +877,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -805,6 +892,12 @@ dependencies = [ "signature", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "elliptic-curve" version = "0.12.3" @@ -886,6 +979,63 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +[[package]] +name = "firezone-client-connlib" +version = "0.1.0" +dependencies = [ + "async-trait", + "boringtun", + "firezone-tunnel", + "libs-common", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "firezone-gateway-connlib" +version = "0.1.0" +dependencies = [ + "async-trait", + "boringtun", + "firezone-tunnel", + "libs-common", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "firezone-tunnel" +version = "0.1.0" +dependencies = [ + "android_logger", + "async-trait", + "boringtun", + "bytes", + "futures", + "futures-util", + "ip_network", + "ip_network_table", + "itertools", + "libc", + "libs-common", + "log", + "netlink-packet-core", + "netlink-packet-route", + "parking_lot", + "rand_core 0.6.4", + "rtnetlink", + "serde", + "thiserror", + "tokio", + "tracing", + "webrtc", + "wintun", +] + [[package]] name = "fnv" version = "1.0.7" @@ -957,7 +1107,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -990,6 +1140,17 @@ dependencies = [ "slab", ] +[[package]] +name = "gateway" +version = "0.1.0" +dependencies = [ + "anyhow", + "firezone-gateway-connlib", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1006,32 +1167,22 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] -[[package]] -name = "ghash" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" -dependencies = [ - "opaque-debug", - "polyval 0.5.3", -] - [[package]] name = "ghash" version = "0.5.0" @@ -1039,9 +1190,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" dependencies = [ "opaque-debug", - "polyval 0.6.0", + "polyval", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.12.1" @@ -1053,6 +1210,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "headless" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "firezone-client-connlib", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "heck" version = "0.4.1" @@ -1092,17 +1261,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" -dependencies = [ - "crypto-mac", - "digest 0.9.0", + "hmac", ] [[package]] @@ -1177,14 +1336,14 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] name = "interceptor" -version = "0.8.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8a11ae2da61704edada656798b61c94b35ecac2c58eb955156987d5e6be90b" +checksum = "5c142385498b53584546abbfa50188b2677af8e4f879da1ee5d905cb7de5b97a" dependencies = [ "async-trait", "bytes", @@ -1201,15 +1360,40 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", "windows-sys 0.48.0", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" +dependencies = [ + "serde", +] + +[[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 = "ipnet" version = "2.7.2" @@ -1228,6 +1412,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1235,10 +1428,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] -name = "js-sys" -version = "0.3.63" +name = "java-locator" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "90003f2fd9c52f212c21d8520f1128da0080bad6fff16b68fe6e7f2f0c3780c2" +dependencies = [ + "glob", + "lazy_static", +] + +[[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" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if 1.0.0", + "combine", + "java-locator", + "jni-sys", + "libloading", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1251,9 +1492,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if 1.0.0", + "winapi", +] [[package]] name = "libm" @@ -1263,21 +1514,48 @@ checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "libs-common" +version = "0.1.0" +dependencies = [ + "async-trait", + "backoff", + "base64 0.21.2", + "boringtun", + "futures", + "futures-util", + "ip_network", + "macros", + "os_info", + "rand_core 0.6.4", + "rtnetlink", + "serde", + "serde_json", + "swift-bridge", + "thiserror", + "tokio", + "tokio-tungstenite 0.18.0", + "tracing", + "url", + "uuid", + "webrtc", +] [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1285,11 +1563,17 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "macros" +version = "0.1.0" dependencies = [ - "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.18", ] [[package]] @@ -1339,14 +1623,79 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" +dependencies = [ + "anyhow", + "bitflags", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-proto" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26305d12193227ef7b8227e7d61ae4eaf174607f79bd8eeceff07aacaefde497" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror", + "tokio", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "bytes", + "futures", + "libc", + "log", + "tokio", ] [[package]] @@ -1356,11 +1705,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 1.0.0", "libc", "memoffset", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if 1.0.0", + "libc", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", + "static_assertions", +] + [[package]] name = "nom" version = "7.1.3" @@ -1409,7 +1782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", - "libm 0.2.6", + "libm 0.2.7", ] [[package]] @@ -1442,9 +1815,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -1458,6 +1831,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -1492,7 +1875,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libm 0.1.4", ] @@ -1508,17 +1891,23 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-targets 0.48.0", ] +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + [[package]] name = "pem" version = "1.1.1" @@ -1554,7 +1943,7 @@ dependencies = [ "serde_json", "thiserror", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.19.0", "tracing", "url", ] @@ -1588,27 +1977,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" [[package]] -name = "polyval" -version = "0.5.3" +name = "poly1305" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash 0.4.1", + "universal-hash", ] [[package]] name = "polyval" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef234e08c11dfcb2e56f79fd70f6f2eb7f025c0ce2333e82f4f0518ecad30c6" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "opaque-debug", - "universal-hash 0.5.1", + "universal-hash", ] [[package]] @@ -1619,9 +2007,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] @@ -1654,9 +2042,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -1697,7 +2085,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", ] [[package]] @@ -1741,15 +2129,6 @@ dependencies = [ "url", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -1761,13 +2140,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -1787,9 +2166,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "relay" @@ -1830,7 +2209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ "crypto-bigint", - "hmac 0.12.1", + "hmac", "zeroize", ] @@ -1844,16 +2223,16 @@ dependencies = [ "libc", "once_cell", "spin", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] [[package]] name = "rtcp" -version = "0.7.2" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1919efd6d4a6a85d13388f9487549bb8e359f17198cc03ffd72f79b553873691" +checksum = "6423493804221c276d27f3cc383cd5cbe1a1f10f210909fd4951b579b01293cd" dependencies = [ "bytes", "thiserror", @@ -1861,12 +2240,29 @@ dependencies = [ ] [[package]] -name = "rtp" -version = "0.6.8" +name = "rtnetlink" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2a095411ff00eed7b12e4c6a118ba984d113e1079582570d56a5ee723f11f80" +checksum = "ed7d42da676fdf7e470e2502717587dd1089d8b48d9d1b846dcc3c01072858cb" +dependencies = [ + "futures", + "log", + "netlink-packet-core", + "netlink-packet-route", + "netlink-packet-utils", + "netlink-proto", + "netlink-sys", + "nix 0.26.2", + "thiserror", + "tokio", +] + +[[package]] +name = "rtp" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b728adb99b88d932f2f0622b540bf7ccb196f81e9823b5b0eeb166526c88138c" dependencies = [ - "async-trait", "bytes", "rand", "serde", @@ -1894,9 +2290,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags", "errno", @@ -1933,9 +2329,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -1959,7 +2355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -1980,6 +2376,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.21" @@ -2002,7 +2407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -2012,7 +2417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -2072,48 +2477,35 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "bdf3bf93142acad5821c99197022e170842cdbc1c30482b98750c688c640842a" dependencies = [ "itoa", "ryu", "serde", ] -[[package]] -name = "sha-1" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha1" version = "0.2.0" @@ -2126,18 +2518,18 @@ version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -2185,6 +2577,15 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smol_str" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.4.9" @@ -2211,6 +2612,12 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.10.0" @@ -2261,9 +2668,9 @@ dependencies = [ [[package]] name = "stun_codec" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972a7fb2957b136c6b6e35bd5e40c7954831613d8fecb43f21bf1edb4a39b218" +checksum = "4089f66744a63bc909eed6ece965b493030ca896f21c24d9f26c659926c7e05b" dependencies = [ "bytecodec", "byteorder", @@ -2284,9 +2691,71 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "swift-bridge" +version = "0.1.51" +source = "git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f#4fbd30f81085dc366bda3c0303eac66345de3ed9" +dependencies = [ + "swift-bridge-build 0.1.51 (git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f)", + "swift-bridge-macro", +] + +[[package]] +name = "swift-bridge-build" +version = "0.1.51" +source = "git+https://github.com/conectado/swift-bridge.git?branch=fix-already-declared#7415b9108beb87de95cd4687c1f841ae7b37ea6f" +dependencies = [ + "proc-macro2", + "swift-bridge-ir 0.1.51 (git+https://github.com/conectado/swift-bridge.git?branch=fix-already-declared)", + "syn 1.0.109", + "tempfile", +] + +[[package]] +name = "swift-bridge-build" +version = "0.1.51" +source = "git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f#4fbd30f81085dc366bda3c0303eac66345de3ed9" +dependencies = [ + "proc-macro2", + "swift-bridge-ir 0.1.51 (git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f)", + "syn 1.0.109", + "tempfile", +] + +[[package]] +name = "swift-bridge-ir" +version = "0.1.51" +source = "git+https://github.com/conectado/swift-bridge.git?branch=fix-already-declared#7415b9108beb87de95cd4687c1f841ae7b37ea6f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "swift-bridge-ir" +version = "0.1.51" +source = "git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f#4fbd30f81085dc366bda3c0303eac66345de3ed9" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "swift-bridge-macro" +version = "0.1.51" +source = "git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f#4fbd30f81085dc366bda3c0303eac66345de3ed9" +dependencies = [ + "proc-macro2", + "quote", + "swift-bridge-ir 0.1.51 (git+https://github.com/chinedufn/swift-bridge.git?rev=4fbd30f)", + "syn 1.0.109", +] [[package]] name = "syn" @@ -2301,9 +2770,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -2324,15 +2793,16 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ - "cfg-if", + "autocfg", + "cfg-if 1.0.0", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2373,7 +2843,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2382,15 +2852,15 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", ] [[package]] name = "time" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -2455,7 +2925,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2468,6 +2938,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.18.0", +] + [[package]] name = "tokio-tungstenite" version = "0.19.0" @@ -2480,7 +2962,7 @@ dependencies = [ "rustls-native-certs", "tokio", "tokio-rustls", - "tungstenite", + "tungstenite 0.19.0", ] [[package]] @@ -2503,7 +2985,7 @@ version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -2512,20 +2994,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "8803eee176538f94ae9a14b55b2804eb7e1441f8210b1c31290b3bccdccff73b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", "valuable", @@ -2589,6 +3071,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand", + "sha1 0.10.5", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "tungstenite" version = "0.19.0" @@ -2649,9 +3150,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -2668,16 +3169,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" -[[package]] -name = "universal-hash" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "universal-hash" version = "0.5.1" @@ -2694,6 +3185,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.0" @@ -2719,11 +3216,12 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.3" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", + "serde", ] [[package]] @@ -2756,6 +3254,16 @@ dependencies = [ "atomic-waker", ] +[[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.9.0+wasi-snapshot-preview1" @@ -2770,34 +3278,34 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2805,28 +3313,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -2839,7 +3347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -2849,18 +3357,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ "ring", - "untrusted", + "untrusted 0.7.1", ] [[package]] name = "webrtc" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0507dc2d9a79fe1aa892e8876d3bb9b3d939c654fa8222bbb29b6331baeb08cc" +checksum = "f60dde9fd592872bc371b3842e4616bc4c6984242e3cd2a7d7cb771db278601b" dependencies = [ "arc-swap", "async-trait", "bytes", + "cfg-if 0.1.10", "hex", "interceptor", "lazy_static", @@ -2876,6 +3385,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "smol_str", "stun", "thiserror", "time", @@ -2915,7 +3425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a00f4242f2db33307347bd5be53263c52a0331c96c14292118c9a6bb48d267" dependencies = [ "aes 0.6.0", - "aes-gcm 0.10.1", + "aes-gcm", "async-trait", "bincode", "block-modes", @@ -2925,7 +3435,7 @@ dependencies = [ "der-parser 8.2.0", "elliptic-curve", "hkdf", - "hmac 0.12.1", + "hmac", "log", "p256", "p384", @@ -2987,9 +3497,9 @@ dependencies = [ [[package]] name = "webrtc-media" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f72e1650a8ae006017d1a5280efb49e2610c19ccc3c0905b03b648aee9554991" +checksum = "cd8e3711a321f6a375973144f48065cf705316ab6709672954aace020c668eb6" dependencies = [ "byteorder", "bytes", @@ -3017,22 +3527,21 @@ dependencies = [ [[package]] name = "webrtc-srtp" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6183edc4c1c6c0175f8812eefdce84dfa0aea9c3ece71c2bf6ddd3c964de3da5" +checksum = "5683b597b3c6af47ff11e695697f881bc42acfd8feeb0d4eb20a5ae9caaee6ae" dependencies = [ "aead 0.4.3", "aes 0.7.5", - "aes-gcm 0.9.4", - "async-trait", + "aes-gcm", "byteorder", "bytes", "ctr 0.8.0", - "hmac 0.11.0", + "hmac", "log", "rtcp", "rtp", - "sha-1", + "sha1 0.10.5", "subtle", "thiserror", "tokio", @@ -3053,13 +3562,19 @@ dependencies = [ "lazy_static", "libc", "log", - "nix", + "nix 0.24.3", "rand", "thiserror", "tokio", "winapi", ] +[[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" @@ -3238,6 +3753,21 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[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" @@ -3313,5 +3843,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5ff638a39..7eced66ef 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,2 +1,18 @@ [workspace] -members = ["relay", "phoenix-channel"] +members = [ + "relay", + "phoenix-channel", + "connlib/clients/android", + "connlib/clients/apple", + "connlib/clients/headless", + "connlib/libs/tunnel", + "connlib/libs/client", + "connlib/libs/gateway", + "connlib/libs/common", + "connlib/gateway", + "connlib/macros", +] + +[workspace.dependencies] +boringtun = { git = "https://github.com/cloudflare/boringtun", rev = "878385f", default-features = false } +swift-bridge = { git = "https://github.com/chinedufn/swift-bridge.git", rev = "4fbd30f" } diff --git a/rust/Dockerfile b/rust/Dockerfile new file mode 100644 index 000000000..0c60bd409 --- /dev/null +++ b/rust/Dockerfile @@ -0,0 +1,21 @@ +FROM rust:1.70-slim as BUILDER +ARG PACKAGE +WORKDIR /build/ +COPY . ./ +RUN --mount=type=cache,target=./target \ + --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/usr/local/rustup \ + apt update && apt install -y musl-tools && \ + cargo build -p $PACKAGE --release --target x86_64-unknown-linux-musl + +RUN --mount=type=cache,target=./target \ + mv ./target/x86_64-unknown-linux-musl/release/$PACKAGE /usr/local/bin/$PACKAGE + +FROM alpine:3.18 +ARG PACKAGE +WORKDIR /app/ +COPY --from=BUILDER /usr/local/bin/$PACKAGE . +ENV RUST_BACKTRACE=1 +ENV PATH "/app:$PATH" +ENV PACKAGE_NAME ${PACKAGE} +CMD ${PACKAGE_NAME} diff --git a/rust/connlib/.gitignore b/rust/connlib/.gitignore new file mode 100644 index 000000000..6ad2400c2 --- /dev/null +++ b/rust/connlib/.gitignore @@ -0,0 +1,170 @@ +### 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 ### +.build/ +DerivedData/ +xcuserdata/ +*.xcuserstate + +Firezone/Developer.xcconfig diff --git a/rust/connlib/README.md b/rust/connlib/README.md new file mode 100644 index 000000000..99da6a73d --- /dev/null +++ b/rust/connlib/README.md @@ -0,0 +1,31 @@ +# Connlib + +Firezone's connectivity library shared by all clients. + +## 🚧 Disclaimer 🚧 + +**NOTE**: This repository is undergoing heavy construction. You could say we're +_Building In The Open™_ in true open source spirit. Do not attempt to use +anything released here until this notice is removed. You have been warned. + +## Building Connlib + +1. You'll need a Rust toolchain installed if you don't have one already. We + recommend following the instructions at https://rustup.rs. +1. `rustup show` will install all needed targets since they are added to `rust-toolchain.toml`. +1. Follow the relevant instructions for your platform: +1. [Apple](#apple) +1. [Android](#android) +1. [Linux](#linux) +1. [Windows](#windows) + +### Apple + +Connlib should build successfully with recent macOS and Xcode versions assuming +you have Rust installed. If not, open a PR with the notes you found. + +### Android + +### Linux + +### Windows diff --git a/rust/connlib/clients/android/Cargo.toml b/rust/connlib/clients/android/Cargo.toml new file mode 100644 index 000000000..6526daa3e --- /dev/null +++ b/rust/connlib/clients/android/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "connlib-android" +version = "0.1.6" +edition = "2021" + +[dependencies] +jni = { version = "0.21.1", features = ["invocation"] } +firezone-client-connlib = { path = "../../libs/client" } +log = "0.4" +android_logger = "0.13" + +[lib] +name = "connlib" +crate-type = ["cdylib"] diff --git a/rust/connlib/clients/android/build.gradle.kts b/rust/connlib/clients/android/build.gradle.kts new file mode 100644 index 000000000..339bd63a8 --- /dev/null +++ b/rust/connlib/clients/android/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("org.mozilla.rust-android-gradle.rust-android") version "0.9.3" + id("com.android.library") version "7.4.2" apply false + id("org.jetbrains.kotlin.android") version "1.7.21" apply false +} + +tasks.register("clean",Delete::class) { + delete(rootProject.buildDir) +} diff --git a/rust/connlib/clients/android/consumer-rules.pro b/rust/connlib/clients/android/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/rust/connlib/clients/android/gradle.properties b/rust/connlib/clients/android/gradle.properties new file mode 100644 index 000000000..f53889cab --- /dev/null +++ b/rust/connlib/clients/android/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +kotlin.code.style=official +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 diff --git a/rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.jar b/rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..ccebba7710deaf9f98673a68957ea02138b60d0a GIT binary patch literal 61608 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 literal 0 HcmV?d00001 diff --git a/rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.properties b/rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..91dc34173 --- /dev/null +++ b/rust/connlib/clients/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/rust/connlib/clients/android/gradlew b/rust/connlib/clients/android/gradlew new file mode 100755 index 000000000..79a61d421 --- /dev/null +++ b/rust/connlib/clients/android/gradlew @@ -0,0 +1,244 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/rust/connlib/clients/android/gradlew.bat b/rust/connlib/clients/android/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/rust/connlib/clients/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rust/connlib/clients/android/lib/build.gradle.kts b/rust/connlib/clients/android/lib/build.gradle.kts new file mode 100644 index 000000000..b30c26986 --- /dev/null +++ b/rust/connlib/clients/android/lib/build.gradle.kts @@ -0,0 +1,94 @@ +plugins { + id("org.mozilla.rust-android-gradle.rust-android") + id("com.android.library") + id("kotlin-android") + id("org.jetbrains.kotlin.android") + `maven-publish` +} + +afterEvaluate { + publishing { + publications { + create("release") { + groupId = "dev.firezone" + artifactId = "connlib" + version = "0.1.6" + from(components["release"]) + } + } + } +} + +publishing { + repositories { + maven { + url = uri("https://maven.pkg.github.com/firezone/connlib") + name = "GitHubPackages" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } +} + +android { + namespace = "dev.firezone.connlib" + compileSdk = 33 + + defaultConfig { + minSdk = 29 + targetSdk = 33 + consumerProguardFiles("consumer-rules.pro") + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + externalNativeBuild { + cmake { + version = "3.22.1" + } + } + ndkVersion = "25.2.9519653" + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + } + } + compileOptions { + sourceCompatibility(JavaVersion.VERSION_1_8) + targetCompatibility(JavaVersion.VERSION_1_8) + } + kotlinOptions { + jvmTarget = "1.8" + } + publishing { + singleVariant("release") + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.test.ext:junit-gtest:1.0.0-alpha01") + implementation("com.android.ndk.thirdparty:googletest:1.11.0-beta-1") + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.21") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") +} + +apply(plugin = "org.mozilla.rust-android-gradle.rust-android") + +cargo { + prebuiltToolchains = true + verbose = true + module = "../" + libname = "connlib" + targets = listOf("arm", "arm64", "x86", "x86_64") +} + +tasks.whenTaskAdded { + if (name.startsWith("javaPreCompile")) { + dependsOn(tasks.named("cargoBuild")) + } +} diff --git a/rust/connlib/clients/android/lib/src/main/AndroidManifest.xml b/rust/connlib/clients/android/lib/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/rust/connlib/clients/android/lib/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt b/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt new file mode 100644 index 000000000..6957ab284 --- /dev/null +++ b/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Logger.kt @@ -0,0 +1,5 @@ +package dev.firezone.connlib + +public object Logger { + public external fun init() +} diff --git a/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt b/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt new file mode 100644 index 000000000..9214534c8 --- /dev/null +++ b/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/Session.kt @@ -0,0 +1,8 @@ +package dev.firezone.connlib + +public object Session { + public external fun connect(portalURL: String, token: String, callback: Any): Long + public external fun disconnect(session: Long): Boolean + public external fun bumpSockets(session: Long): Boolean + public external fun disableSomeRoamingForBrokenMobileSemantics(session: Long): Boolean +} diff --git a/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt b/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt new file mode 100644 index 000000000..fec18c143 --- /dev/null +++ b/rust/connlib/clients/android/lib/src/main/java/dev/firezone/connlib/VpnService.kt @@ -0,0 +1,19 @@ +package dev.firezone.connlib +import android.util.Log + +public class VpnService : android.net.VpnService() { + public override fun onCreate() { + super.onCreate() + Log.d("Connlib", "VpnService.onCreate") + } + + public override fun onDestroy() { + super.onDestroy() + Log.d("Connlib", "VpnService.onDestroy") + } + + public override fun onStartCommand(intent: android.content.Intent?, flags: Int, startId: Int): Int { + Log.d("Connlib", "VpnService.onStartCommand") + return super.onStartCommand(intent, flags, startId) + } +} diff --git a/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt b/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt new file mode 100644 index 000000000..03da60f59 --- /dev/null +++ b/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/ConnlibTest.kt @@ -0,0 +1,9 @@ +package dev.firezone.connlib + +import org.junit.Test + +import org.junit.Assert.* + +class LoggerTest { + // TODO +} diff --git a/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt b/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt new file mode 100644 index 000000000..958cf0b05 --- /dev/null +++ b/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/SessionTest.kt @@ -0,0 +1,9 @@ +package dev.firezone.connlib + +import org.junit.Test + +import org.junit.Assert.* + +class SessionTest { + // TODO +} diff --git a/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt b/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt new file mode 100644 index 000000000..ded39644c --- /dev/null +++ b/rust/connlib/clients/android/lib/src/test/java/dev/firezone/connlib/VpnServiceTest.kt @@ -0,0 +1,10 @@ + +package dev.firezone.connlib + +import org.junit.Test + +import org.junit.Assert.* + +class VpnServiceTest { + // TODO +} diff --git a/rust/connlib/clients/android/proguard-rules.pro b/rust/connlib/clients/android/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/rust/connlib/clients/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/rust/connlib/clients/android/settings.gradle.kts b/rust/connlib/clients/android/settings.gradle.kts new file mode 100644 index 000000000..c429c4fa8 --- /dev/null +++ b/rust/connlib/clients/android/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "Connlib" +include(":lib") diff --git a/rust/connlib/clients/android/src/lib.rs b/rust/connlib/clients/android/src/lib.rs new file mode 100644 index 000000000..1a80f8edf --- /dev/null +++ b/rust/connlib/clients/android/src/lib.rs @@ -0,0 +1,128 @@ +#[macro_use] +extern crate log; +extern crate android_logger; +extern crate jni; +use self::jni::JNIEnv; +use android_logger::Config; +use firezone_client_connlib::{ + Callbacks, Error, ErrorType, ResourceList, Session, TunnelAddresses, +}; +use jni::objects::{JClass, JObject, JString, JValue}; +use log::LevelFilter; + +/// This should be called once after the library is loaded by the system. +#[allow(non_snake_case)] +#[no_mangle] +pub extern "system" fn Java_dev_firezone_connlib_Logger_init(_: JNIEnv, _: JClass) { + #[cfg(debug_assertions)] + let level = LevelFilter::Trace; + #[cfg(not(debug_assertions))] + let level = LevelFilter::Warn; + + android_logger::init_once( + Config::default() + // Allow all log levels + .with_max_level(level) + .with_tag("connlib"), + ) +} + +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 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 { + let portal_url: String = env.get_string(&portal_url).unwrap().into(); + let portal_token: String = env.get_string(&portal_token).unwrap().into(); + + 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\"}]"; + let tunnel_addresses = env.new_string(tunnelAddressesJSON).unwrap(); + match env.call_method( + callback, + "onSetTunnelAddresses", + "(Ljava/lang/String;)Z", + &[JValue::from(&tunnel_addresses)], + ) { + Ok(res) => trace!("onSetTunnelAddresses returned {:?}", res), + Err(e) => error!("Failed to call setTunnelAddresses: {:?}", e), + } + + 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, +) -> bool { + if session_ptr.is_null() { + return false; + } + + 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, +) -> bool { + if session_ptr.is_null() { + return false; + } + + unsafe { (*session_ptr).bump_sockets() }; + + // TODO: See https://github.com/WireGuard/wireguard-apple/blob/2fec12a6e1f6e3460b6ee483aa00ad29cddadab1/Sources/WireGuardKitGo/api-apple.go#LL197C6-L197C50 + 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, +) -> bool { + if session_ptr.is_null() { + return false; + } + + 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 + true +} diff --git a/rust/connlib/clients/apple/.gitignore b/rust/connlib/clients/apple/.gitignore new file mode 100644 index 000000000..d35e9808b --- /dev/null +++ b/rust/connlib/clients/apple/.gitignore @@ -0,0 +1,31 @@ +.DS_Store + +# Rust +/target +Cargo.lock + +### Xcode ### +xcuserdata/ +/.build +/Packages +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +*.xcarchive/ +*.xcframework/ + +*.checksum.txt + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +## Xcode 8 and earlier +*.xcscmblueprint +*.xccheckout diff --git a/rust/connlib/clients/apple/Cargo.toml b/rust/connlib/clients/apple/Cargo.toml new file mode 100644 index 000000000..19ebac6d2 --- /dev/null +++ b/rust/connlib/clients/apple/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "connlib-apple" +version = "0.1.6" +edition = "2021" + +[build-dependencies] +swift-bridge-build = { git = "https://github.com/conectado/swift-bridge.git", branch = "fix-already-declared" } + +[dependencies] +libc = "0.2" +swift-bridge = { workspace = true } +firezone-client-connlib = { path = "../../libs/client" } + +[lib] +name = "connlib" +crate-type = ["staticlib"] diff --git a/rust/connlib/clients/apple/README.md b/rust/connlib/clients/apple/README.md new file mode 100644 index 000000000..cc0a8d357 --- /dev/null +++ b/rust/connlib/clients/apple/README.md @@ -0,0 +1,19 @@ +# Connlib Apple Wrapper + +Apple Package wrapper for Connlib distributed as a binary XCFramework for +inclusion in the Firezone Apple client. + +## Prerequisites + +1. Install [ stable rust ](https://www.rust-lang.org/tools/install) for your + platform +1. Install `llvm` from Homebrew: + +``` + +brew install llvm + +``` + +This fixes build issues with Apple's command line tools. See +https://github.com/briansmith/ring/issues/1374 diff --git a/rust/connlib/clients/apple/Sources/Connlib/Adapter.swift b/rust/connlib/clients/apple/Sources/Connlib/Adapter.swift new file mode 100644 index 000000000..4b8a36ccb --- /dev/null +++ b/rust/connlib/clients/apple/Sources/Connlib/Adapter.swift @@ -0,0 +1,298 @@ +// +// Adapter.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// +import Foundation +import NetworkExtension +import os.log + +public enum AdapterError: Error { + /// Failure to perform an operation in such state. + case invalidState + + /// Failure to set network settings. + case setNetworkSettings(Error) +} + +/// Enum representing internal state of the `WireGuardAdapter` +private enum State { + /// The tunnel is stopped + case stopped + + /// The tunnel is up and running + case started(_ handle: WrappedSession) + + /// The tunnel is temporarily shutdown due to device going offline + case temporaryShutdown +} + +// Loosely inspired from WireGuardAdapter from WireGuardKit +public class Adapter { + private let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") + + // Maintain a handle to the currently instantiated tunnel adapter 🤮 + public static var currentAdapter: Adapter? + + // Maintain a reference to the initialized callback handler + public static var callbackHandler: CallbackHandler? + + // Latest applied NETunnelProviderNetworkSettings + public var lastNetworkSettings: NEPacketTunnelNetworkSettings? + + /// Packet tunnel provider. + private weak var packetTunnelProvider: NEPacketTunnelProvider? + + /// Network routes monitor. + private var networkMonitor: NWPathMonitor? + + /// Private queue used to synchronize access to `WireGuardAdapter` members. + private let workQueue = DispatchQueue(label: "FirezoneAdapterWorkQueue") + + /// Adapter state. + private var state: State = .stopped + + public init(with packetTunnelProvider: NEPacketTunnelProvider) { + self.packetTunnelProvider = packetTunnelProvider + + // There must be a better way than making this a static class var... + Self.currentAdapter = self + Self.callbackHandler = CallbackHandler(adapter: self) + } + + deinit { + // Remove static var reference + Self.currentAdapter = nil + + // Cancel network monitor + networkMonitor?.cancel() + + // Shutdown the tunnel + if case .started(let wrappedSession) = self.state { + self.logger.log(level: .debug, "\(#function)") + wrappedSession.disconnect() + } + } + + /// Start the tunnel tunnel. + /// - Parameters: + /// - completionHandler: completion handler. + public func start(completionHandler: @escaping (AdapterError?) -> Void) throws { + workQueue.async { + guard case .stopped = self.state else { + completionHandler(.invalidState) + return + } + + let networkMonitor = NWPathMonitor() + networkMonitor.pathUpdateHandler = { [weak self] path in + self?.didReceivePathUpdate(path: path) + } + networkMonitor.start(queue: self.workQueue) + + do { + try self.setNetworkSettings(self.generateNetworkSettings(ipv4Routes: [], ipv6Routes: [])) + + self.state = .started( + WrappedSession.connect( + "http://localhost:4568", + "test-token", + Self.callbackHandler! + ) + ) + self.networkMonitor = networkMonitor + completionHandler(nil) + } catch let error as AdapterError { + networkMonitor.cancel() + completionHandler(error) + } catch { + fatalError() + } + } + } + + public func stop(completionHandler: @escaping (AdapterError?) -> Void) { + workQueue.async { + switch self.state { + case .started(let wrappedSession): + wrappedSession.disconnect() + case .temporaryShutdown: + break + + case .stopped: + completionHandler(.invalidState) + return + } + + self.networkMonitor?.cancel() + self.networkMonitor = nil + + self.state = .stopped + + completionHandler(nil) + } + } + + public func generateNetworkSettings( + addresses4: [String] = ["100.100.111.2"], addresses6: [String] = ["fd00:0222:2011:1111::2"], + ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route] + ) + -> NEPacketTunnelNetworkSettings + { + // The destination IP that connlib will assign our DNS proxy to. + let dnsSentinel = "1.1.1.1" + + // We can probably do better than this; see https://www.rfc-editor.org/info/rfc4821 + // But stick with something simple for now. 1280 is the minimum that will work for IPv6. + let mtu = 1280 + + // TODO: replace these with IPs returned from the connect call to portal + let subnetmask = "255.192.0.0" + let networkPrefixLength = NSNumber(value: 64) + + /* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for + * a tunnel to have no endpoint, or for there to be many endpoints, in + * which case, displaying a single one in settings doesn't really + * make sense. So, we fill it in with this placeholder, which is not + * a valid IP address that will actually route over the Internet. + */ + let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + let dnsSettings = NEDNSSettings(servers: [dnsSentinel]) + + // All DNS queries must first go through the tunnel's DNS + dnsSettings.matchDomains = [""] + networkSettings.dnsSettings = dnsSettings + networkSettings.mtu = NSNumber(value: mtu) + + let ipv4Settings = NEIPv4Settings( + addresses: addresses4, + subnetMasks: [subnetmask]) + ipv4Settings.includedRoutes = ipv4Routes + networkSettings.ipv4Settings = ipv4Settings + + let ipv6Settings = NEIPv6Settings( + addresses: addresses6, + networkPrefixLengths: [networkPrefixLength]) + ipv6Settings.includedRoutes = ipv6Routes + networkSettings.ipv6Settings = ipv6Settings + + return networkSettings + } + + public func setNetworkSettings(_ networkSettings: NEPacketTunnelNetworkSettings) throws { + var systemError: Error? + let condition = NSCondition() + + // Activate the condition + condition.lock() + defer { condition.unlock() } + + self.packetTunnelProvider?.setTunnelNetworkSettings(networkSettings) { error in + systemError = error + condition.signal() + } + + // Packet tunnel's `setTunnelNetworkSettings` times out in certain + // scenarios & never calls the given callback. + let setTunnelNetworkSettingsTimeout: TimeInterval = 5 // seconds + + if condition.wait(until: Date().addingTimeInterval(setTunnelNetworkSettingsTimeout)) { + if let systemError = systemError { + throw AdapterError.setNetworkSettings(systemError) + } + } + + // Save the latest applied network settings if there was no error. + if systemError != nil { + self.lastNetworkSettings = networkSettings + } + } + + /// Update runtime configuration. + /// - Parameters: + /// - ipv4Routes: IPv4 routes to send through the tunnel. + /// - ipv6Routes: IPv6 routes to send through the tunnel. + /// - completionHandler: completion handler. + public func update( + ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route], + completionHandler: @escaping (AdapterError?) -> Void + ) { + workQueue.async { + if case .stopped = self.state { + completionHandler(.invalidState) + return + } + + // Tell the system that the tunnel is going to reconnect using new WireGuard + // configuration. + // This will broadcast the `NEVPNStatusDidChange` notification to the GUI process. + self.packetTunnelProvider?.reasserting = true + defer { + self.packetTunnelProvider?.reasserting = false + } + + do { + try self.setNetworkSettings( + self.generateNetworkSettings(ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes)) + + switch self.state { + case .started(let wrappedSession): + self.state = .started(wrappedSession) + + case .temporaryShutdown: + self.state = .temporaryShutdown + + case .stopped: + fatalError() + } + + completionHandler(nil) + } catch let error as AdapterError { + completionHandler(error) + } catch { + fatalError() + } + } + } + + private func didReceivePathUpdate(path: Network.NWPath) { + #if os(macOS) + if case .started(let wrappedSession) = self.state { + wrappedSession.bumpSockets() + } + #elseif os(iOS) + switch self.state { + case .started(let wrappedSession): + if path.status == .satisfied { + wrappedSession.disableSomeRoamingForBrokenMobileSemantics() + wrappedSession.bumpSockets() + } else { + //self.logger.log(.debug, "Connectivity offline, pausing backend.") + self.state = .temporaryShutdown + wrappedSession.disconnect() + } + + case .temporaryShutdown: + guard path.status == .satisfied else { return } + + self.logger.log(level: .debug, "Connectivity online, resuming backend.") + + do { + try self.setNetworkSettings(self.lastNetworkSettings!) + + self.state = .started( + try WrappedSession.connect("http://localhost:4568", "test-token", Self.callbackHandler!) + ) + } catch { + self.logger.log(level: .debug, "Failed to restart backend: \(error.localizedDescription)") + } + + case .stopped: + // no-op + break + } + #else + #error("Unsupported") + #endif + } +} diff --git a/rust/connlib/clients/apple/Sources/Connlib/BridgingHeader.h b/rust/connlib/clients/apple/Sources/Connlib/BridgingHeader.h new file mode 100644 index 000000000..1a7814212 --- /dev/null +++ b/rust/connlib/clients/apple/Sources/Connlib/BridgingHeader.h @@ -0,0 +1,7 @@ +#ifndef BridgingHeader_h +#define BridgingHeader_h + +#include +#include + +#endif diff --git a/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift b/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift new file mode 100644 index 000000000..37c9cbec1 --- /dev/null +++ b/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift @@ -0,0 +1,93 @@ +// +// Callbacks.swift +// connlib +// +// Created by Jamil Bou Kheir on 4/3/23. +// + +import NetworkExtension +import os.log + +public protocol CallbackHandlerDelegate: AnyObject { + func didUpdateResources(_ resourceList: ResourceList) +} + +public class CallbackHandler { + // TODO: Add a table view property here to update? + var adapter: Adapter? + public weak var delegate: CallbackHandlerDelegate? + + init(adapter: Adapter) { + self.adapter = adapter + } + + func onUpdateResources(resourceList: ResourceList) -> Bool { + + // If there's any entity that assigned itself as this callbackHandler's delegate, it will be called every time this `onUpdateResources` method is, allowing that entity to react to resource updates and do whatever they want. + + delegate?.didUpdateResources(resourceList) + + let addresses4 = + self.adapter?.lastNetworkSettings?.ipv4Settings?.addresses ?? ["100.100.111.2"] + let addresses6 = + self.adapter?.lastNetworkSettings?.ipv6Settings?.addresses ?? [ + "fd00:0222:2021:1111::2" + ] + + // TODO: Use actual passed in resources to achieve split tunnel + let ipv4Routes = [NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0")] + let ipv6Routes = [ + NEIPv6Route(destinationAddress: "fd00:0222:2021:1111::0", networkPrefixLength: 64) + ] + + return setTunnelSettingsKeepingSomeExisting( + addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes + ) + } + + func onSetTunnelAddresses(tunnelAddresses: TunnelAddresses) -> Bool { + let addresses4 = [tunnelAddresses.address4.toString()] + let addresses6 = [tunnelAddresses.address6.toString()] + let ipv4Routes = + Adapter.currentAdapter?.lastNetworkSettings?.ipv4Settings?.includedRoutes ?? [] + let ipv6Routes = + Adapter.currentAdapter?.lastNetworkSettings?.ipv6Settings?.includedRoutes ?? [] + + return setTunnelSettingsKeepingSomeExisting( + addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes + ) + } + + private func setTunnelSettingsKeepingSomeExisting( + addresses4: [String], addresses6: [String], ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route] + ) -> Bool { + let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel") + + if self.adapter != nil { + do { + /* If the tunnel interface addresses are being updated, it's impossible for the tunnel to + stay up due to the way WireGuard works. Still, we try not to change the tunnel's routes + here Just In Case™. + */ + try self.adapter!.setNetworkSettings( + self.adapter!.generateNetworkSettings( + addresses4: addresses4, + addresses6: addresses6, + ipv4Routes: ipv4Routes, + ipv6Routes: ipv6Routes + ) + ) + + return true + } catch let error { + logger.log(level: .debug, "Error setting adapter settings: \(String(describing: error))") + + return false + } + } else { + logger.log(level: .debug, "Adapter not initialized!") + + return false + } + } +} diff --git a/rust/connlib/clients/apple/Sources/Connlib/Generated/.gitignore b/rust/connlib/clients/apple/Sources/Connlib/Generated/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/rust/connlib/clients/apple/Sources/Connlib/Generated/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/rust/connlib/clients/apple/Sources/Connlib/connlib.h b/rust/connlib/clients/apple/Sources/Connlib/connlib.h new file mode 100644 index 000000000..cc7c7b14a --- /dev/null +++ b/rust/connlib/clients/apple/Sources/Connlib/connlib.h @@ -0,0 +1,18 @@ +// +// connlib.h +// connlib +// +// Created by Jamil Bou Kheir on 4/3/23. +// + +#import + +//! Project version number for connlib. +FOUNDATION_EXPORT double connlibVersionNumber; + +//! Project version string for connlib. +FOUNDATION_EXPORT const unsigned char connlibVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import diff --git a/rust/connlib/clients/apple/Tests/connlibTests/.gitkeep b/rust/connlib/clients/apple/Tests/connlibTests/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/rust/connlib/clients/apple/build-rust.sh b/rust/connlib/clients/apple/build-rust.sh new file mode 100755 index 000000000..f97b3ce13 --- /dev/null +++ b/rust/connlib/clients/apple/build-rust.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +################################################## +# We call this from an Xcode run script. +################################################## + +set -ex + +if [[ -z "$PROJECT_DIR" ]]; then + echo "Must provide PROJECT_DIR environment variable set to the Xcode project directory." 1>&2 + exit 1 +fi + +cd $PROJECT_DIR + +# Default PLATFORM_NAME to macosx if not set. +: "${PLATFORM_NAME:=macosx}" + + +export PATH="$HOME/.cargo/bin:$PATH" + +base_dir=$(xcrun --sdk $PLATFORM_NAME --show-sdk-path) + +# See https://github.com/briansmith/ring/issues/1332 +export LIBRARY_PATH="${base_dir}/usr/lib" +export INCLUDE_PATH="${base_dir}/usr/include" +export CFLAGS="-L ${LIBRARY_PATH} -I ${INCLUDE_PATH}" +export RUSTFLAGS="-C link-arg=-F$base_dir/System/Library/Frameworks" + +TARGETS="" +if [[ "$PLATFORM_NAME" = "macosx" ]]; then + TARGETS="aarch64-apple-darwin,x86_64-apple-darwin" +else + if [[ "$PLATFORM_NAME" = "iphonesimulator" ]]; then + TARGETS="aarch64-apple-ios-sim,x86_64-apple-ios" + else + if [[ "$PLATFORM_NAME" = "iphoneos" ]]; then + TARGETS="aarch64-apple-ios" + else + echo "Unsupported platform: $PLATFORM_NAME" + exit 1 + fi + fi +fi + +# if [ $ENABLE_PREVIEWS == "NO" ]; then + + if [[ $CONFIGURATION == "Release" ]]; then + echo "BUILDING FOR RELEASE ($TARGETS)" + + cargo lipo --release --manifest-path ./Cargo.toml --targets $TARGETS + else + echo "BUILDING FOR DEBUG ($TARGETS)" + + cargo lipo --manifest-path ./Cargo.toml --targets $TARGETS + fi + +# else +# echo "Skipping the script because of preview mode" +# fi diff --git a/rust/connlib/clients/apple/build-xcframework.sh b/rust/connlib/clients/apple/build-xcframework.sh new file mode 100755 index 000000000..1ac275209 --- /dev/null +++ b/rust/connlib/clients/apple/build-xcframework.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -ex + +for sdk in macosx iphoneos iphonesimulator; do + echo "Building for $sdk" + + xcodebuild archive \ + -scheme Connlib \ + -destination "generic/platform=$sdk" \ + -sdk $sdk \ + -archivePath ./connlib-$sdk \ + SKIP_INSTALL=NO \ + BUILD_LIBRARY_FOR_DISTRIBUTION=YES +done + +xcodebuild -create-xcframework \ + -framework ./connlib-iphoneos.xcarchive/Products/Library/Frameworks/connlib.framework \ + -framework ./connlib-iphonesimulator.xcarchive/Products/Library/Frameworks/connlib.framework \ + -framework ./connlib-macosx.xcarchive/Products/Library/Frameworks/connlib.framework \ + -output ./Connlib.xcframework + +echo "Build successful. Removing temporary archives" +rm -rf ./connlib-iphoneos.xcarchive +rm -rf ./connlib-iphonesimulator.xcarchive +rm -rf ./connlib-macosx.xcarchive + +echo "Computing checksum" +touch Package.swift +zip -r -y Connlib.xcframework.zip Connlib.xcframework +swift package compute-checksum Connlib.xcframework.zip > Connlib.xcframework.zip.checksum.txt + +rm Package.swift +rm -rf Connlib.xcframework diff --git a/rust/connlib/clients/apple/build.rs b/rust/connlib/clients/apple/build.rs new file mode 100644 index 000000000..ddc9a9ccb --- /dev/null +++ b/rust/connlib/clients/apple/build.rs @@ -0,0 +1,14 @@ +const XCODE_CONFIGURATION_ENV: &str = "CONFIGURATION"; + +fn main() { + let out_dir = "Sources/Connlib/Generated"; + + let bridges = vec!["src/lib.rs"]; + for path in &bridges { + println!("cargo:rerun-if-changed={}", path); + } + println!("cargo:rerun-if-env-changed={}", XCODE_CONFIGURATION_ENV); + + swift_bridge_build::parse_bridges(bridges) + .write_all_concatenated(out_dir, env!("CARGO_PKG_NAME")); +} diff --git a/rust/connlib/clients/apple/connlib.xcodeproj/project.pbxproj b/rust/connlib/clients/apple/connlib.xcodeproj/project.pbxproj new file mode 100644 index 000000000..cfa75143c --- /dev/null +++ b/rust/connlib/clients/apple/connlib.xcodeproj/project.pbxproj @@ -0,0 +1,466 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 8D46EDDF29DBC29800FF01CA /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D46EDD729DBC29800FF01CA /* Adapter.swift */; }; + 8D46EDE029DBC29800FF01CA /* CallbackHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D46EDD829DBC29800FF01CA /* CallbackHandler.swift */; }; + 8D967B2B29DBA064000B9D58 /* libconnlib.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D967B2A29DBA03F000B9D58 /* libconnlib.a */; }; + 8DA207F829DBD80C00703A4A /* connlib-apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA207F329DBD80C00703A4A /* connlib-apple.swift */; }; + 8DA207F929DBD80C00703A4A /* connlib-apple.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA207F429DBD80C00703A4A /* connlib-apple.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DA207FA29DBD80C00703A4A /* .gitignore in Resources */ = {isa = PBXBuildFile; fileRef = 8DA207F529DBD80C00703A4A /* .gitignore */; }; + 8DA207FC29DBD80C00703A4A /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA207F729DBD80C00703A4A /* SwiftBridgeCore.swift */; }; + 8DA207FD29DBD86100703A4A /* SwiftBridgeCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 8DA207F629DBD80C00703A4A /* SwiftBridgeCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DA207FE29DBD86100703A4A /* connlib.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D4BADD129DBD6CC00940F0D /* connlib.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8DA207FF29DBD86100703A4A /* BridgingHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D46EDD629DBC29800FF01CA /* BridgingHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8D209DCE29DBE96B00B68D27 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.4.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; + 8D46EDD629DBC29800FF01CA /* BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; + 8D46EDD729DBC29800FF01CA /* Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = ""; }; + 8D46EDD829DBC29800FF01CA /* CallbackHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallbackHandler.swift; sourceTree = ""; }; + 8D4BADD129DBD6CC00940F0D /* connlib.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = connlib.h; sourceTree = ""; }; + 8D7D983129DB8437007B8198 /* connlib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = connlib.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8D967B2629DB9A3B000B9D58 /* build-rust.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "build-rust.sh"; sourceTree = ""; }; + 8D967B2A29DBA03F000B9D58 /* libconnlib.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libconnlib.a; path = target/universal/debug/libconnlib.a; sourceTree = ""; }; + 8DA207F329DBD80C00703A4A /* connlib-apple.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "connlib-apple.swift"; sourceTree = ""; }; + 8DA207F429DBD80C00703A4A /* connlib-apple.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "connlib-apple.h"; sourceTree = ""; }; + 8DA207F529DBD80C00703A4A /* .gitignore */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .gitignore; sourceTree = ""; }; + 8DA207F629DBD80C00703A4A /* SwiftBridgeCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftBridgeCore.h; sourceTree = ""; }; + 8DA207F729DBD80C00703A4A /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftBridgeCore.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D7D982E29DB8437007B8198 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D967B2B29DBA064000B9D58 /* libconnlib.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8D46EDCE29DBC29800FF01CA /* Connlib */ = { + isa = PBXGroup; + children = ( + 8DA207F129DBD80C00703A4A /* Generated */, + 8D4BADD129DBD6CC00940F0D /* connlib.h */, + 8D46EDD629DBC29800FF01CA /* BridgingHeader.h */, + 8D46EDD729DBC29800FF01CA /* Adapter.swift */, + 8D46EDD829DBC29800FF01CA /* CallbackHandler.swift */, + ); + path = Connlib; + sourceTree = ""; + }; + 8D7D982729DB8437007B8198 = { + isa = PBXGroup; + children = ( + 8D967B3E29DBA34C000B9D58 /* Tests */, + 8D967B3D29DBA344000B9D58 /* Sources */, + 8D967B2629DB9A3B000B9D58 /* build-rust.sh */, + 8D7D983229DB8437007B8198 /* Products */, + 8D967B2929DBA03F000B9D58 /* Frameworks */, + ); + sourceTree = ""; + }; + 8D7D983229DB8437007B8198 /* Products */ = { + isa = PBXGroup; + children = ( + 8D7D983129DB8437007B8198 /* connlib.framework */, + ); + name = Products; + sourceTree = ""; + }; + 8D967B2929DBA03F000B9D58 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8D209DCE29DBE96B00B68D27 /* Security.framework */, + 8D967B2A29DBA03F000B9D58 /* libconnlib.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8D967B3D29DBA344000B9D58 /* Sources */ = { + isa = PBXGroup; + children = ( + 8D46EDCE29DBC29800FF01CA /* Connlib */, + ); + path = Sources; + sourceTree = ""; + }; + 8D967B3E29DBA34C000B9D58 /* Tests */ = { + isa = PBXGroup; + children = ( + ); + path = Tests; + sourceTree = ""; + }; + 8DA207F129DBD80C00703A4A /* Generated */ = { + isa = PBXGroup; + children = ( + 8DA207F229DBD80C00703A4A /* connlib-apple */, + 8DA207F529DBD80C00703A4A /* .gitignore */, + 8DA207F629DBD80C00703A4A /* SwiftBridgeCore.h */, + 8DA207F729DBD80C00703A4A /* SwiftBridgeCore.swift */, + ); + path = Generated; + sourceTree = ""; + }; + 8DA207F229DBD80C00703A4A /* connlib-apple */ = { + isa = PBXGroup; + children = ( + 8DA207F329DBD80C00703A4A /* connlib-apple.swift */, + 8DA207F429DBD80C00703A4A /* connlib-apple.h */, + ); + path = "connlib-apple"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 8D7D982C29DB8437007B8198 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DA207F929DBD80C00703A4A /* connlib-apple.h in Headers */, + 8DA207FD29DBD86100703A4A /* SwiftBridgeCore.h in Headers */, + 8DA207FE29DBD86100703A4A /* connlib.h in Headers */, + 8DA207FF29DBD86100703A4A /* BridgingHeader.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 8D7D983029DB8437007B8198 /* connlib */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8D7D984529DB8437007B8198 /* Build configuration list for PBXNativeTarget "connlib" */; + buildPhases = ( + 8D7D982C29DB8437007B8198 /* Headers */, + 8D967B2829DB9A91000B9D58 /* ShellScript */, + 8D7D982D29DB8437007B8198 /* Sources */, + 8D7D982E29DB8437007B8198 /* Frameworks */, + 8D7D982F29DB8437007B8198 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = connlib; + productName = connlib; + productReference = 8D7D983129DB8437007B8198 /* connlib.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8D7D982829DB8437007B8198 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + 8D7D983029DB8437007B8198 = { + CreatedOnToolsVersion = 14.3; + }; + }; + }; + buildConfigurationList = 8D7D982B29DB8437007B8198 /* Build configuration list for PBXProject "connlib" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8D7D982729DB8437007B8198; + productRefGroup = 8D7D983229DB8437007B8198 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8D7D983029DB8437007B8198 /* connlib */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D7D982F29DB8437007B8198 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DA207FA29DBD80C00703A4A /* .gitignore in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 8D967B2829DB9A91000B9D58 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "./build-rust.sh\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D7D982D29DB8437007B8198 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8DA207F829DBD80C00703A4A /* connlib-apple.swift in Sources */, + 8D46EDDF29DBC29800FF01CA /* Adapter.swift in Sources */, + 8D46EDE029DBC29800FF01CA /* CallbackHandler.swift in Sources */, + 8DA207FC29DBD80C00703A4A /* SwiftBridgeCore.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8D7D984329DB8437007B8198 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8D7D984429DB8437007B8198 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 8D7D984629DB8437007B8198 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = "$(PROJECT_DIR)/target/universal/debug"; + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.connlib; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8D7D984729DB8437007B8198 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; + LD_RUNPATH_SEARCH_PATHS = ( + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = ( + "@executable_path/../Frameworks", + "@loader_path/Frameworks", + ); + LIBRARY_SEARCH_PATHS = ( + "$(PROJECT_DIR)/target/universal/release", + "$(PROJECT_DIR)/target/universal/debug", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++20"; + PRODUCT_BUNDLE_IDENTIFIER = dev.firezone.connlib; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = auto; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8D7D982B29DB8437007B8198 /* Build configuration list for PBXProject "connlib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8D7D984329DB8437007B8198 /* Debug */, + 8D7D984429DB8437007B8198 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 8D7D984529DB8437007B8198 /* Build configuration list for PBXNativeTarget "connlib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8D7D984629DB8437007B8198 /* Debug */, + 8D7D984729DB8437007B8198 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8D7D982829DB8437007B8198 /* Project object */; +} diff --git a/rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/rust/connlib/clients/apple/connlib.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/rust/connlib/clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme b/rust/connlib/clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme new file mode 100644 index 000000000..6f69e4a37 --- /dev/null +++ b/rust/connlib/clients/apple/connlib.xcodeproj/xcshareddata/xcschemes/Connlib.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/rust/connlib/clients/apple/src/lib.rs b/rust/connlib/clients/apple/src/lib.rs new file mode 100644 index 000000000..c539b79ef --- /dev/null +++ b/rust/connlib/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/rust/connlib/clients/headless/Cargo.toml b/rust/connlib/clients/headless/Cargo.toml new file mode 100644 index 000000000..c90370320 --- /dev/null +++ b/rust/connlib/clients/headless/Cargo.toml @@ -0,0 +1,14 @@ +[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" } +url = { version = "2.3.1", default-features = false } +tracing-subscriber = { version = "0.3" } +tracing = { version = "0.1" } +anyhow = { version = "1.0" } +clap = { version = "4.3", features = ["derive"] } diff --git a/rust/connlib/clients/headless/src/main.rs b/rust/connlib/clients/headless/src/main.rs new file mode 100644 index 000000000..fb8c3c3f2 --- /dev/null +++ b/rust/connlib/clients/headless/src/main.rs @@ -0,0 +1,70 @@ +use anyhow::{Context, Result}; +use clap::Parser; +use std::str::FromStr; + +use firezone_client_connlib::{ + get_user_agent, 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}"), + } + } +} + +const URL_ENV_VAR: &str = "FZ_URL"; +const SECRET_ENV_VAR: &str = "FZ_SECRET"; + +fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let cli = Cli::parse(); + if cli.print_agent { + println!("{}", get_user_agent()); + return Ok(()); + } + + // TODO: allow passing as arg vars + let url = parse_env_var::(URL_ENV_VAR)?; + let secret = parse_env_var::(SECRET_ENV_VAR)?; + // TODO: This is disgusting + let mut session = Session::::connect::(url, secret).unwrap(); + tracing::info!("Started new session"); + session.wait_for_ctrl_c().unwrap(); + session.disconnect(); + Ok(()) +} + +fn parse_env_var(key: &str) -> Result +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + let res = std::env::var(key) + .with_context(|| format!("`{key}` env variable is unset"))? + .parse() + .with_context(|| format!("failed to parse {key} env variable"))?; + + Ok(res) +} + +// probably will change this to a subcommand in the future +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + #[arg(short, long)] + print_agent: bool, +} diff --git a/rust/connlib/gateway/Cargo.toml b/rust/connlib/gateway/Cargo.toml new file mode 100644 index 000000000..eb9e07c6c --- /dev/null +++ b/rust/connlib/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" } +url = { version = "2.3.1", default-features = false } +tracing-subscriber = { version = "0.3" } +tracing = { version = "0.1" } +anyhow = { version = "1.0" } diff --git a/rust/connlib/gateway/src/main.rs b/rust/connlib/gateway/src/main.rs new file mode 100644 index 000000000..d94f5e4eb --- /dev/null +++ b/rust/connlib/gateway/src/main.rs @@ -0,0 +1,54 @@ +use anyhow::{Context, Result}; +use std::str::FromStr; + +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}"), + } + } +} + +const URL_ENV_VAR: &str = "FZ_URL"; +const SECRET_ENV_VAR: &str = "FZ_SECRET"; + +fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + // TODO: allow passing as arg vars + let url = parse_env_var::(URL_ENV_VAR)?; + let secret = parse_env_var::(SECRET_ENV_VAR)?; + // TODO: This is disgusting + let mut session = Session::::connect::(url, secret).unwrap(); + session.wait_for_ctrl_c().unwrap(); + session.disconnect(); + Ok(()) +} + +fn parse_env_var(key: &str) -> Result +where + T: FromStr, + T::Err: std::error::Error + Send + Sync + 'static, +{ + let res = std::env::var(key) + .with_context(|| format!("`{key}` env variable is unset"))? + .parse() + .with_context(|| format!("failed to parse {key} env variable"))?; + + Ok(res) +} diff --git a/rust/connlib/libs/client/Cargo.toml b/rust/connlib/libs/client/Cargo.toml new file mode 100644 index 000000000..9590b55c9 --- /dev/null +++ b/rust/connlib/libs/client/Cargo.toml @@ -0,0 +1,16 @@ +[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"] } +boringtun = { workspace = true } + +[dev-dependencies] +serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/rust/connlib/libs/client/src/control.rs b/rust/connlib/libs/client/src/control.rs new file mode 100644 index 000000000..ee2901966 --- /dev/null +++ b/rust/connlib/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 boringtun::x25519::StaticSecret; +use libs_common::{ + 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 initialize 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/rust/connlib/libs/client/src/lib.rs b/rust/connlib/libs/client/src/lib.rs new file mode 100644 index 000000000..f85827552 --- /dev/null +++ b/rust/connlib/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}, + get_user_agent, Callbacks, Error, ResourceList, TunnelAddresses, +}; +use messages::Messages; +use messages::ReplyMessages; diff --git a/rust/connlib/libs/client/src/messages.rs b/rust/connlib/libs/client/src/messages.rs new file mode 100644 index 000000000..df8bb3c2f --- /dev/null +++ b/rust/connlib/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 received +// 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/rust/connlib/libs/common/Cargo.toml b/rust/connlib/libs/common/Cargo.toml new file mode 100644 index 000000000..aad27970c --- /dev/null +++ b/rust/connlib/libs/common/Cargo.toml @@ -0,0 +1,36 @@ +[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.8" } +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 } +ip_network = { version = "0.4", default-features = false, features = ["serde"] } +boringtun = { workspace = true } +os_info = { version = "3", default-features = false } + +macros = { path = "../../macros" } + +[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] +swift-bridge = { workspace = true } + +[target.'cfg(target_os = "linux")'.dependencies] +rtnetlink = { version = "0.12", default-features = false, features = ["tokio_socket"] } diff --git a/rust/connlib/libs/common/src/control.rs b/rust/connlib/libs/common/src/control.rs new file mode 100644 index 000000000..5a72cfbf1 --- /dev/null +++ b/rust/connlib/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::{get_user_agent, 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", get_user_agent()) + .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) = receiver.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 task 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!("Received 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 received 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 + /// receive 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/rust/connlib/libs/common/src/error.rs b/rust/connlib/libs/common/src/error.rs new file mode 100644 index 000000000..6daf38838 --- /dev/null +++ b/rust/connlib/libs/common/src/error.rs @@ -0,0 +1,119 @@ +//! 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 error + #[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, + #[cfg(target_os = "linux")] + #[error(transparent)] + NetlinkError(rtnetlink::Error), + /// Io translation of netlink error + /// The IO version is easier to interpret + /// We maintain a different variant from the standard IO for this to keep more context + #[error("IO netlink error: {0}")] + NetlinkErrorIo(std::io::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 documentation. +pub use swift_ffi::SwiftConnlibError; + +#[cfg(target_os = "linux")] +impl From for ConnlibError { + fn from(err: rtnetlink::Error) -> Self { + match err { + rtnetlink::Error::NetlinkError(err) => Self::NetlinkErrorIo(err.to_io()), + err => Self::NetlinkError(err), + } + } +} + +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/rust/connlib/libs/common/src/error_type.rs b/rust/connlib/libs/common/src/error_type.rs new file mode 100644 index 000000000..7f411c87f --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/common/src/lib.rs b/rust/connlib/libs/common/src/lib.rs new file mode 100644 index 000000000..0b47104d7 --- /dev/null +++ b/rust/connlib/libs/common/src/lib.rs @@ -0,0 +1,29 @@ +//! 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 error::ConnlibError as Error; +pub use error::Result; + +pub use session::{Callbacks, ControlSession, ResourceList, Session, TunnelAddresses}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const LIB_NAME: &str = "connlib"; + +pub fn get_user_agent() -> String { + let info = os_info::get(); + let os_type = info.os_type(); + let os_version = info.version(); + let lib_version = VERSION; + let lib_name = LIB_NAME; + format!("{os_type}/{os_version} {lib_name}/{lib_version}") +} diff --git a/rust/connlib/libs/common/src/messages.rs b/rust/connlib/libs/common/src/messages.rs new file mode 100644 index 000000000..f2c1470f5 --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/common/src/messages/key.rs b/rust/connlib/libs/common/src/messages/key.rs new file mode 100644 index 000000000..9499ce814 --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/common/src/session.rs b/rust/connlib/libs/common/src/session.rs new file mode 100644 index 000000000..f48e0397f --- /dev/null +++ b/rust/connlib/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, maintains 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`: Whether 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 compatibility + 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!("Received message: {msg:?}"); + if let Err(e) = sender.send(msg).await { + tracing::warn!("Received a message after handler already closed: {e}. Probably message received 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/rust/connlib/libs/gateway/Cargo.toml b/rust/connlib/libs/gateway/Cargo.toml new file mode 100644 index 000000000..8e1fbb57a --- /dev/null +++ b/rust/connlib/libs/gateway/Cargo.toml @@ -0,0 +1,16 @@ +[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"] } +boringtun = { workspace = true } + +[dev-dependencies] +serde_json = { version = "1.0", default-features = false, features = ["std"] } diff --git a/rust/connlib/libs/gateway/src/control.rs b/rust/connlib/libs/gateway/src/control.rs new file mode 100644 index 000000000..6537be258 --- /dev/null +++ b/rust/connlib/libs/gateway/src/control.rs @@ -0,0 +1,159 @@ +use std::{sync::Arc, time::Duration}; + +use firezone_tunnel::{ControlSignal, Tunnel}; +use boringtun::x25519::StaticSecret; +use libs_common::{ + 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/rust/connlib/libs/gateway/src/lib.rs b/rust/connlib/libs/gateway/src/lib.rs new file mode 100644 index 000000000..6fa63b7cc --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/gateway/src/messages.rs b/rust/connlib/libs/gateway/src/messages.rs new file mode 100644 index 000000000..5ce40cf0b --- /dev/null +++ b/rust/connlib/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 received +// 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/rust/connlib/libs/tunnel/Cargo.toml b/rust/connlib/libs/tunnel/Cargo.toml new file mode 100644 index 000000000..6840336bd --- /dev/null +++ b/rust/connlib/libs/tunnel/Cargo.toml @@ -0,0 +1,40 @@ +[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 } +boringtun = { workspace = true } + +# TODO: research replacing for https://github.com/algesten/str0m +webrtc = { version = "0.8" } + +# 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/rust/connlib/libs/tunnel/src/control_protocol.rs b/rust/connlib/libs/tunnel/src/control_protocol.rs new file mode 100644 index 000000000..eedc05c63 --- /dev/null +++ b/rust/connlib/libs/tunnel/src/control_protocol.rs @@ -0,0 +1,314 @@ +use boringtun::{ + noise::Tunn, + x25519::{PublicKey, StaticSecret}, +}; +use std::sync::Arc; + +use libs_common::{ + 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 receiving 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/rust/connlib/libs/tunnel/src/device_channel_unix.rs b/rust/connlib/libs/tunnel/src/device_channel_unix.rs new file mode 100644 index 000000000..2168a4c18 --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/tunnel/src/device_channel_win.rs b/rust/connlib/libs/tunnel/src/device_channel_win.rs new file mode 100644 index 000000000..2edf498f8 --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/tunnel/src/index.rs b/rust/connlib/libs/tunnel/src/index.rs new file mode 100644 index 000000000..f58fcb9b6 --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/tunnel/src/lib.rs b/rust/connlib/libs/tunnel/src/lib.rs new file mode 100644 index 000000000..9f91aa3fa --- /dev/null +++ b/rust/connlib/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 boringtun::{ + noise::{ + errors::WireGuardError, handshake::parse_handshake_anon, rate_limiter::RateLimiter, + Packet, Tunn, TunnResult, + }, + x25519::{PublicKey, StaticSecret}, +}; +use libs_common::{ + 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/rust/connlib/libs/tunnel/src/peer.rs b/rust/connlib/libs/tunnel/src/peer.rs new file mode 100644 index 000000000..ac38ec64a --- /dev/null +++ b/rust/connlib/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 boringtun::noise::{Tunn, TunnResult}; +use libs_common::{ + 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/rust/connlib/libs/tunnel/src/resource_table.rs b/rust/connlib/libs/tunnel/src/resource_table.rs new file mode 100644 index 000000000..72e581846 --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/tunnel/src/tun_android.rs b/rust/connlib/libs/tunnel/src/tun_android.rs new file mode 100644 index 000000000..d47d94a1d --- /dev/null +++ b/rust/connlib/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/rust/connlib/libs/tunnel/src/tun_darwin.rs b/rust/connlib/libs/tunnel/src/tun_darwin.rs new file mode 100644 index 000000000..10936e8d7 --- /dev/null +++ b/rust/connlib/libs/tunnel/src/tun_darwin.rs @@ -0,0 +1,284 @@ +use ip_network::IpNetwork; +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 async 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 async 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 necessary in theory but it's correct! +impl IfaceConfig { + #[tracing::instrument(level = "trace", skip(self))] + pub async fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + // TODO + + Ok(()) + } + + pub async fn up(&mut self) -> Result<()> { + // TODO + Ok(()) + } + + pub async fn add_route(&mut self, route: IpNetwork) -> Result<()> { + todo!() + } +} + +fn get_last_error() -> Error { + Error::Io(io::Error::last_os_error()) +} diff --git a/rust/connlib/libs/tunnel/src/tun_linux.rs b/rust/connlib/libs/tunnel/src/tun_linux.rs new file mode 100644 index 000000000..158804688 --- /dev/null +++ b/rust/connlib/libs/tunnel/src/tun_linux.rs @@ -0,0 +1,272 @@ +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?; + + // TODO: Disable this when ipv6 is disabled + 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/rust/connlib/libs/tunnel/src/tun_win.rs b/rust/connlib/libs/tunnel/src/tun_win.rs new file mode 100644 index 000000000..9edce1c12 --- /dev/null +++ b/rust/connlib/libs/tunnel/src/tun_win.rs @@ -0,0 +1,22 @@ +use super::InterfaceConfig; +use ip_network::IpNetwork; +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 async fn set_iface_config(&mut self, _config: &InterfaceConfig) -> Result<()> { + todo!() + } + + pub async fn up(&mut self) -> Result<()> { + todo!() + } + + pub async fn add_route(&mut self, route: IpNetwork) -> Result<()> { + todo!() + } +} diff --git a/rust/connlib/macros/Cargo.toml b/rust/connlib/macros/Cargo.toml new file mode 100644 index 000000000..5326335be --- /dev/null +++ b/rust/connlib/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/rust/connlib/macros/src/lib.rs b/rust/connlib/macros/src/lib.rs new file mode 100644 index 000000000..15044226b --- /dev/null +++ b/rust/connlib/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 controlling 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 proper 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/rust/relay.Dockerfile b/rust/relay.Dockerfile deleted file mode 100644 index fddb43a31..000000000 --- a/rust/relay.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# syntax=docker/dockerfile:1.5-labs -FROM rust:1.70-slim as builder - -WORKDIR /workspace -ADD . . -RUN --mount=type=cache,target=./target \ - --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/rustup \ - rustup target add x86_64-unknown-linux-musl && \ - cargo build --release --bin relay --target x86_64-unknown-linux-musl - -RUN --mount=type=cache,target=./target \ - mv ./target/x86_64-unknown-linux-musl/release/relay /usr/local/bin/relay - -FROM scratch -COPY --from=builder /usr/local/bin/relay /usr/local/bin/relay -ENV RUST_BACKTRACE=1 - -EXPOSE 3478/udp -EXPOSE 49152-65535/udp - -# This purposely does not include an `init` process. Use `docker run --init` for proper signal handling. -ENTRYPOINT ["relay"] diff --git a/rust/relay/Cargo.toml b/rust/relay/Cargo.toml index 9b52ddb1b..d5523a44c 100644 --- a/rust/relay/Cargo.toml +++ b/rust/relay/Cargo.toml @@ -29,7 +29,7 @@ url = "2.4.0" serde = { version = "1.0.163", features = ["derive"] } [dev-dependencies] -webrtc = "0.7.2" +webrtc = { version = "0.8" } redis = { version = "0.23.0", default-features = false, features = ["tokio-comp"] } difference = "2.0.0" diff --git a/rust/relay/src/main.rs b/rust/relay/src/main.rs index 5f078940a..e209a9f20 100644 --- a/rust/relay/src/main.rs +++ b/rust/relay/src/main.rs @@ -34,6 +34,9 @@ struct Args { /// If omitted, the relay server will start immediately, otherwise we first log on and wait for the `init` message. #[arg(long, env)] portal_ws_url: Option, + /// Token generated by the portal to authorize websocket connection + #[arg(long, env)] + portal_token: Option, /// Whether to allow connecting to the portal over an insecure connection. #[arg(long)] allow_insecure_ws: bool, @@ -44,6 +47,28 @@ struct Args { rng_seed: Option, } +// TODO: Code repetition from common +fn get_websocket_path(mut url: Url, token: String, ipv4: Ipv4Addr) -> Result { + { + let mut paths = url + .path_segments_mut() + .map_err(|_| anyhow!("invalid url")) + .context("No url base found while trying to format the portal's URL")?; + paths.pop_if_empty(); + paths.push("relay"); + paths.push("websocket"); + } + + { + let mut query_pairs = url.query_pairs_mut(); + query_pairs.clear(); + query_pairs.append_pair("token", &token); + query_pairs.append_pair("ipv4", &ipv4.to_string()); + } + + Ok(url) +} + #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); @@ -70,7 +95,13 @@ async fn main() -> Result<()> { .append_pair("ipv4", &args.listen_ip4_addr.to_string()); let mut channel = PhoenixChannel::::connect( - portal_url.clone(), + get_websocket_path( + portal_url.clone(), + args.portal_token.ok_or(anyhow!( + "PORTAL_TOKEN must be set if you're setting a PORTAL_WS_URL" + ))?, + args.listen_ip4_addr, + )?, format!("relay/{}", env!("CARGO_PKG_VERSION")), ) .await diff --git a/rust/rust-toolchain.toml b/rust/rust-toolchain.toml index 2989cbb2e..42c6afeba 100644 --- a/rust/rust-toolchain.toml +++ b/rust/rust-toolchain.toml @@ -1,4 +1,16 @@ [toolchain] channel = "1.70.0" components = ["rustfmt", "clippy"] -targets = ["x86_64-unknown-linux-musl"] +targets = [ + "x86_64-unknown-linux-musl", + "x86_64-linux-android", + "arm-linux-androideabi", + "aarch64-linux-android", + "armv7-linux-androideabi", + "i686-linux-android", + "aarch64-apple-ios-sim", + "aarch64-apple-ios", + "aarch64-apple-darwin", + "x86_64-apple-ios", + "x86_64-apple-darwin", +]