// // KillSwitchPresenter.swift // Privado // // Created by Juraldinio on 6/21/20. // Copyright © 2020 Privado LLC. All rights reserved. // import Foundation final class KillSwitchPresenter: KillSwitchClientDelegate { typealias KillSwitchPrepareCompletion = () -> Void private enum PacketFilterStatus { case undefined case disabled case enabled } private var prepareCompletion: KillSwitchPrepareCompletion? private var isPrepared = false private var isActivated = false private var requireCheck = false private var ipAddress = "" private var defaultDNS: [String: [String]]? private var delayedEnable: Bool? private var killSwitchEnabled = false { didSet { // Get pf status and temporary disable pf var future = self.clientAllowState() .flatMap { _ in self.firewallStatus() } .flatMap { _ in self.firewall(enable: false) } // Get current DNS settings for static predefined records and save it if !self.isActivated { future = future .flatMap { _ in self.client.currentDNS(hardware: ["Wi-Fi", "Ethernet"]) } .onResult { [weak self] result in if var defaultDNS = try? result.get() { defaultDNS.keys.forEach { key in defaultDNS[key] = defaultDNS[key]?.filter { !Self.dns.contains($0) } } self?.defaultDNS = defaultDNS } else { self?.defaultDNS = nil } }.flatMap { _ in Future(value: true) } } if self.killSwitchEnabled { self.isActivated = true } // Lookup api server ip address, enable killswitch if needed future .flatMap(self.hostLookup) .flatMap { _ in self.enableKillSwitch() } .flatMap { [weak self] enabled -> Future in guard let self = self else { return Future(value: false) } if enabled { if [.connected/*, .prepare*/].contains(self.connectionState) , let vpnService = try? Session.instance().currentVPN , let interface = vpnService.interface , let afnet = vpnService.remoteAddress { return self.client .firewall(activate: KillSwitchPresenter.createRules(forInterface: interface, afinet: afnet)) .flatMap { _ in self.client.flushDNS() } .flatMap {_ in self.client.changeDNSs(dns: Self.dns, ports: ["Wi-Fi", "Ethernet"]) } } else { return self.hostLookup() .flatMap { ip in return self.client.firewall(activate: KillSwitchPresenter.createRulesDisconnected([ip])) } } } else { // deactivate firewall, flush dns and restore dns return self.client .firewallDeactivate() .flatMap { _ in self.client.flushDNS() } .flatMap { _ in self.restoreDNS() } } }.onResult { [weak self] result in print(result) guard let self = self , !self.isPrepared else { return } self.isPrepared = true if let completion = self.prepareCompletion { self.prepareCompletion = nil completion() } } } } private var connectionState: VPNConnectionState = .disconnected private var status: PacketFilterStatus = .undefined private var client: KillSwitchClient // MARK: - Init init(client: (XPCFirewallClient & XPCSystemClient)?) { self.client = KillSwitchClient(client: client) self.client.delegate = self // Handle kill switch option changed UserSettings.shared.killSwitchEmitter.addReaction { [weak self] enabled -> ShouldContinueReceiveNotifications in guard let self = self else { return false } guard self.killSwitchEnabled != enabled else { return true } self.killSwitchEnabled = enabled return true } guard let session = try? Session.instance() else { return } // Connection state changed! session.vpnConnectionStateEmitter.addReaction { [weak self] state, service -> ShouldContinueReceiveNotifications in guard let self = self else { return false } guard self.connectionState != state else { return true } self.connectionState = state guard self.killSwitchEnabled else { return true } switch state { case .prepare: self.firewall(enable: false) case .connected: if let vpnService = service , let interface = vpnService.interface , let afnet = vpnService.remoteAddress { if vpnService.type == .openVPN , vpnService.socket == "TCP" { return true } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { self.firewall(enable: true) .flatMap { _ in self.client.changeDNSs(dns: Self.dns, ports: ["Wi-Fi", "Ethernet"]) } .flatMap { _ in return self.client.firewall(activate: KillSwitchPresenter.createRules(forInterface: interface, afinet: afnet)) } .onResult { _ in DispatchQueue.main.asyncAfter(deadline: .now() + 5) { self.client.flushDNS() } } } } case .disconnecting: self.firewall(enable: false) case .disconnected: if let vpnService = service , vpnService.type == .openVPN , vpnService.socket == "TCP" { return true } DispatchQueue.main.asyncAfter(deadline: .now() + 10) { if state != session.currentVPN?.state { self.firewall(enable: false) .flatMap { _ in self.client.changeDNSs(dns: Self.dnsEmpty, ports: ["Wi-Fi", "Ethernet"]) } .flatMap(self.hostLookup) .flatMap { _ in self.client.flushDNS() } return } self.firewall(enable: true) .flatMap { _ in self.client.changeDNSs(dns: Self.dnsEmpty, ports: ["Wi-Fi", "Ethernet"]) } .flatMap(self.hostLookup) .flatMap { ip in self.client.firewall(activate: KillSwitchPresenter.createRulesDisconnected([ip])) } .flatMap { _ in self.client.flushDNS() } } default: break } return true } session.sessionEmitter.addReaction { [weak self] record -> ShouldContinueReceiveNotifications in if !record.isExist { self?.disable() } return self.isExist } } // MARK: - Interface func prepare(completion: @escaping KillSwitchPrepareCompletion) { guard self.isPrepared else { self.prepareCompletion = completion return } completion() } @discardableResult func enable() -> Bool { guard self.isPrepared , self.isActivated else { self.delayedEnable = true return false } self.killSwitchEnabled = true return true } @discardableResult func disable() -> Future { guard self.isPrepared , self.isActivated else { self.delayedEnable = false return Future(value: false) } return self .firewall(enable: false) .flatMap { _ in self.client.changeDNSs(dns: Self.dnsEmpty, ports: ["Wi-Fi", "Ethernet"]) } } func refresh() { guard self.isPrepared else { return } if let delayed = self.delayedEnable { self.delayedEnable = nil self.killSwitchEnabled = delayed return } let killSwitchEnabled = UserSettings.shared.killSwitchEnabled if killSwitchEnabled != self.killSwitchEnabled { self.killSwitchEnabled = killSwitchEnabled } } // MARK: - KillSwitchClientDelegate func clientChangeState(state: XPCClientState) { // TODO: - need to be refactored! if state == .connected { DispatchQueue.main.async { self.killSwitchEnabled = false } } } // MARK: - Private @discardableResult private func clientAllowState() -> Future { guard self.client.state == .connected else { self.requireCheck = true if self.client.state == .requireUpdate { openApplicationRoute(.hive(type: .install)) } return Future(error: KillSwitchError.installHelper) } self.requireCheck = false return Future(value: true) } @discardableResult private func firewallStatus(operationAllowed: Bool = true) -> Future { return self.client.firewallState() .map({ enabled in Future(value: enabled ? .enabled : .disabled) }, failure: { _ in Future(value: .undefined) }).onResult { [weak self] result in guard let self = self else { return } switch result { case let .success(status): self.status = status default: break } } } @discardableResult private func enableKillSwitch() -> Future { guard self.status == .enabled && !self.killSwitchEnabled || self.status == .disabled && self.killSwitchEnabled else { return Future(value: self.killSwitchEnabled) } return self.firewall(enable: self.killSwitchEnabled) } @discardableResult private func restoreDNS() -> Future { if let defaultDNS = self.defaultDNS { return defaultDNS.keys.reduce(nil as Future?) { result, port in guard let dns = defaultDNS[port] else { return result } if let future = result { return future.flatMap { _ in self.client.changeDNS(dns: dns.isEmpty ? Self.dnsEmpty : dns, port: port) } } else { return self.client.changeDNS(dns: dns.isEmpty ? Self.dnsEmpty : dns, port: port) } } ?? Future(value: true) } else { return self.client.changeDNSs(dns: Self.dnsEmpty, ports: ["Wi-Fi", "Ethernet"]) } } @discardableResult private func hostLookup(_ param: Any? = nil) -> Future { if !self.ipAddress.isEmpty { return Future(value: self.ipAddress) } return self.client .resolveDNS(name: Environment.router.environment.host) .onResult { [weak self] result in if let ip = try? result.get() { self?.ipAddress = ip } } } @discardableResult private func firewall(enable: Bool) -> Future { self.delayedEnable = nil guard self.status != .undefined else { let error = NSError(domain: PrivadoConstants.Hive.error, code: PrivadoConstants.Hive.ErrorCode.firewallStatusUndefined, userInfo: nil) return Future(error: error) } if (self.status == .enabled && enable) || (self.status == .disabled && !enable) { return Future(value: enable) } return self.client .firewallEnable(enable) .onResult { [weak self] result in guard let self = self else { return } switch result { case let .failure(error): if let err = error as NSError? , (err.code == PrivadoConstants.Hive.ErrorCode.firewallAlreadyEnabled || err.code == PrivadoConstants.Hive.ErrorCode.firewallAlreadyDisabled) { self.status = enable ? .enabled : .disabled } else { self.status = .undefined } case .success: self.status = enable ? .enabled : .disabled } } } // MARK: - Static private static var dns = ["198.18.0.1", "198.18.0.2"] private static var dnsEmpty = ["Empty"] private static var dnsRules = dns.flatMap { return ["pass on $inet_define to \($0)", "pass on $inet_define from \($0)"] } private static var dnsVpnRules = dns.flatMap { return ["pass on $ivpn_define to \($0)", "pass on $ivpn_define from \($0)"] } private static func createRules(forInterface interface: String, afinet: String?) -> [String] { var rules = [ // Options "set block-policy drop", "set ruleset-optimization basic", "set skip on lo", "set skip on awdl0", "set skip on p2p0", // "icmptypes = {\"echoreq\", \"unreach\"}", // Interfaces "inet_define = \"en0\"", "ivpn_define = \"\(interface)\"", // the essential line // "nat on $inet_define inet from 172.20.42.0/24 to any -> $inet_define", // Block everything "block out all", "block in all", // Protection: Antispoof "antispoof for $inet_define inet", "antispoof for $ivpn_define inet", // Allow local { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } "pass in quick from { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }", "pass out quick to { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }", // Broadcast "pass from any to 255.255.255.255 keep state", "pass from 255.255.255.255 to any keep state", // Multicast "pass proto udp from any to 224.0.0.0/4 keep state", "pass proto udp from 224.0.0.0/4 to any keep state", // Incoming: DHCP "pass on $inet_define proto {tcp,udp} from any port 67:68 to any port 67:68 keep state" // // "pass on $ivpn_define in inet proto icmp all icmp-type $icmptypes" ] if let afinet = afinet { // Allow pings rules.append(contentsOf: [ "pass on $ivpn_define inet proto icmp from any to \(afinet) icmp-type 8 code 0 keep state", "pass on $ivpn_define inet proto icmp from \(afinet) to any icmp-type 8 code 0 keep state" ]) } rules.append(contentsOf: Self.dnsRules) rules.append(contentsOf: Self.dnsVpnRules) // Allow traffic for VPN if let afinet = afinet { rules.append("pass on $inet_define proto {tcp, udp} from any to \(afinet)") } rules.append("pass on $ivpn_define all") print("ACTIVATE:\n\(rules)") return rules } /* детектить примари интерфейс как я написал выше блокировать ipv6 (что хорошо делать и без КС, а просто для соединения, это тема отдельная) */ private static func createRulesDisconnected(_ afinet: [String]) -> [String] { var rules = [ // Options "set block-policy drop", "set ruleset-optimization basic", "set skip on lo0", "set skip on awdl0", "set skip on p2p0", // Interfaces "inet_define = \"en0\"", // Block everything "block out all", "block in all", // Protection: Antispoof "antispoof for $inet_define inet", // Incoming: DHCP "pass on $inet_define proto {tcp,udp} from any port 67:68 to any port 67:68 keep state" ] rules.append(contentsOf: Self.dnsRules) afinet.forEach { rules.append("pass on $inet_define to \($0) keep state") rules.append("pass on $inet_define from \($0) keep state") } print("DISCONNECTED:\n\(rules)") return rules } }