apple: Set network settings using data from connlib (#1846)

This PR sets the network settings, split-DNS, and macOS UI resources
using the data from connlib callbacks.

This should enable connlib to be developed / tested in Apple platforms
(Caveat: There's no UI to see resources in iOS yet).

Some assumptions being made are:
- It's ok to call disconnect() before onTunnelReady(), but after
connect()
- CIDR addresses don't include enclosing quotes (they currently include
the quotes, like: `"8.8.4.4/32"`)
- CIDR addresses in routes always end with “/n”
- Connlib calls can be made from a queue (non-main thread)

---------

Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Roopesh Chander
2023-08-07 12:20:05 +05:30
committed by GitHub
parent a33c03b158
commit e591b92ec9
6 changed files with 523 additions and 226 deletions

View File

@@ -16,7 +16,7 @@ extension RustString: @unchecked Sendable {}
extension RustString: Error {}
public protocol CallbackHandlerDelegate: AnyObject {
func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String)
func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String)
func onTunnelReady()
func onAddRoute(_: String)
func onRemoveRoute(_: String)
@@ -34,7 +34,8 @@ public class CallbackHandler {
delegate?.onSetInterfaceConfig(
tunnelAddressIPv4: tunnelAddressIPv4.toString(),
tunnelAddressIPv6: tunnelAddressIPv6.toString(),
dnsAddress: dnsAddress.toString()
dnsAddress: dnsAddress.toString(),
dnsFallbackStrategy: "system_resolver" // Will come from a onSetInterfaceConfig arg eventually
)
}

View File

@@ -25,6 +25,8 @@
6FE4550D2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */; };
6FE4550F2A5D112C006549B1 /* connlib-apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550E2A5D112C006549B1 /* connlib-apple.swift */; };
6FE455102A5D112C006549B1 /* connlib-apple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE4550E2A5D112C006549B1 /* connlib-apple.swift */; };
6FE93AFB2A738D7E002D278A /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; };
6FE93AFC2A738D7E002D278A /* NetworkSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */; };
794C38152970A2660029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38142970A2660029F38F /* FirezoneKit */; };
794C38172970A26A0029F38F /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 794C38162970A26A0029F38F /* FirezoneKit */; };
79756C6629704A7A0018E2D5 /* FirezoneKit in Frameworks */ = {isa = PBXBuildFile; productRef = 79756C6529704A7A0018E2D5 /* FirezoneKit */; };
@@ -105,6 +107,7 @@
6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwiftBridgeCore.swift; path = Connlib/Generated/SwiftBridgeCore.swift; sourceTree = "<group>"; };
6FE4550E2A5D112C006549B1 /* connlib-apple.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "connlib-apple.swift"; path = "Connlib/Generated/connlib-apple/connlib-apple.swift"; sourceTree = "<group>"; };
6FE455112A5D13A2006549B1 /* FirezoneNetworkExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FirezoneNetworkExtension-Bridging-Header.h"; sourceTree = "<group>"; };
6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSettings.swift; sourceTree = "<group>"; };
8DCC021928D512AC007E12D2 /* Firezone.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firezone.app; sourceTree = BUILT_PRODUCTS_DIR; };
8DCC021C28D512AC007E12D2 /* FirezoneApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirezoneApp.swift; sourceTree = "<group>"; };
8DCC022528D512AE007E12D2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -159,6 +162,7 @@
05CF1CDE290B1A9000CF4755 /* FirezoneNetworkExtension_macOS.entitlements */,
05833DFA28F73B070008FAB0 /* PacketTunnelProvider.swift */,
6FE454EA2A5BFABA006549B1 /* Adapter.swift */,
6FE93AFA2A738D7E002D278A /* NetworkSettings.swift */,
6FA39A032A6A7248000F0157 /* NetworkResource.swift */,
6FE455082A5D110D006549B1 /* CallbackHandler.swift */,
6FE4550B2A5D111D006549B1 /* SwiftBridgeCore.swift */,
@@ -456,6 +460,7 @@
6FE454F62A5BFB93006549B1 /* Adapter.swift in Sources */,
6FA39A042A6A7248000F0157 /* NetworkResource.swift in Sources */,
6FE4550C2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */,
6FE93AFB2A738D7E002D278A /* NetworkSettings.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -469,6 +474,7 @@
6FE454F72A5BFB93006549B1 /* Adapter.swift in Sources */,
6FA39A052A6A7248000F0157 /* NetworkResource.swift in Sources */,
6FE4550D2A5D111E006549B1 /* SwiftBridgeCore.swift in Sources */,
6FE93AFC2A738D7E002D278A /* NetworkSettings.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -12,30 +12,55 @@ public enum AdapterError: Error {
/// Failure to perform an operation in such state.
case invalidState
/// connlib failed to start
case connlibConnectError(Error)
/// connlib fatal error
case connlibFatalError(String)
/// No network settings were provided
case noNetworkSettings
/// Failure to set network settings.
case setNetworkSettings(Error)
/// stop() called before the tunnel became ready
case stoppedByRequestWhileStarting
}
/// Enum representing internal state of the `WireGuardAdapter`
private enum State {
/// The tunnel is stopped
case stopped
/// Enum representing internal state of the adapter
private enum AdapterState: CustomStringConvertible {
case startingTunnel(session: WrappedSession, onStarted: Adapter.StartTunnelCompletionHandler?)
case tunnelReady(session: WrappedSession)
case stoppingTunnel(session: WrappedSession, onStopped: Adapter.StopTunnelCompletionHandler?)
case stoppedTunnel
case stoppingTunnelTemporarily(session: WrappedSession, onStopped: Adapter.StopTunnelCompletionHandler?)
case stoppedTunnelTemporarily
/// The tunnel is up and running
case started(_ handle: WrappedSession)
/// The tunnel is temporarily shutdown due to device going offline
case temporaryShutdown
var description: String {
switch self {
case .startingTunnel: return "startingTunnel"
case .tunnelReady: return "tunnelReady"
case .stoppingTunnel: return "stoppingTunnel"
case .stoppedTunnel: return "stoppedTunnel"
case .stoppingTunnelTemporarily: return "stoppingTunnelTemporarily"
case .stoppedTunnelTemporarily: return "stoppedTunnelTemporarily"
}
}
}
// Loosely inspired from WireGuardAdapter from WireGuardKit
public class Adapter {
typealias StartTunnelCompletionHandler = ((AdapterError?) -> Void)
typealias StopTunnelCompletionHandler = (() -> Void)
private let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel")
private var callbackHandler: CallbackHandler
// Latest applied NETunnelProviderNetworkSettings
public var lastNetworkSettings: NEPacketTunnelNetworkSettings?
/// Network settings
private var networkSettings: NetworkSettings?
/// Packet tunnel provider.
private weak var packetTunnelProvider: NEPacketTunnelProvider?
@@ -47,25 +72,36 @@ public class Adapter {
private let workQueue = DispatchQueue(label: "FirezoneAdapterWorkQueue")
/// Adapter state.
private var state: State = .stopped
private var state: AdapterState {
didSet {
logger.debug("Adapter state changed to: \(self.state, privacy: .public)")
}
}
/// Keep track of resources
private var displayableResources = DisplayableResources()
public init(with packetTunnelProvider: NEPacketTunnelProvider) {
/// Starting parameters
private var portalURLString: String
private var token: String
public init(portalURLString: String, token: String, packetTunnelProvider: NEPacketTunnelProvider) {
self.portalURLString = portalURLString
self.token = token
self.packetTunnelProvider = packetTunnelProvider
self.callbackHandler = CallbackHandler()
self.callbackHandler.delegate = self
self.state = .stoppedTunnel
}
deinit {
self.logger.debug("Adapter.deinit")
// Cancel network monitor
networkMonitor?.cancel()
// Shutdown the tunnel
if case .started(let wrappedSession) = self.state {
self.logger.log(level: .debug, "\(#function)")
wrappedSession.disconnect()
if case .tunnelReady(let wrappedSession) = self.state {
logger.debug("Adapter.deinit: Shutting down connlib")
_ = wrappedSession.disconnect()
}
}
@@ -73,59 +109,76 @@ public class Adapter {
/// - Parameters:
/// - completionHandler: completion handler.
public func start(completionHandler: @escaping (AdapterError?) -> Void) throws {
workQueue.async {
guard case .stopped = self.state else {
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.start")
guard case .stoppedTunnel = self.state else {
completionHandler(.invalidState)
return
}
let networkMonitor = NWPathMonitor()
networkMonitor.pathUpdateHandler = { [weak self] path in
self?.didReceivePathUpdate(path: path)
}
networkMonitor.start(queue: self.workQueue)
self.callbackHandler.delegate = self
self.logger.debug("Adapter.start: Starting connlib")
do {
try self.setNetworkSettings(self.generateNetworkSettings(ipv4Routes: [], ipv6Routes: []))
self.state = .started(
try WrappedSession.connect("http://localhost:4568", "test-token", self.callbackHandler)
self.state = .startingTunnel(
session: try WrappedSession.connect(self.portalURLString, self.token, self.callbackHandler),
onStarted: completionHandler
)
self.networkMonitor = networkMonitor
completionHandler(nil)
} catch let error as AdapterError {
networkMonitor.cancel()
completionHandler(error)
} catch {
fatalError()
} catch let error {
self.logger.error("Adapter.start: Error: \(error, privacy: .public)")
self.state = .stoppedTunnel
completionHandler(AdapterError.connlibConnectError(error))
}
}
}
public func stop(completionHandler: @escaping (AdapterError?) -> Void) {
workQueue.async {
switch self.state {
case .started(let wrappedSession):
wrappedSession.disconnect()
case .temporaryShutdown:
break
/// Stop the tunnel
public func stop(completionHandler: @escaping () -> Void) {
workQueue.async { [weak self] in
guard let self = self else { return }
case .stopped:
completionHandler(.invalidState)
return
self.logger.debug("Adapter.stop")
switch self.state {
case .stoppedTunnel, .stoppingTunnel:
break
case .tunnelReady(let session):
self.logger.debug("Adapter.stop: Shutting down connlib")
self.state = .stoppingTunnel(session: session, onStopped: completionHandler)
_ = session.disconnect()
case .startingTunnel(let session, let onStarted):
self.logger.debug("Adapter.stop: Shutting down connlib before tunnel ready")
self.state = .stoppingTunnel(session: session, onStopped: {
onStarted?(AdapterError.stoppedByRequestWhileStarting)
completionHandler()
})
// FIXME: Is it ok to call disconnect() while we haven't got onTunnelReady?
_ = session.disconnect()
case .stoppingTunnelTemporarily(let session, let onStopped):
self.state = .stoppingTunnel(session: session, onStopped: {
onStopped?()
completionHandler()
})
case .stoppedTunnelTemporarily:
self.state = .stoppedTunnel
completionHandler()
}
self.networkMonitor?.cancel()
self.networkMonitor = nil
self.state = .stopped
completionHandler(nil)
}
}
/// Get the current set of resources in the completionHandler.
/// If unchanged since referenceVersionString, call completionHandler(nil).
public func getDisplayableResourcesIfVersionDifferentFrom(
referenceVersionString: String, completionHandler: @escaping (DisplayableResources?) -> Void) {
workQueue.async {
workQueue.async { [weak self] in
guard let self = self else { return }
if referenceVersionString == self.displayableResources.versionString {
completionHandler(nil)
} else {
@@ -133,209 +186,255 @@ public class Adapter {
}
}
}
}
public func generateNetworkSettings(
addresses4: [String] = ["100.100.111.2"], addresses6: [String] = ["fd00:0222:2011:1111::2"],
ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route]
)
-> NEPacketTunnelNetworkSettings
{
// The destination IP that connlib will assign our DNS proxy to.
let dnsSentinel = "1.1.1.1"
// MARK: Responding to path updates
// We can probably do better than this; see https://www.rfc-editor.org/info/rfc4821
// But stick with something simple for now. 1280 is the minimum that will work for IPv6.
let mtu = 1280
// TODO: replace these with IPs returned from the connect call to portal
let subnetmask = "255.192.0.0"
let networkPrefixLength = NSNumber(value: 64)
/* iOS requires a tunnel endpoint, whereas in WireGuard it's valid for
* a tunnel to have no endpoint, or for there to be many endpoints, in
* which case, displaying a single one in settings doesn't really
* make sense. So, we fill it in with this placeholder, which is not
* a valid IP address that will actually route over the Internet.
*/
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let dnsSettings = NEDNSSettings(servers: [dnsSentinel])
// All DNS queries must first go through the tunnel's DNS
dnsSettings.matchDomains = [""]
networkSettings.dnsSettings = dnsSettings
networkSettings.mtu = NSNumber(value: mtu)
let ipv4Settings = NEIPv4Settings(
addresses: addresses4,
subnetMasks: [subnetmask])
ipv4Settings.includedRoutes = ipv4Routes
networkSettings.ipv4Settings = ipv4Settings
let ipv6Settings = NEIPv6Settings(
addresses: addresses6,
networkPrefixLengths: [networkPrefixLength])
ipv6Settings.includedRoutes = ipv6Routes
networkSettings.ipv6Settings = ipv6Settings
return networkSettings
}
public func setNetworkSettings(_ networkSettings: NEPacketTunnelNetworkSettings) throws {
var systemError: Error?
let condition = NSCondition()
// Activate the condition
condition.lock()
defer { condition.unlock() }
self.packetTunnelProvider?.setTunnelNetworkSettings(networkSettings) { error in
systemError = error
condition.signal()
}
// Packet tunnel's `setTunnelNetworkSettings` times out in certain
// scenarios & never calls the given callback.
let setTunnelNetworkSettingsTimeout: TimeInterval = 5 // seconds
if condition.wait(until: Date().addingTimeInterval(setTunnelNetworkSettingsTimeout)) {
if let systemError = systemError {
throw AdapterError.setNetworkSettings(systemError)
}
}
// Save the latest applied network settings if there was no error.
if systemError != nil {
self.lastNetworkSettings = networkSettings
}
}
/// Update runtime configuration.
/// - Parameters:
/// - ipv4Routes: IPv4 routes to send through the tunnel.
/// - ipv6Routes: IPv6 routes to send through the tunnel.
/// - completionHandler: completion handler.
public func update(
ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route],
completionHandler: @escaping (AdapterError?) -> Void
) {
workQueue.async {
if case .stopped = self.state {
completionHandler(.invalidState)
return
}
// Tell the system that the tunnel is going to reconnect using new WireGuard
// configuration.
// This will broadcast the `NEVPNStatusDidChange` notification to the GUI process.
self.packetTunnelProvider?.reasserting = true
defer {
self.packetTunnelProvider?.reasserting = false
}
do {
try self.setNetworkSettings(
self.generateNetworkSettings(ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes))
switch self.state {
case .started(let wrappedSession):
self.state = .started(wrappedSession)
case .temporaryShutdown:
self.state = .temporaryShutdown
case .stopped:
fatalError()
}
completionHandler(nil)
} catch let error as AdapterError {
completionHandler(error)
} catch {
fatalError()
}
extension Adapter {
private func beginPathMonitoring() {
self.logger.debug("Beginning path monitoring")
let networkMonitor = NWPathMonitor()
networkMonitor.pathUpdateHandler = { [weak self] path in
self?.didReceivePathUpdate(path: path)
}
networkMonitor.start(queue: self.workQueue)
}
private func didReceivePathUpdate(path: Network.NWPath) {
#if os(macOS)
if case .started(let wrappedSession) = self.state {
self.logger.log(level: .debug, "Suppressing call to bumpSockets()")
// wrappedSession.bumpSockets()
}
#elseif os(iOS)
switch self.state {
case .started(let wrappedSession):
// Will be invoked in the workQueue by the path monitor
switch self.state {
case .startingTunnel(let session, let onStarted):
if path.status != .satisfied {
self.logger.debug("Adapter.didReceivePathUpdate: Offline. Shutting down connlib.")
onStarted?(nil)
self.packetTunnelProvider?.reasserting = true
self.state = .stoppingTunnelTemporarily(session: session, onStopped: nil)
// FIXME: Is it ok to call disconnect() while we haven't got onTunnelReady?
_ = session.disconnect()
}
case .tunnelReady(let session):
if path.status == .satisfied {
self.logger.log(level: .debug, "Suppressing calls to disableSomeRoamingForBrokenMobileSemantics() and bumpSockets()")
self.logger.debug("Suppressing calls to disableSomeRoamingForBrokenMobileSemantics() and bumpSockets()")
// #if os(iOS)
// wrappedSession.disableSomeRoamingForBrokenMobileSemantics()
// #endif
// wrappedSession.bumpSockets()
} else {
//self.logger.log(.debug, "Connectivity offline, pausing backend.")
self.state = .temporaryShutdown
wrappedSession.disconnect()
self.logger.debug("Adapter.didReceivePathUpdate: Offline. Shutting down connlib.")
self.packetTunnelProvider?.reasserting = true
self.state = .stoppingTunnelTemporarily(session: session, onStopped: nil)
_ = session.disconnect()
}
case .temporaryShutdown:
case .stoppingTunnelTemporarily:
break
case .stoppedTunnelTemporarily:
guard path.status == .satisfied else { return }
self.logger.log(level: .debug, "Connectivity online, resuming backend.")
self.logger.debug("Adapter.didReceivePathUpdate: Back online. Starting connlib.")
do {
try self.setNetworkSettings(self.lastNetworkSettings!)
self.state = .started(
try WrappedSession.connect("http://localhost:4568", "test-token", self.callbackHandler)
self.state = .startingTunnel(
session: try WrappedSession.connect(portalURLString, token, self.callbackHandler),
onStarted: { error in
if let error = error {
self.logger.error("Adapter.didReceivePathUpdate: Error starting connlib: \(error, privacy: .public)")
self.packetTunnelProvider?.cancelTunnelWithError(error)
} else {
self.packetTunnelProvider?.reasserting = false
}
}
)
} catch let error as AdapterError {
self.logger.error("Adapter.didReceivePathUpdate: Error: \(error, privacy: .public)")
} catch {
self.logger.log(level: .debug, "Failed to restart backend: \(error.localizedDescription)")
self.logger.error("Adapter.didReceivePathUpdate: Unknown error: \(error, privacy: .public) (fatal)")
}
case .stopped:
case .stoppingTunnel, .stoppedTunnel:
// no-op
break
}
#else
#error("Unsupported")
#endif
}
}
}
// MARK: Implementing CallbackHandlerDelegate
extension Adapter: CallbackHandlerDelegate {
public func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String) {
// Unimplemented
public func onSetInterfaceConfig(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: String) {
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.onSetInterfaceConfig")
switch self.state {
case .startingTunnel:
self.networkSettings = NetworkSettings(
tunnelAddressIPv4: tunnelAddressIPv4, tunnelAddressIPv6: tunnelAddressIPv6,
dnsAddress: dnsAddress, dnsFallbackStrategy: NetworkSettings.DNSFallbackStrategy(dnsFallbackStrategy))
case .tunnelReady:
if let networkSettings = self.networkSettings {
networkSettings.setDNSFallbackStrategy(NetworkSettings.DNSFallbackStrategy(dnsFallbackStrategy))
if let packetTunnelProvider = self.packetTunnelProvider {
networkSettings.apply(on: packetTunnelProvider, logger: self.logger, completionHandler: nil)
}
}
case .stoppingTunnel, .stoppedTunnel, .stoppingTunnelTemporarily, .stoppedTunnelTemporarily:
// This is not expected to happen
break
}
}
}
public func onTunnelReady() {
// Unimplemented
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.onTunnelReady")
guard case .startingTunnel(let session, let onStarted) = self.state else {
self.logger.error("Adapter.onTunnelReady: Unexpected state: \(self.state, privacy: .public)")
return
}
guard let networkSettings = self.networkSettings else {
self.logger.error("Adapter.onTunnelReady: No network settings")
return
}
guard let packetTunnelProvider = self.packetTunnelProvider else {
self.logger.error("Adapter.onTunnelReady: No packet tunnel provider")
return
}
networkSettings.apply(on: packetTunnelProvider, logger: self.logger) { error in
if let error = error {
onStarted?(AdapterError.setNetworkSettings(error))
self.state = .stoppedTunnel
} else {
onStarted?(nil)
self.state = .tunnelReady(session: session)
self.beginPathMonitoring()
}
}
}
}
public func onAddRoute(_: String) {
// Unimplemented
public func onAddRoute(_ route: String) {
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.onAddRoute(\(route, privacy: .public))")
guard let networkSettings = self.networkSettings else {
self.logger.error("Adapter.onAddRoute: No network settings")
return
}
guard let packetTunnelProvider = self.packetTunnelProvider else {
self.logger.error("Adapter.onAddRoute: No packet tunnel provider")
return
}
networkSettings.addRoute(route)
if case .tunnelReady = self.state {
networkSettings.apply(on: packetTunnelProvider, logger: self.logger, completionHandler: nil)
}
}
}
public func onRemoveRoute(_: String) {
// Unimplemented
public func onRemoveRoute(_ route: String) {
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.onRemoveRoute(\(route, privacy: .public))")
guard let networkSettings = self.networkSettings else {
self.logger.error("Adapter.onRemoveRoute: No network settings")
return
}
guard let packetTunnelProvider = self.packetTunnelProvider else {
self.logger.error("Adapter.onRemoveRoute: No packet tunnel provider")
return
}
networkSettings.removeRoute(route)
if case .tunnelReady = self.state {
networkSettings.apply(on: packetTunnelProvider, logger: self.logger, completionHandler: nil)
}
}
}
public func onUpdateResources(resourceList: String) {
workQueue.async {
let jsonString = "[\(resourceList)]"
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.onUpdateResources")
let jsonString = resourceList
guard let jsonData = jsonString.data(using: .utf8) else {
return
}
guard let networkResources = try? JSONDecoder().decode([NetworkResource].self, from: jsonData) else {
return
}
// Note down the resources
self.displayableResources.update(resources: networkResources.map { $0.displayableResource })
// Update DNS in case resource domains is changing
guard let networkSettings = self.networkSettings else {
self.logger.error("Adapter.onUpdateResources: No network settings")
return
}
guard let packetTunnelProvider = self.packetTunnelProvider else {
self.logger.error("Adapter.onUpdateResources: No packet tunnel provider")
return
}
let updatedResourceDomains = networkResources.compactMap { $0.resourceLocation.domain }
networkSettings.setResourceDomains(updatedResourceDomains)
if case .tunnelReady = self.state {
networkSettings.apply(on: packetTunnelProvider, logger: self.logger, completionHandler: nil)
}
}
}
public func onDisconnect(error: Optional<String>) {
// Unimplemented
workQueue.async { [weak self] in
guard let self = self else { return }
self.logger.debug("Adapter.onDisconnect")
if let errorMessage = error {
self.logger.error("Connlib disconnected with unrecoverable error: \(errorMessage, privacy: .public)")
switch self.state {
case .stoppingTunnel(session: _, let onStopped):
onStopped?()
self.state = .stoppedTunnel
case .stoppingTunnelTemporarily(session: _, let onStopped):
onStopped?()
self.state = .stoppedTunnel
case .stoppedTunnel:
// This should not happen
break
case .stoppedTunnelTemporarily:
self.state = .stoppedTunnel
default:
self.packetTunnelProvider?.cancelTunnelWithError(AdapterError.connlibFatalError(errorMessage))
self.state = .stoppedTunnel
}
} else {
self.logger.debug("Connlib disconnected")
switch self.state {
case .stoppingTunnel(session: _, let onStopped):
onStopped?()
self.state = .stoppedTunnel
case .stoppingTunnelTemporarily(session: _, let onStopped):
onStopped?()
self.state = .stoppedTunnelTemporarily
default:
// This should not happen
self.state = .stoppedTunnel
}
}
}
}
public func onError(error: String) {
let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel")
logger.log(level: .error, "Internal connlib error: \(error, privacy: .public)")
self.logger.error("Internal connlib error: \(error, privacy: .public)")
}
}

View File

@@ -17,6 +17,13 @@ public struct NetworkResource: Decodable {
case .cidr(let cidrAddress): return cidrAddress
}
}
var domain: String? {
switch self {
case .dns(let domain, ipv4: _, ipv6: _): return domain
case .cidr(cidrAddress: _): return nil
}
}
}
let name: String

View File

@@ -0,0 +1,194 @@
//
// NetworkSettings.swift
// (c) 2023 Firezone, Inc.
// LICENSE: Apache-2.0
import Foundation
import NetworkExtension
import os.log
class NetworkSettings {
enum DNSFallbackStrategy: String {
// How to handle DNS requests for domains not handled by Firezone
case systemResolver = "system_resolver" // Have the OS handle it using split-DNS
case upstreamResolver = "upstream_resolver" // Have connlib pass it on to a user-specified DNS server
init(_ string: String) {
if string == "upstream_resolver" {
self = .upstreamResolver
} else if string == "system_resolver" {
self = .systemResolver
} else {
// silent default
self = .systemResolver
}
}
}
// Unchanging values
let tunnelAddressIPv4: String
let tunnelAddressIPv6: String
let dnsAddress: String
// Modifiable values
private(set) var dnsFallbackStrategy: DNSFallbackStrategy
private(set) var routes: [String] = []
private(set) var resourceDomains: [String] = []
// To keep track of modifications
private(set) var hasUnappliedChanges: Bool
init(tunnelAddressIPv4: String, tunnelAddressIPv6: String, dnsAddress: String, dnsFallbackStrategy: DNSFallbackStrategy) {
self.tunnelAddressIPv4 = tunnelAddressIPv4
self.tunnelAddressIPv6 = tunnelAddressIPv6
self.dnsAddress = dnsAddress
self.dnsFallbackStrategy = dnsFallbackStrategy
self.hasUnappliedChanges = true
}
func setDNSFallbackStrategy(_ dnsFallbackStrategy: DNSFallbackStrategy) {
if self.dnsFallbackStrategy != dnsFallbackStrategy {
self.dnsFallbackStrategy = dnsFallbackStrategy
self.hasUnappliedChanges = true
}
}
func addRoute(_ route: String) {
if !self.routes.contains(route) {
self.routes.append(route)
self.hasUnappliedChanges = true
}
}
func removeRoute(_ route: String) {
if self.routes.contains(route) {
self.routes.removeAll(where: { $0 == route })
self.hasUnappliedChanges = true
}
}
func setResourceDomains(_ resourceDomains: [String]) {
let sortedResourceDomains = resourceDomains.sorted()
if self.resourceDomains != sortedResourceDomains {
self.resourceDomains = sortedResourceDomains
if dnsFallbackStrategy == .systemResolver {
self.hasUnappliedChanges = true
}
}
}
func apply(on packetTunnelProvider: NEPacketTunnelProvider, logger: Logger, completionHandler: ((Error?) -> Void)?) {
guard self.hasUnappliedChanges else {
logger.error("NetworkSettings.apply: No changes to apply")
completionHandler?(nil)
return
}
logger.debug("NetworkSettings.apply: Applying network settings")
var tunnelIPv4Routes: [NEIPv4Route] = []
var tunnelIPv6Routes: [NEIPv6Route] = []
for route in routes {
let components = route.split(separator: "/")
guard components.count == 2 else {
logger.error("NetworkSettings.apply: Ignoring invalid route '\(route, privacy: .public)'")
continue
}
let address = String(components[0])
let networkPrefixLengthString = String(components[1])
if let groupSeparator = address.first(where: { $0 == "." || $0 == ":"}) {
if groupSeparator == "." { // IPv4 address
if IPv4Address(address) == nil {
logger.error("NetworkSettings.apply: Ignoring invalid IPv4 address '\(address, privacy: .public)'")
continue
}
let validNetworkPrefixLength = Self.validNetworkPrefixLength(fromString: networkPrefixLengthString, maximumValue: 32)
let ipv4SubnetMask = Self.ipv4SubnetMask(networkPrefixLength: validNetworkPrefixLength)
logger.debug("NetworkSettings.apply: Adding IPv4 route: \(address) (subnet mask: \(ipv4SubnetMask))")
tunnelIPv4Routes.append(NEIPv4Route(destinationAddress: address, subnetMask: ipv4SubnetMask))
}
if groupSeparator == ":" { // IPv6 address
if IPv6Address(address) == nil {
logger.error("NetworkSettings.apply: Ignoring invalid IPv6 address '\(address, privacy: .public)'")
continue
}
let validNetworkPrefixLength = Self.validNetworkPrefixLength(fromString: networkPrefixLengthString, maximumValue: 128)
logger.debug("NetworkSettings.apply: Adding IPv6 route: \(address) (prefix length: \(validNetworkPrefixLength))")
tunnelIPv6Routes.append(NEIPv6Route(destinationAddress: address, networkPrefixLength: NSNumber(integerLiteral: validNetworkPrefixLength)))
}
} else {
logger.error("NetworkSettings.apply: Ignoring invalid route '\(route, privacy: .public)'")
}
}
// We don't really know the connlib gateway IP address at this point, but just using 127.0.0.1 is okay
// because the OS doesn't really need this IP address.
// NEPacketTunnelNetworkSettings taking in tunnelRemoteAddress is probably a bad abstraction caused by
// NEPacketTunnelNetworkSettings inheriting from NETunnelNetworkSettings.
let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
let ipv4Settings = NEIPv4Settings(addresses: [tunnelAddressIPv4], subnetMasks: ["255.255.255.255"])
ipv4Settings.includedRoutes = tunnelIPv4Routes
tunnelNetworkSettings.ipv4Settings = ipv4Settings
let ipv6Settings = NEIPv6Settings(addresses: [tunnelAddressIPv6], networkPrefixLengths: [128])
ipv6Settings.includedRoutes = tunnelIPv6Routes
tunnelNetworkSettings.ipv6Settings = ipv6Settings
let dnsSettings = NEDNSSettings(servers: [dnsAddress])
switch dnsFallbackStrategy {
case .systemResolver:
// Enable split-DNS. Only those domains matching the resources will be sent to the tunnel's DNS.
dnsSettings.matchDomains = resourceDomains
case .upstreamResolver:
// All DNS queries go to the tunnel's DNS.
dnsSettings.matchDomains = [""]
}
tunnelNetworkSettings.dnsSettings = dnsSettings
self.hasUnappliedChanges = false
packetTunnelProvider.setTunnelNetworkSettings(tunnelNetworkSettings) { error in
if let error = error {
logger.error("NetworkSettings.apply: Error: \(error, privacy: .public)")
} else {
guard !self.hasUnappliedChanges else {
// Changes were made while the packetTunnelProvider was setting the network settings
logger.debug("NetworkSettings.apply: Applying changes made to network settings while we were applying the network settings")
self.apply(on: packetTunnelProvider, logger: logger, completionHandler: completionHandler)
return
}
logger.debug("NetworkSettings.apply: Applied successfully")
}
completionHandler?(error)
}
}
}
private extension NetworkSettings {
private static func validNetworkPrefixLength(fromString string: String, maximumValue: Int) -> Int {
guard let networkPrefixLength = Int(string) else { return 0 }
if networkPrefixLength < 0 { return 0 }
if networkPrefixLength > maximumValue { return maximumValue }
return networkPrefixLength
}
private static func ipv4SubnetMask(networkPrefixLength: Int) -> String {
precondition(networkPrefixLength >= 0 && networkPrefixLength <= 32)
let mask: UInt32 = 0xFFFFFFFF
let maxPrefixLength = 32
let octets = 4
let subnetMask = mask & (mask << (maxPrefixLength - networkPrefixLength))
var parts: [String] = []
for idx in 0...(octets - 1) {
let part = String(UInt32(0x000000FF) & (subnetMask >> ((octets - 1 - idx) * 8)), radix: 10)
parts.append(part)
}
return parts.joined(separator: ".")
}
}

View File

@@ -16,7 +16,7 @@ enum PacketTunnelProviderError: String, Error {
class PacketTunnelProvider: NEPacketTunnelProvider {
static let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel")
private lazy var adapter = Adapter(with: self)
private var adapter: Adapter?
override func startTunnel(
options _: [String: NSObject]? = nil,
@@ -28,7 +28,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
let providerConfiguration = tunnelProviderProtocol.providerConfiguration
guard let portalURL = providerConfiguration?["portalURL"] as? String else {
guard let portalURLString = providerConfiguration?["portalURL"] as? String else {
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
@@ -36,14 +36,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
completionHandler(PacketTunnelProviderError.savedProtocolConfigurationIsInvalid)
return
}
Self.logger.log("portalURL = \(portalURL, privacy: .public)")
Self.logger.log("token = \(token, privacy: .public)")
let adapter = Adapter(portalURLString: portalURLString, token: token, packetTunnelProvider: self)
self.adapter = adapter
do {
// Once connlib is updated to take in portalURL and token, this call
// should become adapter.start(portalURL: portalURL, token: token)
try adapter.start { error in
try adapter.start() { error in
if let error {
Self.logger.error("Error in adapter.start: \(error)")
}
@@ -55,24 +51,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
}
override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
adapter.stop { error in
if let error {
Self.logger.error("Error in adapter.stop: \(error)")
}
adapter?.stop {
completionHandler()
#if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323
exit(0)
#endif
}
#if os(macOS)
// HACK: This is a filthy hack to work around Apple bug 32073323 (dup'd by us as 47526107).
// Remove it when they finally fix this upstream and the fix has been rolled out to
// sufficient quantities of users.
exit(0)
#endif
}
override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)? = nil) {
let query = String(data: messageData, encoding: .utf8) ?? ""
adapter.getDisplayableResourcesIfVersionDifferentFrom(referenceVersionString: query) { displayableResources in
adapter?.getDisplayableResourcesIfVersionDifferentFrom(referenceVersionString: query) { displayableResources in
completionHandler?(displayableResources?.toData())
}
}