mirror of
https://github.com/oxen-io/lokinet.git
synced 2024-11-11 07:10:36 +00:00
dc358a6eda
If you stop/start the GUI but it doesn't exit on start, the second --start (when lokinet is already running) waits for a state change that doesn't come (because lokinet is already running). This add a check for already-running so that we exit right away in such a case.
244 lines
10 KiB
Swift
244 lines
10 KiB
Swift
import AppKit
|
|
import Foundation
|
|
import NetworkExtension
|
|
import SystemExtensions
|
|
|
|
let app = NSApplication.shared
|
|
|
|
let START = "--start"
|
|
let STOP = "--stop"
|
|
|
|
let HELP_STRING = "usage: lokinet {--start|--stop}"
|
|
|
|
class LokinetMain: NSObject, NSApplicationDelegate {
|
|
var vpnManager = NETunnelProviderManager()
|
|
var mode = START
|
|
let netextBundleId = "org.lokinet.network-extension"
|
|
|
|
func applicationDidFinishLaunching(_: Notification) {
|
|
if mode == START {
|
|
startNetworkExtension()
|
|
} else if mode == STOP {
|
|
tearDownVPNTunnel()
|
|
} else {
|
|
result(msg: HELP_STRING)
|
|
}
|
|
}
|
|
|
|
func bail() {
|
|
app.terminate(self)
|
|
}
|
|
|
|
func result(msg: String) {
|
|
NSLog(msg)
|
|
// TODO: does lokinet continue after this?
|
|
bail()
|
|
}
|
|
|
|
func tearDownVPNTunnel() {
|
|
NSLog("Stopping Lokinet")
|
|
NETunnelProviderManager.loadAllFromPreferences { [self] (savedManagers: [NETunnelProviderManager]?, error: Error?) in
|
|
if let error = error {
|
|
self.result(msg: error.localizedDescription)
|
|
return
|
|
}
|
|
|
|
if let savedManagers = savedManagers {
|
|
for manager in savedManagers {
|
|
if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.netextBundleId {
|
|
manager.connection.stopVPNTunnel()
|
|
self.result(msg: "Lokinet Down")
|
|
}
|
|
}
|
|
}
|
|
self.result(msg: "Lokinet is not up")
|
|
}
|
|
}
|
|
|
|
func startNetworkExtension() {
|
|
#if MACOS_SYSTEM_EXTENSION
|
|
NSLog("Loading Lokinet network extension")
|
|
// Start by activating the system extension
|
|
let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: netextBundleId, queue: .main)
|
|
activationRequest.delegate = self
|
|
OSSystemExtensionManager.shared.submitRequest(activationRequest)
|
|
#else
|
|
setupVPNTunnel()
|
|
#endif
|
|
}
|
|
|
|
func setupVPNTunnel() {
|
|
NSLog("Starting up Lokinet tunnel")
|
|
NETunnelProviderManager.loadAllFromPreferences { [self] (savedManagers: [NETunnelProviderManager]?, error: Error?) in
|
|
if let error = error {
|
|
self.result(msg: error.localizedDescription)
|
|
return
|
|
}
|
|
|
|
if let savedManagers = savedManagers {
|
|
for manager in savedManagers {
|
|
if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.netextBundleId {
|
|
NSLog("Found saved VPN Manager")
|
|
self.vpnManager = manager
|
|
}
|
|
}
|
|
}
|
|
let providerProtocol = NETunnelProviderProtocol()
|
|
providerProtocol.serverAddress = "loki.loki" // Needs to be set to some non-null dummy value
|
|
providerProtocol.username = "anonymous"
|
|
providerProtocol.providerBundleIdentifier = self.netextBundleId
|
|
if #available(macOS 11, *) {
|
|
providerProtocol.enforceRoutes = true
|
|
}
|
|
// macos seems to have trouble when this is true, and reports are that this breaks and
|
|
// doesn't do what it says on the tin in the first place. Needs more testing.
|
|
providerProtocol.includeAllNetworks = false
|
|
self.vpnManager.protocolConfiguration = providerProtocol
|
|
self.vpnManager.isEnabled = true
|
|
// self.vpnManager.isOnDemandEnabled = true
|
|
self.vpnManager.localizedDescription = "lokinet"
|
|
self.vpnManager.saveToPreferences(completionHandler: { [self] error -> Void in
|
|
if error != nil {
|
|
NSLog("Error saving to preferences")
|
|
self.result(msg: error!.localizedDescription)
|
|
} else {
|
|
self.vpnManager.loadFromPreferences(completionHandler: { error in
|
|
if error != nil {
|
|
NSLog("Error loading from preferences")
|
|
self.result(msg: error!.localizedDescription)
|
|
} else {
|
|
do {
|
|
NSLog("Trying to start")
|
|
self.initializeConnectionObserver()
|
|
try self.vpnManager.connection.startVPNTunnel()
|
|
} catch let error as NSError {
|
|
self.result(msg: error.localizedDescription)
|
|
} catch {
|
|
self.result(msg: "There was a fatal error")
|
|
}
|
|
|
|
// Check if we are already connected because, if so, we won't get a
|
|
// status change and will just hang waiting for one.
|
|
if self.vpnManager.connection.status == .connected {
|
|
self.result(msg: "VPN already connected");
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func initializeConnectionObserver() {
|
|
NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: vpnManager.connection, queue: OperationQueue.main) { [self] _ -> Void in
|
|
if self.vpnManager.connection.status == .invalid {
|
|
self.result(msg: "VPN configuration is invalid")
|
|
} else if self.vpnManager.connection.status == .disconnected {
|
|
self.result(msg: "VPN is disconnected.")
|
|
} else if self.vpnManager.connection.status == .connecting {
|
|
NSLog("VPN is connecting...")
|
|
} else if self.vpnManager.connection.status == .reasserting {
|
|
NSLog("VPN is reasserting...")
|
|
} else if self.vpnManager.connection.status == .disconnecting {
|
|
NSLog("VPN is disconnecting...")
|
|
} else if self.vpnManager.connection.status == .connected {
|
|
self.result(msg: "VPN Connected")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if MACOS_SYSTEM_EXTENSION
|
|
|
|
extension LokinetMain: OSSystemExtensionRequestDelegate {
|
|
func request(_: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {
|
|
guard result == .completed else {
|
|
NSLog("Unexpected result %d for system extension request", result.rawValue)
|
|
return
|
|
}
|
|
NSLog("Lokinet system extension loaded")
|
|
setupVPNTunnel()
|
|
}
|
|
|
|
func request(_: OSSystemExtensionRequest, didFailWithError error: Error) {
|
|
NSLog("System extension request failed: %@", error.localizedDescription)
|
|
self.bail()
|
|
}
|
|
|
|
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
|
|
NSLog("Extension %@ requires user approval", request.identifier)
|
|
}
|
|
|
|
func request(_ request: OSSystemExtensionRequest,
|
|
actionForReplacingExtension existing: OSSystemExtensionProperties,
|
|
withExtension extension: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction
|
|
{
|
|
NSLog("Replacing extension %@ version %@ with version %@", request.identifier, existing.bundleShortVersion, `extension`.bundleShortVersion)
|
|
return .replace
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
let args = CommandLine.arguments
|
|
|
|
// If we are invoked with no arguments then exec the gui. This is dumb, but there doesn't seem to
|
|
// be a nicer way to do this on Apple's half-baked platform because:
|
|
// - we have three "bundles" we need to manage: the GUI app, the system extension, and the Lokinet
|
|
// app (this file) which loads the system extension.
|
|
// - if we embed the system extension directly inside the GUI then it fails to launch because the
|
|
// electron GUI's requirements (needed for JIT) conflict with the ability to load a system
|
|
// extensions.
|
|
// - if we embed Lokinet.app inside Lokinet-GUI.app and then the system extension inside Lokinet.app
|
|
// then it works, but macos loses track of the system extension and doesn't remove it when you
|
|
// remove the application. (It breaks your system, leaving an impossible-to-remove system
|
|
// extension, in just the same way it breaks if you don't use Finder to remove the Application.
|
|
// Apple used to say (around 2 years ago as of writing) that they would fix this situation "soon",
|
|
// but hasn't, and has stopped saying anything about it.)
|
|
// - if we try to use multiple executables (one to launch the system extension, one simple shell
|
|
// script to execs the embedded GUI app) inside the Lokinet.app and make the GUI the default for
|
|
// the application then Lokinet gets killed by gatekeeper because code signing only applies the
|
|
// (required-for-system-extensions) provisioningprofile to the main binary in the app.
|
|
//
|
|
// So we are left needing *one* single binary that isn't the GUI but has to do double-duty for both
|
|
// exec'ing the binary and loading lokinet, depending on how it is called.
|
|
//
|
|
// But of course there is no way to specify command-line arguments to the default binary macOS runs,
|
|
// so we can't use a `--gui` flag or anything so abhorrent to macos purity, thus this nasty
|
|
// solution:
|
|
// - no args -- exec the GUI
|
|
// - `--start` -- load the system extension and start lokinet
|
|
// - `--stop` -- stop lokinet
|
|
//
|
|
// macOS: land of half-baked implementations and nasty hacks to make anything work.
|
|
|
|
if args.count == 1 {
|
|
let gui_path = Bundle.main.resourcePath! + "/../Helpers/Lokinet-GUI.app"
|
|
if !FileManager.default.fileExists(atPath: gui_path) {
|
|
NSLog("Could not find gui app at %@", gui_path)
|
|
exit(1)
|
|
}
|
|
let gui_url = URL(fileURLWithPath: gui_path, isDirectory: false)
|
|
let gui_app_conf = NSWorkspace.OpenConfiguration()
|
|
let group = DispatchGroup()
|
|
group.enter()
|
|
NSWorkspace.shared.openApplication(at: gui_url, configuration: gui_app_conf,
|
|
completionHandler: { (app, error) in
|
|
if error != nil {
|
|
NSLog("Error launching gui: %@", error!.localizedDescription)
|
|
} else {
|
|
NSLog("Lauched GUI");
|
|
}
|
|
group.leave()
|
|
})
|
|
group.wait()
|
|
|
|
} else if args.count == 2 {
|
|
let delegate = LokinetMain()
|
|
delegate.mode = args[1]
|
|
app.delegate = delegate
|
|
app.run()
|
|
} else {
|
|
NSLog(HELP_STRING)
|
|
}
|