Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent endless rekey and tunnel update cycle #897

Merged
merged 8 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions Sources/Common/Concurrency/TaskExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ public extension Task where Success == Never, Failure == Error {
}
}
}

static func periodic(delay: TimeInterval? = nil,
interval: TimeInterval,
operation: @escaping @Sendable () async throws -> Void,
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
cancellationHandler: (@Sendable () async -> Void)? = nil) -> Task {

Task {
do {
if let delay {
try await Task<Never, Never>.sleep(interval: delay)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to simplify this down to Task.sleep I believe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll keep it because of this, but not sure why that's even needed. 🤷

Screenshot 2024-07-30 at 11 57 48 AM

}

repeat {
try await operation()

try await Task<Never, Never>.sleep(interval: interval)
} while true
} catch {
await cancellationHandler?()
throw error
}
}
}
}

public extension Task where Success == Never, Failure == Never {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public actor NetworkProtectionEntitlementMonitor {

// MARK: - Start/Stop monitoring

public func start(entitlementCheck: @escaping () async -> Swift.Result<Bool, Error>, callback: @escaping (Result) -> Void) {
public func start(entitlementCheck: @escaping () async -> Swift.Result<Bool, Error>, callback: @escaping (Result) async -> Void) {
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
os_log("⚫️ Starting entitlement monitor", log: .networkProtectionEntitlementMonitorLog)

task = Task.periodic(interval: Self.monitoringInterval) {
Expand All @@ -61,14 +61,14 @@ public actor NetworkProtectionEntitlementMonitor {
case .success(let hasEntitlement):
if hasEntitlement {
os_log("⚫️ Valid entitlement", log: .networkProtectionEntitlementMonitorLog)
callback(.validEntitlement)
await callback(.validEntitlement)
} else {
os_log("⚫️ Invalid entitlement", log: .networkProtectionEntitlementMonitorLog)
callback(.invalidEntitlement)
await callback(.invalidEntitlement)
}
case .failure(let error):
os_log("⚫️ Error retrieving entitlement: %{public}@", log: .networkProtectionEntitlementMonitorLog, error.localizedDescription)
callback(.error(error))
await callback(.error(error))
}
}
}
Expand Down
140 changes: 79 additions & 61 deletions Sources/NetworkProtection/PacketTunnelProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,20 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
providerEvents.fire(.rekeyAttempt(.success))
} catch {
providerEvents.fire(.rekeyAttempt(.failure(error)))
await subscriptionAccessErrorHandler(error)
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
throw error
}
}

private func subscriptionAccessErrorHandler(_ error: Error) async {
switch error {
case TunnelError.vpnAccessRevoked:
await handleAccessRevoked(attemptsShutdown: true)
default:
break
}
}

private func setKeyValidity(_ interval: TimeInterval?) {
if let interval {
let firstExpirationDate = Date().addingTimeInterval(interval)
Expand Down Expand Up @@ -595,7 +605,18 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
settings.changePublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] change in
self?.handleSettingsChange(change)
guard let self else { return }
let handleSettingsChange = handleSettingsChange
let subscriptionAccessErrorHandler = subscriptionAccessErrorHandler

Task { @MainActor in
do {
try await handleSettingsChange(change)
} catch {
await subscriptionAccessErrorHandler(error)
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
throw error
}
}
}.store(in: &cancellables)
}

Expand Down Expand Up @@ -656,7 +677,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}

os_log("🔴 Stopping VPN due to no auth token: %{public}s", log: .networkProtection)
await attemptShutdown()
await attemptShutdownDueToRevokedAccess()

throw error
}
Expand Down Expand Up @@ -943,7 +964,6 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
)
} catch {
if isSubscriptionEnabled, let error = error as? NetworkProtectionError, case .vpnAccessRevoked = error {
await handleInvalidEntitlement(attemptsShutdown: true)
throw TunnelError.vpnAccessRevoked
}

Expand Down Expand Up @@ -1044,16 +1064,14 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
settings.apply(change: change)
}

private func handleSettingsChange(_ change: VPNSettings.Change, completionHandler: ((Data?) -> Void)? = nil) {
@MainActor
private func handleSettingsChange(_ change: VPNSettings.Change) async throws {
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
switch change {
case .setExcludeLocalNetworks:
Task { @MainActor in
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(currentServerSelectionMethod),
reassert: false)
}
completionHandler?(nil)
if case .connected = connectionStatus {
try await updateTunnelConfiguration(
updateMethod: .selectServer(currentServerSelectionMethod),
reassert: false)
}
case .setSelectedServer(let selectedServer):
let serverSelectionMethod: NetworkProtectionServerSelectionMethod
Expand All @@ -1065,13 +1083,10 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
serverSelectionMethod = .preferredServer(serverName: serverName)
}

Task { @MainActor in
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(serverSelectionMethod),
reassert: true)
}
completionHandler?(nil)
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(serverSelectionMethod),
reassert: true)
}
case .setSelectedLocation(let selectedLocation):
let serverSelectionMethod: NetworkProtectionServerSelectionMethod
Expand All @@ -1083,22 +1098,16 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
serverSelectionMethod = .preferredLocation(location)
}

Task { @MainActor in
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(serverSelectionMethod),
reassert: true)
}
completionHandler?(nil)
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(serverSelectionMethod),
reassert: true)
}
case .setDNSSettings:
Task { @MainActor in
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(currentServerSelectionMethod),
reassert: true)
}
completionHandler?(nil)
if case .connected = connectionStatus {
try? await updateTunnelConfiguration(
updateMethod: .selectServer(currentServerSelectionMethod),
reassert: true)
}
case .setConnectOnLogin,
.setIncludeAllNetworks,
Expand All @@ -1109,7 +1118,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
.setShowInMenuBar,
.setDisableRekeying:
// Intentional no-op, as some setting changes don't require any further operation
completionHandler?(nil)
break
}
}

Expand All @@ -1124,7 +1133,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
handleSendTestNotification(completionHandler: completionHandler)
case .disableConnectOnDemandAndShutDown:
Task { [weak self] in
await self?.attemptShutdown()
await self?.attemptShutdownDueToRevokedAccess()
completionHandler?(nil)
}
case .removeVPNConfiguration:
Expand Down Expand Up @@ -1245,26 +1254,23 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}

@available(iOS 17, *)
public func handleShutDown(completionHandler: ((Data?) -> Void)? = nil) {
Task { @MainActor in
let managers = try await NETunnelProviderManager.loadAllFromPreferences()

guard let manager = managers.first else {
completionHandler?(nil)
return
}

os_log("⚪️ Disabling Connect On Demand and shutting down the tunnel", log: .networkProtection)
@MainActor
public func handleShutDown() async throws {
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
os_log("🔴 Disabling Connect On Demand and shutting down the tunnel", log: .networkProtection)
let managers = try await NETunnelProviderManager.loadAllFromPreferences()

manager.isOnDemandEnabled = false
try await manager.saveToPreferences()
try await manager.loadFromPreferences()
guard let manager = managers.first else {
os_log("Could not find a viable manager, bailing out of shutdown", log: .networkProtection)
// Doesn't matter a lot what error we throw here, since we'll try cancelling the
// tunnel.
throw TunnelError.vpnAccessRevoked
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
}

let error = NSError(domain: "com.duckduckgo.vpn", code: 0)
await cancelTunnel(with: error)
manager.isOnDemandEnabled = false
try await manager.loadFromPreferences()
try await manager.saveToPreferences()
samsymons marked this conversation as resolved.
Show resolved Hide resolved

completionHandler?(nil)
}
await cancelTunnel(with: TunnelError.vpnAccessRevoked)
}

private func setIncludedRoutes(_ includedRoutes: [IPAddressRange], completionHandler: ((Data?) -> Void)? = nil) {
Expand Down Expand Up @@ -1451,9 +1457,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
/// Ignore otherwise
switch result {
case .invalidEntitlement:
Task { [weak self] in
await self?.handleInvalidEntitlement(attemptsShutdown: true)
}
await self?.handleAccessRevoked(attemptsShutdown: true)
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved
case .validEntitlement, .error:
break
}
Expand Down Expand Up @@ -1492,7 +1496,7 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
}

@MainActor
private func handleInvalidEntitlement(attemptsShutdown: Bool) async {
private func handleAccessRevoked(attemptsShutdown: Bool) async {
defaults.enableEntitlementMessaging()
notificationsPresenter.showEntitlementNotification()

Expand All @@ -1502,18 +1506,32 @@ open class PacketTunnelProvider: NEPacketTunnelProvider {
try? await Task.sleep(interval: .seconds(5))

if attemptsShutdown {
await attemptShutdown()
await attemptShutdownDueToRevokedAccess()
}
}

// Attempt to shut down the tunnel
// On iOS 16 and below, as a workaround, we rekey to force a 403 error so that the tunnel fails to restart
/// Tries to shut down the tunnel after access has been revoked.
///
/// iOS 17+ supports disabling on-demand, but macOS does not... so we resort to removing the subscription token
/// which should prevent the VPN from even trying to start.
///
@MainActor
private func attemptShutdown() async {
private func attemptShutdownDueToRevokedAccess() async {
let cancelTunnel = {
#if os(macOS)
try? self.tokenStore.deleteToken()
#endif
Comment on lines +1521 to +1523
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check for system extension vs app extension here? I think it should be fine as-is, but checking anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, if access is revoked it should be fine regardless. The only risk I see is if we get false positives as access-revoked, but then the problem would still not be this code, if that makes sense.

self.cancelTunnelWithError(TunnelError.vpnAccessRevoked)
}
diegoreymendez marked this conversation as resolved.
Show resolved Hide resolved

if #available(iOS 17, *) {
handleShutDown()
do {
try await handleShutDown()
} catch {
cancelTunnel()
}
} else {
try? await rekey()
cancelTunnel()
}
}

Expand Down
Loading