From 1afb6ad5291864add3c5b4707e1db882a335516e Mon Sep 17 00:00:00 2001 From: Bug Magnet Date: Wed, 4 Sep 2024 17:29:48 +0200 Subject: [PATCH] Fix the cached relays to include daita information in new app version --- .../ApiHandlers/ServerRelaysResponse.swift | 29 ++++++++ ios/MullvadREST/Relay/IPOverrideWrapper.swift | 7 ++ ios/MullvadREST/Relay/RelayCache.swift | 9 ++- .../RelayCacheTracker/RelayCacheTracker.swift | 72 ++++++++++++++++--- 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift index e38ae3e23ea2..256a101857e6 100644 --- a/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift +++ b/ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift @@ -78,6 +78,22 @@ extension REST { daita: daita ) } + + public func override(daita: Bool) -> Self { + return ServerRelay( + hostname: hostname, + active: active, + owned: owned, + location: location, + provider: provider, + weight: weight, + ipv4AddrIn: ipv4AddrIn, + ipv6AddrIn: ipv6AddrIn, + publicKey: publicKey, + includeInCountry: includeInCountry, + daita: daita + ) + } } public struct ServerWireguardTunnels: Codable, Equatable { @@ -85,6 +101,13 @@ extension REST { public let ipv6Gateway: IPv6Address public let portRanges: [[UInt16]] public let relays: [ServerRelay] + + public init(ipv4Gateway: IPv4Address, ipv6Gateway: IPv6Address, portRanges: [[UInt16]], relays: [ServerRelay]) { + self.ipv4Gateway = ipv4Gateway + self.ipv6Gateway = ipv6Gateway + self.portRanges = portRanges + self.relays = relays + } } public struct ServerShadowsocks: Codable, Equatable { @@ -103,5 +126,11 @@ extension REST { public let locations: [String: ServerLocation] public let wireguard: ServerWireguardTunnels public let bridge: ServerBridges + + public init(locations: [String: ServerLocation], wireguard: ServerWireguardTunnels, bridge: ServerBridges) { + self.locations = locations + self.wireguard = wireguard + self.bridge = bridge + } } } diff --git a/ios/MullvadREST/Relay/IPOverrideWrapper.swift b/ios/MullvadREST/Relay/IPOverrideWrapper.swift index 531112cf5425..84516130d1ec 100644 --- a/ios/MullvadREST/Relay/IPOverrideWrapper.swift +++ b/ios/MullvadREST/Relay/IPOverrideWrapper.swift @@ -25,6 +25,13 @@ public class IPOverrideWrapper: RelayCacheProtocol { return CachedRelays(relays: relayResponse, updatedAt: cache.updatedAt) } + public func readPrebundledRelays() throws -> CachedRelays { + let prebundledRelays = try relayCache.readPrebundledRelays() + let relayResponse = apply(overrides: ipOverrideRepository.fetchAll(), to: prebundledRelays.relays) + + return CachedRelays(relays: relayResponse, updatedAt: prebundledRelays.updatedAt) + } + public func write(record: CachedRelays) throws { try relayCache.write(record: record) } diff --git a/ios/MullvadREST/Relay/RelayCache.swift b/ios/MullvadREST/Relay/RelayCache.swift index 47ea44c6f5d2..eedf3b997ad2 100644 --- a/ios/MullvadREST/Relay/RelayCache.swift +++ b/ios/MullvadREST/Relay/RelayCache.swift @@ -10,7 +10,14 @@ import Foundation import MullvadTypes public protocol RelayCacheProtocol { + /// Reads from a cached list, + /// which falls back to reading from prebundled relays if there was no cache hit func read() throws -> CachedRelays + /// Reads the relays file that were prebundled with the app installation. + /// + /// > Warning: Prefer `read()` over this unless there is an explicit need to read + /// relays from the bundle, because those might contain stale data. + func readPrebundledRelays() throws -> CachedRelays func write(record: CachedRelays) throws } @@ -49,7 +56,7 @@ public final class RelayCache: RelayCacheProtocol { } /// Read pre-bundled relays file from disk. - private func readPrebundledRelays() throws -> CachedRelays { + public func readPrebundledRelays() throws -> CachedRelays { guard let prebundledRelaysFileURL = Bundle(for: Self.self).url(forResource: "relays", withExtension: "json") else { throw CocoaError(.fileNoSuchFile) } diff --git a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift index 8893436cb639..2d2d0d384ed0 100644 --- a/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift +++ b/ios/MullvadVPN/RelayCacheTracker/RelayCacheTracker.swift @@ -37,7 +37,7 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { private let application: UIApplication /// Lock used for synchronization. - private let nslock = NSLock() + private let relayCacheLock = NSLock() /// Internal operation queue. private let operationQueue = AsyncOperationQueue.makeSerial() @@ -64,6 +64,7 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { do { cachedRelays = try cache.read() + try hotfixRelaysThatDoNotHaveDaita() } catch { logger.error( error: error, @@ -74,9 +75,58 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { } } + /// This method updates the cached relay to include daita information + /// + /// This is a hotfix meant to upgrade clients shipped with 2024.5 or before that did not have + /// daita information in their representation of `ServerRelay`. + /// If a version <= 2024.5 is installed less than an hour before a new upgrade, + /// no servers will be shown in locations when filtering for daita relays. + /// + /// > Info: `relayCacheLock` does not need to be accessed here, this method should be ran from `init` only. + private func hotfixRelaysThatDoNotHaveDaita() throws { + guard let cachedRelays else { return } + let daitaPropertyMissing = cachedRelays.relays.wireguard.relays.first { $0.daita ?? false } == nil + // If the cached relays already have daita information, this fix is not necessary + guard daitaPropertyMissing else { return } + + let preBundledRelays = try cache.readPrebundledRelays() + let preBundledDaitaRelays = preBundledRelays.relays.wireguard.relays.filter { $0.daita == true } + var cachedRelaysWithFixedDaita = cachedRelays.relays.wireguard.relays + + // For each daita enabled relay in the prebundled relays + // Find the corresponding relay in the cache by matching relay hostnames + // Then update it to toggle daita + for index in 0 ..< cachedRelaysWithFixedDaita.endIndex { + let relay = cachedRelaysWithFixedDaita[index] + preBundledDaitaRelays.forEach { + if $0.hostname == relay.hostname { + cachedRelaysWithFixedDaita[index] = relay.override(daita: true) + } + } + } + + let wireguard = REST.ServerWireguardTunnels( + ipv4Gateway: + cachedRelays.relays.wireguard.ipv4Gateway, + ipv6Gateway: cachedRelays.relays.wireguard.ipv6Gateway, + portRanges: cachedRelays.relays.wireguard.portRanges, + relays: cachedRelaysWithFixedDaita + ) + + let updatedRelays = REST.ServerRelaysResponse( + locations: cachedRelays.relays.locations, + wireguard: wireguard, + bridge: cachedRelays.relays.bridge + ) + + let updatedCachedRelays = CachedRelays(relays: updatedRelays, updatedAt: Date()) + try cache.write(record: updatedCachedRelays) + self.cachedRelays = updatedCachedRelays + } + func startPeriodicUpdates() { - nslock.lock() - defer { nslock.unlock() } + relayCacheLock.lock() + defer { relayCacheLock.unlock() } guard !isPeriodicUpdatesEnabled else { return } @@ -90,8 +140,8 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { } func stopPeriodicUpdates() { - nslock.lock() - defer { nslock.unlock() } + relayCacheLock.lock() + defer { relayCacheLock.unlock() } guard isPeriodicUpdatesEnabled else { return } @@ -135,8 +185,8 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { } func getCachedRelays() throws -> CachedRelays { - nslock.lock() - defer { nslock.unlock() } + relayCacheLock.lock() + defer { relayCacheLock.unlock() } if let cachedRelays { return cachedRelays @@ -148,9 +198,9 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { func refreshCachedRelays() throws { let newCachedRelays = try cache.read() - nslock.lock() + relayCacheLock.lock() cachedRelays = newCachedRelays - nslock.unlock() + relayCacheLock.unlock() DispatchQueue.main.async { self.observerList.notify { observer in @@ -160,8 +210,8 @@ final class RelayCacheTracker: RelayCacheTrackerProtocol { } func getNextUpdateDate() -> Date { - nslock.lock() - defer { nslock.unlock() } + relayCacheLock.lock() + defer { relayCacheLock.unlock() } return _getNextUpdateDate() }