mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +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
@@ -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