This repository was archived by the owner on Feb 24, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathVPNProxyLauncher.swift
154 lines (129 loc) · 5.43 KB
/
VPNProxyLauncher.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
//
// VPNProxyLauncher.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
import NetworkProtectionProxy
import NetworkExtension
/// Starts and stops the VPN proxy component.
///
/// This class looks at the tunnel and the proxy components and their status and settings, and decides based on
/// a number of conditions whether to start the proxy, stop it, or just leave it be.
///
@MainActor
final class VPNProxyLauncher {
private let tunnelController: NetworkProtectionTunnelController
private let proxyController: TransparentProxyController
private let notificationCenter: NotificationCenter
private var cancellables = Set<AnyCancellable>()
init(tunnelController: NetworkProtectionTunnelController,
proxyController: TransparentProxyController,
notificationCenter: NotificationCenter = .default) {
self.notificationCenter = notificationCenter
self.proxyController = proxyController
self.tunnelController = tunnelController
subscribeToStatusChanges()
subscribeToProxySettingChanges()
}
// MARK: - Status Changes
private func subscribeToStatusChanges() {
notificationCenter.publisher(for: .NEVPNStatusDidChange)
.receive(on: DispatchQueue.main)
.sink { [weak self] notification in
self?.statusChanged(notification: notification)
}
.store(in: &cancellables)
}
private func statusChanged(notification: Notification) {
Task { @MainActor in
let isProxyConnectionStatusChange = await proxyController.connection == notification.object as? NEVPNConnection
try await startOrStopProxyIfNeeded(isProxyConnectionStatusChange: isProxyConnectionStatusChange)
}
}
// MARK: - Proxy Settings Changes
private func subscribeToProxySettingChanges() {
proxyController.settings.changePublisher
.sink { [weak self] notification in
self?.proxySettingChanged(notification)
}
.store(in: &cancellables)
}
private func proxySettingChanged(_ change: TransparentProxySettings.Change) {
Task { @MainActor in
try await startOrStopProxyIfNeeded()
}
}
// MARK: - Auto starting & stopping the proxy component
private var isControllingProxy = false
private func startOrStopProxyIfNeeded(isProxyConnectionStatusChange: Bool = false) async throws {
if await shouldStartProxy {
guard !isControllingProxy else {
return
}
isControllingProxy = true
defer {
isControllingProxy = false
}
// When we're auto-starting the proxy because its own status changed to
// disconnected, we want to give it a pause because if it fails to connect again
// we risk the proxy entering a frenetic connect / disconnect loop
if isProxyConnectionStatusChange {
// If the proxy connection was stopped, let's wait a bit before trying to enable it again
try await Task.sleep(interval: .seconds(1))
// And we want to check again if the proxy still needs to start after waiting
guard await shouldStartProxy else {
return
}
}
do {
try await proxyController.start()
} catch {
throw error
}
} else if await shouldStopProxy {
guard !isControllingProxy else {
return
}
isControllingProxy = true
await proxyController.stop()
isControllingProxy = false
}
}
private var shouldStartProxy: Bool {
get async {
let proxyIsDisconnected = await proxyController.status == .disconnected
let tunnelIsConnected = await tunnelController.status == .connected
// Starting the proxy only when it's required for active features
// is a product decision. It may change once we decide the proxy
// is stable enough to be running at all times.
return proxyIsDisconnected
&& tunnelIsConnected
&& proxyController.isRequiredForActiveFeatures
}
}
private var shouldStopProxy: Bool {
get async {
let proxyIsConnected = await proxyController.status == .connected
let tunnelIsDisconnected = await tunnelController.status == .disconnected
// Stopping the proxy when it's not required for active features
// is a product decision. It may change once we decide the proxy
// is stable enough to be running at all times.
return proxyIsConnected
&& (tunnelIsDisconnected || !proxyController.isRequiredForActiveFeatures)
}
}
}