mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
refactor(apple): Migrate iOS/macOS clients to UniFFI (#10368)
Replace callback-based Adapter with event polling-based AdapterUniFfi This change improves reliability by eliminating callback lifetime issues.
This commit is contained in:
committed by
GitHub
parent
039d0be7b8
commit
cb50800d52
4
.github/actions/setup-rust/action.yml
vendored
4
.github/actions/setup-rust/action.yml
vendored
@@ -21,12 +21,12 @@ outputs:
|
||||
value: ${{
|
||||
(runner.os == 'Linux' && '--workspace') ||
|
||||
(runner.os == 'macOS' && '--workspace') ||
|
||||
(runner.os == 'Windows' && '--workspace --exclude ebpf-turn-router --exclude apple-client-ffi --exclude client-ffi') }}
|
||||
(runner.os == 'Windows' && '--workspace --exclude ebpf-turn-router --exclude client-ffi') }}
|
||||
test-packages:
|
||||
description: Testable packages for the current OS
|
||||
value: ${{
|
||||
(runner.os == 'Linux' && '--workspace') ||
|
||||
(runner.os == 'macOS' && '-p apple-client-ffi -p client-shared -p firezone-tunnel -p snownet') ||
|
||||
(runner.os == 'macOS' && '-p client-ffi -p client-shared -p firezone-tunnel -p snownet') ||
|
||||
(runner.os == 'Windows' && '-p client-shared -p connlib-model -p firezone-bin-shared -p firezone-gui-client -p firezone-headless-client -p firezone-logging -p firezone-telemetry -p firezone-tunnel -p gui-smoke-test -p http-test-server -p ip-packet -p phoenix-channel -p snownet -p socket-factory -p tun') }}
|
||||
nightly_version:
|
||||
description: The nightly version of Rust
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,5 +27,8 @@ gha-creds-*.json
|
||||
# Ignore local `direnv` cache
|
||||
.direnv
|
||||
|
||||
# Ignore personal spelling dictionary
|
||||
/cspell.json
|
||||
|
||||
# Ignore build data for sourcekit-lsp (Swift code)
|
||||
buildServer.json
|
||||
|
||||
78
rust/Cargo.lock
generated
78
rust/Cargo.lock
generated
@@ -311,38 +311,6 @@ version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
|
||||
[[package]]
|
||||
name = "apple-client-ffi"
|
||||
version = "1.5.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"backoff",
|
||||
"client-shared",
|
||||
"connlib-model",
|
||||
"dns-types",
|
||||
"firezone-logging",
|
||||
"firezone-telemetry",
|
||||
"futures",
|
||||
"ip-packet",
|
||||
"ip_network",
|
||||
"libc",
|
||||
"oslog",
|
||||
"phoenix-channel",
|
||||
"rustls",
|
||||
"secrecy",
|
||||
"serde_json",
|
||||
"socket-factory",
|
||||
"swift-bridge",
|
||||
"swift-bridge-build",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"tun",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -1340,6 +1308,7 @@ dependencies = [
|
||||
"ip_network",
|
||||
"libc",
|
||||
"log",
|
||||
"oslog",
|
||||
"phoenix-channel",
|
||||
"rustls",
|
||||
"secrecy",
|
||||
@@ -7336,51 +7305,6 @@ dependencies = [
|
||||
"is_ci",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-bridge"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dca240710850bfee64549b2f86cd2c6fbbb3de64ce5f3da764cb1ecec5c6cc37"
|
||||
dependencies = [
|
||||
"swift-bridge-build",
|
||||
"swift-bridge-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-bridge-build"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d0ea3c38460a65d975df382b71edbc0e22942d937710d63144267d6609ce738"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"swift-bridge-ir",
|
||||
"syn 1.0.109",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-bridge-ir"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5ea2dcd83a40a918fb26a1bb90187691aa0ae8f2c96e8ea5e6963207bc65676"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-bridge-macro"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9b93285ca24a3fd596ecd316b217c4a0fd78c629e09efb1b4990fab1478183"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"swift-bridge-ir",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"apple-client-ffi",
|
||||
"bin-shared",
|
||||
"client-ffi",
|
||||
"client-shared",
|
||||
@@ -42,7 +41,6 @@ edition = "2024"
|
||||
[workspace.dependencies]
|
||||
admx-macro = { path = "gui-client/src-admx-macro" }
|
||||
anyhow = "1.0.99"
|
||||
apple-client-ffi = { path = "apple-client-ffi" }
|
||||
arbitrary = "1.4.2"
|
||||
arboard = { version = "3.6.1", default-features = false }
|
||||
async-trait = { version = "0.1", default-features = false }
|
||||
|
||||
31
rust/apple-client-ffi/.gitignore
vendored
31
rust/apple-client-ffi/.gitignore
vendored
@@ -1,31 +0,0 @@
|
||||
.DS_Store
|
||||
|
||||
# Rust
|
||||
/target
|
||||
Cargo.lock
|
||||
|
||||
### Xcode ###
|
||||
xcuserdata/
|
||||
/.build
|
||||
/Packages
|
||||
DerivedData/
|
||||
.swiftpm/config/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
*.xcarchive/
|
||||
*.xcframework/
|
||||
|
||||
*.checksum.txt
|
||||
|
||||
### Xcode Patch ###
|
||||
*.xcodeproj/*
|
||||
!*.xcodeproj/project.pbxproj
|
||||
!*.xcodeproj/xcshareddata/
|
||||
!*.xcodeproj/project.xcworkspace/
|
||||
!*.xcworkspace/contents.xcworkspacedata
|
||||
/*.gcno
|
||||
**/xcshareddata/WorkspaceSettings.xcsettings
|
||||
|
||||
## Xcode 8 and earlier
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
@@ -1,46 +0,0 @@
|
||||
[package]
|
||||
name = "apple-client-ffi"
|
||||
# mark:next-apple-version
|
||||
version = "1.5.9"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lib]
|
||||
name = "connlib"
|
||||
crate-type = ["staticlib"]
|
||||
doc = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
backoff = { workspace = true }
|
||||
client-shared = { workspace = true }
|
||||
connlib-model = { workspace = true }
|
||||
dns-types = { workspace = true }
|
||||
firezone-logging = { workspace = true }
|
||||
firezone-telemetry = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
ip-packet = { workspace = true }
|
||||
ip_network = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
phoenix-channel = { workspace = true }
|
||||
rustls = { workspace = true }
|
||||
secrecy = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
socket-factory = { workspace = true }
|
||||
swift-bridge = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "sync"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-appender = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tun = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
oslog = { version = "0.2.0", default-features = false }
|
||||
|
||||
[build-dependencies]
|
||||
swift-bridge-build = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,6 +0,0 @@
|
||||
# Connlib Apple Wrapper
|
||||
|
||||
Apple Package wrapper for Connlib for inclusion in the Firezone Apple
|
||||
client.
|
||||
|
||||
This is built as part of the Apple client build.
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
##################################################
|
||||
# We call this from an Xcode run script.
|
||||
##################################################
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cmd=${1:-""}
|
||||
|
||||
# Sanitize the environment to prevent Xcode's shenanigans from leaking
|
||||
# into our highly evolved Rust-based build system.
|
||||
for var in $(env | awk -F= '{print $1}'); do
|
||||
if [[ "$var" != "HOME" ]] &&
|
||||
[[ "$var" != "MACOSX_DEPLOYMENT_TARGET" ]] &&
|
||||
[[ "$var" != "IPHONEOS_DEPLOYMENT_TARGET" ]] &&
|
||||
[[ "$var" != "USER" ]] &&
|
||||
[[ "$var" != "LOGNAME" ]] &&
|
||||
[[ "$var" != "TERM" ]] &&
|
||||
[[ "$var" != "PWD" ]] &&
|
||||
[[ "$var" != "SHELL" ]] &&
|
||||
[[ "$var" != "TMPDIR" ]] &&
|
||||
[[ "$var" != "XPC_FLAGS" ]] &&
|
||||
[[ "$var" != "XPC_SERVICE_NAME" ]] &&
|
||||
[[ "$var" != "PLATFORM_NAME" ]] &&
|
||||
[[ "$var" != "CONFIGURATION" ]] &&
|
||||
[[ "$var" != "NATIVE_ARCH" ]] &&
|
||||
[[ "$var" != "ONLY_ACTIVE_ARCH" ]] &&
|
||||
[[ "$var" != "ARCHS" ]] &&
|
||||
[[ "$var" != "SDKROOT" ]] &&
|
||||
[[ "$var" != "OBJROOT" ]] &&
|
||||
[[ "$var" != "SYMROOT" ]] &&
|
||||
[[ "$var" != "SRCROOT" ]] &&
|
||||
[[ "$var" != "TARGETED_DEVICE_FAMILY" ]] &&
|
||||
[[ "$var" != "RUSTC_WRAPPER" ]] &&
|
||||
[[ "$var" != "RUST_TOOLCHAIN" ]] &&
|
||||
[[ "$var" != "SCCACHE_GCS_BUCKET" ]] &&
|
||||
[[ "$var" != "SCCACHE_GCS_RW_MODE" ]] &&
|
||||
[[ "$var" != "GOOGLE_CLOUD_PROJECT" ]] &&
|
||||
[[ "$var" != "GCP_PROJECT" ]] &&
|
||||
[[ "$var" != "GCLOUD_PROJECT" ]] &&
|
||||
[[ "$var" != "CLOUDSDK_PROJECT" ]] &&
|
||||
[[ "$var" != "CLOUDSDK_CORE_PROJECT" ]] &&
|
||||
[[ "$var" != "GOOGLE_GHA_CREDS_PATH" ]] &&
|
||||
[[ "$var" != "GOOGLE_APPLICATION_CREDENTIALS" ]] &&
|
||||
[[ "$var" != "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE" ]] &&
|
||||
[[ "$var" != "ACTIONS_CACHE_URL" ]] &&
|
||||
[[ "$var" != "ACTIONS_RUNTIME_TOKEN" ]] &&
|
||||
[[ "$var" != "CARGO_INCREMENTAL" ]] &&
|
||||
[[ "$var" != "CARGO_TERM_COLOR" ]] &&
|
||||
[[ "$var" != "FIREZONE_PACKAGE_VERSION" ]] &&
|
||||
[[ "$var" != "CONNLIB_TARGET_DIR" ]]; then
|
||||
unset "$var"
|
||||
fi
|
||||
done
|
||||
|
||||
# Use pristine path; the PATH from Xcode is polluted with stuff we don't want which can
|
||||
# confuse rustc.
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:$HOME/.cargo/bin:/run/current-system/sw/bin/"
|
||||
|
||||
if [[ $cmd == "clean" ]]; then
|
||||
echo "Skipping build during 'clean'"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$PLATFORM_NAME" ]]; then
|
||||
echo "PLATFORM_NAME is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGETS=""
|
||||
if [[ "$PLATFORM_NAME" = "macosx" ]]; then
|
||||
if [[ $CONFIGURATION == "Release" ]] || [[ -z "$NATIVE_ARCH" ]]; then
|
||||
TARGETS=("aarch64-apple-darwin" "x86_64-apple-darwin")
|
||||
else
|
||||
if [[ $NATIVE_ARCH == "arm64" ]]; then
|
||||
TARGETS=("aarch64-apple-darwin")
|
||||
else
|
||||
if [[ $NATIVE_ARCH == "x86_64" ]]; then
|
||||
TARGETS=("x86_64-apple-darwin")
|
||||
else
|
||||
echo "Unsupported native arch for $PLATFORM_NAME: $NATIVE_ARCH"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ "$PLATFORM_NAME" = "iphoneos" ]]; then
|
||||
TARGETS=("aarch64-apple-ios")
|
||||
else
|
||||
echo "Unsupported platform: $PLATFORM_NAME"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
MESSAGE="Building Connlib"
|
||||
CONFIGURATION_ARGS=""
|
||||
|
||||
if [[ $CONFIGURATION == "Release" ]]; then
|
||||
echo "${MESSAGE} for Release"
|
||||
CONFIGURATION_ARGS="--release"
|
||||
else
|
||||
echo "${MESSAGE} for Debug"
|
||||
fi
|
||||
|
||||
if [[ -n "$CONNLIB_TARGET_DIR" ]]; then
|
||||
export CARGO_TARGET_DIR=$CONNLIB_TARGET_DIR
|
||||
fi
|
||||
|
||||
target_list=""
|
||||
for target in "${TARGETS[@]}"; do
|
||||
target_list+="--target $target "
|
||||
done
|
||||
|
||||
target_list="${target_list% }"
|
||||
|
||||
# Build the library
|
||||
cargo build --verbose $target_list $CONFIGURATION_ARGS
|
||||
@@ -1,14 +0,0 @@
|
||||
const XCODE_CONFIGURATION_ENV: &str = "CONFIGURATION";
|
||||
|
||||
fn main() {
|
||||
let out_dir = "../../swift/apple/FirezoneNetworkExtension/Connlib/Generated";
|
||||
|
||||
let bridges = vec!["src/lib.rs"];
|
||||
for path in &bridges {
|
||||
println!("cargo:rerun-if-changed={path}");
|
||||
}
|
||||
println!("cargo:rerun-if-env-changed={XCODE_CONFIGURATION_ENV}");
|
||||
|
||||
swift_bridge_build::parse_bridges(bridges)
|
||||
.write_all_concatenated(out_dir, env!("CARGO_PKG_NAME"));
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
// Swift bridge generated code triggers this below
|
||||
#![allow(clippy::unnecessary_cast, improper_ctypes, non_camel_case_types)]
|
||||
#![cfg(unix)]
|
||||
|
||||
mod make_writer;
|
||||
mod tun;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use backoff::ExponentialBackoffBuilder;
|
||||
use client_shared::{DisconnectError, Event, Session, V4RouteList, V6RouteList};
|
||||
use connlib_model::ResourceView;
|
||||
use dns_types::DomainName;
|
||||
use firezone_logging::err_with_src;
|
||||
use firezone_logging::sentry_layer;
|
||||
use firezone_telemetry::APPLE_DSN;
|
||||
use firezone_telemetry::Telemetry;
|
||||
use firezone_telemetry::analytics;
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use phoenix_channel::LoginUrl;
|
||||
use phoenix_channel::PhoenixChannel;
|
||||
use phoenix_channel::get_user_agent;
|
||||
use secrecy::{Secret, SecretString};
|
||||
use std::sync::OnceLock;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::runtime::Runtime;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tun::Tun;
|
||||
|
||||
/// The Apple client implements reconnect logic in the upper layer using OS provided
|
||||
/// APIs to detect network connectivity changes. The reconnect timeout here only
|
||||
/// applies only in the following conditions:
|
||||
///
|
||||
/// * That reconnect logic fails to detect network changes (not expected to happen)
|
||||
/// * The portal is DOWN
|
||||
///
|
||||
/// Hopefully we aren't down for more than 24 hours.
|
||||
const MAX_PARTITION_TIME: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
/// The Sentry release.
|
||||
///
|
||||
/// This module is only responsible for the connlib part of the MacOS/iOS app.
|
||||
/// Bugs within the MacOS/iOS app itself may use the same DSN but a different component as part of the version string.
|
||||
const RELEASE: &str = concat!("connlib-apple@", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
#[swift_bridge::bridge]
|
||||
mod ffi {
|
||||
extern "Rust" {
|
||||
type WrappedSession;
|
||||
type DisconnectError;
|
||||
|
||||
#[swift_bridge(associated_to = WrappedSession, return_with = err_to_string)]
|
||||
fn connect(
|
||||
api_url: String,
|
||||
token: String,
|
||||
device_id: String,
|
||||
account_slug: String,
|
||||
device_name_override: Option<String>,
|
||||
os_version_override: Option<String>,
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
is_internet_resource_active: bool,
|
||||
callback_handler: CallbackHandler,
|
||||
device_info: String,
|
||||
) -> Result<WrappedSession, String>;
|
||||
|
||||
fn reset(self: &mut WrappedSession, reason: String);
|
||||
|
||||
// Set system DNS resolvers
|
||||
//
|
||||
// `dns_servers` must not have any IPv6 scopes
|
||||
// <https://github.com/firezone/firezone/issues/4350>
|
||||
#[swift_bridge(swift_name = "setDns", return_with = err_to_string)]
|
||||
fn set_dns(self: &mut WrappedSession, dns_servers: String) -> Result<(), String>;
|
||||
|
||||
#[swift_bridge(swift_name = "setInternetResourceState")]
|
||||
fn set_internet_resource_state(self: &mut WrappedSession, active: bool);
|
||||
|
||||
#[swift_bridge(swift_name = "setLogDirectives", return_with = err_to_string)]
|
||||
fn set_log_directives(self: &mut WrappedSession, directives: String) -> Result<(), String>;
|
||||
|
||||
#[swift_bridge(swift_name = "isAuthenticationError")]
|
||||
fn is_authentication_error(self: &DisconnectError) -> bool;
|
||||
|
||||
#[swift_bridge(swift_name = "toString")]
|
||||
fn to_string(self: &DisconnectError) -> String;
|
||||
}
|
||||
|
||||
extern "Swift" {
|
||||
type CallbackHandler;
|
||||
|
||||
#[swift_bridge(swift_name = "onSetInterfaceConfig")]
|
||||
fn on_set_interface_config(
|
||||
&self,
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
searchDomain: Option<String>,
|
||||
dnsAddresses: String,
|
||||
routeListv4: String,
|
||||
routeListv6: String,
|
||||
);
|
||||
|
||||
#[swift_bridge(swift_name = "onUpdateResources")]
|
||||
fn on_update_resources(&self, resourceList: String);
|
||||
|
||||
#[swift_bridge(swift_name = "onDisconnect")]
|
||||
fn on_disconnect(&self, error: DisconnectError);
|
||||
}
|
||||
}
|
||||
|
||||
/// This is used by the apple client to interact with our code.
|
||||
pub struct WrappedSession {
|
||||
inner: Session,
|
||||
runtime: Option<Runtime>,
|
||||
event_stream_handler: Option<JoinHandle<()>>,
|
||||
|
||||
telemetry: Telemetry,
|
||||
}
|
||||
|
||||
// SAFETY: `CallbackHandler.swift` promises to be thread-safe.
|
||||
// TODO: Uphold that promise!
|
||||
unsafe impl Send for ffi::CallbackHandler {}
|
||||
unsafe impl Sync for ffi::CallbackHandler {}
|
||||
|
||||
pub struct CallbackHandler {
|
||||
// Generated Swift opaque type wrappers have a `Drop` impl that decrements the
|
||||
// refcount, but there's no way to generate a `Clone` impl that increments the
|
||||
// recount. Instead, we just wrap it in an `Arc`.
|
||||
inner: ffi::CallbackHandler,
|
||||
}
|
||||
|
||||
impl CallbackHandler {
|
||||
fn on_set_interface_config(
|
||||
&self,
|
||||
tunnel_address_v4: Ipv4Addr,
|
||||
tunnel_address_v6: Ipv6Addr,
|
||||
dns_addresses: Vec<IpAddr>,
|
||||
search_domain: Option<DomainName>,
|
||||
route_list_v4: impl IntoIterator<Item = Ipv4Network>,
|
||||
route_list_v6: impl IntoIterator<Item = Ipv6Network>,
|
||||
) {
|
||||
match (
|
||||
serde_json::to_string(&dns_addresses),
|
||||
serde_json::to_string(&V4RouteList::new(route_list_v4)),
|
||||
serde_json::to_string(&V6RouteList::new(route_list_v6)),
|
||||
) {
|
||||
(Ok(dns_addresses), Ok(route_list_4), Ok(route_list_6)) => {
|
||||
self.inner.on_set_interface_config(
|
||||
tunnel_address_v4.to_string(),
|
||||
tunnel_address_v6.to_string(),
|
||||
search_domain.map(|s| s.to_string()),
|
||||
dns_addresses,
|
||||
route_list_4,
|
||||
route_list_6,
|
||||
);
|
||||
}
|
||||
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => {
|
||||
tracing::error!("Failed to serialize to JSON: {}", err_with_src(&e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_update_resources(&self, resource_list: Vec<ResourceView>) {
|
||||
let resource_list = match serde_json::to_string(&resource_list) {
|
||||
Ok(resource_list) => resource_list,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to serialize resource list: {}", err_with_src(&e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.inner.on_update_resources(resource_list);
|
||||
}
|
||||
|
||||
fn on_disconnect(&self, error: DisconnectError) {
|
||||
if !error.is_authentication_error() {
|
||||
tracing::error!("{error}")
|
||||
}
|
||||
|
||||
self.inner.on_disconnect(error);
|
||||
}
|
||||
}
|
||||
|
||||
static LOGGER_STATE: OnceLock<(
|
||||
firezone_logging::file::Handle,
|
||||
firezone_logging::FilterReloadHandle,
|
||||
)> = OnceLock::new();
|
||||
|
||||
/// Initialises a global logger with the specified log filter.
|
||||
///
|
||||
/// A global logger can only be set once, hence this function uses `static` state to check whether a logger has already been set.
|
||||
/// If so, the new `log_filter` will be applied to the existing logger but a different `log_dir` won't have any effect.
|
||||
///
|
||||
/// From within the FFI module, we have no control over our memory lifecycle and we may get initialised multiple times within the same process.
|
||||
fn init_logging(log_dir: PathBuf, log_filter: String) -> Result<()> {
|
||||
if let Some((_, reload_handle)) = LOGGER_STATE.get() {
|
||||
reload_handle
|
||||
.reload(&log_filter)
|
||||
.context("Failed to apply new log-filter")?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (file_log_filter, file_reload_handle) = firezone_logging::try_filter(&log_filter)?;
|
||||
let (oslog_log_filter, oslog_reload_handle) = firezone_logging::try_filter(&log_filter)?;
|
||||
|
||||
let (file_layer, handle) = firezone_logging::file::layer(&log_dir, "connlib");
|
||||
|
||||
let subscriber = tracing_subscriber::registry()
|
||||
.with(file_layer.with_filter(file_log_filter))
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false)
|
||||
.event_format(
|
||||
firezone_logging::Format::new()
|
||||
.without_timestamp()
|
||||
.without_level(),
|
||||
)
|
||||
.with_writer(make_writer::MakeWriter::new(
|
||||
"dev.firezone.firezone",
|
||||
"connlib",
|
||||
))
|
||||
.with_filter(oslog_log_filter),
|
||||
)
|
||||
.with(sentry_layer());
|
||||
|
||||
let reload_handle = file_reload_handle.merge(oslog_reload_handle);
|
||||
|
||||
firezone_logging::init(subscriber)?;
|
||||
|
||||
LOGGER_STATE
|
||||
.set((handle, reload_handle))
|
||||
.expect("logger state should only ever be initialised once");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl WrappedSession {
|
||||
fn connect(
|
||||
api_url: String,
|
||||
token: String,
|
||||
device_id: String,
|
||||
account_slug: String,
|
||||
device_name_override: Option<String>,
|
||||
os_version_override: Option<String>,
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
is_internet_resource_active: bool,
|
||||
callback_handler: ffi::CallbackHandler,
|
||||
device_info: String,
|
||||
) -> Result<Self> {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(1)
|
||||
.thread_name("connlib")
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
let mut telemetry = Telemetry::new();
|
||||
runtime.block_on(telemetry.start(&api_url, RELEASE, APPLE_DSN, device_id.clone()));
|
||||
Telemetry::set_account_slug(account_slug.clone());
|
||||
|
||||
analytics::identify(RELEASE.to_owned(), Some(account_slug));
|
||||
|
||||
init_logging(log_dir.into(), log_filter)?;
|
||||
install_rustls_crypto_provider();
|
||||
|
||||
let secret = SecretString::from(token);
|
||||
let device_info =
|
||||
serde_json::from_str(&device_info).context("Failed to deserialize `DeviceInfo`")?;
|
||||
|
||||
let url = LoginUrl::client(
|
||||
api_url.as_str(),
|
||||
&secret,
|
||||
device_id.clone(),
|
||||
device_name_override,
|
||||
device_info,
|
||||
)?;
|
||||
|
||||
let _guard = runtime.enter(); // Constructing `PhoenixChannel` requires a runtime context.
|
||||
|
||||
let portal = PhoenixChannel::disconnected(
|
||||
Secret::new(url),
|
||||
get_user_agent(
|
||||
os_version_override,
|
||||
"apple-client",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
),
|
||||
"client",
|
||||
(),
|
||||
|| {
|
||||
ExponentialBackoffBuilder::default()
|
||||
.with_max_elapsed_time(Some(MAX_PARTITION_TIME))
|
||||
.build()
|
||||
},
|
||||
Arc::new(socket_factory::tcp),
|
||||
)?;
|
||||
let (session, mut event_stream) = Session::connect(
|
||||
Arc::new(socket_factory::tcp),
|
||||
Arc::new(socket_factory::udp),
|
||||
portal,
|
||||
is_internet_resource_active,
|
||||
runtime.handle().clone(),
|
||||
);
|
||||
session.set_tun(Box::new(Tun::new()?));
|
||||
|
||||
analytics::new_session(device_id, api_url.to_string());
|
||||
|
||||
let event_stream_handler = runtime.spawn(async move {
|
||||
let callback_handler = CallbackHandler {
|
||||
inner: callback_handler,
|
||||
};
|
||||
|
||||
while let Some(event) = event_stream.next().await {
|
||||
match event {
|
||||
Event::TunInterfaceUpdated(config) => {
|
||||
callback_handler.on_set_interface_config(
|
||||
config.ip.v4,
|
||||
config.ip.v6,
|
||||
config.dns_sentinel_ips(),
|
||||
config.search_domain,
|
||||
config.ipv4_routes,
|
||||
config.ipv6_routes,
|
||||
);
|
||||
}
|
||||
Event::ResourcesUpdated(resource_views) => {
|
||||
callback_handler.on_update_resources(resource_views);
|
||||
}
|
||||
Event::Disconnected(error) => {
|
||||
callback_handler.on_disconnect(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
inner: session,
|
||||
runtime: Some(runtime),
|
||||
event_stream_handler: Some(event_stream_handler),
|
||||
telemetry,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset(&mut self, reason: String) {
|
||||
self.inner.reset(reason)
|
||||
}
|
||||
|
||||
fn set_dns(&mut self, dns_servers: String) -> Result<()> {
|
||||
tracing::debug!(%dns_servers);
|
||||
|
||||
let dns_servers = serde_json::from_str(&dns_servers)
|
||||
.context("Failed to deserialize DNS servers from JSON")?;
|
||||
|
||||
self.inner.set_dns(dns_servers);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_internet_resource_state(&mut self, active: bool) {
|
||||
self.inner.set_internet_resource_state(active);
|
||||
}
|
||||
|
||||
fn set_log_directives(&mut self, directives: String) -> Result<()> {
|
||||
let (_, handle) = LOGGER_STATE.get().context("Logger is not initialised")?;
|
||||
|
||||
handle.reload(&directives)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WrappedSession {
|
||||
fn drop(&mut self) {
|
||||
let Some(runtime) = self.runtime.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.inner.stop(); // Instruct the event-loop to shut down.
|
||||
runtime.block_on(async {
|
||||
self.telemetry.stop().await;
|
||||
|
||||
// The `event_stream_handler` task will exit once the stream is drained.
|
||||
// That only happens once the event-loop has fully shut down.
|
||||
// Hence, waiting for this task here allows us to wait for the graceful shutdown to complete.
|
||||
let Some(event_stream_handler) = self.event_stream_handler.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = tokio::time::timeout(Duration::from_secs(1), event_stream_handler).await;
|
||||
});
|
||||
runtime.shutdown_timeout(Duration::from_secs(1)); // Ensure we don't block forever on a task in the blocking pool.
|
||||
}
|
||||
}
|
||||
|
||||
fn err_to_string<T>(result: Result<T>) -> Result<T, String> {
|
||||
result.map_err(|e| format!("{e:#}"))
|
||||
}
|
||||
|
||||
/// Installs the `ring` crypto provider for rustls.
|
||||
fn install_rustls_crypto_provider() {
|
||||
let existing = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
if existing.is_err() {
|
||||
tracing::debug!("Skipping install of crypto provider because we already have one.");
|
||||
}
|
||||
}
|
||||
12
rust/client-ffi/.cargo/config.toml
Normal file
12
rust/client-ffi/.cargo/config.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Cargo configuration for client-ffi
|
||||
# This helps prevent rebuilds due to environment variable changes
|
||||
|
||||
[env]
|
||||
# Set a consistent macOS deployment target to prevent rebuilds
|
||||
# when switching between Xcode and command-line builds
|
||||
# This must match the Xcode project setting (12.4)
|
||||
MACOSX_DEPLOYMENT_TARGET = "12.4"
|
||||
|
||||
[build]
|
||||
# Use incremental compilation for faster rebuilds
|
||||
incremental = true
|
||||
@@ -38,6 +38,9 @@ tun = { workspace = true }
|
||||
uniffi = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
|
||||
oslog = { version = "0.2.0", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
android_log-sys = "0.3.2"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
mod platform;
|
||||
|
||||
use std::{
|
||||
fmt, io,
|
||||
os::fd::{AsRawFd as _, RawFd},
|
||||
fmt,
|
||||
os::fd::RawFd,
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
@@ -32,7 +32,7 @@ pub struct Session {
|
||||
|
||||
#[derive(uniffi::Object, thiserror::Error, Debug)]
|
||||
#[error("{0:#}")]
|
||||
pub struct Error(anyhow::Error);
|
||||
pub struct ConnlibError(anyhow::Error);
|
||||
|
||||
#[derive(uniffi::Error, thiserror::Error, Debug)]
|
||||
pub enum CallbackError {
|
||||
@@ -78,6 +78,7 @@ pub trait ProtectSocket: Send + Sync + fmt::Debug {
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
#[cfg(target_os = "android")]
|
||||
impl Session {
|
||||
#[uniffi::constructor]
|
||||
#[expect(
|
||||
@@ -96,7 +97,7 @@ impl Session {
|
||||
device_info: String,
|
||||
is_internet_resource_active: bool,
|
||||
protect_socket: Arc<dyn ProtectSocket>,
|
||||
) -> Result<Self, Error> {
|
||||
) -> Result<Self, ConnlibError> {
|
||||
let udp_socket_factory = Arc::new(protected_udp_socket_factory(protect_socket.clone()));
|
||||
let tcp_socket_factory = Arc::new(protected_tcp_socket_factory(protect_socket));
|
||||
|
||||
@@ -115,8 +116,94 @@ impl Session {
|
||||
udp_socket_factory,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnect(&self) -> Result<(), Error> {
|
||||
#[uniffi::export]
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
impl Session {
|
||||
#[uniffi::constructor]
|
||||
#[expect(
|
||||
clippy::too_many_arguments,
|
||||
reason = "This is the API we want to expose over FFI."
|
||||
)]
|
||||
pub fn new_apple(
|
||||
api_url: String,
|
||||
token: String,
|
||||
device_id: String,
|
||||
account_slug: String,
|
||||
device_name: Option<String>,
|
||||
os_version: Option<String>,
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
device_info: String,
|
||||
is_internet_resource_active: bool,
|
||||
) -> Result<Self, ConnlibError> {
|
||||
// iOS doesn't need socket protection like Android
|
||||
let tcp_socket_factory = Arc::new(socket_factory::tcp);
|
||||
let udp_socket_factory = Arc::new(socket_factory::udp);
|
||||
|
||||
let session = connect(
|
||||
api_url,
|
||||
token,
|
||||
device_id,
|
||||
account_slug,
|
||||
device_name,
|
||||
os_version,
|
||||
log_dir,
|
||||
log_filter,
|
||||
device_info,
|
||||
is_internet_resource_active,
|
||||
tcp_socket_factory,
|
||||
udp_socket_factory,
|
||||
)?;
|
||||
|
||||
set_tun_from_search(&session)?;
|
||||
|
||||
Ok(session)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up TUN device with retry logic.
|
||||
///
|
||||
/// Retries a few times with a small delay, as the NetworkExtension
|
||||
/// might still be setting up the TUN interface.
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
fn set_tun_from_search(session: &Session) -> Result<(), ConnlibError> {
|
||||
const MAX_TUN_SETUP_ATTEMPTS: u32 = 5;
|
||||
const TUN_SETUP_RETRY_DELAY_MS: u64 = 100;
|
||||
|
||||
let runtime = session.runtime.as_ref().context("No runtime")?;
|
||||
|
||||
let mut last_error = None;
|
||||
for attempt in 1..=MAX_TUN_SETUP_ATTEMPTS {
|
||||
tracing::debug!("Attempting to find TUN device (attempt {})", attempt);
|
||||
match platform::Tun::new(runtime.handle()) {
|
||||
Ok(tun) => {
|
||||
tracing::debug!("Successfully found and set TUN device");
|
||||
session.inner.set_tun(Box::new(tun));
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Attempt {} failed: {}", attempt, e);
|
||||
last_error = Some(e);
|
||||
if attempt < MAX_TUN_SETUP_ATTEMPTS {
|
||||
std::thread::sleep(std::time::Duration::from_millis(TUN_SETUP_RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"Failed to find TUN device after {} attempts: {}",
|
||||
MAX_TUN_SETUP_ATTEMPTS,
|
||||
last_error.map_or_else(|| "unknown error".to_string(), |e| e.to_string())
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
impl Session {
|
||||
pub fn disconnect(&self) -> Result<(), ConnlibError> {
|
||||
let runtime = self.runtime.as_ref().context("No runtime")?;
|
||||
|
||||
runtime.block_on(async {
|
||||
@@ -131,7 +218,7 @@ impl Session {
|
||||
self.inner.set_internet_resource_state(active);
|
||||
}
|
||||
|
||||
pub fn set_dns(&self, dns_servers: String) -> Result<(), Error> {
|
||||
pub fn set_dns(&self, dns_servers: String) -> Result<(), ConnlibError> {
|
||||
let dns_servers =
|
||||
serde_json::from_str(&dns_servers).context("Failed to deserialize DNS servers")?;
|
||||
|
||||
@@ -144,7 +231,7 @@ impl Session {
|
||||
self.inner.reset(reason)
|
||||
}
|
||||
|
||||
pub fn set_log_directives(&self, directives: String) -> Result<(), Error> {
|
||||
pub fn set_log_directives(&self, directives: String) -> Result<(), ConnlibError> {
|
||||
let (_, reload_handle) = LOGGER_STATE.get().context("Logger not yet initialised")?;
|
||||
|
||||
reload_handle
|
||||
@@ -154,17 +241,19 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_tun(&self, fd: RawFd) -> Result<(), Error> {
|
||||
let _guard = self.runtime.as_ref().context("No runtime")?.enter();
|
||||
pub fn set_tun(&self, fd: RawFd) -> Result<(), ConnlibError> {
|
||||
let runtime = self.runtime.as_ref().context("No runtime")?;
|
||||
// SAFETY: FD must be open.
|
||||
let tun = unsafe { platform::Tun::from_fd(fd).context("Failed to create new Tun")? };
|
||||
let tun = unsafe {
|
||||
platform::Tun::from_fd(fd, runtime.handle()).context("Failed to create new Tun")?
|
||||
};
|
||||
|
||||
self.inner.set_tun(Box::new(tun));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn next_event(&self) -> Result<Option<Event>, Error> {
|
||||
pub async fn next_event(&self) -> Result<Option<Event>, ConnlibError> {
|
||||
match self.events.lock().await.next().await {
|
||||
Some(client_shared::Event::TunInterfaceUpdated(config)) => {
|
||||
let dns = serde_json::to_string(
|
||||
@@ -232,7 +321,7 @@ fn connect(
|
||||
is_internet_resource_active: bool,
|
||||
tcp_socket_factory: Arc<dyn SocketFactory<TcpSocket>>,
|
||||
udp_socket_factory: Arc<dyn SocketFactory<UdpSocket>>,
|
||||
) -> Result<Session, Error> {
|
||||
) -> Result<Session, ConnlibError> {
|
||||
let device_info =
|
||||
serde_json::from_str(&device_info).context("Failed to deserialize `DeviceInfo`")?;
|
||||
let secret = SecretString::from(token);
|
||||
@@ -338,23 +427,27 @@ fn init_logging(log_dir: &Path, log_filter: String) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn protected_tcp_socket_factory(callback: Arc<dyn ProtectSocket>) -> impl SocketFactory<TcpSocket> {
|
||||
move |addr| {
|
||||
let socket = socket_factory::tcp(addr)?;
|
||||
use std::os::fd::AsRawFd;
|
||||
callback
|
||||
.protect_socket(socket.as_raw_fd())
|
||||
.map_err(io::Error::other)?;
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn protected_udp_socket_factory(callback: Arc<dyn ProtectSocket>) -> impl SocketFactory<UdpSocket> {
|
||||
move |addr| {
|
||||
let socket = socket_factory::udp(addr)?;
|
||||
use std::os::fd::AsRawFd;
|
||||
callback
|
||||
.protect_socket(socket.as_raw_fd())
|
||||
.map_err(io::Error::other)?;
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
Ok(socket)
|
||||
}
|
||||
@@ -369,7 +462,7 @@ fn install_rustls_crypto_provider() {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
impl From<anyhow::Error> for ConnlibError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios"
|
||||
))]
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
mod fallback;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
mod android;
|
||||
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
mod apple;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android::*;
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios"
|
||||
))]
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
pub use apple::*;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
pub use fallback::*;
|
||||
|
||||
@@ -54,20 +54,20 @@ impl Tun {
|
||||
///
|
||||
/// - The file descriptor must be open.
|
||||
/// - The file descriptor must not get closed by anyone else.
|
||||
pub unsafe fn from_fd(fd: RawFd) -> io::Result<Self> {
|
||||
pub unsafe fn from_fd(fd: RawFd, runtime: &tokio::runtime::Handle) -> io::Result<Self> {
|
||||
let name = unsafe { interface_name(fd)? };
|
||||
|
||||
let (inbound_tx, inbound_rx) = mpsc::channel(QUEUE_SIZE);
|
||||
let (outbound_tx, outbound_rx) = mpsc::channel(QUEUE_SIZE);
|
||||
|
||||
tokio::spawn(otel::metrics::periodic_system_queue_length(
|
||||
runtime.spawn(otel::metrics::periodic_system_queue_length(
|
||||
outbound_tx.downgrade(),
|
||||
[
|
||||
otel::attr::queue_item_ip_packet(),
|
||||
otel::attr::network_io_direction_transmit(),
|
||||
],
|
||||
));
|
||||
tokio::spawn(otel::metrics::periodic_system_queue_length(
|
||||
runtime.spawn(otel::metrics::periodic_system_queue_length(
|
||||
inbound_tx.downgrade(),
|
||||
[
|
||||
otel::attr::queue_item_ip_packet(),
|
||||
|
||||
28
rust/client-ffi/src/platform/apple.rs
Normal file
28
rust/client-ffi/src/platform/apple.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use firezone_telemetry::Dsn;
|
||||
use std::time::Duration;
|
||||
|
||||
mod make_writer;
|
||||
mod tun;
|
||||
|
||||
// mark:next-apple-version
|
||||
pub const RELEASE: &str = "connlib-apple@1.5.8";
|
||||
|
||||
// mark:next-apple-version
|
||||
pub const VERSION: &str = "1.5.8";
|
||||
|
||||
pub const COMPONENT: &str = "apple-client";
|
||||
|
||||
/// The Apple client implements reconnect logic in the upper layer using OS provided
|
||||
/// APIs to detect network connectivity changes. The reconnect timeout here only
|
||||
/// applies only in the following conditions:
|
||||
///
|
||||
/// * That reconnect logic fails to detect network changes (not expected to happen)
|
||||
/// * The portal is DOWN
|
||||
///
|
||||
/// Hopefully we aren't down for more than 24 hours.
|
||||
pub const MAX_PARTITION_TIME: Duration = Duration::from_secs(60 * 60 * 24);
|
||||
|
||||
pub const DSN: Dsn = firezone_telemetry::APPLE_DSN;
|
||||
|
||||
pub(crate) use make_writer::MakeWriter;
|
||||
pub(crate) use tun::Tun;
|
||||
@@ -24,6 +24,12 @@ impl MakeWriter {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MakeWriter {
|
||||
fn default() -> Self {
|
||||
Self::new("dev.firezone.firezone", "connlib")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'l> tracing_subscriber::fmt::MakeWriter<'l> for MakeWriter {
|
||||
type Writer = Writer<'l>;
|
||||
|
||||
@@ -20,22 +20,38 @@ pub struct Tun {
|
||||
}
|
||||
|
||||
impl Tun {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
pub fn new(runtime: &tokio::runtime::Handle) -> io::Result<Self> {
|
||||
let fd = search_for_tun_fd()?;
|
||||
set_non_blocking(fd)?;
|
||||
Self::from_fd_inner(fd, runtime)
|
||||
}
|
||||
|
||||
/// Create a new [`Tun`] from a raw file descriptor.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// - The file descriptor must be open.
|
||||
/// - The file descriptor must not get closed by anyone else.
|
||||
/// - On iOS/macOS, the NetworkExtension owns the fd, so we don't take ownership.
|
||||
pub unsafe fn from_fd(fd: RawFd, runtime: &tokio::runtime::Handle) -> io::Result<Self> {
|
||||
set_non_blocking(fd)?;
|
||||
Self::from_fd_inner(fd, runtime)
|
||||
}
|
||||
|
||||
fn from_fd_inner(fd: RawFd, runtime: &tokio::runtime::Handle) -> io::Result<Self> {
|
||||
let name = name(fd)?;
|
||||
|
||||
let (inbound_tx, inbound_rx) = mpsc::channel(QUEUE_SIZE);
|
||||
let (outbound_tx, outbound_rx) = mpsc::channel(QUEUE_SIZE);
|
||||
|
||||
tokio::spawn(otel::metrics::periodic_system_queue_length(
|
||||
runtime.spawn(otel::metrics::periodic_system_queue_length(
|
||||
outbound_tx.downgrade(),
|
||||
[
|
||||
otel::attr::queue_item_ip_packet(),
|
||||
otel::attr::network_io_direction_transmit(),
|
||||
],
|
||||
));
|
||||
tokio::spawn(otel::metrics::periodic_system_queue_length(
|
||||
runtime.spawn(otel::metrics::periodic_system_queue_length(
|
||||
inbound_tx.downgrade(),
|
||||
[
|
||||
otel::attr::queue_item_ip_packet(),
|
||||
@@ -40,7 +40,7 @@ impl io::Write for DevNull {
|
||||
pub struct Tun;
|
||||
|
||||
impl Tun {
|
||||
pub unsafe fn from_fd(_: RawFd) -> io::Result<Self> {
|
||||
pub unsafe fn from_fd(_: RawFd, _: &tokio::runtime::Handle) -> io::Result<Self> {
|
||||
Err(io::Error::other("Stub!"))
|
||||
}
|
||||
}
|
||||
|
||||
16
rust/client-ffi/uniffi.toml
Normal file
16
rust/client-ffi/uniffi.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
# UniFFI Configuration for client-ffi
|
||||
|
||||
[bindings.swift]
|
||||
# Enable experimental features for better async support
|
||||
experimental_sendable_value_types = true
|
||||
|
||||
# Configure async runtime for Swift
|
||||
# This enables proper async/await bridging from Rust to Swift
|
||||
async_runtime = "tokio"
|
||||
|
||||
# Generate proper Swift concurrency annotations
|
||||
generate_immutable_records = true
|
||||
|
||||
[bindings.kotlin]
|
||||
# Kotlin configuration (already working)
|
||||
async_runtime = "tokio"
|
||||
@@ -10,28 +10,25 @@
|
||||
05CF1CF1290B1CEE00CF4755 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; };
|
||||
05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */; };
|
||||
05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */; };
|
||||
6FE454F62A5BFB93006549B1 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE454EA2A5BFABA006549B1 /* Adapter.swift */; };
|
||||
6FE455092A5D110D006549B1 /* CallbackHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE455082A5D110D006549B1 /* CallbackHandler.swift */; };
|
||||
6FE4550C2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */; };
|
||||
6FE4550F2A5D112C006549B1 /* apple-client-ffi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550E2A5D112C006549B1 /* apple-client-ffi.swift */; };
|
||||
|
||||
6F0EDF1F2E79A13800D6D632 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177FB893F19457A113042247 /* Adapter.swift */; };
|
||||
6F0EDF212E79A15700D6D632 /* connlib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847ACC9D73EA6F356F2DEE1 /* connlib.swift */; };
|
||||
6FE93AFB2A738D7E002D278A /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; };
|
||||
794C38152970A2660029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38142970A2660029F38F /* FirezoneKit */; };
|
||||
79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 79756C6529704A7A0018E2D5 /* FirezoneKit */; };
|
||||
8D4087D52D24653B005B2BAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8D4087D42D24653B005B2BAF /* Sentry */; };
|
||||
8D4087D92D246541005B2BAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8D4087D82D246541005B2BAF /* Sentry */; };
|
||||
8D4087DD2D246651005B2BAF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 8D4087DC2D246651005B2BAF /* Sentry */; };
|
||||
858AA13A09A55E3B1DD5DEDA /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177FB893F19457A113042247 /* Adapter.swift */; };
|
||||
8D41B9A52D15DD6800D16065 /* TunnelLogArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D41B9A42D15DD6800D16065 /* TunnelLogArchive.swift */; };
|
||||
8D41B9A62D15DD6800D16065 /* TunnelLogArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D41B9A42D15DD6800D16065 /* TunnelLogArchive.swift */; };
|
||||
8D5047F82CE6AA22009802E9 /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 8D5047F72CE6AA22009802E9 /* FirezoneKit */; };
|
||||
8D5047FB2CE6AA37009802E9 /* FirezoneNetworkExtension-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = 6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */; };
|
||||
8D5047FC2CE6AA47009802E9 /* CallbackHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE455082A5D110D006549B1 /* CallbackHandler.swift */; };
|
||||
8D5047FD2CE6AA47009802E9 /* apple-client-ffi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550E2A5D112C006549B1 /* apple-client-ffi.swift */; };
|
||||
8D5047FE2CE6AA54009802E9 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */; };
|
||||
8D5047FF2CE6AA54009802E9 /* Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE454EA2A5BFABA006549B1 /* Adapter.swift */; };
|
||||
8D5048002CE6AA60009802E9 /* SystemConfigurationResolvers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6939312BA2521A00AF4396 /* SystemConfigurationResolvers.swift */; };
|
||||
8D5048012CE6AA60009802E9 /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; };
|
||||
8D5048042CE6B0AE009802E9 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */; };
|
||||
8D69392C2BA24FE600AF4396 /* BindResolvers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D69392B2BA24FE600AF4396 /* BindResolvers.swift */; };
|
||||
146C8E809FF744D18C053A79 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C09DC14A4A04715A37BC04C /* Channel.swift */; };
|
||||
C1892134991C462C8E137DE3 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C09DC14A4A04715A37BC04C /* Channel.swift */; };
|
||||
C4B08E1145E04823BC25E00D /* SessionEventLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */; };
|
||||
6571861AB9324D6A9395BDB3 /* SessionEventLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */; };
|
||||
8DC08BD72B297DB400675F46 /* FirezoneNetworkExtension-Bridging-Header.h in Sources */ = {isa = PBXBuildFile; fileRef = 6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */; };
|
||||
8DC1699D2CFF77D1006801B5 /* dev.firezone.firezone.network-extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 05CF1CF0290B1CEE00CF4755 /* dev.firezone.firezone.network-extension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
8DC169A02CFF77D1006801B5 /* dev.firezone.firezone.network-extension.systemextension in Embed System Extensions */ = {isa = PBXBuildFile; fileRef = 8D5047E32CE6A8F4009802E9 /* dev.firezone.firezone.network-extension.systemextension */; platformFilters = (macos, ); settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
@@ -44,6 +41,8 @@
|
||||
8DCC022A28D512AE007E12D2 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DCC022928D512AE007E12D2 /* Preview Assets.xcassets */; };
|
||||
8DE452A82CE6C194004CEDF9 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5047E82CE6A8F4009802E9 /* main.swift */; };
|
||||
8DFDEAC72D2CEBA500615095 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 8DA12C322BB7DA04007D91EB /* PrivacyInfo.xcprivacy */; };
|
||||
|
||||
CCC04BEF428B758D9BB5F842 /* connlib.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2847ACC9D73EA6F356F2DEE1 /* connlib.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -89,14 +88,14 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
||||
05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelProvider.swift; sourceTree = "<group>"; };
|
||||
05CF1CF0290B1CEE00CF4755 /* dev.firezone.firezone.network-extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "dev.firezone.firezone.network-extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
05CF1CF6290B1CEE00CF4755 /* FirezoneNetworkExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FirezoneNetworkExtension.entitlements; sourceTree = "<group>"; };
|
||||
05D3BB1628FDBD8A00BC3727 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; };
|
||||
6FE454EA2A5BFABA006549B1 /* Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = "<group>"; };
|
||||
6FE455082A5D110D006549B1 /* CallbackHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallbackHandler.swift; sourceTree = "<group>"; };
|
||||
6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Connlib/Generated/SwiftBridgeCore.swift; sourceTree = "<group>"; };
|
||||
6FE4550E2A5D112C006549B1 /* apple-client-ffi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "apple-client-ffi.swift"; path = "Connlib/Generated/apple-client-ffi/apple-client-ffi.swift"; sourceTree = "<group>"; };
|
||||
177FB893F19457A113042247 /* Adapter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = Adapter.swift; sourceTree = "<group>"; };
|
||||
2847ACC9D73EA6F356F2DEE1 /* connlib.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = connlib.swift; path = FirezoneNetworkExtension/Connlib/Generated/connlib.swift; sourceTree = "<group>"; };
|
||||
6FB20C422E7049A300E41294 /* ConnlibFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ConnlibFFI.xcframework; path = Frameworks/ConnlibFFI.xcframework; 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>"; };
|
||||
6FFECD5B2AD6998400E00273 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
|
||||
@@ -106,6 +105,8 @@
|
||||
8D5047E32CE6A8F4009802E9 /* dev.firezone.firezone.network-extension.systemextension */ = {isa = PBXFileReference; explicitFileType = "wrapper.system-extension"; includeInIndex = 0; path = "dev.firezone.firezone.network-extension.systemextension"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8D5047E82CE6A8F4009802E9 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
8D69392B2BA24FE600AF4396 /* BindResolvers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BindResolvers.swift; sourceTree = "<group>"; };
|
||||
6C09DC14A4A04715A37BC04C /* Channel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Channel.swift; sourceTree = "<group>"; };
|
||||
E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionEventLoop.swift; sourceTree = "<group>"; };
|
||||
8D6939312BA2521A00AF4396 /* SystemConfigurationResolvers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemConfigurationResolvers.swift; sourceTree = "<group>"; };
|
||||
8DA12C322BB7DA04007D91EB /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
8DC08BCC2B296C5900675F46 /* libresolv.9.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.9.tbd; path = usr/lib/libresolv.9.tbd; sourceTree = SDKROOT; };
|
||||
@@ -129,7 +130,6 @@
|
||||
files = (
|
||||
8DC333F92D2FA85200E627D5 /* libresolv.tbd in Frameworks */,
|
||||
8DC333FA2D2FA85200E627D5 /* libresolv.9.tbd in Frameworks */,
|
||||
8D4087D52D24653B005B2BAF /* Sentry in Frameworks */,
|
||||
794C38152970A2660029F38F /* FirezoneKit in Frameworks */,
|
||||
05CF1CF1290B1CEE00CF4755 /* NetworkExtension.framework in Frameworks */,
|
||||
);
|
||||
@@ -142,7 +142,6 @@
|
||||
8DC333FB2D2FA89100E627D5 /* SystemConfiguration.framework in Frameworks */,
|
||||
8D5047F82CE6AA22009802E9 /* FirezoneKit in Frameworks */,
|
||||
8DC9FB852CF5A738001BCE6A /* NetworkExtension.framework in Frameworks */,
|
||||
8D4087D92D246541005B2BAF /* Sentry in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -152,7 +151,6 @@
|
||||
files = (
|
||||
05D3BB2128FDE9C000BC3727 /* NetworkExtension.framework in Frameworks */,
|
||||
79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */,
|
||||
8D4087DD2D246651005B2BAF /* Sentry in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -166,15 +164,13 @@
|
||||
8DE1077B2D2313EB00DB5A45 /* Info.macOS.plist */,
|
||||
8D6939312BA2521A00AF4396 /* SystemConfigurationResolvers.swift */,
|
||||
8D69392B2BA24FE600AF4396 /* BindResolvers.swift */,
|
||||
6C09DC14A4A04715A37BC04C /* Channel.swift */,
|
||||
E14DC5E933BD4F63A844A8A6 /* SessionEventLoop.swift */,
|
||||
05CF1CF6290B1CEE00CF4755 /* FirezoneNetworkExtension.entitlements */,
|
||||
8D5047E82CE6A8F4009802E9 /* main.swift */,
|
||||
05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */,
|
||||
6FE454EA2A5BFABA006549B1 /* Adapter.swift */,
|
||||
6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */,
|
||||
6FE455082A5D110D006549B1 /* CallbackHandler.swift */,
|
||||
6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */,
|
||||
8D41B9A42D15DD6800D16065 /* TunnelLogArchive.swift */,
|
||||
6FE4550E2A5D112C006549B1 /* apple-client-ffi.swift */,
|
||||
6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */,
|
||||
);
|
||||
path = FirezoneNetworkExtension;
|
||||
@@ -188,9 +184,28 @@
|
||||
path = Application;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
64A772F0917639D7745EA995 /* Connlib */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
99553B27AEABC2EA3782DDEA /* Generated */,
|
||||
);
|
||||
name = Connlib;
|
||||
path = FirezoneNetworkExtension/Connlib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8BCDECDF49DAFFD25CF140AF /* FirezoneNetworkExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
64A772F0917639D7745EA995 /* Connlib */,
|
||||
177FB893F19457A113042247 /* Adapter.swift */,
|
||||
);
|
||||
path = FirezoneNetworkExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8D3F90C328D64FAD00980124 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6FB20C422E7049A300E41294 /* ConnlibFFI.xcframework */,
|
||||
8DC08BD12B297B7B00675F46 /* libresolv.tbd */,
|
||||
8DC08BCC2B296C5900675F46 /* libresolv.9.tbd */,
|
||||
8DC333F72D2FA85200E627D5 /* libresolv.tbd */,
|
||||
@@ -210,6 +225,8 @@
|
||||
05833DF928F73B070008FAB0 /* FirezoneNetworkExtension */,
|
||||
8DCC021A28D512AC007E12D2 /* Products */,
|
||||
8D3F90C328D64FAD00980124 /* Frameworks */,
|
||||
8BCDECDF49DAFFD25CF140AF /* FirezoneNetworkExtension */,
|
||||
2847ACC9D73EA6F356F2DEE1 /* connlib.swift */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -252,6 +269,14 @@
|
||||
name = Packages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
99553B27AEABC2EA3782DDEA /* Generated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Generated;
|
||||
path = FirezoneNetworkExtension/Connlib/Generated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -355,8 +380,6 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 8DCC021028D512AC007E12D2;
|
||||
packageReferences = (
|
||||
);
|
||||
productRefGroup = 8DCC021A28D512AC007E12D2 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@@ -452,7 +475,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "cd ../../rust/apple-client-ffi\n./build-rust.sh\n";
|
||||
shellScript = "export PLATFORM_NAME=\"${PLATFORM_NAME}\"\nexport CONFIGURATION=\"${CONFIGURATION}\"\nexport NATIVE_ARCH=\"${ARCHS}\"\nexport CONNLIB_TARGET_DIR=\"${CONNLIB_TARGET_DIR}\"\n./build-rust.sh\n";
|
||||
};
|
||||
8D8555832D0A7CBB00A1EA09 /* Build Connlib */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
@@ -472,7 +495,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "cd ../../rust/apple-client-ffi\n./build-rust.sh\n";
|
||||
shellScript = "export PLATFORM_NAME=\"${PLATFORM_NAME}\"\nexport CONFIGURATION=\"${CONFIGURATION}\"\nexport NATIVE_ARCH=\"${ARCHS}\"\nexport CONNLIB_TARGET_DIR=\"${CONNLIB_TARGET_DIR}\"\n./build-rust.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
@@ -481,14 +504,14 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
6F0EDF212E79A15700D6D632 /* connlib.swift in Sources */,
|
||||
6F0EDF1F2E79A13800D6D632 /* Adapter.swift in Sources */,
|
||||
8DC08BD72B297DB400675F46 /* FirezoneNetworkExtension-Bridging-Header.h in Sources */,
|
||||
6FE455092A5D110D006549B1 /* CallbackHandler.swift in Sources */,
|
||||
6FE4550F2A5D112C006549B1 /* apple-client-ffi.swift in Sources */,
|
||||
8D41B9A52D15DD6800D16065 /* TunnelLogArchive.swift in Sources */,
|
||||
05CF1D17290B1FE700CF4755 /* PacketTunnelProvider.swift in Sources */,
|
||||
6FE454F62A5BFB93006549B1 /* Adapter.swift in Sources */,
|
||||
6FE4550C2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */,
|
||||
8D69392C2BA24FE600AF4396 /* BindResolvers.swift in Sources */,
|
||||
146C8E809FF744D18C053A79 /* Channel.swift in Sources */,
|
||||
C4B08E1145E04823BC25E00D /* SessionEventLoop.swift in Sources */,
|
||||
6FE93AFB2A738D7E002D278A /* NetworkSettings.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -500,13 +523,13 @@
|
||||
8D41B9A62D15DD6800D16065 /* TunnelLogArchive.swift in Sources */,
|
||||
8DE452A82CE6C194004CEDF9 /* main.swift in Sources */,
|
||||
8D5047FB2CE6AA37009802E9 /* FirezoneNetworkExtension-Bridging-Header.h in Sources */,
|
||||
8D5048042CE6B0AE009802E9 /* SwiftBridgeCore.swift in Sources */,
|
||||
8D5047FD2CE6AA47009802E9 /* apple-client-ffi.swift in Sources */,
|
||||
8D5047FC2CE6AA47009802E9 /* CallbackHandler.swift in Sources */,
|
||||
8D5047FE2CE6AA54009802E9 /* PacketTunnelProvider.swift in Sources */,
|
||||
8D5048002CE6AA60009802E9 /* SystemConfigurationResolvers.swift in Sources */,
|
||||
8D5048012CE6AA60009802E9 /* NetworkSettings.swift in Sources */,
|
||||
8D5047FF2CE6AA54009802E9 /* Adapter.swift in Sources */,
|
||||
C1892134991C462C8E137DE3 /* Channel.swift in Sources */,
|
||||
6571861AB9324D6A9395BDB3 /* SessionEventLoop.swift in Sources */,
|
||||
858AA13A09A55E3B1DD5DEDA /* Adapter.swift in Sources */,
|
||||
CCC04BEF428B758D9BB5F842 /* connlib.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -558,11 +581,12 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-ios/debug";
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-ios/debug";
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.4;
|
||||
MARKETING_VERSION = 1.5.9;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_LDFLAGS = "-lconnlib";
|
||||
OTHER_SWIFT_FLAGS = "-D UNIFFI";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension";
|
||||
PRODUCT_NAME = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "$(NE_PROFILE_ID)";
|
||||
@@ -600,10 +624,11 @@
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-ios/release";
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(CONNLIB_TARGET_DIR)/aarch64-apple-ios/release";
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.4;
|
||||
MARKETING_VERSION = 1.5.9;
|
||||
OTHER_LDFLAGS = "-lconnlib";
|
||||
OTHER_SWIFT_FLAGS = "-D UNIFFI";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension";
|
||||
PRODUCT_NAME = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "$(NE_PROFILE_ID)";
|
||||
@@ -645,13 +670,16 @@
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(CONNLIB_TARGET_DIR)/x86_64-apple-darwin/debug";
|
||||
MARKETING_VERSION = 1.5.9;
|
||||
OTHER_LDFLAGS = "-lconnlib";
|
||||
OTHER_SWIFT_FLAGS = "-D UNIFFI";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension";
|
||||
PRODUCT_NAME = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "$(NE_PROFILE_ID)";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = UNIFFI;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FirezoneNetworkExtension/FirezoneNetworkExtension-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
@@ -683,13 +711,16 @@
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(CONNLIB_TARGET_DIR)/x86_64-apple-darwin/release";
|
||||
MARKETING_VERSION = 1.5.9;
|
||||
OTHER_LDFLAGS = "-lconnlib";
|
||||
OTHER_SWIFT_FLAGS = "-D UNIFFI";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).network-extension";
|
||||
PRODUCT_NAME = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "$(NE_PROFILE_ID)";
|
||||
SDKROOT = macosx;
|
||||
SKIP_INSTALL = YES;
|
||||
SUPPORTED_PLATFORMS = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = UNIFFI;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_INCLUDE_PATHS = "$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "FirezoneNetworkExtension/FirezoneNetworkExtension-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
@@ -729,7 +760,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CONNLIB_SOURCE_DIR = "$(PROJECT_DIR)/../../rust/apple-client-ffi";
|
||||
CONNLIB_SOURCE_DIR = "$(PROJECT_DIR)/../../rust/client-ffi";
|
||||
CONNLIB_TARGET_DIR = "$(PROJECT_DIR)/../../rust/target";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
@@ -798,7 +829,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CONNLIB_SOURCE_DIR = "$(PROJECT_DIR)/../../rust/apple-client-ffi";
|
||||
CONNLIB_SOURCE_DIR = "$(PROJECT_DIR)/../../rust/client-ffi";
|
||||
CONNLIB_TARGET_DIR = "$(PROJECT_DIR)/../../rust/target";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
@@ -964,7 +995,6 @@
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
794C38142970A2660029F38F /* FirezoneKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
|
||||
@@ -95,6 +95,19 @@
|
||||
fatalError("Version should exist in bundle")
|
||||
}
|
||||
|
||||
Log.info(
|
||||
"Checking system extension - Client version: \(ourBundleShortVersion) (\(ourBundleVersion))"
|
||||
)
|
||||
|
||||
// Log all found extensions for debugging
|
||||
for sysex in properties {
|
||||
if sysex.isEnabled {
|
||||
Log.info(
|
||||
"Found enabled extension - Version: \(sysex.bundleShortVersion) (\(sysex.bundleVersion))"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Up to date if version and build number match
|
||||
let isCurrentVersionInstalled = properties.contains { sysex in
|
||||
sysex.isEnabled
|
||||
@@ -109,13 +122,17 @@
|
||||
|
||||
// Needs replacement if we found our extension, but its version doesn't match
|
||||
// Note this can happen for upgrades _or_ downgrades
|
||||
let isAnyVersionInstalled = properties.contains { $0.isEnabled }
|
||||
if isAnyVersionInstalled {
|
||||
let enabledExtension = properties.first { $0.isEnabled }
|
||||
if let enabledExtension = enabledExtension {
|
||||
Log.warning(
|
||||
"Extension version mismatch - Installed: \(enabledExtension.bundleShortVersion) (\(enabledExtension.bundleVersion)), Expected: \(ourBundleShortVersion) (\(ourBundleVersion))"
|
||||
)
|
||||
resume(returning: .needsReplacement)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.info("No system extension found - needs install")
|
||||
resume(returning: .needsInstall)
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,9 @@ public final class Store: ObservableObject {
|
||||
|
||||
// If already installed but the wrong version, go ahead and install. This shouldn't prompt the user.
|
||||
if systemExtensionStatus == .needsReplacement {
|
||||
Log.info("Replacing system extension with current version")
|
||||
try await systemExtensionRequest(.install)
|
||||
Log.info("System extension replacement completed successfully")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -230,9 +230,10 @@
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (
|
||||
UNNotificationPresentationOptions
|
||||
) -> Void
|
||||
withCompletionHandler completionHandler:
|
||||
@escaping (
|
||||
UNNotificationPresentationOptions
|
||||
) -> Void
|
||||
) {
|
||||
// Show the notification even when the app is in the foreground
|
||||
completionHandler([.badge, .banner, .sound])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//
|
||||
// Adapter.swift
|
||||
// (c) 2024 Firezone, Inc.
|
||||
// (c) 2024-2025 Firezone, Inc.
|
||||
// LICENSE: Apache-2.0
|
||||
//
|
||||
|
||||
@@ -14,7 +14,7 @@ import OSLog
|
||||
|
||||
enum AdapterError: Error {
|
||||
/// Failure to perform an operation in such state.
|
||||
case invalidSession(WrappedSession?)
|
||||
case invalidSession(Session?)
|
||||
|
||||
/// connlib failed to start
|
||||
case connlibConnectError(String)
|
||||
@@ -35,17 +35,19 @@ enum AdapterError: Error {
|
||||
}
|
||||
|
||||
// Loosely inspired from WireGuardAdapter from WireGuardKit
|
||||
class Adapter {
|
||||
typealias StartTunnelCompletionHandler = ((AdapterError?) -> Void)
|
||||
class Adapter: @unchecked Sendable {
|
||||
|
||||
private var callbackHandler: CallbackHandler
|
||||
/// Command sender for sending commands to the session
|
||||
private var commandSender: Sender<SessionCommand>?
|
||||
|
||||
private var session: WrappedSession?
|
||||
/// Task handles for explicit cancellation during cleanup
|
||||
private var eventLoopTask: Task<Void, Never>?
|
||||
private var eventConsumerTask: Task<Void, Never>?
|
||||
|
||||
// Our local copy of the accountSlug
|
||||
private var accountSlug: String
|
||||
private let accountSlug: String
|
||||
|
||||
/// Network settings
|
||||
/// Network settings for tunnel configuration.
|
||||
private var networkSettings: NetworkSettings?
|
||||
|
||||
/// Packet tunnel provider.
|
||||
@@ -54,11 +56,6 @@ class Adapter {
|
||||
/// Network routes monitor.
|
||||
private var networkMonitor: NWPathMonitor?
|
||||
|
||||
/// Used to avoid path update callback cycles on iOS
|
||||
#if os(iOS)
|
||||
private var gateways: [Network.NWEndpoint] = []
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
/// Used for finding system DNS resolvers on macOS when network conditions have changed.
|
||||
private let systemConfigurationResolvers = SystemConfigurationResolvers()
|
||||
@@ -130,7 +127,7 @@ class Adapter {
|
||||
// out of a different interface even when 0.0.0.0 is used as the source.
|
||||
// If our primary interface changes, we can be certain the old socket shouldn't be
|
||||
// used anymore.
|
||||
session?.reset("primary network path changed")
|
||||
self.sendCommand(.reset("primary network path changed"))
|
||||
}
|
||||
|
||||
setSystemDefaultResolvers(path)
|
||||
@@ -139,26 +136,22 @@ class Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Currently disabled resources
|
||||
/// Internet resource enabled state
|
||||
private var internetResourceEnabled: Bool
|
||||
|
||||
/// Cache of internet resource
|
||||
private var internetResource: Resource?
|
||||
|
||||
/// Keep track of resources
|
||||
/// Keep track of resources for UI
|
||||
private var resourceListJSON: String?
|
||||
|
||||
/// Starting parameters
|
||||
private let apiURL: String
|
||||
private let token: Token
|
||||
private let id: String
|
||||
private let deviceId: String
|
||||
private let logFilter: String
|
||||
private let connlibLogFolderPath: String
|
||||
|
||||
init(
|
||||
apiURL: String,
|
||||
token: Token,
|
||||
id: String,
|
||||
deviceId: String,
|
||||
logFilter: String,
|
||||
accountSlug: String,
|
||||
internetResourceEnabled: Bool,
|
||||
@@ -166,58 +159,98 @@ class Adapter {
|
||||
) {
|
||||
self.apiURL = apiURL
|
||||
self.token = token
|
||||
self.id = id
|
||||
self.packetTunnelProvider = packetTunnelProvider
|
||||
self.callbackHandler = CallbackHandler()
|
||||
self.deviceId = deviceId
|
||||
self.logFilter = logFilter
|
||||
self.accountSlug = accountSlug
|
||||
self.connlibLogFolderPath = SharedAccess.connlibLogFolderURL?.path ?? ""
|
||||
self.networkSettings = nil
|
||||
self.internetResourceEnabled = internetResourceEnabled
|
||||
self.packetTunnelProvider = packetTunnelProvider
|
||||
}
|
||||
|
||||
// Could happen abruptly if the process is killed.
|
||||
deinit {
|
||||
Log.log("Adapter.deinit")
|
||||
|
||||
// Cancel all Tasks - this triggers cooperative cancellation
|
||||
// Event loop checks Task.isCancelled in its polling loop
|
||||
// Event consumer will exit when eventSender.deinit closes the stream
|
||||
eventLoopTask?.cancel()
|
||||
eventConsumerTask?.cancel()
|
||||
|
||||
// Cancel network monitor
|
||||
networkMonitor?.cancel()
|
||||
}
|
||||
|
||||
/// Start the tunnel.
|
||||
|
||||
func start() throws {
|
||||
Log.log("Adapter.start")
|
||||
Log.log("Adapter.start: Starting session for account: \(accountSlug)")
|
||||
|
||||
guard session == nil else {
|
||||
throw AdapterError.invalidSession(session)
|
||||
}
|
||||
// Get device metadata
|
||||
let deviceName = DeviceMetadata.getDeviceName()
|
||||
let osVersion = DeviceMetadata.getOSVersion()
|
||||
let deviceInfo = try JSONEncoder().encode(DeviceMetadata.deviceInfo())
|
||||
let deviceInfoStr = String(data: deviceInfo, encoding: .utf8) ?? "{}"
|
||||
let logDir = SharedAccess.connlibLogFolderURL?.path ?? "/tmp/firezone"
|
||||
|
||||
callbackHandler.delegate = self
|
||||
|
||||
Log.log("Adapter.start: Starting connlib")
|
||||
// Create the session
|
||||
let session: Session
|
||||
do {
|
||||
let jsonEncoder = JSONEncoder()
|
||||
jsonEncoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
|
||||
// Grab a session pointer
|
||||
session = try WrappedSession.connect(
|
||||
apiURL,
|
||||
"\(token)",
|
||||
"\(id)",
|
||||
accountSlug,
|
||||
DeviceMetadata.getDeviceName(),
|
||||
DeviceMetadata.getOSVersion(),
|
||||
connlibLogFolderPath,
|
||||
logFilter,
|
||||
internetResourceEnabled,
|
||||
callbackHandler,
|
||||
String(data: jsonEncoder.encode(DeviceMetadata.deviceInfo()), encoding: .utf8)!
|
||||
session = try Session.newApple(
|
||||
apiUrl: apiURL,
|
||||
token: token.description,
|
||||
deviceId: deviceId,
|
||||
accountSlug: accountSlug,
|
||||
deviceName: deviceName,
|
||||
osVersion: osVersion,
|
||||
logDir: logDir,
|
||||
logFilter: logFilter,
|
||||
deviceInfo: deviceInfoStr,
|
||||
isInternetResourceActive: internetResourceEnabled
|
||||
)
|
||||
} catch let error {
|
||||
// `toString` needed to deep copy the string and avoid a possible dangling pointer
|
||||
let msg = (error as? RustString)?.toString() ?? "Unknown error"
|
||||
throw AdapterError.connlibConnectError(msg)
|
||||
} catch {
|
||||
throw AdapterError.connlibConnectError(String(describing: error))
|
||||
}
|
||||
|
||||
// Create channels - following Rust pattern with separate sender/receiver
|
||||
let (commandSender, commandReceiver): (Sender<SessionCommand>, Receiver<SessionCommand>) =
|
||||
Channel.create()
|
||||
self.commandSender = commandSender
|
||||
|
||||
let (eventSender, eventReceiver): (Sender<Event>, Receiver<Event>) = Channel.create()
|
||||
|
||||
// Start event loop - owns session, receives commands, sends events
|
||||
eventLoopTask = Task { [weak self] in
|
||||
defer {
|
||||
// ALWAYS cleanup, even if event loop crashes
|
||||
self?.commandSender = nil
|
||||
Log.log("Adapter: Event loop finished, session dropped")
|
||||
}
|
||||
|
||||
await runSessionEventLoop(
|
||||
session: session,
|
||||
commandReceiver: commandReceiver,
|
||||
eventSender: eventSender
|
||||
)
|
||||
}
|
||||
|
||||
// Start event consumer - consumes events from receiver (Rust pattern: receiver outside)
|
||||
eventConsumerTask = Task { [weak self] in
|
||||
for await event in eventReceiver.stream {
|
||||
// Check self on each iteration - if Adapter is deallocated, stop processing events
|
||||
guard let self = self else {
|
||||
Log.log("Adapter: Event consumer stopping - Adapter deallocated")
|
||||
break
|
||||
}
|
||||
|
||||
await self.handleEvent(event)
|
||||
}
|
||||
|
||||
Log.log("Adapter: Event consumer finished")
|
||||
}
|
||||
|
||||
// Configure DNS and path monitoring
|
||||
startNetworkPathMonitoring()
|
||||
|
||||
Log.log("Adapter.start: Session started successfully")
|
||||
}
|
||||
|
||||
/// Final callback called by packetTunnelProvider when tunnel is to be stopped.
|
||||
@@ -232,11 +265,13 @@ class Adapter {
|
||||
func stop() {
|
||||
Log.log("Adapter.stop")
|
||||
|
||||
// Assigning `nil` will invoke `Drop` on the Rust side
|
||||
session = nil
|
||||
sendCommand(.disconnect)
|
||||
|
||||
networkMonitor?.cancel()
|
||||
networkMonitor = nil
|
||||
|
||||
// Tasks will finish naturally after disconnect command is processed
|
||||
// No need to cancel them here - they'll clean up via their defer blocks
|
||||
}
|
||||
|
||||
/// Get the current set of resources in the completionHandler, only returning
|
||||
@@ -258,119 +293,91 @@ class Adapter {
|
||||
}
|
||||
|
||||
func reset(reason: String, path: Network.NWPath? = nil) {
|
||||
session?.reset(reason)
|
||||
sendCommand(.reset(reason))
|
||||
|
||||
if let path = (path ?? lastPath) {
|
||||
setSystemDefaultResolvers(path)
|
||||
}
|
||||
}
|
||||
|
||||
func resources() -> [Resource] {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
guard let resourceList = resourceListJSON else { return [] }
|
||||
return (try? decoder.decode([Resource].self, from: resourceList.data(using: .utf8)!)) ?? []
|
||||
}
|
||||
|
||||
func setInternetResourceEnabled(_ enabled: Bool) {
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
self.internetResourceEnabled = enabled
|
||||
session?.setInternetResourceState(enabled)
|
||||
self.sendCommand(.setInternetResourceState(enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Responding to path updates
|
||||
// MARK: - Event handling
|
||||
|
||||
extension Adapter {
|
||||
private func beginPathMonitoring() {
|
||||
let networkMonitor = NWPathMonitor()
|
||||
networkMonitor.pathUpdateHandler = self.pathUpdateHandler
|
||||
networkMonitor.start(queue: self.workQueue)
|
||||
}
|
||||
}
|
||||
private func handleEvent(_ event: Event) async {
|
||||
switch event {
|
||||
case .tunInterfaceUpdated(
|
||||
let ipv4, let ipv6, let dns, let searchDomain, let ipv4Routes, let ipv6Routes):
|
||||
Log.log("Received TunInterfaceUpdated event")
|
||||
|
||||
// MARK: Implementing CallbackHandlerDelegate
|
||||
|
||||
extension Adapter: CallbackHandlerDelegate {
|
||||
func onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
searchDomain: String?,
|
||||
dnsAddresses: [String],
|
||||
routeListv4: String,
|
||||
routeListv6: String
|
||||
) {
|
||||
// This is a queued callback to ensure ordering
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let networkSettings =
|
||||
networkSettings ?? NetworkSettings(packetTunnelProvider: packetTunnelProvider)
|
||||
|
||||
guard let data4 = routeListv4.data(using: .utf8),
|
||||
let data6 = routeListv6.data(using: .utf8),
|
||||
// Decode all data into local variables first to ensure all parsing succeeds before applying
|
||||
guard let dnsData = dns.data(using: .utf8),
|
||||
let dnsAddresses = try? JSONDecoder().decode([String].self, from: dnsData),
|
||||
let data4 = ipv4Routes.data(using: .utf8),
|
||||
let data6 = ipv6Routes.data(using: .utf8),
|
||||
let decoded4 = try? JSONDecoder().decode([NetworkSettings.Cidr].self, from: data4),
|
||||
let decoded6 = try? JSONDecoder().decode([NetworkSettings.Cidr].self, from: data6)
|
||||
else {
|
||||
fatalError("Could not decode route list from connlib")
|
||||
fatalError("Could not decode network configuration from connlib")
|
||||
}
|
||||
|
||||
let routes4 = decoded4.compactMap({ $0.asNEIPv4Route })
|
||||
let routes6 = decoded6.compactMap({ $0.asNEIPv6Route })
|
||||
|
||||
networkSettings.tunnelAddressIPv4 = tunnelAddressIPv4
|
||||
networkSettings.tunnelAddressIPv6 = tunnelAddressIPv6
|
||||
// All decoding succeeded - now apply settings atomically
|
||||
guard let provider = packetTunnelProvider else {
|
||||
Log.error(AdapterError.invalidSession(nil))
|
||||
return
|
||||
}
|
||||
|
||||
Log.log("Setting interface config")
|
||||
|
||||
let networkSettings = NetworkSettings(packetTunnelProvider: provider)
|
||||
networkSettings.tunnelAddressIPv4 = ipv4
|
||||
networkSettings.tunnelAddressIPv6 = ipv6
|
||||
networkSettings.dnsAddresses = dnsAddresses
|
||||
networkSettings.routes4 = routes4
|
||||
networkSettings.routes6 = routes6
|
||||
networkSettings.setSearchDomain(domain: searchDomain)
|
||||
self.networkSettings = networkSettings
|
||||
|
||||
// Now that we have our interface configured, start listening for events. The first one will be us applying
|
||||
// our network settings in the call below. We need the physical interface name macOS chooses for us in order
|
||||
// to get the correct DNS resolvers on macOS. For that, we need the path parameter from the path update callback.
|
||||
beginPathMonitoring()
|
||||
|
||||
networkSettings.apply()
|
||||
}
|
||||
}
|
||||
|
||||
func onUpdateResources(resourceList: String) {
|
||||
// This is a queued callback to ensure ordering
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
case .resourcesUpdated(let resources):
|
||||
Log.log("Received ResourcesUpdated event with \(resources.count) bytes")
|
||||
|
||||
if resourceListJSON != resourceList, let networkSettings = self.networkSettings {
|
||||
// Update resource List. We don't care what's inside.
|
||||
resourceListJSON = resourceList
|
||||
// Store resource list
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.resourceListJSON = resources
|
||||
}
|
||||
|
||||
// Apply network settings to flush DNS cache when resources change
|
||||
// This ensures new DNS resources are immediately resolvable
|
||||
// Apply network settings to flush DNS cache when resources change
|
||||
// This ensures new DNS resources are immediately resolvable
|
||||
if let networkSettings = networkSettings {
|
||||
Log.log("Reapplying network settings to flush DNS cache after resource update")
|
||||
networkSettings.apply()
|
||||
}
|
||||
|
||||
session?.setInternetResourceState(self.internetResourceEnabled)
|
||||
}
|
||||
}
|
||||
case .disconnected(let error):
|
||||
let errorMessage = error.message()
|
||||
Log.info("Received Disconnected event: \(errorMessage)")
|
||||
|
||||
func onDisconnect(error: DisconnectError) {
|
||||
// Since connlib has already shutdown by this point, we queue this callback
|
||||
// to ensure that we can clean up even if connlib exits before we are done.
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
// Immediately invalidate our session pointer to prevent workQueue items from trying to use it.
|
||||
// Assigning to `nil` will invoke `Drop` on the Rust side.
|
||||
// This must happen asynchronously and not as part of the callback to allow Rust to break
|
||||
// cyclic dependencies between the runtime and the task that is executing the callback.
|
||||
self.session = nil
|
||||
guard let provider = packetTunnelProvider else {
|
||||
Log.error(AdapterError.invalidSession(nil))
|
||||
return
|
||||
}
|
||||
|
||||
// If auth expired/is invalid, delete stored token and save the reason why so the GUI can act upon it.
|
||||
if error.isAuthenticationError() {
|
||||
// Delete stored token and save the reason for the GUI
|
||||
do {
|
||||
try Token.delete()
|
||||
let reason: NEProviderStopReason = .authenticationCanceled
|
||||
@@ -379,17 +386,26 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
} catch {
|
||||
Log.error(error)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
// iOS notifications should be shown from the tunnel process
|
||||
SessionNotification.showSignedOutNotificationiOS()
|
||||
#endif
|
||||
} else {
|
||||
Log.warning("Disconnected with error: \(errorMessage)")
|
||||
}
|
||||
|
||||
// Tell the system to shut us down
|
||||
self.packetTunnelProvider?.cancelTunnelWithError(nil)
|
||||
// Handle disconnection
|
||||
provider.cancelTunnelWithError(nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func startNetworkPathMonitoring() {
|
||||
networkMonitor = NWPathMonitor()
|
||||
networkMonitor?.pathUpdateHandler = self.pathUpdateHandler
|
||||
networkMonitor?.start(queue: workQueue)
|
||||
}
|
||||
|
||||
private func setSystemDefaultResolvers(_ path: Network.NWPath) {
|
||||
// Step 1: Get system default resolvers
|
||||
#if os(macOS)
|
||||
@@ -448,15 +464,14 @@ extension Adapter: CallbackHandlerDelegate {
|
||||
}
|
||||
|
||||
// Step 4: Send to connlib
|
||||
do {
|
||||
Log.log("Sending resolvers to connlib: \(jsonResolvers)")
|
||||
try session?.setDns(jsonResolvers.intoRustString())
|
||||
} catch let error {
|
||||
// `toString` needed to deep copy the string and avoid a possible dangling pointer
|
||||
let msg = (error as? RustString)?.toString() ?? "Unknown error"
|
||||
Log.error(AdapterError.setDnsError(msg))
|
||||
}
|
||||
Log.log("Sending resolvers to connlib: \(jsonResolvers)")
|
||||
sendCommand(.setDns(jsonResolvers))
|
||||
}
|
||||
|
||||
private func sendCommand(_ command: SessionCommand) {
|
||||
commandSender?.send(command)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Getting System Resolvers on iOS
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// CallbackHandler.swift
|
||||
//
|
||||
|
||||
import FirezoneKit
|
||||
import NetworkExtension
|
||||
import OSLog
|
||||
|
||||
// When the FFI changes from the Rust side, change the CallbackHandler
|
||||
// functions along with that, but not the delegate protocol.
|
||||
// When the app gets updated to use the FFI, the delegate protocol
|
||||
// shall get updated.
|
||||
// This is so that the app stays buildable even when the FFI changes.
|
||||
|
||||
// See https://github.com/chinedufn/swift-bridge/issues/150
|
||||
extension RustString: @unchecked Sendable {}
|
||||
extension RustString: Error {}
|
||||
|
||||
public protocol CallbackHandlerDelegate: AnyObject {
|
||||
func onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: String,
|
||||
tunnelAddressIPv6: String,
|
||||
searchDomain: String?,
|
||||
dnsAddresses: [String],
|
||||
routeListv4: String,
|
||||
routeListv6: String
|
||||
)
|
||||
func onUpdateResources(resourceList: String)
|
||||
func onDisconnect(error: DisconnectError)
|
||||
}
|
||||
|
||||
public class CallbackHandler {
|
||||
public weak var delegate: CallbackHandlerDelegate?
|
||||
func onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: RustString,
|
||||
tunnelAddressIPv6: RustString,
|
||||
searchDomain: RustString?,
|
||||
dnsAddresses: RustString,
|
||||
routeListv4: RustString,
|
||||
routeListv6: RustString
|
||||
) {
|
||||
Log.log(
|
||||
"""
|
||||
CallbackHandler.onSetInterfaceConfig:
|
||||
IPv4: \(tunnelAddressIPv4.toString())
|
||||
IPv6: \(tunnelAddressIPv6.toString())
|
||||
SearchDomain: \(String(describing: (searchDomain?.toString())))
|
||||
DNS: \(dnsAddresses.toString())
|
||||
IPv4 routes: \(routeListv4.toString())
|
||||
IPv6 routes: \(routeListv6.toString())
|
||||
""")
|
||||
|
||||
guard let dnsData = dnsAddresses.toString().data(using: .utf8),
|
||||
let dnsArray = try? JSONDecoder().decode([String].self, from: dnsData)
|
||||
else {
|
||||
fatalError("Should be able to decode DNS Addresses from connlib")
|
||||
}
|
||||
|
||||
delegate?.onSetInterfaceConfig(
|
||||
tunnelAddressIPv4: tunnelAddressIPv4.toString(),
|
||||
tunnelAddressIPv6: tunnelAddressIPv6.toString(),
|
||||
searchDomain: searchDomain?.toString(),
|
||||
dnsAddresses: dnsArray,
|
||||
routeListv4: routeListv4.toString(),
|
||||
routeListv6: routeListv6.toString()
|
||||
)
|
||||
}
|
||||
|
||||
func onUpdateResources(resourceList: RustString) {
|
||||
Log.log("CallbackHandler.onUpdateResources: \(resourceList.toString())")
|
||||
delegate?.onUpdateResources(resourceList: resourceList.toString())
|
||||
}
|
||||
|
||||
func onDisconnect(error: DisconnectError) {
|
||||
let errorStr = error.toString()
|
||||
Log.log("CallbackHandler.onDisconnect: \(errorStr)")
|
||||
delegate?.onDisconnect(error: error)
|
||||
}
|
||||
}
|
||||
82
swift/apple/FirezoneNetworkExtension/Channel.swift
Normal file
82
swift/apple/FirezoneNetworkExtension/Channel.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
import Foundation
|
||||
|
||||
/// Sender side of a channel - can only send values.
|
||||
///
|
||||
/// This wraps AsyncStream.Continuation to provide automatic cleanup via RAII.
|
||||
/// When the sender is deallocated, the continuation is automatically finished,
|
||||
/// matching Rust's behaviour where dropping a sender closes the channel.
|
||||
final class Sender<T: Sendable>: Sendable {
|
||||
private let continuation: AsyncStream<T>.Continuation
|
||||
|
||||
fileprivate init(continuation: AsyncStream<T>.Continuation) {
|
||||
self.continuation = continuation
|
||||
}
|
||||
|
||||
/// Sends a value into the channel.
|
||||
@discardableResult
|
||||
func send(_ value: T) -> AsyncStream<T>.Continuation.YieldResult {
|
||||
continuation.yield(value)
|
||||
}
|
||||
|
||||
/// Explicitly finishes the channel (optional, as deinit will do this automatically).
|
||||
func finish() {
|
||||
continuation.finish()
|
||||
}
|
||||
|
||||
deinit {
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Receiver side of a channel - can only receive values.
|
||||
///
|
||||
/// This wraps AsyncStream to provide the receiving end of the channel.
|
||||
/// Values can be consumed by iterating over the stream:
|
||||
///
|
||||
/// for await value in receiver.stream {
|
||||
/// // handle value
|
||||
/// }
|
||||
///
|
||||
final class Receiver<T: Sendable>: Sendable {
|
||||
let stream: AsyncStream<T>
|
||||
|
||||
fileprivate init(stream: AsyncStream<T>) {
|
||||
self.stream = stream
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel factory - creates sender/receiver pairs matching Rust's channel pattern.
|
||||
///
|
||||
/// This provides a type-safe way to create unidirectional communication channels,
|
||||
/// where the sender can only send and the receiver can only receive.
|
||||
///
|
||||
/// Example usage:
|
||||
///
|
||||
/// let (sender, receiver) = Channel.create<Event>()
|
||||
///
|
||||
/// // Producer task
|
||||
/// Task {
|
||||
/// sender.send(someEvent)
|
||||
/// }
|
||||
///
|
||||
/// // Consumer task
|
||||
/// Task {
|
||||
/// for await event in receiver.stream {
|
||||
/// handle(event)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
struct Channel {
|
||||
/// Creates a sender/receiver pair for type-safe unidirectional communication.
|
||||
static func create<T: Sendable>() -> (Sender<T>, Receiver<T>) {
|
||||
var continuation: AsyncStream<T>.Continuation!
|
||||
let stream = AsyncStream<T> { cont in
|
||||
continuation = cont
|
||||
}
|
||||
|
||||
let sender = Sender(continuation: continuation)
|
||||
let receiver = Receiver(stream: stream)
|
||||
|
||||
return (sender, receiver)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,2 @@
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated/apple-client-ffi/apple-client-ffi.swift
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated/apple-client-ffi/apple-client-ffi.h
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated/SwiftBridgeCore.h
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated/SwiftBridgeCore.swift
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/connlib.h
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated/connlib.swift
|
||||
$(PROJECT_DIR)/FirezoneNetworkExtension/Connlib/Generated/connlibFFI.h
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#ifndef connlib_Bridging_Header_h
|
||||
#define connlib_Bridging_Header_h
|
||||
|
||||
// Import the generated UniFFI C header
|
||||
#import "connlibFFI.h"
|
||||
|
||||
#endif /* connlib_Bridging_Header_h */
|
||||
@@ -1,9 +0,0 @@
|
||||
// Umbrella header for connlib
|
||||
|
||||
#ifndef connlib_h
|
||||
#define connlib_h
|
||||
|
||||
#include "Generated/SwiftBridgeCore.h"
|
||||
#include "Generated/apple-client-ffi/apple-client-ffi.h"
|
||||
|
||||
#endif
|
||||
@@ -2,5 +2,7 @@
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#include "Connlib/connlib.h"
|
||||
// UniFFI generated header
|
||||
#include "Connlib/Generated/connlibFFI.h"
|
||||
|
||||
#include <resolv.h>
|
||||
|
||||
@@ -35,6 +35,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
super.init()
|
||||
|
||||
// Log version information immediately on startup
|
||||
let version =
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
||||
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
|
||||
Log.info(
|
||||
"NetworkExtension starting - Version: \(version), Build: \(build), Bundle ID: \(bundleId)")
|
||||
|
||||
migrateFirezoneId()
|
||||
self.tunnelConfiguration = TunnelConfiguration.tryLoad()
|
||||
}
|
||||
@@ -51,10 +59,17 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
|
||||
// Dummy start to get the extension running on macOS after upgrade
|
||||
if options?["dryRun"] as? Bool == true {
|
||||
Log.info("Dry run startup requested - extension awakened but not starting tunnel")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Log version on actual tunnel start
|
||||
let version =
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown"
|
||||
let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "unknown"
|
||||
Log.info("Starting tunnel - Version: \(version), Build: \(build)")
|
||||
|
||||
// If the tunnel starts up before the GUI after an upgrade crossing the 1.4.15 version boundary,
|
||||
// the old system settings-based config will still be present and the new configuration will be empty.
|
||||
// So handle that edge case gracefully.
|
||||
@@ -93,16 +108,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
let internetResourceEnabled =
|
||||
enabled != nil ? enabled == "true" : (tunnelConfiguration?.internetResourceEnabled ?? false)
|
||||
|
||||
// Create the adapter with all configuration
|
||||
let adapter = Adapter(
|
||||
apiURL: apiURL,
|
||||
token: token,
|
||||
id: id,
|
||||
deviceId: id,
|
||||
logFilter: logFilter,
|
||||
accountSlug: accountSlug,
|
||||
internetResourceEnabled: internetResourceEnabled,
|
||||
packetTunnelProvider: self
|
||||
)
|
||||
|
||||
// Start the adapter
|
||||
try adapter.start()
|
||||
|
||||
self.adapter = adapter
|
||||
@@ -149,11 +166,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
case .setConfiguration(let tunnelConfiguration):
|
||||
tunnelConfiguration.save()
|
||||
self.tunnelConfiguration = tunnelConfiguration
|
||||
|
||||
self.adapter?.setInternetResourceEnabled(tunnelConfiguration.internetResourceEnabled)
|
||||
completionHandler?(nil)
|
||||
|
||||
case .signOut:
|
||||
do { try Token.delete() } catch { Log.error(error) }
|
||||
completionHandler?(nil)
|
||||
case .getResourceList(let hash):
|
||||
guard let adapter = adapter
|
||||
else {
|
||||
@@ -163,6 +182,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
return
|
||||
}
|
||||
|
||||
// Use hash comparison to only return resources if they've changed
|
||||
adapter.getResourcesIfVersionDifferentFrom(hash: hash) { resourceListJSON in
|
||||
completionHandler?(resourceListJSON?.data(using: .utf8))
|
||||
}
|
||||
|
||||
91
swift/apple/FirezoneNetworkExtension/SessionEventLoop.swift
Normal file
91
swift/apple/FirezoneNetworkExtension/SessionEventLoop.swift
Normal file
@@ -0,0 +1,91 @@
|
||||
import FirezoneKit
|
||||
import Foundation
|
||||
|
||||
/// Commands that can be sent to the Session.
|
||||
enum SessionCommand {
|
||||
case disconnect
|
||||
case setInternetResourceState(Bool)
|
||||
case setDns(String)
|
||||
case reset(String)
|
||||
}
|
||||
|
||||
/// Runs the session event loop, owning the Session lifecycle.
|
||||
///
|
||||
/// When either task completes, both are cancelled and the function returns.
|
||||
/// This ensures the Session's Drop is called on the Rust side.
|
||||
func runSessionEventLoop(
|
||||
session: Session,
|
||||
commandReceiver: Receiver<SessionCommand>,
|
||||
eventSender: Sender<Event>
|
||||
) async {
|
||||
|
||||
// Multiplex between commands and events
|
||||
await withTaskGroup(of: Void.self) { group in
|
||||
// Event polling task - polls Rust for events and sends to eventSender
|
||||
group.addTask {
|
||||
while !Task.isCancelled {
|
||||
do {
|
||||
// Poll for next event from Rust
|
||||
guard let event = try await session.nextEvent() else {
|
||||
// No event returned - session has ended
|
||||
Log.log("SessionEventLoop: Event stream ended, exiting event loop")
|
||||
break
|
||||
}
|
||||
|
||||
eventSender.send(event)
|
||||
} catch {
|
||||
Log.error(error)
|
||||
Log.log("SessionEventLoop: Error polling event, continuing")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
Log.log("SessionEventLoop: Event polling finished")
|
||||
}
|
||||
|
||||
// Command handling task - receives commands from commandReceiver
|
||||
group.addTask {
|
||||
for await command in commandReceiver.stream {
|
||||
await handleCommand(command, session: session)
|
||||
|
||||
// Exit loop if disconnect command
|
||||
if case .disconnect = command {
|
||||
Log.log("SessionEventLoop: Disconnect command received, exiting command loop")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
Log.log("SessionEventLoop: Command handling finished")
|
||||
}
|
||||
|
||||
// Wait for first task to complete, then cancel all
|
||||
_ = await group.next()
|
||||
Log.log("SessionEventLoop: One task completed, cancelling event loop")
|
||||
group.cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles a command by calling the appropriate session method.
|
||||
private func handleCommand(_ command: SessionCommand, session: Session) async {
|
||||
switch command {
|
||||
case .disconnect:
|
||||
do {
|
||||
try session.disconnect()
|
||||
} catch {
|
||||
Log.error(error)
|
||||
}
|
||||
|
||||
case .setInternetResourceState(let active):
|
||||
session.setInternetResourceState(active: active)
|
||||
|
||||
case .setDns(let servers):
|
||||
do {
|
||||
try session.setDns(dnsServers: servers)
|
||||
} catch {
|
||||
Log.error(error)
|
||||
}
|
||||
|
||||
case .reset(let reason):
|
||||
session.reset(reason: reason)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,29 @@
|
||||
# Creates a macOS debug build
|
||||
|
||||
PLATFORM=macOS
|
||||
PLATFORM?=macOS
|
||||
ARCH=$(shell uname -m)
|
||||
CONFIGURATION?=Debug
|
||||
|
||||
# Path to rust target directory (relative to this Makefile)
|
||||
RUST_TARGET_DIR=$(abspath ../../rust/target)
|
||||
|
||||
# Set consistent environment to prevent Rust rebuilds
|
||||
# This must match the Xcode project setting
|
||||
export MACOSX_DEPLOYMENT_TARGET=12.4
|
||||
|
||||
build-macos:
|
||||
echo "Building debug build for ${PLATFORM}, ${ARCH}"
|
||||
@xcodebuild build -scheme Firezone -sdk macosx -destination 'platform=${PLATFORM},arch=${ARCH}'
|
||||
# Map architecture names for Rust targets
|
||||
ifeq ($(ARCH),arm64)
|
||||
RUST_TARGET=aarch64-apple-darwin
|
||||
else
|
||||
RUST_TARGET=x86_64-apple-darwin
|
||||
endif
|
||||
|
||||
# Get the current Git SHA for build identification
|
||||
GIT_SHA=$(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
|
||||
# Default target: build and install
|
||||
.PHONY: all
|
||||
all: build install
|
||||
|
||||
# Info for sourcekit-lsp (LSP server for other IDEs)
|
||||
lsp:
|
||||
@@ -17,6 +31,20 @@ lsp:
|
||||
-project Firezone.xcodeproj \
|
||||
-scheme Firezone
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@echo "Building Xcode project for ${PLATFORM}, ${ARCH}"
|
||||
@echo "Git SHA: ${GIT_SHA}"
|
||||
@xcodebuild build \
|
||||
-project Firezone.xcodeproj \
|
||||
-scheme Firezone \
|
||||
-configuration $(CONFIGURATION) \
|
||||
-sdk macosx \
|
||||
-destination 'platform=${PLATFORM},arch=${ARCH}' \
|
||||
CONNLIB_TARGET_DIR="${RUST_TARGET_DIR}" \
|
||||
GIT_SHA="${GIT_SHA}" \
|
||||
ONLY_ACTIVE_ARCH=YES
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
@echo "Stopping any running Firezone instances..."
|
||||
@@ -32,9 +60,18 @@ install:
|
||||
@echo "Launching Firezone..."
|
||||
@open /Applications/Firezone.app
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@xcodebuild clean -scheme Firezone -sdk macosx -destination 'platform=${PLATFORM},arch=${ARCH}'
|
||||
cd ../../rust/apple-client-ffi && rm -rf ./Connlib.xcframework
|
||||
@echo "Cleaning Xcode build"
|
||||
@xcodebuild clean \
|
||||
-project Firezone.xcodeproj \
|
||||
-scheme Firezone \
|
||||
-configuration $(CONFIGURATION) \
|
||||
-sdk macosx
|
||||
@echo "Cleaning Rust build artifacts"
|
||||
@cd ../../rust/client-ffi && cargo clean
|
||||
@echo "Removing Generated bindings"
|
||||
@rm -rf FirezoneNetworkExtension/Connlib/Generated
|
||||
|
||||
.PHONY: format
|
||||
format:
|
||||
@@ -42,6 +79,14 @@ format:
|
||||
@find . -name "*.swift" -not -path "./FirezoneNetworkExtension/Connlib/Generated/*" -not -path "./FirezoneKit/.build/*" | xargs swift format format --in-place --parallel
|
||||
@echo "Linting Swift code..."
|
||||
@find . -name "*.swift" -not -path "./FirezoneNetworkExtension/Connlib/Generated/*" -not -path "./FirezoneKit/.build/*" | xargs swift format lint --parallel --strict
|
||||
|
||||
.PHONY: setup
|
||||
setup:
|
||||
@echo "Installing required Rust targets..."
|
||||
@rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||
@rustup target add aarch64-apple-ios x86_64-apple-ios
|
||||
@echo "Setup complete!"
|
||||
|
||||
.PHONY: show-client-log
|
||||
show-client-log:
|
||||
@echo "Opening latest Firezone client log..."
|
||||
|
||||
206
swift/apple/build-rust.sh
Executable file
206
swift/apple/build-rust.sh
Executable file
@@ -0,0 +1,206 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Error handler
|
||||
trap 'echo "ERROR: Build script failed at line $LINENO" >&2' ERR
|
||||
|
||||
# Get script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
RUST_DIR="$SCRIPT_DIR/../../rust"
|
||||
GENERATED_DIR="$SCRIPT_DIR/FirezoneNetworkExtension/Connlib/Generated"
|
||||
|
||||
# Sanitize the environment to prevent Xcode's shenanigans from leaking
|
||||
# into our highly evolved Rust-based build system.
|
||||
for var in $(env | awk -F= '{print $1}'); do
|
||||
if [[ "$var" != "HOME" ]] &&
|
||||
[[ "$var" != "MACOSX_DEPLOYMENT_TARGET" ]] &&
|
||||
[[ "$var" != "IPHONEOS_DEPLOYMENT_TARGET" ]] &&
|
||||
[[ "$var" != "USER" ]] &&
|
||||
[[ "$var" != "LOGNAME" ]] &&
|
||||
[[ "$var" != "TERM" ]] &&
|
||||
[[ "$var" != "PWD" ]] &&
|
||||
[[ "$var" != "SHELL" ]] &&
|
||||
[[ "$var" != "TMPDIR" ]] &&
|
||||
[[ "$var" != "XPC_FLAGS" ]] &&
|
||||
[[ "$var" != "XPC_SERVICE_NAME" ]] &&
|
||||
[[ "$var" != "PLATFORM_NAME" ]] &&
|
||||
[[ "$var" != "CONFIGURATION" ]] &&
|
||||
[[ "$var" != "NATIVE_ARCH" ]] &&
|
||||
[[ "$var" != "ONLY_ACTIVE_ARCH" ]] &&
|
||||
[[ "$var" != "ARCHS" ]] &&
|
||||
[[ "$var" != "SDKROOT" ]] &&
|
||||
[[ "$var" != "OBJROOT" ]] &&
|
||||
[[ "$var" != "SYMROOT" ]] &&
|
||||
[[ "$var" != "SRCROOT" ]] &&
|
||||
[[ "$var" != "TARGETED_DEVICE_FAMILY" ]] &&
|
||||
[[ "$var" != "RUSTC_WRAPPER" ]] &&
|
||||
[[ "$var" != "RUST_TOOLCHAIN" ]] &&
|
||||
[[ "$var" != "SCCACHE_GCS_BUCKET" ]] &&
|
||||
[[ "$var" != "SCCACHE_GCS_RW_MODE" ]] &&
|
||||
[[ "$var" != "GOOGLE_CLOUD_PROJECT" ]] &&
|
||||
[[ "$var" != "GCP_PROJECT" ]] &&
|
||||
[[ "$var" != "GCLOUD_PROJECT" ]] &&
|
||||
[[ "$var" != "CLOUDSDK_PROJECT" ]] &&
|
||||
[[ "$var" != "CLOUDSDK_CORE_PROJECT" ]] &&
|
||||
[[ "$var" != "GOOGLE_GHA_CREDS_PATH" ]] &&
|
||||
[[ "$var" != "GOOGLE_APPLICATION_CREDENTIALS" ]] &&
|
||||
[[ "$var" != "CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE" ]] &&
|
||||
[[ "$var" != "ACTIONS_CACHE_URL" ]] &&
|
||||
[[ "$var" != "ACTIONS_RUNTIME_TOKEN" ]] &&
|
||||
[[ "$var" != "CARGO_INCREMENTAL" ]] &&
|
||||
[[ "$var" != "CARGO_TERM_COLOR" ]] &&
|
||||
[[ "$var" != "FIREZONE_PACKAGE_VERSION" ]] &&
|
||||
[[ "$var" != "CONNLIB_TARGET_DIR" ]]; then
|
||||
unset "$var"
|
||||
fi
|
||||
done
|
||||
|
||||
# Use pristine path; the PATH from Xcode is polluted with stuff we don't want which can
|
||||
# confuse rustc.
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:$HOME/.cargo/bin:/run/current-system/sw/bin/"
|
||||
|
||||
# Parse Xcode environment
|
||||
PLATFORM_NAME="${PLATFORM_NAME:-macosx}"
|
||||
CONFIGURATION="${CONFIGURATION:-Debug}"
|
||||
NATIVE_ARCH="${ARCHS:-${NATIVE_ARCH:-$(uname -m)}}"
|
||||
|
||||
# Set target directory - use CONNLIB_TARGET_DIR if set, otherwise default
|
||||
export CARGO_TARGET_DIR="${CONNLIB_TARGET_DIR:-$RUST_DIR/target}"
|
||||
|
||||
echo "========================================="
|
||||
echo "Building Connlib for Xcode"
|
||||
echo "Platform: $PLATFORM_NAME"
|
||||
echo "Configuration: $CONFIGURATION"
|
||||
echo "Architecture: $NATIVE_ARCH"
|
||||
echo "Target Directory: $CARGO_TARGET_DIR"
|
||||
echo "========================================="
|
||||
|
||||
# Determine Rust targets based on platform and architecture
|
||||
TARGETS=()
|
||||
case "$PLATFORM_NAME" in
|
||||
macosx)
|
||||
if [[ "$CONFIGURATION" == "Release" ]] || [[ "$CONFIGURATION" == "Profile" ]] || [[ -z "$NATIVE_ARCH" ]]; then
|
||||
# Build universal binary for Release and Profile
|
||||
TARGETS=("aarch64-apple-darwin" "x86_64-apple-darwin")
|
||||
else
|
||||
# Build only for native arch in Debug
|
||||
if [[ "$NATIVE_ARCH" == "arm64" ]]; then
|
||||
TARGETS=("aarch64-apple-darwin")
|
||||
elif [[ "$NATIVE_ARCH" == "x86_64" ]]; then
|
||||
TARGETS=("x86_64-apple-darwin")
|
||||
else
|
||||
echo "ERROR: Unsupported native arch for $PLATFORM_NAME: $NATIVE_ARCH" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
iphoneos)
|
||||
TARGETS=("aarch64-apple-ios")
|
||||
;;
|
||||
iphonesimulator)
|
||||
if [[ "$NATIVE_ARCH" == "arm64" ]]; then
|
||||
TARGETS=("aarch64-apple-ios-sim")
|
||||
elif [[ "$NATIVE_ARCH" == "x86_64" ]]; then
|
||||
TARGETS=("x86_64-apple-ios")
|
||||
else
|
||||
echo "ERROR: Unsupported native arch for $PLATFORM_NAME: $NATIVE_ARCH" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown platform: $PLATFORM_NAME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Prepare cargo build flags
|
||||
if [ "$CONFIGURATION" = "Release" ] || [ "$CONFIGURATION" = "Profile" ]; then
|
||||
CARGO_BUILD_FLAGS="--release"
|
||||
BUILD_DIR="release"
|
||||
else
|
||||
CARGO_BUILD_FLAGS=""
|
||||
BUILD_DIR="debug"
|
||||
fi
|
||||
|
||||
# Ensure RUSTUP_HOME is set
|
||||
if [ -z "${RUSTUP_HOME:-}" ] && [ -d "$HOME/.rustup" ]; then
|
||||
export RUSTUP_HOME="$HOME/.rustup"
|
||||
fi
|
||||
|
||||
# Ensure Rust targets are installed (from rust directory to use correct toolchain)
|
||||
cd "$RUST_DIR"
|
||||
for target in "${TARGETS[@]}"; do
|
||||
if ! rustup target list --installed | grep -q "^$target$"; then
|
||||
echo "Installing Rust target: $target"
|
||||
rustup target add "$target"
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 1: Build Rust library
|
||||
echo ""
|
||||
echo "Step 1/3: Building Rust library..."
|
||||
|
||||
# Build target list for cargo command
|
||||
target_list=""
|
||||
for target in "${TARGETS[@]}"; do
|
||||
target_list+="--target $target "
|
||||
done
|
||||
target_list="${target_list% }"
|
||||
|
||||
cd "$RUST_DIR"
|
||||
cargo build --package client-ffi $target_list $CARGO_BUILD_FLAGS
|
||||
|
||||
# Remove any dylib files to ensure static linking
|
||||
for target in "${TARGETS[@]}"; do
|
||||
LIBRARY_PATH="$CARGO_TARGET_DIR/$target/$BUILD_DIR"
|
||||
if [ -f "$LIBRARY_PATH/libconnlib.dylib" ]; then
|
||||
rm -f "$LIBRARY_PATH/libconnlib.dylib"
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 2: Generate UniFFI bindings
|
||||
echo ""
|
||||
echo "Step 2/3: Generating UniFFI bindings..."
|
||||
mkdir -p "$GENERATED_DIR"
|
||||
|
||||
# Use the first target's library for generating bindings (they're the same for all architectures)
|
||||
FIRST_TARGET="${TARGETS[0]}"
|
||||
LIBRARY_PATH="$CARGO_TARGET_DIR/$FIRST_TARGET/$BUILD_DIR"
|
||||
|
||||
cargo run -p uniffi-bindgen -- generate \
|
||||
--library "$LIBRARY_PATH/libconnlib.a" \
|
||||
--language swift \
|
||||
--out-dir "$GENERATED_DIR"
|
||||
|
||||
# Remove module maps (we use bridging header instead)
|
||||
rm -f "$GENERATED_DIR"/*.modulemap
|
||||
|
||||
# Fix imports in generated Swift file to use bridging header
|
||||
if [ -f "$GENERATED_DIR/connlib.swift" ]; then
|
||||
# Comment out the #if canImport(connlibFFI) block
|
||||
sed -i.bak '/#if canImport(connlibFFI)/,/#endif/s/^/\/\/ /' "$GENERATED_DIR/connlib.swift"
|
||||
rm -f "$GENERATED_DIR/connlib.swift.bak"
|
||||
fi
|
||||
|
||||
# Step 3: Verify generated files
|
||||
echo ""
|
||||
echo "Step 3/3: Verifying generated files..."
|
||||
if [ ! -f "$GENERATED_DIR/connlib.swift" ]; then
|
||||
echo "ERROR: Generated Swift file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$GENERATED_DIR/connlibFFI.h" ]; then
|
||||
echo "ERROR: Generated header file not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Build completed successfully!"
|
||||
echo " Swift bindings: $GENERATED_DIR/connlib.swift"
|
||||
echo " C header: $GENERATED_DIR/connlibFFI.h"
|
||||
echo " Built libraries:"
|
||||
for target in "${TARGETS[@]}"; do
|
||||
echo " - $CARGO_TARGET_DIR/$target/$BUILD_DIR/libconnlib.a"
|
||||
done
|
||||
echo "========================================="
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
ARG=$1
|
||||
|
||||
if [[ -z "$ARG" ]]; then
|
||||
ARG="swift"
|
||||
fi
|
||||
|
||||
if [[ $ARG == "all" ]] || [[ $ARG == "swift" ]] || [[ $ARG == "rust" ]]; then
|
||||
echo "Cleaning up $ARG build artifacts";
|
||||
else
|
||||
echo "Usage: $0 [ all | swift | rust ]"
|
||||
echo " (Default: swift)"
|
||||
fi
|
||||
|
||||
if [[ $ARG == "swift" ]] || [[ $ARG == "all" ]]; then
|
||||
set -x
|
||||
xcodebuild clean
|
||||
rm -rf ./FirezoneNetworkExtension/Connlib
|
||||
set +x
|
||||
fi
|
||||
|
||||
if [[ $ARG == "rust" ]] || [[ $ARG == "all" ]]; then
|
||||
set -x
|
||||
cd ../../rust/apple-client-ffi && cargo clean
|
||||
cd Sources/Connlib/Generated && git clean -df
|
||||
set +x
|
||||
fi
|
||||
Reference in New Issue
Block a user