Files
2022-04-25 07:28:13 +00:00

486 lines
18 KiB
Swift

//
// 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<Bool> 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<Bool> {
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<Bool> {
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<PacketFilterStatus> {
return self.client.firewallState()
.map({ enabled in
Future<PacketFilterStatus>(value: enabled ? .enabled : .disabled)
}, failure: { _ in
Future<PacketFilterStatus>(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<Bool> {
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<Bool> {
if let defaultDNS = self.defaultDNS {
return defaultDNS.keys.reduce(nil as Future<Bool>?) { 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<String> {
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<Bool> {
self.delayedEnable = nil
guard self.status != .undefined else {
let error = NSError(domain: PrivadoConstants.Hive.error,
code: PrivadoConstants.Hive.ErrorCode.firewallStatusUndefined,
userInfo: nil)
return Future<Bool>(error: error)
}
if (self.status == .enabled && enable) ||
(self.status == .disabled && !enable) {
return Future<Bool>(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
}
}