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

Adjust relay selector to support custom lists #5788

Merged
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
32 changes: 18 additions & 14 deletions ios/MullvadREST/Relay/RelaySelector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,24 +150,28 @@ public enum RelaySelector {
}
}

switch constraints.location {
switch constraints.locations {
case .any:
return true
case let .only(relayConstraint):
switch relayConstraint {
case let .country(countryCode):
return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.relay.includeInCountry

case let .city(countryCode, cityCode):
return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.serverLocation.cityCode == cityCode

case let .hostname(countryCode, cityCode, hostname):
return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.serverLocation.cityCode == cityCode &&
relayWithLocation.relay.hostname == hostname
for location in relayConstraint.locations {
switch location {
case let .country(countryCode):
return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.relay.includeInCountry

case let .city(countryCode, cityCode):
return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.serverLocation.cityCode == cityCode

case let .hostname(countryCode, cityCode, hostname):
return relayWithLocation.serverLocation.countryCode == countryCode &&
relayWithLocation.serverLocation.cityCode == cityCode &&
relayWithLocation.relay.hostname == hostname
}
}

return false
}
}.filter { relayWithLocation -> Bool in
relayWithLocation.relay.active
Expand Down
37 changes: 32 additions & 5 deletions ios/MullvadTypes/RelayConstraints.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,59 @@ public class RelayConstraintsUpdater: ConstraintsPropagation {
}

public struct RelayConstraints: Codable, Equatable, CustomDebugStringConvertible {
public var location: RelayConstraint<RelayLocation>
@available(*, deprecated, renamed: "locations")
private var location: RelayConstraint<RelayLocation> = .only(.country("se"))

// Added in 2023.3
public var port: RelayConstraint<UInt16>
public var filter: RelayConstraint<RelayFilter>

// Added in 2024.1
public var locations: RelayConstraint<RelayLocations>

public var debugDescription: String {
"RelayConstraints { location: \(location), port: \(port) }"
"RelayConstraints { locations: \(locations), port: \(port) }"
}

public init(
location: RelayConstraint<RelayLocation> = .only(.country("se")),
locations: RelayConstraint<RelayLocations> = .only(RelayLocations(locations: [.country("se")])),
port: RelayConstraint<UInt16> = .any,
filter: RelayConstraint<RelayFilter> = .any
) {
self.location = location
self.locations = locations
self.port = port
self.filter = filter
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
location = try container.decode(RelayConstraint<RelayLocation>.self, forKey: .location)

// Added in 2023.3
port = try container.decodeIfPresent(RelayConstraint<UInt16>.self, forKey: .port) ?? .any
filter = try container.decodeIfPresent(RelayConstraint<RelayFilter>.self, forKey: .filter) ?? .any

// Added in 2024.1
locations = try container.decodeIfPresent(RelayConstraint<RelayLocations>.self, forKey: .locations)
?? Self.migrateLocations(decoder: decoder)
?? .only(RelayLocations(locations: [.country("se")]))
}
}

extension RelayConstraints {
private static func migrateLocations(decoder: Decoder) -> RelayConstraint<RelayLocations>? {
let container = try? decoder.container(keyedBy: CodingKeys.self)

guard
let location = try? container?.decodeIfPresent(RelayConstraint<RelayLocation>.self, forKey: .location)
else {
return nil
}

switch location {
case .any:
return .any
case let .only(location):
return .only(RelayLocations(locations: [location]))
}
}
}
10 changes: 10 additions & 0 deletions ios/MullvadTypes/RelayLocation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,13 @@ public enum RelayLocation: Codable, Hashable, CustomDebugStringConvertible {
}
}
}

public struct RelayLocations: Codable, Equatable {
public let locations: [RelayLocation]
public let customListId: UUID?

public init(locations: [RelayLocation], customListId: UUID? = nil) {
self.locations = locations
self.customListId = customListId
}
}
8 changes: 6 additions & 2 deletions ios/MullvadVPN/Coordinators/SelectLocationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
guard let self else { return }

var relayConstraints = tunnelManager.settings.relayConstraints
relayConstraints.location = .only(relay)
relayConstraints.locations = .only(RelayLocations(
locations: [relay],
customListId: nil
))

tunnelManager.updateSettings([.relayConstraints(relayConstraints)]) {
self.tunnelManager.startTunnel()
Expand Down Expand Up @@ -98,7 +101,8 @@ class SelectLocationCoordinator: Coordinator, Presentable, Presenting, RelayCach
selectLocationViewController.setCachedRelays(cachedRelays, filter: relayFilter)
}

selectLocationViewController.relayLocation = tunnelManager.settings.relayConstraints.location.value
selectLocationViewController.relayLocation =
tunnelManager.settings.relayConstraints.locations.value?.locations.first

navigationController.pushViewController(selectLocationViewController, animated: false)
}
Expand Down
10 changes: 8 additions & 2 deletions ios/MullvadVPNTests/MigrationManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,10 @@ final class MigrationManagerTests: XCTestCase {

func testSuccessfulMigrationFromV2ToLatest() throws {
var settingsV2 = TunnelSettingsV2()
let osakaRelayConstraints: RelayConstraints = .init(location: .only(.city("jp", "osa")))
let osakaRelayConstraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.city("jp", "osa")]))
)

settingsV2.relayConstraints = osakaRelayConstraints

try migrateToLatest(settingsV2, version: .v2)
Expand All @@ -132,7 +135,10 @@ final class MigrationManagerTests: XCTestCase {

func testSuccessfulMigrationFromV1ToLatest() throws {
var settingsV1 = TunnelSettingsV1()
let osakaRelayConstraints: RelayConstraints = .init(location: .only(.city("jp", "osa")))
let osakaRelayConstraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.city("jp", "osa")]))
)

settingsV1.relayConstraints = osakaRelayConstraints

try migrateToLatest(settingsV1, version: .v1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ final class TunnelSettingsUpdateTests: XCTestCase {

// When:
let relayConstraints = RelayConstraints(
location: .only(.country("zz")),
locations: .only(RelayLocations(locations: [.country("zz")])),
port: .only(9999),
filter: .only(.init(ownership: .rented, providers: .only(["foo", "bar"])))
)
Expand Down
44 changes: 31 additions & 13 deletions ios/MullvadVPNTests/RelaySelectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class RelaySelectorTests: XCTestCase {
let sampleRelays = ServerRelaysResponseStubs.sampleRelays

func testCountryConstraint() throws {
let constraints = RelayConstraints(location: .only(.country("es")))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.country("es")]))
)

let result = try RelaySelector.evaluate(
relays: sampleRelays,
Expand All @@ -30,7 +32,10 @@ class RelaySelectorTests: XCTestCase {
}

func testCityConstraint() throws {
let constraints = RelayConstraints(location: .only(.city("se", "got")))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.city("se", "got")]))
)

let result = try RelaySelector.evaluate(
relays: sampleRelays,
constraints: constraints,
Expand All @@ -41,7 +46,9 @@ class RelaySelectorTests: XCTestCase {
}

func testHostnameConstraint() throws {
let constraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard")))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")]))
)

let result = try RelaySelector.evaluate(
relays: sampleRelays,
Expand All @@ -53,7 +60,10 @@ class RelaySelectorTests: XCTestCase {
}

func testSpecificPortConstraint() throws {
let constraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard")), port: .only(1))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")])),
port: .only(1)
)

let result = try RelaySelector.evaluate(
relays: sampleRelays,
Expand All @@ -65,7 +75,9 @@ class RelaySelectorTests: XCTestCase {
}

func testRandomPortSelectionWithFailedAttempts() throws {
let constraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard")))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")]))
)
let allPorts = portRanges.flatMap { $0 }

var result = try RelaySelector.evaluate(
Expand All @@ -89,15 +101,19 @@ class RelaySelectorTests: XCTestCase {
}

func testClosestShadowsocksRelay() throws {
let constraints = RelayConstraints(location: .only(.city("se", "sto")))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.city("se", "sto")]))
)

let selectedRelay = RelaySelector.closestShadowsocksRelayConstrained(by: constraints, in: sampleRelays)

XCTAssertEqual(selectedRelay?.hostname, "se-sto-br-001")
}

func testClosestShadowsocksRelayIsRandomWhenNoContraintsAreSatisfied() throws {
let constraints = RelayConstraints(location: .only(.country("INVALID COUNTRY")))
let constraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.country("INVALID COUNTRY")]))
)

let selectedRelay = try XCTUnwrap(RelaySelector.closestShadowsocksRelayConstrained(
by: constraints,
Expand All @@ -109,8 +125,9 @@ class RelaySelectorTests: XCTestCase {

func testRelayFilterConstraintWithOwnedOwnership() throws {
let filter = RelayFilter(ownership: .owned, providers: .any)

let constraints = RelayConstraints(
location: .only(.hostname("se", "sto", "se6-wireguard")),
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")])),
filter: .only(filter)
)

Expand All @@ -125,8 +142,9 @@ class RelaySelectorTests: XCTestCase {

func testRelayFilterConstraintWithRentedOwnership() throws {
let filter = RelayFilter(ownership: .rented, providers: .any)

let constraints = RelayConstraints(
location: .only(.hostname("se", "sto", "se6-wireguard")),
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")])),
filter: .only(filter)
)

Expand All @@ -141,10 +159,10 @@ class RelaySelectorTests: XCTestCase {

func testRelayFilterConstraintWithCorrectProvider() throws {
let provider = "31173"

let filter = RelayFilter(ownership: .any, providers: .only([provider]))

let constraints = RelayConstraints(
location: .only(.hostname("se", "sto", "se6-wireguard")),
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")])),
filter: .only(filter)
)

Expand All @@ -159,10 +177,10 @@ class RelaySelectorTests: XCTestCase {

func testRelayFilterConstraintWithIncorrectProvider() throws {
let provider = "DataPacket"

let filter = RelayFilter(ownership: .any, providers: .only([provider]))

let constraints = RelayConstraints(
location: .only(.hostname("se", "sto", "se6-wireguard")),
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")])),
filter: .only(filter)
)

Expand Down
4 changes: 3 additions & 1 deletion ios/PacketTunnelCoreTests/AppMessageHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ final class AppMessageHandlerTests: XCTestCase {
let actor = PacketTunnelActorStub(reconnectExpectation: reconnectExpectation)
let appMessageHandler = createAppMessageHandler(actor: actor)

let relayConstraints = RelayConstraints(location: .only(.hostname("se", "sto", "se6-wireguard")))
let relayConstraints = RelayConstraints(
locations: .only(RelayLocations(locations: [.hostname("se", "sto", "se6-wireguard")]))
)
let selectorResult = try XCTUnwrap(try? RelaySelector.evaluate(
relays: ServerRelaysResponseStubs.sampleRelays,
constraints: relayConstraints,
Expand Down
Loading