mobile_nebula/ios/NebulaNetworkExtension/PacketTunnelProvider.swift

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

254 lines
10 KiB
Swift
Raw Normal View History

2020-07-27 20:43:58 +00:00
import NetworkExtension
import MobileNebula
import os.log
2021-04-27 15:29:28 +00:00
import SwiftyJSON
2020-07-27 20:43:58 +00:00
class PacketTunnelProvider: NEPacketTunnelProvider {
private var networkMonitor: NWPathMonitor?
private var ifname: String?
private var site: Site?
private var _log = OSLog(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
private var nebula: MobileNebulaNebula?
2021-04-23 21:23:06 +00:00
private var didSleep = false
2021-04-27 15:29:28 +00:00
private var cachedRouteDescription: String?
2020-07-27 20:43:58 +00:00
2021-04-27 15:29:28 +00:00
// This is the system completionHandler, only set when we expect the UI to ask us to actually start so that errors can flow back to the UI
private var startCompleter: ((Error?) -> Void)?
private func log(_ message: StaticString, _ args: Any...) {
2020-07-27 20:43:58 +00:00
os_log(message, log: _log, args)
}
2021-04-27 15:29:28 +00:00
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
// 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
// In the end we need to call this completionHandler to inform the system of our state
if options?["expectStart"] != nil {
startCompleter = completionHandler
return
}
// 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
start(completionHandler: completionHandler)
}
private func start(completionHandler: @escaping (Error?) -> Void) {
2020-07-27 20:43:58 +00:00
let proto = self.protocolConfiguration as! NETunnelProviderProtocol
var config: Data
var key: String
do {
config = proto.providerConfiguration?["config"] as! Data
site = try Site(proto: proto)
} catch {
//TODO: need a way to notify the app
log("Failed to render config from vpn object")
return completionHandler(error)
}
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
let _site = site!
_log = OSLog(subsystem: "net.defined.mobileNebula:\(_site.name)", category: "PacketTunnelProvider")
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
do {
key = try _site.getKey()
} catch {
return completionHandler(error)
}
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
let fileDescriptor = (self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as? Int32) ?? -1
if fileDescriptor < 0 {
2021-04-27 15:29:28 +00:00
return completionHandler("Starting tunnel failed: Could not determine file descriptor")
2020-07-27 20:43:58 +00:00
}
var ifnameSize = socklen_t(IFNAMSIZ)
let ifnamePtr = UnsafeMutablePointer<CChar>.allocate(capacity: Int(ifnameSize))
ifnamePtr.initialize(repeating: 0, count: Int(ifnameSize))
if getsockopt(fileDescriptor, 2 /* SYSPROTO_CONTROL */, 2 /* UTUN_OPT_IFNAME */, ifnamePtr, &ifnameSize) == 0 {
self.ifname = String(cString: ifnamePtr)
}
ifnamePtr.deallocate()
// 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)
if (err != nil) {
2021-04-27 15:29:28 +00:00
return completionHandler(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
_site.unsafeRoutes.forEach { unsafeRoute in
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
if (err != nil) {
2021-04-27 15:29:28 +00:00
return completionHandler(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
self.setTunnelNetworkSettings(tunnelNetworkSettings, completionHandler: {(error:Error?) in
if (error != nil) {
2021-04-27 15:29:28 +00:00
return completionHandler(error!)
2020-07-27 20:43:58 +00:00
}
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
var err: NSError?
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, Int(fileDescriptor), &err)
2021-04-27 15:29:28 +00:00
self.startNetworkMonitor()
2020-07-27 20:43:58 +00:00
if err != nil {
2021-04-27 15:29:28 +00:00
return completionHandler(err!)
2020-07-27 20:43:58 +00:00
}
2021-04-27 15:29:28 +00:00
2020-07-27 20:43:58 +00:00
self.nebula!.start()
completionHandler(nil)
})
}
2021-04-23 21:23:06 +00:00
//TODO: Sleep/wake get called aggresively and do nothing to help us here, we should locate why that is and make these work appropriately
// override func sleep(completionHandler: @escaping () -> Void) {
// nebula!.sleep()
// completionHandler()
// }
private func startNetworkMonitor() {
networkMonitor = NWPathMonitor()
networkMonitor!.pathUpdateHandler = self.pathUpdate
networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
}
private func stopNetworkMonitor() {
self.networkMonitor?.cancel()
networkMonitor = nil
}
2020-07-27 20:43:58 +00:00
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
nebula?.stop()
2021-04-23 21:23:06 +00:00
stopNetworkMonitor()
2020-07-27 20:43:58 +00:00
completionHandler()
}
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
}
}
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
var str: [String] = []
endpoints.forEach{ endpoint in
switch endpoint {
case let .hostPort(.ipv6(host), port):
str.append("[\(host)]:\(port)")
case let .hostPort(.ipv4(host), port):
str.append("\(host):\(port)")
default:
return
}
}
return str.sorted().joined(separator: ", ")
2020-07-27 20:43:58 +00:00
}
2021-04-27 15:29:28 +00:00
override func handleAppMessage(_ data: Data, completionHandler: ((Data?) -> Void)? = nil) {
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
2020-07-27 20:43:58 +00:00
log("Failed to decode IPCRequest from network extension")
return
}
var error: Error?
2021-04-27 15:29:28 +00:00
var data: JSON?
// start command has special treatment due to needing to call two completers
if call.command == "start" {
self.start() { error in
// Notify the system of our start result
if self.startCompleter != nil {
if error == nil {
// Clean boot, no errors
self.startCompleter!(nil)
} else {
// We encountered an error, we can just pass NSError() here since ios throws it away
// But we will provide it in the event we can intercept the error without doing this workaround sometime in the future
self.startCompleter!(error!.localizedDescription)
}
}
// Notify the UI if we have a completionHandler
if completionHandler != nil {
if error == nil {
// No response data, this is expected on a clean start
completionHandler!(try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)))
} else {
// Error response has
completionHandler!(try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error!.localizedDescription))))
}
}
}
return
}
if nebula == nil {
// Respond with an empty success message in the event a command comes in before we've truly started
log("Received command but do not have a nebula instance")
return completionHandler!(try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)))
}
2020-07-27 20:43:58 +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:
2021-04-27 15:29:28 +00:00
error = "Unknown IPC message type \(call.command)"
2020-07-27 20:43:58 +00:00
}
if (error != nil) {
2021-04-27 15:29:28 +00:00
completionHandler!(try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))))
2020-07-27 20:43:58 +00:00
} else {
2021-04-27 15:29:28 +00:00
completionHandler!(try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data)))
2020-07-27 20:43:58 +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
}
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
}
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
}
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
}
}