swiftformat .

This commit is contained in:
Caleb Jasik 2025-02-13 16:33:07 -06:00
parent 02e8c5bf35
commit 7c4a1a5faa
No known key found for this signature in database
9 changed files with 411 additions and 423 deletions

View file

@ -5,31 +5,31 @@ let groupName = "group.net.defined.mobileNebula"
class KeyChain { class KeyChain {
class func save(key: String, data: Data, managed: Bool) -> Bool { class func save(key: String, data: Data, managed: Bool) -> Bool {
var query: [String: Any] = [ var query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword as String, kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String : key, kSecAttrAccount as String: key,
kSecValueData as String : data, kSecValueData as String: data,
kSecAttrAccessGroup as String: groupName, kSecAttrAccessGroup as String: groupName,
] ]
if (managed) { if managed {
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
} }
// Attempt to delete an existing key to allow for an overwrite // Attempt to delete an existing key to allow for an overwrite
_ = self.delete(key: key) _ = delete(key: key)
return SecItemAdd(query as CFDictionary, nil) == 0 return SecItemAdd(query as CFDictionary, nil) == 0
} }
class func load(key: String) -> Data? { class func load(key: String) -> Data? {
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword, kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String : key, kSecAttrAccount as String: key,
kSecReturnData as String : kCFBooleanTrue!, kSecReturnData as String: kCFBooleanTrue!,
kSecMatchLimit as String : kSecMatchLimitOne, kSecMatchLimit as String: kSecMatchLimitOne,
kSecAttrAccessGroup as String: groupName, kSecAttrAccessGroup as String: groupName,
] ]
var dataTypeRef: AnyObject? = nil var dataTypeRef: AnyObject?
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef) let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
@ -41,29 +41,27 @@ class KeyChain {
} }
class func delete(key: String) -> Bool { class func delete(key: String) -> Bool {
let query: [String: Any] = [ let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword as String, kSecClass as String: kSecClassGenericPassword as String,
kSecAttrAccount as String : key, kSecAttrAccount as String: key,
kSecAttrAccessGroup as String: groupName, kSecAttrAccessGroup as String: groupName,
] ]
return SecItemDelete(query as CFDictionary) == 0 return SecItemDelete(query as CFDictionary) == 0
} }
} }
extension Data { extension Data {
init<T>(from value: T) { init<T>(from value: T) {
var value = value var value = value
var data = Data() var data = Data()
withUnsafePointer(to: &value, { (ptr: UnsafePointer<T>) -> Void in withUnsafePointer(to: &value) { (ptr: UnsafePointer<T>) in
data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
}) }
self.init(data) self.init(data)
} }
func to<T>(type: T.Type) -> T { func to<T>(type _: T.Type) -> T {
return self.withUnsafeBytes { $0.load(as: T.self) } return withUnsafeBytes { $0.load(as: T.self) }
} }
} }

View file

@ -1,5 +1,5 @@
import NetworkExtension
import MobileNebula import MobileNebula
import NetworkExtension
import os.log import os.log
import SwiftyJSON import SwiftyJSON
@ -17,13 +17,12 @@ enum AppMessageError: Error {
extension AppMessageError: LocalizedError { extension AppMessageError: LocalizedError {
public var description: String? { public var description: String? {
switch self { switch self {
case .unknownIPCType(let command): case let .unknownIPCType(command):
return NSLocalizedString("Unknown IPC message type \(String(command))", comment: "") return NSLocalizedString("Unknown IPC message type \(String(command))", comment: "")
} }
} }
} }
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
private var networkMonitor: NWPathMonitor? private var networkMonitor: NWPathMonitor?
@ -34,7 +33,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private var didSleep = false private var didSleep = false
private var cachedRouteDescription: String? private var cachedRouteDescription: String?
override func startTunnel(options: [String : NSObject]? = nil) async throws { override func startTunnel(options: [String: NSObject]? = nil) async throws {
// There is currently no way to get initialization errors back to the UI via completionHandler here // 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 // `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 { if options?["expectStart"] != nil {
@ -56,27 +55,27 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS // Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
// TODO: Remove else once we drop support for iOS 16 // TODO: Remove else once we drop support for iOS 16
if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) { if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) {
manager = try await self.findManager() manager = try await findManager()
guard let foundManager = manager else { guard let foundManager = manager else {
throw VPNStartError.couldNotFindManager throw VPNStartError.couldNotFindManager
} }
self.site = try Site(manager: foundManager) site = try Site(manager: foundManager)
} else { } else {
// This does not save the manager with the site, which means we cannot update the // This does not save the manager with the site, which means we cannot update the
// vpn profile name when updates happen (rare). // vpn profile name when updates happen (rare).
self.site = try Site(proto: self.protocolConfiguration as! NETunnelProviderProtocol) site = try Site(proto: protocolConfiguration as! NETunnelProviderProtocol)
} }
config = try self.site!.getConfig() config = try site!.getConfig()
} catch { } catch {
//TODO: need a way to notify the app // TODO: need a way to notify the app
self.log.error("Failed to render config from vpn object") log.error("Failed to render config from vpn object")
throw error throw error
} }
let _site = self.site! let _site = site!
key = try _site.getKey() key = try _site.getKey()
guard let fileDescriptor = self.tunnelFileDescriptor else { guard let fileDescriptor = tunnelFileDescriptor else {
throw VPNStartError.noTunFileDescriptor throw VPNStartError.noTunFileDescriptor
} }
let tunFD = Int(fileDescriptor) let tunFD = Int(fileDescriptor)
@ -87,7 +86,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Make sure our ip is routed to the tun device // Make sure our ip is routed to the tun device
var err: NSError? var err: NSError?
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err) let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR]) tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
@ -96,7 +95,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// Add our unsafe routes // Add our unsafe routes
try _site.unsafeRoutes.forEach { unsafeRoute in try _site.unsafeRoutes.forEach { unsafeRoute in
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err) let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)) routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
@ -105,38 +104,38 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes
tunnelNetworkSettings.mtu = _site.mtu as NSNumber tunnelNetworkSettings.mtu = _site.mtu as NSNumber
try await self.setTunnelNetworkSettings(tunnelNetworkSettings) try await setTunnelNetworkSettings(tunnelNetworkSettings)
var nebulaErr: NSError? var nebulaErr: NSError?
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr) nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, site!.logFile, tunFD, &nebulaErr)
self.startNetworkMonitor() startNetworkMonitor()
if nebulaErr != nil { if nebulaErr != nil {
self.log.error("We had an error starting up: \(nebulaErr, privacy: .public)") log.error("We had an error starting up: \(nebulaErr, privacy: .public)")
throw nebulaErr! throw nebulaErr!
} }
self.nebula!.start() nebula!.start()
self.dnUpdater.updateSingleLoop(site: self.site!, onUpdate: self.handleDNUpdate) dnUpdater.updateSingleLoop(site: site!, onUpdate: handleDNUpdate)
} }
private func handleDNUpdate(newSite: Site) { private func handleDNUpdate(newSite: Site) {
do { do {
self.site = newSite site = newSite
try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey()) try nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
} catch { } catch {
log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)") log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)")
} }
} }
//TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately // TODO: Sleep/wake get called aggressively and do nothing to help us here, we should locate why that is and make these work appropriately
// override func sleep(completionHandler: @escaping () -> Void) { // override func sleep(completionHandler: @escaping () -> Void) {
// nebula!.sleep() // nebula!.sleep()
// completionHandler() // completionHandler()
// } // }
private func findManager() async throws -> NETunnelProviderManager { private func findManager() async throws -> NETunnelProviderManager {
let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol let targetProtoConfig = protocolConfiguration as? NETunnelProviderProtocol
guard let targetProviderConfig = targetProtoConfig?.providerConfiguration else { guard let targetProviderConfig = targetProtoConfig?.providerConfiguration else {
throw VPNStartError.noProviderConfig throw VPNStartError.noProviderConfig
} }
@ -150,7 +149,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
throw VPNStartError.noProviderConfig throw VPNStartError.noProviderConfig
} }
let id = mgrProviderConfig["id"] as? String let id = mgrProviderConfig["id"] as? String
if (id == targetID) { if id == targetID {
return manager return manager
} }
} }
@ -161,16 +160,16 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func startNetworkMonitor() { private func startNetworkMonitor() {
networkMonitor = NWPathMonitor() networkMonitor = NWPathMonitor()
networkMonitor!.pathUpdateHandler = self.pathUpdate networkMonitor!.pathUpdateHandler = pathUpdate
networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor")) networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
} }
private func stopNetworkMonitor() { private func stopNetworkMonitor() {
self.networkMonitor?.cancel() networkMonitor?.cancel()
networkMonitor = nil networkMonitor = nil
} }
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { override func stopTunnel(with _: NEProviderStopReason, completionHandler: @escaping () -> Void) {
nebula?.stop() nebula?.stop()
stopNetworkMonitor() stopNetworkMonitor()
completionHandler() completionHandler()
@ -189,14 +188,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String { private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
var str: [String] = [] var str: [String] = []
endpoints.forEach{ endpoint in for endpoint in endpoints {
switch endpoint { switch endpoint {
case let .hostPort(.ipv6(host), port): case let .hostPort(.ipv6(host), port):
str.append("[\(host)]:\(port)") str.append("[\(host)]:\(port)")
case let .hostPort(.ipv4(host), port): case let .hostPort(.ipv4(host), port):
str.append("\(host):\(port)") str.append("\(host):\(port)")
default: default:
return continue
} }
} }
@ -215,39 +214,38 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
// start command has special treatment due to needing to call two completers // start command has special treatment due to needing to call two completers
if call.command == "start" { if call.command == "start" {
do { do {
try await self.start() try await start()
// No response data, this is expected on a clean start // No response data, this is expected on a clean start
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)) return try? JSONEncoder().encode(IPCResponse(type: .success, message: nil))
} catch { } catch {
defer { defer {
self.cancelTunnelWithError(error) self.cancelTunnelWithError(error)
} }
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription))) return try? JSONEncoder().encode(IPCResponse(type: .error, message: JSON(error.localizedDescription)))
} }
} }
if nebula == nil { if nebula == nil {
// Respond with an empty success message in the event a command comes in before we've truly started // 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") log.warning("Received command but do not have a nebula instance")
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil)) return try? JSONEncoder().encode(IPCResponse(type: .success, message: nil))
} }
//TODO: try catch over all this // TODO: try catch over all this
switch call.command { switch call.command {
case "listHostmap": (data, error) = listHostmap(pending: false) case "listHostmap": (data, error) = listHostmap(pending: false)
case "listPendingHostmap": (data, error) = listHostmap(pending: true) case "listPendingHostmap": (data, error) = listHostmap(pending: true)
case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!) case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!)
case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!) case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!)
case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!) case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!)
default: default:
error = AppMessageError.unknownIPCType(command: call.command) error = AppMessageError.unknownIPCType(command: call.command)
} }
if (error != nil) { if error != nil {
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error"))) return try? JSONEncoder().encode(IPCResponse(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
} else { } else {
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data)) return try? JSONEncoder().encode(IPCResponse(type: .success, message: data))
} }
} }
@ -281,7 +279,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
_ = strcpy($0, "com.apple.net.utun_control") _ = strcpy($0, "com.apple.net.utun_control")
} }
} }
for fd: Int32 in 0...1024 { for fd: Int32 in 0 ... 1024 {
var addr = sockaddr_ctl() var addr = sockaddr_ctl()
var ret: Int32 = -1 var ret: Int32 = -1
var len = socklen_t(MemoryLayout.size(ofValue: addr)) var len = socklen_t(MemoryLayout.size(ofValue: addr))
@ -306,4 +304,3 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return nil return nil
} }
} }

View file

@ -1,12 +1,12 @@
import NetworkExtension
import MobileNebula import MobileNebula
import SwiftyJSON import NetworkExtension
import os.log import os.log
import SwiftyJSON
let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site") let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
enum SiteError: Error { enum SiteError: Error {
case nonConforming(site: [String : Any]?) case nonConforming(site: [String: Any]?)
case noCertificate case noCertificate
case keyLoad case keyLoad
case keySave case keySave
@ -21,7 +21,7 @@ enum SiteError: Error {
extension SiteError: CustomStringConvertible { extension SiteError: CustomStringConvertible {
public var description: String { public var description: String {
switch self { switch self {
case .nonConforming(let site): case let .nonConforming(site):
return String("Non-conforming site \(String(describing: site))") return String("Non-conforming site \(String(describing: site))")
case .noCertificate: case .noCertificate:
return "No certificate found" return "No certificate found"
@ -35,20 +35,20 @@ extension SiteError: CustomStringConvertible {
return "failed to find dn credentials in keychain" return "failed to find dn credentials in keychain"
case .dnCredentialSave: case .dnCredentialSave:
return "failed to store dn credentials in keychain" return "failed to store dn credentials in keychain"
case .unexpected(_): case .unexpected:
return "An unexpected error occurred." return "An unexpected error occurred."
} }
} }
} }
enum IPCResponseType: String, Codable { enum IPCResponseType: String, Codable {
case error = "error" case error
case success = "success" case success
} }
class IPCResponse: Codable { class IPCResponse: Codable {
var type: IPCResponseType var type: IPCResponseType
//TODO: change message to data? // TODO: change message to data?
var message: JSON? var message: JSON?
init(type: IPCResponseType, message: JSON?) { init(type: IPCResponseType, message: JSON?) {
@ -131,7 +131,7 @@ struct CertificateValidity: Codable {
} }
} }
let statusMap: Dictionary<NEVPNStatus, Bool> = [ let statusMap: [NEVPNStatus: Bool] = [
NEVPNStatus.invalid: false, NEVPNStatus.invalid: false,
NEVPNStatus.disconnected: false, NEVPNStatus.disconnected: false,
NEVPNStatus.connecting: false, NEVPNStatus.connecting: false,
@ -140,7 +140,7 @@ let statusMap: Dictionary<NEVPNStatus, Bool> = [
NEVPNStatus.disconnecting: true, NEVPNStatus.disconnecting: true,
] ]
let statusString: Dictionary<NEVPNStatus, String> = [ let statusString: [NEVPNStatus: String] = [
NEVPNStatus.invalid: "Invalid configuration", NEVPNStatus.invalid: "Invalid configuration",
NEVPNStatus.disconnected: "Disconnected", NEVPNStatus.disconnected: "Disconnected",
NEVPNStatus.connecting: "Connecting...", NEVPNStatus.connecting: "Connecting...",
@ -156,7 +156,7 @@ class Site: Codable {
var id: String var id: String
// Stored in proto // Stored in proto
var staticHostmap: Dictionary<String, StaticHosts> var staticHostmap: [String: StaticHosts]
var unsafeRoutes: [UnsafeRoute] var unsafeRoutes: [UnsafeRoute]
var cert: CertificateInfo? var cert: CertificateInfo?
var ca: [CertificateInfo] var ca: [CertificateInfo]
@ -166,7 +166,7 @@ class Site: Codable {
var cipher: String var cipher: String
var sortKey: Int var sortKey: Int
var logVerbosity: String var logVerbosity: String
var connected: Bool? //TODO: active is a better name var connected: Bool? // TODO: active is a better name
var status: String? var status: String?
var logFile: String? var logFile: String?
var managed: Bool var managed: Bool
@ -186,12 +186,12 @@ class Site: Codable {
/// Creates a new site from a vpn manager instance. Mainly used by the UI. A manager is required to be able to edit the system profile /// Creates a new site from a vpn manager instance. Mainly used by the UI. A manager is required to be able to edit the system profile
convenience init(manager: NETunnelProviderManager) throws { convenience init(manager: NETunnelProviderManager) throws {
//TODO: Throw an error and have Sites delete the site, notify the user instead of using ! // TODO: Throw an error and have Sites delete the site, notify the user instead of using !
let proto = manager.protocolConfiguration as! NETunnelProviderProtocol let proto = manager.protocolConfiguration as! NETunnelProviderProtocol
try self.init(proto: proto) try self.init(proto: proto)
self.manager = manager self.manager = manager
self.connected = statusMap[manager.connection.status] connected = statusMap[manager.connection.status]
self.status = statusString[manager.connection.status] status = statusString[manager.connection.status]
} }
convenience init(proto: NETunnelProviderProtocol) throws { convenience init(proto: NETunnelProviderProtocol) throws {
@ -202,7 +202,7 @@ class Site: Codable {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let incoming = try decoder.decode(IncomingSite.self, from: config) let incoming = try decoder.decode(IncomingSite.self, from: config)
self.init(incoming: incoming) self.init(incoming: incoming)
self.needsToMigrateToFS = true needsToMigrateToFS = true
return return
} }
@ -244,18 +244,18 @@ class Site: Codable {
do { do {
let rawCert = incoming.cert let rawCert = incoming.cert
let rawDetails = MobileNebulaParseCerts(rawCert, &err) let rawDetails = MobileNebulaParseCerts(rawCert, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
var certs: [CertificateInfo] var certs: [CertificateInfo]
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!) certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
if (certs.count == 0) { if certs.count == 0 {
throw SiteError.noCertificate throw SiteError.noCertificate
} }
cert = certs[0] cert = certs[0]
if (!cert!.validity.valid) { if !cert!.validity.valid {
errors.append("Certificate is invalid: \(cert!.validity.reason)") errors.append("Certificate is invalid: \(cert!.validity.reason)")
} }
@ -266,19 +266,19 @@ class Site: Codable {
do { do {
let rawCa = incoming.ca let rawCa = incoming.ca
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err) let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!) ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
var hasErrors = false var hasErrors = false
ca.forEach { cert in for cert in ca {
if (!cert.validity.valid) { if !cert.validity.valid {
hasErrors = true hasErrors = true
} }
} }
if (hasErrors && !managed) { if hasErrors && !managed {
errors.append("There are issues with 1 or more ca certificates") errors.append("There are issues with 1 or more ca certificates")
} }
@ -288,17 +288,17 @@ class Site: Codable {
} }
do { do {
logFile = try SiteList.getSiteLogFile(id: self.id, createDir: true).path logFile = try SiteList.getSiteLogFile(id: id, createDir: true).path
} catch { } catch {
logFile = nil logFile = nil
errors.append("Unable to create the site directory: \(error.localizedDescription)") errors.append("Unable to create the site directory: \(error.localizedDescription)")
} }
if (managed && (try? getDNCredentials())?.invalid != false) { if managed && (try? getDNCredentials())?.invalid != false {
errors.append("Unable to fetch managed updates - please re-enroll the device") errors.append("Unable to fetch managed updates - please re-enroll the device")
} }
if (errors.isEmpty) { if errors.isEmpty {
do { do {
let encoder = JSONEncoder() let encoder = JSONEncoder()
let rawConfig = try encoder.encode(incoming) let rawConfig = try encoder.encode(incoming)
@ -307,7 +307,7 @@ class Site: Codable {
var err: NSError? var err: NSError?
MobileNebulaTestConfig(strConfig, key, &err) MobileNebulaTestConfig(strConfig, key, &err)
if (err != nil) { if err != nil {
throw err! throw err!
} }
} catch { } catch {
@ -322,12 +322,12 @@ class Site: Codable {
throw SiteError.keyLoad throw SiteError.keyLoad
} }
//TODO: make sure this is valid on return! // TODO: make sure this is valid on return!
return String(decoding: keyData, as: UTF8.self) return String(decoding: keyData, as: UTF8.self)
} }
func getDNCredentials() throws -> DNCredentials { func getDNCredentials() throws -> DNCredentials {
if (!managed) { if !managed {
throw SiteError.unmanagedGetCredentials throw SiteError.unmanagedGetCredentials
} }
@ -344,7 +344,7 @@ class Site: Codable {
let creds = try getDNCredentials() let creds = try getDNCredentials()
creds.invalid = true creds.invalid = true
if (!(try creds.save(siteID: self.id))) { if try !(creds.save(siteID: id)) {
throw SiteError.dnCredentialLoad throw SiteError.dnCredentialLoad
} }
} }
@ -353,13 +353,13 @@ class Site: Codable {
let creds = try getDNCredentials() let creds = try getDNCredentials()
creds.invalid = false creds.invalid = false
if (!(try creds.save(siteID: self.id))) { if try !(creds.save(siteID: id)) {
throw SiteError.dnCredentialSave throw SiteError.dnCredentialSave
} }
} }
func getConfig() throws -> Data { func getConfig() throws -> Data {
return try self.incomingSite!.getConfig() return try incomingSite!.getConfig()
} }
// Limits what we export to the UI // Limits what we export to the UI
@ -429,7 +429,7 @@ class DNCredentials: Codable {
struct IncomingSite: Codable { struct IncomingSite: Codable {
var name: String var name: String
var id: String var id: String
var staticHostmap: Dictionary<String, StaticHosts> var staticHostmap: [String: StaticHosts]
var unsafeRoutes: [UnsafeRoute]? var unsafeRoutes: [UnsafeRoute]?
var cert: String? var cert: String?
var ca: String? var ca: String?
@ -456,11 +456,11 @@ struct IncomingSite: Codable {
return try encoder.encode(config) return try encoder.encode(config)
} }
func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) { func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> Void) {
let configPath: URL let configPath: URL
do { do {
configPath = try SiteList.getSiteConfigFile(id: self.id, createDir: true) configPath = try SiteList.getSiteConfigFile(id: id, createDir: true)
} catch { } catch {
callback(error) callback(error)
@ -469,45 +469,44 @@ struct IncomingSite: Codable {
log.notice("Saving to \(configPath, privacy: .public)") log.notice("Saving to \(configPath, privacy: .public)")
do { do {
if (self.key != nil) { if key != nil {
let data = self.key!.data(using: .utf8) let data = key!.data(using: .utf8)
if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) { if !KeyChain.save(key: "\(id).key", data: data!, managed: managed ?? false) {
return callback(SiteError.keySave) return callback(SiteError.keySave)
} }
} }
do { do {
if ((try self.dnCredentials?.save(siteID: self.id)) == false) { if try (dnCredentials?.save(siteID: id)) == false {
return callback(SiteError.dnCredentialSave) return callback(SiteError.dnCredentialSave)
} }
} catch { } catch {
return callback(error) return callback(error)
} }
try self.getConfig().write(to: configPath) try getConfig().write(to: configPath)
} catch { } catch {
return callback(error) return callback(error)
} }
#if targetEnvironment(simulator)
#if targetEnvironment(simulator) // We are on a simulator and there is no NEVPNManager for us to interact with
// We are on a simulator and there is no NEVPNManager for us to interact with
callback(nil)
#else
if saveToManager {
self.saveToManager(manager: manager, callback: callback)
} else {
callback(nil) callback(nil)
} #else
#endif if saveToManager {
self.saveToManager(manager: manager, callback: callback)
} else {
callback(nil)
}
#endif
} }
private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) { private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> Void) {
if (manager != nil) { if manager != nil {
// We need to refresh our settings to properly update config // We need to refresh our settings to properly update config
manager?.loadFromPreferences { error in manager?.loadFromPreferences { error in
if (error != nil) { if error != nil {
return callback(error) return callback(error)
} }
@ -519,25 +518,25 @@ struct IncomingSite: Codable {
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback) return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
} }
private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) { private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> Void) {
// Stuff our details in the protocol // Stuff our details in the protocol
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol() let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"; proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"
// WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match // WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match
// managers in PacketTunnelProvider.findManager // managers in PacketTunnelProvider.findManager
proto.providerConfiguration = ["id": self.id] proto.providerConfiguration = ["id": id]
proto.serverAddress = "Nebula" proto.serverAddress = "Nebula"
// Finish up the manager, this is what stores everything at the system level // Finish up the manager, this is what stores everything at the system level
manager.protocolConfiguration = proto manager.protocolConfiguration = proto
//TODO: cert name? manager.protocolConfiguration?.username // TODO: cert name? manager.protocolConfiguration?.username
//TODO: This is what is shown on the vpn page. We should add more identifying details in // TODO: This is what is shown on the vpn page. We should add more identifying details in
manager.localizedDescription = self.name manager.localizedDescription = name
manager.isEnabled = true manager.isEnabled = true
manager.saveToPreferences{ error in manager.saveToPreferences { error in
return callback(error) callback(error)
} }
} }
} }

View file

@ -8,7 +8,7 @@ class SiteList {
let fileManager = FileManager.default let fileManager = FileManager.default
let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")! let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
if (!fileManager.fileExists(atPath: rootDir.absoluteString)) { if !fileManager.fileExists(atPath: rootDir.absoluteString) {
try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true) try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true)
} }
@ -19,7 +19,7 @@ class SiteList {
static func getSitesDir() throws -> URL { static func getSitesDir() throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true) let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true)
if (!fileManager.fileExists(atPath: sitesDir.absoluteString)) { if !fileManager.fileExists(atPath: sitesDir.absoluteString) {
try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true) try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true)
} }
return sitesDir return sitesDir
@ -29,7 +29,7 @@ class SiteList {
static func getSiteDir(id: String, create: Bool = false) throws -> URL { static func getSiteDir(id: String, create: Bool = false) throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true) let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true)
if (create && !fileManager.fileExists(atPath: siteDir.absoluteString)) { if create && !fileManager.fileExists(atPath: siteDir.absoluteString) {
try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true) try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true)
} }
return siteDir return siteDir
@ -45,25 +45,25 @@ class SiteList {
return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false) return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false)
} }
init(completion: @escaping ([String: Site]?, Error?) -> ()) { init(completion: @escaping ([String: Site]?, Error?) -> Void) {
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
SiteList.loadAllFromFS { sites, err in SiteList.loadAllFromFS { sites, err in
if sites != nil { if sites != nil {
self.sites = sites! self.sites = sites!
}
completion(sites, err)
} }
completion(sites, err) #else
} SiteList.loadAllFromNETPM { sites, err in
#else if sites != nil {
SiteList.loadAllFromNETPM { sites, err in self.sites = sites!
if sites != nil { }
self.sites = sites! completion(sites, err)
} }
completion(sites, err) #endif
}
#endif
} }
private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> ()) { private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> Void) {
let fileManager = FileManager.default let fileManager = FileManager.default
var siteDirs: [URL] var siteDirs: [URL]
var sites = [String: Site]() var sites = [String: Site]()
@ -76,7 +76,7 @@ class SiteList {
return return
} }
siteDirs.forEach { path in for path in siteDirs {
do { do {
let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json")) let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json"))
sites[site.id] = site sites[site.id] = site
@ -91,15 +91,15 @@ class SiteList {
completion(sites, nil) completion(sites, nil)
} }
private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> ()) { private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> Void) {
var sites = [String: Site]() var sites = [String: Site]()
// dispatchGroup is used to ensure we have migrated all sites before returning them // dispatchGroup is used to ensure we have migrated all sites before returning them
// If there are no sites to migrate, there are never any entrants // If there are no sites to migrate, there are never any entrants
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in NETunnelProviderManager.loadAllFromPreferences { newManagers, err in
if (err != nil) { if err != nil {
return completion(nil, err) return completion(nil, err)
} }
@ -121,10 +121,10 @@ class SiteList {
sites[site.id] = site sites[site.id] = site
} catch { } catch {
//TODO: notify the user about this // TODO: notify the user about this
print("Deleted non conforming site \(manager) \(error)") print("Deleted non conforming site \(manager) \(error)")
manager.removeFromPreferences() manager.removeFromPreferences()
//TODO: delete from disk, we need to try and discover the site id though // TODO: delete from disk, we need to try and discover the site id though
} }
} }

View file

@ -26,17 +26,18 @@ class APIClient {
hostID: hostID, hostID: hostID,
privateKey: privateKey, privateKey: privateKey,
counter: counter, counter: counter,
trustedKeys: trustedKeys) trustedKeys: trustedKeys
)
} catch { } catch {
// type information from Go is not available, use string matching instead // type information from Go is not available, use string matching instead
if (error.localizedDescription == "invalid credentials") { if error.localizedDescription == "invalid credentials" {
throw APIClientError.invalidCredentials throw APIClientError.invalidCredentials
} }
throw error throw error
} }
if (res.fetchedUpdate) { if res.fetchedUpdate {
return try decodeIncomingSite(jsonSite: res.site) return try decodeIncomingSite(jsonSite: res.site)
} }

View file

@ -1,8 +1,8 @@
import UIKit
import Flutter import Flutter
import MobileNebula import MobileNebula
import NetworkExtension import NetworkExtension
import SwiftyJSON import SwiftyJSON
import UIKit
enum ChannelName { enum ChannelName {
static let vpn = "net.defined.mobileNebula/NebulaVpnService" static let vpn = "net.defined.mobileNebula/NebulaVpnService"
@ -19,18 +19,16 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
private var sites: Sites? private var sites: Sites?
private var ui: FlutterMethodChannel? private var ui: FlutterMethodChannel?
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
dnUpdater.updateAllLoop { site in dnUpdater.updateAllLoop { site in
// Signal the site has changed in case the current site details screen is active // Signal the site has changed in case the current site details screen is active
let container = self.sites?.getContainer(id: site.id) let container = self.sites?.getContainer(id: site.id)
if (container != nil) { if container != nil {
// Update references to the site with the new site config // Update references to the site with the new site config
container!.site = site container!.site = site
container!.updater.update(connected: site.connected ?? false, replaceSite: site) container!.updater.update(connected: site.connected ?? false, replaceSite: site)
@ -47,42 +45,38 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
sites = Sites(messenger: controller.binaryMessenger) sites = Sites(messenger: controller.binaryMessenger)
ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger) ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger)
ui!.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in ui!.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
switch call.method { switch call.method {
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result) case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result) case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
case "nebula.renderConfig": return self.nebulaRenderConfig(call: call, result: result) case "nebula.renderConfig": return self.nebulaRenderConfig(call: call, result: result)
case "nebula.verifyCertAndKey": return self.nebulaVerifyCertAndKey(call: call, result: result) case "nebula.verifyCertAndKey": return self.nebulaVerifyCertAndKey(call: call, result: result)
case "dn.enroll": return self.dnEnroll(call: call, result: result) case "dn.enroll": return self.dnEnroll(call: call, result: result)
case "listSites": return self.listSites(result: result) case "listSites": return self.listSites(result: result)
case "deleteSite": return self.deleteSite(call: call, result: result) case "deleteSite": return self.deleteSite(call: call, result: result)
case "saveSite": return self.saveSite(call: call, result: result) case "saveSite": return self.saveSite(call: call, result: result)
case "startSite": return self.startSite(call: call, result: result) case "startSite": return self.startSite(call: call, result: result)
case "stopSite": return self.stopSite(call: call, result: result) case "stopSite": return self.stopSite(call: call, result: result)
case "active.listHostmap": self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result) case "active.listHostmap": self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result)
case "active.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result) case "active.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result)
case "active.getHostInfo": self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result) case "active.getHostInfo": self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result)
case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result) case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result)
case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result) case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
default: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }
}) }
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) { func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) } guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) } guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) }
var err: NSError? var err: NSError?
let json = MobileNebulaParseCerts(certs, &err) let json = MobileNebulaParseCerts(certs, &err)
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription)) return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription))
} }
@ -90,14 +84,14 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} }
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) { func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) } guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) } guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) }
guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) } guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) }
var err: NSError? var err: NSError?
var validd: ObjCBool = false var validd: ObjCBool = false
let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err) let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err)
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription)) return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription))
} }
@ -107,7 +101,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
func nebulaGenerateKeyPair(result: FlutterResult) { func nebulaGenerateKeyPair(result: FlutterResult) {
var err: NSError? var err: NSError?
let kp = MobileNebulaGenerateKeyPair(&err) let kp = MobileNebulaGenerateKeyPair(&err)
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription)) return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription))
} }
@ -119,7 +113,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
var err: NSError? var err: NSError?
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err) let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)) return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
} }
@ -132,9 +126,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
do { do {
let site = try apiClient.enroll(code: code) let site = try apiClient.enroll(code: code)
let oldSite = self.sites?.getSite(id: site.id) let oldSite = sites?.getSite(id: site.id)
site.save(manager: oldSite?.manager) { error in site.save(manager: oldSite?.manager) { error in
if (error != nil) { if error != nil {
return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription)) return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
} }
@ -146,8 +140,8 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} }
func listSites(result: @escaping FlutterResult) { func listSites(result: @escaping FlutterResult) {
self.sites?.loadSites { (sites, err) -> () in sites?.loadSites { sites, err in
if (err != nil) { if err != nil {
return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription)) return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
} }
@ -160,9 +154,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
func deleteSite(call: FlutterMethodCall, result: @escaping FlutterResult) { func deleteSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let id = call.arguments as? String else { return result(NoArgumentsError()) } guard let id = call.arguments as? String else { return result(NoArgumentsError()) }
//TODO: stop the site if its running currently // TODO: stop the site if its running currently
self.sites?.deleteSite(id: id) { error in sites?.deleteSite(id: id) { error in
if (error != nil) { if error != nil {
result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription)) result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
} }
@ -178,9 +172,9 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
return result(NoArgumentsError()) return result(NoArgumentsError())
} }
let oldSite = self.sites?.getSite(id: site.id) let oldSite = sites?.getSite(id: site.id)
site.save(manager: oldSite?.manager) { error in site.save(manager: oldSite?.manager) { error in
if (error != nil) { if error != nil {
return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription)) return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
} }
@ -191,58 +185,58 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} }
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) { func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) } guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) } guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id) let updater = sites?.getUpdater(id: id)
updater?.update(connected: true) updater?.update(connected: true)
#else #else
let container = self.sites?.getContainer(id: id) let container = sites?.getContainer(id: id)
let manager = container?.site.manager let manager = container?.site.manager
manager?.loadFromPreferences{ error in manager?.loadFromPreferences { error in
//TODO: Handle load error // TODO: Handle load error
// This is silly but we need to enable the site each time to avoid situations where folks have multiple sites // This is silly but we need to enable the site each time to avoid situations where folks have multiple sites
manager?.isEnabled = true manager?.isEnabled = true
manager?.saveToPreferences{ error in manager?.saveToPreferences { error in
//TODO: Handle load error // TODO: Handle load error
manager?.loadFromPreferences{ error in manager?.loadFromPreferences { error in
//TODO: Handle load error // TODO: Handle load error
do { do {
container?.updater.startFunc = {() -> Void in container?.updater.startFunc = { () in
return self.vpnRequest(command: "start", arguments: args, result: result) self.vpnRequest(command: "start", arguments: args, result: result)
}
try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)])
} catch {
return result(CallFailedError(message: "Could not start site", details: error.localizedDescription))
} }
try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)])
} catch {
return result(CallFailedError(message: "Could not start site", details: error.localizedDescription))
} }
} }
} }
} #endif
#endif
} }
func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) { func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) } guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) } guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id) let updater = sites?.getUpdater(id: id)
updater?.update(connected: false) updater?.update(connected: false)
#else #else
let manager = self.sites?.getSite(id: id)?.manager let manager = sites?.getSite(id: id)?.manager
manager?.loadFromPreferences{ error in manager?.loadFromPreferences { _ in
//TODO: Handle load error // TODO: Handle load error
manager?.connection.stopVPNTunnel() manager?.connection.stopVPNTunnel()
return result(nil) return result(nil)
} }
#endif #endif
} }
func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) { func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) {
guard let args = arguments as? Dictionary<String, Any> else { return result(NoArgumentsError()) } guard let args = arguments as? [String: Any] else { return result(NoArgumentsError()) }
guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) } guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) }
let container = sites?.getContainer(id: id) let container = sites?.getContainer(id: id)
@ -258,12 +252,12 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
if let session = container!.site.manager?.connection as? NETunnelProviderSession { if let session = container!.site.manager?.connection as? NETunnelProviderSession {
do { do {
try session.sendProviderMessage(try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in try session.sendProviderMessage(JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in
if data == nil { if data == nil {
return result(nil) return result(nil)
} }
//print(String(decoding: data!, as: UTF8.self)) // print(String(decoding: data!, as: UTF8.self))
guard let res = try? JSONDecoder().decode(IPCResponse.self, from: data!) else { guard let res = try? JSONDecoder().decode(IPCResponse.self, from: data!) else {
return result(CallFailedError(message: "Failed to decode response")) return result(CallFailedError(message: "Failed to decode response"))
} }
@ -278,7 +272,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
return result(CallFailedError(message: error.localizedDescription)) return result(CallFailedError(message: error.localizedDescription))
} }
} else { } else {
//TODO: we have a site without a manager, things have gone weird. How to handle since this shouldn't happen? // TODO: we have a site without a manager, things have gone weird. How to handle since this shouldn't happen?
result(nil) result(nil)
} }
} }

View file

@ -6,13 +6,13 @@ class DNUpdater {
private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes private let timer = RepeatingTimer(timeInterval: 15 * 60) // 15 * 60 is 15 minutes
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater") private let log = Logger(subsystem: "net.defined.mobileNebula", category: "DNUpdater")
func updateAll(onUpdate: @escaping (Site) -> ()) { func updateAll(onUpdate: @escaping (Site) -> Void) {
_ = SiteList{ (sites, _) -> () in _ = SiteList { sites, _ in
// NEVPN seems to force us onto the main thread and we are about to make network calls that // NEVPN seems to force us onto the main thread and we are about to make network calls that
// could block for a while. Push ourselves onto another thread to avoid blocking the UI. // could block for a while. Push ourselves onto another thread to avoid blocking the UI.
Task.detached(priority: .userInitiated) { Task.detached(priority: .userInitiated) {
sites?.values.forEach { site in sites?.values.forEach { site in
if (site.connected == true) { if site.connected == true {
// The vpn service is in charge of updating the currently connected site // The vpn service is in charge of updating the currently connected site
return return
} }
@ -23,23 +23,23 @@ class DNUpdater {
} }
} }
func updateAllLoop(onUpdate: @escaping (Site) -> ()) { func updateAllLoop(onUpdate: @escaping (Site) -> Void) {
timer.eventHandler = { timer.eventHandler = {
self.updateAll(onUpdate: onUpdate) self.updateAll(onUpdate: onUpdate)
} }
timer.resume() timer.resume()
} }
func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> ()) { func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) {
timer.eventHandler = { timer.eventHandler = {
self.updateSite(site: site, onUpdate: onUpdate) self.updateSite(site: site, onUpdate: onUpdate)
} }
timer.resume() timer.resume()
} }
func updateSite(site: Site, onUpdate: @escaping (Site) -> ()) { func updateSite(site: Site, onUpdate: @escaping (Site) -> Void) {
do { do {
if (!site.managed) { if !site.managed {
return return
} }
@ -55,7 +55,7 @@ class DNUpdater {
trustedKeys: credentials.trustedKeys trustedKeys: credentials.trustedKeys
) )
} catch (APIClientError.invalidCredentials) { } catch (APIClientError.invalidCredentials) {
if (!credentials.invalid) { if !credentials.invalid {
try site.invalidateDNCredentials() try site.invalidateDNCredentials()
log.notice("Invalidated credentials in site: \(site.name, privacy: .public)") log.notice("Invalidated credentials in site: \(site.name, privacy: .public)")
} }
@ -67,7 +67,7 @@ class DNUpdater {
let shouldSaveToManager = siteManager != nil || ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) let shouldSaveToManager = siteManager != nil || ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in
if (error != nil) { if error != nil {
self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)") self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)")
} }
@ -75,7 +75,7 @@ class DNUpdater {
onUpdate(Site(incoming: newSite!)) onUpdate(Site(incoming: newSite!))
} }
if (credentials.invalid) { if credentials.invalid {
try site.validateDNCredentials() try site.validateDNCredentials()
log.notice("Revalidated credentials in site \(site.name, privacy: .public)") log.notice("Revalidated credentials in site \(site.name, privacy: .public)")
} }
@ -88,7 +88,6 @@ class DNUpdater {
// From https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9 // From https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9
class RepeatingTimer { class RepeatingTimer {
let timeInterval: TimeInterval let timeInterval: TimeInterval
init(timeInterval: TimeInterval) { init(timeInterval: TimeInterval) {

View file

@ -6,7 +6,7 @@ class PackageInfo {
"unknown" "unknown"
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
if (buildNumber == nil) { if buildNumber == nil {
return version return version
} }

View file

@ -1,5 +1,5 @@
import NetworkExtension
import MobileNebula import MobileNebula
import NetworkExtension
class SiteContainer { class SiteContainer {
var site: Site var site: Site
@ -19,15 +19,15 @@ class Sites {
self.messenger = messenger self.messenger = messenger
} }
func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) { func loadSites(completion: @escaping ([String: Site]?, Error?) -> Void) {
_ = SiteList { (sites, err) in _ = SiteList { sites, err in
if (err != nil) { if err != nil {
return completion(nil, err) return completion(nil, err)
} }
sites?.values.forEach{ site in sites?.values.forEach { site in
var updater = self.containers[site.id]?.updater var updater = self.containers[site.id]?.updater
if (updater != nil) { if updater != nil {
updater!.setSite(site: site) updater!.setSite(site: site)
} else { } else {
updater = SiteUpdater(messenger: self.messenger!, site: site) updater = SiteUpdater(messenger: self.messenger!, site: site)
@ -36,14 +36,14 @@ class Sites {
} }
let justSites = self.containers.mapValues { let justSites = self.containers.mapValues {
return $0.site $0.site
} }
completion(justSites, nil) completion(justSites, nil)
} }
} }
func deleteSite(id: String, callback: @escaping (Error?) -> ()) { func deleteSite(id: String, callback: @escaping (Error?) -> Void) {
if let site = self.containers.removeValue(forKey: id) { if let site = containers.removeValue(forKey: id) {
_ = KeyChain.delete(key: "\(site.site.id).dnCredentials") _ = KeyChain.delete(key: "\(site.site.id).dnCredentials")
_ = KeyChain.delete(key: "\(site.site.id).key") _ = KeyChain.delete(key: "\(site.site.id).key")
@ -55,10 +55,10 @@ class Sites {
print("Failed to delete site from fs: \(error.localizedDescription)") print("Failed to delete site from fs: \(error.localizedDescription)")
} }
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
site.site.manager!.removeFromPreferences(completionHandler: callback) site.site.manager!.removeFromPreferences(completionHandler: callback)
return return
#endif #endif
} }
// Nothing to remove // Nothing to remove
@ -66,21 +66,21 @@ class Sites {
} }
func getSite(id: String) -> Site? { func getSite(id: String) -> Site? {
return self.containers[id]?.site return containers[id]?.site
} }
func getUpdater(id: String) -> SiteUpdater? { func getUpdater(id: String) -> SiteUpdater? {
return self.containers[id]?.updater return containers[id]?.updater
} }
func getContainer(id: String) -> SiteContainer? { func getContainer(id: String) -> SiteContainer? {
return self.containers[id] return containers[id]
} }
} }
class SiteUpdater: NSObject, FlutterStreamHandler { class SiteUpdater: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?; private var eventSink: FlutterEventSink?
private var eventChannel: FlutterEventChannel; private var eventChannel: FlutterEventChannel
private var site: Site private var site: Site
private var notification: Any? private var notification: Any?
public var startFunc: (() -> Void)? public var startFunc: (() -> Void)?
@ -90,15 +90,15 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
init(messenger: FlutterBinaryMessenger, site: Site) { init(messenger: FlutterBinaryMessenger, site: Site) {
do { do {
let configPath = try SiteList.getSiteConfigFile(id: site.id, createDir: false) let configPath = try SiteList.getSiteConfigFile(id: site.id, createDir: false)
self.configFd = open(configPath.path, O_EVTONLY) configFd = open(configPath.path, O_EVTONLY)
self.configObserver = DispatchSource.makeFileSystemObjectSource( configObserver = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: self.configFd!, fileDescriptor: configFd!,
eventMask: .write eventMask: .write
) )
} catch { } catch {
// SiteList.getSiteConfigFile should never throw because we are not creating it here // SiteList.getSiteConfigFile should never throw because we are not creating it here
self.configObserver = nil configObserver = nil
} }
eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger) eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
@ -107,15 +107,15 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
eventChannel.setStreamHandler(self) eventChannel.setStreamHandler(self)
self.configObserver?.setEventHandler(handler: self.configUpdated) configObserver?.setEventHandler(handler: configUpdated)
self.configObserver?.setCancelHandler { configObserver?.setCancelHandler {
if self.configFd != nil { if self.configFd != nil {
close(self.configFd!) close(self.configFd!)
} }
self.configObserver = nil self.configObserver = nil
} }
self.configObserver?.resume() configObserver?.resume()
} }
func setSite(site: Site) { func setSite(site: Site) {
@ -123,44 +123,44 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
} }
/// onListen is called when flutter code attaches an event listener /// onListen is called when flutter code attaches an event listener
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { func onListen(withArguments _: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
eventSink = events; eventSink = events
#if !targetEnvironment(simulator) #if !targetEnvironment(simulator)
if site.manager == nil { if site.manager == nil {
//TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error // TODO: The dn updater path seems to race to build a site that lacks a manager. The UI does not display this error
// and a another listen should occur and succeed. // and a another listen should occur and succeed.
return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil) return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil)
}
self.notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection , queue: nil) { n in
let oldConnected = self.site.connected
self.site.status = statusString[self.site.manager!.connection.status]
self.site.connected = statusMap[self.site.manager!.connection.status]
// Check to see if we just moved to connected and if we have a start function to call when that happens
if self.site.connected! && oldConnected != self.site.connected && self.startFunc != nil {
self.startFunc!()
self.startFunc = nil
} }
self.update(connected: self.site.connected!) notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection, queue: nil) { _ in
} let oldConnected = self.site.connected
#endif self.site.status = statusString[self.site.manager!.connection.status]
self.site.connected = statusMap[self.site.manager!.connection.status]
// Check to see if we just moved to connected and if we have a start function to call when that happens
if self.site.connected!, oldConnected != self.site.connected, self.startFunc != nil {
self.startFunc!()
self.startFunc = nil
}
self.update(connected: self.site.connected!)
}
#endif
return nil return nil
} }
/// onCancel is called when the flutter listener stops listening /// onCancel is called when the flutter listener stops listening
func onCancel(withArguments arguments: Any?) -> FlutterError? { func onCancel(withArguments _: Any?) -> FlutterError? {
if (self.notification != nil) { if notification != nil {
NotificationCenter.default.removeObserver(self.notification!) NotificationCenter.default.removeObserver(notification!)
} }
return nil return nil
} }
/// update is a way to send information to the flutter listener and generally should not be used directly /// update is a way to send information to the flutter listener and generally should not be used directly
func update(connected: Bool, replaceSite: Site? = nil) { func update(connected: Bool, replaceSite: Site? = nil) {
if (replaceSite != nil) { if replaceSite != nil {
site = replaceSite! site = replaceSite!
} }
site.connected = connected site.connected = connected
@ -168,18 +168,18 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
let encoder = JSONEncoder() let encoder = JSONEncoder()
let data = try! encoder.encode(site) let data = try! encoder.encode(site)
self.eventSink?(String(data: data, encoding: .utf8)) eventSink?(String(data: data, encoding: .utf8))
} }
private func configUpdated() { private func configUpdated() {
if self.site.connected != true { if site.connected != true {
return return
} }
guard let newSite = try? Site(manager: self.site.manager!) else { guard let newSite = try? Site(manager: site.manager!) else {
return return
} }
self.update(connected: newSite.connected ?? false, replaceSite: newSite) update(connected: newSite.connected ?? false, replaceSite: newSite)
} }
} }