summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBug Magnet <marco.nikic@mullvad.net>2025-09-17 12:20:42 +0200
committerBug Magnet <marco.nikic@mullvad.net>2025-09-19 14:07:33 +0200
commit25dfd440040d759c81ecd754a27fa4d95c543563 (patch)
tree7e1ecc7ffe891c6d9ca23a06268e9cf918bda838
parentacdbc96d329461fdc63568e4b5827308a0e5516b (diff)
downloadmullvadvpn-25dfd440040d759c81ecd754a27fa4d95c543563.tar.xz
mullvadvpn-25dfd440040d759c81ecd754a27fa4d95c543563.zip
Add routine that clears shadowsocks caches on API rotation and failure
-rw-r--r--ios/MullvadREST/Transport/Shadowsocks/ShadowsocksCacheCleaner.swift26
-rw-r--r--ios/MullvadRESTTests/MullvadApiTests.swift3
-rw-r--r--ios/MullvadRESTTests/ShadowsocksCacheCleanerTests.swift87
-rw-r--r--ios/MullvadRustRuntime/MullvadApiContext.swift8
-rw-r--r--ios/MullvadSettings/AccessMethodRepository.swift2
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj12
-rw-r--r--ios/MullvadVPN/AppDelegate.swift7
-rw-r--r--ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift3
-rw-r--r--ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift5
9 files changed, 142 insertions, 11 deletions
diff --git a/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksCacheCleaner.swift b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksCacheCleaner.swift
new file mode 100644
index 0000000000..05af069fad
--- /dev/null
+++ b/ios/MullvadREST/Transport/Shadowsocks/ShadowsocksCacheCleaner.swift
@@ -0,0 +1,26 @@
+//
+// ShadowsocksCacheCleaner.swift
+// MullvadREST
+//
+// Created by Marco Nikic on 2025-09-18.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+import Foundation
+import MullvadSettings
+
+public class ShadowsocksCacheCleaner: MullvadAccessMethodChangeListening {
+ let cache: ShadowsocksConfigurationCacheProtocol
+ var lastChangedUUID = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
+
+ public init(cache: ShadowsocksConfigurationCacheProtocol) {
+ self.cache = cache
+ }
+
+ public func accessMethodChangedTo(_ uuid: UUID) {
+ if lastChangedUUID == AccessMethodRepository.bridgeId {
+ try? cache.clear()
+ }
+ lastChangedUUID = uuid
+ }
+}
diff --git a/ios/MullvadRESTTests/MullvadApiTests.swift b/ios/MullvadRESTTests/MullvadApiTests.swift
index 77d323adbc..9b0fe9a638 100644
--- a/ios/MullvadRESTTests/MullvadApiTests.swift
+++ b/ios/MullvadRESTTests/MullvadApiTests.swift
@@ -40,7 +40,8 @@ class MullvadApiTests: XCTestCase {
methods: accessMethodsRepository
.fetchAll()
),
- addressCacheProvider: addressCache
+ addressCacheProvider: addressCache,
+ accessMethodChangeListeners: []
)
let proxy = REST.MullvadAPIProxy(
diff --git a/ios/MullvadRESTTests/ShadowsocksCacheCleanerTests.swift b/ios/MullvadRESTTests/ShadowsocksCacheCleanerTests.swift
new file mode 100644
index 0000000000..2b52c30065
--- /dev/null
+++ b/ios/MullvadRESTTests/ShadowsocksCacheCleanerTests.swift
@@ -0,0 +1,87 @@
+//
+// ShadowsocksCacheCleanerTests.swift
+// MullvadRESTTests
+//
+// Created by Marco Nikic on 2025-09-18.
+// Copyright © 2025 Mullvad VPN AB. All rights reserved.
+//
+
+@testable import MullvadREST
+@testable import MullvadSettings
+@testable import MullvadTypes
+import Network
+import Testing
+
+actor ShadowsocksCacheCleanerTests {
+ var cache = ShadowsocksCacheStub(configuration:
+ ShadowsocksConfiguration(
+ address: .ipv4(IPv4Address.loopback),
+ port: 1234,
+ password: "password",
+ cipher: "chacha20"
+ )
+ )
+
+ deinit {
+ cache.onRead = nil
+ cache.onWrite = nil
+ cache.onClear = nil
+ }
+
+ @Test func storesLastAccessMethodUUID() async throws {
+ let cacheCleaner = ShadowsocksCacheCleaner(cache: cache)
+ let newMethodUUID = UUID()
+
+ cacheCleaner.accessMethodChangedTo(newMethodUUID)
+ #expect(newMethodUUID == cacheCleaner.lastChangedUUID)
+ }
+
+ @Test func clearsCacheWhenPreviousChangeWasShadowsocksUUID() async throws {
+ let bridges = AccessMethodRepository.bridgeId
+ let direct = AccessMethodRepository.directId
+
+ await confirmation("Did clear cache") { didClearCache in
+ cache.onClear = {
+ didClearCache()
+ }
+ let cacheCleaner = ShadowsocksCacheCleaner(cache: cache)
+ cacheCleaner.accessMethodChangedTo(bridges)
+ cacheCleaner.accessMethodChangedTo(direct)
+ }
+ }
+
+ @Test func doesNotClearCacheWhenOtherMethodsChange() async throws {
+ let encryptedDNS = AccessMethodRepository.encryptedDNSId
+ let direct = AccessMethodRepository.directId
+
+ await confirmation("Did clear cache", expectedCount: 0) { didClearCache in
+ let cacheCleaner = ShadowsocksCacheCleaner(cache: cache)
+ cache.onClear = {
+ didClearCache()
+ }
+ cacheCleaner.accessMethodChangedTo(encryptedDNS)
+ cacheCleaner.accessMethodChangedTo(direct)
+ }
+ }
+}
+
+struct ShadowsocksCacheStub: ShadowsocksConfigurationCacheProtocol {
+ let configuration: ShadowsocksConfiguration
+
+ var onRead: (@Sendable () -> Void)?
+ var onWrite: (@Sendable () -> Void)?
+ var onClear: (@Sendable () -> Void)?
+
+ func read() throws -> ShadowsocksConfiguration {
+ onRead?()
+ return configuration
+ }
+
+ func write(_ configuration: ShadowsocksConfiguration) throws {
+ onWrite?()
+ }
+
+ func clear() throws {
+ onClear?()
+ }
+}
diff --git a/ios/MullvadRustRuntime/MullvadApiContext.swift b/ios/MullvadRustRuntime/MullvadApiContext.swift
index d2d289b951..66d40d800a 100644
--- a/ios/MullvadRustRuntime/MullvadApiContext.swift
+++ b/ios/MullvadRustRuntime/MullvadApiContext.swift
@@ -14,7 +14,7 @@ func onAccessChangeCallback(selfPtr: UnsafeRawPointer?, bytes: UnsafePointer<UIn
let context = Unmanaged<MullvadApiContext>.fromOpaque(selfPtr).takeUnretainedValue()
let uuid = NSUUID(uuidBytes: bytes) as UUID
- context.accessMethodChangeListener?.accessMethodChangedTo(uuid)
+ context.accessMethodChangeListeners.forEach { $0.accessMethodChangedTo(uuid) }
}
public class MullvadApiContext: @unchecked Sendable {
@@ -27,7 +27,7 @@ public class MullvadApiContext: @unchecked Sendable {
private let shadowsocksBridgeProviderWrapper: SwiftShadowsocksLoaderWrapper!
private let addressCacheWrapper: SwiftAddressCacheWrapper!
private let addressCacheProvider: AddressCacheProviding!
- public var accessMethodChangeListener: MullvadAccessMethodChangeListening?
+ public let accessMethodChangeListeners: [MullvadAccessMethodChangeListening]
public init(
host: String,
@@ -36,7 +36,8 @@ public class MullvadApiContext: @unchecked Sendable {
disableTls: Bool = false,
shadowsocksProvider: SwiftShadowsocksBridgeProviding,
accessMethodWrapper: SwiftAccessMethodSettingsWrapper,
- addressCacheProvider: AddressCacheProviding
+ addressCacheProvider: AddressCacheProviding,
+ accessMethodChangeListeners: [MullvadAccessMethodChangeListening]
) throws {
let bridgeProvider = SwiftShadowsocksBridgeProvider(provider: shadowsocksProvider)
self.shadowsocksBridgeProvider = bridgeProvider
@@ -45,6 +46,7 @@ public class MullvadApiContext: @unchecked Sendable {
let defaultAddressCache = DefaultAddressCacheProvider(provider: addressCacheProvider)
self.addressCacheProvider = defaultAddressCache
self.addressCacheWrapper = iniSwiftAddressCacheWrapper(provider: defaultAddressCache)
+ self.accessMethodChangeListeners = accessMethodChangeListeners
let selfPtr = Unmanaged.passUnretained(self).toOpaque()
context = switch disableTls {
diff --git a/ios/MullvadSettings/AccessMethodRepository.swift b/ios/MullvadSettings/AccessMethodRepository.swift
index c39fd20db9..2523dc9d5d 100644
--- a/ios/MullvadSettings/AccessMethodRepository.swift
+++ b/ios/MullvadSettings/AccessMethodRepository.swift
@@ -206,7 +206,7 @@ extension AccessMethodRepository: MullvadAccessMethodChangeListening {
}
Task {
- print("Mullvad API changed access method to \(method.name)")
+ logger.debug("Mullvad API changed access method to \(method.name)")
currentAccessMethodSubject.send(method)
}
}
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj
index 191542b5e9..6df02389f8 100644
--- a/ios/MullvadVPN.xcodeproj/project.pbxproj
+++ b/ios/MullvadVPN.xcodeproj/project.pbxproj
@@ -772,6 +772,8 @@
A9173C322C36CCDD00F6A08C /* EphemeralPeerReceiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A557F42B7E3E5C0017ADA8 /* EphemeralPeerReceiver.swift */; };
A9173C372C36CD2B00F6A08C /* MullvadTypes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58D223D5294C8E5E0029F5F8 /* MullvadTypes.framework */; platformFilter = ios; };
A91D78E42B03C01600FCD5D3 /* MullvadSettings.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 58B2FDD32AA71D2A003EB5C6 /* MullvadSettings.framework */; };
+ A91E26E02E7BE41F00440AB8 /* ShadowsocksCacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E26DF2E7BE41F00440AB8 /* ShadowsocksCacheCleaner.swift */; };
+ A91E26E22E7BE4B300440AB8 /* ShadowsocksCacheCleanerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E26E12E7BE4B300440AB8 /* ShadowsocksCacheCleanerTests.swift */; };
A91EBEDA2C1337040004A84D /* RetryStrategyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91EBED92C1337040004A84D /* RetryStrategyTests.swift */; };
A93181A12B727ED700E341D2 /* TunnelSettingsV4.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93181A02B727ED700E341D2 /* TunnelSettingsV4.swift */; };
A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A932D9F22B5EB61100999395 /* HeadRequestTests.swift */; };
@@ -2346,6 +2348,8 @@
A90C48682C36BF3900DCB94C /* TunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelProvider.swift; sourceTree = "<group>"; };
A91614D02B108D1B00F416EB /* TransportLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportLayer.swift; sourceTree = "<group>"; };
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransportStrategyTests.swift; sourceTree = "<group>"; };
+ A91E26DF2E7BE41F00440AB8 /* ShadowsocksCacheCleaner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksCacheCleaner.swift; sourceTree = "<group>"; };
+ A91E26E12E7BE4B300440AB8 /* ShadowsocksCacheCleanerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowsocksCacheCleanerTests.swift; sourceTree = "<group>"; };
A91EBED92C1337040004A84D /* RetryStrategyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetryStrategyTests.swift; sourceTree = "<group>"; };
A92962582B1F4FDB00DFB93B /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
A92ECC202A77FFAF0052F1B1 /* TunnelSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelSettings.swift; sourceTree = "<group>"; };
@@ -4255,13 +4259,14 @@
58FBFBE7291622580020E046 /* MullvadRESTTests */ = {
isa = PBXGroup;
children = (
- F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */,
- F924C4522D706929001F4660 /* MullvadApiTests.swift */,
58FBFBE8291622580020E046 /* ExponentialBackoffTests.swift */,
A932D9F22B5EB61100999395 /* HeadRequestTests.swift */,
58BDEB9E2A98F6B400F578F2 /* Mocks */,
+ F924C4522D706929001F4660 /* MullvadApiTests.swift */,
58B4656F2A98C53300467203 /* RequestExecutorTests.swift */,
A91EBED92C1337040004A84D /* RetryStrategyTests.swift */,
+ F924C65E2DAE4554001F4660 /* ServerRelayTests.swift */,
+ A91E26E12E7BE4B300440AB8 /* ShadowsocksCacheCleanerTests.swift */,
F0164EC22B4C49D30020268D /* ShadowsocksLoaderStub.swift */,
A917352029FAAA5200D5DCFD /* TransportStrategyTests.swift */,
);
@@ -4874,6 +4879,7 @@
F0DC77A22B2314EF0087F09D /* Shadowsocks */ = {
isa = PBXGroup;
children = (
+ A91E26DF2E7BE41F00440AB8 /* ShadowsocksCacheCleaner.swift */,
F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */,
F0164EBD2B4BFF940020268D /* ShadowsocksLoader.swift */,
F01528BA2BFF3FEE00B01D00 /* ShadowsocksRelaySelector.swift */,
@@ -6026,6 +6032,7 @@
F0F3161B2BF358590078DBCF /* NoRelaysSatisfyingConstraintsError.swift in Sources */,
06799AE028F98E4800ACD94E /* RESTCoding.swift in Sources */,
A90763B72B2857D50045ADF0 /* Socks5DataStreamHandler.swift in Sources */,
+ A91E26E02E7BE41F00440AB8 /* ShadowsocksCacheCleaner.swift in Sources */,
A90763B22B2857D50045ADF0 /* Socks5EndpointReader.swift in Sources */,
A90763B42B2857D50045ADF0 /* NWConnection+Extensions.swift in Sources */,
F06045EA2B23217E00B2D37A /* ShadowsocksTransport.swift in Sources */,
@@ -6946,6 +6953,7 @@
58BDEB9D2A98F69E00F578F2 /* MemoryCache.swift in Sources */,
58BDEB9B2A98F58600F578F2 /* TimeServerProxy.swift in Sources */,
A932D9F52B5EBB9D00999395 /* RESTTransportStub.swift in Sources */,
+ A91E26E22E7BE4B300440AB8 /* ShadowsocksCacheCleanerTests.swift in Sources */,
A91EBEDA2C1337040004A84D /* RetryStrategyTests.swift in Sources */,
58BDEB992A98F4ED00F578F2 /* AnyTransport.swift in Sources */,
A932D9F32B5EB61100999395 /* HeadRequestTests.swift in Sources */,
diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift
index 488c6f4003..f45739eaaa 100644
--- a/ios/MullvadVPN/AppDelegate.swift
+++ b/ios/MullvadVPN/AppDelegate.swift
@@ -55,6 +55,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private var encryptedDNSTransport: EncryptedDNSTransport!
var apiContext: MullvadApiContext!
var accessMethodReceiver: MullvadAccessMethodReceiver!
+ private var shadowsocksCacheCleaner: ShadowsocksCacheCleaner!
// MARK: - Application lifecycle
@@ -102,6 +103,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
shadowsocksLoader: shadowsocksLoader
)
+ shadowsocksCacheCleaner = ShadowsocksCacheCleaner(cache: shadowsocksCache)
+
// swiftlint:disable:next force_try
apiContext = try! MullvadApiContext(
host: REST.defaultAPIHostname,
@@ -109,7 +112,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
domain: REST.encryptedDNSHostname,
shadowsocksProvider: shadowsocksLoader,
accessMethodWrapper: transportStrategy.opaqueAccessMethodSettingsWrapper,
- addressCacheProvider: addressCache
+ addressCacheProvider: addressCache,
+ accessMethodChangeListeners: [accessMethodRepository, shadowsocksCacheCleaner]
)
accessMethodReceiver = MullvadAccessMethodReceiver(
@@ -117,7 +121,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
accessMethodsDataSource: accessMethodRepository.accessMethodsPublisher,
requestDataSource: accessMethodRepository.requestAccessMethodPublisher
)
- apiContext.accessMethodChangeListener = accessMethodRepository
setUpProxies(containerURL: containerURL)
let backgroundTaskProvider = BackgroundTaskProvider(
diff --git a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
index cc496bba63..ac7e787e1c 100644
--- a/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
+++ b/ios/MullvadVPNTests/MullvadVPN/TunnelManager/TunnelManagerTests.swift
@@ -76,7 +76,8 @@ class TunnelManagerTests: XCTestCase {
domain: REST.encryptedDNSHostname,
shadowsocksProvider: shadowsocksLoader,
accessMethodWrapper: transportStrategy.opaqueAccessMethodSettingsWrapper,
- addressCacheProvider: addressCache
+ addressCacheProvider: addressCache,
+ accessMethodChangeListeners: []
)
try SettingsManager.writeSettings(LatestTunnelSettings())
diff --git a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
index cf405b9701..4cc5228c95 100644
--- a/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
+++ b/ios/PacketTunnel/PacketTunnelProvider/PacketTunnelProvider.swift
@@ -39,6 +39,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
var apiContext: MullvadApiContext!
var accessMethodReceiver: MullvadAccessMethodReceiver!
+ private var shadowsocksCacheCleaner: ShadowsocksCacheCleaner!
// swiftlint:disable:next function_body_length
override init() {
@@ -254,6 +255,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
)
let accessMethodRepository = AccessMethodRepository()
+ shadowsocksCacheCleaner = ShadowsocksCacheCleaner(cache: shadowsocksCache)
let transportStrategy = TransportStrategy(
datasource: accessMethodRepository,
@@ -267,7 +269,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
domain: REST.encryptedDNSHostname,
shadowsocksProvider: shadowsocksLoader,
accessMethodWrapper: transportStrategy.opaqueAccessMethodSettingsWrapper,
- addressCacheProvider: addressCache
+ addressCacheProvider: addressCache,
+ accessMethodChangeListeners: [accessMethodRepository, shadowsocksCacheCleaner]
)
accessMethodReceiver = MullvadAccessMethodReceiver(