mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 02:18:47 +00:00
refactor: remove JSON serialization from FFI boundary (#10575)
This PR eliminates JSON-based communication across the FFI boundary, replacing it with proper uniffi-generated types for improved type safety, performance, and reliability. We replace JSON string parameters with native uniffi types for: - Resources (DNS, CIDR, Internet) - Device information - DNS server lists - Network routes (CIDR representation) Also, get rid of JSON serialisation in Swift client IPC in favour of PropertyList based serialisation. Fixes: https://github.com/firezone/firezone/issues/9548 --------- Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
committed by
GitHub
parent
97f3979fa6
commit
e76daaaab3
@@ -19,7 +19,7 @@ spotless {
|
||||
format("misc") {
|
||||
target("*.gradle", "*.md", ".gitignore")
|
||||
trimTrailingWhitespace()
|
||||
indentWithSpaces()
|
||||
leadingTabsToSpaces()
|
||||
endWithNewline()
|
||||
}
|
||||
kotlin {
|
||||
|
||||
@@ -20,9 +20,6 @@ import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.android.gms.tasks.Tasks
|
||||
import com.google.firebase.installations.FirebaseInstallations
|
||||
import com.google.gson.FieldNamingPolicy
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -31,6 +28,7 @@ import dev.firezone.android.core.data.ResourceState
|
||||
import dev.firezone.android.core.data.isEnabled
|
||||
import dev.firezone.android.tunnel.model.Cidr
|
||||
import dev.firezone.android.tunnel.model.Resource
|
||||
import dev.firezone.android.tunnel.model.Site
|
||||
import dev.firezone.android.tunnel.model.isInternetResource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@@ -41,6 +39,7 @@ import kotlinx.coroutines.channels.produce
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.selects.select
|
||||
import uniffi.connlib.DeviceInfo
|
||||
import uniffi.connlib.Event
|
||||
import uniffi.connlib.ProtectSocket
|
||||
import uniffi.connlib.Session
|
||||
@@ -49,10 +48,6 @@ import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import javax.inject.Inject
|
||||
|
||||
data class DeviceInfo(
|
||||
var firebaseInstallationId: String? = null,
|
||||
)
|
||||
|
||||
@AndroidEntryPoint
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class TunnelService : VpnService() {
|
||||
@@ -254,7 +249,7 @@ class TunnelService : VpnService() {
|
||||
}
|
||||
|
||||
fun setDns(dnsList: List<String>) {
|
||||
sendTunnelCommand(TunnelCommand.SetDns(Gson().toJson(dnsList)))
|
||||
sendTunnelCommand(TunnelCommand.SetDns(dnsList))
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
@@ -270,18 +265,20 @@ class TunnelService : VpnService() {
|
||||
tunnelState = State.CONNECTING
|
||||
updateStatusNotification(TunnelStatusNotification.Connecting)
|
||||
|
||||
val deviceInfo = DeviceInfo()
|
||||
runCatching { Tasks.await(FirebaseInstallations.getInstance().id) }
|
||||
.onSuccess { firebaseInstallationId ->
|
||||
deviceInfo.firebaseInstallationId = firebaseInstallationId
|
||||
}.onFailure { exception ->
|
||||
Log.d(TAG, "Failed to obtain firebase installation id: $exception")
|
||||
}
|
||||
val firebaseInstallationId =
|
||||
runCatching { Tasks.await(FirebaseInstallations.getInstance().id) }
|
||||
.getOrElse { exception ->
|
||||
Log.d(TAG, "Failed to obtain firebase installation id: $exception")
|
||||
null
|
||||
}
|
||||
|
||||
val gson: Gson =
|
||||
GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.create()
|
||||
val deviceInfo =
|
||||
DeviceInfo(
|
||||
firebaseInstallationId = firebaseInstallationId,
|
||||
deviceUuid = null,
|
||||
deviceSerial = null,
|
||||
identifierForVendor = null,
|
||||
)
|
||||
|
||||
commandChannel = Channel<TunnelCommand>(Channel.UNLIMITED)
|
||||
|
||||
@@ -298,7 +295,7 @@ class TunnelService : VpnService() {
|
||||
logFilter = config.logFilter,
|
||||
isInternetResourceActive = resourceState.isEnabled(),
|
||||
protectSocket = protectSocket,
|
||||
deviceInfo = gson.toJson(deviceInfo),
|
||||
deviceInfo = deviceInfo,
|
||||
).use { session ->
|
||||
startNetworkMonitoring()
|
||||
startDisconnectMonitoring()
|
||||
@@ -447,7 +444,7 @@ class TunnelService : VpnService() {
|
||||
) : TunnelCommand()
|
||||
|
||||
data class SetDns(
|
||||
val dnsServers: String,
|
||||
val dnsServers: List<String>,
|
||||
) : TunnelCommand()
|
||||
|
||||
data class SetLogDirectives(
|
||||
@@ -506,27 +503,25 @@ class TunnelService : VpnService() {
|
||||
eventChannel.onReceive { event ->
|
||||
when (event) {
|
||||
is Event.ResourcesUpdated -> {
|
||||
tunnelResources =
|
||||
moshi.adapter<List<Resource>>().fromJson(event.resources)!!
|
||||
tunnelResources = event.resources.map { convertResource(it) }
|
||||
resourcesUpdated()
|
||||
}
|
||||
|
||||
is Event.TunInterfaceUpdated -> {
|
||||
tunnelDnsAddresses =
|
||||
moshi.adapter<MutableList<String>>().fromJson(event.dns)!!
|
||||
tunnelDnsAddresses = event.dns.toMutableList()
|
||||
tunnelSearchDomain = event.searchDomain
|
||||
tunnelIpv4Address = event.ipv4
|
||||
tunnelIpv6Address = event.ipv6
|
||||
tunnelRoutes.clear()
|
||||
tunnelRoutes.addAll(
|
||||
moshi
|
||||
.adapter<MutableList<Cidr>>()
|
||||
.fromJson(event.ipv4Routes)!!,
|
||||
event.ipv4Routes.map { cidr ->
|
||||
Cidr(address = cidr.address, prefix = cidr.prefix.toInt())
|
||||
},
|
||||
)
|
||||
tunnelRoutes.addAll(
|
||||
moshi
|
||||
.adapter<MutableList<Cidr>>()
|
||||
.fromJson(event.ipv6Routes)!!,
|
||||
event.ipv6Routes.map { cidr ->
|
||||
Cidr(address = cidr.address, prefix = cidr.prefix.toInt())
|
||||
},
|
||||
)
|
||||
buildVpnService()
|
||||
}
|
||||
@@ -554,6 +549,56 @@ class TunnelService : VpnService() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertResource(resource: uniffi.connlib.Resource): Resource =
|
||||
when (resource) {
|
||||
is uniffi.connlib.Resource.Dns -> {
|
||||
Resource(
|
||||
type = dev.firezone.android.tunnel.model.ResourceType.DNS,
|
||||
id = resource.resource.id,
|
||||
address = resource.resource.address,
|
||||
addressDescription = resource.resource.addressDescription,
|
||||
sites = resource.resource.sites.map { convertSite(it) },
|
||||
name = resource.resource.name,
|
||||
status = convertResourceStatus(resource.resource.status),
|
||||
)
|
||||
}
|
||||
is uniffi.connlib.Resource.Cidr -> {
|
||||
Resource(
|
||||
type = dev.firezone.android.tunnel.model.ResourceType.CIDR,
|
||||
id = resource.resource.id,
|
||||
address = resource.resource.address,
|
||||
addressDescription = resource.resource.addressDescription,
|
||||
sites = resource.resource.sites.map { convertSite(it) },
|
||||
name = resource.resource.name,
|
||||
status = convertResourceStatus(resource.resource.status),
|
||||
)
|
||||
}
|
||||
is uniffi.connlib.Resource.Internet -> {
|
||||
Resource(
|
||||
type = dev.firezone.android.tunnel.model.ResourceType.Internet,
|
||||
id = resource.resource.id,
|
||||
address = null,
|
||||
addressDescription = null,
|
||||
sites = resource.resource.sites.map { convertSite(it) },
|
||||
name = resource.resource.name,
|
||||
status = convertResourceStatus(resource.resource.status),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun convertSite(site: uniffi.connlib.Site): dev.firezone.android.tunnel.model.Site =
|
||||
dev.firezone.android.tunnel.model.Site(
|
||||
id = site.id,
|
||||
name = site.name,
|
||||
)
|
||||
|
||||
private fun convertResourceStatus(status: uniffi.connlib.ResourceStatus): dev.firezone.android.tunnel.model.StatusEnum =
|
||||
when (status) {
|
||||
uniffi.connlib.ResourceStatus.UNKNOWN -> dev.firezone.android.tunnel.model.StatusEnum.UNKNOWN
|
||||
uniffi.connlib.ResourceStatus.ONLINE -> dev.firezone.android.tunnel.model.StatusEnum.ONLINE
|
||||
uniffi.connlib.ResourceStatus.OFFLINE -> dev.firezone.android.tunnel.model.StatusEnum.OFFLINE
|
||||
}
|
||||
|
||||
companion object {
|
||||
enum class State {
|
||||
CONNECTING,
|
||||
|
||||
@@ -10,7 +10,6 @@ use std::{
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use backoff::ExponentialBackoffBuilder;
|
||||
use client_shared::{V4RouteList, V6RouteList};
|
||||
use firezone_logging::sentry_layer;
|
||||
use firezone_telemetry::{Telemetry, analytics};
|
||||
use phoenix_channel::{LoginUrl, PhoenixChannel, get_user_agent};
|
||||
@@ -43,18 +42,89 @@ pub enum CallbackError {
|
||||
#[derive(uniffi::Object, Debug)]
|
||||
pub struct DisconnectError(client_shared::DisconnectError);
|
||||
|
||||
/// Represents a CIDR network (address + prefix length).
|
||||
/// Used for IPv4 and IPv6 route configuration.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct Cidr {
|
||||
pub address: String,
|
||||
pub prefix: u8,
|
||||
}
|
||||
|
||||
/// Device information for telemetry and identification.
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct DeviceInfo {
|
||||
pub firebase_installation_id: Option<String>,
|
||||
pub device_uuid: Option<String>,
|
||||
pub device_serial: Option<String>,
|
||||
pub identifier_for_vendor: Option<String>,
|
||||
}
|
||||
|
||||
/// Resource status enum
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum ResourceStatus {
|
||||
Unknown,
|
||||
Online,
|
||||
Offline,
|
||||
}
|
||||
|
||||
/// Site information for a resource
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct Site {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// DNS resource view
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct DnsResource {
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub address_description: Option<String>,
|
||||
pub sites: Vec<Site>,
|
||||
pub status: ResourceStatus,
|
||||
}
|
||||
|
||||
/// CIDR resource view
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct CidrResource {
|
||||
pub id: String,
|
||||
pub address: String,
|
||||
pub name: String,
|
||||
pub address_description: Option<String>,
|
||||
pub sites: Vec<Site>,
|
||||
pub status: ResourceStatus,
|
||||
}
|
||||
|
||||
/// Internet resource view
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct InternetResource {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub sites: Vec<Site>,
|
||||
pub status: ResourceStatus,
|
||||
}
|
||||
|
||||
/// Resource view enum
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum Resource {
|
||||
Dns { resource: DnsResource },
|
||||
Cidr { resource: CidrResource },
|
||||
Internet { resource: InternetResource },
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum Event {
|
||||
TunInterfaceUpdated {
|
||||
ipv4: String,
|
||||
ipv6: String,
|
||||
dns: String,
|
||||
dns: Vec<String>,
|
||||
search_domain: Option<String>,
|
||||
ipv4_routes: String,
|
||||
ipv6_routes: String,
|
||||
ipv4_routes: Vec<Cidr>,
|
||||
ipv6_routes: Vec<Cidr>,
|
||||
},
|
||||
ResourcesUpdated {
|
||||
resources: String,
|
||||
resources: Vec<Resource>,
|
||||
},
|
||||
Disconnected {
|
||||
error: Arc<DisconnectError>,
|
||||
@@ -94,7 +164,7 @@ impl Session {
|
||||
os_version: String,
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
device_info: String,
|
||||
device_info: DeviceInfo,
|
||||
is_internet_resource_active: bool,
|
||||
protect_socket: Arc<dyn ProtectSocket>,
|
||||
) -> Result<Self, ConnlibError> {
|
||||
@@ -135,7 +205,7 @@ impl Session {
|
||||
os_version: Option<String>,
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
device_info: String,
|
||||
device_info: DeviceInfo,
|
||||
is_internet_resource_active: bool,
|
||||
) -> Result<Self, ConnlibError> {
|
||||
// iOS doesn't need socket protection like Android
|
||||
@@ -218,9 +288,12 @@ impl Session {
|
||||
self.inner.set_internet_resource_state(active);
|
||||
}
|
||||
|
||||
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")?;
|
||||
pub fn set_dns(&self, dns_servers: Vec<String>) -> Result<(), ConnlibError> {
|
||||
let dns_servers: Vec<std::net::IpAddr> = dns_servers
|
||||
.into_iter()
|
||||
.map(|s| s.parse())
|
||||
.collect::<Result<_, _>>()
|
||||
.context("Failed to parse DNS servers")?;
|
||||
|
||||
self.inner.set_dns(dns_servers);
|
||||
|
||||
@@ -253,37 +326,50 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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(
|
||||
&config.dns_by_sentinel.left_values().collect::<Vec<_>>(),
|
||||
)
|
||||
.context("Failed to serialize DNS servers")?;
|
||||
let ipv4_routes = serde_json::to_string(&V4RouteList::new(config.ipv4_routes))
|
||||
.context("Failed to serialize IPv4 routes")?;
|
||||
let ipv6_routes = serde_json::to_string(&V6RouteList::new(config.ipv6_routes))
|
||||
.context("Failed to serialize IPv6 routes")?;
|
||||
pub async fn next_event(&self) -> Option<Event> {
|
||||
match self.events.lock().await.next().await? {
|
||||
client_shared::Event::TunInterfaceUpdated(config) => {
|
||||
let dns: Vec<String> = config
|
||||
.dns_by_sentinel
|
||||
.left_values()
|
||||
.map(|ip| ip.to_string())
|
||||
.collect();
|
||||
|
||||
Ok(Some(Event::TunInterfaceUpdated {
|
||||
let ipv4_routes: Vec<Cidr> = config
|
||||
.ipv4_routes
|
||||
.into_iter()
|
||||
.map(|network| Cidr {
|
||||
address: network.network_address().to_string(),
|
||||
prefix: network.netmask(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ipv6_routes: Vec<Cidr> = config
|
||||
.ipv6_routes
|
||||
.into_iter()
|
||||
.map(|network| Cidr {
|
||||
address: network.network_address().to_string(),
|
||||
prefix: network.netmask(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some(Event::TunInterfaceUpdated {
|
||||
ipv4: config.ip.v4.to_string(),
|
||||
ipv6: config.ip.v6.to_string(),
|
||||
dns,
|
||||
search_domain: config.search_domain.map(|d| d.to_string()),
|
||||
ipv4_routes,
|
||||
ipv6_routes,
|
||||
}))
|
||||
})
|
||||
}
|
||||
Some(client_shared::Event::ResourcesUpdated(resources)) => {
|
||||
let resources = serde_json::to_string(&resources)
|
||||
.context("Failed to serialize resource list")?;
|
||||
client_shared::Event::ResourcesUpdated(resources) => {
|
||||
let resources: Vec<Resource> = resources.into_iter().map(Into::into).collect();
|
||||
|
||||
Ok(Some(Event::ResourcesUpdated { resources }))
|
||||
Some(Event::ResourcesUpdated { resources })
|
||||
}
|
||||
Some(client_shared::Event::Disconnected(error)) => Ok(Some(Event::Disconnected {
|
||||
client_shared::Event::Disconnected(error) => Some(Event::Disconnected {
|
||||
error: Arc::new(DisconnectError(error)),
|
||||
})),
|
||||
None => Ok(None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,13 +403,18 @@ fn connect(
|
||||
os_version: Option<String>,
|
||||
log_dir: String,
|
||||
log_filter: String,
|
||||
device_info: String,
|
||||
device_info: DeviceInfo,
|
||||
is_internet_resource_active: bool,
|
||||
tcp_socket_factory: Arc<dyn SocketFactory<TcpSocket>>,
|
||||
udp_socket_factory: Arc<dyn SocketFactory<UdpSocket>>,
|
||||
) -> Result<Session, ConnlibError> {
|
||||
let device_info =
|
||||
serde_json::from_str(&device_info).context("Failed to deserialize `DeviceInfo`")?;
|
||||
// Convert FFI DeviceInfo to internal phoenix_channel::DeviceInfo
|
||||
let device_info = phoenix_channel::DeviceInfo {
|
||||
device_uuid: device_info.device_uuid,
|
||||
device_serial: device_info.device_serial,
|
||||
identifier_for_vendor: device_info.identifier_for_vendor,
|
||||
firebase_installation_id: device_info.firebase_installation_id,
|
||||
};
|
||||
let secret = SecretString::from(token);
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
@@ -462,6 +553,78 @@ fn install_rustls_crypto_provider() {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<connlib_model::ResourceView> for Resource {
|
||||
fn from(resource: connlib_model::ResourceView) -> Self {
|
||||
match resource {
|
||||
connlib_model::ResourceView::Dns(dns) => Resource::Dns {
|
||||
resource: dns.into(),
|
||||
},
|
||||
connlib_model::ResourceView::Cidr(cidr) => Resource::Cidr {
|
||||
resource: cidr.into(),
|
||||
},
|
||||
connlib_model::ResourceView::Internet(internet) => Resource::Internet {
|
||||
resource: internet.into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<connlib_model::DnsResourceView> for DnsResource {
|
||||
fn from(dns: connlib_model::DnsResourceView) -> Self {
|
||||
DnsResource {
|
||||
id: dns.id.to_string(),
|
||||
address: dns.address,
|
||||
name: dns.name,
|
||||
address_description: dns.address_description,
|
||||
sites: dns.sites.into_iter().map(Into::into).collect(),
|
||||
status: dns.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<connlib_model::CidrResourceView> for CidrResource {
|
||||
fn from(cidr: connlib_model::CidrResourceView) -> Self {
|
||||
CidrResource {
|
||||
id: cidr.id.to_string(),
|
||||
address: cidr.address.to_string(),
|
||||
name: cidr.name,
|
||||
address_description: cidr.address_description,
|
||||
sites: cidr.sites.into_iter().map(Into::into).collect(),
|
||||
status: cidr.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<connlib_model::InternetResourceView> for InternetResource {
|
||||
fn from(internet: connlib_model::InternetResourceView) -> Self {
|
||||
InternetResource {
|
||||
id: internet.id.to_string(),
|
||||
name: internet.name,
|
||||
sites: internet.sites.into_iter().map(Into::into).collect(),
|
||||
status: internet.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<connlib_model::Site> for Site {
|
||||
fn from(site: connlib_model::Site) -> Self {
|
||||
Site {
|
||||
id: site.id.to_string(),
|
||||
name: site.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<connlib_model::ResourceStatus> for ResourceStatus {
|
||||
fn from(status: connlib_model::ResourceStatus) -> Self {
|
||||
match status {
|
||||
connlib_model::ResourceStatus::Unknown => ResourceStatus::Unknown,
|
||||
connlib_model::ResourceStatus::Online => ResourceStatus::Online,
|
||||
connlib_model::ResourceStatus::Offline => ResourceStatus::Offline,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for ConnlibError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self(value)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
//! Main connlib library for clients.
|
||||
pub use crate::serde_routelist::{V4RouteList, V6RouteList};
|
||||
pub use connlib_model::StaticSecret;
|
||||
pub use eventloop::DisconnectError;
|
||||
pub use firezone_tunnel::TunConfig;
|
||||
@@ -23,7 +22,6 @@ use tokio_stream::wrappers::WatchStream;
|
||||
use tun::Tun;
|
||||
|
||||
mod eventloop;
|
||||
mod serde_routelist;
|
||||
|
||||
const PHOENIX_TOPIC: &str = "client";
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
use ip_network::{Ipv4Network, Ipv6Network};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
#[derive(serde::Serialize, Clone, Copy, Debug)]
|
||||
struct Cidr<T> {
|
||||
address: T,
|
||||
prefix: u8,
|
||||
}
|
||||
|
||||
/// Custom adaptor for a different serialisation format for the Apple and Android clients.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct V4RouteList(Vec<Cidr<Ipv4Addr>>);
|
||||
|
||||
impl V4RouteList {
|
||||
pub fn new(route: impl IntoIterator<Item = Ipv4Network>) -> Self {
|
||||
Self(
|
||||
route
|
||||
.into_iter()
|
||||
.map(|n| Cidr {
|
||||
address: n.network_address(),
|
||||
prefix: n.netmask(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom adaptor for a different serialisation format for the Apple and Android clients.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct V6RouteList(Vec<Cidr<Ipv6Addr>>);
|
||||
|
||||
impl V6RouteList {
|
||||
pub fn new(route: impl IntoIterator<Item = Ipv6Network>) -> Self {
|
||||
Self(
|
||||
route
|
||||
.into_iter()
|
||||
.map(|n| Cidr {
|
||||
address: n.network_address(),
|
||||
prefix: n.netmask(),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -33,39 +33,24 @@ public class DeviceMetadata {
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
public static func deviceInfo() -> DeviceInfo {
|
||||
return DeviceInfo(identifierForVendor: UIDevice.current.identifierForVendor!.uuidString)
|
||||
}
|
||||
#else
|
||||
public static func deviceInfo() -> DeviceInfo {
|
||||
return DeviceInfo(deviceUuid: getDeviceUuid()!, deviceSerial: getDeviceSerial()!)
|
||||
public static func getIdentifierForVendor() -> String? {
|
||||
return UIDevice.current.identifierForVendor?.uuidString
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
public struct DeviceInfo: Encodable {
|
||||
let identifierForVendor: String
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
import IOKit
|
||||
|
||||
public struct DeviceInfo: Encodable {
|
||||
let deviceUuid: String
|
||||
let deviceSerial: String
|
||||
}
|
||||
|
||||
func getDeviceUuid() -> String? {
|
||||
public func getDeviceUuid() -> String? {
|
||||
return getDeviceInfo(key: kIOPlatformUUIDKey as CFString)
|
||||
}
|
||||
|
||||
func getDeviceSerial() -> String? {
|
||||
public func getDeviceSerial() -> String? {
|
||||
return getDeviceInfo(key: kIOPlatformSerialNumberKey as CFString)
|
||||
}
|
||||
|
||||
func getDeviceInfo(key: CFString) -> String? {
|
||||
private func getDeviceInfo(key: CFString) -> String? {
|
||||
let matchingDict = IOServiceMatching("IOPlatformExpertDevice")
|
||||
|
||||
let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, matchingDict)
|
||||
|
||||
@@ -109,11 +109,8 @@ class IPCClient {
|
||||
// Save hash to compare against
|
||||
self.resourceListHash = Data(SHA256.hash(data: data))
|
||||
|
||||
let jsonDecoder = JSONDecoder()
|
||||
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
do {
|
||||
let decoded = try jsonDecoder.decode([Resource].self, from: data)
|
||||
let decoded = try self.decoder.decode([Resource].self, from: data)
|
||||
self.resourcesListCache = ResourceList.loaded(decoded)
|
||||
|
||||
continuation.resume(returning: self.resourcesListCache)
|
||||
|
||||
@@ -27,7 +27,7 @@ public enum ResourceList {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Resource: Decodable, Identifiable, Equatable {
|
||||
public struct Resource: Codable, Identifiable, Equatable {
|
||||
public let id: String
|
||||
public var name: String
|
||||
public var address: String?
|
||||
@@ -59,7 +59,7 @@ public struct Resource: Decodable, Identifiable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ResourceStatus: String, Decodable {
|
||||
public enum ResourceStatus: String, Codable {
|
||||
case offline = "Offline"
|
||||
case online = "Online"
|
||||
case unknown = "Unknown"
|
||||
@@ -91,7 +91,7 @@ public enum ResourceStatus: String, Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum ResourceType: String, Decodable {
|
||||
public enum ResourceType: String, Codable {
|
||||
case dns
|
||||
case cidr
|
||||
case ip
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Site: Decodable, Identifiable, Equatable {
|
||||
public struct Site: Codable, Identifiable, Equatable {
|
||||
public let id: String
|
||||
public var name: String
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class Adapter: @unchecked Sendable {
|
||||
private var internetResourceEnabled: Bool
|
||||
|
||||
/// Keep track of resources for UI
|
||||
private var resourceListJSON: String?
|
||||
private var resources: [Resource]?
|
||||
|
||||
/// Starting parameters
|
||||
private let apiURL: String
|
||||
@@ -180,17 +180,30 @@ class Adapter: @unchecked Sendable {
|
||||
networkMonitor?.cancel()
|
||||
}
|
||||
|
||||
|
||||
func start() throws {
|
||||
Log.log("Adapter.start: Starting session for account: \(accountSlug)")
|
||||
|
||||
// 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"
|
||||
|
||||
#if os(iOS)
|
||||
let deviceInfo = DeviceInfo(
|
||||
firebaseInstallationId: nil,
|
||||
deviceUuid: nil,
|
||||
deviceSerial: nil,
|
||||
identifierForVendor: DeviceMetadata.getIdentifierForVendor()
|
||||
)
|
||||
#else
|
||||
let deviceInfo = DeviceInfo(
|
||||
firebaseInstallationId: nil,
|
||||
deviceUuid: getDeviceUuid(),
|
||||
deviceSerial: getDeviceSerial(),
|
||||
identifierForVendor: nil
|
||||
)
|
||||
#endif
|
||||
|
||||
// Create the session
|
||||
let session: Session
|
||||
do {
|
||||
@@ -203,7 +216,7 @@ class Adapter: @unchecked Sendable {
|
||||
osVersion: osVersion,
|
||||
logDir: logDir,
|
||||
logFilter: logFilter,
|
||||
deviceInfo: deviceInfoStr,
|
||||
deviceInfo: deviceInfo,
|
||||
isInternetResourceActive: internetResourceEnabled
|
||||
)
|
||||
} catch {
|
||||
@@ -277,17 +290,31 @@ class Adapter: @unchecked Sendable {
|
||||
/// Get the current set of resources in the completionHandler, only returning
|
||||
/// them if the resource list has changed.
|
||||
func getResourcesIfVersionDifferentFrom(
|
||||
hash: Data, completionHandler: @escaping (String?) -> Void
|
||||
hash: Data, completionHandler: @escaping (Data?) -> Void
|
||||
) {
|
||||
// This is async to avoid blocking the main UI thread
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
if hash == Data(SHA256.hash(data: Data((resourceListJSON ?? "").utf8))) {
|
||||
// Convert uniffi resources to FirezoneKit resources and encode with PropertyList
|
||||
let propertyListData: Data
|
||||
if let uniffiResources = self.resources {
|
||||
let firezoneResources = uniffiResources.map { self.convertResource($0) }
|
||||
guard let encoded = try? PropertyListEncoder().encode(firezoneResources) else {
|
||||
Log.log("Failed to encode resources as PropertyList")
|
||||
completionHandler(nil)
|
||||
return
|
||||
}
|
||||
propertyListData = encoded
|
||||
} else {
|
||||
propertyListData = Data()
|
||||
}
|
||||
|
||||
if hash == Data(SHA256.hash(data: propertyListData)) {
|
||||
// nothing changed
|
||||
completionHandler(nil)
|
||||
} else {
|
||||
completionHandler(resourceListJSON)
|
||||
completionHandler(propertyListData)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,19 +344,13 @@ class Adapter: @unchecked Sendable {
|
||||
let ipv4, let ipv6, let dns, let searchDomain, let ipv4Routes, let ipv6Routes):
|
||||
Log.log("Received TunInterfaceUpdated event")
|
||||
|
||||
// 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 network configuration from connlib")
|
||||
// Convert UniFFI types to NetworkExtension types
|
||||
let routes4 = ipv4Routes.compactMap { cidr in
|
||||
NetworkSettings.Cidr(address: cidr.address, prefix: Int(cidr.prefix)).asNEIPv4Route
|
||||
}
|
||||
let routes6 = ipv6Routes.compactMap { cidr in
|
||||
NetworkSettings.Cidr(address: cidr.address, prefix: Int(cidr.prefix)).asNEIPv6Route
|
||||
}
|
||||
|
||||
let routes4 = decoded4.compactMap({ $0.asNEIPv4Route })
|
||||
let routes6 = decoded6.compactMap({ $0.asNEIPv6Route })
|
||||
|
||||
// All decoding succeeded - now apply settings atomically
|
||||
guard let provider = packetTunnelProvider else {
|
||||
@@ -342,7 +363,7 @@ class Adapter: @unchecked Sendable {
|
||||
let networkSettings = NetworkSettings(packetTunnelProvider: provider)
|
||||
networkSettings.tunnelAddressIPv4 = ipv4
|
||||
networkSettings.tunnelAddressIPv6 = ipv6
|
||||
networkSettings.dnsAddresses = dnsAddresses
|
||||
networkSettings.dnsAddresses = dns
|
||||
networkSettings.routes4 = routes4
|
||||
networkSettings.routes6 = routes6
|
||||
networkSettings.setSearchDomain(domain: searchDomain)
|
||||
@@ -350,13 +371,13 @@ class Adapter: @unchecked Sendable {
|
||||
|
||||
networkSettings.apply()
|
||||
|
||||
case .resourcesUpdated(let resources):
|
||||
Log.log("Received ResourcesUpdated event with \(resources.count) bytes")
|
||||
case .resourcesUpdated(let resourceList):
|
||||
Log.log("Received ResourcesUpdated event with \(resourceList.count) resources")
|
||||
|
||||
// Store resource list
|
||||
workQueue.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.resourceListJSON = resources
|
||||
self.resources = resourceList
|
||||
}
|
||||
|
||||
// Apply network settings to flush DNS cache when resources change
|
||||
@@ -455,23 +476,70 @@ class Adapter: @unchecked Sendable {
|
||||
Log.warning("IP address \(stringAddress) did not parse as either IPv4 or IPv6")
|
||||
}
|
||||
|
||||
// Step 3: Encode
|
||||
guard let encoded = try? JSONEncoder().encode(parsedResolvers),
|
||||
let jsonResolvers = String(data: encoded, encoding: .utf8)
|
||||
else {
|
||||
Log.warning("jsonResolvers conversion failed: \(parsedResolvers)")
|
||||
return
|
||||
}
|
||||
|
||||
// Step 4: Send to connlib
|
||||
Log.log("Sending resolvers to connlib: \(jsonResolvers)")
|
||||
sendCommand(.setDns(jsonResolvers))
|
||||
// Step 3: Send to connlib
|
||||
Log.log("Sending resolvers to connlib: \(parsedResolvers)")
|
||||
sendCommand(.setDns(parsedResolvers))
|
||||
}
|
||||
|
||||
private func sendCommand(_ command: SessionCommand) {
|
||||
commandSender?.send(command)
|
||||
}
|
||||
|
||||
// MARK: - Resource conversion (uniffi → FirezoneKit)
|
||||
|
||||
private func convertResource(_ resource: Resource) -> FirezoneKit.Resource {
|
||||
switch resource {
|
||||
case .dns(let dnsResource):
|
||||
return FirezoneKit.Resource(
|
||||
id: dnsResource.id,
|
||||
name: dnsResource.name,
|
||||
address: dnsResource.address,
|
||||
addressDescription: dnsResource.addressDescription,
|
||||
status: convertResourceStatus(dnsResource.status),
|
||||
sites: dnsResource.sites.map { convertSite($0) },
|
||||
type: .dns
|
||||
)
|
||||
case .cidr(let cidrResource):
|
||||
return FirezoneKit.Resource(
|
||||
id: cidrResource.id,
|
||||
name: cidrResource.name,
|
||||
address: cidrResource.address,
|
||||
addressDescription: cidrResource.addressDescription,
|
||||
status: convertResourceStatus(cidrResource.status),
|
||||
sites: cidrResource.sites.map { convertSite($0) },
|
||||
type: .cidr
|
||||
)
|
||||
case .internet(let internetResource):
|
||||
return FirezoneKit.Resource(
|
||||
id: internetResource.id,
|
||||
name: internetResource.name,
|
||||
address: nil,
|
||||
addressDescription: nil,
|
||||
status: convertResourceStatus(internetResource.status),
|
||||
sites: internetResource.sites.map { convertSite($0) },
|
||||
type: .internet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func convertSite(_ site: Site) -> FirezoneKit.Site {
|
||||
return FirezoneKit.Site(
|
||||
id: site.id,
|
||||
name: site.name
|
||||
)
|
||||
}
|
||||
|
||||
private func convertResourceStatus(_ status: ResourceStatus) -> FirezoneKit.ResourceStatus {
|
||||
switch status {
|
||||
case .unknown:
|
||||
return .unknown
|
||||
case .online:
|
||||
return .online
|
||||
case .offline:
|
||||
return .offline
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Getting System Resolvers on iOS
|
||||
|
||||
@@ -127,19 +127,25 @@ enum IPv4SubnetMaskLookup {
|
||||
]
|
||||
}
|
||||
|
||||
// Route convenience helpers. Data is from connlib and guaranteed to be valid.
|
||||
// Otherwise, we should crash and learn about it.
|
||||
// Route convenience helpers.
|
||||
extension NetworkSettings {
|
||||
struct Cidr: Codable {
|
||||
struct Cidr {
|
||||
let address: String
|
||||
let prefix: Int
|
||||
|
||||
var asNEIPv4Route: NEIPv4Route {
|
||||
return NEIPv4Route(
|
||||
destinationAddress: address, subnetMask: IPv4SubnetMaskLookup.table[prefix]!)
|
||||
var asNEIPv4Route: NEIPv4Route? {
|
||||
guard let subnetMask = IPv4SubnetMaskLookup.table[prefix] else {
|
||||
Log.warning("Invalid IPv4 prefix: \(prefix) for address: \(address)")
|
||||
return nil
|
||||
}
|
||||
return NEIPv4Route(destinationAddress: address, subnetMask: subnetMask)
|
||||
}
|
||||
|
||||
var asNEIPv6Route: NEIPv6Route {
|
||||
var asNEIPv6Route: NEIPv6Route? {
|
||||
guard prefix >= 0 && prefix <= 128 else {
|
||||
Log.warning("Invalid IPv6 prefix: \(prefix) for address: \(address)")
|
||||
return nil
|
||||
}
|
||||
return NEIPv6Route(destinationAddress: address, networkPrefixLength: NSNumber(value: prefix))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,8 +183,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||
}
|
||||
|
||||
// Use hash comparison to only return resources if they've changed
|
||||
adapter.getResourcesIfVersionDifferentFrom(hash: hash) { resourceListJSON in
|
||||
completionHandler?(resourceListJSON?.data(using: .utf8))
|
||||
adapter.getResourcesIfVersionDifferentFrom(hash: hash) { resourceData in
|
||||
completionHandler?(resourceData)
|
||||
}
|
||||
case .clearLogs:
|
||||
clearLogs(completionHandler)
|
||||
|
||||
@@ -5,7 +5,7 @@ import Foundation
|
||||
enum SessionCommand {
|
||||
case disconnect
|
||||
case setInternetResourceState(Bool)
|
||||
case setDns(String)
|
||||
case setDns([String])
|
||||
case reset(String)
|
||||
}
|
||||
|
||||
@@ -24,20 +24,14 @@ func runSessionEventLoop(
|
||||
// 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
|
||||
// Poll for next event from Rust
|
||||
guard let event = await session.nextEvent() else {
|
||||
// No event returned - session has ended
|
||||
Log.log("SessionEventLoop: Event stream ended, exiting event loop")
|
||||
break
|
||||
}
|
||||
|
||||
eventSender.send(event)
|
||||
}
|
||||
|
||||
Log.log("SessionEventLoop: Event polling finished")
|
||||
|
||||
Reference in New Issue
Block a user