mirror of
https://github.com/DefinedNet/mobile_nebula.git
synced 2025-09-07 19:46:06 +00:00
Compare commits
No commits in common. "f6016f5da81e584dca011508a5e9dac99a98b794" and "4d34083572cdcc53bf100d4c840463a10943b07c" have entirely different histories.
f6016f5da8
...
4d34083572
11 changed files with 1503 additions and 1583 deletions
|
@ -2,7 +2,4 @@
|
||||||
9934f226e3e79c3567ce07dbab9e9f6443e7afc5
|
9934f226e3e79c3567ce07dbab9e9f6443e7afc5
|
||||||
|
|
||||||
# Another big flutter format run
|
# Another big flutter format run
|
||||||
ed348ab126160e64ba09899c946383ca9e54768c
|
ed348ab126160e64ba09899c946383ca9e54768c
|
||||||
|
|
||||||
# Start formatting with swift-format
|
|
||||||
4621cbc0006b3c64c8948d920f69b0dc3f503565
|
|
5
.github/workflows/swiftfmt.yml
vendored
5
.github/workflows/swiftfmt.yml
vendored
|
@ -17,5 +17,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
|
|
||||||
- name: Check formating
|
# Re-enable in follow-up PR that does the formatting.
|
||||||
run: ./swift-format.sh check
|
# - name: Check formating
|
||||||
|
# run: ./swift-format.sh check
|
||||||
|
|
|
@ -3,68 +3,67 @@ import Foundation
|
||||||
let groupName = "group.net.defined.mobileNebula"
|
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) {
|
||||||
|
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
||||||
|
}
|
||||||
|
|
||||||
if managed {
|
// Attempt to delete an existing key to allow for an overwrite
|
||||||
query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
|
_ = self.delete(key: key)
|
||||||
|
return SecItemAdd(query as CFDictionary, nil) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to delete an existing key to allow for an overwrite
|
class func load(key: String) -> Data? {
|
||||||
_ = self.delete(key: key)
|
let query: [String: Any] = [
|
||||||
return SecItemAdd(query as CFDictionary, nil) == 0
|
kSecClass as String : kSecClassGenericPassword,
|
||||||
}
|
kSecAttrAccount as String : key,
|
||||||
|
kSecReturnData as String : kCFBooleanTrue!,
|
||||||
|
kSecMatchLimit as String : kSecMatchLimitOne,
|
||||||
|
kSecAttrAccessGroup as String: groupName,
|
||||||
|
]
|
||||||
|
|
||||||
class func load(key: String) -> Data? {
|
var dataTypeRef: AnyObject? = nil
|
||||||
let query: [String: Any] = [
|
|
||||||
kSecClass as String: kSecClassGenericPassword,
|
|
||||||
kSecAttrAccount as String: key,
|
|
||||||
kSecReturnData as String: kCFBooleanTrue!,
|
|
||||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
|
||||||
kSecAttrAccessGroup as String: groupName,
|
|
||||||
]
|
|
||||||
|
|
||||||
var dataTypeRef: AnyObject? = nil
|
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
||||||
|
|
||||||
let status: OSStatus = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
if status == noErr {
|
||||||
|
return dataTypeRef as! Data?
|
||||||
if status == noErr {
|
} else {
|
||||||
return dataTypeRef as! Data?
|
return nil
|
||||||
} else {
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
class func delete(key: String) -> Bool {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String : kSecClassGenericPassword as String,
|
||||||
|
kSecAttrAccount as String : key,
|
||||||
|
kSecAttrAccessGroup as String: groupName,
|
||||||
|
]
|
||||||
|
|
||||||
class func delete(key: String) -> Bool {
|
return SecItemDelete(query as CFDictionary) == 0
|
||||||
let query: [String: Any] = [
|
}
|
||||||
kSecClass as String: kSecClassGenericPassword as String,
|
|
||||||
kSecAttrAccount as String: key,
|
|
||||||
kSecAttrAccessGroup as String: groupName,
|
|
||||||
]
|
|
||||||
|
|
||||||
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(
|
withUnsafePointer(to: &value, { (ptr: UnsafePointer<T>) -> Void in
|
||||||
to: &value,
|
data = Data(buffer: UnsafeBufferPointer(start: ptr, count: 1))
|
||||||
{ (ptr: UnsafePointer<T>) -> Void in
|
})
|
||||||
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 self.withUnsafeBytes { $0.load(as: T.self) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,323 +1,309 @@
|
||||||
import MobileNebula
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
import SwiftyJSON
|
import MobileNebula
|
||||||
import os.log
|
import os.log
|
||||||
|
import SwiftyJSON
|
||||||
|
|
||||||
enum VPNStartError: Error {
|
enum VPNStartError: Error {
|
||||||
case noManagers
|
case noManagers
|
||||||
case couldNotFindManager
|
case couldNotFindManager
|
||||||
case noTunFileDescriptor
|
case noTunFileDescriptor
|
||||||
case noProviderConfig
|
case noProviderConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AppMessageError: Error {
|
enum AppMessageError: Error {
|
||||||
case unknownIPCType(command: String)
|
case unknownIPCType(command: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppMessageError: LocalizedError {
|
extension AppMessageError: LocalizedError {
|
||||||
public var description: String? {
|
public var description: String? {
|
||||||
switch self {
|
switch self {
|
||||||
case .unknownIPCType(let command):
|
case .unknownIPCType(let 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
|
|
||||||
// success/fail by the presence of an error or nil
|
|
||||||
try await start()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func start() async throws {
|
|
||||||
var manager: NETunnelProviderManager?
|
|
||||||
var config: Data
|
|
||||||
var key: String
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
|
|
||||||
// TODO: Remove else once we drop support for iOS 16
|
|
||||||
if ProcessInfo().isOperatingSystemAtLeast(
|
|
||||||
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
|
|
||||||
{
|
|
||||||
manager = try await self.findManager()
|
|
||||||
guard let foundManager = manager else {
|
|
||||||
throw VPNStartError.couldNotFindManager
|
|
||||||
}
|
}
|
||||||
self.site = try Site(manager: foundManager)
|
|
||||||
} else {
|
// 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
|
||||||
// This does not save the manager with the site, which means we cannot update the
|
// success/fail by the presence of an error or nil
|
||||||
// vpn profile name when updates happen (rare).
|
try await start()
|
||||||
self.site = try Site(proto: self.protocolConfiguration as! NETunnelProviderProtocol)
|
|
||||||
}
|
|
||||||
config = try self.site!.getConfig()
|
|
||||||
} catch {
|
|
||||||
//TODO: need a way to notify the app
|
|
||||||
self.log.error("Failed to render config from vpn object")
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let _site = self.site!
|
private func start() async throws {
|
||||||
key = try _site.getKey()
|
var manager: NETunnelProviderManager?
|
||||||
|
var config: Data
|
||||||
guard let fileDescriptor = self.tunnelFileDescriptor else {
|
var key: String
|
||||||
throw VPNStartError.noTunFileDescriptor
|
|
||||||
}
|
do {
|
||||||
let tunFD = Int(fileDescriptor)
|
// Cannot use NETunnelProviderManager.loadAllFromPreferences() in earlier versions of iOS
|
||||||
|
// TODO: Remove else once we drop support for iOS 16
|
||||||
// This is set to 127.0.0.1 because it has to be something..
|
if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0)) {
|
||||||
let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
manager = try await self.findManager()
|
||||||
|
guard let foundManager = manager else {
|
||||||
// Make sure our ip is routed to the tun device
|
throw VPNStartError.couldNotFindManager
|
||||||
var err: NSError?
|
}
|
||||||
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
|
self.site = try Site(manager: foundManager)
|
||||||
if err != nil {
|
} else {
|
||||||
throw err!
|
// This does not save the manager with the site, which means we cannot update the
|
||||||
}
|
// vpn profile name when updates happen (rare).
|
||||||
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(
|
self.site = try Site(proto: self.protocolConfiguration as! NETunnelProviderProtocol)
|
||||||
addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
|
}
|
||||||
var routes: [NEIPv4Route] = [
|
config = try self.site!.getConfig()
|
||||||
NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)
|
} catch {
|
||||||
]
|
//TODO: need a way to notify the app
|
||||||
|
self.log.error("Failed to render config from vpn object")
|
||||||
// Add our unsafe routes
|
throw error
|
||||||
try _site.unsafeRoutes.forEach { unsafeRoute in
|
|
||||||
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
|
|
||||||
if err != nil {
|
|
||||||
throw err!
|
|
||||||
}
|
|
||||||
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
|
|
||||||
}
|
|
||||||
|
|
||||||
tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes
|
|
||||||
tunnelNetworkSettings.mtu = _site.mtu as NSNumber
|
|
||||||
|
|
||||||
try await self.setTunnelNetworkSettings(tunnelNetworkSettings)
|
|
||||||
var nebulaErr: NSError?
|
|
||||||
self.nebula = MobileNebulaNewNebula(
|
|
||||||
String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
|
|
||||||
self.startNetworkMonitor()
|
|
||||||
|
|
||||||
if nebulaErr != nil {
|
|
||||||
self.log.error("We had an error starting up: \(nebulaErr, privacy: .public)")
|
|
||||||
throw nebulaErr!
|
|
||||||
}
|
|
||||||
|
|
||||||
self.nebula!.start()
|
|
||||||
self.dnUpdater.updateSingleLoop(site: self.site!, onUpdate: self.handleDNUpdate)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleDNUpdate(newSite: Site) {
|
|
||||||
do {
|
|
||||||
self.site = newSite
|
|
||||||
try self.nebula?.reload(
|
|
||||||
String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
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
|
|
||||||
// override func sleep(completionHandler: @escaping () -> Void) {
|
|
||||||
// nebula!.sleep()
|
|
||||||
// completionHandler()
|
|
||||||
// }
|
|
||||||
|
|
||||||
private func findManager() async throws -> NETunnelProviderManager {
|
|
||||||
let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol
|
|
||||||
guard let targetProviderConfig = targetProtoConfig?.providerConfiguration else {
|
|
||||||
throw VPNStartError.noProviderConfig
|
|
||||||
}
|
|
||||||
let targetID = targetProviderConfig["id"] as? String
|
|
||||||
|
|
||||||
// Load vpn configs from system, and find the manager matching the one being started
|
|
||||||
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
|
||||||
for manager in managers {
|
|
||||||
let mgrProtoConfig = manager.protocolConfiguration as? NETunnelProviderProtocol
|
|
||||||
guard let mgrProviderConfig = mgrProtoConfig?.providerConfiguration else {
|
|
||||||
throw VPNStartError.noProviderConfig
|
|
||||||
}
|
|
||||||
let id = mgrProviderConfig["id"] as? String
|
|
||||||
if id == targetID {
|
|
||||||
return manager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we didn't find anything, throw an error
|
|
||||||
throw VPNStartError.noManagers
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startNetworkMonitor() {
|
|
||||||
networkMonitor = NWPathMonitor()
|
|
||||||
networkMonitor!.pathUpdateHandler = self.pathUpdate
|
|
||||||
networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stopNetworkMonitor() {
|
|
||||||
self.networkMonitor?.cancel()
|
|
||||||
networkMonitor = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
override func stopTunnel(
|
|
||||||
with reason: NEProviderStopReason, completionHandler: @escaping () -> Void
|
|
||||||
) {
|
|
||||||
nebula?.stop()
|
|
||||||
stopNetworkMonitor()
|
|
||||||
completionHandler()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func pathUpdate(path: Network.NWPath) {
|
|
||||||
let routeDescription = collectAddresses(endpoints: path.gateways)
|
|
||||||
if routeDescription != cachedRouteDescription {
|
|
||||||
// Don't bother to rebind if we don't have any gateways
|
|
||||||
if routeDescription != "" {
|
|
||||||
nebula?.rebind(
|
|
||||||
"network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
|
|
||||||
}
|
|
||||||
cachedRouteDescription = routeDescription
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
|
|
||||||
var str: [String] = []
|
|
||||||
endpoints.forEach { endpoint in
|
|
||||||
switch endpoint {
|
|
||||||
case let .hostPort(.ipv6(host), port):
|
|
||||||
str.append("[\(host)]:\(port)")
|
|
||||||
case let .hostPort(.ipv4(host), port):
|
|
||||||
str.append("\(host):\(port)")
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.sorted().joined(separator: ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func handleAppMessage(_ data: Data) async -> Data? {
|
|
||||||
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
|
|
||||||
log.error("Failed to decode IPCRequest from network extension")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var error: Error?
|
|
||||||
var data: JSON?
|
|
||||||
|
|
||||||
// start command has special treatment due to needing to call two completers
|
|
||||||
if call.command == "start" {
|
|
||||||
do {
|
|
||||||
try await self.start()
|
|
||||||
// No response data, this is expected on a clean start
|
|
||||||
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil))
|
|
||||||
} catch {
|
|
||||||
defer {
|
|
||||||
self.cancelTunnelWithError(error)
|
|
||||||
}
|
}
|
||||||
return try? JSONEncoder().encode(
|
|
||||||
IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if nebula == nil {
|
let _site = self.site!
|
||||||
// Respond with an empty success message in the event a command comes in before we've truly started
|
key = try _site.getKey()
|
||||||
log.warning("Received command but do not have a nebula instance")
|
|
||||||
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil))
|
guard let fileDescriptor = self.tunnelFileDescriptor else {
|
||||||
}
|
throw VPNStartError.noTunFileDescriptor
|
||||||
|
|
||||||
//TODO: try catch over all this
|
|
||||||
switch call.command {
|
|
||||||
case "listHostmap": (data, error) = listHostmap(pending: false)
|
|
||||||
case "listPendingHostmap": (data, error) = listHostmap(pending: true)
|
|
||||||
case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!)
|
|
||||||
case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!)
|
|
||||||
case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!)
|
|
||||||
|
|
||||||
default:
|
|
||||||
error = AppMessageError.unknownIPCType(command: call.command)
|
|
||||||
}
|
|
||||||
|
|
||||||
if error != nil {
|
|
||||||
return try? JSONEncoder().encode(
|
|
||||||
IPCResponse.init(
|
|
||||||
type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
|
|
||||||
} else {
|
|
||||||
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func listHostmap(pending: Bool) -> (JSON?, Error?) {
|
|
||||||
var err: NSError?
|
|
||||||
let res = nebula!.listHostmap(pending, error: &err)
|
|
||||||
return (JSON(res), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getHostInfo(args: JSON) -> (JSON?, Error?) {
|
|
||||||
var err: NSError?
|
|
||||||
let res = nebula!.getHostInfo(
|
|
||||||
byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
|
|
||||||
return (JSON(res), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) {
|
|
||||||
var err: NSError?
|
|
||||||
let res = nebula!.setRemoteForTunnel(
|
|
||||||
args["vpnIp"].string, addr: args["addr"].string, error: &err)
|
|
||||||
return (JSON(res), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func closeTunnel(args: JSON) -> (JSON?, Error?) {
|
|
||||||
let res = nebula!.closeTunnel(args["vpnIp"].string)
|
|
||||||
return (JSON(res), nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var tunnelFileDescriptor: Int32? {
|
|
||||||
var ctlInfo = ctl_info()
|
|
||||||
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
|
|
||||||
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
|
|
||||||
_ = strcpy($0, "com.apple.net.utun_control")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for fd: Int32 in 0...1024 {
|
|
||||||
var addr = sockaddr_ctl()
|
|
||||||
var ret: Int32 = -1
|
|
||||||
var len = socklen_t(MemoryLayout.size(ofValue: addr))
|
|
||||||
withUnsafeMutablePointer(to: &addr) {
|
|
||||||
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
||||||
ret = getpeername(fd, $0, &len)
|
|
||||||
}
|
}
|
||||||
}
|
let tunFD = Int(fileDescriptor)
|
||||||
if ret != 0 || addr.sc_family != AF_SYSTEM {
|
|
||||||
continue
|
// This is set to 127.0.0.1 because it has to be something..
|
||||||
}
|
let tunnelNetworkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1")
|
||||||
if ctlInfo.ctl_id == 0 {
|
|
||||||
ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
|
// Make sure our ip is routed to the tun device
|
||||||
if ret != 0 {
|
var err: NSError?
|
||||||
continue
|
let ipNet = MobileNebulaParseCIDR(_site.cert!.cert.details.ips[0], &err)
|
||||||
|
if (err != nil) {
|
||||||
|
throw err!
|
||||||
}
|
}
|
||||||
}
|
tunnelNetworkSettings.ipv4Settings = NEIPv4Settings(addresses: [ipNet!.ip], subnetMasks: [ipNet!.maskCIDR])
|
||||||
if addr.sc_id == ctlInfo.ctl_id {
|
var routes: [NEIPv4Route] = [NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR)]
|
||||||
return fd
|
|
||||||
}
|
// Add our unsafe routes
|
||||||
|
try _site.unsafeRoutes.forEach { unsafeRoute in
|
||||||
|
let ipNet = MobileNebulaParseCIDR(unsafeRoute.route, &err)
|
||||||
|
if (err != nil) {
|
||||||
|
throw err!
|
||||||
|
}
|
||||||
|
routes.append(NEIPv4Route(destinationAddress: ipNet!.network, subnetMask: ipNet!.maskCIDR))
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelNetworkSettings.ipv4Settings!.includedRoutes = routes
|
||||||
|
tunnelNetworkSettings.mtu = _site.mtu as NSNumber
|
||||||
|
|
||||||
|
try await self.setTunnelNetworkSettings(tunnelNetworkSettings)
|
||||||
|
var nebulaErr: NSError?
|
||||||
|
self.nebula = MobileNebulaNewNebula(String(data: config, encoding: .utf8), key, self.site!.logFile, tunFD, &nebulaErr)
|
||||||
|
self.startNetworkMonitor()
|
||||||
|
|
||||||
|
if nebulaErr != nil {
|
||||||
|
self.log.error("We had an error starting up: \(nebulaErr, privacy: .public)")
|
||||||
|
throw nebulaErr!
|
||||||
|
}
|
||||||
|
|
||||||
|
self.nebula!.start()
|
||||||
|
self.dnUpdater.updateSingleLoop(site: self.site!, onUpdate: self.handleDNUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleDNUpdate(newSite: Site) {
|
||||||
|
do {
|
||||||
|
self.site = newSite
|
||||||
|
try self.nebula?.reload(String(data: newSite.getConfig(), encoding: .utf8), key: newSite.getKey())
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
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
|
||||||
|
// override func sleep(completionHandler: @escaping () -> Void) {
|
||||||
|
// nebula!.sleep()
|
||||||
|
// completionHandler()
|
||||||
|
// }
|
||||||
|
|
||||||
|
private func findManager() async throws -> NETunnelProviderManager {
|
||||||
|
let targetProtoConfig = self.protocolConfiguration as? NETunnelProviderProtocol
|
||||||
|
guard let targetProviderConfig = targetProtoConfig?.providerConfiguration else {
|
||||||
|
throw VPNStartError.noProviderConfig
|
||||||
|
}
|
||||||
|
let targetID = targetProviderConfig["id"] as? String
|
||||||
|
|
||||||
|
// Load vpn configs from system, and find the manager matching the one being started
|
||||||
|
let managers = try await NETunnelProviderManager.loadAllFromPreferences()
|
||||||
|
for manager in managers {
|
||||||
|
let mgrProtoConfig = manager.protocolConfiguration as? NETunnelProviderProtocol
|
||||||
|
guard let mgrProviderConfig = mgrProtoConfig?.providerConfiguration else {
|
||||||
|
throw VPNStartError.noProviderConfig
|
||||||
|
}
|
||||||
|
let id = mgrProviderConfig["id"] as? String
|
||||||
|
if (id == targetID) {
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't find anything, throw an error
|
||||||
|
throw VPNStartError.noManagers
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startNetworkMonitor() {
|
||||||
|
networkMonitor = NWPathMonitor()
|
||||||
|
networkMonitor!.pathUpdateHandler = self.pathUpdate
|
||||||
|
networkMonitor!.start(queue: DispatchQueue(label: "NetworkMonitor"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopNetworkMonitor() {
|
||||||
|
self.networkMonitor?.cancel()
|
||||||
|
networkMonitor = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
||||||
|
nebula?.stop()
|
||||||
|
stopNetworkMonitor()
|
||||||
|
completionHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pathUpdate(path: Network.NWPath) {
|
||||||
|
let routeDescription = collectAddresses(endpoints: path.gateways)
|
||||||
|
if routeDescription != cachedRouteDescription {
|
||||||
|
// Don't bother to rebind if we don't have any gateways
|
||||||
|
if routeDescription != "" {
|
||||||
|
nebula?.rebind("network change to: \(routeDescription); from: \(cachedRouteDescription ?? "none")")
|
||||||
|
}
|
||||||
|
cachedRouteDescription = routeDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func collectAddresses(endpoints: [Network.NWEndpoint]) -> String {
|
||||||
|
var str: [String] = []
|
||||||
|
endpoints.forEach{ endpoint in
|
||||||
|
switch endpoint {
|
||||||
|
case let .hostPort(.ipv6(host), port):
|
||||||
|
str.append("[\(host)]:\(port)")
|
||||||
|
case let .hostPort(.ipv4(host), port):
|
||||||
|
str.append("\(host):\(port)")
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.sorted().joined(separator: ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func handleAppMessage(_ data: Data) async -> Data? {
|
||||||
|
guard let call = try? JSONDecoder().decode(IPCRequest.self, from: data) else {
|
||||||
|
log.error("Failed to decode IPCRequest from network extension")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var error: Error?
|
||||||
|
var data: JSON?
|
||||||
|
|
||||||
|
// start command has special treatment due to needing to call two completers
|
||||||
|
if call.command == "start" {
|
||||||
|
do {
|
||||||
|
try await self.start()
|
||||||
|
// No response data, this is expected on a clean start
|
||||||
|
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil))
|
||||||
|
} catch {
|
||||||
|
defer {
|
||||||
|
self.cancelTunnelWithError(error)
|
||||||
|
}
|
||||||
|
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error.localizedDescription)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nebula == nil {
|
||||||
|
// Respond with an empty success message in the event a command comes in before we've truly started
|
||||||
|
log.warning("Received command but do not have a nebula instance")
|
||||||
|
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: try catch over all this
|
||||||
|
switch call.command {
|
||||||
|
case "listHostmap": (data, error) = listHostmap(pending: false)
|
||||||
|
case "listPendingHostmap": (data, error) = listHostmap(pending: true)
|
||||||
|
case "getHostInfo": (data, error) = getHostInfo(args: call.arguments!)
|
||||||
|
case "setRemoteForTunnel": (data, error) = setRemoteForTunnel(args: call.arguments!)
|
||||||
|
case "closeTunnel": (data, error) = closeTunnel(args: call.arguments!)
|
||||||
|
|
||||||
|
default:
|
||||||
|
error = AppMessageError.unknownIPCType(command: call.command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error != nil) {
|
||||||
|
return try? JSONEncoder().encode(IPCResponse.init(type: .error, message: JSON(error?.localizedDescription ?? "Unknown error")))
|
||||||
|
} else {
|
||||||
|
return try? JSONEncoder().encode(IPCResponse.init(type: .success, message: data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func listHostmap(pending: Bool) -> (JSON?, Error?) {
|
||||||
|
var err: NSError?
|
||||||
|
let res = nebula!.listHostmap(pending, error: &err)
|
||||||
|
return (JSON(res), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getHostInfo(args: JSON) -> (JSON?, Error?) {
|
||||||
|
var err: NSError?
|
||||||
|
let res = nebula!.getHostInfo(byVpnIp: args["vpnIp"].string, pending: args["pending"].boolValue, error: &err)
|
||||||
|
return (JSON(res), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setRemoteForTunnel(args: JSON) -> (JSON?, Error?) {
|
||||||
|
var err: NSError?
|
||||||
|
let res = nebula!.setRemoteForTunnel(args["vpnIp"].string, addr: args["addr"].string, error: &err)
|
||||||
|
return (JSON(res), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closeTunnel(args: JSON) -> (JSON?, Error?) {
|
||||||
|
let res = nebula!.closeTunnel(args["vpnIp"].string)
|
||||||
|
return (JSON(res), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var tunnelFileDescriptor: Int32? {
|
||||||
|
var ctlInfo = ctl_info()
|
||||||
|
withUnsafeMutablePointer(to: &ctlInfo.ctl_name) {
|
||||||
|
$0.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size(ofValue: $0.pointee)) {
|
||||||
|
_ = strcpy($0, "com.apple.net.utun_control")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for fd: Int32 in 0...1024 {
|
||||||
|
var addr = sockaddr_ctl()
|
||||||
|
var ret: Int32 = -1
|
||||||
|
var len = socklen_t(MemoryLayout.size(ofValue: addr))
|
||||||
|
withUnsafeMutablePointer(to: &addr) {
|
||||||
|
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
||||||
|
ret = getpeername(fd, $0, &len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ret != 0 || addr.sc_family != AF_SYSTEM {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ctlInfo.ctl_id == 0 {
|
||||||
|
ret = ioctl(fd, CTLIOCGINFO, &ctlInfo)
|
||||||
|
if ret != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addr.sc_id == ctlInfo.ctl_id {
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,550 +1,543 @@
|
||||||
import MobileNebula
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
import MobileNebula
|
||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
|
let log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
|
||||||
|
|
||||||
enum SiteError: Error {
|
enum SiteError: Error {
|
||||||
case nonConforming(site: [String: Any]?)
|
case nonConforming(site: [String : Any]?)
|
||||||
case noCertificate
|
case noCertificate
|
||||||
case keyLoad
|
case keyLoad
|
||||||
case keySave
|
case keySave
|
||||||
case unmanagedGetCredentials
|
case unmanagedGetCredentials
|
||||||
case dnCredentialLoad
|
case dnCredentialLoad
|
||||||
case dnCredentialSave
|
case dnCredentialSave
|
||||||
|
|
||||||
// Throw in all other cases
|
// Throw in all other cases
|
||||||
case unexpected(code: Int)
|
case unexpected(code: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SiteError: CustomStringConvertible {
|
extension SiteError: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .nonConforming(let site):
|
case .nonConforming(let 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"
|
||||||
case .keyLoad:
|
case .keyLoad:
|
||||||
return "failed to get key from keychain"
|
return "failed to get key from keychain"
|
||||||
case .keySave:
|
case .keySave:
|
||||||
return "failed to store key material in keychain"
|
return "failed to store key material in keychain"
|
||||||
case .unmanagedGetCredentials:
|
case .unmanagedGetCredentials:
|
||||||
return "Cannot get dn credentials for unmanaged site"
|
return "Cannot get dn credentials for unmanaged site"
|
||||||
case .dnCredentialLoad:
|
case .dnCredentialLoad:
|
||||||
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 = "error"
|
||||||
case success = "success"
|
case success = "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?) {
|
||||||
self.type = type
|
self.type = type
|
||||||
self.message = message
|
self.message = message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class IPCRequest: Codable {
|
class IPCRequest: Codable {
|
||||||
var command: String
|
var command: String
|
||||||
var arguments: JSON?
|
var arguments: JSON?
|
||||||
|
|
||||||
init(command: String, arguments: JSON?) {
|
init(command: String, arguments: JSON?) {
|
||||||
self.command = command
|
self.command = command
|
||||||
self.arguments = arguments
|
self.arguments = arguments
|
||||||
}
|
}
|
||||||
|
|
||||||
init(command: String) {
|
init(command: String) {
|
||||||
self.command = command
|
self.command = command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CertificateInfo: Codable {
|
struct CertificateInfo: Codable {
|
||||||
var cert: Certificate
|
var cert: Certificate
|
||||||
var rawCert: String
|
var rawCert: String
|
||||||
var validity: CertificateValidity
|
var validity: CertificateValidity
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case cert = "Cert"
|
case cert = "Cert"
|
||||||
case rawCert = "RawCert"
|
case rawCert = "RawCert"
|
||||||
case validity = "Validity"
|
case validity = "Validity"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Certificate: Codable {
|
struct Certificate: Codable {
|
||||||
var fingerprint: String
|
var fingerprint: String
|
||||||
var signature: String
|
var signature: String
|
||||||
var details: CertificateDetails
|
var details: CertificateDetails
|
||||||
|
|
||||||
/// An empty initializer to make error reporting easier
|
/// An empty initializer to make error reporting easier
|
||||||
init() {
|
init() {
|
||||||
fingerprint = ""
|
fingerprint = ""
|
||||||
signature = ""
|
signature = ""
|
||||||
details = CertificateDetails()
|
details = CertificateDetails()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CertificateDetails: Codable {
|
struct CertificateDetails: Codable {
|
||||||
var name: String
|
var name: String
|
||||||
var notBefore: String
|
var notBefore: String
|
||||||
var notAfter: String
|
var notAfter: String
|
||||||
var publicKey: String
|
var publicKey: String
|
||||||
var groups: [String]
|
var groups: [String]
|
||||||
var ips: [String]
|
var ips: [String]
|
||||||
var subnets: [String]
|
var subnets: [String]
|
||||||
var isCa: Bool
|
var isCa: Bool
|
||||||
var issuer: String
|
var issuer: String
|
||||||
|
|
||||||
/// An empty initializer to make error reporting easier
|
/// An empty initializer to make error reporting easier
|
||||||
init() {
|
init() {
|
||||||
name = ""
|
name = ""
|
||||||
notBefore = ""
|
notBefore = ""
|
||||||
notAfter = ""
|
notAfter = ""
|
||||||
publicKey = ""
|
publicKey = ""
|
||||||
groups = []
|
groups = []
|
||||||
ips = ["ERROR"]
|
ips = ["ERROR"]
|
||||||
subnets = []
|
subnets = []
|
||||||
isCa = false
|
isCa = false
|
||||||
issuer = ""
|
issuer = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CertificateValidity: Codable {
|
struct CertificateValidity: Codable {
|
||||||
var valid: Bool
|
var valid: Bool
|
||||||
var reason: String
|
var reason: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case valid = "Valid"
|
case valid = "Valid"
|
||||||
case reason = "Reason"
|
case reason = "Reason"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let statusMap: [NEVPNStatus: Bool] = [
|
let statusMap: Dictionary<NEVPNStatus, Bool> = [
|
||||||
NEVPNStatus.invalid: false,
|
NEVPNStatus.invalid: false,
|
||||||
NEVPNStatus.disconnected: false,
|
NEVPNStatus.disconnected: false,
|
||||||
NEVPNStatus.connecting: false,
|
NEVPNStatus.connecting: false,
|
||||||
NEVPNStatus.connected: true,
|
NEVPNStatus.connected: true,
|
||||||
NEVPNStatus.reasserting: true,
|
NEVPNStatus.reasserting: true,
|
||||||
NEVPNStatus.disconnecting: true,
|
NEVPNStatus.disconnecting: true,
|
||||||
]
|
]
|
||||||
|
|
||||||
let statusString: [NEVPNStatus: String] = [
|
let statusString: Dictionary<NEVPNStatus, String> = [
|
||||||
NEVPNStatus.invalid: "Invalid configuration",
|
NEVPNStatus.invalid: "Invalid configuration",
|
||||||
NEVPNStatus.disconnected: "Disconnected",
|
NEVPNStatus.disconnected: "Disconnected",
|
||||||
NEVPNStatus.connecting: "Connecting...",
|
NEVPNStatus.connecting: "Connecting...",
|
||||||
NEVPNStatus.connected: "Connected",
|
NEVPNStatus.connected: "Connected",
|
||||||
NEVPNStatus.reasserting: "Reasserting...",
|
NEVPNStatus.reasserting: "Reasserting...",
|
||||||
NEVPNStatus.disconnecting: "Disconnecting...",
|
NEVPNStatus.disconnecting: "Disconnecting...",
|
||||||
]
|
]
|
||||||
|
|
||||||
// Represents a site that was pulled out of the system configuration
|
// Represents a site that was pulled out of the system configuration
|
||||||
class Site: Codable {
|
class Site: Codable {
|
||||||
// Stored in manager
|
// Stored in manager
|
||||||
var name: String
|
var name: String
|
||||||
var id: String
|
var id: String
|
||||||
|
|
||||||
// Stored in proto
|
// Stored in proto
|
||||||
var staticHostmap: [String: StaticHosts]
|
var staticHostmap: Dictionary<String, StaticHosts>
|
||||||
var unsafeRoutes: [UnsafeRoute]
|
var unsafeRoutes: [UnsafeRoute]
|
||||||
var cert: CertificateInfo?
|
var cert: CertificateInfo?
|
||||||
var ca: [CertificateInfo]
|
var ca: [CertificateInfo]
|
||||||
var lhDuration: Int
|
var lhDuration: Int
|
||||||
var port: Int
|
var port: Int
|
||||||
var mtu: Int
|
var mtu: Int
|
||||||
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
|
||||||
// The following fields are present if managed = true
|
// The following fields are present if managed = true
|
||||||
var lastManagedUpdate: String?
|
var lastManagedUpdate: String?
|
||||||
var rawConfig: String?
|
var rawConfig: String?
|
||||||
|
|
||||||
/// If true then this site needs to be migrated to the filesystem. Should be handled by the initiator of the site
|
/// If true then this site needs to be migrated to the filesystem. Should be handled by the initiator of the site
|
||||||
var needsToMigrateToFS: Bool = false
|
var needsToMigrateToFS: Bool = false
|
||||||
|
|
||||||
// A list of error encountered when trying to rehydrate a site from config
|
// A list of error encountered when trying to rehydrate a site from config
|
||||||
var errors: [String]
|
var errors: [String]
|
||||||
|
|
||||||
var manager: NETunnelProviderManager?
|
var manager: NETunnelProviderManager?
|
||||||
|
|
||||||
var incomingSite: IncomingSite?
|
var incomingSite: IncomingSite?
|
||||||
|
|
||||||
/// 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]
|
self.connected = statusMap[manager.connection.status]
|
||||||
self.status = statusString[manager.connection.status]
|
self.status = statusString[manager.connection.status]
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(proto: NETunnelProviderProtocol) throws {
|
|
||||||
let dict = proto.providerConfiguration
|
|
||||||
|
|
||||||
if dict?["config"] != nil {
|
|
||||||
let config = dict?["config"] as? Data ?? Data()
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
let incoming = try decoder.decode(IncomingSite.self, from: config)
|
|
||||||
self.init(incoming: incoming)
|
|
||||||
self.needsToMigrateToFS = true
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = dict?["id"] as? String ?? nil
|
convenience init(proto: NETunnelProviderProtocol) throws {
|
||||||
if id == nil {
|
let dict = proto.providerConfiguration
|
||||||
throw SiteError.nonConforming(site: dict)
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.init(path: SiteList.getSiteConfigFile(id: id!, createDir: false))
|
if dict?["config"] != nil {
|
||||||
}
|
let config = dict?["config"] as? Data ?? Data()
|
||||||
|
let decoder = JSONDecoder()
|
||||||
/// Creates a new site from a path on the filesystem. Mainly ussed by the VPN process or when in simulator where we lack a NEVPNManager
|
let incoming = try decoder.decode(IncomingSite.self, from: config)
|
||||||
convenience init(path: URL) throws {
|
self.init(incoming: incoming)
|
||||||
let config = try Data(contentsOf: path)
|
self.needsToMigrateToFS = true
|
||||||
let decoder = JSONDecoder()
|
return
|
||||||
let incoming = try decoder.decode(IncomingSite.self, from: config)
|
|
||||||
self.init(incoming: incoming)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(incoming: IncomingSite) {
|
|
||||||
var err: NSError?
|
|
||||||
|
|
||||||
incomingSite = incoming
|
|
||||||
errors = []
|
|
||||||
name = incoming.name
|
|
||||||
id = incoming.id
|
|
||||||
staticHostmap = incoming.staticHostmap
|
|
||||||
unsafeRoutes = incoming.unsafeRoutes ?? []
|
|
||||||
lhDuration = incoming.lhDuration
|
|
||||||
port = incoming.port
|
|
||||||
cipher = incoming.cipher
|
|
||||||
sortKey = incoming.sortKey ?? 0
|
|
||||||
logVerbosity = incoming.logVerbosity ?? "info"
|
|
||||||
mtu = incoming.mtu ?? 1300
|
|
||||||
managed = incoming.managed ?? false
|
|
||||||
lastManagedUpdate = incoming.lastManagedUpdate
|
|
||||||
rawConfig = incoming.rawConfig
|
|
||||||
|
|
||||||
do {
|
|
||||||
let rawCert = incoming.cert
|
|
||||||
let rawDetails = MobileNebulaParseCerts(rawCert, &err)
|
|
||||||
if err != nil {
|
|
||||||
throw err!
|
|
||||||
}
|
|
||||||
|
|
||||||
var certs: [CertificateInfo]
|
|
||||||
|
|
||||||
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
|
|
||||||
if certs.count == 0 {
|
|
||||||
throw SiteError.noCertificate
|
|
||||||
}
|
|
||||||
cert = certs[0]
|
|
||||||
if !cert!.validity.valid {
|
|
||||||
errors.append("Certificate is invalid: \(cert!.validity.reason)")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
errors.append("Error while loading certificate: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let rawCa = incoming.ca
|
|
||||||
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
|
|
||||||
if err != nil {
|
|
||||||
throw err!
|
|
||||||
}
|
|
||||||
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
|
|
||||||
|
|
||||||
var hasErrors = false
|
|
||||||
ca.forEach { cert in
|
|
||||||
if !cert.validity.valid {
|
|
||||||
hasErrors = true
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if hasErrors && !managed {
|
let id = dict?["id"] as? String ?? nil
|
||||||
errors.append("There are issues with 1 or more ca certificates")
|
if id == nil {
|
||||||
}
|
throw SiteError.nonConforming(site: dict)
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
try self.init(path: SiteList.getSiteConfigFile(id: id!, createDir: false))
|
||||||
ca = []
|
|
||||||
errors.append("Error while loading certificate authorities: \(error.localizedDescription)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
/// Creates a new site from a path on the filesystem. Mainly ussed by the VPN process or when in simulator where we lack a NEVPNManager
|
||||||
logFile = try SiteList.getSiteLogFile(id: self.id, createDir: true).path
|
convenience init(path: URL) throws {
|
||||||
} catch {
|
let config = try Data(contentsOf: path)
|
||||||
logFile = nil
|
let decoder = JSONDecoder()
|
||||||
errors.append("Unable to create the site directory: \(error.localizedDescription)")
|
let incoming = try decoder.decode(IncomingSite.self, from: config)
|
||||||
|
self.init(incoming: incoming)
|
||||||
}
|
}
|
||||||
|
|
||||||
if managed && (try? getDNCredentials())?.invalid != false {
|
init(incoming: IncomingSite) {
|
||||||
errors.append("Unable to fetch managed updates - please re-enroll the device")
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.isEmpty {
|
|
||||||
do {
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
let rawConfig = try encoder.encode(incoming)
|
|
||||||
let key = try getKey()
|
|
||||||
let strConfig = String(data: rawConfig, encoding: .utf8)
|
|
||||||
var err: NSError?
|
var err: NSError?
|
||||||
|
|
||||||
MobileNebulaTestConfig(strConfig, key, &err)
|
incomingSite = incoming
|
||||||
if err != nil {
|
errors = []
|
||||||
throw err!
|
name = incoming.name
|
||||||
|
id = incoming.id
|
||||||
|
staticHostmap = incoming.staticHostmap
|
||||||
|
unsafeRoutes = incoming.unsafeRoutes ?? []
|
||||||
|
lhDuration = incoming.lhDuration
|
||||||
|
port = incoming.port
|
||||||
|
cipher = incoming.cipher
|
||||||
|
sortKey = incoming.sortKey ?? 0
|
||||||
|
logVerbosity = incoming.logVerbosity ?? "info"
|
||||||
|
mtu = incoming.mtu ?? 1300
|
||||||
|
managed = incoming.managed ?? false
|
||||||
|
lastManagedUpdate = incoming.lastManagedUpdate
|
||||||
|
rawConfig = incoming.rawConfig
|
||||||
|
|
||||||
|
do {
|
||||||
|
let rawCert = incoming.cert
|
||||||
|
let rawDetails = MobileNebulaParseCerts(rawCert, &err)
|
||||||
|
if (err != nil) {
|
||||||
|
throw err!
|
||||||
|
}
|
||||||
|
|
||||||
|
var certs: [CertificateInfo]
|
||||||
|
|
||||||
|
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
|
||||||
|
if (certs.count == 0) {
|
||||||
|
throw SiteError.noCertificate
|
||||||
|
}
|
||||||
|
cert = certs[0]
|
||||||
|
if (!cert!.validity.valid) {
|
||||||
|
errors.append("Certificate is invalid: \(cert!.validity.reason)")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
errors.append("Error while loading certificate: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
errors.append("Config test error: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets the private key from the keystore, we don't always need it in memory
|
do {
|
||||||
func getKey() throws -> String {
|
let rawCa = incoming.ca
|
||||||
guard let keyData = KeyChain.load(key: "\(id).key") else {
|
let rawCaDetails = MobileNebulaParseCerts(rawCa, &err)
|
||||||
throw SiteError.keyLoad
|
if (err != nil) {
|
||||||
|
throw err!
|
||||||
|
}
|
||||||
|
ca = try JSONDecoder().decode([CertificateInfo].self, from: rawCaDetails.data(using: .utf8)!)
|
||||||
|
|
||||||
|
var hasErrors = false
|
||||||
|
ca.forEach { cert in
|
||||||
|
if (!cert.validity.valid) {
|
||||||
|
hasErrors = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasErrors && !managed) {
|
||||||
|
errors.append("There are issues with 1 or more ca certificates")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
ca = []
|
||||||
|
errors.append("Error while loading certificate authorities: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
logFile = try SiteList.getSiteLogFile(id: self.id, createDir: true).path
|
||||||
|
} catch {
|
||||||
|
logFile = nil
|
||||||
|
errors.append("Unable to create the site directory: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managed && (try? getDNCredentials())?.invalid != false) {
|
||||||
|
errors.append("Unable to fetch managed updates - please re-enroll the device")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.isEmpty) {
|
||||||
|
do {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let rawConfig = try encoder.encode(incoming)
|
||||||
|
let key = try getKey()
|
||||||
|
let strConfig = String(data: rawConfig, encoding: .utf8)
|
||||||
|
var err: NSError?
|
||||||
|
|
||||||
|
MobileNebulaTestConfig(strConfig, key, &err)
|
||||||
|
if (err != nil) {
|
||||||
|
throw err!
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errors.append("Config test error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: make sure this is valid on return!
|
// Gets the private key from the keystore, we don't always need it in memory
|
||||||
return String(decoding: keyData, as: UTF8.self)
|
func getKey() throws -> String {
|
||||||
}
|
guard let keyData = KeyChain.load(key: "\(id).key") else {
|
||||||
|
throw SiteError.keyLoad
|
||||||
|
}
|
||||||
|
|
||||||
func getDNCredentials() throws -> DNCredentials {
|
//TODO: make sure this is valid on return!
|
||||||
if !managed {
|
return String(decoding: keyData, as: UTF8.self)
|
||||||
throw SiteError.unmanagedGetCredentials
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let rawDNCredentials = KeyChain.load(key: "\(id).dnCredentials")
|
func getDNCredentials() throws -> DNCredentials {
|
||||||
if rawDNCredentials == nil {
|
if (!managed) {
|
||||||
throw SiteError.dnCredentialLoad
|
throw SiteError.unmanagedGetCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
let rawDNCredentials = KeyChain.load(key: "\(id).dnCredentials")
|
||||||
|
if rawDNCredentials == nil {
|
||||||
|
throw SiteError.dnCredentialLoad
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
return try decoder.decode(DNCredentials.self, from: rawDNCredentials!)
|
||||||
}
|
}
|
||||||
|
|
||||||
let decoder = JSONDecoder()
|
func invalidateDNCredentials() throws {
|
||||||
return try decoder.decode(DNCredentials.self, from: rawDNCredentials!)
|
let creds = try getDNCredentials()
|
||||||
}
|
creds.invalid = true
|
||||||
|
|
||||||
func invalidateDNCredentials() throws {
|
if (!(try creds.save(siteID: self.id))) {
|
||||||
let creds = try getDNCredentials()
|
throw SiteError.dnCredentialLoad
|
||||||
creds.invalid = true
|
}
|
||||||
|
|
||||||
if !(try creds.save(siteID: self.id)) {
|
|
||||||
throw SiteError.dnCredentialLoad
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func validateDNCredentials() throws {
|
func validateDNCredentials() throws {
|
||||||
let creds = try getDNCredentials()
|
let creds = try getDNCredentials()
|
||||||
creds.invalid = false
|
creds.invalid = false
|
||||||
|
|
||||||
if !(try creds.save(siteID: self.id)) {
|
if (!(try creds.save(siteID: self.id))) {
|
||||||
throw SiteError.dnCredentialSave
|
throw SiteError.dnCredentialSave
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func getConfig() throws -> Data {
|
func getConfig() throws -> Data {
|
||||||
return try self.incomingSite!.getConfig()
|
return try self.incomingSite!.getConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limits what we export to the UI
|
// Limits what we export to the UI
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case name
|
case name
|
||||||
case id
|
case id
|
||||||
case staticHostmap
|
case staticHostmap
|
||||||
case cert
|
case cert
|
||||||
case ca
|
case ca
|
||||||
case lhDuration
|
case lhDuration
|
||||||
case port
|
case port
|
||||||
case cipher
|
case cipher
|
||||||
case sortKey
|
case sortKey
|
||||||
case connected
|
case connected
|
||||||
case status
|
case status
|
||||||
case logFile
|
case logFile
|
||||||
case unsafeRoutes
|
case unsafeRoutes
|
||||||
case logVerbosity
|
case logVerbosity
|
||||||
case errors
|
case errors
|
||||||
case mtu
|
case mtu
|
||||||
case managed
|
case managed
|
||||||
case lastManagedUpdate
|
case lastManagedUpdate
|
||||||
case rawConfig
|
case rawConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class StaticHosts: Codable {
|
class StaticHosts: Codable {
|
||||||
var lighthouse: Bool
|
var lighthouse: Bool
|
||||||
var destinations: [String]
|
var destinations: [String]
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnsafeRoute: Codable {
|
class UnsafeRoute: Codable {
|
||||||
var route: String
|
var route: String
|
||||||
var via: String
|
var via: String
|
||||||
var mtu: Int?
|
var mtu: Int?
|
||||||
}
|
}
|
||||||
|
|
||||||
class DNCredentials: Codable {
|
class DNCredentials: Codable {
|
||||||
var hostID: String
|
var hostID: String
|
||||||
var privateKey: String
|
var privateKey: String
|
||||||
var counter: Int
|
var counter: Int
|
||||||
var trustedKeys: String
|
var trustedKeys: String
|
||||||
var invalid: Bool {
|
var invalid: Bool {
|
||||||
get { return _invalid ?? false }
|
get { return _invalid ?? false }
|
||||||
set { _invalid = newValue }
|
set { _invalid = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _invalid: Bool?
|
private var _invalid: Bool?
|
||||||
|
|
||||||
func save(siteID: String) throws -> Bool {
|
func save(siteID: String) throws -> Bool {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
let rawDNCredentials = try encoder.encode(self)
|
let rawDNCredentials = try encoder.encode(self)
|
||||||
|
|
||||||
return KeyChain.save(key: "\(siteID).dnCredentials", data: rawDNCredentials, managed: true)
|
return KeyChain.save(key: "\(siteID).dnCredentials", data: rawDNCredentials, managed: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case hostID
|
case hostID
|
||||||
case privateKey
|
case privateKey
|
||||||
case counter
|
case counter
|
||||||
case trustedKeys
|
case trustedKeys
|
||||||
case _invalid = "invalid"
|
case _invalid = "invalid"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This class represents a site coming in from flutter, meant only to be saved and re-loaded as a proper Site
|
// This class represents a site coming in from flutter, meant only to be saved and re-loaded as a proper Site
|
||||||
struct IncomingSite: Codable {
|
struct IncomingSite: Codable {
|
||||||
var name: String
|
var name: String
|
||||||
var id: String
|
var id: String
|
||||||
var staticHostmap: [String: StaticHosts]
|
var staticHostmap: Dictionary<String, StaticHosts>
|
||||||
var unsafeRoutes: [UnsafeRoute]?
|
var unsafeRoutes: [UnsafeRoute]?
|
||||||
var cert: String?
|
var cert: String?
|
||||||
var ca: String?
|
var ca: String?
|
||||||
var lhDuration: Int
|
var lhDuration: Int
|
||||||
var port: Int
|
var port: Int
|
||||||
var mtu: Int?
|
var mtu: Int?
|
||||||
var cipher: String
|
var cipher: String
|
||||||
var sortKey: Int?
|
var sortKey: Int?
|
||||||
var logVerbosity: String?
|
var logVerbosity: String?
|
||||||
var key: String?
|
var key: String?
|
||||||
var managed: Bool?
|
var managed: Bool?
|
||||||
// The following fields are present if managed = true
|
// The following fields are present if managed = true
|
||||||
var dnCredentials: DNCredentials?
|
var dnCredentials: DNCredentials?
|
||||||
var lastManagedUpdate: String?
|
var lastManagedUpdate: String?
|
||||||
var rawConfig: String?
|
var rawConfig: String?
|
||||||
|
|
||||||
func getConfig() throws -> Data {
|
func getConfig() throws -> Data {
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
var config = self
|
var config = self
|
||||||
|
|
||||||
config.key = nil
|
config.key = nil
|
||||||
config.dnCredentials = nil
|
config.dnCredentials = nil
|
||||||
|
|
||||||
return try encoder.encode(config)
|
return try encoder.encode(config)
|
||||||
}
|
|
||||||
|
|
||||||
func save(
|
|
||||||
manager: NETunnelProviderManager?, saveToManager: Bool = true,
|
|
||||||
callback: @escaping (Error?) -> Void
|
|
||||||
) {
|
|
||||||
let configPath: URL
|
|
||||||
|
|
||||||
do {
|
|
||||||
configPath = try SiteList.getSiteConfigFile(id: self.id, createDir: true)
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
callback(error)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.notice("Saving to \(configPath, privacy: .public)")
|
func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) {
|
||||||
do {
|
let configPath: URL
|
||||||
if self.key != nil {
|
|
||||||
let data = self.key!.data(using: .utf8)
|
do {
|
||||||
if !KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false) {
|
configPath = try SiteList.getSiteConfigFile(id: self.id, createDir: true)
|
||||||
return callback(SiteError.keySave)
|
|
||||||
|
} catch {
|
||||||
|
callback(error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
log.notice("Saving to \(configPath, privacy: .public)")
|
||||||
if (try self.dnCredentials?.save(siteID: self.id)) == false {
|
do {
|
||||||
return callback(SiteError.dnCredentialSave)
|
if (self.key != nil) {
|
||||||
|
let data = self.key!.data(using: .utf8)
|
||||||
|
if (!KeyChain.save(key: "\(self.id).key", data: data!, managed: self.managed ?? false)) {
|
||||||
|
return callback(SiteError.keySave)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if ((try self.dnCredentials?.save(siteID: self.id)) == false) {
|
||||||
|
return callback(SiteError.dnCredentialSave)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return callback(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.getConfig().write(to: configPath)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
return callback(error)
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
return callback(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
try self.getConfig().write(to: configPath)
|
|
||||||
|
|
||||||
} catch {
|
#if targetEnvironment(simulator)
|
||||||
return callback(error)
|
// We are on a simulator and there is no NEVPNManager for us to interact with
|
||||||
}
|
|
||||||
|
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
// 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(
|
private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) {
|
||||||
manager: NETunnelProviderManager?, callback: @escaping (Error?) -> Void
|
if (manager != nil) {
|
||||||
) {
|
// We need to refresh our settings to properly update config
|
||||||
if manager != nil {
|
manager?.loadFromPreferences { error in
|
||||||
// We need to refresh our settings to properly update config
|
if (error != nil) {
|
||||||
manager?.loadFromPreferences { error in
|
return callback(error)
|
||||||
if error != nil {
|
}
|
||||||
return callback(error)
|
|
||||||
|
return self.finishSaveToManager(manager: manager!, callback: callback)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.finishSaveToManager(manager: manager!, callback: callback)
|
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
|
private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) {
|
||||||
}
|
// Stuff our details in the protocol
|
||||||
|
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
|
||||||
|
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension";
|
||||||
|
// WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match
|
||||||
|
// managers in PacketTunnelProvider.findManager
|
||||||
|
proto.providerConfiguration = ["id": self.id]
|
||||||
|
proto.serverAddress = "Nebula"
|
||||||
|
|
||||||
private func finishSaveToManager(
|
// Finish up the manager, this is what stores everything at the system level
|
||||||
manager: NETunnelProviderManager, callback: @escaping (Error?) -> Void
|
manager.protocolConfiguration = proto
|
||||||
) {
|
//TODO: cert name? manager.protocolConfiguration?.username
|
||||||
// Stuff our details in the protocol
|
|
||||||
let proto =
|
|
||||||
manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
|
|
||||||
proto.providerBundleIdentifier = "net.defined.mobileNebula.NebulaNetworkExtension"
|
|
||||||
// WARN: If we stop setting providerConfiguration["id"] here, we'll need to use something else to match
|
|
||||||
// managers in PacketTunnelProvider.findManager
|
|
||||||
proto.providerConfiguration = ["id": self.id]
|
|
||||||
proto.serverAddress = "Nebula"
|
|
||||||
|
|
||||||
// Finish up the manager, this is what stores everything at the system level
|
//TODO: This is what is shown on the vpn page. We should add more identifying details in
|
||||||
manager.protocolConfiguration = proto
|
manager.localizedDescription = self.name
|
||||||
//TODO: cert name? manager.protocolConfiguration?.username
|
manager.isEnabled = true
|
||||||
|
|
||||||
//TODO: This is what is shown on the vpn page. We should add more identifying details in
|
manager.saveToPreferences{ error in
|
||||||
manager.localizedDescription = self.name
|
return callback(error)
|
||||||
manager.isEnabled = true
|
}
|
||||||
|
|
||||||
manager.saveToPreferences { error in
|
|
||||||
return callback(error)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,146 +1,140 @@
|
||||||
import NetworkExtension
|
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(
|
let rootDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
|
||||||
forSecurityApplicationGroupIdentifier: "group.net.defined.mobileNebula")!
|
|
||||||
|
if (!fileManager.fileExists(atPath: rootDir.absoluteString)) {
|
||||||
if !fileManager.fileExists(atPath: rootDir.absoluteString) {
|
try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true)
|
||||||
try fileManager.createDirectory(at: rootDir, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootDir
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists
|
|
||||||
static func getSitesDir() throws -> URL {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true)
|
|
||||||
if !fileManager.fileExists(atPath: sitesDir.absoluteString) {
|
|
||||||
try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
return sitesDir
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the directory where a single site would live, $rootDir/sites/$siteID
|
|
||||||
static func getSiteDir(id: String, create: Bool = false) throws -> URL {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true)
|
|
||||||
if create && !fileManager.fileExists(atPath: siteDir.absoluteString) {
|
|
||||||
try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true)
|
|
||||||
}
|
|
||||||
return siteDir
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json
|
|
||||||
static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL {
|
|
||||||
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
|
|
||||||
static func getSiteLogFile(id: String, createDir: Bool) throws -> URL {
|
|
||||||
return try getSiteDir(id: id, create: createDir).appendingPathComponent(
|
|
||||||
"logs", isDirectory: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(completion: @escaping ([String: Site]?, Error?) -> Void) {
|
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
SiteList.loadAllFromFS { sites, err in
|
|
||||||
if sites != nil {
|
|
||||||
self.sites = sites!
|
|
||||||
}
|
}
|
||||||
completion(sites, err)
|
|
||||||
}
|
return rootDir
|
||||||
#else
|
}
|
||||||
SiteList.loadAllFromNETPM { sites, err in
|
|
||||||
if sites != nil {
|
/// Gets the directory where all sites live, $rootDir/sites. Does ensure the directory exists
|
||||||
self.sites = sites!
|
static func getSitesDir() throws -> URL {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
let sitesDir = try getRootDir().appendingPathComponent("sites", isDirectory: true)
|
||||||
|
if (!fileManager.fileExists(atPath: sitesDir.absoluteString)) {
|
||||||
|
try fileManager.createDirectory(at: sitesDir, withIntermediateDirectories: true)
|
||||||
}
|
}
|
||||||
completion(sites, err)
|
return sitesDir
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> Void) {
|
|
||||||
let fileManager = FileManager.default
|
|
||||||
var siteDirs: [URL]
|
|
||||||
var sites = [String: Site]()
|
|
||||||
|
|
||||||
do {
|
|
||||||
siteDirs = try fileManager.contentsOfDirectory(
|
|
||||||
at: getSitesDir(), includingPropertiesForKeys: nil)
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
completion(nil, error)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
siteDirs.forEach { path in
|
/// Gets the directory where a single site would live, $rootDir/sites/$siteID
|
||||||
do {
|
static func getSiteDir(id: String, create: Bool = false) throws -> URL {
|
||||||
let site = try Site(
|
let fileManager = FileManager.default
|
||||||
path: path.appendingPathComponent("config").appendingPathExtension("json"))
|
let siteDir = try getSitesDir().appendingPathComponent(id, isDirectory: true)
|
||||||
sites[site.id] = site
|
if (create && !fileManager.fileExists(atPath: siteDir.absoluteString)) {
|
||||||
|
try fileManager.createDirectory(at: siteDir, withIntermediateDirectories: true)
|
||||||
} catch {
|
}
|
||||||
print(error)
|
return siteDir
|
||||||
try? fileManager.removeItem(at: path)
|
|
||||||
print("Deleted non conforming site \(path)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
completion(sites, nil)
|
/// Gets the file that represents the site configuration, $rootDir/sites/$siteID/config.json
|
||||||
}
|
static func getSiteConfigFile(id: String, createDir: Bool) throws -> URL {
|
||||||
|
return try getSiteDir(id: id, create: createDir).appendingPathComponent("config", isDirectory: false).appendingPathExtension("json")
|
||||||
private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> Void) {
|
}
|
||||||
var sites = [String: Site]()
|
|
||||||
|
/// Gets the file that represents the site log output, $rootDir/sites/$siteID/log
|
||||||
// dispatchGroup is used to ensure we have migrated all sites before returning them
|
static func getSiteLogFile(id: String, createDir: Bool) throws -> URL {
|
||||||
// If there are no sites to migrate, there are never any entrants
|
return try getSiteDir(id: id, create: createDir).appendingPathComponent("logs", isDirectory: false)
|
||||||
let dispatchGroup = DispatchGroup()
|
}
|
||||||
|
|
||||||
NETunnelProviderManager.loadAllFromPreferences { newManagers, err in
|
init(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||||
if err != nil {
|
#if targetEnvironment(simulator)
|
||||||
return completion(nil, err)
|
SiteList.loadAllFromFS { sites, err in
|
||||||
}
|
if sites != nil {
|
||||||
|
self.sites = sites!
|
||||||
newManagers?.forEach { manager in
|
|
||||||
do {
|
|
||||||
let site = try Site(manager: manager)
|
|
||||||
if site.needsToMigrateToFS {
|
|
||||||
dispatchGroup.enter()
|
|
||||||
site.incomingSite?.save(manager: manager) { error in
|
|
||||||
if error != nil {
|
|
||||||
print("Error while migrating site to fs: \(error!.localizedDescription)")
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Migrated site to fs: \(site.name)")
|
|
||||||
site.needsToMigrateToFS = false
|
|
||||||
dispatchGroup.leave()
|
|
||||||
}
|
}
|
||||||
}
|
completion(sites, err)
|
||||||
sites[site.id] = site
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
//TODO: notify the user about this
|
|
||||||
print("Deleted non conforming site \(manager) \(error)")
|
|
||||||
manager.removeFromPreferences()
|
|
||||||
//TODO: delete from disk, we need to try and discover the site id though
|
|
||||||
}
|
}
|
||||||
}
|
#else
|
||||||
|
SiteList.loadAllFromNETPM { sites, err in
|
||||||
dispatchGroup.notify(queue: .main) {
|
if sites != nil {
|
||||||
completion(sites, nil)
|
self.sites = sites!
|
||||||
}
|
}
|
||||||
|
completion(sites, err)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func loadAllFromFS(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
var siteDirs: [URL]
|
||||||
|
var sites = [String: Site]()
|
||||||
|
|
||||||
|
do {
|
||||||
|
siteDirs = try fileManager.contentsOfDirectory(at: getSitesDir(), includingPropertiesForKeys: nil)
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
completion(nil, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
siteDirs.forEach { path in
|
||||||
|
do {
|
||||||
|
let site = try Site(path: path.appendingPathComponent("config").appendingPathExtension("json"))
|
||||||
|
sites[site.id] = site
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
print(error)
|
||||||
|
try? fileManager.removeItem(at: path)
|
||||||
|
print("Deleted non conforming site \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(sites, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func loadAllFromNETPM(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||||
|
var sites = [String: Site]()
|
||||||
|
|
||||||
|
// 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
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
|
||||||
|
NETunnelProviderManager.loadAllFromPreferences() { newManagers, err in
|
||||||
|
if (err != nil) {
|
||||||
|
return completion(nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newManagers?.forEach { manager in
|
||||||
|
do {
|
||||||
|
let site = try Site(manager: manager)
|
||||||
|
if site.needsToMigrateToFS {
|
||||||
|
dispatchGroup.enter()
|
||||||
|
site.incomingSite?.save(manager: manager) { error in
|
||||||
|
if error != nil {
|
||||||
|
print("Error while migrating site to fs: \(error!.localizedDescription)")
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Migrated site to fs: \(site.name)")
|
||||||
|
site.needsToMigrateToFS = false
|
||||||
|
dispatchGroup.leave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sites[site.id] = site
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
//TODO: notify the user about this
|
||||||
|
print("Deleted non conforming site \(manager) \(error)")
|
||||||
|
manager.removeFromPreferences()
|
||||||
|
//TODO: delete from disk, we need to try and discover the site id though
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchGroup.notify(queue: .main) {
|
||||||
|
completion(sites, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSites() -> [String: Site] {
|
||||||
|
return sites
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func getSites() -> [String: Site] {
|
|
||||||
return sites
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,54 @@
|
||||||
import MobileNebula
|
import MobileNebula
|
||||||
|
|
||||||
enum APIClientError: Error {
|
enum APIClientError: Error {
|
||||||
case invalidCredentials
|
case invalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
apiClient = MobileNebulaNewAPIClient("MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
|
||||||
"MobileNebula/\(packageInfo.getVersion()) (iOS \(packageInfo.getSystemVersion()))")!
|
|
||||||
}
|
|
||||||
|
|
||||||
func enroll(code: String) throws -> IncomingSite {
|
|
||||||
let res = try apiClient.enroll(code)
|
|
||||||
return try decodeIncomingSite(jsonSite: res.site)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tryUpdate(
|
|
||||||
siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String
|
|
||||||
) throws -> IncomingSite? {
|
|
||||||
let res: MobileNebulaTryUpdateResult
|
|
||||||
do {
|
|
||||||
res = try apiClient.tryUpdate(
|
|
||||||
siteName,
|
|
||||||
hostID: hostID,
|
|
||||||
privateKey: privateKey,
|
|
||||||
counter: counter,
|
|
||||||
trustedKeys: trustedKeys)
|
|
||||||
} catch {
|
|
||||||
// type information from Go is not available, use string matching instead
|
|
||||||
if error.localizedDescription == "invalid credentials" {
|
|
||||||
throw APIClientError.invalidCredentials
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.fetchedUpdate {
|
func enroll(code: String) throws -> IncomingSite {
|
||||||
return try decodeIncomingSite(jsonSite: res.site)
|
let res = try apiClient.enroll(code)
|
||||||
|
return try decodeIncomingSite(jsonSite: res.site)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
func tryUpdate(siteName: String, hostID: String, privateKey: String, counter: Int, trustedKeys: String) throws -> IncomingSite? {
|
||||||
}
|
let res: MobileNebulaTryUpdateResult
|
||||||
|
do {
|
||||||
private func decodeIncomingSite(jsonSite: String) throws -> IncomingSite {
|
res = try apiClient.tryUpdate(
|
||||||
do {
|
siteName,
|
||||||
return try json.decode(IncomingSite.self, from: jsonSite.data(using: .utf8)!)
|
hostID: hostID,
|
||||||
} catch {
|
privateKey: privateKey,
|
||||||
print("decodeIncomingSite: \(error)")
|
counter: counter,
|
||||||
throw error
|
trustedKeys: trustedKeys)
|
||||||
|
} catch {
|
||||||
|
// type information from Go is not available, use string matching instead
|
||||||
|
if (error.localizedDescription == "invalid credentials") {
|
||||||
|
throw APIClientError.invalidCredentials
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.fetchedUpdate) {
|
||||||
|
return try decodeIncomingSite(jsonSite: res.site)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func decodeIncomingSite(jsonSite: String) throws -> IncomingSite {
|
||||||
|
do {
|
||||||
|
return try json.decode(IncomingSite.self, from: jsonSite.data(using: .utf8)!)
|
||||||
|
} catch {
|
||||||
|
print("decodeIncomingSite: \(error)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,334 +1,297 @@
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
func MissingArgumentError(message: String, details: Any?) -> FlutterError {
|
||||||
return FlutterError(code: "missing_argument", message: message, details: details)
|
return FlutterError(code: "missing_argument", message: message, details: details)
|
||||||
}
|
}
|
||||||
|
|
||||||
@main
|
@main
|
||||||
@objc class AppDelegate: FlutterAppDelegate {
|
@objc class AppDelegate: FlutterAppDelegate {
|
||||||
private let dnUpdater = DNUpdater()
|
private let dnUpdater = DNUpdater()
|
||||||
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(
|
|
||||||
_ application: UIApplication,
|
override func application(
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
_ application: UIApplication,
|
||||||
) -> Bool {
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
) -> Bool {
|
||||||
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
dnUpdater.updateAllLoop { site in
|
|
||||||
// Signal the site has changed in case the current site details screen is active
|
|
||||||
let container = self.sites?.getContainer(id: site.id)
|
dnUpdater.updateAllLoop { site in
|
||||||
if container != nil {
|
// Signal the site has changed in case the current site details screen is active
|
||||||
// Update references to the site with the new site config
|
let container = self.sites?.getContainer(id: site.id)
|
||||||
container!.site = site
|
if (container != nil) {
|
||||||
container!.updater.update(connected: site.connected ?? false, replaceSite: site)
|
// Update references to the site with the new site config
|
||||||
}
|
container!.site = site
|
||||||
|
container!.updater.update(connected: site.connected ?? false, replaceSite: site)
|
||||||
// Signal to the main screen to reload
|
|
||||||
self.ui?.invokeMethod("refreshSites", arguments: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
|
||||||
fatalError("rootViewController is not type FlutterViewController")
|
|
||||||
}
|
|
||||||
|
|
||||||
sites = Sites(messenger: controller.binaryMessenger)
|
|
||||||
ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger)
|
|
||||||
|
|
||||||
ui!.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
|
||||||
switch call.method {
|
|
||||||
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
|
|
||||||
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
|
|
||||||
case "nebula.renderConfig": return self.nebulaRenderConfig(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 "listSites": return self.listSites(result: result)
|
|
||||||
case "deleteSite": return self.deleteSite(call: call, result: result)
|
|
||||||
case "saveSite": return self.saveSite(call: call, result: result)
|
|
||||||
case "startSite": return self.startSite(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.listPendingHostmap":
|
|
||||||
self.vpnRequest(command: "listPendingHostmap", 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.closeTunnel":
|
|
||||||
self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
|
|
||||||
|
|
||||||
default:
|
|
||||||
result(FlutterMethodNotImplemented)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var err: NSError?
|
|
||||||
let json = MobileNebulaParseCerts(certs, &err)
|
|
||||||
if err != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(
|
|
||||||
message: "Error while parsing certificate(s)", details: err!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
return result(json)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
|
|
||||||
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 key = args["key"] else {
|
|
||||||
return result(MissingArgumentError(message: "key is a required argument"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var err: NSError?
|
|
||||||
var validd: ObjCBool = false
|
|
||||||
let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err)
|
|
||||||
if err != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(
|
|
||||||
message: "Error while verifying certificate and private key",
|
|
||||||
details: err!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
return result(valid)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nebulaGenerateKeyPair(result: FlutterResult) {
|
|
||||||
var err: NSError?
|
|
||||||
let kp = MobileNebulaGenerateKeyPair(&err)
|
|
||||||
if err != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(
|
|
||||||
message: "Error while generating key pairs", details: err!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
return result(kp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nebulaRenderConfig(call: FlutterMethodCall, result: FlutterResult) {
|
|
||||||
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
|
|
||||||
|
|
||||||
var err: NSError?
|
|
||||||
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
|
|
||||||
if err != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(message: "Error while rendering config", details: err!.localizedDescription)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result(yaml)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnEnroll(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let code = call.arguments as? String else { return result(NoArgumentsError()) }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let site = try apiClient.enroll(code: code)
|
|
||||||
|
|
||||||
let oldSite = self.sites?.getSite(id: site.id)
|
|
||||||
site.save(manager: oldSite?.manager) { error in
|
|
||||||
if error != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return result(
|
|
||||||
CallFailedError(message: "Error from DN api", details: error.localizedDescription))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func listSites(result: @escaping FlutterResult) {
|
|
||||||
self.sites?.loadSites { (sites, err) -> Void in
|
|
||||||
if err != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
let data = try! encoder.encode(sites)
|
|
||||||
let ret = String(data: data, encoding: .utf8)
|
|
||||||
result(ret)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let id = call.arguments as? String else { return result(NoArgumentsError()) }
|
|
||||||
//TODO: stop the site if its running currently
|
|
||||||
self.sites?.deleteSite(id: id) { error in
|
|
||||||
if error != nil {
|
|
||||||
result(
|
|
||||||
CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let json = call.arguments as? String 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 {
|
|
||||||
return result(NoArgumentsError())
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldSite = self.sites?.getSite(id: site.id)
|
|
||||||
site.save(manager: oldSite?.manager) { error in
|
|
||||||
if error != nil {
|
|
||||||
return result(
|
|
||||||
CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sites?.loadSites { _, _ in
|
|
||||||
result(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
|
|
||||||
#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
|
|
||||||
//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 = { () -> Void in
|
|
||||||
return 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))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Signal to the main screen to reload
|
||||||
|
self.ui?.invokeMethod("refreshSites", arguments: nil)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#endif
|
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||||
}
|
fatalError("rootViewController is not type FlutterViewController")
|
||||||
|
}
|
||||||
func stopSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
||||||
guard let args = call.arguments as? [String: String] else { return result(NoArgumentsError()) }
|
sites = Sites(messenger: controller.binaryMessenger)
|
||||||
guard let id = args["id"] else {
|
ui = FlutterMethodChannel(name: ChannelName.vpn, binaryMessenger: controller.binaryMessenger)
|
||||||
return result(MissingArgumentError(message: "id is a required argument"))
|
|
||||||
|
ui!.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
||||||
|
switch call.method {
|
||||||
|
case "nebula.parseCerts": return self.nebulaParseCerts(call: call, result: result)
|
||||||
|
case "nebula.generateKeyPair": return self.nebulaGenerateKeyPair(result: result)
|
||||||
|
case "nebula.renderConfig": return self.nebulaRenderConfig(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 "listSites": return self.listSites(result: result)
|
||||||
|
case "deleteSite": return self.deleteSite(call: call, result: result)
|
||||||
|
case "saveSite": return self.saveSite(call: call, result: result)
|
||||||
|
case "startSite": return self.startSite(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.listPendingHostmap": self.vpnRequest(command: "listPendingHostmap", 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.closeTunnel": self.vpnRequest(command: "closeTunnel", arguments: call.arguments, result: result)
|
||||||
|
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
#if targetEnvironment(simulator)
|
|
||||||
let updater = self.sites?.getUpdater(id: id)
|
func nebulaParseCerts(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
updater?.update(connected: false)
|
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||||
|
guard let certs = args["certs"] else { return result(MissingArgumentError(message: "certs is a required argument")) }
|
||||||
#else
|
|
||||||
let manager = self.sites?.getSite(id: id)?.manager
|
var err: NSError?
|
||||||
manager?.loadFromPreferences { error in
|
let json = MobileNebulaParseCerts(certs, &err)
|
||||||
//TODO: Handle load error
|
if (err != nil) {
|
||||||
|
return result(CallFailedError(message: "Error while parsing certificate(s)", details: err!.localizedDescription))
|
||||||
manager?.connection.stopVPNTunnel()
|
}
|
||||||
return result(nil)
|
|
||||||
}
|
return result(json)
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) {
|
|
||||||
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"))
|
|
||||||
}
|
}
|
||||||
let container = sites?.getContainer(id: id)
|
|
||||||
|
func nebulaVerifyCertAndKey(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
if container == nil {
|
guard let args = call.arguments as? Dictionary<String, String> else { return result(NoArgumentsError()) }
|
||||||
// No site for this id
|
guard let cert = args["cert"] else { return result(MissingArgumentError(message: "cert is a required argument")) }
|
||||||
return result(nil)
|
guard let key = args["key"] else { return result(MissingArgumentError(message: "key is a required argument")) }
|
||||||
|
|
||||||
|
var err: NSError?
|
||||||
|
var validd: ObjCBool = false
|
||||||
|
let valid = MobileNebulaVerifyCertAndKey(cert, key, &validd, &err)
|
||||||
|
if (err != nil) {
|
||||||
|
return result(CallFailedError(message: "Error while verifying certificate and private key", details: err!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result(valid)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(container!.site.connected ?? false) {
|
func nebulaGenerateKeyPair(result: FlutterResult) {
|
||||||
// Site isn't connected, no point in sending a command
|
var err: NSError?
|
||||||
return result(nil)
|
let kp = MobileNebulaGenerateKeyPair(&err)
|
||||||
|
if (err != nil) {
|
||||||
|
return result(CallFailedError(message: "Error while generating key pairs", details: err!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result(kp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func nebulaRenderConfig(call: FlutterMethodCall, result: FlutterResult) {
|
||||||
|
guard let config = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
|
|
||||||
|
var err: NSError?
|
||||||
|
let yaml = MobileNebulaRenderConfig(config, "<hidden>", &err)
|
||||||
|
if (err != nil) {
|
||||||
|
return result(CallFailedError(message: "Error while rendering config", details: err!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result(yaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dnEnroll(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let code = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
|
|
||||||
|
do {
|
||||||
|
let site = try apiClient.enroll(code: code)
|
||||||
|
|
||||||
|
let oldSite = self.sites?.getSite(id: site.id)
|
||||||
|
site.save(manager: oldSite?.manager) { error in
|
||||||
|
if (error != nil) {
|
||||||
|
return result(CallFailedError(message: "Failed to enroll", details: error!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return result(CallFailedError(message: "Error from DN api", details: error.localizedDescription))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listSites(result: @escaping FlutterResult) {
|
||||||
|
self.sites?.loadSites { (sites, err) -> () in
|
||||||
|
if (err != nil) {
|
||||||
|
return result(CallFailedError(message: "Failed to load site list", details: err!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
if let session = container!.site.manager?.connection as? NETunnelProviderSession {
|
let encoder = JSONEncoder()
|
||||||
do {
|
let data = try! encoder.encode(sites)
|
||||||
try session.sendProviderMessage(
|
let ret = String(data: data, encoding: .utf8)
|
||||||
try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))
|
result(ret)
|
||||||
) { data in
|
}
|
||||||
if data == nil {
|
}
|
||||||
|
|
||||||
|
func deleteSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let id = call.arguments as? String else { return result(NoArgumentsError()) }
|
||||||
|
//TODO: stop the site if its running currently
|
||||||
|
self.sites?.deleteSite(id: id) { error in
|
||||||
|
if (error != nil) {
|
||||||
|
result(CallFailedError(message: "Failed to delete site", details: error!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveSite(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
guard let json = call.arguments as? String 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 {
|
||||||
|
return result(NoArgumentsError())
|
||||||
|
}
|
||||||
|
|
||||||
|
let oldSite = self.sites?.getSite(id: site.id)
|
||||||
|
site.save(manager: oldSite?.manager) { error in
|
||||||
|
if (error != nil) {
|
||||||
|
return result(CallFailedError(message: "Failed to save site", details: error!.localizedDescription))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.sites?.loadSites { _, _ in
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
//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 = {() -> Void in
|
||||||
|
return 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopSite(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: false)
|
||||||
|
|
||||||
|
#else
|
||||||
|
let manager = self.sites?.getSite(id: id)?.manager
|
||||||
|
manager?.loadFromPreferences{ error in
|
||||||
|
//TODO: Handle load error
|
||||||
|
|
||||||
|
manager?.connection.stopVPNTunnel()
|
||||||
return result(nil)
|
return result(nil)
|
||||||
}
|
|
||||||
|
|
||||||
//print(String(decoding: data!, as: UTF8.self))
|
|
||||||
guard let res = try? JSONDecoder().decode(IPCResponse.self, from: data!) else {
|
|
||||||
return result(CallFailedError(message: "Failed to decode response"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.type == .success {
|
|
||||||
return result(res.message?.object)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result(
|
|
||||||
CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
|
|
||||||
}
|
}
|
||||||
} catch {
|
#endif
|
||||||
return result(CallFailedError(message: error.localizedDescription))
|
}
|
||||||
}
|
|
||||||
} else {
|
func vpnRequest(command: String, arguments: Any?, result: @escaping FlutterResult) {
|
||||||
//TODO: we have a site without a manager, things have gone weird. How to handle since this shouldn't happen?
|
guard let args = arguments as? Dictionary<String, Any> else { return result(NoArgumentsError()) }
|
||||||
result(nil)
|
guard let id = args["id"] as? String else { return result(MissingArgumentError(message: "id is a required argument")) }
|
||||||
|
let container = sites?.getContainer(id: id)
|
||||||
|
|
||||||
|
if container == nil {
|
||||||
|
// No site for this id
|
||||||
|
return result(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(container!.site.connected ?? false) {
|
||||||
|
// Site isn't connected, no point in sending a command
|
||||||
|
return result(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let session = container!.site.manager?.connection as? NETunnelProviderSession {
|
||||||
|
do {
|
||||||
|
try session.sendProviderMessage(try JSONEncoder().encode(IPCRequest(command: command, arguments: JSON(args)))) { data in
|
||||||
|
if data == nil {
|
||||||
|
return result(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
//print(String(decoding: data!, as: UTF8.self))
|
||||||
|
guard let res = try? JSONDecoder().decode(IPCResponse.self, from: data!) else {
|
||||||
|
return result(CallFailedError(message: "Failed to decode response"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.type == .success {
|
||||||
|
return result(res.message?.object)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result(CallFailedError(message: res.message?.debugDescription ?? "Failed to convert error"))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return result(CallFailedError(message: error.localizedDescription))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//TODO: we have a site without a manager, things have gone weird. How to handle since this shouldn't happen?
|
||||||
|
result(nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MissingArgumentError(message: String, details: Error? = nil) -> FlutterError {
|
func MissingArgumentError(message: String, details: Error? = nil) -> FlutterError {
|
||||||
return FlutterError(code: "missingArgument", message: message, details: details)
|
return FlutterError(code: "missingArgument", message: message, details: details)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NoArgumentsError(
|
func NoArgumentsError(message: String? = "no arguments were provided or could not be deserialized", details: Error? = nil) -> FlutterError {
|
||||||
message: String? = "no arguments were provided or could not be deserialized",
|
return FlutterError(code: "noArguments", message: message, details: details)
|
||||||
details: Error? = nil
|
|
||||||
) -> FlutterError {
|
|
||||||
return FlutterError(code: "noArguments", message: message, details: details)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CallFailedError(message: String, details: String? = "") -> FlutterError {
|
func CallFailedError(message: String, details: String? = "") -> FlutterError {
|
||||||
return FlutterError(code: "callFailed", message: message, details: details)
|
return FlutterError(code: "callFailed", message: message, details: details)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,146 +2,141 @@ import Foundation
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
class DNUpdater {
|
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) -> ()) {
|
||||||
|
_ = SiteList{ (sites, _) -> () in
|
||||||
|
// 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.
|
||||||
|
Task.detached(priority: .userInitiated) {
|
||||||
|
sites?.values.forEach { site in
|
||||||
|
if (site.connected == true) {
|
||||||
|
// The vpn service is in charge of updating the currently connected site
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func updateAll(onUpdate: @escaping (Site) -> Void) {
|
self.updateSite(site: site, onUpdate: onUpdate)
|
||||||
_ = SiteList { (sites, _) -> Void in
|
}
|
||||||
// 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.
|
|
||||||
Task.detached(priority: .userInitiated) {
|
|
||||||
sites?.values.forEach { site in
|
|
||||||
if site.connected == true {
|
|
||||||
// The vpn service is in charge of updating the currently connected site
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.updateSite(site: site, onUpdate: onUpdate)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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(
|
siteName: site.name,
|
||||||
siteName: site.name,
|
hostID: credentials.hostID,
|
||||||
hostID: credentials.hostID,
|
privateKey: credentials.privateKey,
|
||||||
privateKey: credentials.privateKey,
|
counter: credentials.counter,
|
||||||
counter: credentials.counter,
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
let siteManager = site.manager
|
||||||
|
let shouldSaveToManager = siteManager != nil || ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
|
||||||
|
|
||||||
|
newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in
|
||||||
|
if (error != nil) {
|
||||||
|
self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload nebula even if we couldn't save the vpn profile
|
||||||
|
onUpdate(Site(incoming: newSite!))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentials.invalid) {
|
||||||
|
try site.validateDNCredentials()
|
||||||
|
log.notice("Revalidated credentials in site \(site.name, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
log.error("Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let siteManager = site.manager
|
|
||||||
let shouldSaveToManager =
|
|
||||||
siteManager != nil
|
|
||||||
|| ProcessInfo().isOperatingSystemAtLeast(
|
|
||||||
OperatingSystemVersion(majorVersion: 17, minorVersion: 0, patchVersion: 0))
|
|
||||||
|
|
||||||
newSite?.save(manager: site.manager, saveToManager: shouldSaveToManager) { error in
|
|
||||||
if error != nil {
|
|
||||||
self.log.error("failed to save update: \(error!.localizedDescription, privacy: .public)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// reload nebula even if we couldn't save the vpn profile
|
|
||||||
onUpdate(Site(incoming: newSite!))
|
|
||||||
}
|
|
||||||
|
|
||||||
if credentials.invalid {
|
|
||||||
try site.validateDNCredentials()
|
|
||||||
log.notice("Revalidated credentials in site \(site.name, privacy: .public)")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
log.error(
|
|
||||||
"Error while updating \(site.name, privacy: .public): \(error.localizedDescription, privacy: .public)"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
self.timeInterval = timeInterval
|
self.timeInterval = timeInterval
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var timer: DispatchSourceTimer = {
|
private lazy var timer: DispatchSourceTimer = {
|
||||||
let t = DispatchSource.makeTimerSource()
|
let t = DispatchSource.makeTimerSource()
|
||||||
t.schedule(deadline: .now(), repeating: self.timeInterval)
|
t.schedule(deadline: .now(), repeating: self.timeInterval)
|
||||||
t.setEventHandler(handler: { [weak self] in
|
t.setEventHandler(handler: { [weak self] in
|
||||||
self?.eventHandler?()
|
self?.eventHandler?()
|
||||||
})
|
})
|
||||||
return t
|
return t
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var eventHandler: (() -> Void)?
|
var eventHandler: (() -> Void)?
|
||||||
|
|
||||||
private enum State {
|
private enum State {
|
||||||
case suspended
|
case suspended
|
||||||
case resumed
|
case resumed
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state: State = .suspended
|
private var state: State = .suspended
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
timer.setEventHandler {}
|
timer.setEventHandler {}
|
||||||
timer.cancel()
|
timer.cancel()
|
||||||
/*
|
/*
|
||||||
If the timer is suspended, calling cancel without resuming
|
If the timer is suspended, calling cancel without resuming
|
||||||
triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
|
triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
|
||||||
*/
|
*/
|
||||||
resume()
|
resume()
|
||||||
eventHandler = nil
|
eventHandler = nil
|
||||||
}
|
|
||||||
|
|
||||||
func resume() {
|
|
||||||
if state == .resumed {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
state = .resumed
|
|
||||||
timer.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func suspend() {
|
func resume() {
|
||||||
if state == .suspended {
|
if state == .resumed {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
state = .resumed
|
||||||
|
timer.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func suspend() {
|
||||||
|
if state == .suspended {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state = .suspended
|
||||||
|
timer.suspend()
|
||||||
}
|
}
|
||||||
state = .suspended
|
|
||||||
timer.suspend()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class PackageInfo {
|
class PackageInfo {
|
||||||
func getVersion() -> String {
|
func getVersion() -> String {
|
||||||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
|
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ??
|
||||||
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
"unknown"
|
||||||
|
let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||||
if buildNumber == nil {
|
|
||||||
return version
|
if (buildNumber == nil) {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
return "\(version)-\(buildNumber!)"
|
||||||
}
|
}
|
||||||
|
|
||||||
return "\(version)-\(buildNumber!)"
|
func getName() -> String {
|
||||||
}
|
return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ??
|
||||||
|
Bundle.main.infoDictionary?["CFBundleName"] as? String ??
|
||||||
func getName() -> String {
|
"Nebula"
|
||||||
return Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? Bundle.main
|
}
|
||||||
.infoDictionary?["CFBundleName"] as? String ?? "Nebula"
|
|
||||||
}
|
func getSystemVersion() -> String {
|
||||||
|
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
||||||
func getSystemVersion() -> String {
|
return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
|
||||||
let osVersion = ProcessInfo.processInfo.operatingSystemVersion
|
}
|
||||||
return "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,192 +1,185 @@
|
||||||
import MobileNebula
|
|
||||||
import NetworkExtension
|
import NetworkExtension
|
||||||
|
import MobileNebula
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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?) {
|
||||||
|
self.messenger = messenger
|
||||||
|
}
|
||||||
|
|
||||||
init(messenger: FlutterBinaryMessenger?) {
|
func loadSites(completion: @escaping ([String: Site]?, Error?) -> ()) {
|
||||||
self.messenger = messenger
|
_ = SiteList { (sites, err) in
|
||||||
}
|
if (err != nil) {
|
||||||
|
return completion(nil, err)
|
||||||
func loadSites(completion: @escaping ([String: Site]?, Error?) -> Void) {
|
}
|
||||||
_ = SiteList { (sites, err) in
|
|
||||||
if err != nil {
|
sites?.values.forEach{ site in
|
||||||
return completion(nil, err)
|
var updater = self.containers[site.id]?.updater
|
||||||
}
|
if (updater != nil) {
|
||||||
|
updater!.setSite(site: site)
|
||||||
sites?.values.forEach { site in
|
} else {
|
||||||
var updater = self.containers[site.id]?.updater
|
updater = SiteUpdater(messenger: self.messenger!, site: site)
|
||||||
if updater != nil {
|
}
|
||||||
updater!.setSite(site: site)
|
self.containers[site.id] = SiteContainer(site: site, updater: updater!)
|
||||||
} else {
|
}
|
||||||
updater = SiteUpdater(messenger: self.messenger!, site: site)
|
|
||||||
|
let justSites = self.containers.mapValues {
|
||||||
|
return $0.site
|
||||||
|
}
|
||||||
|
completion(justSites, nil)
|
||||||
}
|
}
|
||||||
self.containers[site.id] = SiteContainer(site: site, updater: updater!)
|
|
||||||
}
|
|
||||||
|
|
||||||
let justSites = self.containers.mapValues {
|
|
||||||
return $0.site
|
|
||||||
}
|
|
||||||
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 = self.containers.removeValue(forKey: id) {
|
_ = KeyChain.delete(key: "\(site.site.id).dnCredentials")
|
||||||
_ = KeyChain.delete(key: "\(site.site.id).dnCredentials")
|
_ = KeyChain.delete(key: "\(site.site.id).key")
|
||||||
_ = KeyChain.delete(key: "\(site.site.id).key")
|
|
||||||
|
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)
|
try fileManager.removeItem(at: siteDir)
|
||||||
try fileManager.removeItem(at: siteDir)
|
} 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
|
||||||
|
callback(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSite(id: String) -> Site? {
|
||||||
|
return self.containers[id]?.site
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUpdater(id: String) -> SiteUpdater? {
|
||||||
|
return self.containers[id]?.updater
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContainer(id: String) -> SiteContainer? {
|
||||||
|
return self.containers[id]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing to remove
|
|
||||||
callback(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSite(id: String) -> Site? {
|
|
||||||
return self.containers[id]?.site
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUpdater(id: String) -> SiteUpdater? {
|
|
||||||
return self.containers[id]?.updater
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContainer(id: String) -> SiteContainer? {
|
|
||||||
return self.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)
|
self.configFd = open(configPath.path, O_EVTONLY)
|
||||||
self.configObserver = DispatchSource.makeFileSystemObjectSource(
|
self.configObserver = DispatchSource.makeFileSystemObjectSource(
|
||||||
fileDescriptor: self.configFd!,
|
fileDescriptor: self.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
|
self.configObserver = nil
|
||||||
}
|
|
||||||
|
|
||||||
eventChannel = FlutterEventChannel(
|
|
||||||
name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
|
|
||||||
self.site = site
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
eventChannel.setStreamHandler(self)
|
|
||||||
|
|
||||||
self.configObserver?.setEventHandler(handler: self.configUpdated)
|
|
||||||
self.configObserver?.setCancelHandler {
|
|
||||||
if self.configFd != nil {
|
|
||||||
close(self.configFd!)
|
|
||||||
}
|
|
||||||
self.configObserver = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
self.configObserver?.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setSite(site: Site) {
|
|
||||||
self.site = site
|
|
||||||
}
|
|
||||||
|
|
||||||
/// onListen is called when flutter code attaches an event listener
|
|
||||||
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink)
|
|
||||||
-> FlutterError?
|
|
||||||
{
|
|
||||||
eventSink = events
|
|
||||||
|
|
||||||
#if !targetEnvironment(simulator)
|
|
||||||
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
|
|
||||||
// and a another listen should occur and succeed.
|
|
||||||
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!)
|
eventChannel = FlutterEventChannel(name: "net.defined.nebula/\(site.id)", binaryMessenger: messenger)
|
||||||
}
|
self.site = site
|
||||||
#endif
|
super.init()
|
||||||
return nil
|
|
||||||
}
|
eventChannel.setStreamHandler(self)
|
||||||
|
|
||||||
/// onCancel is called when the flutter listener stops listening
|
self.configObserver?.setEventHandler(handler: self.configUpdated)
|
||||||
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
self.configObserver?.setCancelHandler {
|
||||||
if self.notification != nil {
|
if self.configFd != nil {
|
||||||
NotificationCenter.default.removeObserver(self.notification!)
|
close(self.configFd!)
|
||||||
|
}
|
||||||
|
self.configObserver = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.configObserver?.resume()
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
func setSite(site: Site) {
|
||||||
|
self.site = site
|
||||||
/// 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) {
|
|
||||||
if replaceSite != nil {
|
|
||||||
site = replaceSite!
|
|
||||||
}
|
|
||||||
site.connected = connected
|
|
||||||
site.status = connected ? "Connected" : "Disconnected"
|
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
|
||||||
let data = try! encoder.encode(site)
|
|
||||||
self.eventSink?(String(data: data, encoding: .utf8))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func configUpdated() {
|
|
||||||
if self.site.connected != true {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let newSite = try? Site(manager: self.site.manager!) else {
|
/// onListen is called when flutter code attaches an event listener
|
||||||
return
|
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
|
||||||
|
eventSink = events;
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
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
|
||||||
|
// and a another listen should occur and succeed.
|
||||||
|
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!)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// onCancel is called when the flutter listener stops listening
|
||||||
|
func onCancel(withArguments arguments: Any?) -> FlutterError? {
|
||||||
|
if (self.notification != nil) {
|
||||||
|
NotificationCenter.default.removeObserver(self.notification!)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
if (replaceSite != nil) {
|
||||||
|
site = replaceSite!
|
||||||
|
}
|
||||||
|
site.connected = connected
|
||||||
|
site.status = connected ? "Connected" : "Disconnected"
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
let data = try! encoder.encode(site)
|
||||||
|
self.eventSink?(String(data: data, encoding: .utf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configUpdated() {
|
||||||
|
if self.site.connected != true {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let newSite = try? Site(manager: self.site.manager!) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update(connected: newSite.connected ?? false, replaceSite: newSite)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update(connected: newSite.connected ?? false, replaceSite: newSite)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue