Skip to content
This repository was archived by the owner on Jul 5, 2023. It is now read-only.

[WIP] Apple: Adopt FFI changes and update the API exposed to Apple clients #20

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
178 changes: 76 additions & 102 deletions apple/Sources/Connlib/Adapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,9 @@ private enum State {
public class Adapter {
private let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel")

// Maintain a handle to the currently instantiated tunnel adapter 🤮
public static var currentAdapter: Adapter?

// Maintain a reference to the initialized callback handler
public static var callbackHandler: CallbackHandler?

// Latest applied NETunnelProviderNetworkSettings
public var lastNetworkSettings: NEPacketTunnelNetworkSettings?
// Initialize callback handler after the adapter is initialized,
// just when the callback handler needs to be used
private lazy var callbackHandler = CallbackHandler(adapter: self)

/// Packet tunnel provider.
private weak var packetTunnelProvider: NEPacketTunnelProvider?
Expand All @@ -51,19 +46,16 @@ public class Adapter {

/// Adapter state.
private var state: State = .stopped
private var interfaceAddresses: InterfaceAddresses?
private var resources: [Resource] = []

private var isTunnelStarted = false

public init(with packetTunnelProvider: NEPacketTunnelProvider) {
self.packetTunnelProvider = packetTunnelProvider

// There must be a better way than making this a static class var...
Self.currentAdapter = self
Self.callbackHandler = CallbackHandler(adapter: self)
}

deinit {
// Remove static var reference
Self.currentAdapter = nil

// Cancel network monitor
networkMonitor?.cancel()

Expand All @@ -77,7 +69,7 @@ public class Adapter {
/// Start the tunnel tunnel.
/// - Parameters:
/// - completionHandler: completion handler.
public func start(completionHandler: @escaping (AdapterError?) -> Void) throws {
public func start(portalURL: String, token: String, completionHandler: @escaping (AdapterError?) -> Void) throws {
workQueue.async {
guard case .stopped = self.state else {
completionHandler(.invalidState)
Expand All @@ -90,17 +82,20 @@ public class Adapter {
}
networkMonitor.start(queue: self.workQueue)

do {
try self.setNetworkSettings(self.generateNetworkSettings(ipv4Routes: [], ipv6Routes: []))
self.callbackHandler.delegate = self

do {
self.state = .started(
WrappedSession.connect(
"http://localhost:4568",
"test-token",
Self.callbackHandler!
portalURL,
token,
self.callbackHandler
)
)
self.networkMonitor = networkMonitor
self.isTunnelStarted = true
let settings = self.generateNetworkSettings(interfaceAddresses: self.interfaceAddresses, resources: self.resources)
try self.setNetworkSettings(settings)
completionHandler(nil)
} catch let error as AdapterError {
networkMonitor.cancel()
Expand Down Expand Up @@ -133,49 +128,54 @@ 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"
func generateNetworkSettings(interfaceAddresses: InterfaceAddresses?, resources: [Resource]) -> NEPacketTunnelNetworkSettings {

// 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")
// Interface addresses
let ipv4InterfaceAddresses = [interfaceAddresses?.ipv4].compactMap { $0 }
let ipv4SubnetMasks = ipv4InterfaceAddresses.map { _ in "255.255.255.255" }

let ipv6InterfaceAddresses = [interfaceAddresses?.ipv6].compactMap { $0 }
let ipv6SubnetMasks = ipv6InterfaceAddresses.map { _ in NSNumber(integerLiteral: 128) }

// Routes
var ipv4Routes: [NEIPv4Route] = []
var ipv6Routes: [NEIPv6Route] = []

for resource in resources {
switch resource.resourceLocation {
case .dns(_, let ipv4, let ipv6):
if !ipv4.isEmpty {
ipv4Routes.append(NEIPv4Route(destinationAddress: ipv4, subnetMask: "255.255.255.255"))
}
if !ipv6.isEmpty {
ipv6Routes.append(NEIPv6Route(destinationAddress: ipv6, networkPrefixLength: 128))
}
case .cidr(let addressRange):
ipv4Routes.append(NEIPv4Route(destinationAddress: "\(addressRange.maskedAddress())", subnetMask: "\(addressRange.subnetMask())"))
}
}

// DNS
let dnsSentinel = "1.1.1.1" // The destination IP that connlib will assign our DNS proxy to
let dnsSettings = NEDNSSettings(servers: [dnsSentinel])
dnsSettings.matchDomains = [""] // All DNS queries must first go through the tunnel's DNS

// All DNS queries must first go through the tunnel's DNS
dnsSettings.matchDomains = [""]
networkSettings.dnsSettings = dnsSettings
networkSettings.mtu = NSNumber(value: mtu)
// Put it together

let ipv4Settings = NEIPv4Settings(
addresses: addresses4,
subnetMasks: [subnetmask])
let ipv4Settings = NEIPv4Settings(addresses: ipv4InterfaceAddresses, subnetMasks: ipv4SubnetMasks)
let ipv6Settings = NEIPv6Settings(addresses: ipv6InterfaceAddresses, networkPrefixLengths: ipv6SubnetMasks)
ipv4Settings.includedRoutes = ipv4Routes
networkSettings.ipv4Settings = ipv4Settings

let ipv6Settings = NEIPv6Settings(
addresses: addresses6,
networkPrefixLengths: [networkPrefixLength])
ipv6Settings.includedRoutes = ipv6Routes

let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the deleted comment beginning with "iOS requires a tunnel endpoint" still relevant/helpful here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still relevant (comes from WireguardKit as well). I'll retain it.

networkSettings.dnsSettings = dnsSettings
networkSettings.ipv4Settings = ipv4Settings
networkSettings.ipv6Settings = ipv6Settings

// 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.
networkSettings.mtu = NSNumber(value: 1280)

return networkSettings
}

Expand All @@ -201,56 +201,18 @@ public class Adapter {
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
) {
private func updateTunnelNetworkSettings() {
guard isTunnelStarted else {
return
}
let settings = generateNetworkSettings(interfaceAddresses: self.interfaceAddresses, resources: self.resources)
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)
try self.setNetworkSettings(settings)
} catch {
fatalError()
self.logger.log(level: .debug, "setNetworkSettings failed: \(error)")
}
}
}
Expand Down Expand Up @@ -281,7 +243,7 @@ public class Adapter {
try self.setNetworkSettings(self.lastNetworkSettings!)

self.state = .started(
try WrappedSession.connect("http://localhost:4568", "test-token", Self.callbackHandler!)
try WrappedSession.connect("http://localhost:4568", "test-token", self.callbackHandler)
)
} catch {
self.logger.log(level: .debug, "Failed to restart backend: \(error.localizedDescription)")
Expand All @@ -296,3 +258,15 @@ public class Adapter {
#endif
}
}

extension Adapter: CallbackHandlerDelegate {
public func didUpdateResources(_ resources: [Resource]) {
self.resources = resources
updateTunnelNetworkSettings()
}

public func didUpdateInterfaceAddresses(_ interfaceAddresses: InterfaceAddresses) {
self.interfaceAddresses = interfaceAddresses
updateTunnelNetworkSettings()
}
}
92 changes: 34 additions & 58 deletions apple/Sources/Connlib/CallbackHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import NetworkExtension
import os.log

public protocol CallbackHandlerDelegate: AnyObject {
func didUpdateResources(_ resourceList: ResourceList)
func didUpdateResources(_ resources: [Resource])
func didUpdateInterfaceAddresses(_ interfaceAddresses: InterfaceAddresses)
}

public class CallbackHandler {
// TODO: Add a table view property here to update?
var adapter: Adapter?
var adapter: Adapter
public weak var delegate: CallbackHandlerDelegate?

init(adapter: Adapter) {
Expand All @@ -23,71 +23,47 @@ public class CallbackHandler {

func onUpdateResources(resourceList: ResourceList) -> Bool {

// If there's any entity that assigned itself as this callbackHandler's delegate, it will be called everytime this `onUpdateResources` method is, allowing that entity to react to resource updates and do whatever they want.

delegate?.didUpdateResources(resourceList)

let addresses4 =
self.adapter?.lastNetworkSettings?.ipv4Settings?.addresses ?? ["100.100.111.2"]
let addresses6 =
self.adapter?.lastNetworkSettings?.ipv6Settings?.addresses ?? [
"fd00:0222:2021:1111::2"
]
let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel")

// TODO: Use actual passed in resources to achieve split tunnel
let ipv4Routes = [NEIPv4Route(destinationAddress: "100.64.0.0", subnetMask: "255.192.0.0")]
let ipv6Routes = [
NEIPv6Route(destinationAddress: "fd00:0222:2021:1111::0", networkPrefixLength: 64)
]
do {
let resources = try resourceList.toResources()
logger.debug("Resources updated: \(resources)")
delegate?.didUpdateResources(resources)
} catch {
logger.error("Error parsing resource list: \(String(describing: error)) (JSON: \(resourceList.resources.toString()))")
return false
}

return setTunnelSettingsKeepingSomeExisting(
addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes
)
return true
}

func onSetTunnelAddresses(tunnelAddresses: TunnelAddresses) -> Bool {
let addresses4 = [tunnelAddresses.address4.toString()]
let addresses6 = [tunnelAddresses.address6.toString()]
let ipv4Routes =
Adapter.currentAdapter?.lastNetworkSettings?.ipv4Settings?.includedRoutes ?? []
let ipv6Routes =
Adapter.currentAdapter?.lastNetworkSettings?.ipv6Settings?.includedRoutes ?? []

return setTunnelSettingsKeepingSomeExisting(
addresses4: addresses4, addresses6: addresses6, ipv4Routes: ipv4Routes, ipv6Routes: ipv6Routes
)
}

private func setTunnelSettingsKeepingSomeExisting(
addresses4: [String], addresses6: [String], ipv4Routes: [NEIPv4Route], ipv6Routes: [NEIPv6Route]
) -> Bool {
let logger = Logger(subsystem: "dev.firezone.firezone", category: "packet-tunnel")

if self.adapter != nil {
do {
/* If the tunnel interface addresses are being updated, it's impossible for the tunnel to
stay up due to the way WireGuard works. Still, we try not to change the tunnel's routes
here Just In Case™.
*/
Comment on lines -68 to -71

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still relevant?

Copy link
Contributor Author

@roop roop Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not. I think we can update the routes without bringing down the tunnel, but I'm not fully sure because I haven't tried it in practice.

Documentation and forums suggest we are allowed to call setTunnelNetworkSettings() while the tunnel is up. Have not checked it in practice.

try self.adapter!.setNetworkSettings(
self.adapter!.generateNetworkSettings(
addresses4: addresses4,
addresses6: addresses6,
ipv4Routes: ipv4Routes,
ipv6Routes: ipv6Routes
)
)
let interfaceAddresses = tunnelAddresses.toInterfaceAddresses()
logger.debug("Interface addresses updated: (\(interfaceAddresses.ipv4), \(interfaceAddresses.ipv6))")
delegate?.didUpdateInterfaceAddresses(interfaceAddresses)

return true
} catch let error {
logger.log(level: .debug, "Error setting adapter settings: \(String(describing: error))")
return true
}
}

return false
}
} else {
logger.log(level: .debug, "Adapter not initialized!")
extension ResourceList {
enum ParseError: Error {
case notUTF8
}

return false
func toResources() throws -> [Resource] {
let jsonString = resources.toString()
guard let jsonData = jsonString.data(using: .utf8) else {
throw ParseError.notUTF8
}
return try JSONDecoder().decode([Resource].self, from: jsonData)
}
}

extension TunnelAddresses {
func toInterfaceAddresses() -> InterfaceAddresses {
InterfaceAddresses(ipv4: address4.toString(), ipv6: address6.toString())
}
}
Loading