diff options
| author | Bug Magnet <marco.nikic@mullvad.net> | 2025-09-17 12:20:42 +0200 |
|---|---|---|
| committer | Bug Magnet <marco.nikic@mullvad.net> | 2025-09-19 14:07:33 +0200 |
| commit | 25dfd440040d759c81ecd754a27fa4d95c543563 (patch) | |
| tree | 7e1ecc7ffe891c6d9ca23a06268e9cf918bda838 /ios | |
| parent | acdbc96d329461fdc63568e4b5827308a0e5516b (diff) | |
| download | mullvadvpn-25dfd440040d759c81ecd754a27fa4d95c543563.tar.xz mullvadvpn-25dfd440040d759c81ecd754a27fa4d95c543563.zip | |
Add routine that clears shadowsocks caches on API rotation and failure
Diffstat (limited to 'ios')
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( |
