From 01523b250fd1605ed1e78c3a2c19ea6fddb3620b Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 27 Jul 2020 17:50:15 +0200 Subject: [PATCH] Proper VPN simulator with notifications, etc. --- AppCheck.xcodeproj/project.pbxproj | 34 +++- GlassVPN/PacketTunnelProvider.swift | 180 +----------------- main/AppDelegate.swift | 2 +- ...estDataSource.swift => SimulatorVPN.swift} | 27 ++- main/GlassVPN.swift | 17 ++ main/GlassVPNHook.swift | 138 ++++++++++++++ .../CachedConnectionAlert.swift | 43 +++++ .../PushNotification.swift | 0 .../PushNotificationAppOnly.swift | 0 .../UNNotification.swift | 4 +- 10 files changed, 257 insertions(+), 188 deletions(-) rename main/Data Source/{TestDataSource.swift => SimulatorVPN.swift} (65%) create mode 100644 main/GlassVPNHook.swift create mode 100644 main/Push Notifications/CachedConnectionAlert.swift rename main/{Common Classes => Push Notifications}/PushNotification.swift (100%) rename main/{Common Classes => Push Notifications}/PushNotificationAppOnly.swift (100%) rename main/{Extensions => Push Notifications}/UNNotification.swift (97%) diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index 37b60aa..6079347 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -17,6 +17,10 @@ 541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075CD24C9D43A00D6F1BF /* UNNotification.swift */; }; 541075D124CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */; }; 541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */; }; + 541075D524CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */; }; + 541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */; }; + 541075D924CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */; }; + 541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */; }; 5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */; }; 5412FCC224C628FA000DE429 /* TVCReminderAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */; }; 5412FCC324C628FA000DE429 /* TVCChooseAlertTone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */; }; @@ -172,7 +176,7 @@ 54D8B9832471BD8100EB2414 /* DBAppOnly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */; }; 54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */; }; 54E540F2247C423200F7C34A /* DomainFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F1247C423200F7C34A /* DomainFilter.swift */; }; - 54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* TestDataSource.swift */; }; + 54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */; }; 54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; }; 54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.swift */; }; 54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; }; @@ -216,6 +220,8 @@ 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCPreviousRecords.swift; sourceTree = ""; }; 541075CD24C9D43A00D6F1BF /* UNNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotification.swift; sourceTree = ""; }; 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThrottledBatchQueue.swift; sourceTree = ""; }; + 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedConnectionAlert.swift; sourceTree = ""; }; + 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassVPNHook.swift; sourceTree = ""; }; 5412F8ED24571B8100A63D7A /* VCDateFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCDateFilter.swift; sourceTree = ""; }; 5412FCBF24C628F9000DE429 /* TVCReminderAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCReminderAlerts.swift; sourceTree = ""; }; 5412FCC024C628F9000DE429 /* TVCChooseAlertTone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TVCChooseAlertTone.swift; sourceTree = ""; }; @@ -365,7 +371,7 @@ 54D8B9822471BD8100EB2414 /* DBAppOnly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBAppOnly.swift; sourceTree = ""; }; 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedDomainDataSource.swift; sourceTree = ""; }; 54E540F1247C423200F7C34A /* DomainFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainFilter.swift; sourceTree = ""; }; - 54E540F3247D3F2600F7C34A /* TestDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataSource.swift; sourceTree = ""; }; + 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorVPN.swift; sourceTree = ""; }; 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsDB.swift; sourceTree = ""; }; 54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.swift; sourceTree = ""; }; 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsShared.swift; sourceTree = ""; }; @@ -430,6 +436,17 @@ path = Recordings; sourceTree = ""; }; + 541075D324CE284700D6F1BF /* Push Notifications */ = { + isa = PBXGroup; + children = ( + 541075CD24C9D43A00D6F1BF /* UNNotification.swift */, + 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */, + 543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */, + 541075D424CE286200D6F1BF /* CachedConnectionAlert.swift */, + ); + path = "Push Notifications"; + sourceTree = ""; + }; 541AC5CB2399498A00A769D7 = { isa = PBXGroup; children = ( @@ -456,9 +473,11 @@ 54E540F0247C386500F7C34A /* Data Source */, 54B345A4241BB975004C53CC /* Extensions */, 545DDDD224436A03003B6544 /* Common Classes */, + 541075D324CE284700D6F1BF /* Push Notifications */, 548B1F9423D338EC005B047C /* main.entitlements */, 541AC5D72399498A00A769D7 /* AppDelegate.swift */, 54E67E4A24A8C6370025D261 /* GlassVPN.swift */, + 541075D824CE2C7200D6F1BF /* GlassVPNHook.swift */, 542E2A972404973F001462DC /* TBCMain.swift */, 540C6454240D5BAE00E948F9 /* Requests */, 540E677E242D2CD200871BBE /* Recordings */, @@ -538,8 +557,6 @@ 541FC47524A12D01009154D8 /* IBViews.swift */, 5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */, 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */, - 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */, - 543078C824B75CD100278F2D /* PushNotificationAppOnly.swift */, ); path = "Common Classes"; sourceTree = ""; @@ -562,7 +579,6 @@ 54B345A8241BBA0B004C53CC /* Logging.swift */, 54E67E4E24A8E2910025D261 /* Equatable.swift */, 54B345A5241BB982004C53CC /* Notifications.swift */, - 541075CD24C9D43A00D6F1BF /* UNNotification.swift */, 54B345AA241BBA5B004C53CC /* AlertSheet.swift */, 54E67E5024A8E8820025D261 /* View.swift */, 541DCA6024A6B0F6005F1A4B /* Color.swift */, @@ -812,7 +828,7 @@ 54E540F0247C386500F7C34A /* Data Source */ = { isa = PBXGroup; children = ( - 54E540F3247D3F2600F7C34A /* TestDataSource.swift */, + 54E540F3247D3F2600F7C34A /* SimulatorVPN.swift */, 54E540F92482414800F7C34A /* SyncUpdate.swift */, 54D8B98524796E9800EB2414 /* GroupedDomainDataSource.swift */, 54E540F1247C423200F7C34A /* DomainFilter.swift */, @@ -964,7 +980,9 @@ 54E67E4924A8B1280025D261 /* Prefs.swift in Sources */, 54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */, 54E67E4F24A8E2910025D261 /* Equatable.swift in Sources */, - 54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */, + 54E540F4247D3F2600F7C34A /* SimulatorVPN.swift in Sources */, + 541075D924CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */, + 541075D524CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */, 5404AEEF24ACC089003B2F54 /* VCAnalysisBar.swift in Sources */, 545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */, 54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */, @@ -1082,6 +1100,7 @@ 54751E522423955100168273 /* URL.swift in Sources */, 54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */, 54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */, + 541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */, 54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */, 54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */, 54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */, @@ -1112,6 +1131,7 @@ 54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */, 54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */, 54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */, + 541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */, 54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/GlassVPN/PacketTunnelProvider.swift b/GlassVPN/PacketTunnelProvider.swift index 072a84e..149a098 100644 --- a/GlassVPN/PacketTunnelProvider.swift +++ b/GlassVPN/PacketTunnelProvider.swift @@ -1,7 +1,6 @@ import NetworkExtension -import UserNotifications -private let queue = DispatchQueue.init(label: "PSIGlassDNSQueue", qos: .userInteractive, target: .main) +fileprivate var hook : GlassVPNHook! // MARK: ObserverFactory @@ -16,10 +15,7 @@ class LDObserverFactory: ObserverFactory { override func signal(_ event: ProxySocketEvent) { switch event { case .receivedRequest(let session, let socket): - let i = filterIndex(for: session.host) - let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i] - let kill = ignore ? block : procRequest(session.host, blck: block, custA: cA, custB: cB) - // TODO: disable ignore & block during recordings + let kill = hook.processDNSRequest(session.host) if kill { socket.forceDisconnect() } default: break @@ -37,8 +33,6 @@ class PacketTunnelProvider: NEPacketTunnelProvider { private let proxyServerAddress = "127.0.0.1" private var proxyServer: GCDHTTPProxyServer! - private var autoDeleteTimer: Timer? = nil - // MARK: Delegate override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { @@ -82,31 +76,13 @@ class PacketTunnelProvider: NEPacketTunnelProvider { } override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { - let message = String(data: messageData, encoding: .utf8) - if let msg = message, let i = msg.firstIndex(of: ":") { - let action = msg.prefix(upTo: i) - let value = msg.suffix(from: msg.index(after: i)) - switch action { - case "filter-update": - reloadDomainFilter() // TODO: reload only selected domain? - return - case "auto-delete": - setAutoDelete(Int(value) ?? PrefsShared.AutoDeleteLogsDays) - return - case "notify-prefs-change": - reloadNotificationSettings() - return - default: break - } - } - DDLogWarn("This should never happen! Received unknown handleAppMessage: \(message ?? messageData.base64EncodedString())") - reloadSettings() // just in case we fallback to do everything + hook.handleAppMessage(messageData) } // MARK: Helper private func willInitProxy() { - reloadSettings() + hook = GlassVPNHook() } private func createProxy() -> NEPacketTunnelNetworkSettings { @@ -143,155 +119,11 @@ class PacketTunnelProvider: NEPacketTunnelProvider { proxyServer.stop() proxyServer = nil // custom - filterDomains = nil - filterOptions = nil - autoDeleteTimer?.fire() // one last time before we quit - autoDeleteTimer?.invalidate() - notifyTone = nil + hook.cleanUp() + hook = nil if PrefsShared.RestartReminder.Enabled { PushNotification.scheduleRestartReminderBadge(on: true) PushNotification.scheduleRestartReminderBanner() } } - - private func reloadSettings() { - reloadDomainFilter() - setAutoDelete(PrefsShared.AutoDeleteLogsDays) - reloadNotificationSettings() - } -} - - -// ################################################################ -// # -// # MARK: - Domain Filter -// # -// ################################################################ - -fileprivate var filterDomains: [String]! -fileprivate var filterOptions: [(block: Bool, ignore: Bool, customA: Bool, customB: Bool)]! - -extension PacketTunnelProvider { - fileprivate func reloadDomainFilter() { - let tmp = AppDB?.loadFilters()?.map({ - (String($0.reversed()), $1) - }).sorted(by: { $0.0 < $1.0 }) ?? [] - let t1 = tmp.map { $0.0 } - let t2 = tmp.map { ($1.contains(.blocked), - $1.contains(.ignored), - $1.contains(.customA), - $1.contains(.customB)) } - filterDomains = t1 - filterOptions = t2 - } -} - -/// Backward DNS Binary Tree Lookup -fileprivate func filterIndex(for domain: String) -> Int { - let reverseDomain = String(domain.reversed()) - var lo = 0, hi = filterDomains.count - 1 - while lo <= hi { - let mid = (lo + hi)/2 - if filterDomains[mid] < reverseDomain { - lo = mid + 1 - } else if reverseDomain < filterDomains[mid] { - hi = mid - 1 - } else { - return mid - } - } - if lo > 0, reverseDomain.hasPrefix(filterDomains[lo - 1] + ".") { - return lo - 1 - } - return -1 -} - - -// ################################################################ -// # -// # MARK: - Auto-delete Timer -// # -// ################################################################ - -extension PacketTunnelProvider { - - private func setAutoDelete(_ days: Int) { - autoDeleteTimer?.invalidate() - guard days > 0 else { return } - // Repeat interval uses days as hours. min 1 hr, max 24 hrs. - let interval = TimeInterval(min(24, days) * 60 * 60) - autoDeleteTimer = Timer.scheduledTimer(timeInterval: interval, - target: self, selector: #selector(autoDeleteNow), - userInfo: days, repeats: true) - autoDeleteTimer!.fire() - } - - @objc private func autoDeleteNow(_ sender: Timer) { - DDLogInfo("Auto-delete old logs") - queue.async { - do { - try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int) - } catch { - DDLogWarn("Couldn't delete logs, will retry in 5 minutes. \(error)") - if sender.isValid { - sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min - } - } - } - } -} - - -// ################################################################ -// # -// # MARK: - Notifications -// # -// ################################################################ - -fileprivate var notifyEnabled: Bool = false -fileprivate var notifyIvertMode: Bool = false -fileprivate var notifyListBlocked: Bool = false -fileprivate var notifyListCustomA: Bool = false -fileprivate var notifyListCustomB: Bool = false -fileprivate var notifyListElse: Bool = false -fileprivate var notifyTone: AnyObject? - -extension PacketTunnelProvider { - func reloadNotificationSettings() { - notifyEnabled = PrefsShared.ConnectionAlerts.Enabled - guard #available(iOS 10.0, *), notifyEnabled else { - notifyTone = nil - return - } - notifyIvertMode = PrefsShared.ConnectionAlerts.ExcludeMode - notifyListBlocked = PrefsShared.ConnectionAlerts.Lists.Blocked - notifyListCustomA = PrefsShared.ConnectionAlerts.Lists.CustomA - notifyListCustomB = PrefsShared.ConnectionAlerts.Lists.CustomB - notifyListElse = PrefsShared.ConnectionAlerts.Lists.Else - notifyTone = UNNotificationSound.from(string: PrefsShared.ConnectionAlerts.Sound) - } -} - - -// ################################################################ -// # -// # MARK: - Process DNS Request -// # -// ################################################################ - -/// Log domain request and post notification if wanted. -/// - Returns: `true` if the request shoud be blocked -fileprivate func procRequest(_ domain: String, blck: Bool, custA: Bool, custB: Bool) -> Bool { - queue.async { - do { try AppDB?.logWrite(domain, blocked: blck) } - catch { DDLogWarn("Couldn't write: \(error)") } - } - if #available(iOS 10.0, *), notifyEnabled { - let onAnyList = notifyListBlocked && blck || notifyListCustomA && custA || notifyListCustomB && custB || notifyListElse - if notifyIvertMode ? !onAnyList : onAnyList { - // TODO: wait for response to block or allow connection - PushNotification.scheduleConnectionAlert(domain, sound: notifyTone as! UNNotificationSound?) - } - } - return blck } diff --git a/main/AppDelegate.swift b/main/AppDelegate.swift index 9c50024..45001a7 100644 --- a/main/AppDelegate.swift +++ b/main/AppDelegate.swift @@ -19,7 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { PrefsShared.registerDefaults() #if IOS_SIMULATOR - TestDataSource.load() + SimulatorVPN.load() #endif sync.start() diff --git a/main/Data Source/TestDataSource.swift b/main/Data Source/SimulatorVPN.swift similarity index 65% rename from main/Data Source/TestDataSource.swift rename to main/Data Source/SimulatorVPN.swift index cf2f6b4..55f4c01 100644 --- a/main/Data Source/TestDataSource.swift +++ b/main/Data Source/SimulatorVPN.swift @@ -2,7 +2,10 @@ import Foundation #if IOS_SIMULATOR -class TestDataSource { +fileprivate var hook : GlassVPNHook! + +class SimulatorVPN { + static var timer: Timer? static func load() { QLog.Debug("SQLite path: \(URL.internalDB())") @@ -27,13 +30,29 @@ class TestDataSource { db.setFilter("bi.test.com", [.blocked, .ignored]) QLog.Debug("Done") - - Timer.repeating(2, call: #selector(insertRandom), on: self) + } + + static func start() { + hook = GlassVPNHook() + timer = Timer.repeating(2, call: #selector(insertRandom), on: self) + } + + static func stop() { + timer?.invalidate() + timer = nil + hook.cleanUp() + hook = nil } @objc static func insertRandom() { //QLog.Debug("Inserting 1 periodic log entry") - try? AppDB?.logWrite("\(arc4random() % 5).count.test.com", blocked: true) + let domain = "\(arc4random() % 5).count.test.com" + let kill = hook.processDNSRequest(domain) + if kill { QLog.Info("Blocked: \(domain)") } + } + + static func sendMsg(_ messageData: Data) { + hook.handleAppMessage(messageData) } } #endif diff --git a/main/GlassVPN.swift b/main/GlassVPN.swift index 60ee23e..41d8651 100644 --- a/main/GlassVPN.swift +++ b/main/GlassVPN.swift @@ -10,6 +10,10 @@ final class GlassVPNManager { 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)? @@ -24,10 +28,15 @@ final class GlassVPNManager { } } 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 @@ -41,11 +50,18 @@ final class GlassVPNManager { 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 { @@ -53,6 +69,7 @@ final class GlassVPNManager { return true } catch {} } +#endif return false } diff --git a/main/GlassVPNHook.swift b/main/GlassVPNHook.swift new file mode 100644 index 0000000..5d3f21a --- /dev/null +++ b/main/GlassVPNHook.swift @@ -0,0 +1,138 @@ +import Foundation + +class GlassVPNHook { + + private let queue = DispatchQueue.init(label: "PSIGlassDNSQueue", qos: .userInteractive, target: .main) + + private var filterDomains: [String]! + private var filterOptions: [(block: Bool, ignore: Bool, customA: Bool, customB: Bool)]! + private var autoDeleteTimer: Timer? = nil + private var cachedNotify: CachedConnectionAlert! + + init() { reset() } + + /// Reload from stored settings and rebuilt binary search tree + private func reset() { + reloadDomainFilter() + setAutoDelete(PrefsShared.AutoDeleteLogsDays) + cachedNotify = CachedConnectionAlert() + } + + /// Invalidate auto-delete timer and release stored properties. You should nullify this instance afterwards. + func cleanUp() { + filterDomains = nil + filterOptions = nil + autoDeleteTimer?.fire() // one last time before we quit + autoDeleteTimer?.invalidate() + cachedNotify = nil + } + + /// Call this method from `PacketTunnelProvider.handleAppMessage(_:completionHandler:)` + func handleAppMessage(_ messageData: Data) { + let message = String(data: messageData, encoding: .utf8) + if let msg = message, let i = msg.firstIndex(of: ":") { + let action = msg.prefix(upTo: i) + let value = msg.suffix(from: msg.index(after: i)) + switch action { + case "filter-update": + reloadDomainFilter() // TODO: reload only selected domain? + return + case "auto-delete": + setAutoDelete(Int(value) ?? PrefsShared.AutoDeleteLogsDays) + return + case "notify-prefs-change": + cachedNotify = CachedConnectionAlert() + return + default: break + } + } + NSLog("[VPN.WARN] This should never happen! Received unknown handleAppMessage: \(message ?? messageData.base64EncodedString())") + reset() // just in case we fallback to do everything + } + + + // MARK: - Process DNS Request + + /// Log domain request and post notification (if enabled). + /// - Returns: `true` if the request shoud be blocked. + func processDNSRequest(_ domain: String) -> Bool { + let i = filterIndex(for: domain) + // TODO: disable ignore & block during recordings + let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i] + if ignore { + return block + } + queue.async { + do { try AppDB?.logWrite(domain, blocked: block) } + catch { NSLog("[VPN.WARN] Couldn't write: \(error)") } + } + cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB) + // TODO: wait for notify response to block or allow connection + return block + } + + /// Build binary tree for reverse DNS lookup + private func reloadDomainFilter() { + let tmp = AppDB?.loadFilters()?.map({ + (String($0.reversed()), $1) + }).sorted(by: { $0.0 < $1.0 }) ?? [] + let t1 = tmp.map { $0.0 } + let t2 = tmp.map { ($1.contains(.blocked), + $1.contains(.ignored), + $1.contains(.customA), + $1.contains(.customB)) } + filterDomains = t1 + filterOptions = t2 + } + + /// Lookup for reverse DNS binary tree + private func filterIndex(for domain: String) -> Int { + let reverseDomain = String(domain.reversed()) + var lo = 0, hi = filterDomains.count - 1 + while lo <= hi { + let mid = (lo + hi)/2 + if filterDomains[mid] < reverseDomain { + lo = mid + 1 + } else if reverseDomain < filterDomains[mid] { + hi = mid - 1 + } else { + return mid + } + } + if lo > 0, reverseDomain.hasPrefix(filterDomains[lo - 1] + ".") { + return lo - 1 + } + return -1 + } + + + // MARK: - Auto-delete Timer + + /// Prepare auto-delete timer with interval between 1 hr - 1 day. + /// - Parameter days: Max age to keep when deleting + private func setAutoDelete(_ days: Int) { + autoDeleteTimer?.invalidate() + guard days > 0 else { return } + // Repeat interval uses days as hours. min 1 hr, max 24 hrs. + let interval = TimeInterval(min(24, days) * 60 * 60) + autoDeleteTimer = Timer.scheduledTimer(timeInterval: interval, + target: self, selector: #selector(autoDeleteNow), + userInfo: days, repeats: true) + autoDeleteTimer!.fire() + } + + /// Callback fired when old data should be deleted. + @objc private func autoDeleteNow(_ sender: Timer) { + NSLog("[VPN.INFO] Auto-delete old logs") + queue.async { + do { + try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int) + } catch { + NSLog("[VPN.WARN] Couldn't delete logs, will retry in 5 minutes. \(error)") + if sender.isValid { + sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min + } + } + } + } +} diff --git a/main/Push Notifications/CachedConnectionAlert.swift b/main/Push Notifications/CachedConnectionAlert.swift new file mode 100644 index 0000000..d25513c --- /dev/null +++ b/main/Push Notifications/CachedConnectionAlert.swift @@ -0,0 +1,43 @@ +import Foundation +import UserNotifications + +struct CachedConnectionAlert { + let enabled: Bool + let invertedMode: Bool + let listBlocked, listCustomA, listCustomB, listElse: Bool + let tone: AnyObject? + + init() { + enabled = PrefsShared.ConnectionAlerts.Enabled + guard #available(iOS 10.0, *), enabled else { + invertedMode = false + listBlocked = false + listCustomA = false + listCustomB = false + listElse = false + tone = nil + return + } + invertedMode = PrefsShared.ConnectionAlerts.ExcludeMode + listBlocked = PrefsShared.ConnectionAlerts.Lists.Blocked + listCustomA = PrefsShared.ConnectionAlerts.Lists.CustomA + listCustomB = PrefsShared.ConnectionAlerts.Lists.CustomB + listElse = PrefsShared.ConnectionAlerts.Lists.Else + tone = UNNotificationSound.from(string: PrefsShared.ConnectionAlerts.Sound) + } + + /// If notifications are enabled and allowed, schedule new notification. Otherwise NOOP. + /// - Parameters: + /// - domain: Domain will be used as unique identifier for noticiation center and in notification message. + /// - blck: Indicator whether `domain` is part of `blocked` list + /// - custA: Indicator whether `domain` is part of custom list `A` + /// - custB: Indicator whether `domain` is part of custom list `B` + func postOrIgnore(_ domain: String, blck: Bool, custA: Bool, custB: Bool) { + if #available(iOS 10.0, *), enabled { + let onAnyList = listBlocked && blck || listCustomA && custA || listCustomB && custB || listElse + if invertedMode ? !onAnyList : onAnyList { + PushNotification.scheduleConnectionAlert(domain, sound: tone as! UNNotificationSound?) + } + } + } +} diff --git a/main/Common Classes/PushNotification.swift b/main/Push Notifications/PushNotification.swift similarity index 100% rename from main/Common Classes/PushNotification.swift rename to main/Push Notifications/PushNotification.swift diff --git a/main/Common Classes/PushNotificationAppOnly.swift b/main/Push Notifications/PushNotificationAppOnly.swift similarity index 100% rename from main/Common Classes/PushNotificationAppOnly.swift rename to main/Push Notifications/PushNotificationAppOnly.swift diff --git a/main/Extensions/UNNotification.swift b/main/Push Notifications/UNNotification.swift similarity index 97% rename from main/Extensions/UNNotification.swift rename to main/Push Notifications/UNNotification.swift index ebdfdb5..61de3a5 100644 --- a/main/Extensions/UNNotification.swift +++ b/main/Push Notifications/UNNotification.swift @@ -5,11 +5,11 @@ enum NotificationRequestState { @available(iOS 10.0, *) init(_ from: UNAuthorizationStatus) { switch from { - case .notDetermined: self = .NotDetermined case .denied: self = .Denied case .authorized: self = .Authorized case .provisional: self = .Provisional - @unknown default: fatalError() + case .notDetermined: fallthrough + @unknown default: self = .NotDetermined } } }