Skip to content

Commit 74e4505

Browse files
author
Jon Petersson
committed
Allow users to import settings by pasting JSON blobs
1 parent 3a5ba4a commit 74e4505

16 files changed

+760
-38
lines changed

ios/MullvadREST/ApiHandlers/ServerRelaysResponse.swift

+28
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ extension REST {
3434
public let ipv4AddrIn: IPv4Address
3535
public let weight: UInt64
3636
public let includeInCountry: Bool
37+
38+
public func copyWith(ipv4AddrIn: IPv4Address?) -> Self {
39+
return BridgeRelay(
40+
hostname: hostname,
41+
active: active,
42+
owned: owned,
43+
location: location,
44+
provider: provider,
45+
ipv4AddrIn: ipv4AddrIn ?? self.ipv4AddrIn,
46+
weight: weight,
47+
includeInCountry: includeInCountry
48+
)
49+
}
3750
}
3851

3952
public struct ServerRelay: Codable, Equatable {
@@ -47,6 +60,21 @@ extension REST {
4760
public let ipv6AddrIn: IPv6Address
4861
public let publicKey: Data
4962
public let includeInCountry: Bool
63+
64+
public func copyWith(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self {
65+
return ServerRelay(
66+
hostname: hostname,
67+
active: active,
68+
owned: owned,
69+
location: location,
70+
provider: provider,
71+
weight: weight,
72+
ipv4AddrIn: ipv4AddrIn ?? self.ipv4AddrIn,
73+
ipv6AddrIn: ipv6AddrIn ?? self.ipv6AddrIn,
74+
publicKey: publicKey,
75+
includeInCountry: includeInCountry
76+
)
77+
}
5078
}
5179

5280
public struct ServerWireguardTunnels: Codable, Equatable {

ios/MullvadREST/Relay/RelaySelector.swift

+46-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
//
88

99
import Foundation
10+
import MullvadSettings
1011
import MullvadTypes
12+
import Network
1113

1214
private let defaultPort: UInt16 = 53
1315

@@ -19,15 +21,6 @@ public enum RelaySelector {
1921
relays.bridge.shadowsocks.filter { $0.protocol == "tcp" }.randomElement()
2022
}
2123

22-
/// Return a random Shadowsocks bridge relay, or `nil` if no relay were found.
23-
///
24-
/// Non `active` relays are filtered out.
25-
/// - Parameter relays: The list of relays to randomly select from.
26-
/// - Returns: A Shadowsocks relay or `nil` if no active relay were found.
27-
public static func shadowsocksRelay(from relaysResponse: REST.ServerRelaysResponse) -> REST.BridgeRelay? {
28-
relaysResponse.bridge.relays.filter { $0.active }.randomElement()
29-
}
30-
3124
/// Returns the closest Shadowsocks relay using the given `constraints`, or a random relay if `constraints` were
3225
/// unsatisfiable.
3326
///
@@ -41,7 +34,11 @@ public enum RelaySelector {
4134
) -> REST.BridgeRelay? {
4235
let mappedBridges = mapRelays(relays: relaysResponse.bridge.relays, locations: relaysResponse.locations)
4336
let filteredRelays = applyConstraints(constraints, relays: mappedBridges)
44-
guard filteredRelays.isEmpty == false else { return shadowsocksRelay(from: relaysResponse) }
37+
38+
guard filteredRelays.isEmpty == false else {
39+
let relay = shadowsocksRelay(from: relaysResponse)
40+
return relay.flatMap { applyIpOverrides(to: $0) }
41+
}
4542

4643
// Compute the midpoint location from all the filtered relays
4744
// Take *either* the first five relays, OR the relays below maximum bridge distance
@@ -77,7 +74,8 @@ public enum RelaySelector {
7774
UInt64(1 + greatestDistance - relay.distance)
7875
})
7976

80-
return randomRelay?.relay ?? filteredRelays.randomElement()?.relay
77+
let relayToReturn = randomRelay?.relay ?? filteredRelays.randomElement()?.relay
78+
return relayToReturn.flatMap { applyIpOverrides(to: $0) }
8179
}
8280

8381
/**
@@ -97,10 +95,15 @@ public enum RelaySelector {
9795
numberOfFailedAttempts: numberOfFailedAttempts
9896
)
9997

100-
guard let relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else {
98+
guard var relayWithLocation = pickRandomRelayByWeight(relays: filteredRelays), let port else {
10199
throw NoRelaysSatisfyingConstraintsError()
102100
}
103101

102+
relayWithLocation = RelayWithLocation(
103+
relay: applyIpOverrides(to: relayWithLocation.relay),
104+
serverLocation: relayWithLocation.serverLocation
105+
)
106+
104107
let endpoint = MullvadEndpoint(
105108
ipv4Relay: IPv4Endpoint(
106109
ip: relayWithLocation.relay.ipv4AddrIn,
@@ -135,6 +138,15 @@ public enum RelaySelector {
135138
}
136139
}
137140

141+
/// Return a random Shadowsocks bridge relay, or `nil` if no relay were found.
142+
///
143+
/// Non `active` relays are filtered out.
144+
/// - Parameter relays: The list of relays to randomly select from.
145+
/// - Returns: A Shadowsocks relay or `nil` if no active relay were found.
146+
private static func shadowsocksRelay(from relaysResponse: REST.ServerRelaysResponse) -> REST.BridgeRelay? {
147+
relaysResponse.bridge.relays.filter { $0.active }.randomElement()
148+
}
149+
138150
/// Produce a list of `RelayWithLocation` items satisfying the given constraints
139151
private static func applyConstraints<T: AnyRelay>(
140152
_ constraints: RelayConstraints,
@@ -194,6 +206,21 @@ public enum RelaySelector {
194206
}
195207
}
196208

209+
private static func applyIpOverrides<T: AnyRelay>(to relay: T) -> T {
210+
let overrides = IPOverrideRepository().fetchAll()
211+
212+
if let override = overrides.first(where: { host in
213+
host.hostname == relay.hostname
214+
}) {
215+
return relay.copyWith(
216+
ipv4AddrIn: override.ipv4Address,
217+
ipv6AddrIn: override.ipv6Address
218+
)
219+
}
220+
221+
return relay
222+
}
223+
197224
private static func pickRandomRelayByWeight<T: AnyRelay>(relays: [RelayWithLocation<T>])
198225
-> RelayWithLocation<T>? {
199226
rouletteSelection(relays: relays, weightFunction: { relayWithLocation in relayWithLocation.relay.weight })
@@ -314,10 +341,16 @@ public protocol AnyRelay {
314341
var weight: UInt64 { get }
315342
var active: Bool { get }
316343
var includeInCountry: Bool { get }
344+
345+
func copyWith(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> Self
317346
}
318347

319348
extension REST.ServerRelay: AnyRelay {}
320-
extension REST.BridgeRelay: AnyRelay {}
349+
extension REST.BridgeRelay: AnyRelay {
350+
public func copyWith(ipv4AddrIn: IPv4Address?, ipv6AddrIn: IPv6Address?) -> REST.BridgeRelay {
351+
copyWith(ipv4AddrIn: ipv4AddrIn)
352+
}
353+
}
321354

322355
private struct RelayWithLocation<T: AnyRelay> {
323356
let relay: T

ios/MullvadSettings/IPOverride.swift

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//
2+
// IPOverride.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-01-16.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Network
10+
11+
public struct RelayOverrides: Codable {
12+
public let overrides: [IPOverride]
13+
14+
private enum CodingKeys: String, CodingKey {
15+
case overrides = "relay_overrides"
16+
}
17+
}
18+
19+
public struct IPOverride: Codable, Equatable {
20+
public let hostname: String
21+
public var ipv4Address: IPv4Address?
22+
public var ipv6Address: IPv6Address?
23+
24+
private enum CodingKeys: String, CodingKey {
25+
case hostname
26+
case ipv4Address = "ipv4_addr_in"
27+
case ipv6Address = "ipv6_addr_in"
28+
}
29+
30+
init(hostname: String, ipv4Address: IPv4Address?, ipv6Address: IPv6Address?) throws {
31+
self.hostname = hostname
32+
self.ipv4Address = ipv4Address
33+
self.ipv6Address = ipv6Address
34+
35+
if self.ipv4Address.isNil && self.ipv6Address.isNil {
36+
throw NSError(domain: "IPOverrideInitDomain", code: NSFormattingError)
37+
}
38+
}
39+
40+
public init(from decoder: Decoder) throws {
41+
let container = try decoder.container(keyedBy: CodingKeys.self)
42+
43+
self.hostname = try container.decode(String.self, forKey: .hostname)
44+
self.ipv4Address = try container.decodeIfPresent(IPv4Address.self, forKey: .ipv4Address)
45+
self.ipv6Address = try container.decodeIfPresent(IPv6Address.self, forKey: .ipv6Address)
46+
47+
if self.ipv4Address.isNil && self.ipv6Address.isNil {
48+
throw NSError(domain: "IPOverrideInitDomain", code: NSFormattingError)
49+
}
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// IPOverrideRepository.swift
3+
// MullvadVPN
4+
//
5+
// Created by Jon Petersson on 2024-01-16.
6+
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public protocol IPOverrideRepositoryProtocol {
12+
func add(_ overrides: [IPOverride])
13+
func fetchAll() -> [IPOverride]
14+
func fetchByHostname(_ hostname: String) -> IPOverride?
15+
func deleteAll()
16+
func parseData(_ data: Data) throws -> [IPOverride]
17+
}
18+
19+
public class IPOverrideRepository: IPOverrideRepositoryProtocol {
20+
public init() {}
21+
22+
public func add(_ overrides: [IPOverride]) {
23+
var storedOverrides = fetchAll()
24+
25+
overrides.forEach { override in
26+
if let existingOverrideIndex = storedOverrides.firstIndex(where: { $0.hostname == override.hostname }) {
27+
var existingOverride = storedOverrides[existingOverrideIndex]
28+
29+
if let ipv4Address = override.ipv4Address {
30+
existingOverride.ipv4Address = ipv4Address
31+
}
32+
33+
if let ipv6Address = override.ipv6Address {
34+
existingOverride.ipv6Address = ipv6Address
35+
}
36+
37+
storedOverrides[existingOverrideIndex] = existingOverride
38+
} else {
39+
storedOverrides.append(override)
40+
}
41+
}
42+
43+
do {
44+
try writeIpOverrides(storedOverrides)
45+
} catch {
46+
print("Could not add override(s): \(overrides) \nError: \(error)")
47+
}
48+
}
49+
50+
public func fetchAll() -> [IPOverride] {
51+
return (try? readIpOverrides()) ?? []
52+
}
53+
54+
public func fetchByHostname(_ hostname: String) -> IPOverride? {
55+
return fetchAll().first { $0.hostname == hostname }
56+
}
57+
58+
public func deleteAll() {
59+
do {
60+
try SettingsManager.store.delete(key: .ipOverrides)
61+
} catch {
62+
print("Could not delete all overrides. \nError: \(error)")
63+
}
64+
}
65+
66+
public func parseData(_ data: Data) throws -> [IPOverride] {
67+
let decoder = JSONDecoder()
68+
let jsonData = try decoder.decode(RelayOverrides.self, from: data)
69+
70+
return jsonData.overrides
71+
}
72+
73+
private func readIpOverrides() throws -> [IPOverride] {
74+
let parser = makeParser()
75+
let data = try SettingsManager.store.read(key: .ipOverrides)
76+
77+
return try parser.parseUnversionedPayload(as: [IPOverride].self, from: data)
78+
}
79+
80+
private func writeIpOverrides(_ overrides: [IPOverride]) throws {
81+
let parser = makeParser()
82+
let data = try parser.produceUnversionedPayload(overrides)
83+
84+
try SettingsManager.store.write(data, for: .ipOverrides)
85+
}
86+
87+
private func makeParser() -> SettingsParser {
88+
SettingsParser(decoder: JSONDecoder(), encoder: JSONEncoder())
89+
}
90+
}

ios/MullvadSettings/SettingsStore.swift

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public enum SettingsKey: String, CaseIterable {
1212
case settings = "Settings"
1313
case deviceState = "DeviceState"
1414
case apiAccessMethods = "ApiAccessMethods"
15+
case ipOverrides = "IPOverrides"
1516
case lastUsedAccount = "LastUsedAccount"
1617
case shouldWipeSettings = "ShouldWipeSettings"
1718
}

0 commit comments

Comments
 (0)