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 <gabrielalejandro7@gmail.com>
This commit is contained in:
Jamil
2023-08-25 15:44:08 -07:00
committed by GitHub
parent d1de8eac22
commit 4d84e1f12e
33 changed files with 731 additions and 402 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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=

165
rust/Cargo.lock generated
View File

@@ -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",

View File

@@ -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" }

View File

@@ -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<RawFd, Self::Error> {
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<Session<CallbackHandler>, 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<CallbackHandler> {
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)),

View File

@@ -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()
)
}

View File

@@ -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<WrappedSession, String>;
@@ -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<RawFd, Self::Error> {
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<Self, String> {
init_logging();
Session::connect(
None,
portal_url.as_str(),
token,
external_id,
CallbackHandler(callback_handler.into()),
)
.map(|session| Self { session })

View File

@@ -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 }

View File

@@ -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<RawFd, Self::Error> {
Ok(-1)
}
fn on_tunnel_ready(&self) -> Result<(), Self::Error> {
@@ -41,7 +45,7 @@ impl Callbacks for CallbackHandler {
&self,
resource_list: Vec<ResourceDescription>,
) -> 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>(URL_ENV_VAR)?;
let secret = parse_env_var::<String>(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();

View File

@@ -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<RawFd, Self::Error> {
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>(URL_ENV_VAR)?;
let secret = parse_env_var::<String>(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."))

View File

@@ -234,15 +234,13 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
impl<CB: Callbacks + 'static> ControlSession<Messages, CB> for ControlPlane<CB> {
#[tracing::instrument(level = "trace", skip(private_key, callbacks))]
async fn start(
fd: Option<i32>,
private_key: StaticSecret,
receiver: Receiver<MessageResult<Messages>>,
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,

View File

@@ -18,6 +18,8 @@ pub type Session<CB> = 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;

View File

@@ -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 }

View File

@@ -0,0 +1,3 @@
pub struct DeviceRef {
device_ref: std::os::fd::RawFd,
}

View File

@@ -0,0 +1,3 @@
pub struct DeviceRef {
device_ref: std::os::windows::io::RawHandle,
}

View File

@@ -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()
}
}

View File

@@ -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<T, CB: Callbacks> {
/// Start control-plane with the given private-key in the background.
async fn start(
fd: Option<i32>,
private_key: StaticSecret,
receiver: Receiver<MessageResult<T>>,
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<RawFd, Self::Error>;
/// 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<CB: Callbacks> Callbacks for CallbackErrorFacade<CB> {
tunnel_address_v4: Ipv4Addr,
tunnel_address_v6: Ipv6Addr,
dns_address: Ipv4Addr,
) -> Result<()> {
dns_fallback_strategy: String,
) -> Result<RawFd> {
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<i32>,
portal_url: impl TryInto<Url>,
token: String,
external_id: String,
callbacks: CB,
) -> Result<Self> {
// 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<StopRuntime>,
fd: Option<i32>,
portal_url: Url,
token: String,
external_id: String,
callbacks: CallbackErrorFacade<CB>,
) {
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<StopRuntime>,
callbacks: CallbackErrorFacade<CB>,
) {
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::<Ipv4Addr>().unwrap().into(),
name: "Google Public DNS IPv4".to_string(),
},
ResourceDescriptionCidr {
id: Uuid::new_v4(),
address: "2001:4860:4860::8844".parse::<Ipv6Addr>().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<StopRuntime>,
callbacks: &CallbackErrorFacade<CB>,

View File

@@ -144,15 +144,13 @@ impl<CB: Callbacks + 'static> ControlPlane<CB> {
impl<CB: Callbacks + 'static> ControlSession<IngressMessages, CB> for ControlPlane<CB> {
#[tracing::instrument(level = "trace", skip(private_key, callbacks))]
async fn start(
fd: Option<i32>,
private_key: StaticSecret,
receiver: Receiver<MessageResult<IngressMessages>>,
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,

View File

@@ -19,4 +19,4 @@ pub type Session<CB> = libs_common::Session<
CB,
>;
pub use libs_common::{messages::ResourceDescription, Callbacks, Error};
pub use libs_common::{get_external_id, messages::ResourceDescription, Callbacks, Error};

View File

@@ -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());
}
}

View File

@@ -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<i32>) -> Result<(IfaceConfig, DeviceChannel)> {
let dev = Arc::new(IfaceDevice::new(fd).await?.set_non_blocking()?);
pub(crate) async fn create_iface(
config: &Interface,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> 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))
}

View File

@@ -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<i32>,
_: &Interface,
_: &CallbackErrorFacade<impl Callbacks>,
) -> Result<(IfaceConfig, DeviceChannel)> {
todo!()
}

View File

@@ -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<C: ControlSignal, CB: Callbacks> {
next_index: Mutex<IndexLfsr>,
// 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<IfaceConfig>,
device_channel: Arc<DeviceChannel>,
iface_config: tokio::sync::Mutex<Option<IfaceConfig>>,
device_channel: RwLock<Option<Arc<DeviceChannel>>>,
rate_limiter: Arc<RateLimiter>,
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<i32>,
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<Self>, 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<Self>, peer: Arc<Peer>) {
fn start_peer_handler(self: &Arc<Self>, peer: Arc<Peer>) -> 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<Self>) {
fn start_iface_handler(self: &Arc<Self>, device_channel: Arc<DeviceChannel>) {
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::<Vec<_>>(),
);
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!(

View File

@@ -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<Peer>, addr: IpAddr, packet: &mut [u8]) {
pub(crate) async fn send_to_resource(
&self,
device_channel: &DeviceChannel,
peer: &Arc<Peer>,
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}");
}

View File

@@ -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<i32>) -> Result<Self> {
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<Self> {
// 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<impl Callbacks>,
) -> Result<Self> {
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<String> {
@@ -146,14 +148,6 @@ fn get_last_error() -> Error {
}
impl IfaceConfig {
pub async fn set_iface_config(
&mut self,
config: &InterfaceConfig,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<()> {
callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL)
}
pub async fn add_route(
&mut self,
route: IpNetwork,

View File

@@ -97,7 +97,10 @@ impl IfaceDevice {
}
}
pub async fn new(_fd: Option<i32>) -> Result<Self> {
pub async fn new(
config: &InterfaceConfig,
callbacks: &CallbackErrorFacade<impl Callbacks>,
) -> Result<Self> {
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<Self> {
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<impl Callbacks>,
) -> Result<()> {
callbacks.on_set_interface_config(config.ipv4, config.ipv6, DNS_SENTINEL)
}
pub async fn add_route(
&mut self,
route: IpNetwork,

View File

@@ -78,7 +78,10 @@ impl IfaceDevice {
}
}
pub async fn new(_fd: Option<i32>) -> Result<IfaceDevice> {
pub async fn new(
config: &InterfaceConfig,
_: &CallbackErrorFacade<impl Callbacks>,
) -> Result<IfaceDevice> {
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<Self> {
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(()),
},
}
}

View File

@@ -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 = "<group>"; };
6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FirezoneNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSettings.swift; sourceTree = "<group>"; };
8D2F64EC2A97336C00B6176A /* PrimaryMacAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryMacAddress.swift; sourceTree = "<group>"; };
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 = "<group>"; };
8DCC022528D512AE007E12D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -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 */,

View File

@@ -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<String>) {
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
}
}
}

View File

@@ -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
}
}