From 4d84e1f12ee847bb440d40ff6bbc4c577f2fc607 Mon Sep 17 00:00:00 2001 From: Jamil Date: Fri, 25 Aug 2023 15:44:08 -0700 Subject: [PATCH] chore(connlib): Add external ID to FFI, return `fd` in `on_set_interface_config` (#1945) (Supersedes #1944) * Fixes https://github.com/firezone/product/issues/649 * Passes `dns_fallback_strategy` over FFI (these are hardcoded for now) * Incorporates @conectado 's #1944 and cleans up a few places `fd` was still passed Draft for now until I can test it more tomorrow --------- Co-authored-by: conectado --- .github/workflows/rust.yml | 20 +- .../session/backend/SessionManager.kt | 39 +-- .../android/tunnel/TunnelCallbacks.kt | 19 +- .../firezone/android/tunnel/TunnelSession.kt | 2 +- kotlin/android/local.properties.template | 9 + rust/Cargo.lock | 165 ++++++++-- rust/Cargo.toml | 2 +- rust/connlib/clients/android/src/lib.rs | 39 ++- .../Sources/Connlib/CallbackHandler.swift | 4 +- rust/connlib/clients/apple/src/lib.rs | 12 +- rust/connlib/clients/headless/Cargo.toml | 1 + rust/connlib/clients/headless/src/main.rs | 15 +- rust/connlib/gateway/src/main.rs | 11 +- rust/connlib/libs/client/src/control.rs | 4 +- rust/connlib/libs/client/src/lib.rs | 4 +- rust/connlib/libs/common/Cargo.toml | 4 + .../libs/common/src/device_ref_unix.rs | 3 + .../connlib/libs/common/src/device_ref_win.rs | 3 + rust/connlib/libs/common/src/lib.rs | 21 ++ rust/connlib/libs/common/src/session.rs | 102 ++----- rust/connlib/libs/gateway/src/control.rs | 4 +- rust/connlib/libs/gateway/src/lib.rs | 2 +- .../libs/tunnel/src/control_protocol.rs | 10 +- .../libs/tunnel/src/device_channel_unix.rs | 12 +- .../libs/tunnel/src/device_channel_win.rs | 5 +- rust/connlib/libs/tunnel/src/lib.rs | 116 +++---- .../libs/tunnel/src/resource_sender.rs | 30 +- rust/connlib/libs/tunnel/src/tun_android.rs | 34 +-- rust/connlib/libs/tunnel/src/tun_darwin.rs | 28 +- rust/connlib/libs/tunnel/src/tun_linux.rs | 45 ++- .../apple/Firezone.xcodeproj/project.pbxproj | 4 + .../FirezoneNetworkExtension/Adapter.swift | 287 ++++++++++-------- .../PrimaryMacAddress.swift | 77 +++++ 33 files changed, 731 insertions(+), 402 deletions(-) create mode 100644 kotlin/android/local.properties.template create mode 100644 rust/connlib/libs/common/src/device_ref_unix.rs create mode 100644 rust/connlib/libs/common/src/device_ref_win.rs create mode 100644 swift/apple/FirezoneNetworkExtension/PrimaryMacAddress.swift diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index de8b21a1c..c982eb466 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,6 +30,20 @@ jobs: - macos-12 - windows-2019 - windows-2022 + # TODO: https://github.com/rust-lang/cargo/issues/5220 + include: + - runs-on: ubuntu-20.04 + packages: -p headless -p gateway + - runs-on: ubuntu-22.04 + packages: -p headless -p gateway + - runs-on: macos-11 + packages: -p connlib-apple + - runs-on: macos-12 + packages: -p connlib-apple + - runs-on: windows-2019 + packages: -p firezone-client-connlib + - runs-on: windows-2022 + packages: -p firezone-client-connlib runs-on: ${{ matrix.runs-on }} steps: - uses: actions/checkout@v3 @@ -64,11 +78,11 @@ jobs: mv target/tools/windows/nasm/nasm.exe target/tools/nasm.exe - run: cargo fmt -- --check - - run: cargo doc --all-features --no-deps --document-private-items + - run: cargo doc --all-features --no-deps --document-private-items ${{ matrix.packages }} env: RUSTDOCFLAGS: "-D warnings" - - run: cargo clippy --all-targets --all-features -- -D warnings - - run: cargo test --all-features + - run: cargo clippy --all-targets --all-features ${{ matrix.packages }} -- -D warnings + - run: cargo test --all-features ${{ matrix.packages }} rust_smoke-test-relay: runs-on: ubuntu-latest diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt index ba45ac98c..47e46fced 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/backend/SessionManager.kt @@ -2,6 +2,7 @@ package dev.firezone.android.features.session.backend import android.net.VpnService import android.util.Log +import android.provider.Settings import dev.firezone.android.BuildConfig import dev.firezone.android.core.domain.preference.GetConfigUseCase import dev.firezone.android.core.domain.preference.SaveIsConnectedUseCase @@ -26,20 +27,15 @@ internal class SessionManager @Inject constructor( Log.d("Connlib", "token: ${config.token}") if (config.accountId != null && config.token != null) { - Log.d("Connlib", "Attempting to establish VPN connection...") - buildVpnService().establish()?.let { - Log.d("Connlib", "VPN connection established! Attempting to start connlib session...") - sessionPtr = TunnelSession.connect( - it.detachFd(), - BuildConfig.CONTROL_PLANE_URL, - config.token, - TunnelCallbacks() - ) - Log.d("Connlib", "connlib session started! sessionPtr: $sessionPtr") - setConnectionStatus(true) - } ?: let { - Log.d("Connlib", "Failed to build VpnService") - } + Log.d("Connlib", "Attempting to establish TunnelSession...") + sessionPtr = TunnelSession.connect( + BuildConfig.CONTROL_PLANE_URL, + config.token, + Settings.Secure.ANDROID_ID, + TunnelCallbacks() + ) + Log.d("Connlib", "connlib session started! sessionPtr: $sessionPtr") + setConnectionStatus(true) } } catch (exception: Exception) { Log.e("Connection error:", exception.message.toString()) @@ -59,22 +55,7 @@ internal class SessionManager @Inject constructor( saveIsConnectedUseCase.sync(value) } - private fun buildVpnService(): VpnService.Builder = - TunnelService().Builder().apply { - // Add a dummy address for now. Needed for the "establish" call to succeed. - // TODO: Remove these in favor of connecting the TunnelSession *without* the fd, and then - // returning the fd in the onSetInterfaceConfig callback. This is being worked on by @conectado - addAddress("100.100.111.1", 32) - addAddress("fd00:2021:1111::100:100:111:1", 128) - // TODO: These are the staging Resources. Remove these in favor of the onUpdateResources callback. - addRoute("172.31.93.123", 32) - addRoute("172.31.83.10", 32) - addRoute("172.31.82.179", 32) - - setSession("Firezone VPN") - setMtu(1280) - } internal companion object { var sessionPtr: Long? = null diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt index 0cdfb26cc..31bf3c341 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelCallbacks.kt @@ -1,5 +1,6 @@ package dev.firezone.android.tunnel +import android.net.VpnService import android.util.Log class TunnelCallbacks { @@ -13,8 +14,9 @@ class TunnelCallbacks { tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String, - ) { + ): Int { Log.d(TunnelCallbacks.TAG, "onSetInterfaceConfig: [IPv4:$tunnelAddressIPv4] [IPv6:$tunnelAddressIPv6] [dns:$dnsAddress] [dnsFallbackStrategy:$dnsFallbackStrategy]") + return buildVpnService(tunnelAddressIPv4, tunnelAddressIPv6).establish()?.detachFd() ?: -1 } fun onTunnelReady(): Boolean { @@ -42,6 +44,21 @@ class TunnelCallbacks { return true } + private fun buildVpnService(ipv4Address: String, ipv6Address: String): VpnService.Builder = + TunnelService().Builder().apply { + addAddress(ipv4Address, 32) + addAddress(ipv6Address, 128) + + // TODO: These are the staging Resources. Remove these in favor of the onUpdateResources callback. + addRoute("172.31.93.123", 32) + addRoute("172.31.83.10", 32) + addRoute("172.31.82.179", 32) + + setSession("Firezone VPN") + + // TODO: Can we do better? + setMtu(1280) + } companion object { private const val TAG = "TunnelCallbacks" diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt index 12f4a5d88..bbdaf8b61 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelSession.kt @@ -1,6 +1,6 @@ package dev.firezone.android.tunnel object TunnelSession { - external fun connect(fd: Int, controlPlaneUrl: String, token: String, callback: Any): Long + external fun connect(controlPlaneUrl: String, token: String, externalId: String, callback: Any): Long external fun disconnect(session: Long): Boolean } diff --git a/kotlin/android/local.properties.template b/kotlin/android/local.properties.template new file mode 100644 index 000000000..b6cb17675 --- /dev/null +++ b/kotlin/android/local.properties.template @@ -0,0 +1,9 @@ +# Copy this file to local.properties and define the variables below. +# +# Location of the SDK. This is only used by Gradle. For customization when using a Version Control System, please read the +# header note. +# e.g. /Users/jamil/Library/Android/sdk +sdk.dir= + +# Auth token from portal to use for debugUser in order to bypass the auth flow and send to connlib. +token= diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 054ded22e..e1d56aac3 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -124,6 +124,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "1.0.2" @@ -792,16 +801,32 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d" +dependencies = [ + "core-foundation-sys 0.6.2", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.4", "libc", ] +[[package]] +name = "core-foundation-sys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -1007,6 +1032,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dmidecode" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bbcc83e06814bcafa454ec0586c41d3000db69c0451208119270c05b840247" +dependencies = [ + "aho-corasick 0.6.10", + "bitflags 1.3.2", + "failure", + "failure_derive", + "lazy_static", +] + [[package]] name = "domain" version = "0.8.0" @@ -1098,6 +1136,28 @@ dependencies = [ "libc", ] +[[package]] +name = "failure" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +dependencies = [ + "backtrace", + "failure_derive", +] + +[[package]] +name = "failure_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1314,6 +1374,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.10" @@ -1365,6 +1434,7 @@ dependencies = [ "anyhow", "clap", "ctrlc", + "dmidecode", "firezone-client-connlib", "ip_network", "tracing", @@ -1493,7 +1563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", - "core-foundation-sys", + "core-foundation-sys 0.8.4", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", @@ -1540,7 +1610,7 @@ dependencies = [ [[package]] name = "interceptor" version = "0.9.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "async-trait", "bytes", @@ -1555,6 +1625,16 @@ dependencies = [ "webrtc-util", ] +[[package]] +name = "io-kit-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21dcc74995dd4cd090b147e79789f8d65959cbfb5f0b118002db869ea3bd0a0" +dependencies = [ + "core-foundation-sys 0.6.2", + "mach 0.2.3", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -1744,6 +1824,7 @@ dependencies = [ "rtnetlink", "serde", "serde_json", + "smbios-lib", "swift-bridge", "thiserror", "tokio", @@ -1782,6 +1863,24 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "mach" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86dd2487cdfea56def77b88438a2c915fb45113c5319bfe7e14306ca4cd0b0e1" +dependencies = [ + "libc", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2454,7 +2553,7 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", "regex-automata 0.3.6", "regex-syntax 0.7.4", @@ -2475,7 +2574,7 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", "regex-syntax 0.7.4", ] @@ -2557,7 +2656,7 @@ dependencies = [ [[package]] name = "rtcp" version = "0.9.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "bytes", "thiserror", @@ -2585,7 +2684,7 @@ dependencies = [ [[package]] name = "rtp" version = "0.8.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "bytes", "rand", @@ -2778,7 +2877,7 @@ dependencies = [ [[package]] name = "sdp" version = "0.5.3" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "rand", "substring", @@ -2807,8 +2906,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.4", "libc", "security-framework-sys", ] @@ -2819,7 +2918,7 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.4", "libc", ] @@ -2937,6 +3036,22 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +[[package]] +name = "smbios-lib" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923b9d6161c2f7e29070f0b8642a9cf9f53bffd9bcf6c12cf0e295387f2fbaaa" +dependencies = [ + "core-foundation 0.6.4", + "core-foundation-sys 0.6.2", + "getopts", + "io-kit-sys", + "libc", + "mach 0.3.2", + "serde", + "serde_json", +] + [[package]] name = "smol_str" version = "0.2.0" @@ -3020,7 +3135,7 @@ dependencies = [ [[package]] name = "stun" version = "0.4.4" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "base64 0.21.2", "crc", @@ -3530,7 +3645,7 @@ dependencies = [ [[package]] name = "turn" version = "0.6.1" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "async-trait", "base64 0.21.2", @@ -3578,6 +3693,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -3797,7 +3918,7 @@ dependencies = [ [[package]] name = "webrtc" version = "0.8.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "arc-swap", "async-trait", @@ -3839,7 +3960,7 @@ dependencies = [ [[package]] name = "webrtc-data" version = "0.7.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "bytes", "log", @@ -3852,7 +3973,7 @@ dependencies = [ [[package]] name = "webrtc-dtls" version = "0.7.2" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "aes 0.6.0", "aes-gcm", @@ -3888,7 +4009,7 @@ dependencies = [ [[package]] name = "webrtc-ice" version = "0.9.1" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "arc-swap", "async-trait", @@ -3911,7 +4032,7 @@ dependencies = [ [[package]] name = "webrtc-mdns" version = "0.5.2" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "log", "socket2 0.4.9", @@ -3923,7 +4044,7 @@ dependencies = [ [[package]] name = "webrtc-media" version = "0.6.1" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "byteorder", "bytes", @@ -3935,7 +4056,7 @@ dependencies = [ [[package]] name = "webrtc-sctp" version = "0.8.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "arc-swap", "async-trait", @@ -3951,7 +4072,7 @@ dependencies = [ [[package]] name = "webrtc-srtp" version = "0.10.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "aead 0.4.3", "aes 0.7.5", @@ -3973,7 +4094,7 @@ dependencies = [ [[package]] name = "webrtc-util" version = "0.7.0" -source = "git+https://github.com/firezone/webrtc?rev=9ddd589#9ddd5897e6f27b65261f2b9cf38e0d8649af2360" +source = "git+https://github.com/firezone/webrtc?rev=672e728#672e728b2386e17d80c210a8be6ec364916ffb17" dependencies = [ "async-trait", "bitflags 1.3.2", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 6c31bb44d..2fd631559 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,4 +22,4 @@ backoff = { version = "0.4", features = ["tokio"] } # (the `patch` section can't be used for build deps...) [patch.crates-io] ring = { git = "https://github.com/firezone/ring", branch = "v0.16.20-cc-fix" } -webrtc = { git = "https://github.com/firezone/webrtc", rev = "9ddd589" } +webrtc = { git = "https://github.com/firezone/webrtc", rev = "672e728" } diff --git a/rust/connlib/clients/android/src/lib.rs b/rust/connlib/clients/android/src/lib.rs index 9da6d4bc6..26b30ce19 100644 --- a/rust/connlib/clients/android/src/lib.rs +++ b/rust/connlib/clients/android/src/lib.rs @@ -8,14 +8,14 @@ use ip_network::IpNetwork; use jni::{ objects::{GlobalRef, JClass, JObject, JString, JValue}, strings::JNIString, - sys::jint, JNIEnv, JavaVM, }; -use std::net::{Ipv4Addr, Ipv6Addr}; +use std::{ + net::{Ipv4Addr, Ipv6Addr}, + os::fd::RawFd, +}; use thiserror::Error; -const DNS_FALLBACK_STRATEGY: &str = "upstream_resolver"; - /// This should be called once after the library is loaded by the system. #[allow(non_snake_case)] #[no_mangle] @@ -97,7 +97,8 @@ impl Callbacks for CallbackHandler { tunnel_address_v4: Ipv4Addr, tunnel_address_v6: Ipv6Addr, dns_address: Ipv4Addr, - ) -> Result<(), Self::Error> { + dns_fallback_strategy: String, + ) -> Result { self.env(|mut env| { let tunnel_address_v4 = env.new_string(tunnel_address_v4.to_string()) @@ -118,17 +119,18 @@ impl Callbacks for CallbackHandler { } })?; let dns_fallback_strategy = - env.new_string(DNS_FALLBACK_STRATEGY).map_err(|source| { + env.new_string(dns_fallback_strategy).map_err(|source| { CallbackError::NewStringFailed { name: "dns_fallback_strategy", source, } })?; - call_method( - &mut env, + + let name = "onSetInterfaceConfig"; + env.call_method( &self.callback_handler, - "onSetInterfaceConfig", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + name, + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", &[ JValue::from(&tunnel_address_v4), JValue::from(&tunnel_address_v6), @@ -136,6 +138,8 @@ impl Callbacks for CallbackHandler { JValue::from(&dns_fallback_strategy), ], ) + .and_then(|val| val.i()) + .map_err(|source| CallbackError::CallMethodFailed { name, source }) }) } @@ -283,9 +287,9 @@ enum ConnectError { fn connect( env: &mut JNIEnv, - fd: jint, portal_url: JString, portal_token: JString, + external_id: JString, callback_handler: GlobalRef, ) -> Result, ConnectError> { let portal_url = String::from(env.get_string(&portal_url).map_err(|source| { @@ -305,10 +309,17 @@ fn connect( vm: env.get_java_vm().map_err(ConnectError::GetJavaVmFailed)?, callback_handler, }; + let external_id = env + .get_string(&external_id) + .map_err(|source| ConnectError::StringInvalid { + name: "external_id", + source, + })? + .into(); Session::connect( - Some(fd), portal_url.as_str(), portal_token, + external_id, callback_handler, ) .map_err(Into::into) @@ -322,15 +333,15 @@ fn connect( pub unsafe extern "system" fn Java_dev_firezone_android_tunnel_TunnelSession_connect( mut env: JNIEnv, _class: JClass, - fd: jint, portal_url: JString, portal_token: JString, + external_id: JString, callback_handler: JObject, ) -> *const Session { let Ok(callback_handler) = env.new_global_ref(callback_handler) else { return std::ptr::null() }; if let Some(result) = catch_and_throw(&mut env, |env| { - connect(env, fd, portal_url, portal_token, callback_handler) + connect(env, portal_url, portal_token, external_id, callback_handler) }) { match result { Ok(session) => return Box::into_raw(Box::new(session)), diff --git a/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift b/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift index 22a90a913..5b93d12d4 100644 --- a/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift +++ b/rust/connlib/clients/apple/Sources/Connlib/CallbackHandler.swift @@ -29,13 +29,13 @@ public class CallbackHandler { public weak var delegate: CallbackHandlerDelegate? private let logger = Logger.make(for: CallbackHandler.self) - func onSetInterfaceConfig(tunnelAddressIPv4: RustString, tunnelAddressIPv6: RustString, dnsAddress: RustString) { + func onSetInterfaceConfig(tunnelAddressIPv4: RustString, tunnelAddressIPv6: RustString, dnsAddress: RustString, dnsFallbackStrategy: RustString) { logger.debug("CallbackHandler.onSetInterfaceConfig: IPv4: \(tunnelAddressIPv4.toString(), privacy: .public), IPv6: \(tunnelAddressIPv6.toString(), privacy: .public), DNS: \(dnsAddress.toString(), privacy: .public)") delegate?.onSetInterfaceConfig( tunnelAddressIPv4: tunnelAddressIPv4.toString(), tunnelAddressIPv6: tunnelAddressIPv6.toString(), dnsAddress: dnsAddress.toString(), - dnsFallbackStrategy: "system_resolver" // Will come from a onSetInterfaceConfig arg eventually + dnsFallbackStrategy: dnsFallbackStrategy.toString() ) } diff --git a/rust/connlib/clients/apple/src/lib.rs b/rust/connlib/clients/apple/src/lib.rs index 6958a83c4..f1567e67a 100644 --- a/rust/connlib/clients/apple/src/lib.rs +++ b/rust/connlib/clients/apple/src/lib.rs @@ -6,6 +6,7 @@ use firezone_client_connlib::{Callbacks, Error, ResourceDescription, Session}; use ip_network::IpNetwork; use std::{ net::{Ipv4Addr, Ipv6Addr}, + os::fd::RawFd, sync::Arc, }; @@ -18,6 +19,7 @@ mod ffi { fn connect( portal_url: String, token: String, + external_id: String, callback_handler: CallbackHandler, ) -> Result; @@ -39,6 +41,7 @@ mod ffi { tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, + dnsFallbackStrategy: String, ); #[swift_bridge(swift_name = "onTunnelReady")] @@ -86,13 +89,15 @@ impl Callbacks for CallbackHandler { tunnel_address_v4: Ipv4Addr, tunnel_address_v6: Ipv6Addr, dns_address: Ipv4Addr, - ) -> Result<(), Self::Error> { + dns_fallback_strategy: String, + ) -> Result { self.0.on_set_interface_config( tunnel_address_v4.to_string(), tunnel_address_v6.to_string(), dns_address.to_string(), + dns_fallback_strategy.to_string(), ); - Ok(()) + Ok(-1) } fn on_tunnel_ready(&self) -> Result<(), Self::Error> { @@ -149,13 +154,14 @@ impl WrappedSession { fn connect( portal_url: String, token: String, + external_id: String, callback_handler: ffi::CallbackHandler, ) -> Result { init_logging(); Session::connect( - None, portal_url.as_str(), token, + external_id, CallbackHandler(callback_handler.into()), ) .map(|session| Self { session }) diff --git a/rust/connlib/clients/headless/Cargo.toml b/rust/connlib/clients/headless/Cargo.toml index f80d246d8..8a69cec16 100644 --- a/rust/connlib/clients/headless/Cargo.toml +++ b/rust/connlib/clients/headless/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +dmidecode = "0.7" firezone-client-connlib = { path = "../../libs/client" } ip_network = "0.4" url = { version = "2.3.1", default-features = false } diff --git a/rust/connlib/clients/headless/src/main.rs b/rust/connlib/clients/headless/src/main.rs index ceba3d2d6..a6e3990fd 100644 --- a/rust/connlib/clients/headless/src/main.rs +++ b/rust/connlib/clients/headless/src/main.rs @@ -3,10 +3,13 @@ use clap::Parser; use ip_network::IpNetwork; use std::{ net::{Ipv4Addr, Ipv6Addr}, + os::fd::RawFd, str::FromStr, }; -use firezone_client_connlib::{get_user_agent, Callbacks, Error, ResourceDescription, Session}; +use firezone_client_connlib::{ + get_external_id, get_user_agent, Callbacks, Error, ResourceDescription, Session, +}; use url::Url; #[derive(Clone)] @@ -20,8 +23,9 @@ impl Callbacks for CallbackHandler { _tunnel_address_v4: Ipv4Addr, _tunnel_address_v6: Ipv6Addr, _dns_address: Ipv4Addr, - ) -> Result<(), Self::Error> { - Ok(()) + _dns_fallback_strategy: String, + ) -> Result { + Ok(-1) } fn on_tunnel_ready(&self) -> Result<(), Self::Error> { @@ -41,7 +45,7 @@ impl Callbacks for CallbackHandler { &self, resource_list: Vec, ) -> Result<(), Self::Error> { - tracing::trace!("Resources updated, current list: {resource_list:?}"); + tracing::trace!(message = "Resources updated", ?resource_list); Ok(()) } @@ -78,7 +82,8 @@ fn main() -> Result<()> { // TODO: allow passing as arg vars let url = parse_env_var::(URL_ENV_VAR)?; let secret = parse_env_var::(SECRET_ENV_VAR)?; - let mut session = Session::connect(None, url, secret, CallbackHandler).unwrap(); + let external_id = get_external_id(); + let mut session = Session::connect(url, secret, external_id, CallbackHandler).unwrap(); tracing::info!("Started new session"); block_on_ctrl_c(); diff --git a/rust/connlib/gateway/src/main.rs b/rust/connlib/gateway/src/main.rs index 9715b9242..f36e3adba 100644 --- a/rust/connlib/gateway/src/main.rs +++ b/rust/connlib/gateway/src/main.rs @@ -1,11 +1,12 @@ use anyhow::{Context, Result}; use ip_network::IpNetwork; +use std::os::fd::RawFd; use std::{ net::{Ipv4Addr, Ipv6Addr}, str::FromStr, }; -use firezone_gateway_connlib::{Callbacks, Error, ResourceDescription, Session}; +use firezone_gateway_connlib::{get_external_id, Callbacks, Error, ResourceDescription, Session}; use url::Url; #[derive(Clone)] @@ -19,8 +20,9 @@ impl Callbacks for CallbackHandler { _tunnel_address_v4: Ipv4Addr, _tunnel_address_v6: Ipv6Addr, _dns_address: Ipv4Addr, - ) -> Result<(), Self::Error> { - Ok(()) + _dns_fallback_strategy: String, + ) -> Result { + Ok(-1) } fn on_tunnel_ready(&self) -> Result<(), Self::Error> { @@ -64,7 +66,8 @@ fn main() -> Result<()> { // TODO: allow passing as arg vars let url = parse_env_var::(URL_ENV_VAR)?; let secret = parse_env_var::(SECRET_ENV_VAR)?; - let mut session = Session::connect(None, url, secret, CallbackHandler).unwrap(); + let external_id = get_external_id(); + let mut session = Session::connect(url, secret, external_id, CallbackHandler).unwrap(); let (tx, rx) = std::sync::mpsc::channel(); ctrlc::set_handler(move || tx.send(()).expect("Could not send stop signal on channel.")) diff --git a/rust/connlib/libs/client/src/control.rs b/rust/connlib/libs/client/src/control.rs index 231ef7352..1ecdefd9f 100644 --- a/rust/connlib/libs/client/src/control.rs +++ b/rust/connlib/libs/client/src/control.rs @@ -234,15 +234,13 @@ impl ControlPlane { impl ControlSession for ControlPlane { #[tracing::instrument(level = "trace", skip(private_key, callbacks))] async fn start( - fd: Option, private_key: StaticSecret, receiver: Receiver>, control_signal: PhoenixSenderWithTopic, callbacks: CB, ) -> Result<()> { let control_signaler = ControlSignaler { control_signal }; - let tunnel = - Arc::new(Tunnel::new(fd, private_key, control_signaler.clone(), callbacks).await?); + let tunnel = Arc::new(Tunnel::new(private_key, control_signaler.clone(), callbacks).await?); let control_plane = ControlPlane { tunnel, diff --git a/rust/connlib/libs/client/src/lib.rs b/rust/connlib/libs/client/src/lib.rs index 584302a71..7209ec0f7 100644 --- a/rust/connlib/libs/client/src/lib.rs +++ b/rust/connlib/libs/client/src/lib.rs @@ -18,6 +18,8 @@ pub type Session = libs_common::Session< CB, >; -pub use libs_common::{get_user_agent, messages::ResourceDescription, Callbacks, Error}; +pub use libs_common::{ + get_external_id, get_user_agent, messages::ResourceDescription, Callbacks, Error, +}; use messages::Messages; use messages::ReplyMessages; diff --git a/rust/connlib/libs/common/Cargo.toml b/rust/connlib/libs/common/Cargo.toml index f417bc66d..16fcb90ae 100644 --- a/rust/connlib/libs/common/Cargo.toml +++ b/rust/connlib/libs/common/Cargo.toml @@ -34,6 +34,10 @@ parking_lot = "0.12" # Needed for Android logging until tracing is working log = "0.4" +# smbios fails to build on iOS and Android +[target.'cfg(not(any(target_os = "ios", target_os = "android")))'.dependencies] +smbios-lib = "0.9" + [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] swift-bridge = { workspace = true } diff --git a/rust/connlib/libs/common/src/device_ref_unix.rs b/rust/connlib/libs/common/src/device_ref_unix.rs new file mode 100644 index 000000000..922e0feed --- /dev/null +++ b/rust/connlib/libs/common/src/device_ref_unix.rs @@ -0,0 +1,3 @@ +pub struct DeviceRef { + device_ref: std::os::fd::RawFd, +} diff --git a/rust/connlib/libs/common/src/device_ref_win.rs b/rust/connlib/libs/common/src/device_ref_win.rs new file mode 100644 index 000000000..d0c2b48d2 --- /dev/null +++ b/rust/connlib/libs/common/src/device_ref_win.rs @@ -0,0 +1,3 @@ +pub struct DeviceRef { + device_ref: std::os::windows::io::RawHandle, +} diff --git a/rust/connlib/libs/common/src/lib.rs b/rust/connlib/libs/common/src/lib.rs index 6d0b1b7c7..05be80496 100644 --- a/rust/connlib/libs/common/src/lib.rs +++ b/rust/connlib/libs/common/src/lib.rs @@ -26,3 +26,24 @@ pub fn get_user_agent() -> String { let lib_name = LIB_NAME; format!("{os_type}/{os_version} {lib_name}/{lib_version}") } + +/// Returns the SMBios Serial of the device, or a random UUIDv4 if it can't be found. +pub fn get_external_id() -> String { + // smbios fails to build on mobile, but it works for other platforms. + #[cfg(not(any(target_os = "ios", target_os = "android")))] + match smbioslib::table_load_from_device() { + Ok(data) => { + match data.find_map(|sys_info: smbioslib::SMBiosSystemInformation| sys_info.uuid()) { + Some(uuid) => uuid.to_string(), + None => uuid::Uuid::new_v4().to_string(), + } + } + Err(_err) => uuid::Uuid::new_v4().to_string(), + } + + #[cfg(any(target_os = "ios", target_os = "android"))] + { + tracing::debug!("smbios is not supported on iOS and Android, using random UUIDv4"); + uuid::Uuid::new_v4().to_string() + } +} diff --git a/rust/connlib/libs/common/src/session.rs b/rust/connlib/libs/common/src/session.rs index ba835bde8..91d0a9da1 100644 --- a/rust/connlib/libs/common/src/session.rs +++ b/rust/connlib/libs/common/src/session.rs @@ -10,19 +10,18 @@ use std::{ marker::PhantomData, net::{Ipv4Addr, Ipv6Addr}, result::Result as StdResult, - time::Duration, }; use tokio::{runtime::Runtime, sync::mpsc::Receiver}; use url::Url; -use uuid::Uuid; use crate::{ control::{MessageResult, PhoenixChannel, PhoenixSenderWithTopic}, - messages::{Key, ResourceDescription, ResourceDescriptionCidr}, + messages::{Key, ResourceDescription}, Error, Result, }; pub const DNS_SENTINEL: Ipv4Addr = Ipv4Addr::new(100, 100, 111, 1); +type RawFd = i32; struct StopRuntime; @@ -32,7 +31,6 @@ struct StopRuntime; pub trait ControlSession { /// Start control-plane with the given private-key in the background. async fn start( - fd: Option, private_key: StaticSecret, receiver: Receiver>, control_signal: PhoenixSenderWithTopic, @@ -69,7 +67,8 @@ pub trait Callbacks: Clone + Send + Sync { tunnel_address_v4: Ipv4Addr, tunnel_address_v6: Ipv6Addr, dns_address: Ipv4Addr, - ) -> StdResult<(), Self::Error>; + dns_fallback_strategy: String, + ) -> StdResult; /// Called when the tunnel is connected. fn on_tunnel_ready(&self) -> StdResult<(), Self::Error>; /// Called when when a route is added. @@ -101,10 +100,16 @@ impl Callbacks for CallbackErrorFacade { tunnel_address_v4: Ipv4Addr, tunnel_address_v6: Ipv6Addr, dns_address: Ipv4Addr, - ) -> Result<()> { + dns_fallback_strategy: String, + ) -> Result { let result = self .0 - .on_set_interface_config(tunnel_address_v4, tunnel_address_v6, dns_address) + .on_set_interface_config( + tunnel_address_v4, + tunnel_address_v6, + dns_address, + dns_fallback_strategy, + ) .map_err(|err| Error::OnSetInterfaceConfigFailed(err.to_string())); if let Err(err) = result.as_ref() { tracing::error!("{err}"); @@ -201,19 +206,14 @@ where /// 2. Connect to the control plane to the portal /// 3. Start the tunnel in the background and forward control plane messages to it. /// - /// If a fd is passed in, it's used for the tunnel interface. This is useful on Android where - /// we can't create interfaces but we can easily get its file descriptor from the OS. - /// If no fd is passed in, a new interface will be created (Linux) or we'll walk the fd table - /// to find the interface (iOS/macOS). - /// /// The generic parameter `CB` 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( - fd: Option, portal_url: impl TryInto, token: String, + external_id: String, callbacks: CB, ) -> Result { // TODO: We could use tokio::runtime::current() to get the current runtime @@ -250,18 +250,14 @@ where })); } - if cfg!(feature = "mock") { - Self::connect_mock(tx.clone(), this.callbacks.clone()); - } else { - Self::connect_inner( - &runtime, - tx, - fd, - portal_url.try_into().map_err(|_| Error::UriError)?, - token, - this.callbacks.clone(), - ); - } + Self::connect_inner( + &runtime, + tx, + portal_url.try_into().map_err(|_| Error::UriError)?, + token, + external_id, + this.callbacks.clone(), + ); std::thread::spawn(move || { rx.blocking_recv(); runtime.shutdown_background(); @@ -273,18 +269,17 @@ where fn connect_inner( runtime: &Runtime, runtime_stopper: tokio::sync::mpsc::Sender, - fd: Option, portal_url: Url, token: String, + external_id: String, callbacks: CallbackErrorFacade, ) { runtime.spawn(async move { let private_key = StaticSecret::random_from_rng(OsRng); - let self_id = uuid::Uuid::new_v4(); let name_suffix: String = thread_rng().sample_iter(&Alphanumeric).take(8).map(char::from).collect(); 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(), &name_suffix), + get_websocket_path(portal_url, token, T::socket_path(), &Key(PublicKey::from(&private_key).to_bytes()), &external_id, &name_suffix), runtime_stopper, &callbacks ); @@ -309,7 +304,7 @@ where let topic = T::socket_path().to_string(); let internal_sender = connection.sender_with_topic(topic.clone()); fatal_error!( - T::start(fd, private_key, control_plane_receiver, internal_sender, callbacks.0.clone()).await, + T::start(private_key, control_plane_receiver, internal_sender, callbacks.0.clone()).await, runtime_stopper, &callbacks ); @@ -343,55 +338,6 @@ where }); } - fn connect_mock( - runtime_stopper: tokio::sync::mpsc::Sender, - callbacks: CallbackErrorFacade, - ) { - std::thread::sleep(Duration::from_secs(1)); - fatal_error!( - callbacks.on_set_interface_config( - "100.100.111.2".parse().unwrap(), - "fd00:0222:2021:1111::2".parse().unwrap(), - DNS_SENTINEL, - ), - runtime_stopper, - &callbacks - ); - fatal_error!(callbacks.on_tunnel_ready(), runtime_stopper, &callbacks); - let handle = { - let callbacks = callbacks.clone(); - std::thread::spawn(move || -> Result<()> { - std::thread::sleep(Duration::from_secs(3)); - let resources = vec![ - ResourceDescriptionCidr { - id: Uuid::new_v4(), - address: "8.8.4.4".parse::().unwrap().into(), - name: "Google Public DNS IPv4".to_string(), - }, - ResourceDescriptionCidr { - id: Uuid::new_v4(), - address: "2001:4860:4860::8844".parse::().unwrap().into(), - name: "Google Public DNS IPv6".to_string(), - }, - ]; - for resource in &resources { - callbacks.on_add_route(resource.address)?; - } - callbacks.on_update_resources( - resources - .into_iter() - .map(ResourceDescription::Cidr) - .collect(), - ) - }) - }; - fatal_error!( - handle.join().expect("mock thread panicked"), - runtime_stopper, - &callbacks - ); - } - fn disconnect_inner( runtime_stopper: tokio::sync::mpsc::Sender, callbacks: &CallbackErrorFacade, diff --git a/rust/connlib/libs/gateway/src/control.rs b/rust/connlib/libs/gateway/src/control.rs index f8a5c3b8b..343c72185 100644 --- a/rust/connlib/libs/gateway/src/control.rs +++ b/rust/connlib/libs/gateway/src/control.rs @@ -144,15 +144,13 @@ impl ControlPlane { impl ControlSession for ControlPlane { #[tracing::instrument(level = "trace", skip(private_key, callbacks))] async fn start( - fd: Option, private_key: StaticSecret, receiver: Receiver>, control_signal: PhoenixSenderWithTopic, callbacks: CB, ) -> Result<()> { let control_signaler = ControlSignaler { control_signal }; - let tunnel = - Arc::new(Tunnel::new(fd, private_key, control_signaler.clone(), callbacks).await?); + let tunnel = Arc::new(Tunnel::new(private_key, control_signaler.clone(), callbacks).await?); let control_plane = ControlPlane { tunnel, diff --git a/rust/connlib/libs/gateway/src/lib.rs b/rust/connlib/libs/gateway/src/lib.rs index bc8e519ad..c0c9bbd03 100644 --- a/rust/connlib/libs/gateway/src/lib.rs +++ b/rust/connlib/libs/gateway/src/lib.rs @@ -19,4 +19,4 @@ pub type Session = libs_common::Session< CB, >; -pub use libs_common::{messages::ResourceDescription, Callbacks, Error}; +pub use libs_common::{get_external_id, messages::ResourceDescription, Callbacks, Error}; diff --git a/rust/connlib/libs/tunnel/src/control_protocol.rs b/rust/connlib/libs/tunnel/src/control_protocol.rs index ca556bb9d..dda282d01 100644 --- a/rust/connlib/libs/tunnel/src/control_protocol.rs +++ b/rust/connlib/libs/tunnel/src/control_protocol.rs @@ -84,7 +84,7 @@ where } } - self.start_peer_handler(peer); + self.start_peer_handler(peer)?; Ok(()) } @@ -401,7 +401,11 @@ where tracing::trace!("new data channel opened!"); Box::pin(async move { { - let mut iface_config = tunnel.iface_config.lock().await; + let Some(ref mut iface_config) = *tunnel.iface_config.lock().await else { + tracing::error!(message = "Error opening channel", error = "Tried to open a channel before interface was ready"); + let _ = tunnel.callbacks().on_error(&Error::NoIface); + return; + }; for &ip in &peer.ips { if let Err(e) = iface_config.add_route(ip, tunnel.callbacks()).await { @@ -429,7 +433,7 @@ where let conn = tunnel.peer_connections.lock().remove(&client_id); if let Some(conn) = conn { if let Err(e) = conn.close().await { - tracing::error!("Problem while trying to close channel: {e:?}"); + tracing::error!(message = "Error trying to close channel", error = ?e); let _ = tunnel.callbacks().on_error(&e.into()); } } diff --git a/rust/connlib/libs/tunnel/src/device_channel_unix.rs b/rust/connlib/libs/tunnel/src/device_channel_unix.rs index 581f39fa9..5371cd339 100644 --- a/rust/connlib/libs/tunnel/src/device_channel_unix.rs +++ b/rust/connlib/libs/tunnel/src/device_channel_unix.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use libs_common::{Error, Result}; +use libs_common::{messages::Interface, CallbackErrorFacade, Callbacks, Error, Result}; use tokio::io::unix::AsyncFd; use crate::tun::{IfaceConfig, IfaceDevice}; @@ -60,11 +60,15 @@ impl DeviceChannel { } } -pub(crate) async fn create_iface(fd: Option) -> Result<(IfaceConfig, DeviceChannel)> { - let dev = Arc::new(IfaceDevice::new(fd).await?.set_non_blocking()?); +pub(crate) async fn create_iface( + config: &Interface, + callbacks: &CallbackErrorFacade, +) -> Result<(IfaceConfig, DeviceChannel)> { + let dev = Arc::new(IfaceDevice::new(config, callbacks).await?); let async_dev = Arc::clone(&dev); let device_channel = DeviceChannel(AsyncFd::new(async_dev)?); - let iface_config = IfaceConfig(dev); + let mut iface_config = IfaceConfig(dev); + iface_config.up().await?; 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 index 1c4246a7e..59988d397 100644 --- a/rust/connlib/libs/tunnel/src/device_channel_win.rs +++ b/rust/connlib/libs/tunnel/src/device_channel_win.rs @@ -1,5 +1,5 @@ use crate::tun::IfaceConfig; -use libs_common::Result; +use libs_common::{messages::Interface, CallbackErrorFacade, Callbacks, Result}; #[derive(Debug)] pub(crate) struct DeviceChannel; @@ -23,7 +23,8 @@ impl DeviceChannel { } pub(crate) async fn create_iface( - _device_handle: Option, + _: &Interface, + _: &CallbackErrorFacade, ) -> Result<(IfaceConfig, DeviceChannel)> { todo!() } diff --git a/rust/connlib/libs/tunnel/src/lib.rs b/rust/connlib/libs/tunnel/src/lib.rs index 19a2e3ebf..a0bc6c851 100644 --- a/rust/connlib/libs/tunnel/src/lib.rs +++ b/rust/connlib/libs/tunnel/src/lib.rs @@ -11,7 +11,7 @@ use boringtun::{ }; use ip_network::IpNetwork; use ip_network_table::IpNetworkTable; -use libs_common::{Callbacks, DNS_SENTINEL}; +use libs_common::{Callbacks, Error, DNS_SENTINEL}; use async_trait::async_trait; use bytes::Bytes; @@ -78,16 +78,11 @@ mod tun; #[path = "tun_android.rs"] mod tun; -#[cfg(any( - target_os = "macos", - target_os = "ios", - target_os = "linux", - target_os = "android" -))] +#[cfg(target_family = "unix")] #[path = "device_channel_unix.rs"] mod device_channel; -#[cfg(target_os = "windows")] +#[cfg(target_family = "windows")] #[path = "device_channel_win.rs"] mod device_channel; @@ -141,8 +136,8 @@ 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, + iface_config: tokio::sync::Mutex>, + device_channel: RwLock>>, rate_limiter: Arc, private_key: StaticSecret, public_key: PublicKey, @@ -170,7 +165,6 @@ where /// - `control_signaler`: this is used to send SDP from the tunnel to the control plane. #[tracing::instrument(level = "trace", skip(private_key, control_signaler, callbacks))] pub async fn new( - fd: Option, private_key: StaticSecret, control_signaler: C, callbacks: CB, @@ -179,15 +173,14 @@ where 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(fd).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(); let resources_gateways = Default::default(); let gateway_awaiting_connection = Default::default(); + let iface_config = Default::default(); + let device_channel = Default::default(); // ICE let mut media_engine = MediaEngine::default(); @@ -215,9 +208,9 @@ where peers_by_ip, next_index, webrtc_api, + resources, iface_config, device_channel, - resources, awaiting_connection, gateway_awaiting_connection, control_signaler, @@ -233,7 +226,10 @@ where #[tracing::instrument(level = "trace", skip(self))] pub async fn add_resource(&self, resource_description: ResourceDescription) -> Result<()> { { - let mut iface_config = self.iface_config.lock().await; + let Some(ref mut iface_config) = *self.iface_config.lock().await else { + tracing::error!("Received resource add before initialization."); + return Err(Error::ControlProtocolError) + }; for ip in resource_description.ips() { iface_config.add_route(ip, self.callbacks()).await?; } @@ -250,23 +246,16 @@ where /// 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, self.callbacks()) - .await - .expect("Couldn't initiate interface"); - iface_config - .up() - .await - .expect("Couldn't initiate interface"); - iface_config - .add_route(DNS_SENTINEL.into(), self.callbacks()) - .await?; - } + let (mut iface_config, device_channel) = create_iface(config, self.callbacks()).await?; + iface_config + .add_route(DNS_SENTINEL.into(), self.callbacks()) + .await?; + let device_channel = Arc::new(device_channel); + *self.device_channel.write() = Some(device_channel.clone()); + *self.iface_config.lock().await = Some(iface_config); self.start_timers(); - self.start_iface_handler(); + self.start_iface_handler(device_channel); self.callbacks.on_tunnel_ready()?; @@ -386,7 +375,8 @@ where } } - fn start_peer_handler(self: &Arc, peer: Arc) { + fn start_peer_handler(self: &Arc, peer: Arc) -> Result<()> { + let Some(device_channel) = self.device_channel.read().clone() else { return Err(Error::NoIface); }; let tunnel = Arc::clone(self); tokio::spawn(async move { let mut src_buf = [0u8; MAX_UDP_SIZE]; @@ -400,7 +390,8 @@ where break; } - tracing::trace!("read {size} bytes from peer"); + tracing::trace!(action = "read", bytes = size, 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 @@ -417,7 +408,7 @@ where continue; } Err(TunnResult::Err(e)) => { - tracing::error!("Wireguard error: {e:?}"); + tracing::error!(message = "Wireguard error", error = ?e); let _ = tunnel.callbacks().on_error(&e.into()); continue; } @@ -443,9 +434,9 @@ where let mut flush = false; match decapsulate_result { TunnResult::Done => {} - TunnResult::Err(err) => { - tracing::error!("Error decapsulating packet: {err:?}"); - let _ = tunnel.callbacks().on_error(&err.into()); + TunnResult::Err(e) => { + tracing::error!(message = "Error decapsulating packet", error = ?e); + let _ = tunnel.callbacks().on_error(&e.into()); continue; } TunnResult::WriteToNetwork(packet) => { @@ -453,10 +444,14 @@ where peer.send_infallible(packet, &tunnel.callbacks).await; } TunnResult::WriteToTunnelV4(packet, addr) => { - tunnel.send_to_resource(&peer, addr.into(), packet).await; + tunnel + .send_to_resource(&device_channel, &peer, addr.into(), packet) + .await; } TunnResult::WriteToTunnelV6(packet, addr) => { - tunnel.send_to_resource(&peer, addr.into(), packet).await; + tunnel + .send_to_resource(&device_channel, &peer, addr.into(), packet) + .await; } }; @@ -471,16 +466,18 @@ where } } }); + + Ok(()) } - async fn write4_device_infallible(&self, packet: &[u8]) { - if let Err(e) = self.device_channel.write4(packet).await { + async fn write4_device_infallible(&self, device_channel: &DeviceChannel, packet: &[u8]) { + if let Err(e) = device_channel.write4(packet).await { let _ = self.callbacks.on_error(&e.into()); } } - async fn write6_device_infallible(&self, packet: &[u8]) { - if let Err(e) = self.device_channel.write6(packet).await { + async fn write6_device_infallible(&self, device_channel: &DeviceChannel, packet: &[u8]) { + if let Err(e) = device_channel.write6(packet).await { let _ = self.callbacks.on_error(&e.into()); } } @@ -494,7 +491,7 @@ where } } - fn start_iface_handler(self: &Arc) { + fn start_iface_handler(self: &Arc, device_channel: Arc) { let dev = self.clone(); tokio::spawn(async move { loop { @@ -505,32 +502,36 @@ where // 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 { + match device_channel.mtu().await { // XXX: Do we need to fetch the mtu every time? In most clients it'll // be hardcoded to 1280, and if not, it'll only change before packets start // to flow. - Ok(mtu) => match dev.device_channel.read(&mut src[..mtu]).await { + Ok(mtu) => match device_channel.read(&mut src[..mtu]).await { Ok(res) => res, Err(err) => { - tracing::error!("Couldn't read packet from interface: {err}"); + tracing::error!(message = "Couldn't read packet from interface", error = ?err); let _ = dev.callbacks.on_error(&err.into()); continue; } }, Err(err) => { - tracing::error!("Couldn't obtain iface mtu: {err}"); + tracing::error!(message = "Couldn't obtain iface mtu", error = ?err); let _ = dev.callbacks.on_error(&err); continue; } } }; - tracing::trace!("Reading from iface {res} bytes"); + tracing::trace!(action = "reading", bytes = res, from = "iface"); if let Some(r) = dev.check_for_dns(&src[..res]) { match r { - dns::SendPacket::Ipv4(r) => dev.write4_device_infallible(&r[..]).await, - dns::SendPacket::Ipv6(r) => dev.write6_device_infallible(&r[..]).await, + dns::SendPacket::Ipv4(r) => { + dev.write4_device_infallible(&device_channel, &r[..]).await + } + dns::SendPacket::Ipv6(r) => { + dev.write6_device_infallible(&device_channel, &r[..]).await + } } continue; } @@ -582,7 +583,10 @@ where 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..."); + tracing::trace!( + message = "Found new intent to send packets to resource", + resource_ip = %dst_addr + ); awaiting_connection.insert(id); let dev = Arc::clone(&dev); @@ -597,7 +601,7 @@ where dev.resources_gateways.lock().values().collect::>(), ); tracing::trace!( - "Currently connected gateways: {connected_gateway_ids:?}" + message = "Currently connected gateways", gateways = ?connected_gateway_ids ); tokio::spawn(async move { if let Err(e) = dev @@ -607,7 +611,7 @@ where { // 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}"); + tracing::error!(message = "couldn't start protocol for new connection to resource", error = ?e); let _ = dev.callbacks.on_error(&e); } }); @@ -626,11 +630,11 @@ where } TunnResult::Err(e) => { - tracing::error!(message = "Encapsulate error for resource corresponding to {dst_addr}", error = ?e); + tracing::error!(message = "Encapsulate error for resource", resource_address = %dst_addr, error = ?e); let _ = dev.callbacks.on_error(&e.into()); } TunnResult::WriteToNetwork(packet) => { - tracing::trace!("writing iface packet to peer: {dst_addr}"); + tracing::trace!(action = "writing", from = "iface", to = %dst_addr); if let Err(e) = channel.write(&Bytes::copy_from_slice(packet)).await { tracing::error!("Couldn't write packet to channel: {e}"); if matches!( diff --git a/rust/connlib/libs/tunnel/src/resource_sender.rs b/rust/connlib/libs/tunnel/src/resource_sender.rs index 2da833906..0a8548458 100644 --- a/rust/connlib/libs/tunnel/src/resource_sender.rs +++ b/rust/connlib/libs/tunnel/src/resource_sender.rs @@ -3,7 +3,9 @@ use std::{ sync::Arc, }; -use crate::{ip_packet::MutableIpPacket, peer::Peer, ControlSignal, Tunnel}; +use crate::{ + device_channel::DeviceChannel, ip_packet::MutableIpPacket, peer::Peer, ControlSignal, Tunnel, +}; use boringtun::noise::Tunn; use libs_common::{messages::ResourceDescription, Callbacks, Error}; @@ -16,7 +18,12 @@ where ((addr.is_ipv4() && ip.is_ipv4()) || (addr.is_ipv6() && ip.is_ipv6())).then_some(ip) } - async fn update_and_send_packet(&self, packet: &mut [u8], dst_addr: IpAddr) { + async fn update_and_send_packet( + &self, + device_channel: &DeviceChannel, + packet: &mut [u8], + dst_addr: IpAddr, + ) { let Some(mut pkt) = MutableIpPacket::new(packet) else { return }; pkt.set_dst(dst_addr); pkt.update_checksum(); @@ -24,16 +31,22 @@ where match dst_addr { IpAddr::V4(addr) => { tracing::trace!("Sending packet to {addr}"); - self.write4_device_infallible(packet).await; + self.write4_device_infallible(device_channel, packet).await; } IpAddr::V6(addr) => { tracing::trace!("Sending packet to {addr}"); - self.write6_device_infallible(packet).await; + self.write6_device_infallible(device_channel, packet).await; } } } - pub(crate) async fn send_to_resource(&self, peer: &Arc, addr: IpAddr, packet: &mut [u8]) { + pub(crate) async fn send_to_resource( + &self, + device_channel: &DeviceChannel, + peer: &Arc, + addr: IpAddr, + packet: &mut [u8], + ) { if peer.is_allowed(addr) { let Some(resources) = &peer.resources else { // If there's no associated resource it means that we are in a client, then the packet comes from a gateway @@ -41,8 +54,8 @@ where // In gateways this should never happen. tracing::trace!("Writing to interface with addr: {addr}"); match addr { - IpAddr::V4(_) => self.write4_device_infallible(packet).await, - IpAddr::V6(_) => self.write6_device_infallible(packet).await, + IpAddr::V4(_) => self.write4_device_infallible(device_channel, packet).await, + IpAddr::V6(_) => self.write6_device_infallible(device_channel, packet).await, } return; }; @@ -98,7 +111,8 @@ where } }; - self.update_and_send_packet(packet, dst_addr).await; + self.update_and_send_packet(device_channel, packet, dst_addr) + .await; } else { tracing::warn!("Received packet from peer with an unallowed ip: {addr}"); } diff --git a/rust/connlib/libs/tunnel/src/tun_android.rs b/rust/connlib/libs/tunnel/src/tun_android.rs index c91afed68..4ab506c1d 100644 --- a/rust/connlib/libs/tunnel/src/tun_android.rs +++ b/rust/connlib/libs/tunnel/src/tun_android.rs @@ -13,6 +13,9 @@ use std::{ }; mod wrapped_socket; +// Android doesn't support Split DNS. So we intercept all requests and forward +// the non-Firezone name resolution requests to the upstream DNS resolver. +const DNS_FALLBACK_STRATEGY: &str = "upstream_resolver"; #[repr(C)] union IfrIfru { @@ -69,18 +72,17 @@ impl IfaceDevice { } } - pub async fn new(fd: Option) -> Result { - log::debug!("tunnel allocation unimplemented on Android; using provided fd"); - Ok(Self { - fd: fd.expect("file descriptor must be provided!") as RawFd, - }) - } - - pub fn set_non_blocking(self) -> Result { - // Android already opens the tun device in non-blocking mode and we can't change it from - // here. - log::debug!("`set_non_blocking` unimplemented on Android"); - Ok(self) + pub async fn new( + config: &InterfaceConfig, + callbacks: &CallbackErrorFacade, + ) -> Result { + let fd = callbacks.on_set_interface_config( + config.ipv4, + config.ipv6, + DNS_SENTINEL, + DNS_FALLBACK_STRATEGY.to_string(), + )?; + Ok(Self { fd }) } pub fn name(&self) -> Result { @@ -146,14 +148,6 @@ fn get_last_error() -> Error { } impl IfaceConfig { - pub async fn set_iface_config( - &mut self, - config: &InterfaceConfig, - callbacks: &CallbackErrorFacade, - ) -> Result<()> { - callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL) - } - pub async fn add_route( &mut self, route: IpNetwork, diff --git a/rust/connlib/libs/tunnel/src/tun_darwin.rs b/rust/connlib/libs/tunnel/src/tun_darwin.rs index fcf737765..67a327566 100644 --- a/rust/connlib/libs/tunnel/src/tun_darwin.rs +++ b/rust/connlib/libs/tunnel/src/tun_darwin.rs @@ -97,7 +97,10 @@ impl IfaceDevice { } } - pub async fn new(_fd: Option) -> Result { + pub async fn new( + config: &InterfaceConfig, + callbacks: &CallbackErrorFacade, + ) -> Result { let mut info = ctl_info { ctl_id: 0, ctl_name: [0; 96], @@ -152,19 +155,27 @@ impl IfaceDevice { } if addr.sc_id == info.ctl_id { - return Ok(Self { fd }); + let _ = callbacks.on_set_interface_config( + config.ipv4, + config.ipv6, + DNS_SENTINEL, + "system_resolver".to_string(), + ); + let this = Self { fd }; + let _ = this.set_non_blocking(); + return Ok(this); } } Err(get_last_error()) } - pub fn set_non_blocking(self) -> Result { + 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), + _ => Ok(()), }, } } @@ -258,15 +269,6 @@ impl IfaceDevice { // 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, callbacks))] - pub async fn set_iface_config( - &mut self, - config: &InterfaceConfig, - callbacks: &CallbackErrorFacade, - ) -> Result<()> { - callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL) - } - pub async fn add_route( &mut self, route: IpNetwork, diff --git a/rust/connlib/libs/tunnel/src/tun_linux.rs b/rust/connlib/libs/tunnel/src/tun_linux.rs index ad5d77174..79fb2caa9 100644 --- a/rust/connlib/libs/tunnel/src/tun_linux.rs +++ b/rust/connlib/libs/tunnel/src/tun_linux.rs @@ -78,7 +78,10 @@ impl IfaceDevice { } } - pub async fn new(_fd: Option) -> Result { + pub async fn new( + config: &InterfaceConfig, + _: &CallbackErrorFacade, + ) -> Result { debug_assert!(IFACE_NAME.as_bytes().len() < IFNAMSIZ); let fd = match unsafe { open(TUN_FILE.as_ptr() as _, O_RDWR) } { @@ -112,20 +115,52 @@ impl IfaceDevice { .header .index; - Ok(Self { + let mut this = Self { fd, handle, connection: join_handle, interface_index, - }) + }; + + this.set_iface_config(config).await?; + this.set_non_blocking()?; + + Ok(this) } - pub fn set_non_blocking(self) -> Result { + async fn set_iface_config(&mut self, config: &InterfaceConfig) -> Result<()> { + let ips = self + .handle + .address() + .get() + .set_link_index_filter(self.interface_index) + .execute(); + + ips.try_for_each(|ip| self.handle.address().del(ip).execute()) + .await?; + + self.handle + .address() + .add(self.interface_index, config.ipv4.into(), 32) + .execute() + .await?; + + // TODO: Disable this when ipv6 is disabled + self.handle + .address() + .add(self.interface_index, config.ipv6.into(), 128) + .execute() + .await?; + + Ok(()) + } + + 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), + _ => Ok(()), }, } } diff --git a/swift/apple/Firezone.xcodeproj/project.pbxproj b/swift/apple/Firezone.xcodeproj/project.pbxproj index 1a607a9d5..b47e0d242 100644 --- a/swift/apple/Firezone.xcodeproj/project.pbxproj +++ b/swift/apple/Firezone.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 794C38152970A2660029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38142970A2660029F38F /* FirezoneKit */; }; 794C38172970A26A0029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38162970A26A0029F38F /* FirezoneKit */; }; 79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 79756C6529704A7A0018E2D5 /* FirezoneKit */; }; + 8D2F64EF2A973F7000B6176A /* PrimaryMacAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D2F64EC2A97336C00B6176A /* PrimaryMacAddress.swift */; }; 8DCC021D28D512AC007E12D2 /* FirezoneApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCC021C28D512AC007E12D2 /* FirezoneApp.swift */; }; 8DCC022628D512AE007E12D2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCC022528D512AE007E12D2 /* Assets.xcassets */; }; 8DCC022A28D512AE007E12D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCC022928D512AE007E12D2 /* Preview Assets.xcassets */; }; @@ -108,6 +109,7 @@ 6FE4550E2A5D112C006549B1 /* connlib-apple.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "connlib-apple.swift"; path = "Connlib/Generated/connlib-apple/connlib-apple.swift"; sourceTree = ""; }; 6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FirezoneNetworkExtension-Bridging-Header.h"; sourceTree = ""; }; 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSettings.swift; sourceTree = ""; }; + 8D2F64EC2A97336C00B6176A /* PrimaryMacAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryMacAddress.swift; sourceTree = ""; }; 8DCC021928D512AC007E12D2 /* Firezone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firezone.app; sourceTree = BUILT_PRODUCTS_DIR; }; 8DCC021C28D512AC007E12D2 /* FirezoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirezoneApp.swift; sourceTree = ""; }; 8DCC022528D512AE007E12D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -167,6 +169,7 @@ 6FE455082A5D110D006549B1 /* CallbackHandler.swift */, 6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */, 6FE4550E2A5D112C006549B1 /* connlib-apple.swift */, + 8D2F64EC2A97336C00B6176A /* PrimaryMacAddress.swift */, 6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */, ); path = FirezoneNetworkExtension; @@ -468,6 +471,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8D2F64EF2A973F7000B6176A /* PrimaryMacAddress.swift in Sources */, 6FE4550A2A5D110D006549B1 /* CallbackHandler.swift in Sources */, 6FE455102A5D112C006549B1 /* connlib-apple.swift in Sources */, 05CF1D16290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */, diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index ad3863184..334f260b3 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -1,13 +1,16 @@ -// // Adapter.swift // (c) 2023 Firezone, Inc. // LICENSE: Apache-2.0 // +import FirezoneKit import Foundation import NetworkExtension -import FirezoneKit import OSLog +#if os(iOS) + import UIKit.UIDevice +#endif + public enum AdapterError: Error { /// Failure to perform an operation in such state. case invalidState @@ -34,17 +37,18 @@ private enum AdapterState: CustomStringConvertible { case tunnelReady(session: WrappedSession) case stoppingTunnel(session: WrappedSession, onStopped: Adapter.StopTunnelCompletionHandler?) case stoppedTunnel - case stoppingTunnelTemporarily(session: WrappedSession, onStopped: Adapter.StopTunnelCompletionHandler?) + case stoppingTunnelTemporarily( + session: WrappedSession, onStopped: Adapter.StopTunnelCompletionHandler?) case stoppedTunnelTemporarily var description: String { switch self { - case .startingTunnel: return "startingTunnel" - case .tunnelReady: return "tunnelReady" - case .stoppingTunnel: return "stoppingTunnel" - case .stoppedTunnel: return "stoppedTunnel" - case .stoppingTunnelTemporarily: return "stoppingTunnelTemporarily" - case .stoppedTunnelTemporarily: return "stoppedTunnelTemporarily" + case .startingTunnel: return "startingTunnel" + case .tunnelReady: return "tunnelReady" + case .stoppingTunnel: return "stoppingTunnel" + case .stoppedTunnel: return "stoppedTunnel" + case .stoppingTunnelTemporarily: return "stoppingTunnelTemporarily" + case .stoppedTunnelTemporarily: return "stoppedTunnelTemporarily" } } } @@ -85,7 +89,9 @@ public class Adapter { private var controlPlaneURLString: String private var token: String - public init(controlPlaneURLString: String, token: String, packetTunnelProvider: NEPacketTunnelProvider) { + public init( + controlPlaneURLString: String, token: String, packetTunnelProvider: NEPacketTunnelProvider + ) { self.controlPlaneURLString = controlPlaneURLString self.token = token self.packetTunnelProvider = packetTunnelProvider @@ -123,7 +129,8 @@ public class Adapter { self.logger.debug("Adapter.start: Starting connlib") do { self.state = .startingTunnel( - session: try WrappedSession.connect(self.controlPlaneURLString, self.token, self.callbackHandler), + session: try WrappedSession.connect( + self.controlPlaneURLString, self.token, self.getExternalId(), self.callbackHandler), onStarted: completionHandler ) } catch let error { @@ -143,27 +150,31 @@ public class Adapter { self.logger.debug("Adapter.stop") switch self.state { - case .stoppedTunnel, .stoppingTunnel: - break - case .tunnelReady(let session): - self.logger.debug("Adapter.stop: Shutting down connlib") - self.state = .stoppingTunnel(session: session, onStopped: completionHandler) - session.disconnect() - case .startingTunnel(let session, let onStarted): - self.logger.debug("Adapter.stop: Shutting down connlib before tunnel ready") - self.state = .stoppingTunnel(session: session, onStopped: { + case .stoppedTunnel, .stoppingTunnel: + break + case .tunnelReady(let session): + self.logger.debug("Adapter.stop: Shutting down connlib") + self.state = .stoppingTunnel(session: session, onStopped: completionHandler) + session.disconnect() + case .startingTunnel(let session, let onStarted): + self.logger.debug("Adapter.stop: Shutting down connlib before tunnel ready") + self.state = .stoppingTunnel( + session: session, + onStopped: { onStarted?(AdapterError.stoppedByRequestWhileStarting) completionHandler() }) - session.disconnect() - case .stoppingTunnelTemporarily(let session, let onStopped): - self.state = .stoppingTunnel(session: session, onStopped: { + session.disconnect() + case .stoppingTunnelTemporarily(let session, let onStopped): + self.state = .stoppingTunnel( + session: session, + onStopped: { onStopped?() completionHandler() }) - case .stoppedTunnelTemporarily: - self.state = .stoppedTunnel - completionHandler() + case .stoppedTunnelTemporarily: + self.state = .stoppedTunnel + completionHandler() } self.networkMonitor?.cancel() @@ -174,16 +185,38 @@ public class Adapter { /// Get the current set of resources in the completionHandler. /// If unchanged since referenceVersionString, call completionHandler(nil). public func getDisplayableResourcesIfVersionDifferentFrom( - referenceVersionString: String, completionHandler: @escaping (DisplayableResources?) -> Void) { - workQueue.async { [weak self] in - guard let self = self else { return } + referenceVersionString: String, completionHandler: @escaping (DisplayableResources?) -> Void + ) { + workQueue.async { [weak self] in + guard let self = self else { return } - if referenceVersionString == self.displayableResources.versionString { - completionHandler(nil) - } else { - completionHandler(self.displayableResources) - } + if referenceVersionString == self.displayableResources.versionString { + completionHandler(nil) + } else { + completionHandler(self.displayableResources) } + } + } +} + +// MARK: Device unique identifiers +extension Adapter { + func getExternalId() -> String { + #if os(iOS) + guard let uuid = UIDevice.current.identifierForVendor?.uuidString else { + // Send a blank string, letting either connlib or the portal handle this + return "" + } + return uuid + #elseif os(macOS) + guard let macBytes = PrimaryMacAddress.copy_mac_address() else { + // Send a blank string, letting either connlib or the portal handle this + return "" + } + return (macBytes as Data).base64EncodedString() + #else + #error("Unsupported platform") + #endif } } @@ -203,58 +236,62 @@ extension Adapter { // Will be invoked in the workQueue by the path monitor switch self.state { - case .startingTunnel(let session, let onStarted): - if path.status != .satisfied { - self.logger.debug("Adapter.didReceivePathUpdate: Offline. Shutting down connlib.") - onStarted?(nil) - self.packetTunnelProvider?.reasserting = true - self.state = .stoppingTunnelTemporarily(session: session, onStopped: nil) - session.disconnect() - } + case .startingTunnel(let session, let onStarted): + if path.status != .satisfied { + self.logger.debug("Adapter.didReceivePathUpdate: Offline. Shutting down connlib.") + onStarted?(nil) + self.packetTunnelProvider?.reasserting = true + self.state = .stoppingTunnelTemporarily(session: session, onStopped: nil) + session.disconnect() + } - case .tunnelReady(let session): - if path.status == .satisfied { - self.logger.debug("Suppressing calls to disableSomeRoamingForBrokenMobileSemantics() and bumpSockets()") - // #if os(iOS) - // wrappedSession.disableSomeRoamingForBrokenMobileSemantics() - // #endif - // wrappedSession.bumpSockets() - } else { - self.logger.debug("Adapter.didReceivePathUpdate: Offline. Shutting down connlib.") - self.packetTunnelProvider?.reasserting = true - self.state = .stoppingTunnelTemporarily(session: session, onStopped: nil) - session.disconnect() - } + case .tunnelReady(let session): + if path.status == .satisfied { + self.logger.debug( + "Suppressing calls to disableSomeRoamingForBrokenMobileSemantics() and bumpSockets()") + // #if os(iOS) + // wrappedSession.disableSomeRoamingForBrokenMobileSemantics() + // #endif + // wrappedSession.bumpSockets() + } else { + self.logger.debug("Adapter.didReceivePathUpdate: Offline. Shutting down connlib.") + self.packetTunnelProvider?.reasserting = true + self.state = .stoppingTunnelTemporarily(session: session, onStopped: nil) + session.disconnect() + } - case .stoppingTunnelTemporarily: - break + case .stoppingTunnelTemporarily: + break - case .stoppedTunnelTemporarily: - guard path.status == .satisfied else { return } + case .stoppedTunnelTemporarily: + guard path.status == .satisfied else { return } - self.logger.debug("Adapter.didReceivePathUpdate: Back online. Starting connlib.") + self.logger.debug("Adapter.didReceivePathUpdate: Back online. Starting connlib.") - do { - self.state = .startingTunnel( - session: try WrappedSession.connect(controlPlaneURLString, token, self.callbackHandler), - onStarted: { error in - if let error = error { - self.logger.error("Adapter.didReceivePathUpdate: Error starting connlib: \(error, privacy: .public)") - self.packetTunnelProvider?.cancelTunnelWithError(error) - } else { - self.packetTunnelProvider?.reasserting = false - } + do { + self.state = .startingTunnel( + session: try WrappedSession.connect( + controlPlaneURLString, token, self.getExternalId(), self.callbackHandler), + onStarted: { error in + if let error = error { + self.logger.error( + "Adapter.didReceivePathUpdate: Error starting connlib: \(error, privacy: .public)") + self.packetTunnelProvider?.cancelTunnelWithError(error) + } else { + self.packetTunnelProvider?.reasserting = false } - ) - } catch let error as AdapterError { - self.logger.error("Adapter.didReceivePathUpdate: Error: \(error, privacy: .public)") - } catch { - self.logger.error("Adapter.didReceivePathUpdate: Unknown error: \(error, privacy: .public) (fatal)") - } + } + ) + } catch let error as AdapterError { + self.logger.error("Adapter.didReceivePathUpdate: Error: \(error, privacy: .public)") + } catch { + self.logger.error( + "Adapter.didReceivePathUpdate: Unknown error: \(error, privacy: .public) (fatal)") + } - case .stoppingTunnel, .stoppedTunnel: - // no-op - break + case .stoppingTunnel, .stoppedTunnel: + // no-op + break } } } @@ -262,28 +299,34 @@ extension Adapter { // MARK: Implementing CallbackHandlerDelegate extension Adapter: CallbackHandlerDelegate { - public func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String) { + public func onSetInterfaceConfig( + tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, + dnsFallbackStrategy: String + ) { workQueue.async { [weak self] in guard let self = self else { return } self.logger.debug("Adapter.onSetInterfaceConfig") switch self.state { - case .startingTunnel: - self.networkSettings = NetworkSettings( - tunnelAddressIPv4: tunnelAddressIPv4, tunnelAddressIPv6: tunnelAddressIPv6, - dnsAddress: dnsAddress, dnsFallbackStrategy: NetworkSettings.DNSFallbackStrategy(dnsFallbackStrategy)) - case .tunnelReady: - if let networkSettings = self.networkSettings { - networkSettings.setDNSFallbackStrategy(NetworkSettings.DNSFallbackStrategy(dnsFallbackStrategy)) - if let packetTunnelProvider = self.packetTunnelProvider { - networkSettings.apply(on: packetTunnelProvider, logger: self.logger, completionHandler: nil) - } + case .startingTunnel: + self.networkSettings = NetworkSettings( + tunnelAddressIPv4: tunnelAddressIPv4, tunnelAddressIPv6: tunnelAddressIPv6, + dnsAddress: dnsAddress, + dnsFallbackStrategy: NetworkSettings.DNSFallbackStrategy(dnsFallbackStrategy)) + case .tunnelReady: + if let networkSettings = self.networkSettings { + networkSettings.setDNSFallbackStrategy( + NetworkSettings.DNSFallbackStrategy(dnsFallbackStrategy)) + if let packetTunnelProvider = self.packetTunnelProvider { + networkSettings.apply( + on: packetTunnelProvider, logger: self.logger, completionHandler: nil) } + } - case .stoppingTunnel, .stoppedTunnel, .stoppingTunnelTemporarily, .stoppedTunnelTemporarily: - // This is not expected to happen - break + case .stoppingTunnel, .stoppedTunnel, .stoppingTunnelTemporarily, .stoppedTunnelTemporarily: + // This is not expected to happen + break } } } @@ -294,7 +337,8 @@ extension Adapter: CallbackHandlerDelegate { self.logger.debug("Adapter.onTunnelReady") guard case .startingTunnel(let session, let onStarted) = self.state else { - self.logger.error("Adapter.onTunnelReady: Unexpected state: \(self.state, privacy: .public)") + self.logger.error( + "Adapter.onTunnelReady: Unexpected state: \(self.state, privacy: .public)") return } guard let networkSettings = self.networkSettings else { @@ -368,7 +412,8 @@ extension Adapter: CallbackHandlerDelegate { guard let jsonData = jsonString.data(using: .utf8) else { return } - guard let networkResources = try? JSONDecoder().decode([NetworkResource].self, from: jsonData) else { + guard let networkResources = try? JSONDecoder().decode([NetworkResource].self, from: jsonData) + else { return } @@ -392,41 +437,43 @@ extension Adapter: CallbackHandlerDelegate { } } - public func onDisconnect(error: Optional) { + public func onDisconnect(error: String?) { workQueue.async { [weak self] in guard let self = self else { return } self.logger.debug("Adapter.onDisconnect") if let errorMessage = error { - self.logger.error("Connlib disconnected with unrecoverable error: \(errorMessage, privacy: .public)") + self.logger.error( + "Connlib disconnected with unrecoverable error: \(errorMessage, privacy: .public)") switch self.state { - case .stoppingTunnel(session: _, let onStopped): - onStopped?() - self.state = .stoppedTunnel - case .stoppingTunnelTemporarily(session: _, let onStopped): - onStopped?() - self.state = .stoppedTunnel - case .stoppedTunnel: - // This should not happen - break - case .stoppedTunnelTemporarily: - self.state = .stoppedTunnel - default: - self.packetTunnelProvider?.cancelTunnelWithError(AdapterError.connlibFatalError(errorMessage)) - self.state = .stoppedTunnel + case .stoppingTunnel(session: _, let onStopped): + onStopped?() + self.state = .stoppedTunnel + case .stoppingTunnelTemporarily(session: _, let onStopped): + onStopped?() + self.state = .stoppedTunnel + case .stoppedTunnel: + // This should not happen + break + case .stoppedTunnelTemporarily: + self.state = .stoppedTunnel + default: + self.packetTunnelProvider?.cancelTunnelWithError( + AdapterError.connlibFatalError(errorMessage)) + self.state = .stoppedTunnel } } else { self.logger.debug("Connlib disconnected") switch self.state { - case .stoppingTunnel(session: _, let onStopped): - onStopped?() - self.state = .stoppedTunnel - case .stoppingTunnelTemporarily(session: _, let onStopped): - onStopped?() - self.state = .stoppedTunnelTemporarily - default: - // This should not happen - self.state = .stoppedTunnel + case .stoppingTunnel(session: _, let onStopped): + onStopped?() + self.state = .stoppedTunnel + case .stoppingTunnelTemporarily(session: _, let onStopped): + onStopped?() + self.state = .stoppedTunnelTemporarily + default: + // This should not happen + self.state = .stoppedTunnel } } } diff --git a/swift/apple/FirezoneNetworkExtension/PrimaryMacAddress.swift b/swift/apple/FirezoneNetworkExtension/PrimaryMacAddress.swift new file mode 100644 index 000000000..e92411196 --- /dev/null +++ b/swift/apple/FirezoneNetworkExtension/PrimaryMacAddress.swift @@ -0,0 +1,77 @@ +// +// PrimaryMacAddress.swift +// (c) 2023 Firezone, Inc. +// LICENSE: Apache-2.0 +// +// Contains convenience methods for getting a device ID for macOS. + +// Believe it or not, this is Apple's recommended way of doing things for macOS +// See https://developer.apple.com/documentation/appstorereceipts/validating_receipts_on_the_device#//apple_ref/doc/uid/TP40010573-CH1-SW14 + +import IOKit +import Foundation +import OSLog + +public class PrimaryMacAddress { + // Returns an object with a +1 retain count; the caller needs to release. + private static func io_service(named name: String, wantBuiltIn: Bool) -> io_service_t? { + let default_port = kIOMainPortDefault + var iterator = io_iterator_t() + defer { + if iterator != IO_OBJECT_NULL { + IOObjectRelease(iterator) + } + } + + guard let matchingDict = IOBSDNameMatching(default_port, 0, name), + IOServiceGetMatchingServices(default_port, + matchingDict as CFDictionary, + &iterator) == KERN_SUCCESS, + iterator != IO_OBJECT_NULL + else { + return nil + } + + var candidate = IOIteratorNext(iterator) + while candidate != IO_OBJECT_NULL { + if let cftype = IORegistryEntryCreateCFProperty(candidate, + "IOBuiltin" as CFString, + kCFAllocatorDefault, + 0) { + let isBuiltIn = cftype.takeRetainedValue() as! CFBoolean + if wantBuiltIn == CFBooleanGetValue(isBuiltIn) { + return candidate + } + } + + IOObjectRelease(candidate) + candidate = IOIteratorNext(iterator) + } + + return nil + } + + public static func copy_mac_address() -> CFData? { + // Prefer built-in network interfaces. + // For example, an external Ethernet adaptor can displace + // the built-in Wi-Fi as en0. + guard let service = io_service(named: "en0", wantBuiltIn: true) + ?? io_service(named: "en1", wantBuiltIn: true) + ?? io_service(named: "en0", wantBuiltIn: false) + else { return nil } + defer { IOObjectRelease(service) } + + + if let cftype = IORegistryEntrySearchCFProperty( + service, + kIOServicePlane, + "IOMACAddress" as CFString, + kCFAllocatorDefault, + IOOptionBits(kIORegistryIterateRecursively | kIORegistryIterateParents)) { + return (cftype as! CFData) + } + + + return nil + } +}