mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
Within the event-loop, we already react to the channel being closed
which happens when the `Sender` within the `Session` gets dropped. As
such, there is no need to send an explicit `Stop` command, dropping the
`Session` is equivalent.
As it turns out, `swift-bridge` already calls `Drop` for us when the
last pointer is set to `nil`:
280a9dd999/swift/apple/FirezoneNetworkExtension/Connlib/Generated/connlib-client-apple/connlib-client-apple.swift (L24-L28)
Thus, we can also remove the explicit `disconnect` call to
`WrappedSession` entirely.
342 lines
11 KiB
Rust
342 lines
11 KiB
Rust
// 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 connlib_client_shared::{Callbacks, DisconnectError, 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::Telemetry;
|
|
use firezone_telemetry::APPLE_DSN;
|
|
use ip_network::{Ipv4Network, Ipv6Network};
|
|
use phoenix_channel::get_user_agent;
|
|
use phoenix_channel::LoginUrl;
|
|
use phoenix_channel::PhoenixChannel;
|
|
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 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;
|
|
|
|
#[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,
|
|
callback_handler: CallbackHandler,
|
|
device_info: String,
|
|
) -> Result<WrappedSession, String>;
|
|
|
|
fn reset(&mut self);
|
|
|
|
// 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(&mut self, dns_servers: String) -> Result<(), String>;
|
|
|
|
#[swift_bridge(swift_name = "setDisabledResources", return_with = err_to_string)]
|
|
fn set_disabled_resources(&mut self, disabled_resources: String) -> Result<(), 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: String);
|
|
}
|
|
}
|
|
|
|
/// This is used by the apple client to interact with our code.
|
|
pub struct WrappedSession {
|
|
inner: Session,
|
|
runtime: Runtime,
|
|
|
|
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 {}
|
|
|
|
#[derive(Clone)]
|
|
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: Arc<ffi::CallbackHandler>,
|
|
}
|
|
|
|
impl Callbacks for 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: Vec<Ipv4Network>,
|
|
route_list_v6: Vec<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) {
|
|
self.inner.on_disconnect(error.to_string());
|
|
}
|
|
}
|
|
|
|
/// 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<()> {
|
|
static LOGGER_STATE: OnceLock<(
|
|
firezone_logging::file::Handle,
|
|
firezone_logging::FilterReloadHandle,
|
|
)> = OnceLock::new();
|
|
|
|
if let Some((_, reload_handle)) = LOGGER_STATE.get() {
|
|
reload_handle
|
|
.reload(&log_filter)
|
|
.context("Failed to apply new log-filter")?;
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
let (env_filter, 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(env_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(file_layer)
|
|
.with(sentry_layer());
|
|
|
|
firezone_logging::init(subscriber)?;
|
|
|
|
LOGGER_STATE
|
|
.set((handle, reload_handle))
|
|
.expect("logger state should only ever be initialised once");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
impl WrappedSession {
|
|
// TODO: Refactor this when we refactor PhoenixChannel.
|
|
// See https://github.com/firezone/firezone/issues/2158
|
|
#[expect(clippy::too_many_arguments)]
|
|
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,
|
|
callback_handler: ffi::CallbackHandler,
|
|
device_info: String,
|
|
) -> Result<Self> {
|
|
let mut telemetry = Telemetry::default();
|
|
telemetry.start(&api_url, RELEASE, APPLE_DSN);
|
|
Telemetry::set_firezone_id(device_id.clone());
|
|
Telemetry::set_account_slug(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,
|
|
device_name_override,
|
|
device_info,
|
|
)?;
|
|
|
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
|
.worker_threads(1)
|
|
.thread_name("connlib")
|
|
.enable_all()
|
|
.build()?;
|
|
let _guard = runtime.enter(); // Constructing `PhoenixChannel` requires a runtime context.
|
|
|
|
let portal = PhoenixChannel::disconnected(
|
|
Secret::new(url),
|
|
get_user_agent(os_version_override, env!("CARGO_PKG_VERSION")),
|
|
"client",
|
|
(),
|
|
|| {
|
|
ExponentialBackoffBuilder::default()
|
|
.with_max_elapsed_time(Some(MAX_PARTITION_TIME))
|
|
.build()
|
|
},
|
|
Arc::new(socket_factory::tcp),
|
|
)?;
|
|
let session = Session::connect(
|
|
Arc::new(socket_factory::tcp),
|
|
Arc::new(socket_factory::udp),
|
|
CallbackHandler {
|
|
inner: Arc::new(callback_handler),
|
|
},
|
|
portal,
|
|
runtime.handle().clone(),
|
|
);
|
|
session.set_tun(Box::new(Tun::new()?));
|
|
|
|
Ok(Self {
|
|
inner: session,
|
|
runtime,
|
|
telemetry,
|
|
})
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
self.inner.reset()
|
|
}
|
|
|
|
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_disabled_resources(&mut self, disabled_resources: String) -> Result<()> {
|
|
tracing::debug!(%disabled_resources);
|
|
|
|
let disabled_resources = serde_json::from_str(&disabled_resources)
|
|
.context("Failed to deserialize disabled resources from JSON")?;
|
|
|
|
self.inner.set_disabled_resources(disabled_resources);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for WrappedSession {
|
|
fn drop(&mut self) {
|
|
self.runtime.block_on(self.telemetry.stop());
|
|
}
|
|
}
|
|
|
|
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.");
|
|
}
|
|
}
|