mirror of
https://github.com/DefinedNet/mobile_nebula.git
synced 2025-02-15 16:25:26 +00:00
swiftformat .
This commit is contained in:
parent
02e8c5bf35
commit
7c4a1a5faa
9 changed files with 411 additions and 423 deletions
|
@ -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)
|
||||||
|
|
||||||
|
@ -39,31 +39,29 @@ class KeyChain {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import NetworkExtension
|
|
||||||
import MobileNebula
|
import MobileNebula
|
||||||
|
import NetworkExtension
|
||||||
import os.log
|
import os.log
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
|
@ -17,66 +17,65 @@ 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?
|
||||||
|
|
||||||
private var site: Site?
|
private var site: Site?
|
||||||
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
|
private let log = Logger(subsystem: "net.defined.mobileNebula", category: "PacketTunnelProvider")
|
||||||
private var nebula: MobileNebulaNebula?
|
private var nebula: MobileNebulaNebula?
|
||||||
private var dnUpdater = DNUpdater()
|
private var dnUpdater = DNUpdater()
|
||||||
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 {
|
||||||
// startTunnel must complete before IPC will work
|
// startTunnel must complete before IPC will work
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// VPN is being booted out of band of the UI. Use the system completion handler as there will be nothing to route initialization errors to but we still need to report
|
// VPN is being booted out of band of the UI. Use the system completion handler as there will be nothing to route initialization errors to but we still need to report
|
||||||
// success/fail by the presence of an error or nil
|
// success/fail by the presence of an error or nil
|
||||||
try await start()
|
try await start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func start() async throws {
|
private func start() async throws {
|
||||||
var manager: NETunnelProviderManager?
|
var manager: NETunnelProviderManager?
|
||||||
var config: Data
|
var config: Data
|
||||||
var key: String
|
var key: String
|
||||||
|
|
||||||
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(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,43 +104,43 @@ 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
|
||||||
}
|
}
|
||||||
let targetID = targetProviderConfig["id"] as? String
|
let targetID = targetProviderConfig["id"] as? String
|
||||||
|
|
||||||
// Load vpn configs from system, and find the manager matching the one being started
|
// Load vpn configs from system, and find the manager matching the one being started
|
||||||
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
||||||
for manager in managers {
|
for manager in managers {
|
||||||
|
@ -150,32 +149,32 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we didn't find anything, throw an error
|
// If we didn't find anything, throw an error
|
||||||
throw VPNStartError.noManagers
|
throw VPNStartError.noManagers
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pathUpdate(path: Network.NWPath) {
|
private func pathUpdate(path: Network.NWPath) {
|
||||||
let routeDescription = collectAddresses(endpoints: path.gateways)
|
let routeDescription = collectAddresses(endpoints: path.gateways)
|
||||||
if routeDescription != cachedRouteDescription {
|
if routeDescription != cachedRouteDescription {
|
||||||
|
@ -186,94 +185,93 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
|
||||||
cachedRouteDescription = routeDescription
|
cachedRouteDescription = routeDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return str.sorted().joined(separator: ", ")
|
return str.sorted().joined(separator: ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
override func handleAppMessage(_ data: Data) async -> Data? {
|
override func handleAppMessage(_ data: Data) async -> Data? {
|
||||||
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
|
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
|
||||||
log.error("Failed to decode IPCRequest from network extension")
|
log.error("Failed to decode IPCRequest from network extension")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var error: Error?
|
var error: Error?
|
||||||
var data: JSON?
|
var data: JSON?
|
||||||
|
|
||||||
// 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func listHostmap(pending: Bool) -> (JSON?, Error?) {
|
private func listHostmap(pending: Bool) -> (JSON?, Error?) {
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
let res = nebula!.listHostmap(pending, error: &err)
|
let res = nebula!.listHostmap(pending, error: &err)
|
||||||
return (JSON(res), err)
|
return (JSON(res), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func getHostInfo(args: JSON) -> (JSON?, Error?) {
|
private func getHostInfo(args: JSON) -> (JSON?, Error?) {
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
|
let res = nebula!.getHostInfo(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(args["vpnIp"].string, addr: args["addr"].string, error: &err)
|
let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err)
|
||||||
return (JSON(res), err)
|
return (JSON(res), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func closeTunnel(args: JSON) -> (JSON?, Error?) {
|
private func closeTunnel(args: JSON) -> (JSON?, Error?) {
|
||||||
let res = nebula!.closeTunnel(args["vpnIp"].string)
|
let res = nebula!.closeTunnel(args["vpnIp"].string)
|
||||||
return (JSON(res), nil)
|
return (JSON(res), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var tunnelFileDescriptor: Int32? {
|
private var tunnelFileDescriptor: Int32? {
|
||||||
var ctlInfo = ctl_info()
|
var ctlInfo = ctl_info()
|
||||||
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
|
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,107 +2,107 @@ import NetworkExtension
|
||||||
|
|
||||||
class SiteList {
|
class SiteList {
|
||||||
private var sites = [String: Site]()
|
private var sites = [String: Site]()
|
||||||
|
|
||||||
/// 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(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rootDir
|
return rootDir
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists
|
/// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the directory where a single site would live, $rootDir/sites/$siteID
|
/// Gets the directory where a single site would live, $rootDir/sites/$siteID
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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("config", isDirectory: false).appendingPathExtension("json")
|
return try getSiteDir(id: id, create: createDir).appendingPathComponent("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("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]()
|
||||||
|
|
||||||
do {
|
do {
|
||||||
siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil)
|
siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
completion(nil, error)
|
completion(nil, error)
|
||||||
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
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
try? fileManager.removeItem(at: path)
|
try? fileManager.removeItem(at: path)
|
||||||
print("Deleted non conforming site \(path)")
|
print("Deleted non conforming site \(path)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
newManagers?.forEach { manager in
|
newManagers?.forEach { manager in
|
||||||
do {
|
do {
|
||||||
let site = try Site(manager: manager)
|
let site = try Site(manager: manager)
|
||||||
|
@ -112,28 +112,28 @@ class SiteList {
|
||||||
if error != nil {
|
if error != nil {
|
||||||
print("Error while migrating site to fs: \(error!.localizedDescription)")
|
print("Error while migrating site to fs: \(error!.localizedDescription)")
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Migrated site to fs: \(site.name)")
|
print("Migrated site to fs: \(site.name)")
|
||||||
site.needsToMigrateToFS = false
|
site.needsToMigrateToFS = false
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchGroup.notify(queue: .main) {
|
dispatchGroup.notify(queue: .main) {
|
||||||
completion(sites, nil)
|
completion(sites, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSites() -> [String: Site] {
|
func getSites() -> [String: Site] {
|
||||||
return sites
|
return sites
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,17 +7,17 @@ enum APIClientError: Error {
|
||||||
class APIClient {
|
class APIClient {
|
||||||
let apiClient: MobileNebulaAPIClient
|
let apiClient: MobileNebulaAPIClient
|
||||||
let json = JSONDecoder()
|
let json = JSONDecoder()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let packageInfo = PackageInfo()
|
let packageInfo = PackageInfo()
|
||||||
apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
|
apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
|
||||||
}
|
}
|
||||||
|
|
||||||
func enroll(code: String) throws -> IncomingSite {
|
func enroll(code: String) throws -> IncomingSite {
|
||||||
let res = try apiClient.enroll(code)
|
let res = try apiClient.enroll(code)
|
||||||
return try decodeIncomingSite(jsonSite: res.site)
|
return try decodeIncomingSite(jsonSite: res.site)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? {
|
func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? {
|
||||||
let res: MobileNebulaTryUpdateResult
|
let res: MobileNebulaTryUpdateResult
|
||||||
do {
|
do {
|
||||||
|
@ -26,23 +26,24 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func decodeIncomingSite(jsonSite: String) throws -> IncomingSite {
|
private func decodeIncomingSite(jsonSite: String) throws -> IncomingSite {
|
||||||
do {
|
do {
|
||||||
return try json.decode(IncomingSite.self, from: jsonSite.data(using: .utf8)!)
|
return try json.decode(IncomingSite.self, from: jsonSite.data(using: .utf8)!)
|
||||||
|
|
|
@ -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"
|
||||||
|
@ -18,136 +18,130 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
||||||
private let apiClient = APIClient()
|
private let apiClient = APIClient()
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signal to the main screen to reload
|
// Signal to the main screen to reload
|
||||||
self.ui?.invokeMethod("refreshSites", arguments: nil)
|
self.ui?.invokeMethod("refreshSites", arguments: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||||
fatalError("rootViewController is not type FlutterViewController")
|
fatalError("rootViewController is not type FlutterViewController")
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result(json)
|
return result(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result(valid)
|
return result(valid)
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result(kp)
|
return result(kp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func nebulaRenderConfig(call: FlutterMethodCall, result: FlutterResult) {
|
func nebulaRenderConfig(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
|
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result(yaml)
|
return result(yaml)
|
||||||
}
|
}
|
||||||
|
|
||||||
func dnEnroll(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
func dnEnroll(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
guard let code = call.arguments as? String else { return result(NoArgumentsError()) }
|
guard let code = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
result(nil)
|
result(nil)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription))
|
return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,30 +151,30 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
||||||
result(ret)
|
result(ret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
result(nil)
|
result(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
func saveSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
guard let json = call.arguments as? String else { return result(NoArgumentsError()) }
|
guard let json = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
guard let data = json.data(using: .utf8) else { return result(NoArgumentsError()) }
|
guard let data = json.data(using: .utf8) else { return result(NoArgumentsError()) }
|
||||||
|
|
||||||
guard let site = try? JSONDecoder().decode(IncomingSite.self, from: data) else {
|
guard let site = try? JSONDecoder().decode(IncomingSite.self, from: data) else {
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,68 +183,68 @@ func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
|
||||||
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
|
|
||||||
|
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
let updater = self.sites?.getUpdater(id: id)
|
|
||||||
updater?.update(connected: true)
|
|
||||||
#else
|
|
||||||
let container = self.sites?.getContainer(id: id)
|
|
||||||
let manager = container?.site.manager
|
|
||||||
|
|
||||||
manager?.loadFromPreferences{ error in
|
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
//TODO: Handle load error
|
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
|
||||||
// This is silly but we need to enable the site each time to avoid situations where folks have multiple sites
|
guard let id = args["id"] else { return result(MissingArgumentError(message: "id is a required argument")) }
|
||||||
manager?.isEnabled = true
|
|
||||||
manager?.saveToPreferences{ error in
|
#if targetEnvironment(simulator)
|
||||||
//TODO: Handle load error
|
let updater = sites?.getUpdater(id: id)
|
||||||
manager?.loadFromPreferences{ error in
|
updater?.update(connected: true)
|
||||||
//TODO: Handle load error
|
#else
|
||||||
do {
|
let container = sites?.getContainer(id: id)
|
||||||
container?.updater.startFunc = {() -> Void in
|
let manager = container?.site.manager
|
||||||
return self.vpnRequest(command: "start", arguments: args, result: result)
|
|
||||||
|
manager?.loadFromPreferences { error in
|
||||||
|
// TODO: Handle load error
|
||||||
|
// This is silly but we need to enable the site each time to avoid situations where folks have multiple sites
|
||||||
|
manager?.isEnabled = true
|
||||||
|
manager?.saveToPreferences { error in
|
||||||
|
// TODO: Handle load error
|
||||||
|
manager?.loadFromPreferences { error in
|
||||||
|
// TODO: Handle load error
|
||||||
|
do {
|
||||||
|
container?.updater.startFunc = { () in
|
||||||
|
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)
|
||||||
|
|
||||||
if container == nil {
|
if container == nil {
|
||||||
// No site for this id
|
// No site for this id
|
||||||
return result(nil)
|
return result(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(container!.site.connected ?? false) {
|
if !(container!.site.connected ?? false) {
|
||||||
// Site isn't connected, no point in sending a command
|
// Site isn't connected, no point in sending a command
|
||||||
return result(nil)
|
return result(nil)
|
||||||
|
@ -258,27 +252,27 @@ 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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.type == .success {
|
if res.type == .success {
|
||||||
return result(res.message?.object)
|
return result(res.message?.object)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
|
return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,14 @@ class DNUpdater {
|
||||||
private let apiClient = APIClient()
|
private let apiClient = APIClient()
|
||||||
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
|
||||||
}
|
}
|
||||||
|
@ -22,29 +22,29 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
let credentials = try site.getDNCredentials()
|
let credentials = try site.getDNCredentials()
|
||||||
|
|
||||||
let newSite: IncomingSite?
|
let newSite: IncomingSite?
|
||||||
do {
|
do {
|
||||||
newSite = try apiClient.tryUpdate(
|
newSite = try apiClient.tryUpdate(
|
||||||
|
@ -55,31 +55,31 @@ 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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let siteManager = site.manager
|
let siteManager = site.manager
|
||||||
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)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload nebula even if we couldn't save the vpn profile
|
// reload nebula even if we couldn't save the vpn profile
|
||||||
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("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
log.error("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, 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) {
|
||||||
|
|
|
@ -5,11 +5,11 @@ class PackageInfo {
|
||||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ??
|
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ??
|
||||||
"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
|
||||||
}
|
}
|
||||||
|
|
||||||
return "\(version)-\(buildNumber!)"
|
return "\(version)-\(buildNumber!)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ class PackageInfo {
|
||||||
Bundle.main.infoDictionary?["CFBundleName"] as? String ??
|
Bundle.main.infoDictionary?["CFBundleName"] as? String ??
|
||||||
"Nebula"
|
"Nebula"
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSystemVersion() -> String {
|
func getSystemVersion() -> String {
|
||||||
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||||
return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
|
return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import NetworkExtension
|
|
||||||
import MobileNebula
|
import MobileNebula
|
||||||
|
import NetworkExtension
|
||||||
|
|
||||||
class SiteContainer {
|
class SiteContainer {
|
||||||
var site: Site
|
var site: Site
|
||||||
var updater: SiteUpdater
|
var updater: SiteUpdater
|
||||||
|
|
||||||
init(site: Site, updater: SiteUpdater) {
|
init(site: Site, updater: SiteUpdater) {
|
||||||
self.site = site
|
self.site = site
|
||||||
self.updater = updater
|
self.updater = updater
|
||||||
|
@ -14,39 +14,39 @@ class SiteContainer {
|
||||||
class Sites {
|
class Sites {
|
||||||
private var containers = [String: SiteContainer]()
|
private var containers = [String: SiteContainer]()
|
||||||
private var messenger: FlutterBinaryMessenger?
|
private var messenger: FlutterBinaryMessenger?
|
||||||
|
|
||||||
init(messenger: FlutterBinaryMessenger?) {
|
init(messenger: FlutterBinaryMessenger?) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
self.containers[site.id] = SiteContainer(site: site, updater: updater!)
|
self.containers[site.id] = SiteContainer(site: site, updater: updater!)
|
||||||
}
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
let siteDir = try SiteList.getSiteDir(id: site.site.id)
|
let siteDir = try SiteList.getSiteDir(id: site.site.id)
|
||||||
|
@ -54,132 +54,132 @@ class Sites {
|
||||||
} catch {
|
} catch {
|
||||||
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
|
||||||
callback(nil)
|
callback(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)?
|
||||||
private var configFd: Int32? = nil
|
private var configFd: Int32? = nil
|
||||||
private var configObserver: DispatchSourceFileSystemObject? = nil
|
private var configObserver: DispatchSourceFileSystemObject? = nil
|
||||||
|
|
||||||
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)
|
||||||
self.site = site
|
self.site = site
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
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) {
|
||||||
self.site = site
|
self.site = site
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
site.status = connected ? "Connected" : "Disconnected"
|
site.status = connected ? "Connected" : "Disconnected"
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue