486 lines
18 KiB
Swift
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
|
|
}
|
|
}
|