Compare commits

..

No commits in common. "f6016f5da81e584dca011508a5e9dac99a98b794" and "4d34083572cdcc53bf100d4c840463a10943b07c" have entirely different histories.

11 changed files with 1503 additions and 1583 deletions

View file

@ -3,6 +3,3 @@
# Another big flutter format run # Another big flutter format run
ed348ab126160e64ba09899c946383ca9e54768c ed348ab126160e64ba09899c946383ca9e54768c
# Start formatting with swift-format
4621cbc0006b3c64c8948d920f69b0dc3f503565

View file

@ -17,5 +17,6 @@ jobs:
with: with:
show-progress: false show-progress: false
- name: Check formating # Re-enable in follow-up PR that does the formatting.
run: ./swift-format.sh check # - name: Check formating
# run: ./swift-format.sh check

View file

@ -5,13 +5,13 @@ 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
} }
@ -22,10 +22,10 @@ class KeyChain {
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,
] ]
@ -42,8 +42,8 @@ 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,
] ]
@ -56,9 +56,7 @@ 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( withUnsafePointer(to: &value, { (ptr: UnsafePointer<T>) -> Void in
to: &value,
{ (ptr: UnsafePointer<T>) -> Void in
data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
}) })
self.init(data) self.init(data)
@ -68,3 +66,4 @@ extension Data {
return self.withUnsafeBytes { $0.load(as: T.self) } return self.withUnsafeBytes { $0.load(as: T.self) }
} }
} }

View file

@ -1,7 +1,7 @@
import MobileNebula
import NetworkExtension import NetworkExtension
import SwiftyJSON import MobileNebula
import os.log import os.log
import SwiftyJSON
enum VPNStartError: Error { enum VPNStartError: Error {
case noManagers case noManagers
@ -23,6 +23,7 @@ extension AppMessageError: LocalizedError {
} }
} }
class PacketTunnelProvider: NEPacketTunnelProvider { class PacketTunnelProvider: NEPacketTunnelProvider {
private var networkMonitor: NWPathMonitor? private var networkMonitor: NWPathMonitor?
@ -33,7 +34,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 {
@ -54,9 +55,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
do { do {
// 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( if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) {
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
{
manager = try await self.findManager() manager = try await self.findManager()
guard let foundManager = manager else { guard let foundManager = manager else {
throw VPNStartError.couldNotFindManager throw VPNStartError.couldNotFindManager
@ -88,19 +87,16 @@ 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( tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR]) var routes: [NEIPv4Route] = [NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)]
var routes: [NEIPv4Route] = [
NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)
]
// 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))
@ -111,8 +107,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
try await self.setTunnelNetworkSettings(tunnelNetworkSettings) try await self.setTunnelNetworkSettings(tunnelNetworkSettings)
var nebulaErr: NSError? var nebulaErr: NSError?
self.nebula = MobileNebulaNewNebula( self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
self.startNetworkMonitor() self.startNetworkMonitor()
if nebulaErr != nil { if nebulaErr != nil {
@ -127,20 +122,18 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func handleDNUpdate(newSite: Site) { private func handleDNUpdate(newSite: Site) {
do { do {
self.site = newSite self.site = newSite
try self.nebula?.reload( try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
} catch { } catch {
log.error( log.error("Got an error while updating nebula \(error.localizedDescription, privacy: .public)")
"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 = self.protocolConfiguration as? NETunnelProviderProtocol
@ -157,7 +150,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
} }
} }
@ -177,9 +170,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
networkMonitor = nil networkMonitor = nil
} }
override func stopTunnel( override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
with reason: NEProviderStopReason, completionHandler: @escaping () -> Void
) {
nebula?.stop() nebula?.stop()
stopNetworkMonitor() stopNetworkMonitor()
completionHandler() completionHandler()
@ -190,8 +181,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
if routeDescription != cachedRouteDescription { if routeDescription != cachedRouteDescription {
// Don't bother to rebind if we don't have any gateways // Don't bother to rebind if we don't have any gateways
if routeDescription != "" { if routeDescription != "" {
nebula?.rebind( nebula?.rebind("network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
"network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
} }
cachedRouteDescription = routeDescription cachedRouteDescription = routeDescription
} }
@ -199,7 +189,7 @@ 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 endpoints.forEach{ endpoint in
switch endpoint { switch endpoint {
case let .hostPort(.ipv6(host), port): case let .hostPort(.ipv6(host), port):
str.append("[\(host)]:\(port)") str.append("[\(host)]:\(port)")
@ -232,8 +222,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
defer { defer {
self.cancelTunnelWithError(error) self.cancelTunnelWithError(error)
} }
return try? JSONEncoder().encode( return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
} }
} }
@ -255,10 +244,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
error = AppMessageError.unknownIPCType(command: call.command) error = AppMessageError.unknownIPCType(command: call.command)
} }
if error != nil { if (error != nil) {
return try? JSONEncoder().encode( return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
IPCResponse.init(
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.init(type: .success, message: data))
} }
@ -272,15 +259,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
private func getHostInfo(args: JSON) -> (JSON?, Error?) { private func getHostInfo(args: JSON) -> (JSON?, Error?) {
var err: NSError? var err: NSError?
let res = nebula!.getHostInfo( let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
return (JSON(res), err) return (JSON(res), err)
} }
private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) { private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) {
var err: NSError? var err: NSError?
let res = nebula!.setRemoteForTunnel( let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err)
args["vpnIp"].string, addr: args["addr"].string, error: &err)
return (JSON(res), err) return (JSON(res), err)
} }
@ -321,3 +306,4 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return nil return nil
} }
} }

View file

@ -1,12 +1,12 @@
import MobileNebula
import NetworkExtension import NetworkExtension
import MobileNebula
import SwiftyJSON import SwiftyJSON
import os.log import os.log
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
@ -131,7 +131,7 @@ struct CertificateValidity: Codable {
} }
} }
let statusMap: [NEVPNStatus: Bool] = [ let statusMap: Dictionary<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: [NEVPNStatus: Bool] = [
NEVPNStatus.disconnecting: true, NEVPNStatus.disconnecting: true,
] ]
let statusString: [NEVPNStatus: String] = [ let statusString: Dictionary<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: [String: StaticHosts] var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute] var unsafeRoutes: [UnsafeRoute]
var cert: CertificateInfo? var cert: CertificateInfo?
var ca: [CertificateInfo] var ca: [CertificateInfo]
@ -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 ca.forEach { cert in
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")
} }
@ -294,11 +294,11 @@ class Site: Codable {
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 {
@ -327,7 +327,7 @@ class Site: Codable {
} }
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: self.id))) {
throw SiteError.dnCredentialLoad throw SiteError.dnCredentialLoad
} }
} }
@ -353,7 +353,7 @@ 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: self.id))) {
throw SiteError.dnCredentialSave throw SiteError.dnCredentialSave
} }
} }
@ -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: [String: StaticHosts] var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]? var unsafeRoutes: [UnsafeRoute]?
var cert: String? var cert: String?
var ca: String? var ca: String?
@ -456,10 +456,7 @@ struct IncomingSite: Codable {
return try encoder.encode(config) return try encoder.encode(config)
} }
func save( func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) {
manager: NETunnelProviderManager?, saveToManager: Bool = true,
callback: @escaping (Error?) -> Void
) {
let configPath: URL let configPath: URL
do { do {
@ -472,15 +469,15 @@ struct IncomingSite: Codable {
log.notice("Saving to \(configPath, privacy: .public)") log.notice("Saving to \(configPath, privacy: .public)")
do { do {
if self.key != nil { if (self.key != nil) {
let data = self.key!.data(using: .utf8) let data = self.key!.data(using: .utf8)
if !KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false) { if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) {
return callback(SiteError.keySave) return callback(SiteError.keySave)
} }
} }
do { do {
if (try self.dnCredentials?.save(siteID: self.id)) == false { if ((try self.dnCredentials?.save(siteID: self.id)) == false) {
return callback(SiteError.dnCredentialSave) return callback(SiteError.dnCredentialSave)
} }
} catch { } catch {
@ -493,25 +490,24 @@ struct IncomingSite: Codable {
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) callback(nil)
#else #else
if saveToManager { if saveToManager {
self.saveToManager(manager: manager, callback: callback) self.saveToManager(manager: manager, callback: callback)
} else { } else {
callback(nil) callback(nil)
} }
#endif #endif
} }
private func saveToManager( private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) {
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)
} }
@ -523,13 +519,10 @@ struct IncomingSite: Codable {
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback) return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
} }
private func finishSaveToManager( private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) {
manager: NETunnelProviderManager, callback: @escaping (Error?) -> Void
) {
// Stuff our details in the protocol // Stuff our details in the protocol
let proto = let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
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": self.id]
@ -543,7 +536,7 @@ struct IncomingSite: Codable {
manager.localizedDescription = self.name manager.localizedDescription = self.name
manager.isEnabled = true manager.isEnabled = true
manager.saveToPreferences { error in manager.saveToPreferences{ error in
return callback(error) return callback(error)
} }
} }

View file

@ -6,10 +6,9 @@ class SiteList {
/// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists /// Gets the root directory that can be used to share files between the UI and VPN process. Does ensure the directory exists
static func getRootDir() throws -> URL { static func getRootDir() throws -> URL {
let fileManager = FileManager.default let fileManager = FileManager.default
let rootDir = fileManager.containerURL( let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
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)
} }
@ -20,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
@ -30,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
@ -38,43 +37,39 @@ class SiteList {
/// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json /// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json
static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL { static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL {
return try getSiteDir(id: id, create: createDir).appendingPathComponent( return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false).appendingPathExtension("json")
"config", isDirectory: false
).appendingPathExtension("json")
} }
/// Gets the file that represents the site log output, $rootDir/sites/$siteID/log /// Gets the file that represents the site log output, $rootDir/sites/$siteID/log
static func getSiteLogFile(id: String, createDir: Bool) throws -> URL { static func getSiteLogFile(id: String, createDir: Bool) throws -> URL {
return try getSiteDir(id: id, create: createDir).appendingPathComponent( return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false)
"logs", isDirectory: false)
} }
init(completion: @escaping ([String: Site]?, Error?) -> Void) { init(completion: @escaping ([String: Site]?, Error?) -> ()) {
#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 #else
SiteList.loadAllFromNETPM { sites, err in SiteList.loadAllFromNETPM { sites, err in
if sites != nil { if sites != nil {
self.sites = sites! self.sites = sites!
} }
completion(sites, err) completion(sites, err)
} }
#endif #endif
} }
private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> Void) { private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> ()) {
let fileManager = FileManager.default let fileManager = FileManager.default
var siteDirs: [URL] var siteDirs: [URL]
var sites = [String: Site]() var sites = [String: Site]()
do { do {
siteDirs = try fileManager.contentsOfDirectory( siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil)
at: getSitesDir(), includingPropertiesForKeys: nil)
} catch { } catch {
completion(nil, error) completion(nil, error)
@ -83,8 +78,7 @@ class SiteList {
siteDirs.forEach { path in siteDirs.forEach { path in
do { do {
let site = try Site( let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json"))
path: path.appendingPathComponent("config").appendingPathExtension("json"))
sites[site.id] = site sites[site.id] = site
} catch { } catch {
@ -97,15 +91,15 @@ class SiteList {
completion(sites, nil) completion(sites, nil)
} }
private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> Void) { private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> ()) {
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)
} }

View file

@ -10,8 +10,7 @@ class APIClient {
init() { init() {
let packageInfo = PackageInfo() let packageInfo = PackageInfo()
apiClient = MobileNebulaNewAPIClient( apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
"MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
} }
func enroll(code: String) throws -> IncomingSite { func enroll(code: String) throws -> IncomingSite {
@ -19,9 +18,7 @@ class APIClient {
return try decodeIncomingSite(jsonSite: res.site) return try decodeIncomingSite(jsonSite: res.site)
} }
func tryUpdate( func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? {
siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String
) throws -> IncomingSite? {
let res: MobileNebulaTryUpdateResult let res: MobileNebulaTryUpdateResult
do { do {
res = try apiClient.tryUpdate( res = try apiClient.tryUpdate(
@ -32,14 +29,14 @@ class APIClient {
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,16 +19,18 @@ 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)
@ -45,7 +47,7 @@ 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) -> Void 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)
@ -60,16 +62,11 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
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": case "active.listHostmap": self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result)
self.vpnRequest(command: "listHostmap", arguments: call.arguments, result: result) case "active.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result)
case "active.listPendingHostmap": case "active.getHostInfo": self.vpnRequest(command: "getHostInfo", arguments: call.arguments, result: result)
self.vpnRequest(command: "listPendingHostmap", arguments: call.arguments, result: result) case "active.setRemoteForTunnel": self.vpnRequest(command: "setRemoteForTunnel", arguments: call.arguments, result: result)
case "active.getHostInfo": case "active.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
self.vpnRequest(command: "getHostInfo", 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)
default: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
@ -80,39 +77,28 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
} }
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) { func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) } guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let certs = args["certs"] else { guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) }
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( return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription))
CallFailedError(
message: "Error while parsing certificate(s)", details: err!.localizedDescription))
} }
return result(json) return result(json)
} }
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) { func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) } guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let cert = args["cert"] else { guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) }
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( return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription))
CallFailedError(
message: "Error while verifying certificate and private key",
details: err!.localizedDescription))
} }
return result(valid) return result(valid)
@ -121,10 +107,8 @@ 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( return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription))
CallFailedError(
message: "Error while generating key pairs", details: err!.localizedDescription))
} }
return result(kp) return result(kp)
@ -135,10 +119,8 @@ 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( return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)
)
} }
return result(yaml) return result(yaml)
@ -152,24 +134,21 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
let oldSite = self.sites?.getSite(id: site.id) let oldSite = self.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( return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
} }
result(nil) result(nil)
} }
} catch { } catch {
return result( return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription))
CallFailedError(message: "Error from DN api", details: error.localizedDescription))
} }
} }
func listSites(result: @escaping FlutterResult) { func listSites(result: @escaping FlutterResult) {
self.sites?.loadSites { (sites, err) -> Void in self.sites?.loadSites { (sites, err) -> () in
if err != nil { if (err != nil) {
return result( return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
} }
let encoder = JSONEncoder() let encoder = JSONEncoder()
@ -183,9 +162,8 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
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 self.sites?.deleteSite(id: id) { error in
if error != nil { if (error != nil) {
result( result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
} }
result(nil) result(nil)
@ -202,9 +180,8 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
let oldSite = self.sites?.getSite(id: site.id) let oldSite = self.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( return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
} }
self.sites?.loadSites { _, _ in self.sites?.loadSites { _, _ in
@ -214,67 +191,59 @@ 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? [String: String] else { return result(NoArgumentsError()) } guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
return result(MissingArgumentError(message: "id is a required argument"))
}
#if targetEnvironment(simulator) #if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id) let updater = self.sites?.getUpdater(id: id)
updater?.update(connected: true) updater?.update(connected: true)
#else #else
let container = self.sites?.getContainer(id: id) let container = self.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 = {() -> Void in
return self.vpnRequest(command: "start", arguments: args, result: result) return self.vpnRequest(command: "start", arguments: args, result: result)
} }
try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)]) try manager?.connection.startVPNTunnel(options: ["expectStart": NSNumber(1)])
} catch { } catch {
return result( return result(CallFailedError(message: "Could not start site", details: error.localizedDescription))
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? [String: String] else { return result(NoArgumentsError()) } guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
guard let id = args["id"] else { guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
return result(MissingArgumentError(message: "id is a required argument")) #if targetEnvironment(simulator)
}
#if targetEnvironment(simulator)
let updater = self.sites?.getUpdater(id: id) let updater = self.sites?.getUpdater(id: id)
updater?.update(connected: false) updater?.update(connected: false)
#else #else
let manager = self.sites?.getSite(id: id)?.manager let manager = self.sites?.getSite(id: id)?.manager
manager?.loadFromPreferences { error in manager?.loadFromPreferences{ error 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? [String: Any] else { return result(NoArgumentsError()) } guard let args = arguments as? Dictionary<String, Any> else { return result(NoArgumentsError()) }
guard let id = args["id"] as? String else { guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) }
return result(MissingArgumentError(message: "id is a required argument"))
}
let container = sites?.getContainer(id: id) let container = sites?.getContainer(id: id)
if container == nil { if container == nil {
@ -289,9 +258,7 @@ 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 session.sendProviderMessage(try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in
try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))
) { data in
if data == nil { if data == nil {
return result(nil) return result(nil)
} }
@ -305,8 +272,7 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
return result(res.message?.object) return result(res.message?.object)
} }
return result( return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
} }
} catch { } catch {
return result(CallFailedError(message: error.localizedDescription)) return result(CallFailedError(message: error.localizedDescription))
@ -322,10 +288,7 @@ func MissingArgumentError(message: String, details: Error? = nil) -> FlutterErro
return FlutterError(code: "missingArgument", message: message, details: details) return FlutterError(code: "missingArgument", message: message, details: details)
} }
func NoArgumentsError( func NoArgumentsError(message: String? = "no arguments were provided or could not be deserialized", details: Error? = nil) -> FlutterError {
message: String? = "no arguments were provided or could not be deserialized",
details: Error? = nil
) -> FlutterError {
return FlutterError(code: "noArguments", message: message, details: details) return FlutterError(code: "noArguments", message: message, details: details)
} }

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) -> Void) { func updateAll(onUpdate: @escaping (Site) -> ()) {
_ = SiteList { (sites, _) -> Void 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) -> Void) { func updateAllLoop(onUpdate: @escaping (Site) -> ()) {
timer.eventHandler = { timer.eventHandler = {
self.updateAll(onUpdate: onUpdate) self.updateAll(onUpdate: onUpdate)
} }
timer.resume() timer.resume()
} }
func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> Void) { func updateSingleLoop(site: Site, onUpdate: @escaping (Site) -> ()) {
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) -> Void) { func updateSite(site: Site, onUpdate: @escaping (Site) -> ()) {
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)")
} }
@ -64,13 +64,10 @@ class DNUpdater {
} }
let siteManager = site.manager let siteManager = site.manager
let shouldSaveToManager = let shouldSaveToManager = siteManager != nil || ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
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)")
} }
@ -78,15 +75,13 @@ 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)")
} }
} catch { } catch {
log.error( log.error("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)")
"Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)"
)
} }
} }
} }

View file

@ -2,10 +2,11 @@ import Foundation
class PackageInfo { class PackageInfo {
func getVersion() -> String { func getVersion() -> String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ??
"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
} }
@ -13,8 +14,9 @@ class PackageInfo {
} }
func getName() -> String { func getName() -> String {
return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle.main return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ??
.infoDictionary?["CFBundleName"] as? String ?? "Nebula" Bundle.main.infoDictionary?["CFBundleName"] as? String ??
"Nebula"
} }
func getSystemVersion() -> String { func getSystemVersion() -> String {

View file

@ -1,5 +1,5 @@
import MobileNebula
import NetworkExtension import NetworkExtension
import MobileNebula
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?) -> Void) { func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) {
_ = 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)
@ -42,7 +42,7 @@ class Sites {
} }
} }
func deleteSite(id: String, callback: @escaping (Error?) -> Void) { func deleteSite(id: String, callback: @escaping (Error?) -> ()) {
if let site = self.containers.removeValue(forKey: id) { if let site = self.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
@ -79,8 +79,8 @@ class Sites {
} }
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)?
@ -101,8 +101,7 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
self.configObserver = nil self.configObserver = nil
} }
eventChannel = FlutterEventChannel( eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
self.site = site self.site = site
super.init() super.init()
@ -124,23 +123,17 @@ 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) func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
-> 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( return FlutterError(code: "Internal Error", message: "Flutter manager was not present", details: nil)
code: "Internal Error", message: "Flutter manager was not present", details: nil)
} }
self.notification = NotificationCenter.default.addObserver( self.notification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection , queue: nil) { n in
forName: NSNotification.Name.NEVPNStatusDidChange, object: site.manager!.connection,
queue: nil
) { n in
let oldConnected = self.site.connected let oldConnected = self.site.connected
self.site.status = statusString[self.site.manager!.connection.status] self.site.status = statusString[self.site.manager!.connection.status]
self.site.connected = statusMap[self.site.manager!.connection.status] self.site.connected = statusMap[self.site.manager!.connection.status]
@ -153,13 +146,13 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
self.update(connected: self.site.connected!) self.update(connected: self.site.connected!)
} }
#endif #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 arguments: Any?) -> FlutterError? {
if self.notification != nil { if (self.notification != nil) {
NotificationCenter.default.removeObserver(self.notification!) NotificationCenter.default.removeObserver(self.notification!)
} }
return nil return nil
@ -167,7 +160,7 @@ class SiteUpdater: NSObject, FlutterStreamHandler {
/// 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