mobile_nebula/ios/NebulaNetworkExtension/PacketTunnelProvider.swift

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

307 lines
12 KiB
Swift
Raw Normal View History

2020-07-27 20:43:58 +00:00
import MobileNebula
2025-02-13 22:33:07 +00:00
import NetworkExtension
2020-07-27 20:43:58 +00:00
import os.log
2021-04-27 15:29:28 +00:00
import SwiftyJSON
2020-07-27 20:43:58 +00:00
enum VPNStartError: Error {
case noManagers
case couldNotFindManager
case noTunFileDescriptor
case noProviderConfig
}
enum AppMessageError: Error {
case unknownIPCType(command: String)
}
extension AppMessageError: LocalizedError {
public var description: String? {
switch self {
2025-02-13 22:33:07 +00:00
case let .unknownIPCType(command):
return NSLocalizedString("Unknown IPC message type \(String(command))", comment: "")
}
}
}
2020-07-27 20:43:58 +00:00
class PacketTunnelProvider: NEPacketTunnelProvider {
private var networkMonitor: NWPathMonitor?
2025-02-13 22:33:07 +00:00
2020-07-27 20:43:58 +00:00
private var site: Site?
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
2020-07-27 20:43:58 +00:00
private var nebula: MobileNebulaNebula?
private var dnUpdater = DNUpdater()
2021-04-23 21:23:06 +00:00
private var didSleep = false
2021-04-27 15:29:28 +00:00
private var cachedRouteDescription: String?
2025-02-13 22:33:07 +00:00
override func startTunnel(options: [String: NSObject]? = nil) async throws {
2021-04-27 15:29:28 +00:00
// There is currently no way to get initialization errors back to the UI via completionHandler here
// `expectStart` is sent only via the UI which means we should wait for the real start command which has another completion handler the UI can intercept
if options?["expectStart"] != nil {
// startTunnel must complete before IPC will work
2021-04-27 15:29:28 +00:00
return
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
// VPN is being booted out of band of the UI. Use the system completion handler as there will be nothing to route initialization errors to but we still need to report
// success/fail by the presence of an error or nil
try await start()
2021-04-27 15:29:28 +00:00
}
2025-02-13 22:33:07 +00:00
private func start() async throws {
var manager: NETunnelProviderManager?
2020-07-27 20:43:58 +00:00
var config: Data
var key: String
2025-02-13 22:33:07 +00:00
2020-07-27 20:43:58 +00:00
do {
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
// TODO: Remove else once we drop support for iOS 16
if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) {
2025-02-13 22:33:07 +00:00
manager = try await findManager()
guard let foundManager = manager else {
throw VPNStartError.couldNotFindManager
}
2025-02-13 22:33:07 +00:00
site = try Site(manager: foundManager)
} else {
// This does not save the manager with the site, which means we cannot update the
// vpn profile name when updates happen (rare).
2025-02-13 22:33:07 +00:00
site = try Site(proto: protocolConfiguration as! NETunnelProviderProtocol)
}
2025-02-13 22:33:07 +00:00
config = try site!.getConfig()
2020-07-27 20:43:58 +00:00
} catch {
2025-02-13 22:33:07 +00:00
// TODO: need a way to notify the app
log.error("Failed to render config from vpn object")
throw error
2020-07-27 20:43:58 +00:00
}
2021-04-27 15:29:28 +00:00
2025-02-13 22:33:07 +00:00
let _site = site!
key = try _site.getKey()
2025-02-13 22:33:07 +00:00
guard let fileDescriptor = tunnelFileDescriptor else {
throw VPNStartError.noTunFileDescriptor
2020-07-27 20:43:58 +00:00
}
let tunFD = Int(fileDescriptor)
2020-07-27 20:43:58 +00:00
// This is set to 127.0.0.1 because it has to be something..
let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
// Make sure our ip is routed to the tun device
var err: NSError?
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
2025-02-13 22:33:07 +00:00
if err != nil {
throw err!
2020-07-27 20:43:58 +00:00
}
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
var routes: [NEIPv4Route] = [NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)]
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
// Add our unsafe routes
try _site.unsafeRoutes.forEach { unsafeRoute in
2020-07-27 20:43:58 +00:00
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
2025-02-13 22:33:07 +00:00
if err != nil {
throw err!
2020-07-27 20:43:58 +00:00
}
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
}
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes
tunnelNetworkSettings.mtu = _site.mtu as NSNumber
2025-02-13 22:33:07 +00:00
try await setTunnelNetworkSettings(tunnelNetworkSettings)
var nebulaErr: NSError?
2025-02-13 22:33:07 +00:00
nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr)
startNetworkMonitor()
2021-04-27 15:29:28 +00:00
if nebulaErr != nil {
2025-02-13 22:33:07 +00:00
log.error("We had an error starting up: \(nebulaErr, privacy: .public)")
throw nebulaErr!
}
2025-02-13 22:33:07 +00:00
nebula!.start()
dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate)
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
private func handleDNUpdate(newSite: Site) {
do {
2025-02-13 22:33:07 +00:00
site = newSite
try nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
} catch {
log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)")
}
}
2025-02-13 22:33:07 +00:00
// TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately
2021-04-23 21:23:06 +00:00
// override func sleep(completionHandler: @escaping () -> Void) {
// nebula!.sleep()
// completionHandler()
// }
2025-02-13 22:33:07 +00:00
private func findManager() async throws -> NETunnelProviderManager {
2025-02-13 22:33:07 +00:00
let targetProtoConfig = protocolConfiguration as? NETunnelProviderProtocol
guard let targetProviderConfig = targetProtoConfig?.providerConfiguration else {
throw VPNStartError.noProviderConfig
}
let targetID = targetProviderConfig["id"] as? String
2025-02-13 22:33:07 +00:00
// Load vpn configs from system, and find the manager matching the one being started
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
for manager in managers {
let mgrProtoConfig = manager.protocolConfiguration as? NETunnelProviderProtocol
guard let mgrProviderConfig = mgrProtoConfig?.providerConfiguration else {
throw VPNStartError.noProviderConfig
}
let id = mgrProviderConfig["id"] as? String
2025-02-13 22:33:07 +00:00
if id == targetID {
return manager
}
}
2025-02-13 22:33:07 +00:00
// If we didn't find anything, throw an error
throw VPNStartError.noManagers
}
2025-02-13 22:33:07 +00:00
2021-04-23 21:23:06 +00:00
private func startNetworkMonitor() {
networkMonitor = NWPathMonitor()
2025-02-13 22:33:07 +00:00
networkMonitor!.pathUpdateHandler = pathUpdate
2021-04-23 21:23:06 +00:00
networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
}
2025-02-13 22:33:07 +00:00
2021-04-23 21:23:06 +00:00
private func stopNetworkMonitor() {
2025-02-13 22:33:07 +00:00
networkMonitor?.cancel()
2021-04-23 21:23:06 +00:00
networkMonitor = nil
}
2025-02-13 22:33:07 +00:00
override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
2020-07-27 20:43:58 +00:00
nebula?.stop()
2021-04-23 21:23:06 +00:00
stopNetworkMonitor()
2020-07-27 20:43:58 +00:00
completionHandler()
}
2025-02-13 22:33:07 +00:00
2020-07-27 20:43:58 +00:00
private func pathUpdate(path: Network.NWPath) {
2021-04-27 15:29:28 +00:00
let routeDescription = collectAddresses(endpoints: path.gateways)
if routeDescription != cachedRouteDescription {
// Don't bother to rebind if we don't have any gateways
if routeDescription != "" {
nebula?.rebind("network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
}
cachedRouteDescription = routeDescription
}
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
var str: [String] = []
2025-02-13 22:33:07 +00:00
for endpoint in endpoints {
2021-04-27 15:29:28 +00:00
switch endpoint {
case let .hostPort(.ipv6(host), port):
str.append("[\(host)]:\(port)")
case let .hostPort(.ipv4(host), port):
str.append("\(host):\(port)")
default:
2025-02-13 22:33:07 +00:00
continue
2021-04-27 15:29:28 +00:00
}
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
return str.sorted().joined(separator: ", ")
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
override func handleAppMessage(_ data: Data) async -> Data? {
2021-04-27 15:29:28 +00:00
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
log.error("Failed to decode IPCRequest from network extension")
return nil
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
2020-07-27 20:43:58 +00:00
var error: Error?
2021-04-27 15:29:28 +00:00
var data: JSON?
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
// start command has special treatment due to needing to call two completers
if call.command == "start" {
do {
2025-02-13 22:33:07 +00:00
try await start()
// No response data, this is expected on a clean start
2025-02-13 22:33:07 +00:00
return try? JSONEncoder().encode(IPCResponse(type: .success, message: nil))
} catch {
defer {
self.cancelTunnelWithError(error)
2021-04-27 15:29:28 +00:00
}
2025-02-13 22:33:07 +00:00
return try? JSONEncoder().encode(IPCResponse(type: .error, message: JSON(error.localizedDescription)))
2021-04-27 15:29:28 +00:00
}
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
if nebula == nil {
// Respond with an empty success message in the event a command comes in before we've truly started
log.warning("Received command but do not have a nebula instance")
2025-02-13 22:33:07 +00:00
return try? JSONEncoder().encode(IPCResponse(type: .success, message: nil))
2021-04-27 15:29:28 +00:00
}
2025-02-13 22:33:07 +00:00
// TODO: try catch over all this
2021-04-27 15:29:28 +00:00
switch call.command {
2020-07-27 20:43:58 +00:00
case "listHostmap": (data, error) = listHostmap(pending: false)
case "listPendingHostmap": (data, error) = listHostmap(pending: true)
case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!)
case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!)
case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!)
default:
error = AppMessageError.unknownIPCType(command: call.command)
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
if error != nil {
return try? JSONEncoder().encode(IPCResponse(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
2020-07-27 20:43:58 +00:00
} else {
2025-02-13 22:33:07 +00:00
return try? JSONEncoder().encode(IPCResponse(type: .success, message: data))
2020-07-27 20:43:58 +00:00
}
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
private func listHostmap(pending: Bool) -> (JSON?, Error?) {
2020-07-27 20:43:58 +00:00
var err: NSError?
let res = nebula!.listHostmap(pending, error: &err)
2021-04-27 15:29:28 +00:00
return (JSON(res), err)
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
private func getHostInfo(args: JSON) -> (JSON?, Error?) {
2020-07-27 20:43:58 +00:00
var err: NSError?
2021-04-27 15:29:28 +00:00
let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
return (JSON(res), err)
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) {
2020-07-27 20:43:58 +00:00
var err: NSError?
2021-04-27 15:29:28 +00:00
let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err)
return (JSON(res), err)
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
2021-04-27 15:29:28 +00:00
private func closeTunnel(args: JSON) -> (JSON?, Error?) {
let res = nebula!.closeTunnel(args["vpnIp"].string)
return (JSON(res), nil)
2020-07-27 20:43:58 +00:00
}
2025-02-13 22:33:07 +00:00
2021-09-21 14:31:26 +00:00
private var tunnelFileDescriptor: Int32? {
var ctlInfo = ctl_info()
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
_ = strcpy($0, "com.apple.net.utun_control")
}
}
2025-02-13 22:33:07 +00:00
for fd: Int32 in 0 ... 1024 {
2021-09-21 14:31:26 +00:00
var addr = sockaddr_ctl()
var ret: Int32 = -1
var len = socklen_t(MemoryLayout.size(ofValue: addr))
withUnsafeMutablePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
ret = getpeername(fd, $0, &len)
}
}
if ret != 0 || addr.sc_family != AF_SYSTEM {
continue
}
if ctlInfo.ctl_id == 0 {
ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
if ret != 0 {
continue
}
}
if addr.sc_id == ctlInfo.ctl_id {
return fd
}
}
return nil
}
2020-07-27 20:43:58 +00:00
}