mobile_nebula/ios/NebulaNetworkExtension/Site.swift

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

508 lines
15 KiB
Swift
Raw Normal View History

2020-07-27 20:43:58 +00:00
import NetworkExtension
import MobileNebula
2021-04-27 15:29:28 +00:00
import SwiftyJSON
2024-10-14 20:16:02 +00:00
import os.log
var log = Logger(subsystem: "net.defined.mobileNebula", category: "Site")
2020-07-27 20:43:58 +00:00
extension String: Error {}
2021-04-27 15:29:28 +00:00
enum IPCResponseType: String, Codable {
case error = "error"
case success = "success"
}
2020-07-27 20:43:58 +00:00
2021-04-27 15:29:28 +00:00
class IPCResponse: Codable {
var type: IPCResponseType
//TODO: change message to data?
var message: JSON?
2021-04-27 15:29:28 +00:00
init(type: IPCResponseType, message: JSON?) {
2020-07-27 20:43:58 +00:00
self.type = type
self.message = message
}
}
2021-04-27 15:29:28 +00:00
class IPCRequest: Codable {
var command: String
var arguments: JSON?
2021-04-27 15:29:28 +00:00
init(command: String, arguments: JSON?) {
self.command = command
2020-07-27 20:43:58 +00:00
self.arguments = arguments
}
2021-04-27 15:29:28 +00:00
init(command: String) {
self.command = command
2020-07-27 20:43:58 +00:00
}
}
struct CertificateInfo: Codable {
var cert: Certificate
var rawCert: String
var validity: CertificateValidity
2020-07-27 20:43:58 +00:00
enum CodingKeys: String, CodingKey {
case cert = "Cert"
case rawCert = "RawCert"
case validity = "Validity"
}
}
struct Certificate: Codable {
var fingerprint: String
var signature: String
var details: CertificateDetails
2022-11-22 16:20:35 +00:00
/// An empty initializer to make error reporting easier
2020-07-27 20:43:58 +00:00
init() {
fingerprint = ""
signature = ""
details = CertificateDetails()
}
}
struct CertificateDetails: Codable {
var name: String
var notBefore: String
var notAfter: String
var publicKey: String
var groups: [String]
var ips: [String]
var subnets: [String]
var isCa: Bool
var issuer: String
2022-11-22 16:20:35 +00:00
/// An empty initializer to make error reporting easier
2020-07-27 20:43:58 +00:00
init() {
name = ""
notBefore = ""
notAfter = ""
publicKey = ""
groups = []
ips = ["ERROR"]
subnets = []
isCa = false
issuer = ""
}
}
struct CertificateValidity: Codable {
var valid: Bool
var reason: String
2020-07-27 20:43:58 +00:00
enum CodingKeys: String, CodingKey {
case valid = "Valid"
case reason = "Reason"
}
}
let statusMap: Dictionary<NEVPNStatus, Bool> = [
NEVPNStatus.invalid: false,
NEVPNStatus.disconnected: false,
NEVPNStatus.connecting: false,
2020-07-27 20:43:58 +00:00
NEVPNStatus.connected: true,
NEVPNStatus.reasserting: true,
NEVPNStatus.disconnecting: true,
]
let statusString: Dictionary<NEVPNStatus, String> = [
NEVPNStatus.invalid: "Invalid configuration",
NEVPNStatus.disconnected: "Disconnected",
NEVPNStatus.connecting: "Connecting...",
NEVPNStatus.connected: "Connected",
NEVPNStatus.reasserting: "Reasserting...",
NEVPNStatus.disconnecting: "Disconnecting...",
]
// Represents a site that was pulled out of the system configuration
2021-04-27 15:29:28 +00:00
class Site: Codable {
2020-07-27 20:43:58 +00:00
// Stored in manager
var name: String
var id: String
2020-07-27 20:43:58 +00:00
// Stored in proto
var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]
var cert: CertificateInfo?
var ca: [CertificateInfo]
var lhDuration: Int
var port: Int
var mtu: Int
var cipher: String
var sortKey: Int
var logVerbosity: String
2021-04-27 15:29:28 +00:00
var connected: Bool? //TODO: active is a better name
2020-07-27 20:43:58 +00:00
var status: String?
var logFile: String?
var managed: Bool
// The following fields are present if managed = true
var lastManagedUpdate: 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
var needsToMigrateToFS: Bool = false
2020-07-27 20:43:58 +00:00
// A list of error encountered when trying to rehydrate a site from config
var errors: [String]
2021-04-27 15:29:28 +00:00
var manager: NETunnelProviderManager?
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
2021-04-27 15:29:28 +00:00
convenience init(manager: NETunnelProviderManager) throws {
2020-07-27 20:43:58 +00:00
//TODO: Throw an error and have Sites delete the site, notify the user instead of using !
let proto = manager.protocolConfiguration as! NETunnelProviderProtocol
try self.init(proto: proto)
self.manager = manager
self.connected = statusMap[manager.connection.status]
self.status = statusString[manager.connection.status]
}
2021-04-27 15:29:28 +00:00
convenience init(proto: NETunnelProviderProtocol) throws {
2020-07-27 20:43:58 +00:00
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
if id == nil {
throw("Non-conforming site \(String(describing: dict))")
}
try self.init(path: SiteList.getSiteConfigFile(id: id!, createDir: false))
}
/// 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
convenience init(path: URL) throws {
let config = try Data(contentsOf: path)
2020-07-27 20:43:58 +00:00
let decoder = JSONDecoder()
let incoming = try decoder.decode(IncomingSite.self, from: config)
2020-07-27 20:43:58 +00:00
self.init(incoming: incoming)
}
2020-07-27 20:43:58 +00:00
init(incoming: IncomingSite) {
var err: NSError?
incomingSite = incoming
2020-07-27 20:43:58 +00:00
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
2020-07-27 20:43:58 +00:00
do {
let rawCert = incoming.cert
let rawDetails = MobileNebulaParseCerts(rawCert, &err)
if (err != nil) {
throw err!
}
2020-07-27 20:43:58 +00:00
var certs: [CertificateInfo]
2020-07-27 20:43:58 +00:00
certs = try JSONDecoder().decode([CertificateInfo].self, from: rawDetails.data(using: .utf8)!)
if (certs.count == 0) {
throw "No certificate found"
}
cert = certs[0]
if (!cert!.validity.valid) {
errors.append("Certificate is invalid: \(cert!.validity.reason)")
}
2020-07-27 20:43:58 +00:00
} catch {
errors.append("Error while loading certificate: \(error.localizedDescription)")
}
2020-07-27 20:43:58 +00:00
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)!)
2020-07-27 20:43:58 +00:00
var hasErrors = false
ca.forEach { cert in
if (!cert.validity.valid) {
hasErrors = true
}
}
if (hasErrors && !managed) {
2020-07-27 20:43:58 +00:00
errors.append("There are issues with 1 or more ca certificates")
}
2020-07-27 20:43:58 +00:00
} 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)")
}
}
2020-07-27 20:43:58 +00:00
}
2020-07-27 20:43:58 +00:00
// Gets the private key from the keystore, we don't always need it in memory
func getKey() throws -> String {
guard let keyData = KeyChain.load(key: "\(id).key") else {
throw "failed to get key from keychain"
2020-07-27 20:43:58 +00:00
}
//TODO: make sure this is valid on return!
return String(decoding: keyData, as: UTF8.self)
}
func getDNCredentials() throws -> DNCredentials {
if (!managed) {
throw "unmanaged site has no dn credentials"
}
let rawDNCredentials = KeyChain.load(key: "\(id).dnCredentials")
if rawDNCredentials == nil {
throw "failed to find dn credentials in keychain"
}
let decoder = JSONDecoder()
return try decoder.decode(DNCredentials.self, from: rawDNCredentials!)
}
func invalidateDNCredentials() throws {
let creds = try getDNCredentials()
creds.invalid = true
if (!(try creds.save(siteID: self.id))) {
throw "failed to store dn credentials in keychain"
}
}
func validateDNCredentials() throws {
let creds = try getDNCredentials()
creds.invalid = false
if (!(try creds.save(siteID: self.id))) {
throw "failed to store dn credentials in keychain"
}
}
func getConfig() throws -> Data {
return try self.incomingSite!.getConfig()
}
2020-07-27 20:43:58 +00:00
// Limits what we export to the UI
private enum CodingKeys: String, CodingKey {
case name
case id
case staticHostmap
case cert
case ca
case lhDuration
case port
case cipher
case sortKey
case connected
case status
case logFile
case unsafeRoutes
case logVerbosity
case errors
case mtu
case managed
case lastManagedUpdate
case rawConfig
2020-07-27 20:43:58 +00:00
}
}
class StaticHosts: Codable {
var lighthouse: Bool
var destinations: [String]
}
class UnsafeRoute: Codable {
var route: String
var via: String
var mtu: Int?
}
class DNCredentials: Codable {
var hostID: String
var privateKey: String
var counter: Int
var trustedKeys: String
var invalid: Bool {
get { return _invalid ?? false }
set { _invalid = newValue }
}
private var _invalid: Bool?
func save(siteID: String) throws -> Bool {
let encoder = JSONEncoder()
let rawDNCredentials = try encoder.encode(self)
return KeyChain.save(key: "\(siteID).dnCredentials", data: rawDNCredentials, managed: true)
}
enum CodingKeys: String, CodingKey {
case hostID
case privateKey
case counter
case trustedKeys
case _invalid = "invalid"
}
}
2020-07-27 20:43:58 +00:00
// This class represents a site coming in from flutter, meant only to be saved and re-loaded as a proper Site
struct IncomingSite: Codable {
var name: String
var id: String
var staticHostmap: Dictionary<String, StaticHosts>
var unsafeRoutes: [UnsafeRoute]?
var cert: String
var ca: String
var lhDuration: Int
var port: Int
var mtu: Int?
var cipher: String
var sortKey: Int?
var logVerbosity: String?
var key: String?
var managed: Bool?
// The following fields are present if managed = true
var dnCredentials: DNCredentials?
var lastManagedUpdate: String?
var rawConfig: String?
func getConfig() throws -> Data {
2020-07-27 20:43:58 +00:00
let encoder = JSONEncoder()
var config = self
config.key = nil
config.dnCredentials = nil
return try encoder.encode(config)
}
2020-07-27 20:43:58 +00:00
func save(manager: NETunnelProviderManager?, saveToManager: Bool = true, callback: @escaping (Error?) -> ()) {
let configPath: URL
do {
configPath = try SiteList.getSiteConfigFile(id: self.id, createDir: true)
} catch {
callback(error)
return
}
2024-10-14 20:16:02 +00:00
log.notice("Saving to \(configPath, privacy: .public)")
2020-07-27 20:43:58 +00:00
do {
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("failed to store key material in keychain")
}
}
do {
if ((try self.dnCredentials?.save(siteID: self.id)) == false) {
return callback("failed to store dn credentials in keychain")
}
} catch {
return callback(error)
}
try self.getConfig().write(to: configPath)
2020-07-27 20:43:58 +00:00
} catch {
return callback(error)
}
#if targetEnvironment(simulator)
// We are on a simulator and there is no NEVPNManager for us to interact with
2020-07-27 20:43:58 +00:00
callback(nil)
#else
if saveToManager {
self.saveToManager(manager: manager, callback: callback)
} else {
callback(nil)
}
#endif
}
private func saveToManager(manager: NETunnelProviderManager?, callback: @escaping (Error?) -> ()) {
2020-07-27 20:43:58 +00:00
if (manager != nil) {
// We need to refresh our settings to properly update config
manager?.loadFromPreferences { error in
if (error != nil) {
return callback(error)
}
return self.finishSaveToManager(manager: manager!, callback: callback)
2020-07-27 20:43:58 +00:00
}
return
}
return finishSaveToManager(manager: NETunnelProviderManager(), callback: callback)
2020-07-27 20:43:58 +00:00
}
private func finishSaveToManager(manager: NETunnelProviderManager, callback: @escaping (Error?) -> ()) {
2020-07-27 20:43:58 +00:00
// Stuff our details in the protocol
let proto = manager.protocolConfiguration as? NETunnelProviderProtocol ?? NETunnelProviderProtocol()
proto.providerConfiguration = ["id": self.id]
proto.serverAddress = "Nebula"
2020-07-27 20:43:58 +00:00
// Finish up the manager, this is what stores everything at the system level
manager.protocolConfiguration = proto
//TODO: cert name? manager.protocolConfiguration?.username
//TODO: This is what is shown on the vpn page. We should add more identifying details in
manager.localizedDescription = self.name
2020-07-27 20:43:58 +00:00
manager.isEnabled = true
manager.saveToPreferences{ error in
return callback(error)
}
}
}