Files
appchk-app/main/GlassVPN.swift
2021-04-01 15:37:15 +02:00

173 lines
4.9 KiB
Swift

import NetworkExtension
let GlassVPN = GlassVPNManager()
enum VPNState : Int { case on = 1, inbetween, off }
final class GlassVPNManager {
static let bundleIdentifier = "de.uni-bamberg.psi.AppCheck.VPN"
private var managerVPN: NETunnelProviderManager?
private(set) var state: VPNState = .off
fileprivate init() {
#if IOS_SIMULATOR
postProcessedVPNState(.on)
SimulatorVPN.start()
#else
NETunnelProviderManager.loadAllFromPreferences { managers, error in
self.managerVPN = managers?.first {
($0.protocolConfiguration as? NETunnelProviderProtocol)?
.providerBundleIdentifier == GlassVPNManager.bundleIdentifier
}
guard let mgr = self.managerVPN else {
self.postRawVPNState(.invalid)
return
}
mgr.loadFromPreferences { _ in
self.postRawVPNState(mgr.connection.status)
}
}
NSNotification.Name.NEVPNStatusDidChange.observe(call: #selector(vpnStatusChanged(_:)), on: self)
#endif
NotifyDNSFilterChanged.observe(call: #selector(didChangeDomainFilter), on: self)
}
func setEnabled(_ newState: Bool) {
#if IOS_SIMULATOR
postProcessedVPNState(newState ? .on : .off)
newState ? SimulatorVPN.start() : SimulatorVPN.stop()
#else
guard let mgr = self.managerVPN else {
self.createNewVPN { manager in
self.managerVPN = manager
self.setEnabled(newState)
}
return
}
let state = mgr.isEnabled && (mgr.connection.status == .connected)
if state != newState {
self.updateVPN({ mgr.isEnabled = true }) {
newState ? try? mgr.connection.startVPNTunnel() : mgr.connection.stopVPNTunnel()
}
}
#endif
}
/// Notify VPN extension about changes
/// - Returns: `true` on success, `false` if VPN is off or message could not be converted to `.utf8`
@discardableResult func send(_ message: VPNAppMessage) -> Bool {
#if IOS_SIMULATOR
if state == .on, let data = message.raw {
SimulatorVPN.sendMsg(data)
return true
}
#else
if let session = self.managerVPN?.connection as? NETunnelProviderSession,
session.status == .connected, let data = message.raw {
do {
try session.sendProviderMessage(data, responseHandler: nil)
return true
} catch {}
}
#endif
return false
}
// MARK: - Notify callback
@objc private func vpnStatusChanged(_ notification: Notification) {
postRawVPNState((notification.object as? NETunnelProviderSession)?.status ?? .invalid)
}
@objc private func didChangeDomainFilter(_ notification: Notification) {
send(.filterUpdate(domain: notification.object as? String))
}
// MARK: - Manage configuration
private func createNewVPN(_ success: @escaping (_ manager: NETunnelProviderManager) -> Void) {
let mgr = NETunnelProviderManager()
mgr.localizedDescription = "AppChk Monitor"
let proto = NETunnelProviderProtocol()
proto.providerBundleIdentifier = GlassVPNManager.bundleIdentifier
proto.serverAddress = "127.0.0.1"
mgr.protocolConfiguration = proto
mgr.isEnabled = true
mgr.saveToPreferences { error in
guard error == nil else {
self.postProcessedVPNState(.off)
//ErrorAlert(error!).presentIn(self.window?.rootViewController)
return
}
success(mgr)
}
}
private func updateVPN(_ body: @escaping () -> Void, _ onSuccess: @escaping () -> Void) {
self.managerVPN?.loadFromPreferences { error in
guard error == nil else { return }
body()
self.managerVPN?.saveToPreferences { error in
guard error == nil else { return }
onSuccess()
}
}
}
// MARK: - Post Notifications
private func postRawVPNState(_ origState: NEVPNStatus) {
let state: VPNState
switch origState {
case .connected: state = .on
case .connecting, .disconnecting, .reasserting: state = .inbetween
case .invalid, .disconnected: fallthrough
@unknown default: state = .off
}
postProcessedVPNState(state)
}
private func postProcessedVPNState(_ state: VPNState) {
self.state = state
NotifyVPNStateChanged.post()
}
}
// ---------------------------------------------------------------
// |
// | MARK: - VPN message
// |
// ---------------------------------------------------------------
struct VPNAppMessage {
let raw: Data?
init(_ string: String) { raw = string.data(using: .utf8) }
static func filterUpdate(domain: String? = nil) -> Self {
.init("filter-update:\(domain ?? "")")
}
static func autoDelete(after interval: Int) -> Self {
.init("auto-delete:\(interval)")
}
/// Only used for connection alert notifications
static func notificationSettingsChanged() -> Self {
.init("notify-prefs-change:1")
}
/// Triggered whenever user taps on the start/stop recording button
static func isRecording(_ state: CurrentRecordingState) -> Self {
.init("recording-now:\(state.rawValue)")
}
/// Triggered whenever user taps on the switch in settings
static func disconnectUnresolvable(_ state: Bool) -> Self {
.init("disconnect-unresolvable:\(state ? 1 : 0)")
}
/// Triggered whenever user taps on the switch in settings
static func disconnectSWCD(_ state: Bool) -> Self {
.init("disconnect-swcd:\(state ? 1 : 0)")
}
}