Proper VPN simulator with notifications, etc.
This commit is contained in:
@@ -19,7 +19,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
PrefsShared.registerDefaults()
|
||||
|
||||
#if IOS_SIMULATOR
|
||||
TestDataSource.load()
|
||||
SimulatorVPN.load()
|
||||
#endif
|
||||
|
||||
sync.start()
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
138
main/GlassVPNHook.swift
Normal file
138
main/GlassVPNHook.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
main/Push Notifications/CachedConnectionAlert.swift
Normal file
43
main/Push Notifications/CachedConnectionAlert.swift
Normal file
@@ -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?)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user