mirror of
https://github.com/confirmedcode/Lockdown-iOS.git
synced 2026-10-19 02:18:24 +02:00
535 lines
19 KiB
Swift
535 lines
19 KiB
Swift
//
|
|
// PacketTunnelProvider.swift
|
|
// LockdownTunnel
|
|
//
|
|
// Copyright © 2019 Confirmed Inc. All rights reserved.
|
|
//
|
|
|
|
import NetworkExtension
|
|
import NEKit
|
|
import Dnscryptproxy
|
|
import Network
|
|
import PromiseKit
|
|
import CocoaLumberjack
|
|
|
|
var latestBlockedDomains = getAllBlockedDomains()
|
|
|
|
class PacketTunnelProvider: NEPacketTunnelProvider {
|
|
|
|
let dnsServerAddress = "127.0.0.1"
|
|
var dns: DNSCryptThread?
|
|
|
|
let proxyServerAddress = "127.0.0.1"
|
|
let proxyServerPort: UInt16 = 9090
|
|
var proxyServer: GCDHTTPProxyServer?
|
|
|
|
let monitor = NWPathMonitor()
|
|
let fileManager = FileManager.default
|
|
let groupContainer = "group.com.confirmed"
|
|
|
|
let lastReachabilityKillKey = "lastReachabilityKillTime"
|
|
private var token: NSObjectProtocol?
|
|
private let center = NotificationCenter.default
|
|
private var proxyError: Error?
|
|
|
|
func log(_ str: String) {
|
|
PacketTunnelProviderLogs.log(str)
|
|
NSLog("ptplog - " + str)
|
|
}
|
|
|
|
override func cancelTunnelWithError(_ error: Error?) {
|
|
self.log("===== ERROR - cancelTunnelWithError \(error?.localizedDescription ?? "")")
|
|
}
|
|
|
|
override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
|
|
log("+++++ startTunnel NEW")
|
|
|
|
// reachability check
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
self?.pathUpdateHandler(path: path)
|
|
}
|
|
|
|
|
|
log("Calling setTunnelNetworkSettings")
|
|
initializeDns();
|
|
initializeProxy();
|
|
|
|
setupObserverDNSCryptProxyReady(completionHandler: completionHandler)
|
|
|
|
startDns();
|
|
proxyError = startProxy()
|
|
}
|
|
|
|
private func setupObserverDNSCryptProxyReady(completionHandler: @escaping (Error?) -> Void) {
|
|
token = center.addObserver(
|
|
forName: Notification.Name(kDNSCryptProxyReady),
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] notification in
|
|
self?.dnsCryptProxyReady(completionHandler: completionHandler)
|
|
}
|
|
}
|
|
|
|
private func dnsCryptProxyReady(completionHandler: @escaping (Error?) -> Void) {
|
|
log("Found available resolvers, tell iOS we are ready")
|
|
if let token {
|
|
center.removeObserver(token)
|
|
}
|
|
updateTunnelSetting(completionHandler: completionHandler)
|
|
let queue = DispatchQueue(label: "Monitor")
|
|
monitor.start(queue: queue)
|
|
}
|
|
|
|
private func updateTunnelSetting(completionHandler: @escaping (Error?) -> Void) {
|
|
let networkSettings = getNetworkSettings();
|
|
|
|
self.setTunnelNetworkSettings(networkSettings) { [weak self] error in
|
|
guard let self else { return }
|
|
if let error {
|
|
self.log("ERROR - StartTunnel \(error.localizedDescription)")
|
|
completionHandler(error);
|
|
} else {
|
|
self.log("No error on setTunnelNetworkSettings, starting dns and proxy")
|
|
|
|
if let proxyError = self.proxyError {
|
|
self.log("ERROR - Failed to start proxy: \(proxyError)")
|
|
completionHandler(proxyError)
|
|
}
|
|
else {
|
|
self.log("SUCCESS - startTunnel")
|
|
completionHandler(nil)
|
|
}
|
|
self.proxyError = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
private func refreshServers() {
|
|
stopProxyServer()
|
|
dns?.closeIdleConnections()
|
|
dns?.refreshServersInfo()
|
|
initializeProxy()
|
|
_ = startProxy()
|
|
}
|
|
|
|
override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
|
|
self.log("+++++ stopTunnel with reason: \(reason)")
|
|
monitor.cancel()
|
|
stopProxyServer()
|
|
stopDnsServer()
|
|
self.log("stopTunnel completionHandler, exit")
|
|
completionHandler();
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
// override func wake() {
|
|
// log("===== wake")
|
|
// flushBlockLog(log: log)
|
|
// log("wake setting tunnel network settings to nil")
|
|
// self.setTunnelNetworkSettings(nil, completionHandler: { error in
|
|
// if (error != nil) {
|
|
// self.log("error setting tunnelnetworksettings to nil: \(error)")
|
|
// }
|
|
// self.log("wake calling reactivate tunnel")
|
|
// self.reactivateTunnel()
|
|
// })
|
|
// }
|
|
|
|
func getNetworkSettings() -> NEPacketTunnelNetworkSettings {
|
|
log("===== getNetworkSettings")
|
|
|
|
let networkSettings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: dnsServerAddress)
|
|
networkSettings.mtu = 1500
|
|
|
|
let proxySettings = NEProxySettings()
|
|
proxySettings.httpEnabled = true;
|
|
proxySettings.httpServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
|
proxySettings.httpsEnabled = true;
|
|
proxySettings.httpsServer = NEProxyServer(address: proxyServerAddress, port: Int(proxyServerPort))
|
|
proxySettings.excludeSimpleHostnames = false;
|
|
proxySettings.exceptionList = []
|
|
proxySettings.matchDomains = getAllWhitelistedDomains()
|
|
networkSettings.proxySettings = proxySettings;
|
|
|
|
let dnsSettings = NEDNSSettings(servers: [dnsServerAddress])
|
|
dnsSettings.matchDomains = [""];
|
|
networkSettings.dnsSettings = dnsSettings;
|
|
|
|
return networkSettings;
|
|
}
|
|
|
|
func initializeAndReturnConfigPath() -> String {
|
|
log("===== initializeAndReturnConfigPath")
|
|
|
|
let configFile = Bundle.main.url(forResource: "dnscrypt-proxy", withExtension: "toml")
|
|
let sharedDir = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupContainer)
|
|
|
|
// remove blocklist if it exists
|
|
let newBlocklistFile = sharedDir!.appendingPathComponent("blocklist.txt")
|
|
if fileManager.fileExists(atPath: newBlocklistFile.path) {
|
|
log("blocklist.txt exists")
|
|
do {
|
|
try fileManager.removeItem(atPath: newBlocklistFile.path)
|
|
log("removed old blocklist.txt")
|
|
} catch {
|
|
log("ERROR - couldnt remove old blocklist.txt: \(error)")
|
|
}
|
|
}
|
|
|
|
// generate text for new blocklist
|
|
let blockedDomainsArray = getAllBlockedDomains()
|
|
var blockedDomains: String = testFirewallDomain
|
|
for blockedDomain in blockedDomainsArray {
|
|
blockedDomains = blockedDomains + "\n" + blockedDomain
|
|
}
|
|
|
|
// copy blocklist file into shared dir
|
|
do {
|
|
try blockedDomains.write(to: newBlocklistFile, atomically: false, encoding: .utf8)
|
|
log("wrote content to blocklist.txt")
|
|
}
|
|
catch {
|
|
log("ERROR - couldnt write content to blocklist.txt: \(error)")
|
|
}
|
|
|
|
// clear prefix suffix files
|
|
let prefixFile = sharedDir!.appendingPathComponent("blacklist.txt.prefixes")
|
|
let suffixFile = sharedDir!.appendingPathComponent("blacklist.txt.suffixes")
|
|
if fileManager.fileExists(atPath: prefixFile.path){
|
|
log("prefix file exists at: \(prefixFile.path)")
|
|
do {
|
|
try fileManager.removeItem(atPath: prefixFile.path)
|
|
log("prefix file removed at: \(prefixFile.path)")
|
|
} catch {
|
|
}
|
|
}
|
|
if fileManager.fileExists(atPath: suffixFile.path){
|
|
do {
|
|
try fileManager.removeItem(atPath: suffixFile.path)
|
|
log("suffix file removed at: \(suffixFile.path)")
|
|
} catch {
|
|
log("ERROR - error removing suffix file: \(error)")
|
|
}
|
|
}
|
|
|
|
// create new prefix/suffix files
|
|
let errorPtr: NSErrorPointer = nil
|
|
log("filling in prefix/suffix files at: \(newBlocklistFile.path)")
|
|
DnscryptproxyFillPatternlistTrees(newBlocklistFile.path, errorPtr)
|
|
if let error = errorPtr?.pointee {
|
|
log("ERROR - filling in prefix/suffix files: \(error)")
|
|
}
|
|
|
|
// read config file template
|
|
var configFileText = ""
|
|
do {
|
|
configFileText = try String(contentsOf: configFile!, encoding: .utf8)
|
|
log("Read config file template")
|
|
}
|
|
catch {
|
|
log("ERROR - couldn't read config file template text at: \(configFile!.path)")
|
|
}
|
|
|
|
// replace BLOCKLIST_FILE_HERE and BLOCKLIST_LOG_HERE with urls of blocklist file/log
|
|
let replacedConfig = configFileText.replacingOccurrences(of: "BLOCKLIST_FILE_HERE", with: "\(newBlocklistFile.path)").replacingOccurrences(of: "BLOCKLIST_LOG_HERE", with: "\(sharedDir!.appendingPathComponent("blocklist.log").path)")
|
|
|
|
// write replaced string to new file
|
|
let replacedConfigURL = sharedDir!.appendingPathComponent("replaced-config.toml")
|
|
log("replaced config file url: \(replacedConfigURL.path)")
|
|
do {
|
|
try replacedConfig.write(to: replacedConfigURL, atomically: false, encoding: .utf8)
|
|
log("replaced config written")
|
|
}
|
|
catch {
|
|
log("ERROR - couldn't write replaced config: \(error)")
|
|
}
|
|
log("returning replacedConfigURL \(replacedConfigURL)")
|
|
return replacedConfigURL.path
|
|
}
|
|
|
|
func initializeDns() {
|
|
log("===== initialize DNS server")
|
|
// stopDnsServer()
|
|
log("initializing DNSCryptThread")
|
|
dns = DNSCryptThread(arguments: [initializeAndReturnConfigPath()]);
|
|
}
|
|
|
|
func initializeProxy() {
|
|
log("===== initialize proxy server")
|
|
// stopProxyServer()
|
|
log("initializing GCDHTTPProxyServer")
|
|
proxyServer = GCDHTTPProxyServer(address: IPAddress(fromString: self.proxyServerAddress), port: Port(port: self.proxyServerPort))
|
|
}
|
|
|
|
func startProxy() -> Error? {
|
|
log("===== startProxy")
|
|
do {
|
|
try self.proxyServer?.start()
|
|
log("started proxyServer")
|
|
return nil
|
|
} catch {
|
|
log("ERROR - couldnt start proxyServer")
|
|
return error
|
|
}
|
|
}
|
|
|
|
func startDns() {
|
|
log("===== startDns")
|
|
dns?.start()
|
|
}
|
|
|
|
func stopDnsServer() {
|
|
log("===== stopDnsServer")
|
|
guard let dns else { return }
|
|
|
|
log("dns is not nil")
|
|
log("dns closing idle connections")
|
|
dns.closeIdleConnections()
|
|
log("dns stopApp")
|
|
dns.stopApp()
|
|
|
|
log("dns set to nil")
|
|
self.dns = nil
|
|
}
|
|
|
|
func stopProxyServer() {
|
|
log("===== stopProxyServer")
|
|
guard let proxyServer else { return }
|
|
|
|
log("proxyServer is not nil")
|
|
log("proxyServer stop")
|
|
proxyServer.stop()
|
|
|
|
log("proxyServer nil")
|
|
self.proxyServer = nil
|
|
}
|
|
|
|
// func reactivateTunnel() {
|
|
// log("===== reactivateTunnel, reasserting true")
|
|
// reasserting = true
|
|
//
|
|
// let networkSettings = getNetworkSettings()
|
|
//
|
|
// self.setTunnelNetworkSettings(networkSettings) { [weak self] error in
|
|
// guard let self else { return }
|
|
// if let error {
|
|
// self.log("ERROR - reactivateTunnel setTunnelNetworkSettings: \(error.localizedDescription)")
|
|
// }
|
|
// self.log("reactivateTunnel setTunnelNetworkSettings complete, reasserting false")
|
|
// self.reasserting = false
|
|
//
|
|
// self._dns.closeIdleConnections()
|
|
// self.log("closed idle connections")
|
|
//
|
|
// self.log("||||| reactivate AFTER - checking availability to apple.com")
|
|
// self.checkNetworkConnection { [weak self] success in
|
|
// guard let self else { return }
|
|
// self.log("ReactivateTunnel checkNetworkConnection result: \(success)")
|
|
// }
|
|
// }
|
|
//
|
|
// startDns()
|
|
// }
|
|
|
|
|
|
// MARK: - reachability
|
|
|
|
func pathUpdateHandler(path: Network.NWPath) {
|
|
log("REACHABILITY - Connected: \(path.status == .satisfied) - NWPATH: \(path.debugDescription)")
|
|
if path.usesInterfaceType(.wifi) {
|
|
log("REACHABILITY - have connection to wifi")
|
|
}
|
|
if path.usesInterfaceType(.cellular) {
|
|
log("REACHABILITY - have connection to cellular")
|
|
}
|
|
let servers = Resolver().getservers().map(Resolver.getnameinfo)
|
|
log("REACHABILITY DNS Servers: \(servers)")
|
|
|
|
log("reachability testing network")
|
|
|
|
// self.checkNetworkConnection { [weak self] success in
|
|
// guard let self else { return }
|
|
// self.log("reachability network check result: \(success)")
|
|
// if( success == false ) {
|
|
// self.log("ERROR - network check failed, killing PTP if not killed in the last 30 seconds")
|
|
//
|
|
// // only kill PTP if it hasnt been killed in the last 30 seconds - to avoid race conditions/infinite loop
|
|
// // TODO: maybe force VPN restart too?
|
|
// // TODO: maybe force wait a second on stopping?
|
|
// // TODO: make this smarter e.g- if PTP has been killed in the last 30 seconds, wait 10 seconds to kill it
|
|
// let timeIntervalOfLastReachabilityKill = defaults.double(forKey: self.lastReachabilityKillKey)
|
|
// let dateOfLastReachabilityKill = Date(timeIntervalSince1970: timeIntervalOfLastReachabilityKill)
|
|
// let timeSinceLastReachabilityKill = Date().timeIntervalSince(dateOfLastReachabilityKill)
|
|
// self.log("REACHABILITY kill - time since last kill: \(timeSinceLastReachabilityKill)")
|
|
// if (timeSinceLastReachabilityKill < 60) {
|
|
// self.log("REACHABILITY kill - did this < 30 seconds ago, not calling it again")
|
|
// return
|
|
// }
|
|
// else {
|
|
// // do the kill
|
|
// defaults.set(Date().timeIntervalSince1970, forKey: self.lastReachabilityKillKey)
|
|
// }
|
|
// }
|
|
// }
|
|
}
|
|
|
|
func checkNetworkConnection(callback: @escaping (Bool) -> Void, attempt: Int = 1) {
|
|
|
|
log("===== checkNetworkConnection - attempt #\(attempt)")
|
|
URLCache.shared.removeAllCachedResponses()
|
|
firstly {
|
|
try makeNetworkConnection()
|
|
}
|
|
.map { [weak self] data, response -> Void in
|
|
guard let self else { return }
|
|
try self.validateNetworkResponse(response: response)
|
|
callback(true)
|
|
}
|
|
.catch { [weak self] error in
|
|
guard let self else { return }
|
|
self.log("ERROR - failed checkNetworkConnection attempt #\(attempt): \(error)")
|
|
if attempt < 3 {
|
|
DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + (attempt == 1 ? 5 : 15)) {
|
|
self.checkNetworkConnection(callback: callback, attempt: attempt + 1)
|
|
}
|
|
} else {
|
|
self.log("ERROR - failed checkNetworkConnection attempt #\(attempt): \(error)")
|
|
callback(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func makeNetworkConnection() throws -> Promise<(data: Data, response: URLResponse)> {
|
|
return URLSession.shared.dataTask(.promise, with: try Client.makeGetRequest(urlString: "https://apple.com"))
|
|
}
|
|
|
|
func validateNetworkResponse(response: URLResponse?) throws {
|
|
self.log("validating checkNetworkConnection response")
|
|
if let resp = response as? HTTPURLResponse {
|
|
if (resp.statusCode >= 400 || resp.statusCode <= 0) {
|
|
self.log("response has bad status code \(resp.statusCode)")
|
|
throw "response has bad status code \(resp.statusCode)"
|
|
}
|
|
else {
|
|
self.log("response has good status code (2xx, 3xx) and no error code")
|
|
}
|
|
}
|
|
else {
|
|
throw "Invalid URL Response received: \(String(describing: response))"
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension PacketTunnelProvider {
|
|
|
|
#if DEBUG
|
|
static let debugLogsKey = AppGroupStorage.Key<[String]>(rawValue: "com.confirmed.packettunnelprovider.debuglogs")
|
|
|
|
func debugLog(_ string: String) {
|
|
let string = "DEBUG LOG \(PacketTunnelProviderLogs.dateFormatter.string(from: Date())) \(string)"
|
|
if var existing = AppGroupStorage.shared.read(key: PacketTunnelProvider.debugLogsKey) {
|
|
existing.append(string)
|
|
AppGroupStorage.shared.write(content: existing, key: PacketTunnelProvider.debugLogsKey)
|
|
} else {
|
|
AppGroupStorage.shared.write(content: [string], key: PacketTunnelProvider.debugLogsKey)
|
|
}
|
|
}
|
|
|
|
func flushDebugLogsToPacketTunnelProviderLogs() {
|
|
if let existing = AppGroupStorage.shared.read(key: PacketTunnelProvider.debugLogsKey) {
|
|
for entry in existing {
|
|
PacketTunnelProviderLogs.log(entry)
|
|
}
|
|
AppGroupStorage.shared.delete(forKey: PacketTunnelProvider.debugLogsKey)
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
open class Resolver {
|
|
|
|
fileprivate var state = __res_9_state()
|
|
|
|
public init() {
|
|
res_9_ninit(&state)
|
|
}
|
|
|
|
deinit {
|
|
res_9_ndestroy(&state)
|
|
}
|
|
|
|
public final func getservers() -> [res_9_sockaddr_union] {
|
|
|
|
let maxServers = 10
|
|
var servers = [res_9_sockaddr_union](repeating: res_9_sockaddr_union(), count: maxServers)
|
|
let found = Int(res_9_getservers(&state, &servers, Int32(maxServers)))
|
|
|
|
// filter is to remove the erroneous empty entry when there's no real servers
|
|
return Array(servers[0 ..< found]).filter() { $0.sin.sin_len > 0 }
|
|
}
|
|
}
|
|
|
|
extension Resolver {
|
|
public static func getnameinfo(_ s: res_9_sockaddr_union) -> String {
|
|
var s = s
|
|
var hostBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
|
|
|
|
let sinlen = socklen_t(s.sin.sin_len)
|
|
let _ = withUnsafePointer(to: &s) {
|
|
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
|
|
Darwin.getnameinfo($0, sinlen,
|
|
&hostBuffer, socklen_t(hostBuffer.count),
|
|
nil, 0,
|
|
NI_NUMERICHOST)
|
|
}
|
|
}
|
|
|
|
return String(cString: hostBuffer)
|
|
}
|
|
}
|
|
|
|
extension NEProviderStopReason: CustomDebugStringConvertible {
|
|
|
|
public var debugDescription: String {
|
|
switch self {
|
|
case .none:
|
|
return "none"
|
|
case .userInitiated:
|
|
return "userInitiated"
|
|
case .providerFailed:
|
|
return "providerFailed"
|
|
case .noNetworkAvailable:
|
|
return "noNetworkAvailable"
|
|
case .unrecoverableNetworkChange:
|
|
return "unrecoverableNetworkChange"
|
|
case .providerDisabled:
|
|
return "providerDisabled"
|
|
case .authenticationCanceled:
|
|
return "authenticationCanceled"
|
|
case .configurationFailed:
|
|
return "configurationFailed"
|
|
case .idleTimeout:
|
|
return "idleTimeout"
|
|
case .configurationDisabled:
|
|
return "configurationDisabled"
|
|
case .configurationRemoved:
|
|
return "configurationRemoved"
|
|
case .superceded:
|
|
return "superceded"
|
|
case .userLogout:
|
|
return "userLogout"
|
|
case .userSwitch:
|
|
return "userSwitch"
|
|
case .connectionFailed:
|
|
return "connectionFailed"
|
|
case .sleep:
|
|
return "sleep"
|
|
case .appUpdate:
|
|
return "appUpdate"
|
|
case .internalError:
|
|
return "internalError"
|
|
}
|
|
}
|
|
}
|