diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2020-03-09 15:11:55 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2020-03-09 15:11:55 +0100 |
| commit | ed8e2493560e56e4eec890efd6eeb7a1a74346ac (patch) | |
| tree | b737af05f4ec2047118e3bde084576a5d2ef1329 | |
| parent | 9b273dc769e888c4f0c78e882e4fea9cee81828a (diff) | |
| parent | b2b2270c6f99d8a5acc7c03c76567817950c04aa (diff) | |
| download | mullvadvpn-ed8e2493560e56e4eec890efd6eeb7a1a74346ac.tar.xz mullvadvpn-ed8e2493560e56e4eec890efd6eeb7a1a74346ac.zip | |
Merge branch 'weighted-relay-selector-ios'
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 14 | ||||
| -rw-r--r-- | ios/MullvadVPN/ConnectViewController.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/Location.swift (renamed from ios/MullvadVPN/GeoLocation.swift) | 6 | ||||
| -rw-r--r-- | ios/MullvadVPN/PacketTunnelIpc.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayCache.swift | 2 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelayList.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/RelaySelector.swift | 177 | ||||
| -rw-r--r-- | ios/MullvadVPN/SimulatorTunnelProviderHost.swift | 4 | ||||
| -rw-r--r-- | ios/MullvadVPNTests/RelaySelectorTests.swift | 8 | ||||
| -rw-r--r-- | ios/PacketTunnel/PacketTunnelProvider.swift | 5 |
10 files changed, 109 insertions, 119 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index a594ed8022..b8689830b1 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -28,6 +28,9 @@ 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; 5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; 584B26FF237435A90073B10E /* RelaySelector+RelayCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584B26FD237435990073B10E /* RelaySelector+RelayCache.swift */; }; + 584E96BC240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; + 584E96BD240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; + 584E96BE240FD4DB00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; 58561C99239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 5860F1C223A785C600CEA666 /* WireguardDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5860F1C123A785C600CEA666 /* WireguardDevice.swift */; }; @@ -61,8 +64,6 @@ 588AE72F2362001F009F9F2E /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */; }; 588AE730236200E2009F9F2E /* MutuallyExclusive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */; }; 589AB4F7227B64450039131E /* BasicTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589AB4F6227B64450039131E /* BasicTableViewCell.swift */; }; - 58A1AA8723F43901009F7EA6 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* GeoLocation.swift */; }; - 58A1AA8823F43901009F7EA6 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* GeoLocation.swift */; }; 58A1AA8C23F5584C009F7EA6 /* ConnectionPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */; }; 58A8BE81239FBE62006B74AC /* IPEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */; }; 58A8BE8323A0F362006B74AC /* UIAlertController+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A8BE8223A0F362006B74AC /* UIAlertController+Error.swift */; }; @@ -206,7 +207,7 @@ 588AE72E2362001F009F9F2E /* MutuallyExclusive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutuallyExclusive.swift; sourceTree = "<group>"; }; 5894E725236B2801008A2793 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 589AB4F6227B64450039131E /* BasicTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicTableViewCell.swift; sourceTree = "<group>"; }; - 58A1AA8623F43901009F7EA6 /* GeoLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoLocation.swift; sourceTree = "<group>"; }; + 58A1AA8623F43901009F7EA6 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = "<group>"; }; 58A1AA8B23F5584B009F7EA6 /* ConnectionPanelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionPanelView.swift; sourceTree = "<group>"; }; 58A8BE8223A0F362006B74AC /* UIAlertController+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Error.swift"; sourceTree = "<group>"; }; 58A99ED2240014A0006599E9 /* ConsentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentViewController.swift; sourceTree = "<group>"; }; @@ -356,7 +357,6 @@ 582BB1B0229569620055B6EF /* CustomNavigationBar.swift */, 58C6B35D22BBBFE3003C19AD /* Data+HexCoding.swift */, 5873884C239E6D7E00E96C4E /* EmbeddedViewContainerView.swift */, - 58A1AA8623F43901009F7EA6 /* GeoLocation.swift */, 58CE5E6F224146210008646E /* Info.plist */, 5840250022B1124600E4CFEC /* IpAddress+Codable.swift */, 58C6B34E22BB7AC0003C19AD /* IPAddressRange.swift */, @@ -364,6 +364,7 @@ 58ADDB3B227B1BD200FAFEA7 /* JsonRpc.swift */, 58AEEF642344A36000C9BBD5 /* KeychainError.swift */, 58CE5E6C224146210008646E /* LaunchScreen.storyboard */, + 58A1AA8623F43901009F7EA6 /* Location.swift */, 58BA692D23E99EFF009DC256 /* Locking.swift */, 587B08DF229433EB000E6F17 /* LoginState.swift */, 58CE5E65224146200008646E /* LoginViewController.swift */, @@ -688,6 +689,7 @@ files = ( 58B0A2AA238EE6A900BC001D /* RelaySelector.swift in Sources */, 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, + 584E96BE240FD4DB00D3334F /* Location.swift in Sources */, 58B0A2AC238EE6D500BC001D /* IpAddress+Codable.swift in Sources */, 58B0A2AB238EE6BF00BC001D /* RelayList.swift in Sources */, 58B0A2AD238EE6EC00BC001D /* MullvadEndpoint.swift in Sources */, @@ -728,6 +730,7 @@ 5868585524054096000B8131 /* AppButton.swift in Sources */, 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */, 58781CC922AE7CA8009B9D8E /* RelayConstraints.swift in Sources */, + 584E96BC240FD4DA00D3334F /* Location.swift in Sources */, 58ADDB3E227B1CD900FAFEA7 /* MullvadAPI.swift in Sources */, 58B8743222B25A7600015324 /* WireguardAssociatedAddresses.swift in Sources */, 587B08E0229433EB000E6F17 /* LoginState.swift in Sources */, @@ -742,7 +745,6 @@ 5877152E23981C5B001F8237 /* SettingsBasicCell.swift in Sources */, 5835B7CC233B76CB0096D79F /* TunnelManager.swift in Sources */, 58ADDB3C227B1BD200FAFEA7 /* JsonRpc.swift in Sources */, - 58A1AA8723F43901009F7EA6 /* GeoLocation.swift in Sources */, 581CBCEE229826FD00727D7F /* StaticTableViewDataSource.swift in Sources */, 58CE5E64224146200008646E /* AppDelegate.swift in Sources */, 58C6B35E22BBBFE3003C19AD /* Data+HexCoding.swift in Sources */, @@ -796,12 +798,12 @@ 58BFA5C722A7C97F00A6173D /* RelayCache.swift in Sources */, 58BFA5C022A7C8A900A6173D /* MullvadAPI.swift in Sources */, 58AEEF692344A43A00C9BBD5 /* TunnelConfigurationCoder.swift in Sources */, + 584E96BD240FD4DA00D3334F /* Location.swift in Sources */, 58C6B36122C0EC82003C19AD /* AnyIPEndpoint+DNS64.swift in Sources */, 58C6B36722C106FC003C19AD /* WireguardCommand.swift in Sources */, 58561C9A239A5D1500BD6B5E /* IPEndpoint.swift in Sources */, 584B26FF237435A90073B10E /* RelaySelector+RelayCache.swift in Sources */, 58781CCE22AE8918009B9D8E /* RelayConstraints.swift in Sources */, - 58A1AA8823F43901009F7EA6 /* GeoLocation.swift in Sources */, 58781CD522AFBA39009B9D8E /* RelaySelector.swift in Sources */, 5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */, ); diff --git a/ios/MullvadVPN/ConnectViewController.swift b/ios/MullvadVPN/ConnectViewController.swift index 6a5c9527d0..25d4acf77a 100644 --- a/ios/MullvadVPN/ConnectViewController.swift +++ b/ios/MullvadVPN/ConnectViewController.swift @@ -100,8 +100,8 @@ class ConnectViewController: UIViewController, RootContainment, TunnelControlVie switch tunnelState { case .connected(let connectionInfo), .reconnecting(let connectionInfo): - cityLabel.attributedText = attributedStringForLocation(string: connectionInfo.geoLocation.city) - countryLabel.attributedText = attributedStringForLocation(string: connectionInfo.geoLocation.country) + cityLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.city) + countryLabel.attributedText = attributedStringForLocation(string: connectionInfo.location.country) connectionPanel.dataSource = ConnectionPanelData( inAddress: "\(connectionInfo.ipv4Relay) UDP", diff --git a/ios/MullvadVPN/GeoLocation.swift b/ios/MullvadVPN/Location.swift index 80db85a81f..ab9ef6fa5e 100644 --- a/ios/MullvadVPN/GeoLocation.swift +++ b/ios/MullvadVPN/Location.swift @@ -1,5 +1,5 @@ // -// GeoLocation.swift +// Location.swift // MullvadVPN // // Created by pronebird on 12/02/2020. @@ -8,9 +8,11 @@ import Foundation -struct GeoLocation: Codable, Equatable { +struct Location: Codable, Equatable { var country: String + var countryCode: String var city: String + var cityCode: String var latitude: Double var longitude: Double } diff --git a/ios/MullvadVPN/PacketTunnelIpc.swift b/ios/MullvadVPN/PacketTunnelIpc.swift index 6362b2edb8..3e2d6b9af9 100644 --- a/ios/MullvadVPN/PacketTunnelIpc.swift +++ b/ios/MullvadVPN/PacketTunnelIpc.swift @@ -52,7 +52,7 @@ struct TunnelConnectionInfo: Codable, Equatable { let ipv4Relay: IPv4Endpoint let ipv6Relay: IPv6Endpoint? let hostname: String - let geoLocation: GeoLocation + let location: Location } extension TunnelConnectionInfo: CustomDebugStringConvertible { @@ -60,7 +60,7 @@ extension TunnelConnectionInfo: CustomDebugStringConvertible { return "{ ipv4Relay: \(String(reflecting: ipv4Relay)), " + "ipv6Relay: \(String(reflecting: ipv6Relay)), " + "hostname: \(String(reflecting: hostname))," + - "geoLocation: \(String(reflecting: geoLocation)) }" + "location: \(String(reflecting: location)) }" } } diff --git a/ios/MullvadVPN/RelayCache.swift b/ios/MullvadVPN/RelayCache.swift index 1aec01c72c..5bb4082fbb 100644 --- a/ios/MullvadVPN/RelayCache.swift +++ b/ios/MullvadVPN/RelayCache.swift @@ -133,7 +133,7 @@ class RelayCache { var filteredCity = city filteredCity.relays = city.relays - .map { (relay) -> RelayList.Hostname in + .map { (relay) -> RelayList.Relay in var filteredRelay = relay // filter out tunnels without ports diff --git a/ios/MullvadVPN/RelayList.swift b/ios/MullvadVPN/RelayList.swift index 190432e805..dbebfb6304 100644 --- a/ios/MullvadVPN/RelayList.swift +++ b/ios/MullvadVPN/RelayList.swift @@ -22,10 +22,10 @@ struct RelayList: Codable { var code: String var latitude: Double var longitude: Double - var relays: [Hostname] + var relays: [Relay] } - struct Hostname: Codable { + struct Relay: Codable { var hostname: String var ipv4AddrIn: IPv4Address var includeInCountry: Bool diff --git a/ios/MullvadVPN/RelaySelector.swift b/ios/MullvadVPN/RelaySelector.swift index 4ff29ef41d..e5504d6ce2 100644 --- a/ios/MullvadVPN/RelaySelector.swift +++ b/ios/MullvadVPN/RelaySelector.swift @@ -10,10 +10,15 @@ import Foundation import Network struct RelaySelectorResult { - var relay: RelayList.Hostname + var relay: RelayList.Relay var tunnel: RelayList.WireguardTunnel var endpoint: MullvadEndpoint - var geoLocation: GeoLocation + var location: Location +} + +private struct RelayWithLocation { + var relay: RelayList.Relay + var location: Location } struct RelaySelector { @@ -24,94 +29,19 @@ struct RelaySelector { self.relayList = relayList } - /// Produce a `RelayList` satisfying the given constraints - private func applyConstraints(_ constraints: RelayConstraints) -> RelayList { - let filteredCountries = relayList.countries.filter { (country) -> Bool in - switch constraints.location { - case .any: - return true - case .only(let constraint): - switch constraint { - case .country(let countryCode): - return countryCode == country.code - case .city(let countryCode, _): - return countryCode == country.code - case .hostname(let countryCode, _, _): - return countryCode == country.code - } - } - }.map { (country) -> RelayList.Country in - var filteredCountry = country - filteredCountry.cities = country.cities.filter { (city) -> Bool in - switch constraints.location { - case .any: - return true - case .only(let constraint): - switch constraint { - case .country: - return true - case .city(_, let cityCode): - return cityCode == city.code - case .hostname(_, let cityCode, _): - return cityCode == city.code - } - } - }.map { (city) -> RelayList.City in - var filteredCity = city - filteredCity.relays = city.relays.filter { (relay) -> Bool in - switch constraints.location { - case .any: - return true - case .only(let constraint): - switch constraint { - case .country, .city: - return true - case .hostname(_, _, let hostname): - return hostname == relay.hostname - } - } - } - .map({ (relay) -> RelayList.Hostname in - var filteredRelay = relay - filteredRelay.tunnels?.wireguard = relay.tunnels?.wireguard? - .filter { !$0.portRanges.isEmpty } - - return filteredRelay - }).filter { (relay) -> Bool in - guard let wireguardTunnels = relay.tunnels?.wireguard else { return false } - - return relay.active && !wireguardTunnels.isEmpty - } - - return filteredCity - }.filter({ (city) -> Bool in - return !city.relays.isEmpty - }) - - return filteredCountry - }.filter { (country) -> Bool in - return !country.cities.isEmpty - } - - return RelayList(countries: filteredCountries) - } - func evaluate(with constraints: RelayConstraints) -> RelaySelectorResult? { - let filteredRelayList = applyConstraints(constraints) - - guard let country = filteredRelayList.countries.randomElement() else { - return nil - } + let relays = Self.applyConstraints(constraints, relays: Self.parseRelayList(self.relayList)) + let totalWeight = relays.reduce(0) { $0 + $1.relay.weight } - guard let city = country.cities.randomElement() else { - return nil - } + guard totalWeight > 0 else { return nil } + guard var i = (0...totalWeight).randomElement() else { return nil } - guard let relay = city.relays.randomElement() else { - return nil - } + let relayWithLocation = relays.first { (relayWithLocation) -> Bool in + i -= relayWithLocation.relay.weight + return i <= 0 + }.unsafelyUnwrapped - guard let tunnel = relay.tunnels?.wireguard?.randomElement() else { + guard let tunnel = relayWithLocation.relay.tunnels?.wireguard?.randomElement() else { return nil } @@ -120,26 +50,81 @@ struct RelaySelector { } let endpoint = MullvadEndpoint( - ipv4Relay: IPv4Endpoint(ip: relay.ipv4AddrIn, port: port), + ipv4Relay: IPv4Endpoint(ip: relayWithLocation.relay.ipv4AddrIn, port: port), ipv6Relay: nil, ipv4Gateway: tunnel.ipv4Gateway, ipv6Gateway: tunnel.ipv6Gateway, publicKey: tunnel.publicKey ) - let geoLocation = GeoLocation( - country: country.name, - city: city.name, - latitude: city.latitude, - longitude: city.longitude - ) - return RelaySelectorResult( - relay: relay, + relay: relayWithLocation.relay, tunnel: tunnel, endpoint: endpoint, - geoLocation: geoLocation + location: relayWithLocation.location ) } + /// Produce a list of `RelayWithLocation` items satisfying the given constraints + private static func applyConstraints(_ constraints: RelayConstraints, relays: [RelayWithLocation]) -> [RelayWithLocation] { + return relays.filter { (relayWithLocation) -> Bool in + switch constraints.location { + case .any: + return true + case .only(let relayConstraint): + switch relayConstraint { + case .country(let countryCode): + return relayWithLocation.location.countryCode == countryCode + + case .city(let countryCode, let cityCode): + return relayWithLocation.location.countryCode == countryCode && + relayWithLocation.location.cityCode == cityCode + + case .hostname(let countryCode, let cityCode, let hostname): + return relayWithLocation.location.countryCode == countryCode && + relayWithLocation.location.cityCode == cityCode && + relayWithLocation.relay.hostname == hostname + } + } + }.map({ (relayWithLocation) -> RelayWithLocation in + var filteredRelay = relayWithLocation + let wireguardTunnels = filteredRelay.relay.tunnels?.wireguard? + .filter { !$0.portRanges.isEmpty } + + filteredRelay.relay.tunnels?.wireguard = wireguardTunnels + + return filteredRelay + }).filter { (relayWithLocation) -> Bool in + guard let wireguardTunnels = relayWithLocation.relay.tunnels?.wireguard else { return false } + + return relayWithLocation.relay.active && !wireguardTunnels.isEmpty + } + } + + private static func parseRelayList(_ relayList: RelayList) -> [RelayWithLocation] { + var relays = [RelayWithLocation]() + + for country in relayList.countries { + for city in country.cities { + for relay in city.relays { + let location = Location( + country: country.name, + countryCode: country.code, + city: city.name, + cityCode: city.code, + latitude: city.latitude, + longitude: city.longitude + ) + let relayWithLocation = RelayWithLocation( + relay: relay, + location: location + ) + relays.append(relayWithLocation) + } + } + } + + return relays + } + } diff --git a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift index 5bfd0cc859..89d6423732 100644 --- a/ios/MullvadVPN/SimulatorTunnelProviderHost.swift +++ b/ios/MullvadVPN/SimulatorTunnelProviderHost.swift @@ -24,9 +24,11 @@ class SimulatorTunnelProviderHost: SimulatorTunnelProviderDelegate { ipv4Relay: IPv4Endpoint(ip: IPv4Address("1.2.3.4")!, port: 53), ipv6Relay: nil, hostname: "se7-wireguard", - geoLocation: GeoLocation( + location: Location( country: "Sweden", + countryCode: "se", city: "Stockholm", + cityCode: "sto", latitude: 59.3289, longitude: 18.0649 ) diff --git a/ios/MullvadVPNTests/RelaySelectorTests.swift b/ios/MullvadVPNTests/RelaySelectorTests.swift index b3323744ee..983674edca 100644 --- a/ios/MullvadVPNTests/RelaySelectorTests.swift +++ b/ios/MullvadVPNTests/RelaySelectorTests.swift @@ -52,7 +52,7 @@ private let sampleRelayList = RelayList(countries: [ ipv4AddrIn: .loopback, includeInCountry: true, active: true, - weight: 0, + weight: 500, tunnels: .init(wireguard: [ .init( ipv4Gateway: .loopback, @@ -74,7 +74,7 @@ private let sampleRelayList = RelayList(countries: [ ipv4AddrIn: .loopback, includeInCountry: true, active: true, - weight: 0, + weight: 1000, tunnels: .init(wireguard: [ .init( ipv4Gateway: .loopback, @@ -94,7 +94,7 @@ private let sampleRelayList = RelayList(countries: [ ipv4AddrIn: .loopback, includeInCountry: true, active: true, - weight: 0, + weight: 50, tunnels: .init(wireguard: [ .init( ipv4Gateway: .loopback, @@ -108,7 +108,7 @@ private let sampleRelayList = RelayList(countries: [ ipv4AddrIn: IPv4Address.loopback, includeInCountry: true, active: true, - weight: 0, + weight: 100, tunnels: .init(wireguard: [ .init( ipv4Gateway: .loopback, diff --git a/ios/PacketTunnel/PacketTunnelProvider.swift b/ios/PacketTunnel/PacketTunnelProvider.swift index 1b5dddb281..102d77ad65 100644 --- a/ios/PacketTunnel/PacketTunnelProvider.swift +++ b/ios/PacketTunnel/PacketTunnelProvider.swift @@ -290,11 +290,10 @@ class PacketTunnelProvider: NEPacketTunnelProvider { ipv4Relay: selectorResult.endpoint.ipv4Relay, ipv6Relay: selectorResult.endpoint.ipv6Relay, hostname: selectorResult.relay.hostname, - geoLocation: selectorResult.geoLocation + location: selectorResult.location ) - os_log(.default, log: tunnelProviderLog, - "Selected relay: %{public}s", + os_log(.default, log: tunnelProviderLog, "Selected relay: %{public}s", selectorResult.relay.hostname) } |
