169 lines
5.5 KiB
Swift
169 lines
5.5 KiB
Swift
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!
|
|
private var currentlyRecording: Bool = false
|
|
|
|
public var isBackgroundRecording: Bool = false
|
|
public var forceDisconnectUnresolvable: Bool = false
|
|
public var forceDisconnectSWCD: Bool = false
|
|
|
|
|
|
init() { reset() }
|
|
|
|
/// Reload from stored settings and rebuilt binary search tree
|
|
private func reset() {
|
|
reloadDomainFilter()
|
|
setAutoDelete(PrefsShared.AutoDeleteLogsDays)
|
|
cachedNotify = CachedConnectionAlert()
|
|
currentlyRecording = PrefsShared.CurrentlyRecording != .Off
|
|
isBackgroundRecording = PrefsShared.CurrentlyRecording == .Background
|
|
forceDisconnectUnresolvable = PrefsShared.ForceDisconnectUnresolvableDNS
|
|
forceDisconnectSWCD = PrefsShared.ForceDisconnectSWCD
|
|
}
|
|
|
|
/// 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
|
|
currentlyRecording = false
|
|
isBackgroundRecording = false
|
|
forceDisconnectUnresolvable = false
|
|
forceDisconnectSWCD = false
|
|
}
|
|
|
|
/// 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
|
|
case "recording-now":
|
|
let newState = CurrentRecordingState(rawValue: Int(value) ?? 0)
|
|
currentlyRecording = newState != .Off
|
|
isBackgroundRecording = newState == .Background
|
|
return
|
|
case "disconnect-unresolvable":
|
|
forceDisconnectUnresolvable = value == "1"
|
|
return
|
|
case "disconnect-swcd":
|
|
forceDisconnectSWCD = value == "1"
|
|
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)
|
|
let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i]
|
|
if ignore, !currentlyRecording {
|
|
return block
|
|
}
|
|
let blockActive = block && !currentlyRecording
|
|
queue.async {
|
|
do { try AppDB?.logWrite(domain, blocked: blockActive) }
|
|
catch { NSLog("[VPN.WARN] Couldn't write: \(error)") }
|
|
}
|
|
// TODO: disable notifications during recording?
|
|
cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB)
|
|
// TODO: wait for notify response to block or allow connection
|
|
return blockActive
|
|
}
|
|
|
|
func silentlyPrevented(_ domain: String) {
|
|
// TODO: persist in a separate db/table?
|
|
NSLog("[VPN.INFO] preventing connection to \(domain)")
|
|
}
|
|
|
|
/// 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 {
|
|
guard sender.isValid else { return }
|
|
do {
|
|
try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int)
|
|
} catch {
|
|
NSLog("[VPN.WARN] Couldn't delete logs, will retry in 5 minutes. \(error)")
|
|
sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min
|
|
}
|
|
}
|
|
}
|
|
}
|