-
Notifications
You must be signed in to change notification settings - Fork 85
/
FirewallController.swift
229 lines (210 loc) · 9.19 KB
/
FirewallController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//
// FirewallController.swift
// Lockdown
//
// Copyright © 2019 Confirmed, Inc. All rights reserved.
//
import UIKit
import NetworkExtension
import CocoaLumberjackSwift
import PromiseKit
import WidgetKit
let kFirewallTunnelLocalizedDescription = "Lockdown Configuration"
class FirewallController: NSObject {
static let shared = FirewallController()
var manager: NETunnelProviderManager?
private override init() {
super.init()
refreshManager()
}
func refreshManager(completion: @escaping (_ error: Error?) -> Void = {_ in }) {
// get the reference to the latest manager in Settings
NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in
if let managers = managers, managers.count > 0 {
if (self.manager == managers[0]) {
DDLogInfo("Encountered same manager while refreshing manager, not replacing it.")
} else {
self.manager = nil
self.manager = managers[0]
}
completion(nil)
}
completion(error)
}
}
func existingManagerCount(completion: @escaping (Int?) -> Void) {
NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
completion(managers?.count)
}
}
func status() -> NEVPNStatus {
if let manager {
return manager.connection.status
}
else {
return .invalid
}
}
func deleteConfigurationAndAddAgain() {
refreshManager { (error) in
self.manager?.removeFromPreferences(completionHandler: { (removeError) in
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.setEnabled(true, isUserExplicitToggle: true)
}
})
}
}
func restart(completion: @escaping (_ error: Error?) -> Void = {_ in }) {
DDLogInfo("FirewallController.restart called")
// Don't let this affect userWantsFirewallOn/Off config
FirewallController.shared.setEnabled(false, completion: {
error in
DDLogInfo("FirewallController.restart completed disabling")
// TODO: Handle the error (throw?)
if error != nil {
DDLogError("Error disabling on Firewall restart: \(error!)")
}
// waiting for a little bit before re-enabling:
// without it, sometimes Firewall fails to enable
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
DDLogInfo("FirewallController.restart wait completed")
FirewallController.shared.setEnabled(true, completion: {
error in
DDLogInfo("FirewallController.restart completed enabling")
if error != nil {
DDLogError("Error enabling on Firewall restart: \(error!)")
}
completion(error)
})
}
})
}
struct CombinedBlockListEmptyError: Error { }
private func handleUserDeniedAccessToFirewallConfiguration() {
setUserWantsFirewallEnabled(false)
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
WidgetCenter.shared.reloadAllTimelines()
}
manager = nil
}
func setEnabled(_ enabled: Bool, isUserExplicitToggle: Bool = false, completion: @escaping (_ error: Error?) -> Void = {_ in }) {
DDLogInfo("FirewallController set enabled: \(enabled)")
// only change this boolean if it's user action
if (isUserExplicitToggle) {
setUserWantsFirewallEnabled(enabled)
if #available(iOSApplicationExtension 14.0, iOS 14.0, *) {
WidgetCenter.shared.reloadAllTimelines()
}
}
if enabled && getIsCombinedBlockListEmpty() {
DDLogError("Trying to enable Firewall when combined block list is empty; not allowing")
completion(FirewallController.CombinedBlockListEmptyError())
assertionFailure("Trying to enable Firewall when combined block list is empty; not allowing. This crash only happens in DEBUG mode")
return
}
// just to be sure, reload the managers to make sure we don't make multiple configs
NETunnelProviderManager.loadAllFromPreferences { (managers, error) -> Void in
if let managers = managers, managers.count > 0 {
self.manager = nil
self.manager = managers[0]
}
else {
self.manager = nil
self.manager = NETunnelProviderManager()
self.manager?.protocolConfiguration = NETunnelProviderProtocol()
}
self.manager?.localizedDescription = kFirewallTunnelLocalizedDescription
self.manager?.protocolConfiguration?.serverAddress = kFirewallTunnelLocalizedDescription
self.manager?.isEnabled = enabled
self.manager?.isOnDemandEnabled = enabled
self.manager?.protocolConfiguration?.disconnectOnSleep = false
let connectRule = NEOnDemandRuleConnect()
connectRule.interfaceTypeMatch = .any
self.manager?.onDemandRules = [connectRule]
self.manager?.saveToPreferences(completionHandler: { [weak self] (error) -> Void in
// TODO: Handle each case specifically
if let e = error as? NEVPNError {
DDLogError("VPN Error while saving state: \(enabled) \(e)")
switch e.code {
case .configurationDisabled:
break;
case .configurationInvalid:
break;
case .configurationReadWriteFailed:
self?.handleUserDeniedAccessToFirewallConfiguration()
return
case .configurationStale:
break;
case .configurationUnknown:
break;
case .connectionFailed:
break;
}
completion(e)
}
else if let e = error {
DDLogError("Error saving config for enabled state: \(enabled): \(e)")
completion(e)
}
else {
self?.loadFromPreferenceAndStartFirewall(enabled, completion: completion)
}
})
}
}
private func loadFromPreferenceAndStartFirewall(_ enabled: Bool, completion: @escaping (_ error: Error?) -> Void) {
manager?.loadFromPreferences { [weak self] error in
if let error {
DDLogError("Read preference error before start firewall: " + error.localizedDescription)
}
DDLogInfo("Successfully saved config for enabled state: \(enabled)")
// manually activate the starting of the tunnel, and also do a dummy connect to a nonexistant, invalid URL to force enabling
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if (enabled) {
self?.startFirewallTunnel(completion: completion)
}
else {
DDLogInfo("FirewallController.setEnabled not enabled, no need to call startVPNTunnel")
completion(nil)
}
}
}
}
private func startFirewallTunnel(completion: @escaping (_ error: Error?) -> Void) {
guard let manager else {
DDLogInfo("FirewallController.setEnabled ignore: empty manager")
completion(nil)
return
}
DDLogInfo("FirewallController.setEnabled enabled, calling startVPNTunnel")
do {
try manager.connection.startVPNTunnel()
let config = URLSessionConfiguration.default
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
let session = URLSession.init(configuration: config)
if let url = URL(string: "https://nonexistant_invalid_url") {
let task = session.dataTask(with: url) { (data, response, error) in
DDLogInfo("FirewallController.setEnabled response from calling nonexistant url")
return
}
DDLogInfo("FirewallController.setEnabled calling nonexistant url")
task.resume()
}
DDLogInfo("FirewallController.setEnabled refreshing manager")
refreshManager(completion: { error in
if let error {
DDLogInfo("FirewallController.setEnabled error response from refreshing manager: \(error)")
}
else {
DDLogInfo("FirewallController.setEnabled no error from refreshing manager")
}
completion(nil)
})
}
catch {
DDLogError("Unable to start the tunnel after saving: " + error.localizedDescription)
completion(error.localizedDescription)
}
}
}