diff options
| author | Emīls <emils@mullvad.net> | 2025-11-20 11:29:17 +0100 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2025-11-20 11:29:17 +0100 |
| commit | 0b7747f64607f8f65e862135acf58118c6b4d29b (patch) | |
| tree | 45cd158f9a083e9fe8d210664ae6f21be8a83464 | |
| parent | 8beacc616749851c1e6d1fb18fc4a493f3c961f6 (diff) | |
| parent | 15ba64661b9ebb172c5baa3710b21105e611b805 (diff) | |
| download | mullvadvpn-bug-bash-2025-11-20.tar.xz mullvadvpn-bug-bash-2025-11-20.zip | |
Merge branch 'migrate-storekit2-calls-ios-1018' into bug-bash-2025-11-20bug-bash-2025-11-20
70 files changed, 1837 insertions, 1608 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index db177318fa..aa84a28589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ Line wrap the file at 100 chars. Th * **Security**: in case of vulnerabilities. ## [Unreleased] +### Added +- Add port 443 to list of valid UDP2TCP ports. + + +## [2025.14-beta1] - 2025-11-11 ### Changed - Change `mullvad reconnect` to print an error message and exit with a non-zero exit code if issued in the disconnected state. diff --git a/Cargo.lock b/Cargo.lock index e2bd798f4d..81bacc870e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2472,15 +2472,6 @@ dependencies = [ ] [[package]] -name = "iprange" -version = "0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37209be0ad225457e63814401415e748e2453a5297f9b637338f5fb8afa4ec00" -dependencies = [ - "ipnet", -] - -[[package]] name = "iri-string" version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2624,17 +2615,6 @@ dependencies = [ ] [[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - -[[package]] name = "keccak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2779,12 +2759,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "lru_time_cache" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" - -[[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3147,7 +3121,6 @@ dependencies = [ "mullvad-types", "oslog", "serde_json", - "shadowsocks-service", "talpid-future", "talpid-tunnel-config-client", "talpid-types", @@ -3993,51 +3966,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] -name = "pest" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8" -dependencies = [ - "memchr", - "thiserror 1.0.59", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.108", -] - -[[package]] -name = "pest_meta" -version = "2.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - -[[package]] name = "petgraph" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5187,43 +5115,6 @@ dependencies = [ ] [[package]] -name = "shadowsocks-service" -version = "1.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c0ce5a3a15e2688b7014c37ed3db33dc296d8b8fda36643c053882bf1d5f44" -dependencies = [ - "arc-swap", - "async-trait", - "byte_string", - "byteorder", - "bytes", - "cfg-if", - "futures", - "http-body-util", - "httparse", - "hyper", - "idna", - "ipnet", - "iprange", - "json5", - "libc", - "log", - "lru_time_cache", - "nix 0.29.0", - "once_cell", - "pin-project", - "rand 0.8.5", - "regex", - "serde", - "shadowsocks", - "socket2 0.5.8", - "spin", - "thiserror 1.0.59", - "tokio", - "windows-sys 0.59.0", -] - -[[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6301,12 +6192,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - -[[package]] name = "udp-over-tcp" version = "0.3.0" source = "git+https://github.com/mullvad/udp-over-tcp?rev=87936ac29b68b902565955f138ab02294bcc8593#87936ac29b68b902565955f138ab02294bcc8593" diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 235c5fecca..b5fdc5c044 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -22,6 +22,17 @@ Line wrap the file at 100 chars. Th * **Security**: in case of vulnerabilities. ## [Unreleased] +### Removed +- Remove "Automatic" as a setting for the "Quantum-resistant tunnel" option. + + +## [android/2025.10-beta2] - 2025-11-18 +### Security +- Fix regression introduced in 2025.10-beta1 where IPv6 traffic would leak when enabling + Local Network Sharing and disabling In-tunnel IPv6. + +### Fixed +- Fix Android 16 upgrade warning text not being displayed properly in some languages. ## [android/2025.10-beta1] - 2025-11-10 @@ -39,9 +50,6 @@ Line wrap the file at 100 chars. Th ### Fixed - Recents will now always show the selected location. -### Removed -- Remove "Automatic" as a setting for the "Quantum-resistant tunnel" option. - ## [android/2025.9] - 2025-10-20 Identical to `android/2025.9-beta1` except for updated translations. diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt index cef0ab2cf9..506fe691f9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt @@ -90,22 +90,26 @@ fun ConnectionDetails( width = Dimension.wrapContent }, ) - Text( - text = inIPV4, - color = MaterialTheme.colorScheme.onPrimary, - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + SelectionContainer( modifier = - Modifier.testTag(LOCATION_INFO_CONNECTION_IN_TEST_TAG).constrainAs(inAddr) { + Modifier.constrainAs(inAddr) { start.linkTo(headerBarrier) end.linkTo(parent.end) top.linkTo(parent.top) bottom.linkTo(inAddrBarrier) height = Dimension.wrapContent width = Dimension.fillToConstraints - }, - ) + } + ) { + Text( + text = inIPV4, + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(LOCATION_INFO_CONNECTION_IN_TEST_TAG), + ) + } if (outIPV4 != null) { Text( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt index 6f1a753b9d..6834708274 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt @@ -4,7 +4,7 @@ import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.PortRange val WIREGUARD_PRESET_PORTS = listOf(Port(51820), Port(53)) -val UDP2TCP_PRESET_PORTS = listOf(Port(80), Port(5001)) +val UDP2TCP_PRESET_PORTS = listOf(Port(80), Port(443), Port(5001)) val SHADOWSOCKS_PRESET_PORTS = emptyList<Port>() val SHADOWSOCKS_AVAILABLE_PORTS = // Currently we consider all ports to be available diff --git a/build-windows-modules.sh b/build-windows-modules.sh index c5bf60885f..97392cc9d6 100755 --- a/build-windows-modules.sh +++ b/build-windows-modules.sh @@ -88,7 +88,12 @@ function build_solution_config { fi set -x - cmd.exe "/c msbuild.exe $MAX_CPU_COUNT_ARG $(to_win_path "$sln") /p:Configuration=$config /p:Platform=$platform" + # Note: We've seen issues in CI (Windows ARM) indicating that the amount of memory that VS is allowed to reserve + # for pre-compiled headers is too small. '/Zm' allow us to tweak this value. /Zm100 is the default, and the value + # represents a multiplier expressed in percents. That is, /Zm400 equates to 4x the amount of memory VS is allowed + # to reserve compared to the default value. This parameter may be subject to tweaking if the issue persists. + # /Zm200 was not enough from our empirical testing, so /Zm400 was semi-arbitrarily chosen for now. + cmd.exe "/c msbuild.exe $MAX_CPU_COUNT_ARG $(to_win_path "$sln") /p:Configuration=$config /p:Platform=$platform /p:AdditionalOptions=/Zm400" set +x } diff --git a/desktop/packages/mullvad-vpn/src/renderer/components/views/udp-over-tcp-settings/components/udp-over-tcp-port-setting/UdpOverTcpPortSetting.tsx b/desktop/packages/mullvad-vpn/src/renderer/components/views/udp-over-tcp-settings/components/udp-over-tcp-port-setting/UdpOverTcpPortSetting.tsx index 83d7a3797b..90b9b7698f 100644 --- a/desktop/packages/mullvad-vpn/src/renderer/components/views/udp-over-tcp-settings/components/udp-over-tcp-port-setting/UdpOverTcpPortSetting.tsx +++ b/desktop/packages/mullvad-vpn/src/renderer/components/views/udp-over-tcp-settings/components/udp-over-tcp-port-setting/UdpOverTcpPortSetting.tsx @@ -13,7 +13,7 @@ import InfoButton from '../../../../InfoButton'; import { ModalMessage } from '../../../../Modal'; import { SettingsListbox } from '../../../../settings-listbox'; -const UDP2TCP_PORTS = [80, 5001]; +const UDP2TCP_PORTS = [80, 443, 5001]; function mapPortToSelectorItem(value: number): SelectorItem<number> { return { label: value.toString(), value }; diff --git a/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/tunnel-state.spec.ts b/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/tunnel-state.spec.ts index ace44e9b35..bf99ef77a1 100644 --- a/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/tunnel-state.spec.ts +++ b/desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/tunnel-state.spec.ts @@ -128,10 +128,10 @@ test.describe('Tunnel state and settings', () => { await routes.main.expandConnectionPanel(); const inIp = routes.main.getInIp(); - await expect(inIp).toHaveText(new RegExp(`${escapeRegExp(IN_IP!)}:(80|5001) TCP`)); + await expect(inIp).toHaveText(new RegExp(`${escapeRegExp(IN_IP!)}:(80|443|5001) TCP`)); }); - for (const port of [80, 5001]) { + for (const port of [80, 443, 5001]) { test(`App should show port ${port}`, async () => { await gotoUdpOverTcpSettings(); await routes.udpOverTcpSettings.selectPort(port); diff --git a/dist-assets/android-version-code.txt b/dist-assets/android-version-code.txt index 9d1b9ac2a2..484e1336ca 100644 --- a/dist-assets/android-version-code.txt +++ b/dist-assets/android-version-code.txt @@ -1 +1 @@ -25101001 +25101002 diff --git a/dist-assets/android-version-name.txt b/dist-assets/android-version-name.txt index d0e3b100a7..96247370a4 100644 --- a/dist-assets/android-version-name.txt +++ b/dist-assets/android-version-name.txt @@ -1 +1 @@ -2025.10-beta1 +2025.10-beta2 diff --git a/dist-assets/desktop-product-version.txt b/dist-assets/desktop-product-version.txt index 2042303cab..7243e21ea9 100644 --- a/dist-assets/desktop-product-version.txt +++ b/dist-assets/desktop-product-version.txt @@ -1 +1 @@ -2025.13 +2025.14-beta1 diff --git a/docs/relay-selector.md b/docs/relay-selector.md index 6a0987e098..76c1a061bb 100644 --- a/docs/relay-selector.md +++ b/docs/relay-selector.md @@ -63,7 +63,7 @@ As such, the above algorithm is simplified to the following version: ### Random Ports for UDP2TCP and Shadowsocks -- The UDP2TCP random port is **either** 80 **or** 5001 +- The UDP2TCP random port is one of 80, 443 or 5001. - The Shadowsocks port is random within a certain range of ports defined by the relay list ### Ports for QUIC diff --git a/ios/Assets/Localizable.xcstrings b/ios/Assets/Localizable.xcstrings index 3e31f54d0a..b8b0e1fd47 100644 --- a/ios/Assets/Localizable.xcstrings +++ b/ios/Assets/Localizable.xcstrings @@ -1,9 +1,6 @@ { "sourceLanguage" : "en", "strings" : { - "“%@ Local network sharing” requires restarting the VPN connection, which will disconnect you and briefly expose your traffic.\nTo prevent this, manually enable Airplane Mode and turn off Wi-Fi before continuing.\nWould you like to continue to enable “Local network sharing”?" : { - - }, "**Attention: This increases network traffic and will also negatively affect speed, latency, and battery usage. Use with caution on limited plans.**" : { "localizations" : { "da" : { @@ -98,6 +95,9 @@ "%@" : { }, + "%@ “Local network sharing” requires restarting the VPN connection, which will disconnect you and briefly expose your traffic.\nTo prevent this, manually enable Airplane Mode and turn off Wi-Fi before continuing.\nWould you like to continue to enable “Local network sharing”?" : { + + }, "%@ (%@) hides patterns in your encrypted VPN traffic." : { "localizations" : { "da" : { @@ -343,6 +343,9 @@ } } }, + "%@ have been added to your account." : { + + }, "%@ left on this account" : { }, @@ -2153,6 +2156,9 @@ } } }, + "Add Time" : { + + }, "Adelaide" : { "localizations" : { "da" : { @@ -3796,7 +3802,7 @@ "App logs" : { }, - "AppStore receipt is not found on disk." : { + "App Store receipt is not found on disk." : { }, "Are you sure you want to log %@ out?" : { @@ -7482,10 +7488,10 @@ "Cannot complete the purchase" : { }, - "Cannot read the AppStore receipt from disk" : { + "Cannot read the App Store receipt from disk" : { }, - "Cannot refresh the AppStore receipt: %@" : { + "Cannot refresh the App Store receipt: %@" : { }, "Cannot restore purchases" : { @@ -14739,6 +14745,9 @@ } } }, + "Failed to reach Mullvad servers to initiate purchase" : { + + }, "Failed to send" : { "localizations" : { "da" : { @@ -14872,6 +14881,12 @@ "Failed to stop the tunnel." : { }, + "Failed to upload receipt to Mullvad servers. Try again later or contact support for help." : { + + }, + "Failed to verify transaction receipt" : { + + }, "FAQs & Guides" : { }, @@ -22799,9 +22814,6 @@ } } }, - "Make a purchase with StoreKit2" : { - - }, "Malaysia" : { "localizations" : { "da" : { @@ -45284,6 +45296,9 @@ "Your previous purchases have already been added to this account." : { }, + "Your previous purchases have been added to your account." : { + + }, "Your purchase was successfully refunded." : { }, diff --git a/ios/Assets/RelayLocationList.swift b/ios/Assets/RelayLocationList.swift index 20878208a2..6a2d22a20b 100644 --- a/ios/Assets/RelayLocationList.swift +++ b/ios/Assets/RelayLocationList.swift @@ -2,7 +2,7 @@ import Foundation -let allLocations: [String: String] = [ +private let relayLocationList: [String: String] = [ "Australia": NSLocalizedString("Australia", comment: ""), "Netherlands": NSLocalizedString("Netherlands", comment: ""), "USA": NSLocalizedString("USA", comment: ""), diff --git a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift index 767a9e3e4d..6f1e780a3c 100644 --- a/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift +++ b/ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift @@ -19,7 +19,7 @@ struct APIProxyStub: APIQuerying { var sendProblemReportResult: Result<Void, Error> = .failure(APIProxyStubError()) var submitVoucherResult: Result<REST.SubmitVoucherResponse, Error> = .failure(APIProxyStubError()) var legacyStorekitPaymentResult: Result<REST.CreateApplePaymentResponse, Error> = .failure(APIProxyStubError()) - var initStorekitPaymentResult: Result<String, Error> = .failure(APIProxyStubError()) + var initStorekitPaymentResult: Result<UUID, Error> = .failure(APIProxyStubError()) var checkStorekitPaymentResult: Result<Void, Error> = .failure(APIProxyStubError()) var checkApiAvailabilityResult: Result<Bool, Error> = .failure(APIProxyStubError()) @@ -40,15 +40,6 @@ struct APIProxyStub: APIQuerying { return AnyCancellable() } - func createApplePayment( - accountNumber: String, - receiptString: Data - ) -> any RESTRequestExecutor<REST.CreateApplePaymentResponse> { - RESTRequestExecutorStub<REST.CreateApplePaymentResponse>(success: { - .timeAdded(42, .distantFuture) - }) - } - func sendProblemReport( _ body: ProblemReportRequest, retryStrategy: REST.RetryStrategy, @@ -68,9 +59,9 @@ struct APIProxyStub: APIQuerying { return AnyCancellable() } - func legacyStorekitPayment( + func legacyStoreKitPayment( accountNumber: String, - request: LegacyStorekitRequest, + request: LegacyStoreKitRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<REST.CreateApplePaymentResponse> ) -> any Cancellable { @@ -78,18 +69,17 @@ struct APIProxyStub: APIQuerying { return AnyCancellable() } - func initStorekitPayment( + func initStoreKitPayment( accountNumber: String, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler<String> + completionHandler: @escaping ProxyCompletionHandler<UUID> ) -> any MullvadTypes.Cancellable { completionHandler(initStorekitPaymentResult) return AnyCancellable() } - func checkStorekitPayment( - accountNumber: String, - transaction: StorekitTransaction, + func checkStoreKitPayment( + transaction: StoreKitTransaction, retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<Void> ) -> any MullvadTypes.Cancellable { diff --git a/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift new file mode 100644 index 0000000000..a84e53101a --- /dev/null +++ b/ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift @@ -0,0 +1,253 @@ +// +// RESTAPIProxy.swift +// MullvadREST +// +// Created by pronebird on 10/07/2020. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import MullvadRustRuntime +import MullvadTypes +import Operations +import WireGuardKitTypes + +extension REST { + public final class APIProxy: Proxy<AuthProxyConfiguration>, APIQuerying, @unchecked Sendable { + public init(configuration: AuthProxyConfiguration) { + super.init( + name: "APIProxy", + configuration: configuration, + requestFactory: RequestFactory.withDefaultAPICredentials( + pathPrefix: "/app/v1", + bodyEncoder: Coding.makeJSONEncoder() + ), + responseDecoder: Coding.makeJSONDecoder() + ) + } + + public func getAddressList( + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<[AnyIPEndpoint]> + ) -> Cancellable { + let requestHandler = AnyRequestHandler { endpoint in + try self.requestFactory.createRequest( + endpoint: endpoint, + method: .get, + pathTemplate: "api-addrs" + ) + } + + let responseHandler = REST.defaultResponseHandler( + decoding: [AnyIPEndpoint].self, + with: responseDecoder + ) + + let executor = makeRequestExecutor( + name: "get-api-addrs", + requestHandler: requestHandler, + responseHandler: responseHandler + ) + + return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler) + } + + public func getRelays( + etag: String?, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<ServerRelaysCacheResponse> + ) -> Cancellable { + let requestHandler = AnyRequestHandler { endpoint in + var requestBuilder = try self.requestFactory.createRequestBuilder( + endpoint: endpoint, + method: .get, + pathTemplate: "relays" + ) + + if let etag { + requestBuilder.setETagHeader(etag: etag) + } + + return requestBuilder.getRequest() + } + + let responseHandler = + AnyResponseHandler { response, data -> ResponseHandlerResult<ServerRelaysCacheResponse> in + let httpStatus = HTTPStatus(rawValue: response.statusCode) + + switch httpStatus { + case let httpStatus where httpStatus.isSuccess: + return .decoding { + // Discarding result since we're only interested in knowing that it's parseable. + _ = try self.responseDecoder.decode( + ServerRelaysResponse.self, + from: data + ) + let newEtag = response.value(forHTTPHeaderField: HTTPHeader.etag) + + return .newContent(newEtag, data) + } + + case .notModified where etag != nil: + return .success(.notModified) + + default: + return .unhandledResponse( + try? self.responseDecoder.decode( + ServerErrorResponse.self, + from: data + ) + ) + } + } + + let executor = makeRequestExecutor( + name: "get-relays", + requestHandler: requestHandler, + responseHandler: responseHandler + ) + + return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler) + } + + public func sendProblemReport( + _ body: ProblemReportRequest, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<Void> + ) -> Cancellable { + let requestHandler = AnyRequestHandler { endpoint in + var requestBuilder = try self.requestFactory.createRequestBuilder( + endpoint: endpoint, + method: .post, + pathTemplate: "problem-report" + ) + + try requestBuilder.setHTTPBody(value: body) + + return requestBuilder.getRequest() + } + + let responseHandler = + AnyResponseHandler { response, data -> ResponseHandlerResult<Void> in + if HTTPStatus.isSuccess(response.statusCode) { + return .success(()) + } else { + return .unhandledResponse( + try? self.responseDecoder.decode( + ServerErrorResponse.self, + from: data + ) + ) + } + } + + let executor = makeRequestExecutor( + name: "send-problem-report", + requestHandler: requestHandler, + responseHandler: responseHandler + ) + + return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler) + } + + public func submitVoucher( + voucherCode: String, + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping @Sendable ProxyCompletionHandler<SubmitVoucherResponse> + ) -> Cancellable { + let requestHandler = AnyRequestHandler( + createURLRequest: { endpoint, authorization in + var requestBuilder = try self.requestFactory.createRequestBuilder( + endpoint: endpoint, + method: .post, + pathTemplate: "submit-voucher" + ) + + requestBuilder.setAuthorization(authorization) + + try requestBuilder.setHTTPBody(value: SubmitVoucherRequest(voucherCode: voucherCode)) + + return requestBuilder.getRequest() + }, + authorizationProvider: createAuthorizationProvider(accountNumber: accountNumber) + ) + + let responseHandler = AnyResponseHandler { response, data -> ResponseHandlerResult<SubmitVoucherResponse> in + if HTTPStatus.isSuccess(response.statusCode) { + return .decoding { + try self.responseDecoder.decode(SubmitVoucherResponse.self, from: data) + } + } else { + return .unhandledResponse( + try? self.responseDecoder.decode(ServerErrorResponse.self, from: data) + ) + } + } + + let executor = makeRequestExecutor( + name: "submit-voucher", + requestHandler: requestHandler, + responseHandler: responseHandler + ) + + return executor.execute(retryStrategy: retryStrategy, completionHandler: completionHandler) + } + + /// Not implemented. Use `MullvadAPIProxy` instead. + public func legacyStoreKitPayment( + accountNumber: String, + request: LegacyStoreKitRequest, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<REST.CreateApplePaymentResponse> + ) -> any Cancellable { + fatalError("Not implemented. Use `MullvadAPIProxy` instead.") + } + + /// Not implemented. Use `MullvadAPIProxy` instead. + public func initStoreKitPayment( + accountNumber: String, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<UUID> + ) -> any Cancellable { + fatalError("Not implemented. Use `MullvadAPIProxy` instead.") + } + + /// Not implemented. Use `MullvadAPIProxy` instead. + public func checkStoreKitPayment( + transaction: StoreKitTransaction, + retryStrategy: REST.RetryStrategy, + completionHandler: @escaping ProxyCompletionHandler<Void> + ) -> any Cancellable { + fatalError("Not implemented. Use `MullvadAPIProxy` instead.") + } + + /// Not implemented. Use `MullvadAPIProxy` instead. + public func checkApiAvailability( + retryStrategy: REST.RetryStrategy, + accessMethod: PersistentAccessMethod, + completion: @escaping ProxyCompletionHandler<Bool> + ) -> any Cancellable { + fatalError("Not implemented. Use `MullvadAPIProxy` instead.") + } + } + + // MARK: - Response types + + private struct SubmitVoucherRequest: Encodable, Sendable { + let voucherCode: String + } + + public struct SubmitVoucherResponse: Decodable, Sendable { + public let timeAdded: Int + public let newExpiry: Date + + public var dateComponents: DateComponents { + DateComponents(second: timeAdded) + } + } + + public struct UUIDParseError: Swift.Error { + public let payload: String + } +} diff --git a/ios/MullvadREST/Assets/relays.json b/ios/MullvadREST/Assets/relays.json index 6fc256d732..17105c7348 100644 --- a/ios/MullvadREST/Assets/relays.json +++ b/ios/MullvadREST/Assets/relays.json @@ -1 +1 @@ -{"locations":{"au-adl":{"country":"Australia","city":"Adelaide","latitude":-34.92123,"longitude":138.599503},"nl-ams":{"country":"Netherlands","city":"Amsterdam","latitude":52.35,"longitude":4.916667},"us-qas":{"country":"USA","city":"Ashburn, VA","latitude":39.043757,"longitude":-77.487442},"gr-ath":{"country":"Greece","city":"Athens","latitude":37.98381,"longitude":23.727539},"us-atl":{"country":"USA","city":"Atlanta, GA","latitude":33.753746,"longitude":-84.38633},"nz-akl":{"country":"New Zealand","city":"Auckland","latitude":-36.848461,"longitude":174.763336},"th-bkk":{"country":"Thailand","city":"Bangkok","latitude":13.756331,"longitude":100.501762},"es-bcn":{"country":"Spain","city":"Barcelona","latitude":41.385063,"longitude":2.173404},"rs-beg":{"country":"Serbia","city":"Belgrade","latitude":44.787197,"longitude":20.457273},"de-ber":{"country":"Germany","city":"Berlin","latitude":52.520008,"longitude":13.404954},"co-bog":{"country":"Colombia","city":"Bogota","latitude":4.624335,"longitude":-74.063644},"fr-bod":{"country":"France","city":"Bordeaux","latitude":44.837788,"longitude":-0.57918},"us-bos":{"country":"USA","city":"Boston, MA","latitude":42.361145,"longitude":-71.057083},"sk-bts":{"country":"Slovakia","city":"Bratislava","latitude":48.148598,"longitude":17.107748},"au-bne":{"country":"Australia","city":"Brisbane","latitude":-27.471,"longitude":153.0234},"be-bru":{"country":"Belgium","city":"Brussels","latitude":50.833333,"longitude":4.333333},"ro-buh":{"country":"Romania","city":"Bucharest","latitude":44.433333,"longitude":26.1},"hu-bud":{"country":"Hungary","city":"Budapest","latitude":47.5,"longitude":19.083333},"ar-bue":{"country":"Argentina","city":"Buenos Aires","latitude":-34.474561,"longitude":-58.664522},"ca-yyc":{"country":"Canada","city":"Calgary","latitude":51.037007,"longitude":-114.058315},"us-chi":{"country":"USA","city":"Chicago, IL","latitude":41.881832,"longitude":-87.623177},"dk-cph":{"country":"Denmark","city":"Copenhagen","latitude":55.666667,"longitude":12.583333},"us-dal":{"country":"USA","city":"Dallas, TX","latitude":32.89748,"longitude":-97.040443},"us-den":{"country":"USA","city":"Denver, CO","latitude":39.739236,"longitude":-104.990251},"us-det":{"country":"USA","city":"Detroit, MI","latitude":42.331389,"longitude":-83.045833},"ie-dub":{"country":"Ireland","city":"Dublin","latitude":53.35014,"longitude":-6.266155},"de-dus":{"country":"Germany","city":"Dusseldorf","latitude":51.233334,"longitude":6.783333},"br-for":{"country":"Brazil","city":"Fortaleza","latitude":-3.732714,"longitude":-38.526997},"de-fra":{"country":"Germany","city":"Frankfurt","latitude":50.110924,"longitude":8.682127},"gb-glw":{"country":"UK","city":"Glasgow","latitude":55.86515,"longitude":-4.25763},"se-got":{"country":"Sweden","city":"Gothenburg","latitude":57.70887,"longitude":11.97456},"fi-hel":{"country":"Finland","city":"Helsinki","latitude":60.192059,"longitude":24.945831},"hk-hkg":{"country":"Hong Kong","city":"Hong Kong","latitude":22.283333,"longitude":114.15},"us-hou":{"country":"USA","city":"Houston, TX","latitude":29.749907,"longitude":-95.358421},"tr-ist":{"country":"Turkey","city":"Istanbul","latitude":41.00824,"longitude":28.978359},"id-jpu":{"country":"Indonesia","city":"Jakarta","latitude":-6.17511,"longitude":106.865036},"za-jnb":{"country":"South Africa","city":"Johannesburg","latitude":-26.195246,"longitude":28.034088},"us-mkc":{"country":"USA","city":"Kansas City, MO","latitude":39.099789,"longitude":-94.57856},"my-kul":{"country":"Malaysia","city":"Kuala Lumpur","latitude":3.139003,"longitude":101.686852},"ua-iev":{"country":"Ukraine","city":"Kyiv","latitude":50.4501,"longitude":30.5234},"ng-los":{"country":"Nigeria","city":"Lagos","latitude":6.524379,"longitude":3.379206},"pe-lim":{"country":"Peru","city":"Lima","latitude":-12.046373,"longitude":-77.042755},"pt-lis":{"country":"Portugal","city":"Lisbon","latitude":38.736946,"longitude":-9.142685},"si-lju":{"country":"Slovenia","city":"Ljubljana","latitude":46.0569,"longitude":14.5057},"gb-lon":{"country":"UK","city":"London","latitude":51.514125,"longitude":-0.093689},"us-lax":{"country":"USA","city":"Los Angeles, CA","latitude":34.052235,"longitude":-118.243683},"es-mad":{"country":"Spain","city":"Madrid","latitude":40.408566,"longitude":-3.69222},"se-mma":{"country":"Sweden","city":"Malmö","latitude":55.607075,"longitude":13.002716},"gb-mnc":{"country":"UK","city":"Manchester","latitude":53.5,"longitude":-2.216667},"ph-mnl":{"country":"Philippines","city":"Manila","latitude":14.599512,"longitude":120.984222},"fr-mrs":{"country":"France","city":"Marseille","latitude":43.29648,"longitude":5.38107},"us-txc":{"country":"USA","city":"McAllen, TX","latitude":26.203407,"longitude":-98.230011},"au-mel":{"country":"Australia","city":"Melbourne","latitude":-37.815018,"longitude":144.946014},"us-mia":{"country":"USA","city":"Miami, FL","latitude":25.761681,"longitude":-80.191788},"it-mil":{"country":"Italy","city":"Milan","latitude":45.466667,"longitude":9.2},"ca-mtr":{"country":"Canada","city":"Montreal","latitude":45.5053,"longitude":-73.5525},"us-nyc":{"country":"USA","city":"New York, NY","latitude":40.73061,"longitude":-73.935242},"cy-nic":{"country":"Cyprus","city":"Nicosia","latitude":35.17025,"longitude":33.3587},"jp-osa":{"country":"Japan","city":"Osaka","latitude":34.672314,"longitude":135.484802},"no-osl":{"country":"Norway","city":"Oslo","latitude":59.916667,"longitude":10.75},"it-pmo":{"country":"Italy","city":"Palermo","latitude":38.115688,"longitude":13.361267},"fr-par":{"country":"France","city":"Paris","latitude":48.866667,"longitude":2.333333},"au-per":{"country":"Australia","city":"Perth","latitude":-31.953512,"longitude":115.857048},"us-phx":{"country":"USA","city":"Phoenix, AZ","latitude":33.448376,"longitude":-112.074036},"cz-prg":{"country":"Czech Republic","city":"Prague","latitude":50.083333,"longitude":14.466667},"mx-qro":{"country":"Mexico","city":"Queretaro","latitude":20.592774,"longitude":-100.390225},"us-rag":{"country":"USA","city":"Raleigh, NC","latitude":35.787743,"longitude":-78.644257},"us-slc":{"country":"USA","city":"Salt Lake City, UT","latitude":40.758701,"longitude":-111.876183},"us-sjc":{"country":"USA","city":"San Jose, CA","latitude":37.338208,"longitude":-121.886329},"cl-scl":{"country":"Chile","city":"Santiago","latitude":-33.448891,"longitude":-70.669266},"br-sao":{"country":"Brazil","city":"Sao Paulo","latitude":-23.533773,"longitude":-46.62529},"us-sea":{"country":"USA","city":"Seattle, WA","latitude":47.608013,"longitude":-122.335167},"us-uyk":{"country":"USA","city":"Secaucus, NJ","latitude":40.789543,"longitude":-74.0565},"sg-sin":{"country":"Singapore","city":"Singapore","latitude":1.293056,"longitude":103.855833},"bg-sof":{"country":"Bulgaria","city":"Sofia","latitude":42.683333,"longitude":23.316667},"no-svg":{"country":"Norway","city":"Stavanger","latitude":58.964432,"longitude":5.72625},"se-sto":{"country":"Sweden","city":"Stockholm","latitude":59.3289,"longitude":18.0649},"au-syd":{"country":"Australia","city":"Sydney","latitude":-33.861481,"longitude":151.205475},"ee-tll":{"country":"Estonia","city":"Tallinn","latitude":59.436961,"longitude":24.753575},"il-tlv":{"country":"Israel","city":"Tel Aviv","latitude":32.0853,"longitude":34.781768},"al-tia":{"country":"Albania","city":"Tirana","latitude":41.327953,"longitude":19.819025},"jp-tyo":{"country":"Japan","city":"Tokyo","latitude":35.685,"longitude":139.751389},"ca-tor":{"country":"Canada","city":"Toronto","latitude":43.666667,"longitude":-79.416667},"es-vlc":{"country":"Spain","city":"Valencia","latitude":39.466667,"longitude":-0.375},"ca-van":{"country":"Canada","city":"Vancouver","latitude":49.25,"longitude":-123.133333},"at-vie":{"country":"Austria","city":"Vienna","latitude":48.210033,"longitude":16.363449},"pl-waw":{"country":"Poland","city":"Warsaw","latitude":52.25,"longitude":21.0},"us-was":{"country":"USA","city":"Washington DC","latitude":38.889484,"longitude":-77.035278},"hr-zag":{"country":"Croatia","city":"Zagreb","latitude":45.821,"longitude":15.973},"ch-zrh":{"country":"Switzerland","city":"Zurich","latitude":47.366667,"longitude":8.55}},"openvpn":{"relays":[{"hostname":"at-vie-ovpn-001","location":"at-vie","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.116.194","include_in_country":true,"weight":100},{"hostname":"at-vie-ovpn-002","location":"at-vie","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.116.226","include_in_country":true,"weight":100},{"hostname":"au-adl-ovpn-301","location":"au-adl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.214.20.146","include_in_country":true,"weight":100},{"hostname":"au-adl-ovpn-302","location":"au-adl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.214.20.162","include_in_country":true,"weight":100},{"hostname":"au-bne-ovpn-301","location":"au-bne","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.216.220.50","include_in_country":true,"weight":100},{"hostname":"au-bne-ovpn-302","location":"au-bne","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.216.220.66","include_in_country":true,"weight":100},{"hostname":"au-mel-ovpn-301","location":"au-mel","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.229.82","include_in_country":true,"weight":100},{"hostname":"au-mel-ovpn-302","location":"au-mel","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.229.98","include_in_country":true,"weight":100},{"hostname":"au-per-ovpn-301","location":"au-per","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.231.82","include_in_country":true,"weight":100},{"hostname":"au-per-ovpn-302","location":"au-per","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.231.98","include_in_country":true,"weight":100},{"hostname":"au-syd-ovpn-001","location":"au-syd","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.200.130","include_in_country":true,"weight":100},{"hostname":"au-syd-ovpn-002","location":"au-syd","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.200.66","include_in_country":true,"weight":100},{"hostname":"be-bru-ovpn-101","location":"be-bru","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"82.102.19.66","include_in_country":true,"weight":100},{"hostname":"be-bru-ovpn-102","location":"be-bru","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"91.207.57.130","include_in_country":true,"weight":100},{"hostname":"bg-sof-ovpn-001","location":"bg-sof","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.188.66","include_in_country":true,"weight":100},{"hostname":"bg-sof-ovpn-002","location":"bg-sof","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.188.2","include_in_country":true,"weight":100},{"hostname":"ca-mtr-ovpn-001","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"37.120.237.66","include_in_country":true,"weight":100},{"hostname":"ca-mtr-ovpn-002","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"45.133.182.194","include_in_country":true,"weight":100},{"hostname":"ca-tor-ovpn-001","location":"ca-tor","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.214.193","include_in_country":true,"weight":100},{"hostname":"ca-tor-ovpn-002","location":"ca-tor","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.214.206","include_in_country":true,"weight":100},{"hostname":"ca-van-ovpn-201","location":"ca-van","active":true,"owned":false,"provider":"techfutures","stboot":true,"ipv4_addr_in":"104.193.135.132","include_in_country":false,"weight":100},{"hostname":"ca-van-ovpn-202","location":"ca-van","active":true,"owned":false,"provider":"techfutures","stboot":true,"ipv4_addr_in":"104.193.135.164","include_in_country":false,"weight":100},{"hostname":"ch-zrh-ovpn-001","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.81","include_in_country":true,"weight":1},{"hostname":"ch-zrh-ovpn-002","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.82","include_in_country":true,"weight":1},{"hostname":"ch-zrh-ovpn-003","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.83","include_in_country":true,"weight":1},{"hostname":"ch-zrh-ovpn-201","location":"ch-zrh","active":true,"owned":false,"provider":"PrivateLayer","stboot":true,"ipv4_addr_in":"46.19.140.194","include_in_country":true,"weight":100},{"hostname":"ch-zrh-ovpn-202","location":"ch-zrh","active":true,"owned":false,"provider":"PrivateLayer","stboot":true,"ipv4_addr_in":"81.17.16.66","include_in_country":true,"weight":100},{"hostname":"ch-zrh-ovpn-501","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.134.130","include_in_country":true,"weight":100},{"hostname":"ch-zrh-ovpn-502","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.134.162","include_in_country":true,"weight":100},{"hostname":"cz-prg-ovpn-101","location":"cz-prg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.129.162","include_in_country":true,"weight":100},{"hostname":"cz-prg-ovpn-102","location":"cz-prg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.129.194","include_in_country":true,"weight":100},{"hostname":"de-ber-ovpn-001","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.72","include_in_country":true,"weight":100},{"hostname":"de-fra-ovpn-001","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.66","include_in_country":true,"weight":100},{"hostname":"de-fra-ovpn-002","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.67","include_in_country":true,"weight":100},{"hostname":"de-fra-ovpn-003","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.68","include_in_country":true,"weight":100},{"hostname":"de-fra-ovpn-004","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.69","include_in_country":true,"weight":100},{"hostname":"de-fra-ovpn-101","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.66","include_in_country":true,"weight":100},{"hostname":"de-fra-ovpn-102","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.98","include_in_country":true,"weight":100},{"hostname":"dk-cph-ovpn-001","location":"dk-cph","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.254.71","include_in_country":true,"weight":100},{"hostname":"dk-cph-ovpn-002","location":"dk-cph","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.129.56.81","include_in_country":true,"weight":100},{"hostname":"dk-cph-ovpn-401","location":"dk-cph","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.197.66","include_in_country":true,"weight":100},{"hostname":"dk-cph-ovpn-402","location":"dk-cph","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.197.2","include_in_country":true,"weight":100},{"hostname":"es-mad-ovpn-201","location":"es-mad","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.128.162","include_in_country":true,"weight":100},{"hostname":"es-mad-ovpn-202","location":"es-mad","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.74.98","include_in_country":true,"weight":100},{"hostname":"fi-hel-ovpn-002","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.172","include_in_country":false,"weight":1},{"hostname":"fi-hel-ovpn-003","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.173","include_in_country":false,"weight":1},{"hostname":"fi-hel-ovpn-004","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.174","include_in_country":false,"weight":1},{"hostname":"fi-hel-ovpn-005","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.175","include_in_country":false,"weight":1},{"hostname":"fi-hel-ovpn-006","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.176","include_in_country":false,"weight":1},{"hostname":"fi-hel-ovpn-007","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.212.149.201","include_in_country":false,"weight":1},{"hostname":"fi-hel-ovpn-101","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.217","include_in_country":true,"weight":101},{"hostname":"fi-hel-ovpn-102","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.237","include_in_country":true,"weight":100},{"hostname":"fr-par-ovpn-001","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.81","include_in_country":true,"weight":100},{"hostname":"fr-par-ovpn-002","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.82","include_in_country":true,"weight":100},{"hostname":"fr-par-ovpn-101","location":"fr-par","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.184.130","include_in_country":true,"weight":1},{"hostname":"fr-par-ovpn-102","location":"fr-par","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.184.194","include_in_country":true,"weight":1},{"hostname":"gb-lon-ovpn-001","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.131","include_in_country":true,"weight":100},{"hostname":"gb-lon-ovpn-002","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.132","include_in_country":true,"weight":100},{"hostname":"gb-lon-ovpn-003","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.133","include_in_country":true,"weight":100},{"hostname":"gb-lon-ovpn-301","location":"gb-lon","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.119.98","include_in_country":true,"weight":100},{"hostname":"gb-lon-ovpn-302","location":"gb-lon","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.119.130","include_in_country":true,"weight":100},{"hostname":"gb-mnc-ovpn-001","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.2","include_in_country":false,"weight":100},{"hostname":"gb-mnc-ovpn-002","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.34","include_in_country":false,"weight":100},{"hostname":"gb-mnc-ovpn-003","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.66","include_in_country":false,"weight":100},{"hostname":"gr-ath-ovpn-101","location":"gr-ath","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.246.28","include_in_country":true,"weight":100},{"hostname":"gr-ath-ovpn-102","location":"gr-ath","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.246.41","include_in_country":true,"weight":100},{"hostname":"hk-hkg-ovpn-201","location":"hk-hkg","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.125.233.33","include_in_country":true,"weight":100},{"hostname":"hk-hkg-ovpn-202","location":"hk-hkg","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.125.233.48","include_in_country":true,"weight":100},{"hostname":"hk-hkg-ovpn-301","location":"hk-hkg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.224.130","include_in_country":true,"weight":100},{"hostname":"hk-hkg-ovpn-302","location":"hk-hkg","active":false,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.224.196","include_in_country":true,"weight":100},{"hostname":"hu-bud-ovpn-101","location":"hu-bud","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.196.66","include_in_country":true,"weight":100},{"hostname":"hu-bud-ovpn-102","location":"hu-bud","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.196.2","include_in_country":true,"weight":100},{"hostname":"ie-dub-ovpn-101","location":"ie-dub","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.189.130","include_in_country":true,"weight":100},{"hostname":"ie-dub-ovpn-102","location":"ie-dub","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.189.194","include_in_country":true,"weight":100},{"hostname":"it-mil-ovpn-201","location":"it-mil","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.225.130","include_in_country":true,"weight":100},{"hostname":"it-mil-ovpn-202","location":"it-mil","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.225.194","include_in_country":true,"weight":100},{"hostname":"jp-tyo-ovpn-201","location":"jp-tyo","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.201.130","include_in_country":true,"weight":100},{"hostname":"jp-tyo-ovpn-202","location":"jp-tyo","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.201.194","include_in_country":true,"weight":100},{"hostname":"nl-ams-ovpn-001","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.71","include_in_country":true,"weight":100},{"hostname":"nl-ams-ovpn-002","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.72","include_in_country":true,"weight":100},{"hostname":"nl-ams-ovpn-003","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.73","include_in_country":true,"weight":100},{"hostname":"nl-ams-ovpn-004","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.74","include_in_country":true,"weight":100},{"hostname":"nl-ams-ovpn-005","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.75","include_in_country":true,"weight":100},{"hostname":"no-osl-ovpn-001","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"91.90.44.11","include_in_country":true,"weight":100},{"hostname":"no-osl-ovpn-002","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"91.90.44.12","include_in_country":true,"weight":100},{"hostname":"no-osl-ovpn-003","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"91.90.44.13","include_in_country":true,"weight":100},{"hostname":"no-svg-ovpn-001","location":"no-svg","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.114","include_in_country":true,"weight":100},{"hostname":"no-svg-ovpn-002","location":"no-svg","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.145","include_in_country":true,"weight":100},{"hostname":"nz-akl-ovpn-301","location":"nz-akl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.75.11.82","include_in_country":true,"weight":100},{"hostname":"nz-akl-ovpn-302","location":"nz-akl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.75.11.98","include_in_country":true,"weight":100},{"hostname":"pl-waw-ovpn-201","location":"pl-waw","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.144.66","include_in_country":true,"weight":100},{"hostname":"pl-waw-ovpn-202","location":"pl-waw","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.144.98","include_in_country":true,"weight":100},{"hostname":"ro-buh-ovpn-001","location":"ro-buh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.124.162","include_in_country":true,"weight":100},{"hostname":"ro-buh-ovpn-002","location":"ro-buh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"37.120.246.130","include_in_country":true,"weight":100},{"hostname":"rs-beg-ovpn-101","location":"rs-beg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.193.194","include_in_country":true,"weight":100},{"hostname":"rs-beg-ovpn-102","location":"rs-beg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.193.130","include_in_country":true,"weight":100},{"hostname":"se-got-ovpn-001","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.131","include_in_country":false,"weight":100},{"hostname":"se-mma-ovpn-001","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.131","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-002","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.132","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-013","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.83","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-014","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.84","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-015","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.85","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-016","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.86","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-017","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.87","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-018","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.88","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-019","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.89","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-020","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.90","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-021","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.91","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-022","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.92","include_in_country":true,"weight":100},{"hostname":"se-mma-ovpn-102","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.83.220.92","include_in_country":true,"weight":100},{"hostname":"se-sto-ovpn-001","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.80","include_in_country":true,"weight":100},{"hostname":"se-sto-ovpn-002","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.81","include_in_country":true,"weight":100},{"hostname":"se-sto-ovpn-003","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.82","include_in_country":true,"weight":100},{"hostname":"se-sto-ovpn-004","location":"se-sto","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.83","include_in_country":true,"weight":100},{"hostname":"sg-sin-ovpn-101","location":"sg-sin","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.199.66","include_in_country":true,"weight":100},{"hostname":"sg-sin-ovpn-102","location":"sg-sin","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.199.2","include_in_country":true,"weight":100},{"hostname":"us-atl-ovpn-001","location":"us-atl","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.140.156","include_in_country":false,"weight":100},{"hostname":"us-atl-ovpn-002","location":"us-atl","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.140.169","include_in_country":false,"weight":100},{"hostname":"us-dal-ovpn-001","location":"us-dal","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.211.194","include_in_country":true,"weight":100},{"hostname":"us-dal-ovpn-002","location":"us-dal","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.177.66","include_in_country":true,"weight":100},{"hostname":"us-lax-ovpn-101","location":"us-lax","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.44.129.162","include_in_country":false,"weight":100},{"hostname":"us-lax-ovpn-102","location":"us-lax","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.44.129.130","include_in_country":false,"weight":100},{"hostname":"us-lax-ovpn-201","location":"us-lax","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.203.41","include_in_country":false,"weight":100},{"hostname":"us-lax-ovpn-202","location":"us-lax","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.203.54","include_in_country":false,"weight":100},{"hostname":"us-mia-ovpn-101","location":"us-mia","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.187.194","include_in_country":true,"weight":100},{"hostname":"us-mia-ovpn-102","location":"us-mia","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.183.66","include_in_country":true,"weight":100},{"hostname":"us-nyc-ovpn-501","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.168.2","include_in_country":true,"weight":100},{"hostname":"us-nyc-ovpn-502","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.166.2","include_in_country":true,"weight":100},{"hostname":"us-nyc-ovpn-503","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.166.66","include_in_country":true,"weight":100},{"hostname":"us-nyc-ovpn-601","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.171.194","include_in_country":true,"weight":100},{"hostname":"us-nyc-ovpn-602","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.185.130","include_in_country":true,"weight":100},{"hostname":"us-nyc-ovpn-603","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.185.66","include_in_country":true,"weight":100},{"hostname":"us-qas-ovpn-001","location":"us-qas","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.135.162","include_in_country":false,"weight":100},{"hostname":"us-qas-ovpn-002","location":"us-qas","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.135.194","include_in_country":false,"weight":50},{"hostname":"us-qas-ovpn-101","location":"us-qas","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.156.46.169","include_in_country":true,"weight":100},{"hostname":"us-qas-ovpn-102","location":"us-qas","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.156.46.182","include_in_country":true,"weight":100},{"hostname":"us-sea-ovpn-101","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.131.34","include_in_country":false,"weight":0},{"hostname":"us-sea-ovpn-102","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.131.66","include_in_country":false,"weight":0},{"hostname":"us-sjc-ovpn-001","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.134.34","include_in_country":false,"weight":100},{"hostname":"us-sjc-ovpn-002","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.134.66","include_in_country":false,"weight":100},{"hostname":"us-slc-ovpn-201","location":"us-slc","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.150","include_in_country":false,"weight":100},{"hostname":"us-slc-ovpn-202","location":"us-slc","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.151","include_in_country":false,"weight":100}],"ports":[{"port":1194,"protocol":"udp"},{"port":1195,"protocol":"udp"},{"port":1196,"protocol":"udp"},{"port":1197,"protocol":"udp"},{"port":1300,"protocol":"udp"},{"port":1301,"protocol":"udp"},{"port":1302,"protocol":"udp"},{"port":443,"protocol":"tcp"},{"port":80,"protocol":"tcp"}]},"wireguard":{"relays":[{"hostname":"al-tia-wg-003","location":"al-tia","active":true,"owned":false,"provider":"iRegister","stboot":true,"ipv4_addr_in":"103.124.165.130","include_in_country":true,"weight":100,"public_key":"rWiQxq5lAWD8v/bws9ITSAvThyZW8cR2x+Ins9ZvvRo=","ipv6_addr_in":"2a04:27c0:0:c::f001","shadowsocks_extra_addr_in":["103.204.123.136"],"features":{"quic":{"addr_in":["103.124.165.135","2a04:27c0:0:c::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"al-tia-wg-003.blockerad.eu"}}},{"hostname":"al-tia-wg-004","location":"al-tia","active":true,"owned":false,"provider":"iRegister","stboot":true,"ipv4_addr_in":"103.124.165.191","include_in_country":true,"weight":100,"public_key":"x62J1c4gfHu/bF3DSjwIjC0qOE3azRG03i/YW6bOEGY=","ipv6_addr_in":"2a04:27c0:0:d::f001","shadowsocks_extra_addr_in":["103.124.165.197"],"features":{"quic":{"addr_in":["103.124.165.196","2a04:27c0:0:d::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"al-tia-wg-004.blockerad.eu"}}},{"hostname":"ar-bue-wg-001","location":"ar-bue","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.22.83.2","include_in_country":true,"weight":100,"public_key":"1WN0Mqa0Azw7cYYEamHPgHXE8SuylNIG2QobZKOaclA=","ipv6_addr_in":"2a02:6ea0:f002:1::f001","shadowsocks_extra_addr_in":["149.22.83.5"],"features":{"quic":{"addr_in":["149.22.83.4"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ar-bue-wg-001.blockerad.eu"}}},{"hostname":"ar-bue-wg-002","location":"ar-bue","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.22.83.31","include_in_country":true,"weight":100,"public_key":"gGrdozNHVCnmcX5x8OPOXfnyZ+TZWqD9GlHl1kRekyA=","ipv6_addr_in":"2a02:6ea0:f002:2::f001","shadowsocks_extra_addr_in":["149.22.83.34"],"features":{"quic":{"addr_in":["149.22.83.33"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ar-bue-wg-002.blockerad.eu"}}},{"hostname":"at-vie-wg-001","location":"at-vie","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.116.98","include_in_country":true,"weight":100,"public_key":"TNrdH73p6h2EfeXxUiLOCOWHcjmjoslLxZptZpIPQXU=","ipv6_addr_in":"2001:ac8:29:84::a01f"},{"hostname":"at-vie-wg-002","location":"at-vie","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.116.130","include_in_country":true,"weight":100,"public_key":"ehXBc726YX1N6Dm7fDAVMG5cIaYAFqCA4Lbpl4VWcWE=","ipv6_addr_in":"2001:ac8:29:85::a02f"},{"hostname":"at-vie-wg-003","location":"at-vie","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.116.162","include_in_country":true,"weight":100,"public_key":"ddllelPu2ndjSX4lHhd/kdCStaSJOQixs9z551qN6B8=","ipv6_addr_in":"2001:ac8:29:86::a03f"},{"hostname":"at-vie-wg-101","location":"at-vie","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.24.11.130","include_in_country":true,"weight":100,"public_key":"dj3qNfJfA4dWXsWokPcDh4oo6xaPtOTPfbr5UzHKZ0M=","ipv6_addr_in":"2a02:6ea0:cb1b:1::f001","shadowsocks_extra_addr_in":["185.24.11.132"]},{"hostname":"at-vie-wg-102","location":"at-vie","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.24.11.159","include_in_country":true,"weight":100,"public_key":"DANFtH+sFB19BnW1CYEwZ2pOIt7P8nLjSadjpS2rLWE=","ipv6_addr_in":"2a02:6ea0:cb1b:2::f001","shadowsocks_extra_addr_in":["185.24.11.161"]},{"hostname":"au-adl-wg-301","location":"au-adl","active":false,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.214.20.50","include_in_country":true,"weight":100,"public_key":"rm2hpBiN91c7reV+cYKlw7QNkYtME/+js7IMyYBB2Aw=","ipv6_addr_in":"2404:f780:0:deb::c1f","features":{"lwo":{}}},{"hostname":"au-adl-wg-302","location":"au-adl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.214.20.130","include_in_country":true,"weight":100,"public_key":"e4jouH8n4e8oyi/Z7d6lJLd6975hlPZmnynJeoU+nWM=","ipv6_addr_in":"2404:f780:0:dec::c2f"},{"hostname":"au-bne-wg-301","location":"au-bne","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.216.220.18","include_in_country":true,"weight":100,"public_key":"1H/gj8SVNebAIEGlvMeUVC5Rnf274dfVKbyE+v5G8HA=","ipv6_addr_in":"2404:f780:4:deb::f001"},{"hostname":"au-bne-wg-302","location":"au-bne","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.216.220.34","include_in_country":true,"weight":100,"public_key":"z+JG0QA4uNd/wRTpjCqn9rDpQsHKhf493omqQ5rqYAc=","ipv6_addr_in":"2404:f780:4:dec::a02f"},{"hostname":"au-mel-wg-302","location":"au-mel","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.229.66","include_in_country":true,"weight":100,"public_key":"npTb63jWEaJToBfn0B1iVNbnLXEwwlus5SsolsvUhgU=","ipv6_addr_in":"2406:d501:f:dec::a02f"},{"hostname":"au-per-wg-301","location":"au-per","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.231.50","include_in_country":true,"weight":100,"public_key":"hQXsNk/9R2We0pzP1S9J3oNErEu2CyENlwTdmDUYFhg=","ipv6_addr_in":"2404:f780:8:deb::a01f"},{"hostname":"au-per-wg-302","location":"au-per","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.108.231.66","include_in_country":true,"weight":100,"public_key":"t3Ly8bBdF2gMHzT3d529bVLDw8Jd2/FFG9GXoBEx01g=","ipv6_addr_in":"2404:f780:8:dec::f001"},{"hostname":"au-syd-wg-001","location":"au-syd","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.200.2","include_in_country":true,"weight":100,"public_key":"4JpfHBvthTFOhCK0f5HAbzLXAVcB97uAkuLx7E8kqW0=","ipv6_addr_in":"2001:ac8:84:5::f001","features":{"lwo":{}}},{"hostname":"au-syd-wg-002","location":"au-syd","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.141.194","include_in_country":true,"weight":100,"public_key":"lUeDAOy+iAhZDuz5+6zh0Co8wZcs3ahdu2jfqQoDW3E=","ipv6_addr_in":"2001:ac8:84:6::2f"},{"hostname":"au-syd-wg-003","location":"au-syd","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.200.194","include_in_country":true,"weight":100,"public_key":"LXuRwa9JRTt2/UtldklKGlj/IVLORITqgET4II4DRkU=","ipv6_addr_in":"2001:ac8:84:4::3f"},{"hostname":"au-syd-wg-101","location":"au-syd","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.136.147.3","include_in_country":true,"weight":100,"public_key":"NKP4jSvSDZg5HJ3JxpGYMxIYt7QzoxSFrU2F0m1ZxwA=","ipv6_addr_in":"2a11:3:500::f001"},{"hostname":"au-syd-wg-102","location":"au-syd","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.136.147.65","include_in_country":true,"weight":100,"public_key":"w825smx7YI9/SrwSYGdsuwD1Qt5UsS/CyaGTjwSYljU=","ipv6_addr_in":"2a11:3:500::f101"},{"hostname":"au-syd-wg-103","location":"au-syd","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.136.147.129","include_in_country":true,"weight":100,"public_key":"poOHsF6v91yURxDrNe/P/adyNUqsRGzhFIioyBYUPww=","ipv6_addr_in":"2a11:3:500::f201"},{"hostname":"au-syd-wg-104","location":"au-syd","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.136.147.197","include_in_country":true,"weight":100,"public_key":"61Ovy3ObuHqllZK/P/5cOWZnY26SY2csmjzVK1q+fFs=","ipv6_addr_in":"2a11:3:500::f301"},{"hostname":"au-syd-wg-301","location":"au-syd","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.120.6.2","include_in_country":true,"weight":100,"public_key":"bQIQLk9zVOZLEGJsQOMu0K3rCMc85gExkS/0b1tSVBk=","ipv6_addr_in":"2a06:3040:18:210::f001","shadowsocks_extra_addr_in":["103.120.6.13"],"features":{"lwo":{},"quic":{"addr_in":["103.120.6.12","2a06:3040:18:210::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"au-syd-wg-301.blockerad.eu"}}},{"hostname":"au-syd-wg-302","location":"au-syd","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.120.6.127","include_in_country":true,"weight":100,"public_key":"6tTqSMUVPhaMsFFdphijwdura5RnNOzlz33Ekp1oCmc=","ipv6_addr_in":"2a06:3040:18:210::f101","shadowsocks_extra_addr_in":["103.120.6.138"],"features":{"quic":{"addr_in":["103.120.6.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"au-syd-wg-302.blockerad.eu"}}},{"hostname":"au-syd-wg-303","location":"au-syd","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.141.60.2","include_in_country":true,"weight":100,"public_key":"RK/eoKsyX4fu7iJ9F5mTf07en/WgYOMAtPGivKTntlw=","ipv6_addr_in":"2a06:3040:18:210::f201","shadowsocks_extra_addr_in":["103.141.60.13"],"features":{"quic":{"addr_in":["103.141.60.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"au-syd-wg-303.blockerad.eu"}}},{"hostname":"au-syd-wg-304","location":"au-syd","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.141.60.127","include_in_country":true,"weight":100,"public_key":"gXZZhcHfOD7FtnwTw8APUnccwMTVQYDNs4bbjHGS3CI=","ipv6_addr_in":"2a06:3040:18:210::f301","shadowsocks_extra_addr_in":["103.141.60.138"],"features":{"quic":{"addr_in":["103.141.60.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"au-syd-wg-304.blockerad.eu"}}},{"hostname":"be-bru-wg-101","location":"be-bru","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"91.90.123.2","include_in_country":true,"weight":100,"public_key":"GE2WP6hmwVggSvGVWLgq2L10T3WM2VspnUptK5F4B0U=","ipv6_addr_in":"2001:ac8:27:88::a01f"},{"hostname":"be-bru-wg-102","location":"be-bru","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"194.110.115.34","include_in_country":true,"weight":100,"public_key":"IY+FKw487MEWqMGNyyrT4PnTrJxce8oiGNHT0zifam8=","ipv6_addr_in":"2001:ac8:27:89::a02f"},{"hostname":"be-bru-wg-103","location":"be-bru","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"194.110.115.2","include_in_country":true,"weight":100,"public_key":"b5A1ela+BVI+AbNXz7SWekZHvdWWpt3rqUKTJj0SqCU=","ipv6_addr_in":"2001:ac8:27:92::a03f"},{"hostname":"bg-sof-wg-001","location":"bg-sof","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.188.130","include_in_country":true,"weight":100,"public_key":"J8KysHmHZWqtrVKKOppneDXSks/PDsB1XTlRHpwiABA=","ipv6_addr_in":"2001:ac8:30:56::f001"},{"hostname":"bg-sof-wg-002","location":"bg-sof","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.188.194","include_in_country":true,"weight":100,"public_key":"dg+Fw7GnKvDPBxFpnj1KPoNIu1GakuVoDJjKRni+pRU=","ipv6_addr_in":"2001:ac8:30:57::f001"},{"hostname":"br-for-wg-001","location":"br-for","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"98.98.12.178","include_in_country":true,"weight":100,"public_key":"CiPqGvrQidRVmKc6T8TORsAAZtQbsGzNEAKyd1iVlWY=","ipv6_addr_in":"2604:980:e007:100::f001","shadowsocks_extra_addr_in":["155.2.219.14"],"features":{"quic":{"addr_in":["155.2.219.13","2604:980:e007:100::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"br-for-wg-001.blockerad.eu"}}},{"hostname":"br-for-wg-002","location":"br-for","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"98.98.12.182","include_in_country":true,"weight":100,"public_key":"qXISz0Kl4oC0sypcjD6hIxplv8zzZGIJPQZc4/EGz2k=","ipv6_addr_in":"2604:980:e007:100::f101","shadowsocks_extra_addr_in":["155.2.219.139"],"features":{"quic":{"addr_in":["155.2.219.138","2604:980:e007:100::f10a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"br-for-wg-002.blockerad.eu"}}},{"hostname":"br-sao-wg-201","location":"br-sao","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.198.66","include_in_country":true,"weight":100,"public_key":"8c9M6w1BQbgMVr/Zgrj4GwSdU6q3qfQfWs17kMLC9y4=","ipv6_addr_in":"2a02:6ea0:d00e:1::a01f","daita":true,"features":{"daita":{}}},{"hostname":"br-sao-wg-202","location":"br-sao","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.198.79","include_in_country":true,"weight":100,"public_key":"jWURoz8SLBUlRTQnAFTA/LDZUTpvlO0ghiVWH7MgaHQ=","ipv6_addr_in":"2a02:6ea0:d00e:2::a02f"},{"hostname":"br-sao-wg-302","location":"br-sao","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.139.178.63","include_in_country":true,"weight":100,"public_key":"Xv1QvURPbgywITL6MNVhbYtfZXTm0lR98SPaf3AXeCc=","ipv6_addr_in":"2a06:3040:10:610::f101","shadowsocks_extra_addr_in":["103.139.178.74","103.139.178.75"],"features":{"quic":{"addr_in":["103.139.178.73","2a06:3040:10:610::f10a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"br-sao-wg-302.blockerad.eu"}}},{"hostname":"br-sao-wg-303","location":"br-sao","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.139.178.123","include_in_country":true,"weight":100,"public_key":"oYrzNmnieX0iZS2nLxdM3mNcDjQZEWn5yaFCtX76qDk=","ipv6_addr_in":"2a06:3040:10:610::f201","shadowsocks_extra_addr_in":["103.139.178.134","103.139.178.135"],"features":{"quic":{"addr_in":["103.139.178.133","2a06:3040:10:610::f20a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"br-sao-wg-303.blockerad.eu"}}},{"hostname":"br-sao-wg-304","location":"br-sao","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.139.178.183","include_in_country":true,"weight":100,"public_key":"hmO6+lN2CrWMFdpiPtSZ3oPRmcsJlpIi00P+c5p6rQQ=","ipv6_addr_in":"2a06:3040:10:610::f301","shadowsocks_extra_addr_in":["103.139.178.194","103.139.178.195"],"features":{"quic":{"addr_in":["103.139.178.193","2a06:3040:10:610::f30a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"br-sao-wg-304.blockerad.eu"}}},{"hostname":"ca-mtr-wg-001","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.198.66","include_in_country":true,"weight":100,"public_key":"TUCaQc26/R6AGpkDUr8A8ytUs/e5+UVlIVujbuBwlzI=","ipv6_addr_in":"2a0d:5600:9:c::f001","features":{"lwo":{},"quic":{"addr_in":["146.70.198.126","2a0d:5600:9:c::c101"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-mtr-wg-001.blockerad.eu"}}},{"hostname":"ca-mtr-wg-002","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.198.130","include_in_country":true,"weight":100,"public_key":"7X6zOgtJfJAK8w8C3z+hekcS9Yf3qK3Bp4yx56lqxBQ=","ipv6_addr_in":"2a0d:5600:9:d::f001"},{"hostname":"ca-mtr-wg-003","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.198.194","include_in_country":true,"weight":100,"public_key":"57Zu2qPzRScZWsoC2NhXgz0FiC0HiKkbEa559sbxB3k=","ipv6_addr_in":"2a0d:5600:9:e::a02f"},{"hostname":"ca-mtr-wg-004","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"188.241.176.194","include_in_country":true,"weight":100,"public_key":"Cc5swfQ9f2tAgLduuIqC3bLbwDVoOFkkETghsE6/twA=","ipv6_addr_in":"2a0d:5600:9:16::f001"},{"hostname":"ca-mtr-wg-201","location":"ca-mtr","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"62.93.167.130","include_in_country":true,"weight":100,"public_key":"m1DF8sQgOBo+vfdl1//sCvu2TnsHKdRzfsiszbBZQzs=","ipv6_addr_in":"2a02:6ea0:a03:2::f001","daita":true,"shadowsocks_extra_addr_in":["62.93.167.132"],"features":{"daita":{}}},{"hostname":"ca-mtr-wg-202","location":"ca-mtr","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"62.93.167.160","include_in_country":true,"weight":100,"public_key":"NqU0AZRAYH1p8BDUbirqITPJX47WYJsyxO73RHcEjEQ=","ipv6_addr_in":"2a02:6ea0:a03::f001","shadowsocks_extra_addr_in":["62.93.167.162"]},{"hostname":"ca-tor-wg-001","location":"ca-tor","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.214.2","include_in_country":true,"weight":100,"public_key":"HjcUGVDXWdrRkaKNpc/8494RM5eICO6DPyrhCtTv9Ws=","ipv6_addr_in":"2a02:6ea0:de08:1::f001","daita":true,"features":{"daita":{}}},{"hostname":"ca-tor-wg-002","location":"ca-tor","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.214.15","include_in_country":true,"weight":100,"public_key":"iqZSgVlU9H67x/uYE5xsnzLCDXf7FL9iMfyKfl6WsV8=","ipv6_addr_in":"2a02:6ea0:de08:2::a29f"},{"hostname":"ca-tor-wg-201","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.84.2","include_in_country":true,"weight":100,"public_key":"94nJF3WyWKsZQOFhWWco8cjBOrSYADsMSTeivfbWQyw=","ipv6_addr_in":"2607:9000:600:31::f001","shadowsocks_extra_addr_in":["23.234.84.13"],"features":{"lwo":{},"quic":{"addr_in":["23.234.84.12","2607:9000:600:31::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-201.blockerad.eu"}}},{"hostname":"ca-tor-wg-202","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.84.127","include_in_country":true,"weight":100,"public_key":"vr/sAm+36c3N8jWfo14Sw6El0xJeSvu/soU/8JWSb3U=","ipv6_addr_in":"2607:9000:600:32::f001","shadowsocks_extra_addr_in":["23.234.84.138"],"features":{"quic":{"addr_in":["23.234.84.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-202.blockerad.eu"}}},{"hostname":"ca-tor-wg-203","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.85.2","include_in_country":true,"weight":100,"public_key":"kpXVJa66qgBFlwCmHx6siJT3R9afvtbjdDOTKkkbiUI=","ipv6_addr_in":"2607:9000:600:33::f001","shadowsocks_extra_addr_in":["23.234.85.13"],"features":{"quic":{"addr_in":["23.234.85.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-203.blockerad.eu"}}},{"hostname":"ca-tor-wg-204","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.85.127","include_in_country":true,"weight":100,"public_key":"APcoI2H2zdWIMdVYXslcJm4zzgePc4PESsDHgkm0UnQ=","ipv6_addr_in":"2607:9000:600:34::f001","shadowsocks_extra_addr_in":["23.234.85.138"],"features":{"quic":{"addr_in":["23.234.85.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-204.blockerad.eu"}}},{"hostname":"ca-tor-wg-205","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.86.2","include_in_country":true,"weight":100,"public_key":"iePZCguBaIxO/7gqQGDDYwl6YzMZj5910nK2Q9bDP14=","ipv6_addr_in":"2607:9000:600:35::f001","shadowsocks_extra_addr_in":["23.234.86.13"],"features":{"quic":{"addr_in":["23.234.86.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-205.blockerad.eu"}}},{"hostname":"ca-tor-wg-206","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.86.127","include_in_country":true,"weight":100,"public_key":"PGPyMyMPZ7Lue4pvFK7hlavToQ5FfBODmQBoiaUZ40I=","ipv6_addr_in":"2607:9000:600:36::f001","shadowsocks_extra_addr_in":["23.234.86.138"],"features":{"quic":{"addr_in":["23.234.86.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-206.blockerad.eu"}}},{"hostname":"ca-tor-wg-207","location":"ca-tor","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.87.2","include_in_country":true,"weight":100,"public_key":"8VmxIxjz5W44ubaBuIt5JEngh7fKCvdmdG9WBN2oqhw=","ipv6_addr_in":"2607:9000:600:37::f001","shadowsocks_extra_addr_in":["23.234.87.13"],"features":{"quic":{"addr_in":["23.234.87.12","2607:9000:600:37::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ca-tor-wg-207.blockerad.eu"}}},{"hostname":"ca-van-wg-201","location":"ca-van","active":true,"owned":false,"provider":"techfutures","stboot":true,"ipv4_addr_in":"104.193.135.196","include_in_country":true,"weight":100,"public_key":"hYbb2NQKB0g2RefngdHl3bfaLImUuzeVIv2i1VCVIlQ=","ipv6_addr_in":"2606:9580:103:e::f001"},{"hostname":"ca-van-wg-202","location":"ca-van","active":true,"owned":false,"provider":"techfutures","stboot":true,"ipv4_addr_in":"104.193.135.100","include_in_country":true,"weight":100,"public_key":"wGqcNxXH7A3bSptHZo7Dfmymy/Y30Ea/Zd47UkyEbzo=","ipv6_addr_in":"2606:9580:103:f::f001"},{"hostname":"ca-van-wg-301","location":"ca-van","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.22.81.194","include_in_country":true,"weight":100,"public_key":"BzYINbABQiSbRLDZIlmgsLgL88offQJCEH3JkcjRGUk=","ipv6_addr_in":"2a02:6ea0:5100:1::f001","daita":true,"features":{"daita":{}}},{"hostname":"ca-van-wg-302","location":"ca-van","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.22.81.207","include_in_country":true,"weight":100,"public_key":"EOOkxbmbdHmjb8F45s33yKrIzKWH6lGIgJf2kTOxwFw=","ipv6_addr_in":"2a02:6ea0:5100:2::f001"},{"hostname":"ca-yyc-wg-201","location":"ca-yyc","active":true,"owned":false,"provider":"techfutures","stboot":true,"ipv4_addr_in":"38.240.225.36","include_in_country":true,"weight":100,"public_key":"L4RcVwk0cJJp2u8O9+86sdyUpxfYnr+ME57Ex0RY1Wo=","ipv6_addr_in":"2606:9580:438:32::b01f"},{"hostname":"ca-yyc-wg-202","location":"ca-yyc","active":true,"owned":false,"provider":"techfutures","stboot":true,"ipv4_addr_in":"38.240.225.68","include_in_country":true,"weight":100,"public_key":"u9J/fzrSqM2aEFjTs91KEKgBsaQ/I/4XkIP1Z/zYkXA=","ipv6_addr_in":"2606:9580:438:64::b02f"},{"hostname":"ch-zrh-wg-001","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.66","include_in_country":true,"weight":1,"public_key":"/iivwlyqWqxQ0BVWmJRhcXIFdJeo0WbHQ/hZwuXaN3g=","ipv6_addr_in":"2a03:1b20:a:f011::f001","features":{"quic":{"addr_in":["193.32.127.99","2a03:1b20:a:f011::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"ch-zrh-wg-001.blockerad.eu"}}},{"hostname":"ch-zrh-wg-002","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.67","include_in_country":true,"weight":1,"public_key":"qcvI02LwBnTb7aFrOyZSWvg4kb7zNW9/+rS6alnWyFE=","ipv6_addr_in":"2a03:1b20:a:f011::f101"},{"hostname":"ch-zrh-wg-003","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.68","include_in_country":true,"weight":1,"public_key":"5Ms10UxGjCSzwImTrvEjcygsWY8AfMIdYyRvgFuTqH8=","ipv6_addr_in":"2a03:1b20:a:f011::f201"},{"hostname":"ch-zrh-wg-004","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.69","include_in_country":true,"weight":1,"public_key":"C3jAgPirUZG6sNYe4VuAgDEYunENUyG34X42y+SBngQ=","ipv6_addr_in":"2a03:1b20:a:f011::f301","daita":true,"features":{"daita":{}}},{"hostname":"ch-zrh-wg-005","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.70","include_in_country":true,"weight":1,"public_key":"dV/aHhwG0fmp0XuvSvrdWjCtdyhPDDFiE/nuv/1xnRM=","ipv6_addr_in":"2a03:1b20:a:f011::f401","daita":true,"features":{"daita":{}}},{"hostname":"ch-zrh-wg-006","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.84","include_in_country":true,"weight":1,"public_key":"wDjbvO94t0UI1RlimpEFFv7kJ6DngthvuRX6uBN0wAA=","ipv6_addr_in":"2a03:1b20:a:f011::f601","daita":true,"features":{"daita":{}}},{"hostname":"ch-zrh-wg-201","location":"ch-zrh","active":true,"owned":false,"provider":"PrivateLayer","stboot":true,"ipv4_addr_in":"179.43.189.66","include_in_country":true,"weight":100,"public_key":"66NPINP4+1AlojLP0J6O9GxdloiegNnGMV4Yit9Kzg0=","ipv6_addr_in":"2a02:29b8:dc01:1832::a1f"},{"hostname":"ch-zrh-wg-202","location":"ch-zrh","active":true,"owned":false,"provider":"PrivateLayer","stboot":true,"ipv4_addr_in":"46.19.136.226","include_in_country":true,"weight":100,"public_key":"gSLSfY2zNFRczxHndeda258z+ayMvd7DqTlKYlKWJUo=","ipv6_addr_in":"2a02:29b8:dc01:1831::f002"},{"hostname":"ch-zrh-wg-401","location":"ch-zrh","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.6.194","include_in_country":true,"weight":80,"public_key":"45ud3I5O6GmPXTrMJiqkiPMI/ubucDqzGaiq3CHJXk8=","ipv6_addr_in":"2a02:6ea0:d406:1::a18f","daita":true,"features":{"daita":{}}},{"hostname":"ch-zrh-wg-402","location":"ch-zrh","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.6.207","include_in_country":true,"weight":80,"public_key":"7VCMEE+Oljm/qKfQJSUCOYPtRSwdOnuPyqo5Vob+GRY=","ipv6_addr_in":"2a02:6ea0:d406:2::a19f"},{"hostname":"ch-zrh-wg-403","location":"ch-zrh","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.6.220","include_in_country":true,"weight":80,"public_key":"Jmhds6oPu6/j94hjllJCIaKLDyWu6V+ZNRrVVFhWJkI=","ipv6_addr_in":"2a02:6ea0:d406:3::a20f"},{"hostname":"ch-zrh-wg-404","location":"ch-zrh","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.6.233","include_in_country":true,"weight":80,"public_key":"zfNQqDyPmSUY8+20wxACe/wpk4Q5jpZm5iBqjXj2hk8=","ipv6_addr_in":"2a02:6ea0:d406:4::a21f"},{"hostname":"ch-zrh-wg-501","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.134.98","include_in_country":true,"weight":1,"public_key":"HQzvIK88XSsRujBlwoYvvZ7CMKwiYuOqLXyuckkTPHg=","ipv6_addr_in":"2001:ac8:28:a7::a36f"},{"hostname":"ch-zrh-wg-502","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.126.162","include_in_country":true,"weight":100,"public_key":"TOA/MQWS6TzJVEa//GPyaET5d52VpHO2isS4786GGwU=","ipv6_addr_in":"2001:ac8:28:a1::f001"},{"hostname":"ch-zrh-wg-503","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.126.194","include_in_country":true,"weight":100,"public_key":"ApOUMLFcpTpj/sDAMub0SvASFdsSWtsy+vvw/nWvEmY=","ipv6_addr_in":"2001:ac8:28:a2::f001"},{"hostname":"ch-zrh-wg-504","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.126.226","include_in_country":true,"weight":100,"public_key":"I5XiRYHPmxnmGtPJ90Yio6QXL441C/+kYV6UH6wU+jk=","ipv6_addr_in":"2001:ac8:28:a3::f001"},{"hostname":"ch-zrh-wg-505","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.134.2","include_in_country":true,"weight":100,"public_key":"dc16Gcid7jLcHRD7uHma1myX3vWhEy/bZIBtqZw0B2I=","ipv6_addr_in":"2001:ac8:28:a4::a33f"},{"hostname":"ch-zrh-wg-506","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.134.34","include_in_country":true,"weight":100,"public_key":"7xVJLzW0nfmACr1VMc+/SiSMFh0j0EI3DrU/8Fnj1zM=","ipv6_addr_in":"2001:ac8:28:a5::a34f"},{"hostname":"ch-zrh-wg-507","location":"ch-zrh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.134.66","include_in_country":true,"weight":100,"public_key":"RNTpvmWTyjNf8w9qdP+5XlFnyAk5TrVvT+CRa8a0zys=","ipv6_addr_in":"2001:ac8:28:a6::a35f"},{"hostname":"cl-scl-wg-001","location":"cl-scl","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.104.2","include_in_country":true,"weight":100,"public_key":"03qeK7CSn6wcMzfqilmVt6Tf81VZIPWnSG04euSkyxM=","ipv6_addr_in":"2a02:6ea0:fc02:2::f001"},{"hostname":"cl-scl-wg-002","location":"cl-scl","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.104.15","include_in_country":true,"weight":100,"public_key":"rn9O+cXj0WQgZAkGCoYvvWgzaB5GcOaVfke3WKsp1Ro=","ipv6_addr_in":"2a02:6ea0:fc02:3::f101"},{"hostname":"co-bog-wg-001","location":"co-bog","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"154.47.16.34","include_in_country":true,"weight":100,"public_key":"iaMa84nCHK+v4TnQH4h2rxkqwwxemORXM12VbJDRZSU=","ipv6_addr_in":"2a02:6ea0:f101:1::f001","daita":true,"features":{"daita":{}}},{"hostname":"co-bog-wg-002","location":"co-bog","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"154.47.16.47","include_in_country":true,"weight":100,"public_key":"IZDwbG9C/NrOOGVUrn+fDaPr8ZwD/yhvST7XWGk1ln8=","ipv6_addr_in":"2a02:6ea0:f101:2::f001"},{"hostname":"cy-nic-wg-001","location":"cy-nic","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"195.47.194.131","include_in_country":true,"weight":100,"public_key":"Ae9YcQjcQT+W8MU0EhKXx6KPWo6ticS1NI91e+Zy5GA=","ipv6_addr_in":"2a06:3040:f:601::f001","shadowsocks_extra_addr_in":["195.47.194.133"]},{"hostname":"cy-nic-wg-002","location":"cy-nic","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"195.47.194.161","include_in_country":true,"weight":100,"public_key":"LOd1SY9YCHGiJUVT+XdYRdORu6ZMw4CqOKQBW2ElLg8=","ipv6_addr_in":"2a06:3040:f:601::f101","shadowsocks_extra_addr_in":["195.47.194.163"]},{"hostname":"cz-prg-wg-101","location":"cz-prg","active":false,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.129.98","include_in_country":true,"weight":100,"public_key":"wLBxTaISMJ++vUht4hlAOUog9fhZxDql16TaYWaboDc=","ipv6_addr_in":"2001:ac8:33:c::a01f"},{"hostname":"cz-prg-wg-102","location":"cz-prg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.129.130","include_in_country":true,"weight":100,"public_key":"cRCJ0vULwKRbTfzuo9W+fIt0fJGQE7DLvojIiURIpiI=","ipv6_addr_in":"2001:ac8:33:d::a02f","features":{"lwo":{},"quic":{"addr_in":["146.70.129.158"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"cz-prg-wg-102.blockerad.eu"}}},{"hostname":"cz-prg-wg-201","location":"cz-prg","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.209.162","include_in_country":true,"weight":100,"public_key":"5FZW+fNA2iVBSY99HFl+KjGc9AFVNE+UFAedLNhu8lc=","ipv6_addr_in":"2a02:6ea0:c201:1::f001"},{"hostname":"cz-prg-wg-202","location":"cz-prg","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.209.175","include_in_country":true,"weight":100,"public_key":"ReGrGPKDHri64D7qeXmgcLzjsTJ0B/yM7eekFz1P/34=","ipv6_addr_in":"2a02:6ea0:c201:1::f101"},{"hostname":"de-ber-wg-001","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.66","include_in_country":true,"weight":100,"public_key":"0qSP0VxoIhEhRK+fAHVvmfRdjPs2DmmpOCNLFP/7cGw=","ipv6_addr_in":"2a03:1b20:b:f011::a01f","shadowsocks_extra_addr_in":["193.32.248.86"],"features":{"quic":{"addr_in":["193.32.248.87"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-001.blockerad.eu"}}},{"hostname":"de-ber-wg-002","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.67","include_in_country":true,"weight":100,"public_key":"8ov1Ws0ut3ixWDh9Chp7/WLVn9qC6/WVHtcBcuWBlgo=","ipv6_addr_in":"2a03:1b20:b:f011::a02f","shadowsocks_extra_addr_in":["193.32.248.88"],"features":{"quic":{"addr_in":["193.32.248.89"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-002.blockerad.eu"}}},{"hostname":"de-ber-wg-003","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.68","include_in_country":true,"weight":100,"public_key":"USrMatdHiCL5AKdVMpHuYgWuMiK/GHPwRB3Xx00FhU0=","ipv6_addr_in":"2a03:1b20:b:f011::a03f","shadowsocks_extra_addr_in":["193.32.248.90"],"features":{"quic":{"addr_in":["193.32.248.91"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-003.blockerad.eu"}}},{"hostname":"de-ber-wg-004","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.69","include_in_country":true,"weight":100,"public_key":"6PchzRRxzeeHdNLyn3Nz0gmN7pUyjoZMpKmKzJRL4GM=","ipv6_addr_in":"2a03:1b20:b:f011::a04f","shadowsocks_extra_addr_in":["193.32.248.92"],"features":{"quic":{"addr_in":["193.32.248.93"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-004.blockerad.eu"}}},{"hostname":"de-ber-wg-005","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.70","include_in_country":true,"weight":100,"public_key":"I4Y7e8LrtBC/7DLpUgRd5k+IZk+whOFVAZgbSivoiBI=","ipv6_addr_in":"2a03:1b20:b:f011::a05f","shadowsocks_extra_addr_in":["193.32.248.94"],"features":{"quic":{"addr_in":["193.32.248.95"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-005.blockerad.eu"}}},{"hostname":"de-ber-wg-006","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.71","include_in_country":true,"weight":100,"public_key":"eprzkkkSbXCANngQDo305DIAvkKAnZaN71IpTNaOoTk=","ipv6_addr_in":"2a03:1b20:b:f011::a06f","shadowsocks_extra_addr_in":["193.32.248.96"],"features":{"quic":{"addr_in":["193.32.248.97"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-006.blockerad.eu"}}},{"hostname":"de-ber-wg-007","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.75","include_in_country":true,"weight":100,"public_key":"/ejdxiEsmYbeXXCN6UzvzJ0U/mLuB6baIfQRYKYHWzU=","ipv6_addr_in":"2a03:1b20:b:f011::f701","shadowsocks_extra_addr_in":["193.32.248.98"],"features":{"quic":{"addr_in":["193.32.248.99"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-007.blockerad.eu"}}},{"hostname":"de-ber-wg-008","location":"de-ber","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.248.74","include_in_country":true,"weight":100,"public_key":"qwXs9gwhwqWgRtLjPiZ+zMphZJA3OStsn/aXcCAd5m0=","ipv6_addr_in":"2a03:1b20:b:f011::f801","shadowsocks_extra_addr_in":["193.32.248.233"],"features":{"quic":{"addr_in":["193.32.248.232","2a03:1b20:b:f011::f80a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-ber-wg-008.blockerad.eu"}}},{"hostname":"de-dus-wg-001","location":"de-dus","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.254.75.3","include_in_country":true,"weight":150,"public_key":"ku1NYeOAGbY65YL/JKZhrqVzDJKXQiVj9USXbfkOBA0=","ipv6_addr_in":"2a03:d9c0:3000::a20f"},{"hostname":"de-dus-wg-002","location":"de-dus","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.254.75.4","include_in_country":true,"weight":150,"public_key":"TPAIPTgu9jIitgX1Bz5xMCZJ9pRRZTdtZEOIxArO0Hc=","ipv6_addr_in":"2a03:d9c0:3000::a21f"},{"hostname":"de-dus-wg-003","location":"de-dus","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.254.75.5","include_in_country":true,"weight":150,"public_key":"XgSe9UwEV4JJNPPzFFOVYS6scMTL4DeNlwqBl32lDw0=","ipv6_addr_in":"2a03:d9c0:3000::a22f"},{"hostname":"de-fra-wg-001","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.73","include_in_country":true,"weight":100,"public_key":"HQHCrq4J6bSpdW1fI5hR/bvcrYa6HgGgwaa5ZY749ik=","ipv6_addr_in":"2a03:1b20:6:f011::f001","features":{"quic":{"addr_in":["185.213.155.99","2a03:1b20:6:f011::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-fra-wg-001.blockerad.eu"}}},{"hostname":"de-fra-wg-002","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.74","include_in_country":true,"weight":100,"public_key":"s1c/NsfnqnwQSxao70DY4Co69AFT9e0h88IFuMD5mjs=","ipv6_addr_in":"2a03:1b20:6:f011::f101","shadowsocks_extra_addr_in":["151.241.163.30","151.241.163.31","151.241.163.32","151.241.163.33","151.241.163.34","151.241.163.35","151.241.163.36","151.241.163.37","151.241.163.38","151.241.163.39","151.241.163.40","151.241.163.41","151.241.163.42","151.241.163.43","151.241.163.44","151.241.163.45","151.241.163.46","151.241.163.47","151.241.163.48","151.241.163.49"],"features":{"lwo":{},"quic":{"addr_in":["151.241.163.20","151.241.163.21","151.241.163.22","151.241.163.23","151.241.163.24","151.241.163.25","151.241.163.26","151.241.163.27","151.241.163.28","151.241.163.29","151.241.163.50","151.241.163.51","151.241.163.52","151.241.163.53","151.241.163.54","151.241.163.55","151.241.163.56","151.241.163.57","151.241.163.58","151.241.163.59"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-fra-wg-002.blockerad.eu"}}},{"hostname":"de-fra-wg-003","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.196.73","include_in_country":true,"weight":100,"public_key":"vVQKs2TeTbdAvl3sH16UWLSESncXAj0oBaNuFIUkLVk=","ipv6_addr_in":"2a03:1b20:6:f011::f201","shadowsocks_extra_addr_in":["151.241.163.100","151.241.163.101","151.241.163.102","151.241.163.103","151.241.163.104","151.241.163.105","151.241.163.106","151.241.163.107","151.241.163.108","151.241.163.109","151.241.163.110","151.241.163.111","151.241.163.112","151.241.163.113","151.241.163.114","151.241.163.115","151.241.163.116","151.241.163.117","151.241.163.118","151.241.163.119","2a03:1b20:6:f011:3:b:0:1","2a03:1b20:6:f011:3:b:0:2","2a03:1b20:6:f011:3:b:0:3","2a03:1b20:6:f011:3:b:0:4","2a03:1b20:6:f011:3:b:0:5","2a03:1b20:6:f011:3:b:0:6","2a03:1b20:6:f011:3:b:0:7","2a03:1b20:6:f011:3:b:0:8","2a03:1b20:6:f011:3:b:0:9","2a03:1b20:6:f011:3:b:0:a","2a03:1b20:6:f011:3:b:0:b","2a03:1b20:6:f011:3:b:0:c","2a03:1b20:6:f011:3:b:0:d","2a03:1b20:6:f011:3:b:0:e","2a03:1b20:6:f011:3:b:0:f"],"features":{"quic":{"addr_in":["151.241.163.90","151.241.163.91","151.241.163.92","151.241.163.93","151.241.163.94","151.241.163.95","151.241.163.96","151.241.163.97","151.241.163.98","151.241.163.99","151.241.163.80","151.241.163.81","151.241.163.82","151.241.163.83","151.241.163.84","151.241.163.85","151.241.163.86","151.241.163.87","151.241.163.88","151.241.163.89","2a03:1b20:6:f011:3:c:0:1","2a03:1b20:6:f011:3:c:0:2","2a03:1b20:6:f011:3:c:0:3","2a03:1b20:6:f011:3:c:0:4","2a03:1b20:6:f011:3:c:0:5","2a03:1b20:6:f011:3:c:0:6","2a03:1b20:6:f011:3:c:0:7","2a03:1b20:6:f011:3:c:0:8","2a03:1b20:6:f011:3:c:0:9","2a03:1b20:6:f011:3:c:0:a","2a03:1b20:6:f011:3:c:0:b","2a03:1b20:6:f011:3:c:0:c","2a03:1b20:6:f011:3:c:0:d","2a03:1b20:6:f011:3:c:0:e","2a03:1b20:6:f011:3:c:0:f"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-fra-wg-003.blockerad.eu"}}},{"hostname":"de-fra-wg-004","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.196.74","include_in_country":true,"weight":1,"public_key":"tzYLWgBdwrbbBCXYHRSoYIho4dHtrm+8bdONU1I8xzc=","ipv6_addr_in":"2a03:1b20:6:f011::f301","daita":true,"shadowsocks_extra_addr_in":["151.241.163.160","151.241.163.161","151.241.163.162","151.241.163.163","151.241.163.164","151.241.163.165","151.241.163.166","151.241.163.167","151.241.163.168","151.241.163.169","151.241.163.170","151.241.163.171","151.241.163.172","151.241.163.173","151.241.163.174","151.241.163.175","151.241.163.176","151.241.163.177","151.241.163.178","151.241.163.179"],"features":{"daita":{},"lwo":{},"quic":{"addr_in":["151.241.163.140","151.241.163.141","151.241.163.142","151.241.163.143","151.241.163.144","151.241.163.145","151.241.163.146","151.241.163.147","151.241.163.148","151.241.163.149","151.241.163.150","151.241.163.151","151.241.163.152","151.241.163.153","151.241.163.154","151.241.163.155","151.241.163.156","151.241.163.157","151.241.163.158","151.241.163.159"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-fra-wg-004.blockerad.eu"}}},{"hostname":"de-fra-wg-005","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.196.75","include_in_country":true,"weight":100,"public_key":"tpobOO6t18CzHjOg0S3RlZJMxd2tz4+BnRYS7NrjTnM=","ipv6_addr_in":"2a03:1b20:6:f011::f401","shadowsocks_extra_addr_in":["151.241.163.220","151.241.163.221","151.241.163.222","151.241.163.223","151.241.163.224","151.241.163.225","151.241.163.226","151.241.163.227","151.241.163.228","151.241.163.229","151.241.163.230","151.241.163.231","151.241.163.232","151.241.163.233","151.241.163.234","151.241.163.235","151.241.163.236","151.241.163.237","151.241.163.238","151.241.163.239","151.241.163.240","151.241.163.241","151.241.163.242","151.241.163.243","151.241.163.244","151.241.163.245","151.241.163.246","151.241.163.247","151.241.163.248","151.241.163.249"],"features":{"lwo":{},"quic":{"addr_in":["151.241.163.200","151.241.163.201","151.241.163.202","151.241.163.203","151.241.163.204","151.241.163.205","151.241.163.206","151.241.163.207","151.241.163.208","151.241.163.209","151.241.163.210","151.241.163.210","151.241.163.211","151.241.163.212","151.241.163.213","151.241.163.214","151.241.163.215","151.241.163.216","151.241.163.217","151.241.163.218","151.241.163.219"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-fra-wg-005.blockerad.eu"}}},{"hostname":"de-fra-wg-006","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.196.76","include_in_country":true,"weight":100,"public_key":"nAF0wrLG2+avwQfqxnXhBGPUBCvc3QCqWKH4nK5PfEU=","ipv6_addr_in":"2a03:1b20:6:f011::f501"},{"hostname":"de-fra-wg-007","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.196.77","include_in_country":true,"weight":100,"public_key":"mTmrSuXmTnIC9l2Ur3/QgodGrVEhhIE3pRwOHZpiYys=","ipv6_addr_in":"2a03:1b20:6:f011::f601"},{"hostname":"de-fra-wg-008","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.196.78","include_in_country":true,"weight":100,"public_key":"+DuVLLPwGNlfZFoI24PRPdaTrO4i+WPDlYaOVcavHDo=","ipv6_addr_in":"2a03:1b20:6:f011::f701"},{"hostname":"de-fra-wg-009","location":"de-fra","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.72","include_in_country":true,"weight":100,"public_key":"flq7zR8W5FxouHBuZoTRHY0A0qFEMQZF5uAgV4+sHVw=","ipv6_addr_in":"2a03:1b20:6:f011::f901"},{"hostname":"de-fra-wg-101","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.162","include_in_country":true,"weight":1,"public_key":"Voioje9Gfb7aTiK2/H6VyHFK1AFap1glIX0Z1EX2mRQ=","ipv6_addr_in":"2001:ac8:20:274::a99f","features":{"lwo":{},"quic":{"addr_in":["146.70.117.190"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"de-fra-wg-101.blockerad.eu"}}},{"hostname":"de-fra-wg-102","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.194","include_in_country":true,"weight":100,"public_key":"ydXFN45/kROELJrF6id+uIrnS5DvTKSCkZDjfL9De2Q=","ipv6_addr_in":"2001:ac8:20:275::f001"},{"hostname":"de-fra-wg-103","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.226","include_in_country":true,"weight":100,"public_key":"KkShcqgwbkX2A9n1hhST6qu+m3ldxdJ2Lx8Eiw6mdXw=","ipv6_addr_in":"2001:ac8:20:276::f001"},{"hostname":"de-fra-wg-104","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.107.194","include_in_country":true,"weight":100,"public_key":"uKTC5oP/zfn6SSjayiXDDR9L82X0tGYJd5LVn5kzyCc=","ipv6_addr_in":"2001:ac8:20:277::f001"},{"hostname":"de-fra-wg-105","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.2","include_in_country":true,"weight":100,"public_key":"Sttn2cr14dvIcCrE8qdlRGHXriqvTyvQWC7dzujH/iM=","ipv6_addr_in":"2001:ac8:20:269::f001"},{"hostname":"de-fra-wg-106","location":"de-fra","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.117.34","include_in_country":true,"weight":100,"public_key":"9ldhvN7r4xGZkGehbsNfYb5tpyTJ5KBb5B3TbxCwklw=","ipv6_addr_in":"2001:ac8:20:270::f001"},{"hostname":"de-fra-wg-301","location":"de-fra","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.36.25.3","include_in_country":true,"weight":100,"public_key":"dNKRyh2MkJGZdg9jyUJtf9w5GHjX3+/fYatg+xi9TUM=","ipv6_addr_in":"2a07:fe00:1::a23f"},{"hostname":"de-fra-wg-302","location":"de-fra","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.36.25.18","include_in_country":true,"weight":100,"public_key":"A3DbIgPycEJhJ1fQ4zzcajLOKTZsJMeawjdPQiWav20=","ipv6_addr_in":"2a07:fe00:1::a24f"},{"hostname":"de-fra-wg-303","location":"de-fra","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.36.25.33","include_in_country":true,"weight":100,"public_key":"2P+9SjwVCEnMDnBiYfZtQLq9p2S2TFhCM0xJBoevYk4=","ipv6_addr_in":"2a07:fe00:1::a25f"},{"hostname":"de-fra-wg-304","location":"de-fra","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.36.25.48","include_in_country":true,"weight":100,"public_key":"VgNcwWy8MRhfEZY+XSisDM1ykX+uXlHQScOLqqGMLkc=","ipv6_addr_in":"2a07:fe00:1::a26f"},{"hostname":"de-fra-wg-401","location":"de-fra","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.201.2","include_in_country":true,"weight":100,"public_key":"AbM8fnQWmmX6Nv0Tz68LigPbGkamJgNjxgzPfENOdXU=","ipv6_addr_in":"2a02:6ea0:c762:1::a35f"},{"hostname":"de-fra-wg-402","location":"de-fra","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.201.15","include_in_country":true,"weight":100,"public_key":"6/PBbPtoeWpJA+HZc9Iqg/PPQWD7mGVvZdwQlr1vtRk=","ipv6_addr_in":"2a02:6ea0:c762:2::a36f"},{"hostname":"de-fra-wg-403","location":"de-fra","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.201.28","include_in_country":true,"weight":100,"public_key":"HWzSNMbQOQafkVp68B7aLRirhNJ6x5Wjw8/y7oUuHW0=","ipv6_addr_in":"2a02:6ea0:c762:3::a37f"},{"hostname":"dk-cph-wg-001","location":"dk-cph","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.129.56.67","include_in_country":true,"weight":100,"public_key":"egl+0TkpFU39F5O6r6+hIBMPQLOa8/t5CymOZV6CC3Y=","ipv6_addr_in":"2a03:1b20:8:f011::f001","features":{"quic":{"addr_in":["141.98.254.99","2a03:1b20:8:f011::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"dk-cph-wg-001.blockerad.eu"}}},{"hostname":"dk-cph-wg-002","location":"dk-cph","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.129.56.68","include_in_country":true,"weight":100,"public_key":"R5LUBgM/1UjeAR4lt+L/yA30Gee6/VqVZ9eAB3ZTajs=","ipv6_addr_in":"2a03:1b20:8:f011::f101"},{"hostname":"dk-cph-wg-401","location":"dk-cph","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.197.194","include_in_country":true,"weight":100,"public_key":"Jjml2TSqKlgzW6UzPiJszaun743QYpyl5jQk8UOQYg0=","ipv6_addr_in":"2001:ac8:37:97::f001"},{"hostname":"dk-cph-wg-402","location":"dk-cph","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.197.130","include_in_country":true,"weight":100,"public_key":"ML0NcFPqy+x+ZJg7y9vfh77hXAOtgueIqp1j+CJVrXM=","ipv6_addr_in":"2001:ac8:37:96::f001"},{"hostname":"ee-tll-wg-001","location":"ee-tll","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.127.167.67","include_in_country":true,"weight":100,"public_key":"bdq37KtfoG1Tm7yQcfitdRyGeZOn/c7PwLN+LgG/6nA=","ipv6_addr_in":"2a07:d880:2::a01f"},{"hostname":"ee-tll-wg-002","location":"ee-tll","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.127.167.87","include_in_country":true,"weight":100,"public_key":"vqGmmcERr/PAKDzy6Dxax8g4150rC93kmKYabZuAzws=","ipv6_addr_in":"2a07:d880:2::a02f"},{"hostname":"ee-tll-wg-003","location":"ee-tll","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.127.167.107","include_in_country":true,"weight":100,"public_key":"+8dUgpD7YA4wMPnRQkO7EI7AeYd30QPMKh/hOaaGIXY=","ipv6_addr_in":"2a07:d880:2::a03f"},{"hostname":"es-bcn-wg-001","location":"es-bcn","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.188.61.195","include_in_country":true,"weight":100,"public_key":"asbfbY0oP07dBdmVNDSuO3o5rbkGnR56PkXTGXO7YFg=","ipv6_addr_in":"2a06:3040:2:210::f001"},{"hostname":"es-bcn-wg-002","location":"es-bcn","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.188.61.225","include_in_country":true,"weight":100,"public_key":"SoTWu5Cf7JSfaPVftMrTVzeyICGc7oc+ODl6GfqzUHA=","ipv6_addr_in":"2a06:3040:2:210::f101"},{"hostname":"es-bcn-wg-101","location":"es-bcn","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"185.253.99.30","include_in_country":true,"weight":100,"public_key":"TQDQ/SUW7pme5aRWFT4ugr9YAABS/uwJNZgqYKTM+iU=","ipv6_addr_in":"2001:ac8:17:20::f001","features":{"quic":{"addr_in":["146.70.22.130","2001:ac8:17:20::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"es-bcn-wg-101.blockerad.eu"}}},{"hostname":"es-bcn-wg-102","location":"es-bcn","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"185.253.99.98","include_in_country":true,"weight":100,"public_key":"GqDjspXPQWM3V5nh1M9IhnxgiIwctvxuFyj73oYTRwo=","ipv6_addr_in":"2001:ac8:17:20::f101","features":{"quic":{"addr_in":["146.70.22.194","2001:ac8:17:20::f10a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"es-bcn-wg-102.blockerad.eu"}}},{"hostname":"es-mad-wg-101","location":"es-mad","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.213.194","include_in_country":true,"weight":100,"public_key":"oPpPeyiQhUYqtOxwR387dmFfII8OK5LX2RPyns1rx2U=","ipv6_addr_in":"2a02:6ea0:c318:1::a06f"},{"hostname":"es-mad-wg-102","location":"es-mad","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.213.207","include_in_country":true,"weight":100,"public_key":"1Wo/cQeVHX2q9k95nxN+48lgkGLsPQ+uesRb/9XdY1Y=","ipv6_addr_in":"2a02:6ea0:c318:2::a07f"},{"hostname":"es-mad-wg-201","location":"es-mad","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.128.194","include_in_country":true,"weight":100,"public_key":"LyO4Xs1eV8JwFr63a1FRnKboQn2Tu/oeMzHhbr7Y6GU=","ipv6_addr_in":"2001:ac8:23:85::a01f"},{"hostname":"es-mad-wg-202","location":"es-mad","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.128.226","include_in_country":true,"weight":100,"public_key":"iehXacO91FbBqni2IFxedEYPlW2Wvvt9GtRPPPMo9zc=","ipv6_addr_in":"2001:ac8:23:86::a02f"},{"hostname":"es-vlc-wg-001","location":"es-vlc","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"193.19.207.195","include_in_country":true,"weight":100,"public_key":"aEObX8ThiHcN/Y40UqY8dXaGMJsVQUWhrEphbpuQRkw=","ipv6_addr_in":"2a06:3040:3:210::f001"},{"hostname":"es-vlc-wg-002","location":"es-vlc","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"193.19.207.225","include_in_country":true,"weight":100,"public_key":"JEDqyG7iGjy/rYsE/9H7y0Sz8Sl+KWYYUvkPG7NnCjk=","ipv6_addr_in":"2a06:3040:3:210::f101"},{"hostname":"fi-hel-wg-001","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.203","include_in_country":true,"weight":100,"public_key":"veLqpZazR9j/Ol2G8TfrO32yEhc1i543MCN8rpy1FBA=","ipv6_addr_in":"2a0c:f040:0:2790::a01f","features":{"quic":{"addr_in":["185.77.218.49","2a0c:f040:0:2790::d099"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"fi-hel-wg-001.blockerad.eu"}}},{"hostname":"fi-hel-wg-002","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.211","include_in_country":true,"weight":25,"public_key":"8BbP3GS01dGkN5ENk1Rgedxfd80friyVOABrdMgD3EY=","ipv6_addr_in":"2a0c:f040:0:2790::a02f","shadowsocks_extra_addr_in":["2a0c:f040:0:2790::b201","2a0c:f040:0:2790::b202","2a0c:f040:0:2790::b203","2a0c:f040:0:2790::b204","2a0c:f040:0:2790::b205","2a0c:f040:0:2790::b206","2a0c:f040:0:2790::b207","2a0c:f040:0:2790::b208","2a0c:f040:0:2790::b209","2a0c:f040:0:2790::b20a","2a0c:f040:0:2790::b20b","2a0c:f040:0:2790::b20c","2a0c:f040:0:2790::b20d","2a0c:f040:0:2790::b20e","2a0c:f040:0:2790::b20f"],"features":{"quic":{"addr_in":["185.77.218.84","2a0c:f040:0:2790::c201","2a0c:f040:0:2790::c202","2a0c:f040:0:2790::c203","2a0c:f040:0:2790::c204","2a0c:f040:0:2790::c205","2a0c:f040:0:2790::c206","2a0c:f040:0:2790::c207","2a0c:f040:0:2790::c208","2a0c:f040:0:2790::c209","2a0c:f040:0:2790::c20a","2a0c:f040:0:2790::c20b","2a0c:f040:0:2790::c20c","2a0c:f040:0:2790::c20d","2a0c:f040:0:2790::c20e","2a0c:f040:0:2790::c20f"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"fi-hel-wg-002.blockerad.eu"}}},{"hostname":"fi-hel-wg-003","location":"fi-hel","active":true,"owned":true,"provider":"Creanova","stboot":true,"ipv4_addr_in":"185.204.1.219","include_in_country":true,"weight":25,"public_key":"FKodo9V6BehkNphL+neI0g4/G/cjbZyYhoptSWf3Si4=","ipv6_addr_in":"2a0c:f040:0:2790::a03f","shadowsocks_extra_addr_in":["2a0c:f040:0:2790::b31","2a0c:f040:0:2790::b32","2a0c:f040:0:2790::b33","2a0c:f040:0:2790::b34","2a0c:f040:0:2790::b35","2a0c:f040:0:2790::b36","2a0c:f040:0:2790::b37","2a0c:f040:0:2790::b38","2a0c:f040:0:2790::b39"],"features":{"quic":{"addr_in":["185.77.218.125","2a0c:f040:0:2790::c31","2a0c:f040:0:2790::c32","2a0c:f040:0:2790::c33","2a0c:f040:0:2790::c34","2a0c:f040:0:2790::c35","2a0c:f040:0:2790::c36","2a0c:f040:0:2790::c37","2a0c:f040:0:2790::c38","2a0c:f040:0:2790::c39","2a0c:f040:0:2790::c398"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"fi-hel-wg-003.blockerad.eu"}}},{"hostname":"fi-hel-wg-101","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.137","include_in_country":true,"weight":100,"public_key":"2S3G7Sm9DVG6+uJtlDu4N6ed5V97sTbA5dCSkUelWyk=","ipv6_addr_in":"2a02:ed04:3581:1::f001","features":{"lwo":{},"quic":{"addr_in":["193.138.7.156"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"fi-hel-wg-101.blockerad.eu"}}},{"hostname":"fi-hel-wg-102","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.157","include_in_country":true,"weight":100,"public_key":"xeHVhXxyyFqUEE+nsu5Tzd/t9en+++4fVFcSFngpcAU=","ipv6_addr_in":"2a02:ed04:3581:2::f001"},{"hostname":"fi-hel-wg-103","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.177","include_in_country":true,"weight":100,"public_key":"Mlvu14bSD6jb7ajH/CiJ/IO8W+spB8H6VmdGkFGOcUQ=","ipv6_addr_in":"2a02:ed04:3581:3::f001"},{"hostname":"fi-hel-wg-104","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.197","include_in_country":true,"weight":100,"public_key":"keRQGHUbYP2qgDTbYqOsI9byfNb0LOpTZ/KdC67cJiA=","ipv6_addr_in":"2a02:ed04:3581:4::f001"},{"hostname":"fr-bod-wg-001","location":"fr-bod","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"45.134.79.67","include_in_country":true,"weight":100,"public_key":"y6dcYS7MPeApbLoWLahjku5w5cufnNkwHzj1iwDPpS0=","ipv6_addr_in":"2a06:3040:4:610::f001"},{"hostname":"fr-bod-wg-002","location":"fr-bod","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"45.134.79.97","include_in_country":true,"weight":100,"public_key":"ZBOJ2w5DqG35T1zjV/F1UgrXkDhNxObnwdm2FUwyu2o=","ipv6_addr_in":"2a06:3040:4:610::f101"},{"hostname":"fr-mrs-wg-001","location":"fr-mrs","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.15.162","include_in_country":true,"weight":300,"public_key":"MOk2OTDEaFFN4vsCAgf+qQi6IlY99nCeDEzpXyo65wg=","ipv6_addr_in":"2a02:6ea0:dc05::a15f","daita":true,"features":{"daita":{}}},{"hostname":"fr-mrs-wg-002","location":"fr-mrs","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.15.146","include_in_country":true,"weight":300,"public_key":"Z0LEgZIPhNj0+/VWknU3roHlVI3qqAfoV6th9NSC0F0=","ipv6_addr_in":"2a02:6ea0:dc06::a16f"},{"hostname":"fr-par-wg-001","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.66","include_in_country":true,"weight":100,"public_key":"ov323GyDOEHLT0sNRUUPYiE3BkvFDjpmi1a4fzv49hE=","ipv6_addr_in":"2a03:1b20:9:f011::a01f"},{"hostname":"fr-par-wg-002","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.67","include_in_country":true,"weight":100,"public_key":"R5Ve+PJD24QjNXi2Dim7szwCiOLnv+6hg+WyTudAYmE=","ipv6_addr_in":"2a03:1b20:9:f011::f101"},{"hostname":"fr-par-wg-003","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.68","include_in_country":true,"weight":100,"public_key":"w4r/o6VImF7l0/De3JpOGnpzjAFv9wcCu8Rop5eZkWc=","ipv6_addr_in":"2a03:1b20:9:f011::f201"},{"hostname":"fr-par-wg-004","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.69","include_in_country":true,"weight":100,"public_key":"E/KjR7nlFouuRXh1pwGDr7iK2TAZ6c4K0LjjmA1A2Tc=","ipv6_addr_in":"2a03:1b20:9:f011::f301"},{"hostname":"fr-par-wg-005","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.70","include_in_country":true,"weight":100,"public_key":"cmqtSjWUa4/0bENQDKxdr0vQqf4nFVDodarHm0Pc0hY=","ipv6_addr_in":"2a03:1b20:9:f011::f401"},{"hostname":"fr-par-wg-006","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.84","include_in_country":true,"weight":100,"public_key":"x0k8A2S7Dx7VNX2Yo2qRPZW/VefIogID5bVynklBugE=","ipv6_addr_in":"2a03:1b20:9:f011::f001"},{"hostname":"fr-par-wg-007","location":"fr-par","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.126.83","include_in_country":true,"weight":100,"public_key":"D2o4woLw59apODi8NgvVtsbEJOAF5HRxXCp3R4mzGAs=","ipv6_addr_in":"2a03:1b20:9:f011::3f"},{"hostname":"fr-par-wg-101","location":"fr-par","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.184.2","include_in_country":true,"weight":100,"public_key":"e2uj1eu/ZuTPqfY+9ULa6KFPRGLkSWCaooXBg9u9igA=","ipv6_addr_in":"2001:ac8:25:3a::f001","features":{"lwo":{}}},{"hostname":"fr-par-wg-102","location":"fr-par","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.184.66","include_in_country":true,"weight":100,"public_key":"TR0Gedkbp2mRRXKZ7VB7qaAvJHuQlwaaLFc4fxb4q2M=","ipv6_addr_in":"2001:ac8:25:3b::f001"},{"hostname":"fr-par-wg-301","location":"fr-par","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"95.173.222.2","include_in_country":true,"weight":100,"public_key":"gCYpOei4ZYsWJ3mOgCdQo6bnsRgdLNJR9SWEA69U7Gw=","ipv6_addr_in":"2a02:6ea0:1901:2::f001","shadowsocks_extra_addr_in":["95.173.222.5"]},{"hostname":"fr-par-wg-302","location":"fr-par","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"95.173.222.31","include_in_country":true,"weight":100,"public_key":"CpbLl0WVeiW+YbJKNod5khzAI03D2hX2dhq2CCYc2Xc=","ipv6_addr_in":"2a02:6ea0:1901:3::f001","shadowsocks_extra_addr_in":["95.173.222.33"]},{"hostname":"gb-glw-wg-001","location":"gb-glw","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.201.188.3","include_in_country":false,"weight":100,"public_key":"xCPSxGj0QVKC637D8HpRsUUCaSfgAF4ephG/CjhQ2kU=","ipv6_addr_in":"2a06:3040:d:410::f001"},{"hostname":"gb-glw-wg-002","location":"gb-glw","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.201.188.33","include_in_country":false,"weight":100,"public_key":"tX+LKwiFvZhGtbuJq8e62+/vhogHNqdAdjHeoOlWqws=","ipv6_addr_in":"2a06:3040:d:410::f101"},{"hostname":"gb-lon-wg-001","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.130","include_in_country":true,"weight":100,"public_key":"IJJe0TQtuQOyemL4IZn6oHEsMKSPqOuLfD5HoAWEPTY=","ipv6_addr_in":"2a03:1b20:7:f011::a01f","shadowsocks_extra_addr_in":["185.195.232.98","2a03:1b20:7:f011::f008"],"features":{"quic":{"addr_in":["185.195.232.99","185.195.232.95","185.195.232.96","185.195.232.97","2a03:1b20:7:f011::f009","2a03:1b20:7:f011::f00a","2a03:1b20:7:f011::f00b","2a03:1b20:7:f011::f00c","2a03:1b20:7:f011::f00d","2a03:1b20:7:f011::f00e","2a03:1b20:7:f011::f00f"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"gb-lon-wg-001.blockerad.eu"}}},{"hostname":"gb-lon-wg-002","location":"gb-lon","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.222","include_in_country":true,"weight":1,"public_key":"J57ba81Q8bigy9RXBXvl0DgABTrbl81nb37GuX50gnY=","ipv6_addr_in":"2a03:1b20:7:f011::a02f"},{"hostname":"gb-lon-wg-003","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.232.66","include_in_country":true,"weight":100,"public_key":"VZwE8hrpNzg6SMwn9LtEqonXzSWd5dkFk62PrNWFW3Y=","ipv6_addr_in":"2a03:1b20:7:f011::a11f"},{"hostname":"gb-lon-wg-004","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.232.67","include_in_country":true,"weight":100,"public_key":"PLpO9ikFX1garSFaeUpo7XVSMrILrTB8D9ZwQt6Zgwk=","ipv6_addr_in":"2a03:1b20:7:f011::a12f"},{"hostname":"gb-lon-wg-005","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.232.68","include_in_country":true,"weight":100,"public_key":"bG6WulLmMK408n719B8nQJNuTRyRA3Qjm7bsm9d6v2M=","ipv6_addr_in":"2a03:1b20:7:f011::a13f"},{"hostname":"gb-lon-wg-006","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.232.69","include_in_country":true,"weight":100,"public_key":"INRhM0h4T1hi9j28pcC+vRv47bp7DIsNKtagaFZFSBI=","ipv6_addr_in":"2a03:1b20:7:f011::a14f"},{"hostname":"gb-lon-wg-007","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.232.70","include_in_country":true,"weight":100,"public_key":"MVqe9e9aDwfFuvEhEn4Wd/zWV3cmiCX9fZMWetz+23A=","ipv6_addr_in":"2a03:1b20:7:f011::a15f"},{"hostname":"gb-lon-wg-008","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.138","include_in_country":true,"weight":1,"public_key":"uHkxYjfx6yzPHSdyqYqSEHsgFNFV8QCSV6aghuQK3AA=","ipv6_addr_in":"2a03:1b20:7:f011::f801","daita":true,"features":{"daita":{}}},{"hostname":"gb-lon-wg-201","location":"gb-lon","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.248.85.3","include_in_country":true,"weight":100,"public_key":"b71Y8V/vVwNRGkL4d1zvApDVL18u7m31dN+x+i5OJVs=","ipv6_addr_in":"2a0b:89c1:3::a33f"},{"hostname":"gb-lon-wg-202","location":"gb-lon","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.248.85.18","include_in_country":true,"weight":100,"public_key":"+iQWuT3wb2DCy1u2eUKovhJTCB4aUdJUnpxGtONDIVE=","ipv6_addr_in":"2a0b:89c1:3::a34f"},{"hostname":"gb-lon-wg-203","location":"gb-lon","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.248.85.33","include_in_country":true,"weight":100,"public_key":"G7XDQqevQOw1SVL7Iarn9PM+RvmI6H/CfkmahBYEG0g=","ipv6_addr_in":"2a0b:89c1:3::a35f"},{"hostname":"gb-lon-wg-204","location":"gb-lon","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"185.248.85.48","include_in_country":true,"weight":100,"public_key":"tJVHqpfkV2Xgmd4YK60aoErSt6PmJKJjkggHNDfWwiU=","ipv6_addr_in":"2a0b:89c1:3::a36f"},{"hostname":"gb-lon-wg-301","location":"gb-lon","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.119.66","include_in_country":true,"weight":40,"public_key":"Gn9WbiHw83r8BI+v/Usx3mSR+TpMAWLFFz0r9Lfy7XQ=","ipv6_addr_in":"2001:ac8:31:f007::a39f"},{"hostname":"gb-lon-wg-302","location":"gb-lon","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.119.2","include_in_country":true,"weight":40,"public_key":"w1EhKvpp4ZktxiXbuvhb09j4DblrYz3b/SheVywFakI=","ipv6_addr_in":"2001:ac8:31:f005::a37f"},{"hostname":"gb-lon-wg-304","location":"gb-lon","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.119.162","include_in_country":true,"weight":1,"public_key":"nirHSVXCvnxR3aIW95BN0YV02vW/2I7DaeSexqgHW1I=","ipv6_addr_in":"2001:ac8:31:f00a::f001"},{"hostname":"gb-mnc-wg-001","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.133.98","include_in_country":false,"weight":100,"public_key":"Q2khJLbTSFxmppPGHgq2HdxMQx7CczPZCgVpYZMoNnM=","ipv6_addr_in":"2001:ac8:8b:2d::a47f"},{"hostname":"gb-mnc-wg-002","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.130","include_in_country":false,"weight":100,"public_key":"SkERuKByX8fynFxSFAJVjUFJAeu9b/dfW2FynTM7XAk=","ipv6_addr_in":"2001:ac8:8b:26::f001"},{"hostname":"gb-mnc-wg-003","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.162","include_in_country":false,"weight":100,"public_key":"c+RjxBk+wZCv0s4jffQesHdInakRVR3oV0IhpVo0WRY=","ipv6_addr_in":"2001:ac8:8b:27::f001"},{"hostname":"gb-mnc-wg-004","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.194","include_in_country":false,"weight":100,"public_key":"DiMqK85O8U1T65HdVgOGh9uI63I3by9Dt6Shik2xbyM=","ipv6_addr_in":"2001:ac8:8b:28::f001"},{"hostname":"gb-mnc-wg-005","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.226","include_in_country":false,"weight":100,"public_key":"kbVlSaqHQSpnewQn1X0j5R+WKiSW2e2Gq+I4XZj3Bjk=","ipv6_addr_in":"2001:ac8:8b:29::f001"},{"hostname":"gb-mnc-wg-006","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.133.2","include_in_country":false,"weight":100,"public_key":"zKOZzAitVBxfdxtXgGIyk7zmTtoHrVts7RQGrtsRIxo=","ipv6_addr_in":"2001:ac8:8b:2a::f001"},{"hostname":"gb-mnc-wg-007","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.133.34","include_in_country":false,"weight":100,"public_key":"ANaRAtjxqpPgp7r9VjTDfnBMis+MzSgCXc7TZMa0Vno=","ipv6_addr_in":"2001:ac8:8b:2b::f001"},{"hostname":"gb-mnc-wg-008","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.133.66","include_in_country":true,"weight":100,"public_key":"2bciRobW0TPtjrZ2teilr+7PjyiBMUGfixvAKOE52Xo=","ipv6_addr_in":"2001:ac8:8b:2c::f001"},{"hostname":"gb-mnc-wg-009","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.132.98","include_in_country":true,"weight":100,"public_key":"+XsiGXrwqMIgHAnCagmKZZvWJwWb0kifQ/HreBglAzI=","ipv6_addr_in":"2001:ac8:8b:25::f001"},{"hostname":"gb-mnc-wg-201","location":"gb-mnc","active":true,"owned":false,"provider":"Veloxserv","stboot":true,"ipv4_addr_in":"167.160.13.3","include_in_country":false,"weight":200,"public_key":"x3APiw/mxJzdD+3WAPxTFnvOZHVotm6SGomHtMoR4Hg=","ipv6_addr_in":"2a03:ee40:3304::f001","shadowsocks_extra_addr_in":["167.160.13.14"]},{"hostname":"gb-mnc-wg-202","location":"gb-mnc","active":true,"owned":false,"provider":"Veloxserv","stboot":true,"ipv4_addr_in":"167.160.13.127","include_in_country":false,"weight":200,"public_key":"OpQgffPufxbHQUbItRoezS2V+yAEBKZ10jfU82YIByI=","ipv6_addr_in":"2a03:ee40:3304::f101","shadowsocks_extra_addr_in":["167.160.13.139"]},{"hostname":"gr-ath-wg-101","location":"gr-ath","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.246.2","include_in_country":true,"weight":100,"public_key":"li+thkAD7s6IZDgUoiKw4YSjM/U1q203PuthMzIJIU0=","ipv6_addr_in":"2a02:6ea0:f501:2::f001"},{"hostname":"gr-ath-wg-102","location":"gr-ath","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.246.15","include_in_country":true,"weight":100,"public_key":"OL0gbjlNt1s26CDQjRP9wgMZbgYff7/xyUI8ypOn01s=","ipv6_addr_in":"2a02:6ea0:f501:3::f001"},{"hostname":"hk-hkg-wg-201","location":"hk-hkg","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.125.233.18","include_in_country":true,"weight":100,"public_key":"Oxh13dmwY6nNUa5rVHr7sLiFOj0fjzsaAUAUV87/nGs=","ipv6_addr_in":"2403:2c81:1000::a06f"},{"hostname":"hk-hkg-wg-202","location":"hk-hkg","active":false,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.125.233.3","include_in_country":true,"weight":100,"public_key":"zmhMPHfkgo+uQxP+l919Gw7cj5NTatg9nMU37eEUWis=","ipv6_addr_in":"2403:2c81:1000::a05f"},{"hostname":"hk-hkg-wg-301","location":"hk-hkg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.224.2","include_in_country":true,"weight":100,"public_key":"qbvU06SBHXnqMnpb49rnE0yC4AOWQcWl2bEScu18dh8=","ipv6_addr_in":"2001:ac8:a:f::f001","features":{"lwo":{},"quic":{"addr_in":["146.70.224.62"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"hk-hkg-wg-301.blockerad.eu"}}},{"hostname":"hk-hkg-wg-302","location":"hk-hkg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.224.66","include_in_country":true,"weight":100,"public_key":"7FADgmd9KyAVs3eFJE/ob9tV3E6m/klONEEIOfCoPTU=","ipv6_addr_in":"2001:ac8:a:19::f001"},{"hostname":"hr-zag-wg-001","location":"hr-zag","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"154.47.29.2","include_in_country":true,"weight":100,"public_key":"PJvsgLogdAgZiVSxwTDyk9ri02mLZGuElklHShIjDGM=","ipv6_addr_in":"2a02:6ea0:f401:1::a01f"},{"hostname":"hr-zag-wg-002","location":"hr-zag","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"154.47.29.15","include_in_country":true,"weight":100,"public_key":"V0iDOyLSj870sjGGenDvAWqJudlPKDc212cQN85snEo=","ipv6_addr_in":"2a02:6ea0:f401:2::a01f"},{"hostname":"hu-bud-wg-101","location":"hu-bud","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.196.194","include_in_country":true,"weight":100,"public_key":"u+h0GmQJ8UBaMTi2BP9Ls6UUszcGC51y6vTmNr/y+AU=","ipv6_addr_in":"2001:ac8:26:55::f001"},{"hostname":"hu-bud-wg-102","location":"hu-bud","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.196.130","include_in_country":true,"weight":100,"public_key":"iEWLm2F4xV013ZETeZcT1dyUd5O+JnyndHso8RP8txw=","ipv6_addr_in":"2001:ac8:26:54::f001"},{"hostname":"hu-bud-wg-201","location":"hu-bud","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.182.130","include_in_country":true,"weight":100,"public_key":"RPhw+caiytSurUMQfZhEFlxGK83xcwWMNtXCkpTqJBI=","ipv6_addr_in":"2a02:6ea0:5700:1::f001","shadowsocks_extra_addr_in":["79.127.182.132"]},{"hostname":"hu-bud-wg-202","location":"hu-bud","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.182.160","include_in_country":true,"weight":100,"public_key":"xiC/w18znzSImAuzMYpP5NH+1T912cwZXo8M1V4Ruiw=","ipv6_addr_in":"2a02:6ea0:5700:2::f001","shadowsocks_extra_addr_in":["79.127.182.162"]},{"hostname":"id-jpu-wg-001","location":"id-jpu","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"129.227.46.130","include_in_country":true,"weight":100,"public_key":"XYQvOrRqu8j521Hy/8+jGRDLZoSAssOvCectyKz350Y=","ipv6_addr_in":"2602:ffe4:c0d:801d::f001"},{"hostname":"id-jpu-wg-002","location":"id-jpu","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"129.227.46.162","include_in_country":true,"weight":100,"public_key":"gWsH1w7lTYbsS+WxsE6w6vtXSAJoHM6PhDX5DFMYM1k=","ipv6_addr_in":"2602:ffe4:c0d:801e::f101"},{"hostname":"ie-dub-wg-101","location":"ie-dub","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.189.2","include_in_country":true,"weight":100,"public_key":"lHrukA9+vn7Jjzx2Nb/1NQ0WiaiKppEqVxrGT5X1RFQ=","ipv6_addr_in":"2001:ac8:88:84::a01f"},{"hostname":"ie-dub-wg-102","location":"ie-dub","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.189.66","include_in_country":true,"weight":100,"public_key":"8YhrVbViPmYFZ2KJF2pR7d10EaBz8PJbPtoEiAs1IXA=","ipv6_addr_in":"2001:ac8:88:85::f001"},{"hostname":"il-tlv-wg-101","location":"il-tlv","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.227.197","include_in_country":true,"weight":100,"public_key":"XOedjVJaT2IrEDJbzvtZeL4hP5uPRHzFxvD1cwVwUFo=","ipv6_addr_in":"2a02:6ea0:3b00:1::a01f"},{"hostname":"il-tlv-wg-102","location":"il-tlv","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.227.210","include_in_country":true,"weight":100,"public_key":"UNeML4rXjvOerAstTNf4gG5B+OfjVzjSQrWE6mrswD0=","ipv6_addr_in":"2a02:6ea0:3b00:2::a02f"},{"hostname":"il-tlv-wg-103","location":"il-tlv","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.227.222","include_in_country":true,"weight":100,"public_key":"11FJ/NY3jaAw1PSYG9w7bxsMxAzlI+1p8/juh1LJPT0=","ipv6_addr_in":"2a02:6ea0:3b00:3::a03f"},{"hostname":"it-mil-wg-001","location":"it-mil","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.211.66","include_in_country":true,"weight":200,"public_key":"Sa9fFFthvihGMO4cPExJ7ZaWSHNYoXmOqZMvJsaxOVk=","ipv6_addr_in":"2a02:6ea0:d509:1::a09f"},{"hostname":"it-mil-wg-002","location":"it-mil","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.211.79","include_in_country":true,"weight":200,"public_key":"RJ7e37UEP6hfyLQM/lJ2K5wcZOJQFhm2VhFaBniH1kg=","ipv6_addr_in":"2a02:6ea0:d509:2::a10f"},{"hostname":"it-mil-wg-003","location":"it-mil","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"178.249.211.92","include_in_country":true,"weight":200,"public_key":"WOyki5Gzoez07X7D3jAhG68hpoiYIWAx1yypVbkQaVY=","ipv6_addr_in":"2a02:6ea0:d509:3::a11f"},{"hostname":"it-mil-wg-201","location":"it-mil","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.225.2","include_in_country":true,"weight":100,"public_key":"XHwDoIVZGoVfUYbfcPiRp1LhaOCDc0A3QrS72i3ztBw=","ipv6_addr_in":"2001:ac8:24:17::f001"},{"hostname":"it-mil-wg-202","location":"it-mil","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.225.66","include_in_country":true,"weight":100,"public_key":"y5raL0QZx2CpOozrL+Knmjj7nnly3JKatFnxynjXpE0=","ipv6_addr_in":"2001:ac8:24:18::f001"},{"hostname":"it-pmo-wg-001","location":"it-pmo","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.22.91.66","include_in_country":true,"weight":100,"public_key":"cE6s9wV8jfAa84sgXWJ5C4d769m5Ki/XA3rxPdMWhVw=","ipv6_addr_in":"2a02:6ea0:4f00::f001"},{"hostname":"it-pmo-wg-002","location":"it-pmo","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.22.91.79","include_in_country":true,"weight":100,"public_key":"bGtOejMzRDKzFR1gNBAi185dkr/5RtN+QiC8EVl4kU4=","ipv6_addr_in":"2a02:6ea0:4f00::f101"},{"hostname":"jp-osa-wg-001","location":"jp-osa","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.114.136.3","include_in_country":true,"weight":100,"public_key":"uhbuY1A7g0yNu0lRhLTi020kYeAx34ED30BA5DQRHFo=","ipv6_addr_in":"2403:fbc0:7000::f001","features":{"lwo":{},"quic":{"addr_in":["194.114.136.33"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"jp-osa-wg-001.blockerad.eu"}}},{"hostname":"jp-osa-wg-002","location":"jp-osa","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.114.136.34","include_in_country":true,"weight":100,"public_key":"wzGXxsYOraTCPZuRxfXVTNmoWsRkMFLqMqDxI4PutBg=","ipv6_addr_in":"2403:fbc0:7000::f101"},{"hostname":"jp-osa-wg-003","location":"jp-osa","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.114.136.65","include_in_country":true,"weight":100,"public_key":"Pt18GnBffElW0sqnd6IDRr5r0B/NDezy6NicoPI+fG8=","ipv6_addr_in":"2403:fbc0:7000::f201"},{"hostname":"jp-osa-wg-004","location":"jp-osa","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"194.114.136.96","include_in_country":true,"weight":100,"public_key":"JpDAtRuR39GLFKoQNiKvpzuJ65jOOLD7h85ekZ3reVc=","ipv6_addr_in":"2403:fbc0:7000::f301"},{"hostname":"jp-tyo-wg-001","location":"jp-tyo","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.21.239","include_in_country":true,"weight":200,"public_key":"AUo2zhQ0wCDy3/jmZgOe4QMncWWqrdME7BbY2UlkgyI=","ipv6_addr_in":"2a02:6ea0:d31c::a15f","daita":true,"features":{"daita":{}}},{"hostname":"jp-tyo-wg-002","location":"jp-tyo","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.21.226","include_in_country":true,"weight":200,"public_key":"zdlqydCbeR7sG1y5L8sS65X1oOtRKvfVbAuFgqEGhi4=","ipv6_addr_in":"2a02:6ea0:d31b::a14f"},{"hostname":"jp-tyo-wg-201","location":"jp-tyo","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.138.194","include_in_country":true,"weight":100,"public_key":"0j7u9Vd+EsqFs8XeV/T/ZM7gE+TWgEsYCsqcZUShvzc=","ipv6_addr_in":"2001:ac8:40:11::b01f"},{"hostname":"jp-tyo-wg-202","location":"jp-tyo","active":false,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.201.2","include_in_country":true,"weight":100,"public_key":"yLKGIH/eaNUnrOEPRtgvC3PSMTkyAFK/0t8lNjam02k=","ipv6_addr_in":"2001:ac8:40:13::b02f"},{"hostname":"jp-tyo-wg-203","location":"jp-tyo","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.201.66","include_in_country":true,"weight":100,"public_key":"tgTYDEfbDgr35h6hYW01MH76CJrwuBvbQFhyVsazEic=","ipv6_addr_in":"2001:ac8:40:14::b03f"},{"hostname":"mx-qro-wg-001","location":"mx-qro","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.22.129","include_in_country":true,"weight":100,"public_key":"yxyntWsANEwxeR0pOPNAcfWY7zEVICZe9G+GxortzEY=","ipv6_addr_in":"2a02:6ea0:f803::f001","daita":true,"features":{"daita":{},"lwo":{},"quic":{"addr_in":["149.88.22.141"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"mx-qro-wg-001.blockerad.eu"}}},{"hostname":"mx-qro-wg-002","location":"mx-qro","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.22.142","include_in_country":true,"weight":100,"public_key":"kGkalo3qvm8MynKdzwW7CGBYXkqRwGhHfYVssgKOWnU=","ipv6_addr_in":"2a02:6ea0:f803:1::f001"},{"hostname":"mx-qro-wg-003","location":"mx-qro","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.22.155","include_in_country":true,"weight":100,"public_key":"hRamkTwXw0usPFDorPl2vf1qP8chczEBcqeV5bA1QDA=","ipv6_addr_in":"2a02:6ea0:f803:2::f001"},{"hostname":"mx-qro-wg-004","location":"mx-qro","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.22.168","include_in_country":true,"weight":100,"public_key":"Q3yqhnYHK/bFjrd6yqti8gSV1gzOwvnl5N5tXuUxMyk=","ipv6_addr_in":"2a02:6ea0:f803:3::f001"},{"hostname":"my-kul-wg-001","location":"my-kul","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"98.98.47.130","include_in_country":true,"weight":100,"public_key":"RnwTFcAl6z4UfXio9ApLqlOjBcYvD0gWG0htl6fiCl4=","ipv6_addr_in":"2602:ffe4:c20:112::f001","shadowsocks_extra_addr_in":["98.98.47.132"]},{"hostname":"my-kul-wg-002","location":"my-kul","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"162.128.129.98","include_in_country":true,"weight":100,"public_key":"BVh+R5uifa9kn6fDNozd1OrnlGlV8qTr/IUIg0PDGl0=","ipv6_addr_in":"2602:ffe4:c20:112::f101","shadowsocks_extra_addr_in":["162.128.129.100"]},{"hostname":"ng-los-wg-001","location":"ng-los","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.149.130","include_in_country":true,"weight":100,"public_key":"nlpbIResE9vYypA9M/tKvfbUamsmCSawTqmq0cbVJjw=","ipv6_addr_in":"2a02:6ea0:5400:1::f001","shadowsocks_extra_addr_in":["79.127.149.132"]},{"hostname":"ng-los-wg-002","location":"ng-los","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.149.159","include_in_country":true,"weight":100,"public_key":"Hel+ma9otIsWedjgK6Dp51t/WmUys+Q/hUqpvN7qBXg=","ipv6_addr_in":"2a02:6ea0:5400:2::f001","shadowsocks_extra_addr_in":["79.127.149.161"]},{"hostname":"nl-ams-wg-001","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.249.66","include_in_country":true,"weight":100,"public_key":"UrQiI9ISdPPzd4ARw1NHOPKKvKvxUhjwRjaI0JpJFgM=","ipv6_addr_in":"2a03:1b20:3:f011::f001","daita":true,"features":{"daita":{}}},{"hostname":"nl-ams-wg-002","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.82","include_in_country":true,"weight":1,"public_key":"DVui+5aifNFRIVDjH3v2y+dQ+uwI+HFZOd21ajbEpBo=","ipv6_addr_in":"2a03:1b20:3:f011::a02f","daita":true,"features":{"daita":{},"lwo":{},"quic":{"addr_in":["185.65.134.218"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"nl-ams-wg-002.blockerad.eu"}}},{"hostname":"nl-ams-wg-003","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.83","include_in_country":true,"weight":100,"public_key":"if4HpJZbN7jft5E9R9wAoTcggIu6eZhgYDvqxnwrXic=","ipv6_addr_in":"2a03:1b20:3:f011::f201"},{"hostname":"nl-ams-wg-004","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.249.69","include_in_country":true,"weight":100,"public_key":"hnRyse6QxPPcZOoSwRsHUtK1W+APWXnIoaDTmH6JsHQ=","ipv6_addr_in":"2a03:1b20:3:f011::f301"},{"hostname":"nl-ams-wg-005","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.249.70","include_in_country":true,"weight":100,"public_key":"33BoONMGCm2vknq2eq72eozRsHmHQY6ZHEEZ4851TkY=","ipv6_addr_in":"2a03:1b20:3:f011::f401"},{"hostname":"nl-ams-wg-006","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.86","include_in_country":true,"weight":100,"public_key":"xpZ3ZDEukbqKQvdHwaqKMUhsYhcYD3uLPUh1ACsVr1s=","ipv6_addr_in":"2a03:1b20:3:f011::f501"},{"hostname":"nl-ams-wg-007","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.76","include_in_country":true,"weight":1,"public_key":"Os/BwxAIWehlypQ8QjrKVEK5PhY84b413+U3YWZJYXQ=","ipv6_addr_in":"2a03:1b20:3:f011::f701"},{"hostname":"nl-ams-wg-008","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.249.73","include_in_country":true,"weight":100,"public_key":"hf+klJbIyUoGUaFHgac9W+yriwb9uvSnafDfnmEW9Hc=","ipv6_addr_in":"2a03:1b20:3:f011::f801","shadowsocks_extra_addr_in":["193.32.249.209"]},{"hostname":"nl-ams-wg-101","location":"nl-ams","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"92.60.40.194","include_in_country":true,"weight":50,"public_key":"m9w2Fr0rcN6R1a9HYrGnUTU176rTZIq2pcsovPd9sms=","ipv6_addr_in":"2a0c:59c0:18::a20f"},{"hostname":"nl-ams-wg-102","location":"nl-ams","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"92.60.40.209","include_in_country":true,"weight":50,"public_key":"uUYbYGKoA6UBh1hfkAz5tAWFv4SmteYC9kWh7/K6Ah0=","ipv6_addr_in":"2a0c:59c0:18::a21f"},{"hostname":"nl-ams-wg-103","location":"nl-ams","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"92.60.40.224","include_in_country":true,"weight":1,"public_key":"CE7mlfDJ4gpwLPB/CyPfIusITnGZwDI9v4IlVueGT24=","ipv6_addr_in":"2a0c:59c0:18::a22f","features":{"lwo":{},"quic":{"addr_in":["92.60.40.237"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"nl-ams-wg-103.blockerad.eu"}}},{"hostname":"nl-ams-wg-201","location":"nl-ams","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.196.2","include_in_country":true,"weight":100,"public_key":"vt+yTcpxWvH8qiSncd1wSPV/78vt2aE2BBU8ZbG7x1Q=","ipv6_addr_in":"2a02:6ea0:c034:1::a30f","daita":true,"features":{"daita":{}}},{"hostname":"nl-ams-wg-202","location":"nl-ams","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.196.15","include_in_country":true,"weight":100,"public_key":"BChJDLOwZu9Q1oH0UcrxcHP6xxHhyRbjrBUsE0e07Vk=","ipv6_addr_in":"2a02:6ea0:c034:2::a31f"},{"hostname":"nl-ams-wg-203","location":"nl-ams","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.196.28","include_in_country":true,"weight":100,"public_key":"M5z8TKjJYpIJ3FXoXy7k58IUaoVro2tWMKSgC5WIqR8=","ipv6_addr_in":"2a02:6ea0:c034:3::a32f"},{"hostname":"no-osl-wg-001","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"176.125.235.71","include_in_country":true,"weight":100,"public_key":"jOUZjMq2PWHDzQxu3jPXktYB7EKeFwBzGZx56cTXXQg=","ipv6_addr_in":"2a02:20c8:4124::a01f","shadowsocks_extra_addr_in":["178.255.149.131"],"features":{"quic":{"addr_in":["178.255.149.132","178.255.149.133","178.255.149.134","178.255.149.139"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"no-osl-wg-001.blockerad.eu"}}},{"hostname":"no-osl-wg-002","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"176.125.235.72","include_in_country":true,"weight":100,"public_key":"IhhpKphSFWpwja1P4HBctZ367G3Q53EgdeFGZro29Tc=","ipv6_addr_in":"2a02:20c8:4124::a02f","shadowsocks_extra_addr_in":["176.125.235.95"],"features":{"quic":{"addr_in":["176.125.235.96","176.125.235.97","176.125.235.98","176.125.235.99"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"no-osl-wg-002.blockerad.eu"}}},{"hostname":"no-osl-wg-003","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"176.125.235.73","include_in_country":true,"weight":100,"public_key":"zOBWmQ3BEOZKsYKbj4dC2hQjxCbr3eKa6wGWyEDYbC4=","ipv6_addr_in":"2a02:20c8:4124::a03f","shadowsocks_extra_addr_in":["176.125.235.105"],"features":{"quic":{"addr_in":["176.125.235.106","176.125.235.107","176.125.235.108","176.125.235.109"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"no-osl-wg-003.blockerad.eu"}}},{"hostname":"no-osl-wg-004","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"176.125.235.74","include_in_country":true,"weight":100,"public_key":"veeEoYS9a2T6K8WMs/MvRCdNJG580XbhnLfbFjp3B0M=","ipv6_addr_in":"2a02:20c8:4124::a04f","shadowsocks_extra_addr_in":["176.125.235.115"],"features":{"quic":{"addr_in":["176.125.235.116","176.125.235.117","176.125.235.118","176.125.235.119"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"no-osl-wg-004.blockerad.eu"}}},{"hostname":"no-osl-wg-005","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"178.255.149.140","include_in_country":true,"weight":100,"public_key":"ScQu/AqslSPwpXMIEyimrYZWTIdJJXLLeXrijWOF0SE=","ipv6_addr_in":"2a02:20c8:4124::f401"},{"hostname":"no-osl-wg-006","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"178.255.149.165","include_in_country":true,"weight":100,"public_key":"LBlNBTuT7gNEZoAuxO0PTVPpaDuYA7nAeCyMpg9Agyo=","ipv6_addr_in":"2a02:20c8:4124::f501"},{"hostname":"no-osl-wg-007","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"91.90.44.15","include_in_country":true,"weight":100,"public_key":"A0gisFM9hOZB0ezDcSIg2WwnyeprHV/dmb5JnzST0EE=","ipv6_addr_in":"2a02:20c8:4124::f701"},{"hostname":"no-osl-wg-008","location":"no-osl","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"91.90.44.18","include_in_country":true,"weight":100,"public_key":"/4+nwTRYLjT2UK0g3+S4sjE4oaIQiS6L2b/lpO2bfwI=","ipv6_addr_in":"2a02:20c8:4124::f801"},{"hostname":"no-svg-wg-001","location":"no-svg","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.2","include_in_country":true,"weight":300,"public_key":"kduYoE/b1mA2Pjszx1CzE4Lktsdc2zsUU8Relul2m2U=","ipv6_addr_in":"2a02:20c8:4120::a01f"},{"hostname":"no-svg-wg-002","location":"no-svg","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.31","include_in_country":true,"weight":300,"public_key":"U9fbFesIIr2HotWdkfMpKyOEPk+RYtE2oYn3KoLmkj4=","ipv6_addr_in":"2a02:20c8:4120::a02f"},{"hostname":"no-svg-wg-003","location":"no-svg","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.62","include_in_country":true,"weight":300,"public_key":"btc4mh3n9jVCW6yikw3cOPct0x3B5cDK+kKnvgCV0S0=","ipv6_addr_in":"2a02:20c8:4120::a03f"},{"hostname":"no-svg-wg-004","location":"no-svg","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.93","include_in_country":true,"weight":300,"public_key":"Fu98PLCZw/FTcQqyTy0vzaepkfxuSLAah7wnafGVO1g=","ipv6_addr_in":"2a02:20c8:4120::a04f"},{"hostname":"nz-akl-wg-301","location":"nz-akl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.75.11.50","include_in_country":true,"weight":100,"public_key":"BOEOP01bcND1a0zvmOxRHPB/ObgjgPIzBJE5wbm7B0M=","ipv6_addr_in":"2404:f780:5:deb::f001"},{"hostname":"nz-akl-wg-302","location":"nz-akl","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"103.75.11.66","include_in_country":true,"weight":100,"public_key":"80WGWgFP9q3eU16MuLJISB1fzAu2LM2heschmokVSVU=","ipv6_addr_in":"2404:f780:5:dec::c02f"},{"hostname":"pe-lim-wg-001","location":"pe-lim","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"95.173.223.130","include_in_country":true,"weight":100,"public_key":"S4j4wshSstg9Au6ewFWr9vsZ8giovGPpKbKehXN8Nwc=","ipv6_addr_in":"2a02:6ea0:5500:1::f001","shadowsocks_extra_addr_in":["95.173.223.132"]},{"hostname":"pe-lim-wg-002","location":"pe-lim","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"95.173.223.159","include_in_country":true,"weight":100,"public_key":"y7LsVrzYjeMLlTZmVUuuDkFvJp0kONC6+w+wP0gUIyo=","ipv6_addr_in":"2a02:6ea0:5500:2::f001","shadowsocks_extra_addr_in":["95.173.223.161"]},{"hostname":"ph-mnl-wg-001","location":"ph-mnl","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"129.227.118.162","include_in_country":true,"weight":100,"public_key":"hORxMf/YMmN2/8VWOnTCdgGzGfEyXUEQQ5EBfoCyFDM=","ipv6_addr_in":"2602:ffe4:c06:11e::f001","shadowsocks_extra_addr_in":["129.227.118.164"]},{"hostname":"ph-mnl-wg-002","location":"ph-mnl","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"156.59.127.194","include_in_country":true,"weight":100,"public_key":"TfNj4SJuIZzaXSxulpNzreDZXcX6GJJj+UYpqA2XMVE=","ipv6_addr_in":"2602:ffe4:c06:11e::f101","shadowsocks_extra_addr_in":["156.59.127.196"]},{"hostname":"pl-waw-wg-101","location":"pl-waw","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.212.66","include_in_country":true,"weight":100,"public_key":"fO4beJGkKZxosCZz1qunktieuPyzPnEVKVQNhzanjnA=","ipv6_addr_in":"2a02:6ea0:ce08:1::f001"},{"hostname":"pl-waw-wg-102","location":"pl-waw","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.212.79","include_in_country":true,"weight":100,"public_key":"nJEWae9GebEY7yJONXQ1j4gbURV4QULjx388woAlbDs=","ipv6_addr_in":"2a02:6ea0:ce08:2::a06f"},{"hostname":"pl-waw-wg-103","location":"pl-waw","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.212.92","include_in_country":true,"weight":100,"public_key":"07eUtSNhiJ9dQXBmUqFODj0OqhmbKQGbRikIq9f90jM=","ipv6_addr_in":"2a02:6ea0:ce08:3::a07f"},{"hostname":"pl-waw-wg-201","location":"pl-waw","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"45.128.38.226","include_in_country":true,"weight":100,"public_key":"XwFAczY5LdogFwE9soDecXWqywSCDGuRyJhr/0psI00=","ipv6_addr_in":"2a0d:5600:13:67::a01f"},{"hostname":"pl-waw-wg-202","location":"pl-waw","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.144.34","include_in_country":true,"weight":100,"public_key":"nyfOkamv1ryTS62lsmyU96cqI0dtqek84DhyxWgAQGY=","ipv6_addr_in":"2a0d:5600:13:c47::a02f"},{"hostname":"pt-lis-wg-201","location":"pt-lis","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.20.206","include_in_country":true,"weight":100,"public_key":"JCAe7D/owe11Ii2rhpIKhGZvP/V1P1cVZwZAjpSRqmc=","ipv6_addr_in":"2a02:6ea0:fb01:1::f001"},{"hostname":"pt-lis-wg-202","location":"pt-lis","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.88.20.193","include_in_country":true,"weight":100,"public_key":"5P4CQYQeSozk/3KQZh/kl7tUMFGgRB60Ttx6x2nh+F8=","ipv6_addr_in":"2a02:6ea0:fb01:2::f002"},{"hostname":"pt-lis-wg-301","location":"pt-lis","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.92.210.195","include_in_country":true,"weight":100,"public_key":"A2+7EIVBsq1jZlnx0AWb8xkoaTkkn8LRFwAl3Qb/xTc=","ipv6_addr_in":"2a06:3040:0:1410::f001"},{"hostname":"pt-lis-wg-302","location":"pt-lis","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.92.210.225","include_in_country":true,"weight":100,"public_key":"4V8TnXninUL+vjZqXKUIFnBPOhjFEicdVHa5ZMZhSzc=","ipv6_addr_in":"2a06:3040:0:1410::f101"},{"hostname":"ro-buh-wg-001","location":"ro-buh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.124.130","include_in_country":true,"weight":100,"public_key":"xpKhRTf9JI269S2PujLbrJm1TwIe67HD5CLe+sP4tUU=","ipv6_addr_in":"2a04:9dc0:0:133::a01f"},{"hostname":"ro-buh-wg-002","location":"ro-buh","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.124.194","include_in_country":true,"weight":100,"public_key":"Ekc3+qU88FuMfkEMyLlgRqDYv+WHJvUsfOMI/C0ydE4=","ipv6_addr_in":"2a04:9dc0:0:135::f001"},{"hostname":"rs-beg-wg-101","location":"rs-beg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.193.2","include_in_country":true,"weight":100,"public_key":"Orrce1127WpljZa+xKbF21zJkJ9wM1M3VJ5GJ/UsIDU=","ipv6_addr_in":"2001:ac8:7d:37::a01f"},{"hostname":"rs-beg-wg-102","location":"rs-beg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.193.66","include_in_country":true,"weight":100,"public_key":"35lawt+YUx10ELTFhZhg4/xzXRmjxCl/j1O4RK5d60M=","ipv6_addr_in":"2001:ac8:7d:38::a02f"},{"hostname":"se-got-wg-001","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.66","include_in_country":true,"weight":1,"public_key":"5JMPeO7gXIbR5CnUa/NPNK4L5GqUnreF0/Bozai4pl4=","ipv6_addr_in":"2a03:1b20:5:f011:31::a03f","shadowsocks_extra_addr_in":["185.213.154.238"]},{"hostname":"se-got-wg-002","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.67","include_in_country":true,"weight":1,"public_key":"AtvE5KdPeQtOcE2QyXaPt9eQoBV3GBxzimQ2FIuGQ2U=","ipv6_addr_in":"2a03:1b20:5:f011::a05f"},{"hostname":"se-got-wg-003","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.68","include_in_country":true,"weight":100,"public_key":"BLNHNoGO88LjV/wDBa7CUUwUzPq/fO2UwcGLy56hKy4=","ipv6_addr_in":"2a03:1b20:5:f011::a09f","shadowsocks_extra_addr_in":["2a03:1b20:5:f011:3:b:0:1","2a03:1b20:5:f011:3:b:0:2","2a03:1b20:5:f011:3:b:0:3","2a03:1b20:5:f011:3:b:0:4","2a03:1b20:5:f011:3:b:0:5","2a03:1b20:5:f011:3:b:0:6","2a03:1b20:5:f011:3:b:0:7","2a03:1b20:5:f011:3:b:0:8","2a03:1b20:5:f011:3:b:0:9","2a03:1b20:5:f011:3:b:0:a","2a03:1b20:5:f011:3:b:0:b","2a03:1b20:5:f011:3:b:0:e","2a03:1b20:5:f011:3:b:0:d","2a03:1b20:5:f011:3:b:0:e","2a03:1b20:5:f011:3:b:0:f","2a03:1b20:5:f011:3:b:0:10","2a03:1b20:5:f011:3:b:0:11","2a03:1b20:5:f011:3:b:0:12","2a03:1b20:5:f011:3:b:0:13","2a03:1b20:5:f011:3:b:0:14","2a03:1b20:5:f011:3:b:0:15","2a03:1b20:5:f011:3:b:0:16","2a03:1b20:5:f011:3:b:0:17","2a03:1b20:5:f011:3:b:0:18","2a03:1b20:5:f011:3:b:0:19","2a03:1b20:5:f011:3:b:0:1a","2a03:1b20:5:f011:3:b:0:1b","2a03:1b20:5:f011:3:b:0:1c","2a03:1b20:5:f011:3:b:0:1d","2a03:1b20:5:f011:3:b:0:1e","2a03:1b20:5:f011:3:b:0:1f","2a03:1b20:5:f011:3:b:0:20","2a03:1b20:5:f011:3:b:0:21","2a03:1b20:5:f011:3:b:0:22","2a03:1b20:5:f011:3:b:0:23","2a03:1b20:5:f011:3:b:0:24","2a03:1b20:5:f011:3:b:0:25","2a03:1b20:5:f011:3:b:0:26","2a03:1b20:5:f011:3:b:0:27","2a03:1b20:5:f011:3:b:0:28","2a03:1b20:5:f011:3:b:0:29","2a03:1b20:5:f011:3:b:0:2a","2a03:1b20:5:f011:3:b:0:2b","2a03:1b20:5:f011:3:b:0:2c","2a03:1b20:5:f011:3:b:0:2d","2a03:1b20:5:f011:3:b:0:2e","2a03:1b20:5:f011:3:b:0:2f","2a03:1b20:5:f011:3:b:0:30","2a03:1b20:5:f011:3:b:0:31","2a03:1b20:5:f011:3:b:0:32","2a03:1b20:5:f011:3:b:0:33","2a03:1b20:5:f011:3:b:0:34","2a03:1b20:5:f011:3:b:0:35","2a03:1b20:5:f011:3:b:0:36","2a03:1b20:5:f011:3:b:0:37","2a03:1b20:5:f011:3:b:0:38","2a03:1b20:5:f011:3:b:0:39","2a03:1b20:5:f011:3:b:0:3a","2a03:1b20:5:f011:3:b:0:3b","2a03:1b20:5:f011:3:b:0:3c","2a03:1b20:5:f011:3:b:0:3d","2a03:1b20:5:f011:3:b:0:3e","2a03:1b20:5:f011:3:b:0:3f"],"features":{"quic":{"addr_in":["2a03:1b20:5:f011:3:c:0:1","2a03:1b20:5:f011:3:c:0:2","2a03:1b20:5:f011:3:c:0:3","2a03:1b20:5:f011:3:c:0:4","2a03:1b20:5:f011:3:c:0:5","2a03:1b20:5:f011:3:c:0:6","2a03:1b20:5:f011:3:c:0:7","2a03:1b20:5:f011:3:c:0:8","2a03:1b20:5:f011:3:c:0:9","2a03:1b20:5:f011:3:c:0:a","2a03:1b20:5:f011:3:c:0:b","2a03:1b20:5:f011:3:c:0:e","2a03:1b20:5:f011:3:c:0:d","2a03:1b20:5:f011:3:c:0:e","2a03:1b20:5:f011:3:c:0:f","2a03:1b20:5:f011:3:c:0:10","2a03:1b20:5:f011:3:c:0:11","2a03:1b20:5:f011:3:c:0:12","2a03:1b20:5:f011:3:c:0:13","2a03:1b20:5:f011:3:c:0:14","2a03:1b20:5:f011:3:c:0:15","2a03:1b20:5:f011:3:c:0:16","2a03:1b20:5:f011:3:c:0:17","2a03:1b20:5:f011:3:c:0:18","2a03:1b20:5:f011:3:c:0:19","2a03:1b20:5:f011:3:c:0:1a","2a03:1b20:5:f011:3:c:0:1b","2a03:1b20:5:f011:3:c:0:1c","2a03:1b20:5:f011:3:c:0:1d","2a03:1b20:5:f011:3:c:0:1e","2a03:1b20:5:f011:3:c:0:1f","2a03:1b20:5:f011:3:c:0:20","2a03:1b20:5:f011:3:c:0:21","2a03:1b20:5:f011:3:c:0:22","2a03:1b20:5:f011:3:c:0:23","2a03:1b20:5:f011:3:c:0:24","2a03:1b20:5:f011:3:c:0:25","2a03:1b20:5:f011:3:c:0:26","2a03:1b20:5:f011:3:c:0:27","2a03:1b20:5:f011:3:c:0:28","2a03:1b20:5:f011:3:c:0:29","2a03:1b20:5:f011:3:c:0:2a","2a03:1b20:5:f011:3:c:0:2b","2a03:1b20:5:f011:3:c:0:2c","2a03:1b20:5:f011:3:c:0:2d","2a03:1b20:5:f011:3:c:0:2e","2a03:1b20:5:f011:3:c:0:2f","2a03:1b20:5:f011:3:c:0:30","2a03:1b20:5:f011:3:c:0:31","2a03:1b20:5:f011:3:c:0:32","2a03:1b20:5:f011:3:c:0:33","2a03:1b20:5:f011:3:c:0:34","2a03:1b20:5:f011:3:c:0:35","2a03:1b20:5:f011:3:c:0:36","2a03:1b20:5:f011:3:c:0:37","2a03:1b20:5:f011:3:c:0:38","2a03:1b20:5:f011:3:c:0:39","2a03:1b20:5:f011:3:c:0:3a","2a03:1b20:5:f011:3:c:0:3b","2a03:1b20:5:f011:3:c:0:3c","2a03:1b20:5:f011:3:c:0:3d","2a03:1b20:5:f011:3:c:0:3e","2a03:1b20:5:f011:3:c:0:3f"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-got-wg-003.blockerad.eu"}}},{"hostname":"se-got-wg-004","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.69","include_in_country":true,"weight":100,"public_key":"veGD6/aEY6sMfN3Ls7YWPmNgu3AheO7nQqsFT47YSws=","ipv6_addr_in":"2a03:1b20:5:f011::a10f"},{"hostname":"se-got-wg-005","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.199.2","include_in_country":true,"weight":100,"public_key":"x4h55uXoIIKUqKjjm6PzNiZlzLjxjuAIKzvgU9UjOGw=","ipv6_addr_in":"2a03:1b20:5:f011:5::f001","features":{"quic":{"addr_in":["185.209.199.6","2a03:1b20:5:f011:5::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-got-wg-005.blockerad.eu"}}},{"hostname":"se-got-wg-006","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.199.7","include_in_country":true,"weight":100,"public_key":"dcSpHioI+TY37dbZcviFA/sxSUqmpECXRZIapwR8pVg=","ipv6_addr_in":"2a03:1b20:5:f011:6::f001","features":{"quic":{"addr_in":["185.209.199.11","2a03:1b20:5:f011:6::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-got-wg-006.blockerad.eu"}}},{"hostname":"se-got-wg-007","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.199.12","include_in_country":true,"weight":100,"public_key":"ywfkKYdoVAnjsSYW145ACtrw3DV8xTzFS1hlIO7QRD4=","ipv6_addr_in":"2a03:1b20:5:f011:7::f001","features":{"quic":{"addr_in":["185.209.199.16","2a03:1b20:5:f011:7::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-got-wg-007.blockerad.eu"}}},{"hostname":"se-got-wg-008","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.209.199.17","include_in_country":true,"weight":100,"public_key":"Vh3Y2LsBG1yN4kDeebOr3J6dFooGJIBTftzVqlWhiD4=","ipv6_addr_in":"2a03:1b20:5:f011:8::f001","features":{"quic":{"addr_in":["185.209.199.21","2a03:1b20:5:f011:8::f00a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-got-wg-008.blockerad.eu"}}},{"hostname":"se-got-wg-101","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.70","include_in_country":true,"weight":100,"public_key":"B8UVAeNkAW4NiGHd1lpl933Drh4y7pMqpXJpH0SrGjQ=","ipv6_addr_in":"2a03:1b20:5:f011::aaaf","features":{"lwo":{}}},{"hostname":"se-mma-wg-001","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.220","include_in_country":true,"weight":1,"public_key":"Qn1QaXYTJJSmJSMw18CGdnFiVM0/Gj/15OdkxbXCSG0=","ipv6_addr_in":"2a03:1b20:1:f410::a01f"},{"hostname":"se-mma-wg-002","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.80","include_in_country":true,"weight":1,"public_key":"5y66WShsFXqM5K7/4CPEGCWfk7PQyNhVBT2ILjbGm2I=","ipv6_addr_in":"2a03:1b20:1:f410::a15f"},{"hostname":"se-mma-wg-003","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.83","include_in_country":true,"weight":1,"public_key":"fZFAcd8vqWOBpRqlXifsjzGf16gMTg2GuwKyZtkG6UU=","ipv6_addr_in":"2a03:1b20:1:f410::a18f"},{"hostname":"se-mma-wg-004","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.130","include_in_country":false,"weight":1,"public_key":"m4jnogFbACz7LByjo++8z5+1WV0BuR1T7E1OWA+n8h0=","ipv6_addr_in":"2a03:1b20:1:f410:40::a04f"},{"hostname":"se-mma-wg-005","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.82","include_in_country":true,"weight":1,"public_key":"qnJrQEf2JiDHMnMWFFxWz8I9NREockylVgYVE95s72s=","ipv6_addr_in":"2a03:1b20:1:f410::a17f","features":{"lwo":{}}},{"hostname":"se-mma-wg-011","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.94","include_in_country":true,"weight":2,"public_key":"vclzw8ytARhkEqw4cLUJPC3REvMZqWsO+7TYD/U2UVk=","ipv6_addr_in":"2a03:1b20:1:f410::f101","shadowsocks_extra_addr_in":["141.98.255.96"]},{"hostname":"se-mma-wg-012","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.255.97","include_in_country":true,"weight":2,"public_key":"aPcHJj3I1oISU8cwLz2Uyq4ctUOXdTpuS96aW89snUs=","ipv6_addr_in":"2a03:1b20:1:f410::f201","shadowsocks_extra_addr_in":["141.98.255.99"]},{"hostname":"se-mma-wg-101","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.83.220.68","include_in_country":true,"weight":100,"public_key":"7ncbaCb+9za3jnXlR95I6dJBkwL1ABB5i4ndFUesYxE=","ipv6_addr_in":"2a03:1b20:1:e011::a21f","features":{"lwo":{}}},{"hostname":"se-mma-wg-102","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.83.220.69","include_in_country":true,"weight":100,"public_key":"cwglRdgLQ4gMG36TIYlc5OIemLNrYs4UM1KTc8mnzxk=","ipv6_addr_in":"2a03:1b20:1:e011::a22f"},{"hostname":"se-mma-wg-103","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.83.220.70","include_in_country":true,"weight":100,"public_key":"XscA5gebj51nmhAr6o+aUCnMHWGjbS1Gvvd0tuLRiFE=","ipv6_addr_in":"2a03:1b20:1:e011::a23f"},{"hostname":"se-mma-wg-111","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.129.59.19","include_in_country":true,"weight":2,"public_key":"vi0PPk0ZCDvDMCSQD0mctmPFFH7NiawLxJquyPIGwAY=","ipv6_addr_in":"2a03:1b20:1:e011::f701","daita":true,"shadowsocks_extra_addr_in":["45.129.59.29"],"features":{"daita":{},"quic":{"addr_in":["45.129.59.25","2a03:1b20:1:e011::f70a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-mma-wg-111.blockerad.eu"}}},{"hostname":"se-mma-wg-112","location":"se-mma","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"45.129.59.129","include_in_country":true,"weight":2,"public_key":"bysuFAwy+jwl5IePhY06/j7ByWDsAtU5pKPo44k4qEY=","ipv6_addr_in":"2a03:1b20:1:e011::f601","daita":true,"shadowsocks_extra_addr_in":["45.129.59.139"],"features":{"daita":{},"quic":{"addr_in":["45.129.59.135","2a03:1b20:1:e011::f60a"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-mma-wg-112.blockerad.eu"}}},{"hostname":"se-sto-wg-001","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.233.76","include_in_country":true,"weight":100,"public_key":"MkP/Jytkg51/Y/EostONjIN6YaFRpsAYiNKMX27/CAY=","ipv6_addr_in":"2a03:1b20:4:f011::999f"},{"hostname":"se-sto-wg-002","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.67","include_in_country":true,"weight":100,"public_key":"q2ZZPfumPaRVl4DJfzNdQF/GHfe6BYAzQ2GZZHb6rmI=","ipv6_addr_in":"2a03:1b20:4:f011::a02f"},{"hostname":"se-sto-wg-003","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.68","include_in_country":true,"weight":100,"public_key":"qZbwfoY4LHhDPzUROFbG+LqOjB0+Odwjg/Nv3kGolWc=","ipv6_addr_in":"2a03:1b20:4:f011::f201"},{"hostname":"se-sto-wg-004","location":"se-sto","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.69","include_in_country":true,"weight":100,"public_key":"94qIvXgF0OXZ4IcquoS7AO57OV6JswUFgdONgGiq+jo=","ipv6_addr_in":"2a03:1b20:4:f011::f301"},{"hostname":"se-sto-wg-005","location":"se-sto","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.72","include_in_country":true,"weight":100,"public_key":"5rVa0M13oMNobMY7ToAMU1L/Mox7AYACvV+nfsE7zF0=","ipv6_addr_in":"2a03:1b20:4:f011::f401"},{"hostname":"se-sto-wg-006","location":"se-sto","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.73","include_in_country":true,"weight":100,"public_key":"5WNG/KKCtgF4+49e/4iqvHVY/i+6dzUmVKXcJj7zi3I=","ipv6_addr_in":"2a03:1b20:4:f011::f501"},{"hostname":"se-sto-wg-007","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.70","include_in_country":true,"weight":100,"public_key":"YD4k8xaiw2kcRhfLRf2UiRNcDmvvu5NV0xT4d5xOFzU=","ipv6_addr_in":"2a03:1b20:4:f011::b07f"},{"hostname":"se-sto-wg-008","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.71","include_in_country":true,"weight":100,"public_key":"4nOXEaCDYBV//nsVXk7MrnHpxLV9MbGjt+IGQY//p3k=","ipv6_addr_in":"2a03:1b20:4:f011::f701"},{"hostname":"se-sto-wg-009","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.233.69","include_in_country":false,"weight":1,"public_key":"t1XlQD7rER0JUPrmh3R5IpxjUP9YOqodJAwfRorNxl4=","ipv6_addr_in":"2a03:1b20:4:f011::a09f","daita":true,"features":{"daita":{},"lwo":{},"quic":{"addr_in":["185.195.233.172"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-009.blockerad.eu"}}},{"hostname":"se-sto-wg-010","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.233.70","include_in_country":true,"weight":1,"public_key":"pWhNidLbYca9j66c7iw/3kgtU+UyFRIgc75xy8riqzg=","ipv6_addr_in":"2a03:1b20:4:f011::a10f"},{"hostname":"se-sto-wg-011","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.233.71","include_in_country":true,"weight":100,"public_key":"GqKpm8VwKJQLQEQ0PXbkRueY9hDqiMibr+EpW3n9syk=","ipv6_addr_in":"2a03:1b20:4:f011::a11f"},{"hostname":"se-sto-wg-012","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.233.66","include_in_country":true,"weight":100,"public_key":"1493vtFUbIfSpQKRBki/1d0YgWIQwMV4AQAvGxjCNVM=","ipv6_addr_in":"2a03:1b20:4:f011::fb01"},{"hostname":"se-sto-wg-013","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.195.233.67","include_in_country":true,"weight":100,"public_key":"u3pZZjXm0NHCNqPIhKlZ7Vy6CQm5G9YpfgvaywurTho=","ipv6_addr_in":"2a03:1b20:4:f011::fe01"},{"hostname":"se-sto-wg-201","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"89.37.63.10","include_in_country":true,"weight":200,"public_key":"V5RUvv8xp3xYc9b/KoGjTL6EUEb2mTv+8egxuEvUAnc=","ipv6_addr_in":"2a02:6ea0:1508:1::f001","shadowsocks_extra_addr_in":["89.37.63.16"],"features":{"quic":{"addr_in":["89.37.63.15","2a02:6ea0:1508:1::f009"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-201.blockerad.eu"}}},{"hostname":"se-sto-wg-202","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"89.37.63.66","include_in_country":true,"weight":200,"public_key":"S4fVJ6wjUxrRQsDZwWvKVLtcNBJgoSshkqy3wXWG0UM=","ipv6_addr_in":"2a02:6ea0:1508:2::f001","shadowsocks_extra_addr_in":["89.37.63.72"],"features":{"quic":{"addr_in":["89.37.63.71","2a02:6ea0:1508:2::f009"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-202.blockerad.eu"}}},{"hostname":"se-sto-wg-203","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"89.37.63.129","include_in_country":true,"weight":200,"public_key":"mkBf9JhtMPHS3w0FfJOwSS5kfmUQ0RGSLXBdxUNlzTs=","ipv6_addr_in":"2a02:6ea0:1508:3::f001","shadowsocks_extra_addr_in":["89.37.63.135"],"features":{"quic":{"addr_in":["89.37.63.134","2a02:6ea0:1508:3::f009"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-203.blockerad.eu"}}},{"hostname":"se-sto-wg-204","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"89.37.63.190","include_in_country":true,"weight":200,"public_key":"cPhM7ShRWQmKiJtD9Wd1vDh0GwIlaMvFb/WPrP58FH8=","ipv6_addr_in":"2a02:6ea0:1508:4::f001","shadowsocks_extra_addr_in":["89.37.63.196"],"features":{"lwo":{},"quic":{"addr_in":["89.37.63.195","2a02:6ea0:1508:4::c101"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-204.blockerad.eu"}}},{"hostname":"se-sto-wg-205","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"170.62.100.10","include_in_country":true,"weight":1,"public_key":"9V4G5BERZI4xHudcIf5wdDG77XZSY08lVEiXrAGXuEE=","ipv6_addr_in":"2a02:6ea0:1508:5::f001","shadowsocks_extra_addr_in":["170.62.100.16"],"features":{"quic":{"addr_in":["170.62.100.15"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-205.blockerad.eu"}}},{"hostname":"se-sto-wg-206","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"170.62.100.66","include_in_country":true,"weight":200,"public_key":"9KHMuwHqa1Mx2VrKX3cvqLN/ZPDjH5/z0q+IWbfrmW8=","ipv6_addr_in":"2a02:6ea0:1508:6::f001","shadowsocks_extra_addr_in":["170.62.100.72"],"features":{"quic":{"addr_in":["170.62.100.71"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-206.blockerad.eu"}}},{"hostname":"se-sto-wg-207","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"170.62.100.129","include_in_country":true,"weight":200,"public_key":"OatHF/w6fOg8w2415s8zPSXw6LtcYOm+90pqyJ5ZsVY=","ipv6_addr_in":"2a02:6ea0:1508:7::f001","shadowsocks_extra_addr_in":["170.62.100.135"],"features":{"quic":{"addr_in":["170.62.100.134"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-207.blockerad.eu"}}},{"hostname":"se-sto-wg-208","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"170.62.100.170","include_in_country":true,"weight":1,"public_key":"HozGhf1OfjFEASBzjmktB9AKkIgbC+OhSabZKwT6EHc=","ipv6_addr_in":"2a02:6ea0:1508:8::f001","daita":true,"shadowsocks_extra_addr_in":["170.62.100.176"],"features":{"daita":{},"quic":{"addr_in":["170.62.100.175"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-208.blockerad.eu"}}},{"hostname":"se-sto-wg-209","location":"se-sto","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"170.62.100.211","include_in_country":true,"weight":1,"public_key":"r3360zyxOKxUthx90sfRkLZBt1Q5alk45/H9Dkq5kFM=","ipv6_addr_in":"2a02:6ea0:1508:9::f001","shadowsocks_extra_addr_in":["170.62.100.217"],"features":{"quic":{"addr_in":["170.62.100.216"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"se-sto-wg-209.blockerad.eu"}}},{"hostname":"sg-sin-wg-001","location":"sg-sin","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.60.2","include_in_country":true,"weight":100,"public_key":"sFHv/qzG7b6ds5pow+oAR3G5Wqp9eFbBD3BmEGBuUWU=","ipv6_addr_in":"2a02:6ea0:d13e:1::a09f","daita":true,"features":{"daita":{}}},{"hostname":"sg-sin-wg-002","location":"sg-sin","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.60.15","include_in_country":true,"weight":100,"public_key":"WM5I4IFwQcVysM4fF4NXZtQXNrSkqVWkQxNPPygOiF0=","ipv6_addr_in":"2a02:6ea0:d13e:2::a10f"},{"hostname":"sg-sin-wg-003","location":"sg-sin","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.60.28","include_in_country":true,"weight":100,"public_key":"3HtGdhEXUPKQIDRW49wCUoTK2ZXfq+QfzjfYoldNchg=","ipv6_addr_in":"2a02:6ea0:d13e:3::a11f","features":{"lwo":{}}},{"hostname":"sg-sin-wg-101","location":"sg-sin","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.199.194","include_in_country":true,"weight":100,"public_key":"KB6ZA1PAixd74c+mO0VBY4j7LaitK8B4L1APbFIQyQ0=","ipv6_addr_in":"2a0d:5600:d:44::a01f"},{"hostname":"sg-sin-wg-102","location":"sg-sin","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.199.130","include_in_country":true,"weight":100,"public_key":"qrhHOwk0ree+LFxW6htvGEfVFuhM2efQ/M+4p0sx/gA=","ipv6_addr_in":"2a0d:5600:d:43::a02f"},{"hostname":"si-lju-wg-001","location":"si-lju","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"93.115.0.3","include_in_country":true,"weight":100,"public_key":"fXWKnogYH3IORGePtkyFg3r/56ZQGkF6hjdw2svhmw8=","ipv6_addr_in":"2a06:3040:7:210::f001"},{"hostname":"si-lju-wg-002","location":"si-lju","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"93.115.0.33","include_in_country":true,"weight":100,"public_key":"HkPoWKRG/KV2C8afaaah9Jl5lYuvJo1loCaFadKDZVU=","ipv6_addr_in":"2a06:3040:7:210::f101"},{"hostname":"sk-bts-wg-001","location":"sk-bts","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.34.129","include_in_country":true,"weight":100,"public_key":"QEVIaIycN8p5twXCuZeQTEj9utozakw/MU8H6+/whls=","ipv6_addr_in":"2a02:6ea0:2901:1::f001"},{"hostname":"sk-bts-wg-002","location":"sk-bts","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.34.143","include_in_country":true,"weight":100,"public_key":"JeEuObwimNmoVtPn4kpMI1y1UM+IChGVBLtmP3CNNVQ=","ipv6_addr_in":"2a02:6ea0:2901::a02f"},{"hostname":"th-bkk-wg-001","location":"th-bkk","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"156.59.50.194","include_in_country":true,"weight":100,"public_key":"zX6pm3TVJe7rjQ9GrFH1IY29vw/PJL6LGh3/ALxEyx4=","ipv6_addr_in":"2602:ffe4:c09:10a::f001"},{"hostname":"th-bkk-wg-002","location":"th-bkk","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"156.59.50.226","include_in_country":true,"weight":100,"public_key":"L8CCv3NWDaMyUh4dxO44LSy07ETWCcWBeeGFyQZIlyo=","ipv6_addr_in":"2602:ffe4:c09:109::f101"},{"hostname":"tr-ist-wg-001","location":"tr-ist","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.229.129","include_in_country":true,"weight":100,"public_key":"jPhK/ziQfJ1Z5GCPj+qR3A7YV2mIQSQtEPCRuG7TUW8=","ipv6_addr_in":"2a02:6ea0:e813::f001","shadowsocks_extra_addr_in":["149.102.229.131"]},{"hostname":"tr-ist-wg-002","location":"tr-ist","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.229.158","include_in_country":true,"weight":100,"public_key":"TDHn9OvFYoHh9nwlYG7OCpPRvCjfODUOksSQPzhguTg=","ipv6_addr_in":"2a02:6ea0:e813:1::f001","shadowsocks_extra_addr_in":["149.102.229.160"]},{"hostname":"ua-iev-wg-001","location":"ua-iev","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.240.79","include_in_country":true,"weight":100,"public_key":"PO2o3ewguPP24wLy8bbDqx1xuAnTOIVzdzVGVT0d8kU=","ipv6_addr_in":"2a02:6ea0:e109:2::a01f"},{"hostname":"ua-iev-wg-002","location":"ua-iev","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.102.240.66","include_in_country":true,"weight":100,"public_key":"HUj/J8Rxx7QVGh3kJsFgPZoqtm2BQIX03vKJSIyTOSo=","ipv6_addr_in":"2a02:6ea0:e109:1::a02f"},{"hostname":"us-atl-wg-001","location":"us-atl","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.140.130","include_in_country":false,"weight":100,"public_key":"nvyBkaEXHwyPBAm8spGB0TFzf2W5wPAl8EEuJ0t+bzs=","ipv6_addr_in":"2a02:6ea0:c122:1::b79f","features":{"lwo":{},"quic":{"addr_in":["45.134.140.142"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-atl-wg-001.blockerad.eu"}}},{"hostname":"us-atl-wg-002","location":"us-atl","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.140.143","include_in_country":false,"weight":100,"public_key":"ECeGYeh8CfPJO3v56ucCDdl+PlKcj2bBszUGkT+hVWQ=","ipv6_addr_in":"2a02:6ea0:c122:2::b80f"},{"hostname":"us-atl-wg-301","location":"us-atl","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"67.213.209.116","include_in_country":true,"weight":100,"public_key":"SUO0TkKNce4tNTHB3F7PrlvkUzAQeLBSefsgbVnbTkM=","ipv6_addr_in":"2606:2e00:8000:4::f001","shadowsocks_extra_addr_in":["155.2.190.6"]},{"hostname":"us-atl-wg-302","location":"us-atl","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"67.213.209.117","include_in_country":true,"weight":100,"public_key":"OODmjMlAuaUXGeTUzwagEiG42GF3m0ZlHh+3Ssw1Ckg=","ipv6_addr_in":"2606:2e00:8000:4::f101","shadowsocks_extra_addr_in":["155.2.190.41"]},{"hostname":"us-atl-wg-303","location":"us-atl","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"67.213.209.118","include_in_country":true,"weight":100,"public_key":"IR4ZTWn7TBujt2nMDoB9xYISoVigWYTRyaG8mHLji1o=","ipv6_addr_in":"2606:2e00:8000:4::f201","shadowsocks_extra_addr_in":["155.2.190.76"]},{"hostname":"us-atl-wg-304","location":"us-atl","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"67.213.209.119","include_in_country":true,"weight":100,"public_key":"1JswEeh7qEEq0oy2sQBeqg+QjNkTJRsZ/N9/CN92SCs=","ipv6_addr_in":"2606:2e00:8000:4::f301","shadowsocks_extra_addr_in":["155.2.190.111"]},{"hostname":"us-atl-wg-305","location":"us-atl","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"67.213.209.120","include_in_country":true,"weight":100,"public_key":"wGxVyRjNKWba7RidWKab0jPpdNKQAgeLFzwx/bz3CWQ=","ipv6_addr_in":"2606:2e00:8000:4::f401","shadowsocks_extra_addr_in":["155.2.190.146"]},{"hostname":"us-atl-wg-306","location":"us-atl","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"67.213.209.121","include_in_country":true,"weight":100,"public_key":"Q8oqrXk9nC9+94GLVUXJ7E8xtV10ggdzQIiQgZI3Em4=","ipv6_addr_in":"2606:2e00:8000:4::f501","shadowsocks_extra_addr_in":["155.2.190.181"]},{"hostname":"us-atl-wg-401","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.108.3","include_in_country":true,"weight":100,"public_key":"49LoowbQpQfc/Yw+1DZ0A/Gien3wRnxwJzvo7Gz8Zhw=","ipv6_addr_in":"2607:9000:c00:31::f001","shadowsocks_extra_addr_in":["23.234.108.14"]},{"hostname":"us-atl-wg-402","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.108.127","include_in_country":true,"weight":100,"public_key":"9CYtFK8cSKxzEiFYpiCuKgYnjMO5Jqri2iFiG2lDUlM=","ipv6_addr_in":"2607:9000:c00:32::f001","shadowsocks_extra_addr_in":["23.234.108.139"]},{"hostname":"us-atl-wg-403","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.109.3","include_in_country":true,"weight":100,"public_key":"a64LFZfr0htAq1mk5EvVPmQPi52oboISpVUHaYKGE1E=","ipv6_addr_in":"2607:9000:c00:33::f001","shadowsocks_extra_addr_in":["23.234.109.14"]},{"hostname":"us-atl-wg-404","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.109.127","include_in_country":true,"weight":100,"public_key":"kYm0GM0/fD37iCZM3+60vAxRV1w/PUW+WQkYV14QZFo=","ipv6_addr_in":"2607:9000:c00:34::f001","shadowsocks_extra_addr_in":["23.234.109.139"]},{"hostname":"us-atl-wg-405","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.110.3","include_in_country":true,"weight":100,"public_key":"643HR3VhN/WVydahUwuKL/e8jT8UNDHduBfDPMF/2E8=","ipv6_addr_in":"2607:9000:c00:35::f001","shadowsocks_extra_addr_in":["23.234.110.14"]},{"hostname":"us-atl-wg-406","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.110.127","include_in_country":true,"weight":100,"public_key":"spmw1nZv6lgKC/T1KMSuLKyAfCifmrxCdXvVPNhQFCY=","ipv6_addr_in":"2607:9000:c00:36::f001","shadowsocks_extra_addr_in":["23.234.110.139"]},{"hostname":"us-atl-wg-407","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.111.3","include_in_country":true,"weight":100,"public_key":"e2UcVruvaIHcCXYjaHe6alKbZW/Qc9lbYzYh1AckWEU=","ipv6_addr_in":"2607:9000:c00:37::f001","shadowsocks_extra_addr_in":["23.234.111.14"]},{"hostname":"us-atl-wg-408","location":"us-atl","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.111.127","include_in_country":true,"weight":100,"public_key":"x3sKDqtdNIUhOjCiZCzMSZmoMps20J0JloSQMebYK2o=","ipv6_addr_in":"2607:9000:c00:38::f001","shadowsocks_extra_addr_in":["23.234.111.139"]},{"hostname":"us-bos-wg-001","location":"us-bos","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"43.225.189.131","include_in_country":true,"weight":100,"public_key":"CsysTnZ0HvyYRjsKMPx60JIgy777JhD0h9WpbHbV83o=","ipv6_addr_in":"2a06:3040:12:610::a01f"},{"hostname":"us-bos-wg-002","location":"us-bos","active":false,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"43.225.189.162","include_in_country":true,"weight":100,"public_key":"LLkA2XSBvfUeXgLdMKP+OTQeKhtGB03kKskJEwlzAE8=","ipv6_addr_in":"2a06:3040:12:610::a02f"},{"hostname":"us-bos-wg-101","location":"us-bos","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.40.50.98","include_in_country":true,"weight":100,"public_key":"oxJ2PIqrQOmS0uiyXvnxT64E1uZnjZDWPbP/+APToAE=","ipv6_addr_in":"2a02:6ea0:f901::a01f"},{"hostname":"us-bos-wg-102","location":"us-bos","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"149.40.50.112","include_in_country":true,"weight":100,"public_key":"wcmmadJObux2/62ES+QbIO21BkU7p2I0s6n4WNZZgW0=","ipv6_addr_in":"2a02:6ea0:f901:1::a02f"},{"hostname":"us-chi-wg-201","location":"us-chi","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"87.249.134.1","include_in_country":true,"weight":1,"public_key":"+Xx2mJnoJ+JS11Z6g8mp6aUZV7p6DAN9ZTAzPaHakhM=","ipv6_addr_in":"2a02:6ea0:c61f::b63f","daita":true,"features":{"daita":{}}},{"hostname":"us-chi-wg-202","location":"us-chi","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"87.249.134.14","include_in_country":true,"weight":100,"public_key":"rmN4IM0I0gF7V9503/xnQMOLsu9txl8GTqci9dgUO18=","ipv6_addr_in":"2a02:6ea0:c61f:1::b64f"},{"hostname":"us-chi-wg-203","location":"us-chi","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"87.249.134.27","include_in_country":true,"weight":100,"public_key":"V0ilKm3bVqt0rmJ80sP0zSVK4m6O3nADi88IQAL5kjw=","ipv6_addr_in":"2a02:6ea0:c61f:2::f001"},{"hostname":"us-chi-wg-301","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.2","include_in_country":true,"weight":500,"public_key":"g9Dlad9R9OcM9w1yu3gq9pQWARQBc3Muj4KfeRY1p20=","ipv6_addr_in":"2607:9000:0:101::f001","shadowsocks_extra_addr_in":["68.235.46.4"]},{"hostname":"us-chi-wg-302","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.33","include_in_country":true,"weight":500,"public_key":"rEVQ7I5Ckvg44uLaSg1l085FcQvFHfM01hMfHxyAQz0=","ipv6_addr_in":"2607:9000:0:102::f001","daita":true,"shadowsocks_extra_addr_in":["68.235.46.35"],"features":{"daita":{}}},{"hostname":"us-chi-wg-303","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.64","include_in_country":true,"weight":500,"public_key":"DfmNHT84TTS6JcJJfZJwT7tZZVgKIKRJU/2AE5sJ6A4=","ipv6_addr_in":"2607:9000:0:103::f001","daita":true,"shadowsocks_extra_addr_in":["68.235.46.66"],"features":{"daita":{}}},{"hostname":"us-chi-wg-304","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.95","include_in_country":true,"weight":500,"public_key":"Tr2rkoiqX7bERbeLMDw9CLiTaB0dp9/Fov/Ytz3C+xY=","ipv6_addr_in":"2607:9000:0:104::f001","shadowsocks_extra_addr_in":["68.235.46.97"]},{"hostname":"us-chi-wg-305","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.126","include_in_country":true,"weight":500,"public_key":"jx/3CJiJRozty6hUTs40M/Swhfcch0z3yElmS1VKoVg=","ipv6_addr_in":"2607:9000:0:105::f001","shadowsocks_extra_addr_in":["68.235.46.128"]},{"hostname":"us-chi-wg-306","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.157","include_in_country":true,"weight":500,"public_key":"WlEbNkNAx/186YZH/UPE6YWkMyAMxRpMRP+IqWrq+TE=","ipv6_addr_in":"2607:9000:0:106::f001","shadowsocks_extra_addr_in":["68.235.46.159"]},{"hostname":"us-chi-wg-307","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.188","include_in_country":true,"weight":500,"public_key":"U9UAYlVm8nXZjWPrF/vbb1P9oqSRmHo+IfK52yDYpGo=","ipv6_addr_in":"2607:9000:0:107::f001","shadowsocks_extra_addr_in":["68.235.46.190"]},{"hostname":"us-chi-wg-308","location":"us-chi","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"68.235.46.209","include_in_country":true,"weight":1,"public_key":"gJsL4BfGcf2QOLGY1Std2Mjg6V2t2w7T2FScANlkJ2I=","ipv6_addr_in":"2607:9000:0:108::f001","shadowsocks_extra_addr_in":["68.235.46.211"]},{"hostname":"us-dal-wg-001","location":"us-dal","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.211.66","include_in_country":true,"weight":100,"public_key":"EAzbWMQXxJGsd8j2brhYerGB3t5cPOXqdIDFspDGSng=","ipv6_addr_in":"2001:ac8:9a:76::1f","features":{"lwo":{},"quic":{"addr_in":["146.70.211.126"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-dal-wg-001.blockerad.eu"}}},{"hostname":"us-dal-wg-002","location":"us-dal","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.211.2","include_in_country":true,"weight":100,"public_key":"OYG1hxzz3kUGpVeGjx9DcCYreMO3S6tZN17iHUK+zDE=","ipv6_addr_in":"2001:ac8:9a:75::2f"},{"hostname":"us-dal-wg-003","location":"us-dal","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.211.130","include_in_country":true,"weight":100,"public_key":"jn/i/ekJOkkRUdMj2I4ViUKd3d/LAdTQ+ICKmBy1tkM=","ipv6_addr_in":"2001:ac8:9a:78::3f"},{"hostname":"us-dal-wg-401","location":"us-dal","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.200.156","include_in_country":true,"weight":100,"public_key":"xZsnCxFN7pOvx6YlTbi92copdsY5xgekTCp//VUMyhE=","ipv6_addr_in":"2a02:6ea0:d20c:3::b72f"},{"hostname":"us-dal-wg-402","location":"us-dal","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.200.143","include_in_country":true,"weight":100,"public_key":"sPQEji8BhxuM/Za0Q0/9aWYxyACtQF0qRpzaBLumEzo=","ipv6_addr_in":"2a02:6ea0:d20c:2::b71f"},{"hostname":"us-dal-wg-403","location":"us-dal","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.200.130","include_in_country":true,"weight":100,"public_key":"4s9JIhxC/D02tosXYYcgrD+pHI+C7oTAFsXzVisKjRs=","ipv6_addr_in":"2a02:6ea0:d20c:1::f001"},{"hostname":"us-dal-wg-502","location":"us-dal","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"206.217.206.47","include_in_country":false,"weight":100,"public_key":"7RegQnJ70PNlB0bpICSlc/W48GCtzszhSelTdlK5QQ0=","ipv6_addr_in":"2606:2e00:8007:a:ae1f:6bff:fef5:7b21"},{"hostname":"us-dal-wg-503","location":"us-dal","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"206.217.206.67","include_in_country":false,"weight":100,"public_key":"si+P5Ef8D21CAkzh9NgrnIhbZDBcFxoYDaN6amSTkWE=","ipv6_addr_in":"2606:2e00:8007:a:ae1f:6bff:fef5:7beb"},{"hostname":"us-dal-wg-504","location":"us-dal","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"206.217.206.87","include_in_country":false,"weight":100,"public_key":"YROBTYZewygT97VTgMHxEwqaUiAjAvsuwTsuh5IBH1Y=","ipv6_addr_in":"2606:2e00:8007:a:ae1f:6bff:fef5:7b1b"},{"hostname":"us-dal-wg-505","location":"us-dal","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"206.217.206.107","include_in_country":false,"weight":100,"public_key":"bf59QZip/y9tvCF6S9pir32LuFtvWH7nayqhzplyGkQ=","ipv6_addr_in":"2606:2e00:8007:a:ae1f:6bff:fef5:7983"},{"hostname":"us-dal-wg-506","location":"us-dal","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"206.217.206.4","include_in_country":false,"weight":100,"public_key":"ry32nhX3WEpktDBR8CnYNbAnm3NOGBUtXmxomWZjKGU=","ipv6_addr_in":"2606:2e00:8007:a:ae1f:6bff:fef5:7b8d"},{"hostname":"us-dal-wg-507","location":"us-dal","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"206.217.206.16","include_in_country":false,"weight":100,"public_key":"7v5alccqwh+9jA+hRqwc1uZIEebXs9g5i/jH29Gr5k0=","ipv6_addr_in":"2606:2e00:8007:a:ae1f:6bff:fef5:7b27"},{"hostname":"us-dal-wg-601","location":"us-dal","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.102.246.3","include_in_country":true,"weight":100,"public_key":"353XrBhmW0VisDN/ztYLb7WwnxQUTnL9Ys5FBPUHHVw=","ipv6_addr_in":"2a01:4740:2::f001","shadowsocks_extra_addr_in":["103.102.246.14"]},{"hostname":"us-dal-wg-602","location":"us-dal","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.102.246.127","include_in_country":true,"weight":100,"public_key":"Vm9kQsUFIGgNeb+NBrY0KZpW51dC84cAQWMY9eMa8Ho=","ipv6_addr_in":"2a01:4740:2::f101","shadowsocks_extra_addr_in":["103.102.246.139"]},{"hostname":"us-dal-wg-603","location":"us-dal","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.102.247.3","include_in_country":true,"weight":100,"public_key":"Y5SK28yMGjcZVByWZkMb24KdmbmY07mDnwLN6817yEE=","ipv6_addr_in":"2a01:4740:2::f201","shadowsocks_extra_addr_in":["103.102.247.14"]},{"hostname":"us-dal-wg-604","location":"us-dal","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.102.247.127","include_in_country":true,"weight":100,"public_key":"7Wyh9lX1nIkKfIpwUywcSUVF8pOxP0c04EehfEeIWg4=","ipv6_addr_in":"2a01:4740:2::f301","shadowsocks_extra_addr_in":["103.102.247.139"]},{"hostname":"us-den-wg-101","location":"us-den","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.210.1","include_in_country":true,"weight":100,"public_key":"74U+9EQrMwVOafgXuSp8eaKG0+p4zjSsDe3J7+ojhx0=","ipv6_addr_in":"2a02:6ea0:d70a::b57f"},{"hostname":"us-den-wg-102","location":"us-den","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.210.14","include_in_country":true,"weight":100,"public_key":"T44stCRbQXFCBCcpdDbZPlNHp2eZEi91ooyk0JDC21E=","ipv6_addr_in":"2a02:6ea0:d70a:1::b58f"},{"hostname":"us-den-wg-103","location":"us-den","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.210.27","include_in_country":true,"weight":100,"public_key":"Az+PGHQ0xFElmRBv+PKZuRnEzKPrPtUpRD3vpxb4si4=","ipv6_addr_in":"2a02:6ea0:d70a:2::b59f"},{"hostname":"us-den-wg-201","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.68.2","include_in_country":true,"weight":100,"public_key":"MsF1hhYtyCsvPt4B8f48biVcVYd692STflhcbKwTGAw=","ipv6_addr_in":"2607:9000:2000:41::f001","shadowsocks_extra_addr_in":["23.234.68.13"]},{"hostname":"us-den-wg-202","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.68.127","include_in_country":true,"weight":100,"public_key":"YP20qT+/cY/sbBhlXo6fWZlfVhRU+emQlZ1am+vUNnw=","ipv6_addr_in":"2607:9000:2000:42::f001","shadowsocks_extra_addr_in":["23.234.68.138"]},{"hostname":"us-den-wg-203","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.69.2","include_in_country":true,"weight":100,"public_key":"D8TSWEfmRIm1qMS0RgO8uireFMMZCMi+XxhIJ2jPBEU=","ipv6_addr_in":"2607:9000:2000:43::f001","shadowsocks_extra_addr_in":["23.234.69.13"]},{"hostname":"us-den-wg-204","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.69.127","include_in_country":true,"weight":100,"public_key":"DZcEpwNSf+6BoDcHknHBVPwAA0ZJjz7DgQ+llATpAzg=","ipv6_addr_in":"2607:9000:2000:44::f001","shadowsocks_extra_addr_in":["23.234.69.138"]},{"hostname":"us-den-wg-205","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.70.2","include_in_country":true,"weight":100,"public_key":"0LQQJLKBZD0Wf0s0nwFfyMW0MMEKoxNPZ14ZbxkogiY=","ipv6_addr_in":"2607:9000:2000:45::f001","shadowsocks_extra_addr_in":["23.234.70.13"]},{"hostname":"us-den-wg-206","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.70.127","include_in_country":true,"weight":100,"public_key":"Y4waCBM7GE9iOT+xl9PcZ2mNKGiawEOBv8UkH84CaAo=","ipv6_addr_in":"2607:9000:2000:46::f001","shadowsocks_extra_addr_in":["23.234.70.138"]},{"hostname":"us-den-wg-207","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.71.2","include_in_country":true,"weight":100,"public_key":"nUnmeY34CDLjW4Q3TAbJQ168jVXmkY4MVAp28rmpzEc=","ipv6_addr_in":"2607:9000:2000:47::f001","shadowsocks_extra_addr_in":["23.234.71.13"]},{"hostname":"us-den-wg-208","location":"us-den","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.71.127","include_in_country":true,"weight":100,"public_key":"Fo6J7nLUeSnNPenB1NiPoivVod3m4fN4OE5yjafxYXY=","ipv6_addr_in":"2607:9000:2000:48::f001","shadowsocks_extra_addr_in":["23.234.71.138"]},{"hostname":"us-det-wg-001","location":"us-det","active":false,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.141.119.131","include_in_country":true,"weight":100,"public_key":"+USmlxhnLmlNkDnBbu+rXwjUwa383e0ilYEqPkEkNHA=","ipv6_addr_in":"2a06:3040:11:610::f001"},{"hostname":"us-det-wg-002","location":"us-det","active":false,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"185.141.119.161","include_in_country":true,"weight":100,"public_key":"cYqP1UqhOYuaj47e4jAbgL55h52L+ALjtML26OtBvFU=","ipv6_addr_in":"2a06:3040:11:610::f101"},{"hostname":"us-hou-wg-001","location":"us-hou","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.221.130","include_in_country":true,"weight":200,"public_key":"NKscQ4mm24nsYWfpL85Cve+BKIExR0JaysldUtVSlzg=","ipv6_addr_in":"2a02:6ea0:e001::f001"},{"hostname":"us-hou-wg-002","location":"us-hou","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.221.143","include_in_country":true,"weight":200,"public_key":"tzSfoiq9ZbCcE5I0Xz9kCrsWksDn0wgvaz9TiHYTmnU=","ipv6_addr_in":"2a02:6ea0:e001:1::f001"},{"hostname":"us-hou-wg-003","location":"us-hou","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.221.156","include_in_country":true,"weight":200,"public_key":"fNSu30TCgbADxNKACx+5qWY6XGJOga4COmTZZE0k0R4=","ipv6_addr_in":"2a02:6ea0:e001:2::b55f"},{"hostname":"us-hou-wg-004","location":"us-hou","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"37.19.221.169","include_in_country":true,"weight":200,"public_key":"NkZMYUEcHykPkAFdm3dE8l2U9P2mt58Dw6j6BWhzaCc=","ipv6_addr_in":"2a02:6ea0:e001:3::b56f"},{"hostname":"us-lax-wg-101","location":"us-lax","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.44.129.98","include_in_country":false,"weight":100,"public_key":"IDXrg8s0qYFAWcMcXFb6P/EHOESkTyotZCSlerQfyCQ=","ipv6_addr_in":"2607:9000:3000:15::a49f"},{"hostname":"us-lax-wg-102","location":"us-lax","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.44.129.66","include_in_country":false,"weight":100,"public_key":"Ldwvbs6mOxEbpXLRA3Z/qmEyJo2wVTdQ94+v3UFsbBw=","ipv6_addr_in":"2607:9000:3000:14::a50f"},{"hostname":"us-lax-wg-103","location":"us-lax","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.44.129.34","include_in_country":false,"weight":100,"public_key":"gabX4D/Yhut0IMl/9jRK+kMoHbkL38qaUm7r/dH5rWg=","ipv6_addr_in":"2607:9000:3000:13::a51f"},{"hostname":"us-lax-wg-201","location":"us-lax","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.203.2","include_in_country":false,"weight":100,"public_key":"xWobY7DWTL+vL1yD4NWwbQ3V4e8qz10Yz+EFdkIjq0Y=","ipv6_addr_in":"2a02:6ea0:c859:1::a01f"},{"hostname":"us-lax-wg-202","location":"us-lax","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.203.15","include_in_country":false,"weight":100,"public_key":"SDnciTlujuy2APFTkhzfq5X+LDi+lhfU38wI2HBCxxs=","ipv6_addr_in":"2a02:6ea0:c859:2::a02f"},{"hostname":"us-lax-wg-203","location":"us-lax","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"169.150.203.28","include_in_country":true,"weight":1,"public_key":"W6/Yamxmfx3geWTwwtBbJe/J8UdEzOfa6M+cEpNPIwg=","ipv6_addr_in":"2a02:6ea0:c859:3::a03f","daita":true,"features":{"daita":{},"lwo":{}}},{"hostname":"us-lax-wg-402","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.173.66","include_in_country":false,"weight":100,"public_key":"EKZXvHlSDeqAjfC/m9aQR0oXfQ6Idgffa9L0DH5yaCo=","ipv6_addr_in":"2a0d:5600:8:6::d2f"},{"hostname":"us-lax-wg-403","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.173.130","include_in_country":false,"weight":100,"public_key":"mBqaWs6pti93U+1feyj6LRzzveNmeklancn3XuKoPWI=","ipv6_addr_in":"2a0d:5600:8:d::d3f"},{"hostname":"us-lax-wg-404","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.173.194","include_in_country":false,"weight":100,"public_key":"YGl+lj1tk08U9x9Z73zowUW3rk8i0nPmYkxGzNdE4VM=","ipv6_addr_in":"2a0d:5600:8:2f::f001"},{"hostname":"us-lax-wg-405","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.172.2","include_in_country":false,"weight":100,"public_key":"Pe86fNGUd+AIeaabsn7Hk4clQf1kJvxOXPykfVGjeho=","ipv6_addr_in":"2a0d:5600:8:37::f001"},{"hostname":"us-lax-wg-406","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.174.2","include_in_country":false,"weight":1,"public_key":"K3KF3TCWbYcHF5XHL2zaifvQGHrPWoCjFYxDaJO71GA=","ipv6_addr_in":"2a0d:5600:8:3b::f001","features":{"lwo":{},"quic":{"addr_in":["146.70.174.62"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-lax-wg-406.blockerad.eu"}}},{"hostname":"us-lax-wg-407","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.172.66","include_in_country":true,"weight":100,"public_key":"1nGkBr+oLwK5lQcVt9vF6rGM5R3ra5bmYTGJfGIh0lk=","ipv6_addr_in":"2a0d:5600:8:38::f001"},{"hostname":"us-lax-wg-408","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.172.130","include_in_country":true,"weight":100,"public_key":"9L5cW9VuUJUS2gH6H7ln2JeCI66fMnnjLiD5UymAtlo=","ipv6_addr_in":"2a0d:5600:8:39::f001"},{"hostname":"us-lax-wg-409","location":"us-lax","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.172.194","include_in_country":true,"weight":100,"public_key":"V+LTWA5DxEVITAXqHexqBzeZo95b8r+3WR8g1FsbPQ4=","ipv6_addr_in":"2a0d:5600:8:3a::f001"},{"hostname":"us-lax-wg-601","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.162.40.3","include_in_country":true,"weight":100,"public_key":"oA6/33kBggrBOJ7z+uo5gZW6L1w2zYcCILKXXax8knY=","ipv6_addr_in":"2602:fa19::f001","shadowsocks_extra_addr_in":["23.162.40.14"]},{"hostname":"us-lax-wg-602","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.162.40.127","include_in_country":true,"weight":100,"public_key":"oLu1H6C8YaoWtmaPzAFboFX8r102Wb1uma9spVPqAX8=","ipv6_addr_in":"2602:fa19::f101","shadowsocks_extra_addr_in":["23.162.40.139"]},{"hostname":"us-lax-wg-603","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.159.216.3","include_in_country":true,"weight":100,"public_key":"qeojNB247YQbT0/ysFyZDjs9RJ6Y4bFaKCLu6PjeMxA=","ipv6_addr_in":"2602:fa47::f001","shadowsocks_extra_addr_in":["23.159.216.14"]},{"hostname":"us-lax-wg-604","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.159.216.127","include_in_country":true,"weight":100,"public_key":"GKl6nhOl96/1AJtVCdEZpOO6F0BS5/TMkrjdH2fb93A=","ipv6_addr_in":"2602:fa47::f101","shadowsocks_extra_addr_in":["23.159.216.139"]},{"hostname":"us-lax-wg-605","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.160.24.3","include_in_country":true,"weight":100,"public_key":"DbZksYqiYPGxOEF7iIaAiyN4+hZc+8HcuMMqpLW3XmA=","ipv6_addr_in":"2602:fa45::f001","shadowsocks_extra_addr_in":["23.160.24.14"]},{"hostname":"us-lax-wg-606","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.160.24.127","include_in_country":true,"weight":100,"public_key":"AW1YO/vYtXJioBmD8BhSGpz1DQNIQeU+jOu+3F7KBDY=","ipv6_addr_in":"2602:fa45::f101","shadowsocks_extra_addr_in":["23.160.24.139"]},{"hostname":"us-lax-wg-607","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.168.216.3","include_in_country":true,"weight":100,"public_key":"ItEcyDXwTXtq6bQubbO6lY0K/oh0dfk26AV+muU+Ah4=","ipv6_addr_in":"2602:f99d::f001","shadowsocks_extra_addr_in":["23.168.216.14"]},{"hostname":"us-lax-wg-608","location":"us-lax","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.168.216.127","include_in_country":true,"weight":100,"public_key":"ikLR1TUKk+PTWFnydqwZ9m0HaD1dPaMNI9DwZTvzYBs=","ipv6_addr_in":"2602:f99d::f101","shadowsocks_extra_addr_in":["23.168.216.139"]},{"hostname":"us-lax-wg-701","location":"us-lax","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.251.26.3","include_in_country":true,"weight":100,"public_key":"EM/33kdl9RiNOF5jvwtp/nfchPAD/sq7MJleg1bZikU=","ipv6_addr_in":"2a01:4740:3::f001","shadowsocks_extra_addr_in":["103.251.26.14"]},{"hostname":"us-lax-wg-702","location":"us-lax","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.251.26.127","include_in_country":true,"weight":100,"public_key":"ddv7vosBlf396nOa79nWn6qXQu2LzezGXfNUDO3hAXQ=","ipv6_addr_in":"2a01:4740:3::f101","shadowsocks_extra_addr_in":["103.251.26.139"]},{"hostname":"us-lax-wg-703","location":"us-lax","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.251.27.3","include_in_country":true,"weight":100,"public_key":"/FWUNK2WlEQbmwYaXuTzUmtshvIYvJnKWVnuqgzlfXw=","ipv6_addr_in":"2a01:4740:3::f201","shadowsocks_extra_addr_in":["103.251.27.14"]},{"hostname":"us-lax-wg-704","location":"us-lax","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.251.27.127","include_in_country":true,"weight":100,"public_key":"KPjr8jrGP3dVI+GbMq2LNc9eREW6EhGHndoSWHqakxE=","ipv6_addr_in":"2a01:4740:3::f301","shadowsocks_extra_addr_in":["103.251.27.139"]},{"hostname":"us-mia-wg-001","location":"us-mia","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.142.219","include_in_country":true,"weight":100,"public_key":"FVEKAMJqaJU2AwWn5Mg9TK9IAfJc4XDUmSzEeC/VXGs=","ipv6_addr_in":"2a02:6ea0:cc1f:2::b62f"},{"hostname":"us-mia-wg-002","location":"us-mia","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.142.206","include_in_country":true,"weight":200,"public_key":"H5t7PsMDnUAHrR8D2Jt3Mh6N6w43WmCzrOHShlEU+zw=","ipv6_addr_in":"2a02:6ea0:cc1f:1::b61f"},{"hostname":"us-mia-wg-003","location":"us-mia","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"45.134.142.193","include_in_country":true,"weight":200,"public_key":"N/3F0QvCuiWWzCwaJmnPZO53LZrKn6sr7rItecrQSQY=","ipv6_addr_in":"2a02:6ea0:cc1f::f001"},{"hostname":"us-mia-wg-101","location":"us-mia","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.187.2","include_in_country":true,"weight":100,"public_key":"50/sEK7t3on/H2sunx+gzIjJI6E9/Y6gHOHQrvzsij4=","ipv6_addr_in":"2a0d:5600:6:104::a01f"},{"hostname":"us-mia-wg-102","location":"us-mia","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.187.66","include_in_country":true,"weight":100,"public_key":"sJw9LzH2sunqRes2FNi8l6+bd8jqFAiYFfUGTbCXlA4=","ipv6_addr_in":"2a0d:5600:6:105::f001"},{"hostname":"us-mia-wg-103","location":"us-mia","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.187.130","include_in_country":true,"weight":100,"public_key":"TpPDIhObMTeoMVx0MvSstQaIH1EfRYqW2vzGTB+ETVk=","ipv6_addr_in":"2a0d:5600:6:106::f001"},{"hostname":"us-mkc-wg-001","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.116.3","include_in_country":true,"weight":100,"public_key":"0EwMQYJ2uo6xu0C3lOEfsxMdc4NpFOURVc0JPJqkhlI=","ipv6_addr_in":"2607:9000:e00:2::f001","shadowsocks_extra_addr_in":["23.234.116.14"]},{"hostname":"us-mkc-wg-002","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.116.127","include_in_country":true,"weight":100,"public_key":"XeYfktwZerrhrXzVVEd9FQUw5MaUwv2gZmkTJWK8wSU=","ipv6_addr_in":"2607:9000:e00:3::f001","shadowsocks_extra_addr_in":["23.234.116.139"]},{"hostname":"us-mkc-wg-003","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.117.3","include_in_country":true,"weight":100,"public_key":"FOPZvXEnM1XWHH+WY17JX25N4EwJz6SSmqmAxP5y7CA=","ipv6_addr_in":"2607:9000:e00:4::f001","shadowsocks_extra_addr_in":["23.234.117.14"]},{"hostname":"us-mkc-wg-004","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.117.127","include_in_country":true,"weight":100,"public_key":"8ueNKHj+2nbTpnnv4MpxY98VrhtIKSMMwh0R9HF6iyE=","ipv6_addr_in":"2607:9000:e00:5::f001","shadowsocks_extra_addr_in":["23.234.117.139"]},{"hostname":"us-mkc-wg-005","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.118.3","include_in_country":true,"weight":100,"public_key":"KHbQz9XJVVj5M/sK3azWfgNyybdLNOjXahnHIzYYqXY=","ipv6_addr_in":"2607:9000:e00:6::f001","shadowsocks_extra_addr_in":["23.234.118.14"]},{"hostname":"us-mkc-wg-006","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.118.127","include_in_country":true,"weight":100,"public_key":"bQWDsoitERBoPnP0AXI/jCUhk4AX8cMFbhCW93wn3HM=","ipv6_addr_in":"2607:9000:e00:7::f001","shadowsocks_extra_addr_in":["23.234.118.139"]},{"hostname":"us-mkc-wg-007","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.119.3","include_in_country":true,"weight":100,"public_key":"e7BOh4K+tNWSnwUMWKI7yDCiskyamqXtpLjcg00KTn8=","ipv6_addr_in":"2607:9000:e00:8::f001","shadowsocks_extra_addr_in":["23.234.119.14"]},{"hostname":"us-mkc-wg-008","location":"us-mkc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.119.127","include_in_country":true,"weight":100,"public_key":"9PByPs4okeg0lWhPYkW7tuyw1XKy5+BKhgVuQbxSOk4=","ipv6_addr_in":"2607:9000:e00:9::f001","shadowsocks_extra_addr_in":["23.234.119.139"]},{"hostname":"us-mkc-wg-101","location":"us-mkc","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"155.2.191.3","include_in_country":true,"weight":100,"public_key":"Nk6dTIVRHBQzIZ6CUrJH7l4dItXEz3XOSBwmI933WUo=","ipv6_addr_in":"2602:fed2:7e0a::f001","shadowsocks_extra_addr_in":["155.2.191.14"]},{"hostname":"us-mkc-wg-102","location":"us-mkc","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"155.2.191.53","include_in_country":true,"weight":100,"public_key":"ie5ag1xd2x/P3fxodzB21vPbLEmqhtfqKcUs1OR0BDs=","ipv6_addr_in":"2602:fed2:7e0a::f101","shadowsocks_extra_addr_in":["155.2.191.64"]},{"hostname":"us-mkc-wg-103","location":"us-mkc","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"155.2.191.103","include_in_country":true,"weight":100,"public_key":"5dpXegNTyOFwWswBaJxViEwrXSAgC+Je9KokpT1sdjU=","ipv6_addr_in":"2602:fed2:7e0a::f201","shadowsocks_extra_addr_in":["155.2.191.114"]},{"hostname":"us-mkc-wg-104","location":"us-mkc","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"155.2.191.153","include_in_country":true,"weight":100,"public_key":"wugRpkJNlfAV2lE3p1UXsTfZtO8JYWEdA9ZMLFeF3G4=","ipv6_addr_in":"2602:fed2:7e0a::f301","shadowsocks_extra_addr_in":["155.2.191.164"]},{"hostname":"us-mkc-wg-105","location":"us-mkc","active":true,"owned":false,"provider":"hostuniversal","stboot":true,"ipv4_addr_in":"155.2.191.203","include_in_country":true,"weight":100,"public_key":"9w1yXK8tpAKZ2au6JHcu+L7TytOYmmZo9q7qfCwa1U8=","ipv6_addr_in":"2602:fed2:7e0a::f401","shadowsocks_extra_addr_in":["155.2.191.214"]},{"hostname":"us-nyc-wg-301","location":"us-nyc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"143.244.47.65","include_in_country":true,"weight":100,"public_key":"IzqkjVCdJYC1AShILfzebchTlKCqVCt/SMEXolaS3Uc=","ipv6_addr_in":"2a02:6ea0:c43f::f001"},{"hostname":"us-nyc-wg-302","location":"us-nyc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"143.244.47.78","include_in_country":true,"weight":100,"public_key":"gH/fZJwc9iLv9fazk09J/DUWT2X7/LFXijRS15e2n34=","ipv6_addr_in":"2a02:6ea0:c43f:1::f001"},{"hostname":"us-nyc-wg-303","location":"us-nyc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"143.244.47.91","include_in_country":true,"weight":1,"public_key":"KRO+RzrFV92Ah+qpHgAMKZH2jtjRlmJ4ayl0gletY3c=","ipv6_addr_in":"2a02:6ea0:c43f:2::b52f","features":{"lwo":{},"quic":{"addr_in":["143.244.47.103"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-303.blockerad.eu"}}},{"hostname":"us-nyc-wg-501","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.165.2","include_in_country":true,"weight":1,"public_key":"FMNXnFgDHNTrT9o49U8bb3Z8J90LZzVJPpRzKtJM9W8=","ipv6_addr_in":"2a0d:5600:24:2b6::f001"},{"hostname":"us-nyc-wg-502","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.165.130","include_in_country":true,"weight":100,"public_key":"cmUR4g9aIFDa5Xnp4B6Zjyp20jwgTTMgBdhcdvDV0FM=","ipv6_addr_in":"2a0d:5600:24:2b8::f001"},{"hostname":"us-nyc-wg-503","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.165.194","include_in_country":true,"weight":100,"public_key":"czE6NJ8CccA5jnJkKoZGDpMXFqSudeVTzxU5scLP/H8=","ipv6_addr_in":"2a0d:5600:24:2b9::f001"},{"hostname":"us-nyc-wg-504","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.166.130","include_in_country":true,"weight":100,"public_key":"MVa5yuoYnjXJtSCeBsyvaemuaK4KFN1p78+37Nvm2m0=","ipv6_addr_in":"2a0d:5600:24:2c2::f001"},{"hostname":"us-nyc-wg-505","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.166.194","include_in_country":true,"weight":100,"public_key":"jrjogHbVDuPxyloBldvtB51TmebNJo+4rW2JFrN33iM=","ipv6_addr_in":"2a0d:5600:24:2c3::f001"},{"hostname":"us-nyc-wg-506","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.165.66","include_in_country":true,"weight":100,"public_key":"IjdtI6sz8ZjU5tlK3eW4HAPp+GRvHErDtqxBcr8JvTM=","ipv6_addr_in":"2a0d:5600:24:2b7::f001"},{"hostname":"us-nyc-wg-601","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.185.2","include_in_country":true,"weight":1,"public_key":"OKyEPafS1lnUTWqtVeWElkTzcmkvLi9dncBHbSyFrH8=","ipv6_addr_in":"2a0d:5600:24:136a::f001","features":{"lwo":{},"quic":{"addr_in":["146.70.185.62"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-601.blockerad.eu"}}},{"hostname":"us-nyc-wg-602","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.168.130","include_in_country":true,"weight":100,"public_key":"4Lg7yQlukAMp6EX+2Ap+q4O+QIV/OEZyybtFJmN9umw=","ipv6_addr_in":"2a0d:5600:24:1378::f001"},{"hostname":"us-nyc-wg-603","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.168.66","include_in_country":true,"weight":100,"public_key":"s3N8Xeh6khECbgRYPk9pp5slw2uE0deOxa9rSJ6bzwE=","ipv6_addr_in":"2a0d:5600:24:1377::f001"},{"hostname":"us-nyc-wg-604","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.171.66","include_in_country":true,"weight":100,"public_key":"FIcFPDjxfF24xBrv+W7Bcqb2wADSWd+HAWPKYo6xZEk=","ipv6_addr_in":"2a0d:5600:24:1372::f001"},{"hostname":"us-nyc-wg-605","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.171.130","include_in_country":true,"weight":100,"public_key":"78nFhfPEjrfOxBkUf2ylM7w6upYBEcHXm93sr8CMTE4=","ipv6_addr_in":"2a0d:5600:24:1374::f001"},{"hostname":"us-nyc-wg-606","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.168.194","include_in_country":true,"weight":100,"public_key":"a8+VB6Cgah7Q5mWY860VfgU/h3Zf+pMpMdHB22e1uTQ=","ipv6_addr_in":"2a0d:5600:24:1379::f001"},{"hostname":"us-nyc-wg-701","location":"us-nyc","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.162.8.3","include_in_country":true,"weight":1,"public_key":"S3X2pCfD9X6c29fd4C6b86mEO0b01mc/WUCDN5OgyjM=","ipv6_addr_in":"2602:fa1f:1::f001","daita":true,"shadowsocks_extra_addr_in":["23.162.8.9"],"features":{"daita":{}}},{"hostname":"us-nyc-wg-702","location":"us-nyc","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.162.8.67","include_in_country":true,"weight":100,"public_key":"O81+YN0WHF4wuWRejhPG62PGK9bv/8BQTa6Ni3fomWM=","ipv6_addr_in":"2602:fa1f:1::f033","shadowsocks_extra_addr_in":["23.162.8.73"]},{"hostname":"us-nyc-wg-703","location":"us-nyc","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"23.162.8.130","include_in_country":true,"weight":1,"public_key":"Ycm86cSu1NKGpC+vZA6htq6YE9BUFk9wweE2/RySA1g=","ipv6_addr_in":"2602:fa1f:1:3::f001","shadowsocks_extra_addr_in":["23.162.8.136"]},{"hostname":"us-nyc-wg-801","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.100.3","include_in_country":true,"weight":100,"public_key":"3XVRp858LSMwQ6pA2Zo5LFGf4nIjLnuTkbXTJiNPcmo=","ipv6_addr_in":"2607:9000:a000:31::f001","daita":true,"shadowsocks_extra_addr_in":["23.234.100.14"],"features":{"daita":{},"quic":{"addr_in":["23.234.100.13"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-801.blockerad.eu"}}},{"hostname":"us-nyc-wg-802","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.100.127","include_in_country":true,"weight":100,"public_key":"SG1mcVhXNEZDZkir3GGLA7DCltIfPr71rPW6nFzW1Rc=","ipv6_addr_in":"2607:9000:a000:32::f001","daita":true,"shadowsocks_extra_addr_in":["23.234.100.139"],"features":{"daita":{},"quic":{"addr_in":["23.234.100.138"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-802.blockerad.eu"}}},{"hostname":"us-nyc-wg-803","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.101.3","include_in_country":true,"weight":100,"public_key":"X1ZCLofMRmOnoJiNUTokpTazaRrdtbdH8+yAFvyCMnM=","ipv6_addr_in":"2607:9000:a000:33::f001","daita":true,"shadowsocks_extra_addr_in":["23.234.101.14"],"features":{"daita":{},"quic":{"addr_in":["23.234.101.13"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-803.blockerad.eu"}}},{"hostname":"us-nyc-wg-804","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.101.127","include_in_country":true,"weight":100,"public_key":"uNKXjnTzZE0najF3fo5HiDBkg/fCF+anDkVuBNTCfhs=","ipv6_addr_in":"2607:9000:a000:34::f001","shadowsocks_extra_addr_in":["23.234.101.139"],"features":{"quic":{"addr_in":["23.234.101.138"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-804.blockerad.eu"}}},{"hostname":"us-nyc-wg-805","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.102.3","include_in_country":true,"weight":100,"public_key":"i97V7R9Fk5IrrbYw+k7H35i8frXOHvbES13AAoRHrWY=","ipv6_addr_in":"2607:9000:a000:35::f001","shadowsocks_extra_addr_in":["23.234.102.14"],"features":{"quic":{"addr_in":["23.234.102.13"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-805.blockerad.eu"}}},{"hostname":"us-nyc-wg-806","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.102.127","include_in_country":true,"weight":100,"public_key":"TJxsTwqSLYLKHFJLf7Uo87GiqVXnpYFaNbiiP9qwfBE=","ipv6_addr_in":"2607:9000:a000:36::f001","shadowsocks_extra_addr_in":["23.234.102.139"],"features":{"quic":{"addr_in":["23.234.102.138"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-806.blockerad.eu"}}},{"hostname":"us-nyc-wg-807","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.103.3","include_in_country":true,"weight":100,"public_key":"THimhBHKHZ2KeoTRURvpNIVir4nlaxx5NSYwqNuF1wk=","ipv6_addr_in":"2607:9000:a000:37::f001","shadowsocks_extra_addr_in":["23.234.103.14"],"features":{"quic":{"addr_in":["23.234.103.13"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-807.blockerad.eu"}}},{"hostname":"us-nyc-wg-808","location":"us-nyc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.103.127","include_in_country":true,"weight":100,"public_key":"tI+Ddqg8MZ3nqXLuvA5Kzryih//XLId7IM4IEgROiFk=","ipv6_addr_in":"2607:9000:a000:38::f001","shadowsocks_extra_addr_in":["23.234.103.139"],"features":{"quic":{"addr_in":["23.234.103.138"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-nyc-wg-808.blockerad.eu"}}},{"hostname":"us-phx-wg-201","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.88.3","include_in_country":true,"weight":100,"public_key":"8mie5kslgD63v3pZkbFmwGdj3dg5mu8Wm2Ji5kntfXA=","ipv6_addr_in":"2607:9000:700:41::f001","shadowsocks_extra_addr_in":["23.234.88.14"]},{"hostname":"us-phx-wg-202","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.88.127","include_in_country":true,"weight":100,"public_key":"I8eCMCSsFlb78N8VNaFqSJKM4Z2+3iVdlG6CvJkYEiA=","ipv6_addr_in":"2607:9000:700:42::f001","shadowsocks_extra_addr_in":["23.234.88.139"]},{"hostname":"us-phx-wg-203","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.89.3","include_in_country":true,"weight":100,"public_key":"nSeEIx++JuzqxNqLPOm2BVCXwPpR72Q3QLflDUK8tTA=","ipv6_addr_in":"2607:9000:700:43::f001","shadowsocks_extra_addr_in":["23.234.89.14"]},{"hostname":"us-phx-wg-204","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.89.127","include_in_country":true,"weight":100,"public_key":"0is4/9V/KIBWwfeuDVDlBmPa134UuV5gaFGUqI1emXY=","ipv6_addr_in":"2607:9000:700:44::f001","shadowsocks_extra_addr_in":["23.234.89.139"]},{"hostname":"us-phx-wg-205","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.90.3","include_in_country":true,"weight":100,"public_key":"sZMMs5XaQN+p0HXCK15zYV1jo7IJN7agm1Ftm7JDnnk=","ipv6_addr_in":"2607:9000:700:45::f001","shadowsocks_extra_addr_in":["23.234.90.14"]},{"hostname":"us-phx-wg-206","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.90.127","include_in_country":true,"weight":100,"public_key":"fdA7Lc9cpwb1oPy4Oa/A8sPm+RaDpW5yrdgjykde4jM=","ipv6_addr_in":"2607:9000:700:46::f001","shadowsocks_extra_addr_in":["23.234.90.139"]},{"hostname":"us-phx-wg-207","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.91.3","include_in_country":true,"weight":100,"public_key":"G5tIt92dwuvvln9nlsi/cI3Au47U94mOMgSdODRMIxs=","ipv6_addr_in":"2607:9000:700:47::f001","shadowsocks_extra_addr_in":["23.234.91.14"]},{"hostname":"us-phx-wg-208","location":"us-phx","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.91.127","include_in_country":true,"weight":100,"public_key":"4nHL/Y9efuazXJB0cx6ktdBb0L0gkITWzCFCswfCh04=","ipv6_addr_in":"2607:9000:700:48::f001","shadowsocks_extra_addr_in":["23.234.91.139"]},{"hostname":"us-qas-wg-001","location":"us-qas","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.135.34","include_in_country":false,"weight":50,"public_key":"UKNLCimke54RqRdj6UFyIuBO6nv2VVpDT3vM9N25VyI=","ipv6_addr_in":"2607:9000:9000:12::b46f"},{"hostname":"us-qas-wg-002","location":"us-qas","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.135.66","include_in_country":false,"weight":50,"public_key":"UUCBSYnGq+zEDqA6Wyse3JXv8fZuqKEgavRZTnCXlBg=","ipv6_addr_in":"2607:9000:9000:13::b47f"},{"hostname":"us-qas-wg-003","location":"us-qas","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.135.98","include_in_country":false,"weight":50,"public_key":"0s0NdIzo+pq0OiHstZHqapYsdevGQGopQ5NM54g/9jo=","ipv6_addr_in":"2607:9000:9000:14::b48f"},{"hostname":"us-qas-wg-004","location":"us-qas","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"198.54.135.130","include_in_country":false,"weight":50,"public_key":"TvqnL6VkJbz0KrjtHnUYWvA7zRt9ysI64LjTOx2vmm4=","ipv6_addr_in":"2607:9000:9000:15::b49f"},{"hostname":"us-qas-wg-101","location":"us-qas","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.156.46.130","include_in_country":true,"weight":100,"public_key":"JEuuPzZE8uE53OFhd3YFiZuwwANLqwmdXWMHPUbBwnk=","ipv6_addr_in":"2a02:6ea0:e206:1::a01f"},{"hostname":"us-qas-wg-102","location":"us-qas","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.156.46.143","include_in_country":true,"weight":100,"public_key":"5hlEb3AjTzVIJyYWCYvJvbgA4p25Ltfp2cYnys90LQ0=","ipv6_addr_in":"2a02:6ea0:e206:2::a02f"},{"hostname":"us-qas-wg-103","location":"us-qas","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"185.156.46.156","include_in_country":true,"weight":100,"public_key":"oD9IFZsA5sync37K/sekVXaww76MwA3IvDRpR/irZWQ=","ipv6_addr_in":"2a02:6ea0:e206:3::a03f"},{"hostname":"us-qas-wg-201","location":"us-qas","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.81.230.3","include_in_country":true,"weight":100,"public_key":"no0QE15NRHLECYe/B976IH9mLn22QecYBbcYl3LZhD0=","ipv6_addr_in":"2a01:4740:1::f001","shadowsocks_extra_addr_in":["103.81.230.14"]},{"hostname":"us-qas-wg-202","location":"us-qas","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.81.230.127","include_in_country":true,"weight":100,"public_key":"eUTgjAPvpLba9wwMcC0HNcLwC2Q42QYCKZuFPy34+Ug=","ipv6_addr_in":"2a01:4740:1::f101","shadowsocks_extra_addr_in":["103.81.230.139"]},{"hostname":"us-qas-wg-203","location":"us-qas","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.81.231.3","include_in_country":true,"weight":100,"public_key":"tI5F6pIZMEf0aJUTG/I2ZvYkkJDJpOxVakObTLLmBAI=","ipv6_addr_in":"2a01:4740:1::f201","shadowsocks_extra_addr_in":["103.81.231.14"]},{"hostname":"us-qas-wg-204","location":"us-qas","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"103.81.231.127","include_in_country":true,"weight":100,"public_key":"2bvhh22EcdV3MIuIJze47gD2KmXpplYrNbCjNff2ID8=","ipv6_addr_in":"2a01:4740:1::f301","shadowsocks_extra_addr_in":["103.81.231.139"]},{"hostname":"us-rag-wg-201","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.76.2","include_in_country":true,"weight":100,"public_key":"MuKjekVqBwpSizHLNwVRl4b8bwi6aTCBOshPiOOWrEQ=","ipv6_addr_in":"2607:9000:4000:31::f001","shadowsocks_extra_addr_in":["23.234.76.13"]},{"hostname":"us-rag-wg-202","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.76.127","include_in_country":true,"weight":100,"public_key":"T2diUJ97txooCDntCrB6Q29Qe0fm/hMdZDzdc9uOUgQ=","ipv6_addr_in":"2607:9000:4000:32::f001","shadowsocks_extra_addr_in":["23.234.76.138"]},{"hostname":"us-rag-wg-203","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.77.2","include_in_country":true,"weight":100,"public_key":"4BioSTLTYH1qL/oYGY/z5IZ049I7oSzs5IKoFZzrgn0=","ipv6_addr_in":"2607:9000:4000:33::f001","shadowsocks_extra_addr_in":["23.234.77.13"]},{"hostname":"us-rag-wg-204","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.77.127","include_in_country":true,"weight":100,"public_key":"Tk5lPM5K5qrXPWDktHH+AvcxC+UxhGSX6aILsPi33zU=","ipv6_addr_in":"2607:9000:4000:34::f001","shadowsocks_extra_addr_in":["23.234.77.138"]},{"hostname":"us-rag-wg-205","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.78.2","include_in_country":true,"weight":100,"public_key":"z7vhWZ1oY+UkE7PoXF/QtofOhTNGnNfoP20al/cniyc=","ipv6_addr_in":"2607:9000:4000:35::f001","shadowsocks_extra_addr_in":["23.234.78.13"]},{"hostname":"us-rag-wg-206","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.78.127","include_in_country":true,"weight":100,"public_key":"ekRrcTqihriWz4TldL2deIEbHlqwytL3pu1WV+v7zjw=","ipv6_addr_in":"2607:9000:4000:36::f001","shadowsocks_extra_addr_in":["23.234.78.138"]},{"hostname":"us-rag-wg-207","location":"us-rag","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.79.2","include_in_country":true,"weight":100,"public_key":"Y16tMAXHpCEExSZJ8AL5LfskKqPqIrZWeLFbSLE/piE=","ipv6_addr_in":"2607:9000:4000:37::f001","shadowsocks_extra_addr_in":["23.234.79.13"]},{"hostname":"us-rag-wg-208","location":"us-rag","active":false,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.79.127","include_in_country":true,"weight":100,"public_key":"lCyIXwxSGEBSpUah0kYSyuaZuDJJB0Cwia7gv4r7XTA=","ipv6_addr_in":"2607:9000:4000:38::f001","shadowsocks_extra_addr_in":["23.234.79.138"]},{"hostname":"us-sea-wg-001","location":"us-sea","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.43.91","include_in_country":true,"weight":100,"public_key":"bZQF7VRDRK/JUJ8L6EFzF/zRw2tsqMRk6FesGtTgsC0=","ipv6_addr_in":"2a02:6ea0:d80b:3::b75f"},{"hostname":"us-sea-wg-002","location":"us-sea","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.43.78","include_in_country":true,"weight":100,"public_key":"Xt80FGN9eLy1vX3F29huj6oW2MnQt7ne3DMBpo525Qw=","ipv6_addr_in":"2a02:6ea0:d80b:2::f001"},{"hostname":"us-sea-wg-003","location":"us-sea","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"138.199.43.65","include_in_country":true,"weight":100,"public_key":"4ke8ZSsroiI6Sp23OBbMAU6yQmdF3xU2N8CyzQXE/Qw=","ipv6_addr_in":"2a02:6ea0:d80b:1::b73f"},{"hostname":"us-sea-wg-401","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.80.2","include_in_country":true,"weight":100,"public_key":"wRvkGNE3N2UklxKajU06gbBJ3Bg7KmhZsU7a5HIFBw8=","ipv6_addr_in":"2607:9000:5000:31::f001","shadowsocks_extra_addr_in":["23.234.80.13"],"features":{"quic":{"addr_in":["23.234.80.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-401.blockerad.eu"}}},{"hostname":"us-sea-wg-402","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.80.127","include_in_country":true,"weight":100,"public_key":"NBnpCxDrc0tdX91KUm5cEmQv7BSMOZqd7dS/d7piQl0=","ipv6_addr_in":"2607:9000:5000:32::f001","shadowsocks_extra_addr_in":["23.234.80.138"],"features":{"quic":{"addr_in":["23.234.80.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-402.blockerad.eu"}}},{"hostname":"us-sea-wg-403","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.81.2","include_in_country":true,"weight":100,"public_key":"cJ8317JqMtNDvxvd/8z29lWurK/3sb5nFZuOY5mw3ys=","ipv6_addr_in":"2607:9000:5000:33::f001","shadowsocks_extra_addr_in":["23.234.81.13"],"features":{"quic":{"addr_in":["23.234.81.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-403.blockerad.eu"}}},{"hostname":"us-sea-wg-404","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.81.127","include_in_country":true,"weight":100,"public_key":"G6+A375GVmuFCAtvwgx3SWCWhrMvdQ+cboXQ8zp2ang=","ipv6_addr_in":"2607:9000:5000:34::f001","shadowsocks_extra_addr_in":["23.234.81.138"],"features":{"quic":{"addr_in":["23.234.81.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-404.blockerad.eu"}}},{"hostname":"us-sea-wg-405","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.82.2","include_in_country":true,"weight":100,"public_key":"X+efE4ntYuAEHBHU32SBMq/U0lAFEKeX5/nl3CKtrVM=","ipv6_addr_in":"2607:9000:5000:35::f001","shadowsocks_extra_addr_in":["23.234.82.13"],"features":{"quic":{"addr_in":["23.234.82.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-405.blockerad.eu"}}},{"hostname":"us-sea-wg-406","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.82.127","include_in_country":true,"weight":100,"public_key":"kT695K8pTGd+I6Q4a4URU2AdXN2VAtHyi7kNSRjUEiw=","ipv6_addr_in":"2607:9000:5000:36::f001","shadowsocks_extra_addr_in":["23.234.82.138"],"features":{"quic":{"addr_in":["23.234.82.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-406.blockerad.eu"}}},{"hostname":"us-sea-wg-407","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.83.2","include_in_country":true,"weight":100,"public_key":"HrhtkMqLmKtpAHiUIw7uLHwt48mDlhyLOt4+1kpNj3Y=","ipv6_addr_in":"2607:9000:5000:37::f001","shadowsocks_extra_addr_in":["23.234.83.13"],"features":{"quic":{"addr_in":["23.234.83.12"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-407.blockerad.eu"}}},{"hostname":"us-sea-wg-408","location":"us-sea","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.83.127","include_in_country":true,"weight":100,"public_key":"tfhYXF12+7tB6bEOhqZ7eMODDv08fDMnQSBTmlau9VI=","ipv6_addr_in":"2607:9000:5000:38::f001","shadowsocks_extra_addr_in":["23.234.83.138"],"features":{"quic":{"addr_in":["23.234.83.137"],"token":"d18a23fc-7d5e-4fd2-8372-5e0e73de741a","domain":"us-sea-wg-408.blockerad.eu"}}},{"hostname":"us-sjc-wg-302","location":"us-sjc","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"142.147.89.210","include_in_country":true,"weight":40,"public_key":"8wVb4HUgmpQEa5a1Q8Ff1hTDTJVaHts487bksJVugEo=","ipv6_addr_in":"2604:e8c0:7::f001"},{"hostname":"us-sjc-wg-303","location":"us-sjc","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"142.147.89.225","include_in_country":true,"weight":40,"public_key":"2ZQTRk/3jT+ccfG3G/QoJV3NFC4CFHQwGBCSokOvBnA=","ipv6_addr_in":"2604:e8c0:7::b68f"},{"hostname":"us-sjc-wg-401","location":"us-sjc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.217.34","include_in_country":true,"weight":100,"public_key":"2q0LGwWvnV2qbNEAgOOHh4tvol5vGeQXJZDAbazCSBY=","ipv6_addr_in":"2a02:6ea0:e611::f001"},{"hostname":"us-sjc-wg-402","location":"us-sjc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.217.47","include_in_country":true,"weight":100,"public_key":"+UZsgTzYTdG3LvqpL+V9ZkwEMiFcls32YlpuI0cqDQ4=","ipv6_addr_in":"2a02:6ea0:e611:1::f001"},{"hostname":"us-sjc-wg-501","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.92.3","include_in_country":true,"weight":100,"public_key":"wbK8/hP/ZMr972OtanZxugSqUVt/sM/G9rnTiQ5YbSw=","ipv6_addr_in":"2607:9000:800:31::f001","shadowsocks_extra_addr_in":["23.234.92.14"]},{"hostname":"us-sjc-wg-502","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.92.127","include_in_country":true,"weight":100,"public_key":"dCHtOy+ieOOMzgpZPr557dXygpmlj9RRNCAvUQvXj3I=","ipv6_addr_in":"2607:9000:800:32::f001","shadowsocks_extra_addr_in":["23.234.92.139"]},{"hostname":"us-sjc-wg-503","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.93.3","include_in_country":true,"weight":100,"public_key":"HqWGqZbCgLxYLGHQG7P3jHYWQgc5p6ImqaxgqA97WCY=","ipv6_addr_in":"2607:9000:800:33::f001","shadowsocks_extra_addr_in":["23.234.93.14"]},{"hostname":"us-sjc-wg-504","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.93.127","include_in_country":true,"weight":100,"public_key":"hQ/UqXflXztKM2T39HmQRPpPjWP8I2r9FMqnqFWy53M=","ipv6_addr_in":"2607:9000:800:34::f001","shadowsocks_extra_addr_in":["23.234.93.139"]},{"hostname":"us-sjc-wg-505","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.94.3","include_in_country":true,"weight":100,"public_key":"v0CXmb+wIy1P+IbFA/1nYTB3KoDzFyW5k7n8vXoTxiY=","ipv6_addr_in":"2607:9000:800:35::f001","shadowsocks_extra_addr_in":["23.234.94.14"]},{"hostname":"us-sjc-wg-506","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.94.127","include_in_country":true,"weight":100,"public_key":"sjWKL/W2+21cyjEBjtMd4TQQlWTsLTUN4skYOF7YgnU=","ipv6_addr_in":"2607:9000:800:36::f001","shadowsocks_extra_addr_in":["23.234.94.139"]},{"hostname":"us-sjc-wg-507","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.95.3","include_in_country":true,"weight":100,"public_key":"IKhrTUlWJXlcH30jNV82mlWlj6NEre3PZffJ7MoT0zc=","ipv6_addr_in":"2607:9000:800:37::f001","shadowsocks_extra_addr_in":["23.234.95.14"]},{"hostname":"us-sjc-wg-508","location":"us-sjc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.95.127","include_in_country":true,"weight":100,"public_key":"rIqRJ1o3UxvuwXj9B7VDquiTZ8BtQdab4wsb4F1L7l8=","ipv6_addr_in":"2607:9000:800:38::f001","shadowsocks_extra_addr_in":["23.234.95.139"]},{"hostname":"us-slc-wg-201","location":"us-slc","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.9","include_in_country":true,"weight":100,"public_key":"sSoow0tFfqSrZIUhFRaGsTvwQsUTe33RA/9PLn93Cno=","ipv6_addr_in":"2607:fc98:0:8a::f301"},{"hostname":"us-slc-wg-202","location":"us-slc","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.149","include_in_country":true,"weight":100,"public_key":"mKD4untTerTbg+1pJh3FA9zjOAOtoTHqOJzIP0lnqH4=","ipv6_addr_in":"2607:fc98:0:8a::f401"},{"hostname":"us-slc-wg-203","location":"us-slc","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.131","include_in_country":true,"weight":100,"public_key":"2yVEeOFScneJRCVTrqCjKlKHg3J2wwOwkY28iy47J1Q=","ipv6_addr_in":"2607:fc98:0:8a::f501"},{"hostname":"us-slc-wg-204","location":"us-slc","active":true,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.10","include_in_country":true,"weight":100,"public_key":"SE7HGeByhTo8Ak7FGsjvrYOUJTydQ2L8fWjo17IvhSw=","ipv6_addr_in":"2607:fc98:0:8a::f601"},{"hostname":"us-slc-wg-301","location":"us-slc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.112.3","include_in_country":true,"weight":100,"public_key":"tA/k8ouzYiVmgCRCF7TWibVzv5xRp3cgv9TJX66poGE=","ipv6_addr_in":"2607:9000:d00:2::f001","shadowsocks_extra_addr_in":["23.234.112.14"]},{"hostname":"us-slc-wg-302","location":"us-slc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.112.127","include_in_country":true,"weight":100,"public_key":"6hF/LLVCkxlW+mH6wFob5ZiDZNu2DqfEj82w8wzVnHY=","ipv6_addr_in":"2607:9000:d00:3::f001","shadowsocks_extra_addr_in":["23.234.112.139"]},{"hostname":"us-slc-wg-303","location":"us-slc","active":false,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.113.3","include_in_country":true,"weight":100,"public_key":"Taa3TPPNUUcD3upveDHqpi9uaNHbeXzh6kX+sQUcM0s=","ipv6_addr_in":"2607:9000:d00:4::f001","shadowsocks_extra_addr_in":["23.234.113.14"]},{"hostname":"us-slc-wg-304","location":"us-slc","active":false,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.113.127","include_in_country":true,"weight":100,"public_key":"+ru/ZmhgTYWNLyn3ZWuC57PMOU64yMfuWPZ5ow0XY3A=","ipv6_addr_in":"2607:9000:d00:5::f001","shadowsocks_extra_addr_in":["23.234.113.139"]},{"hostname":"us-slc-wg-305","location":"us-slc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.114.3","include_in_country":true,"weight":100,"public_key":"QkiwUWRNNtI+8YSK0SPCq6KeKPSohpX/8FbFZkx+uHg=","ipv6_addr_in":"2607:9000:d00:6::f001","shadowsocks_extra_addr_in":["23.234.114.14"]},{"hostname":"us-slc-wg-306","location":"us-slc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.114.127","include_in_country":true,"weight":100,"public_key":"Gv4GZOT46WQsTOAH4mYlauwFRxtpCX08BI0bzot5Piw=","ipv6_addr_in":"2607:9000:d00:7::f001","shadowsocks_extra_addr_in":["23.234.114.139"]},{"hostname":"us-slc-wg-307","location":"us-slc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.115.3","include_in_country":true,"weight":100,"public_key":"6pJrsejyhJLRotKdS+RCZez28/WqlOw6qEs1BbjQVSI=","ipv6_addr_in":"2607:9000:d00:8::f001","shadowsocks_extra_addr_in":["23.234.115.14"]},{"hostname":"us-slc-wg-308","location":"us-slc","active":true,"owned":false,"provider":"Tzulo","stboot":true,"ipv4_addr_in":"23.234.115.127","include_in_country":true,"weight":100,"public_key":"f7xT56F8RP9XDXW7UCxBcjgP+SAGyH+y2OZ4fieR4X8=","ipv6_addr_in":"2607:9000:d00:9::f001","shadowsocks_extra_addr_in":["23.234.115.139"]},{"hostname":"us-txc-wg-001","location":"us-txc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.222.194","include_in_country":true,"weight":100,"public_key":"+OCONjBoN5RytiPy000VOzhZsiu1tSzecmc1hl/q8hI=","ipv6_addr_in":"2a02:6ea0:fe00:1::f001"},{"hostname":"us-txc-wg-002","location":"us-txc","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"79.127.222.207","include_in_country":true,"weight":100,"public_key":"mjv8qVNwhVKO0ePAI97CRil188uwdR/VR6ihcNY/hio=","ipv6_addr_in":"2a02:6ea0:fe00:2::f001"},{"hostname":"us-uyk-wg-201","location":"us-uyk","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"104.36.50.3","include_in_country":true,"weight":100,"public_key":"utm9eFUk8tH0j/dwKLJvlb6BRSfm3GZbomxr52ZDGn0=","ipv6_addr_in":"2a06:3040:12:620::f001","shadowsocks_extra_addr_in":["104.36.50.5"]},{"hostname":"us-uyk-wg-202","location":"us-uyk","active":true,"owned":false,"provider":"HostRoyale","stboot":true,"ipv4_addr_in":"104.36.50.33","include_in_country":true,"weight":100,"public_key":"8Rh2Qc+vXTREhJb/RfCcpXS13U9xSqy4Pnw4+Wwt7iE=","ipv6_addr_in":"2a06:3040:12:620::f101","shadowsocks_extra_addr_in":["104.36.50.35"]},{"hostname":"us-was-wg-001","location":"us-was","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"185.213.193.3","include_in_country":true,"weight":100,"public_key":"qD3AH8vI8MhEVc9+0+2O8zV0Gx9FfKdy7ri3Bnpzo10=","ipv6_addr_in":"2604:980:1002:11::f001","shadowsocks_extra_addr_in":["185.213.193.14"]},{"hostname":"us-was-wg-002","location":"us-was","active":true,"owned":false,"provider":"Zenlayer","stboot":true,"ipv4_addr_in":"185.213.193.127","include_in_country":true,"weight":100,"public_key":"2AvJGG4MJfnJMRSR6kcha9FZMMkhJM/AtktI5DSESSI=","ipv6_addr_in":"2604:980:1002:11::f101","shadowsocks_extra_addr_in":["185.213.193.139"]},{"hostname":"za-jnb-wg-001","location":"za-jnb","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"154.47.30.130","include_in_country":true,"weight":100,"public_key":"5dOGXJ9JK/Bul0q57jsuvjNnc15gRpSO1rMbxkf4J2M=","ipv6_addr_in":"2a02:6ea0:f206::f001"},{"hostname":"za-jnb-wg-002","location":"za-jnb","active":true,"owned":false,"provider":"DataPacket","stboot":true,"ipv4_addr_in":"154.47.30.143","include_in_country":true,"weight":100,"public_key":"lTq6+yUYfYsXwBpj/u3LnYqpLhW8ZJXQQ19N/ybP2B8=","ipv6_addr_in":"2a02:6ea0:f207::f001"}],"port_ranges":[[53,53],[123,123],[443,443],[4000,33433],[33565,51820],[52001,60000]],"shadowsocks_port_ranges":[[51900,51949]],"ipv4_gateway":"10.64.0.1","ipv6_gateway":"fc00:bbbb:bbbb:bb01::1"},"bridge":{"relays":[{"hostname":"au-syd-br-001","location":"au-syd","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.141.154","include_in_country":true,"weight":100},{"hostname":"ca-mtr-br-001","location":"ca-mtr","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"217.138.213.18","include_in_country":true,"weight":100},{"hostname":"ch-zrh-br-001","location":"ch-zrh","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.32.127.117","include_in_country":true,"weight":1},{"hostname":"cz-prg-br-101","location":"cz-prg","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"217.138.199.106","include_in_country":true,"weight":100},{"hostname":"de-fra-br-001","location":"de-fra","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.155.117","include_in_country":true,"weight":100},{"hostname":"fi-hel-br-101","location":"fi-hel","active":true,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"193.138.7.132","include_in_country":true,"weight":100},{"hostname":"gb-lon-br-001","location":"gb-lon","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"141.98.252.66","include_in_country":true,"weight":100},{"hostname":"gb-mnc-br-001","location":"gb-mnc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"89.238.134.58","include_in_country":false,"weight":100},{"hostname":"hk-hkg-br-201","location":"hk-hkg","active":false,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"103.125.233.210","include_in_country":true,"weight":100},{"hostname":"jp-tyo-br-201","location":"jp-tyo","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"185.242.4.34","include_in_country":true,"weight":100},{"hostname":"nl-ams-br-001","location":"nl-ams","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.134.116","include_in_country":true,"weight":100},{"hostname":"no-svg-br-001","location":"no-svg","active":false,"owned":true,"provider":"Blix","stboot":true,"ipv4_addr_in":"194.127.199.245","include_in_country":true,"weight":100},{"hostname":"se-got-br-001","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.213.154.117","include_in_country":true,"weight":100},{"hostname":"se-mma-br-001","location":"se-mma","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"193.138.218.71","include_in_country":true,"weight":100},{"hostname":"se-sto-br-001","location":"se-sto","active":false,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"185.65.135.115","include_in_country":true,"weight":100},{"hostname":"sg-sin-br-101","location":"sg-sin","active":false,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.192.38","include_in_country":true,"weight":100},{"hostname":"us-lax-br-401","location":"us-lax","active":false,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"62.133.44.202","include_in_country":false,"weight":100},{"hostname":"us-mia-br-101","location":"us-mia","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"146.70.183.34","include_in_country":true,"weight":100},{"hostname":"us-nyc-br-501","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"212.103.48.226","include_in_country":true,"weight":100},{"hostname":"us-nyc-br-601","location":"us-nyc","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"38.132.121.146","include_in_country":true,"weight":100},{"hostname":"us-slc-br-201","location":"us-slc","active":false,"owned":false,"provider":"100TB","stboot":true,"ipv4_addr_in":"69.4.234.8","include_in_country":false,"weight":100}],"shadowsocks":[{"protocol":"tcp","port":443,"cipher":"aes-256-gcm","password":"mullvad"},{"protocol":"udp","port":1234,"cipher":"aes-256-cfb","password":"mullvad"},{"protocol":"udp","port":1236,"cipher":"aes-256-gcm","password":"mullvad"}]}}
\ No newline at end of file +{"locations":{"ie-dub":{"country":"Ireland","city":"Dublin","latitude":53.35014,"longitude":-6.266155},"de-fra":{"country":"Germany","city":"Frankfurt","latitude":50.110924,"longitude":8.682127},"se-got":{"country":"Sweden","city":"Gothenburg","latitude":57.70887,"longitude":11.97456},"aa-rsw":{"country":"Relay Software Country","city":"Relay Software city","latitude":0.0,"longitude":0.0},"se-sto":{"country":"Sweden","city":"Stockholm","latitude":59.3289,"longitude":18.0649}},"openvpn":{"relays":[{"hostname":"ie-dub-ovpn-001","location":"ie-dub","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"85.203.53.227","include_in_country":true,"weight":100},{"hostname":"se-got-ovpn-001","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"85.203.53.159","include_in_country":true,"weight":100},{"hostname":"se-sto-ovpn-001","location":"se-sto","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"85.203.53.225","include_in_country":true,"weight":100}],"ports":[{"port":1194,"protocol":"udp"},{"port":1195,"protocol":"udp"},{"port":1196,"protocol":"udp"},{"port":1197,"protocol":"udp"},{"port":1300,"protocol":"udp"},{"port":1301,"protocol":"udp"},{"port":1302,"protocol":"udp"},{"port":443,"protocol":"tcp"},{"port":80,"protocol":"tcp"}]},"wireguard":{"relays":[{"hostname":"de-fra-wg-001","location":"de-fra","active":true,"owned":false,"provider":"xtom","stboot":true,"ipv4_addr_in":"85.203.53.104","include_in_country":true,"weight":100,"public_key":"9NuXfdBjkHkVy5IdQN+8wMNS7CiFC3n+VRdFsPzgmVM=","ipv6_addr_in":"2a03:1b20:5:3::104","daita":true,"features":{"daita":{},"lwo":{},"quic":{"addr_in":["85.203.53.108","2a03:1b20:5:3::108"],"token":"28234bf5-c4ec-4f28-8975-7d7dc5d537c9","domain":"de-fra-wg-001.relays.stagemole.eu"}}},{"hostname":"ie-dub-wg-001","location":"ie-dub","active":true,"owned":false,"provider":"M247","stboot":true,"ipv4_addr_in":"85.203.53.102","include_in_country":true,"weight":100,"public_key":"PeXs6GHjC4yKfo+1giEN6gGDkae7wTo8hdZFT1kV3Ho=","ipv6_addr_in":"2a03:1b20:5:3::102","features":{"lwo":{},"quic":{"addr_in":["85.203.53.102","2a03:1b20:5:3::102"],"token":"28234bf5-c4ec-4f28-8975-7d7dc5d537c9","domain":"ie-dub-wg-001.relays.stagemole.eu"}}},{"hostname":"se-got-wg-001","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"85.203.53.140","include_in_country":true,"weight":100,"public_key":"ZCvlPfPzOf728BIQcSmWzGFuInKK0SdVTyTCZkdrvUk=","ipv6_addr_in":"2a03:1b20:5:3::140","features":{"lwo":{},"quic":{"addr_in":["2a03:1b20:5:3::14ff"],"token":"28234bf5-c4ec-4f28-8975-7d7dc5d537c9","domain":"se-got-wg-001.relays.stagemole.eu"}}},{"hostname":"se-got-wg-002","location":"aa-rsw","active":true,"owned":true,"provider":"RelaySoftwareTeam","stboot":true,"ipv4_addr_in":"85.203.53.145","include_in_country":true,"weight":0,"public_key":"IyER1oEmmuiijmyjI2D4ihrDuButvK4B00h5Z3+0nRM=","ipv6_addr_in":"2a03:1b20:5:3::145","daita":true,"features":{"daita":{},"lwo":{}}},{"hostname":"se-got-wg-003","location":"aa-rsw","active":true,"owned":true,"provider":"RelaySoftwareTeam","stboot":true,"ipv4_addr_in":"85.203.53.147","include_in_country":true,"weight":0,"public_key":"f6A7xEIcAYhpxNgf2KPj76zlaU/ebqYewmmoIHL+ABQ=","ipv6_addr_in":"2a03:1b20:5:3::147","daita":true,"features":{"daita":{},"lwo":{}}},{"hostname":"se-got-wg-004","location":"aa-rsw","active":true,"owned":true,"provider":"RelaySoftwareTeam","stboot":true,"ipv4_addr_in":"85.203.53.231","include_in_country":true,"weight":0,"public_key":"VYtjgSxNzWi9uRaMlMilxjWeuBVQqdTguamP+Fcjj2o=","ipv6_addr_in":"2a03:1b20:5:3::231","daita":true,"features":{"daita":{}}},{"hostname":"se-got-wg-005","location":"se-got","active":true,"owned":true,"provider":"31173","stboot":true,"ipv4_addr_in":"85.203.53.222","include_in_country":true,"weight":100,"public_key":"czbIog4ERYNb8MkMAZmHZ6dC4Eg7tOAjqgJUgxd9Nnk=","ipv6_addr_in":"2a03:1b20:5:3::222","features":{"lwo":{}}},{"hostname":"se-sto-wg-001","location":"se-sto","active":true,"owned":true,"provider":"Mullvad","stboot":true,"ipv4_addr_in":"85.203.53.81","include_in_country":true,"weight":100,"public_key":"2KS+F8ZAOUSMwygl2CYqkqFhbi3L5u58b3kIpaylaEk=","ipv6_addr_in":"2a03:1b20:5:3::81","features":{"lwo":{},"quic":{"addr_in":["85.203.53.81","2a03:1b20:5:3::81"],"token":"28234bf5-c4ec-4f28-8975-7d7dc5d537c9","domain":"se-sto-wg-001.relays.stagemole.eu"}}}],"port_ranges":[[53,53],[123,123],[4000,33433],[33565,51820],[52001,60000]],"shadowsocks_port_ranges":[[51900,51949]],"ipv4_gateway":"10.64.0.1","ipv6_gateway":"fc00:bbbb:bbbb:bb01::1"},"bridge":{"relays":[{"hostname":"se-got-br-001","location":"se-got","active":true,"owned":true,"provider":"Mullvad","stboot":true,"ipv4_addr_in":"85.203.53.200","include_in_country":true,"weight":100}],"shadowsocks":[{"protocol":"tcp","port":443,"cipher":"aes-256-gcm","password":"mullvad"},{"protocol":"udp","port":1234,"cipher":"aes-256-cfb","password":"mullvad"},{"protocol":"udp","port":1236,"cipher":"aes-256-gcm","password":"mullvad"}]}}
\ No newline at end of file diff --git a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift index b48095abf5..eecb62bed1 100644 --- a/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift +++ b/ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift @@ -23,9 +23,9 @@ public protocol APIQuerying: Sendable { completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.ServerRelaysCacheResponse> ) -> Cancellable - func legacyStorekitPayment( + func legacyStoreKitPayment( accountNumber: String, - request: LegacyStorekitRequest, + request: LegacyStoreKitRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.CreateApplePaymentResponse> ) -> Cancellable @@ -43,15 +43,14 @@ public protocol APIQuerying: Sendable { completionHandler: @escaping @Sendable ProxyCompletionHandler<REST.SubmitVoucherResponse> ) -> Cancellable - func initStorekitPayment( + func initStoreKitPayment( accountNumber: String, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping @Sendable ProxyCompletionHandler<String> + completionHandler: @escaping @Sendable ProxyCompletionHandler<UUID> ) -> Cancellable - func checkStorekitPayment( - accountNumber: String, - transaction: StorekitTransaction, + func checkStoreKitPayment( + transaction: StoreKitTransaction, retryStrategy: REST.RetryStrategy, completionHandler: @escaping @Sendable ProxyCompletionHandler<Void> ) -> Cancellable @@ -169,9 +168,9 @@ extension REST { } } - public func legacyStorekitPayment( + public func legacyStoreKitPayment( accountNumber: String, - request: LegacyStorekitRequest, + request: LegacyStoreKitRequest, retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<REST.CreateApplePaymentResponse> ) -> Cancellable { @@ -208,13 +207,13 @@ extension REST { ) } - public func initStorekitPayment( + public func initStoreKitPayment( accountNumber: String, retryStrategy: REST.RetryStrategy, - completionHandler: @escaping ProxyCompletionHandler<String> + completionHandler: @escaping ProxyCompletionHandler<UUID> ) -> Cancellable { struct InitStorekitPaymentResponse: Codable { - let paymentToken: String + let paymentToken: UUID } let responseHandler = rustResponseHandler( @@ -230,9 +229,8 @@ extension REST { ) } - public func checkStorekitPayment( - accountNumber: String, - transaction: StorekitTransaction, + public func checkStoreKitPayment( + transaction: StoreKitTransaction, retryStrategy: REST.RetryStrategy, completionHandler: @escaping ProxyCompletionHandler<Void> ) -> Cancellable { @@ -242,7 +240,6 @@ extension REST { request: .checkStorekitPayment( retryStrategy: retryStrategy, - accountNumber: accountNumber, transaction: transaction ), responseHandler: responseHandler, diff --git a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift index 65ce546b16..cb4bacc948 100644 --- a/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift +++ b/ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift @@ -22,14 +22,10 @@ public enum APIRequest: Codable, Sendable { case legacyStorekitPayment( retryStrategy: REST.RetryStrategy, accountNumber: String, - request: LegacyStorekitRequest + request: LegacyStoreKitRequest ) case initStorekitPayment(retryStrategy: REST.RetryStrategy, accountNumber: String) - case checkStorekitPayment( - retryStrategy: REST.RetryStrategy, - accountNumber: String, - transaction: StorekitTransaction - ) + case checkStorekitPayment(retryStrategy: REST.RetryStrategy, transaction: StoreKitTransaction) // Device Proxy case getDevice(_ retryStrategy: REST.RetryStrategy, accountNumber: String, identifier: String) @@ -93,7 +89,7 @@ public enum APIRequest: Codable, Sendable { let .rotateDeviceKey(strategy, _, _, _), let .legacyStorekitPayment(strategy, _, _), let .initStorekitPayment(strategy, _), - let .checkStorekitPayment(strategy, _, _), + let .checkStorekitPayment(strategy, _), let .checkApiAvailability(strategy, _): strategy } diff --git a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift index 4ef1b14acd..0b80867b97 100644 --- a/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift +++ b/ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift @@ -160,7 +160,6 @@ public struct MullvadApiRequestFactory: Sendable { )) case let .checkStorekitPayment( retryStrategy: retryStrategy, - accountNumber: accountNumber, transaction: transaction ): let body = try encoder.encode(transaction) @@ -169,7 +168,6 @@ public struct MullvadApiRequestFactory: Sendable { apiContext.context, rawCompletionPointer, retryStrategy.toRustStrategy(), - accountNumber, body.map { $0 }, UInt(body.count) )) diff --git a/ios/MullvadREST/RetryStrategy/RetryStrategy.swift b/ios/MullvadREST/RetryStrategy/RetryStrategy.swift index b1bdfeafb3..4ccfb088cd 100644 --- a/ios/MullvadREST/RetryStrategy/RetryStrategy.swift +++ b/ios/MullvadREST/RetryStrategy/RetryStrategy.swift @@ -72,24 +72,17 @@ extension REST { /// Strategy configured with 2 retry attempts and exponential backoff. public static let `default` = RetryStrategy( maxRetryCount: 2, - delay: defaultRetryDelay, + delay: .default, applyJitter: true ) /// Strategy configured with 10 retry attempts and exponential backoff. public static let aggressive = RetryStrategy( maxRetryCount: 10, - delay: defaultRetryDelay, + delay: .default, applyJitter: true ) - /// Default retry delay. - public static let defaultRetryDelay: RetryDelay = .exponentialBackoff( - initial: .seconds(2), - multiplier: 2, - maxDelay: .seconds(8) - ) - public static let postQuantumKeyExchange = RetryStrategy( maxRetryCount: 10, delay: .exponentialBackoff( @@ -109,6 +102,12 @@ extension REST { ), applyJitter: true ) + + public static let purchaseReceiptUpload = RetryStrategy( + maxRetryCount: 3, + delay: .default, + applyJitter: true + ) } public enum RetryDelay: Codable, Equatable, Sendable { @@ -142,6 +141,13 @@ extension REST { )) } } + + /// Default retry delay. + public static let `default`: RetryDelay = .exponentialBackoff( + initial: .seconds(2), + multiplier: 2, + maxDelay: .seconds(8) + ) } public struct CodableDuration: Codable, Equatable, Sendable { diff --git a/ios/MullvadRustRuntime/EncryptedDNSProxy.swift b/ios/MullvadRustRuntime/EncryptedDNSProxy.swift deleted file mode 100644 index b5929b5ba4..0000000000 --- a/ios/MullvadRustRuntime/EncryptedDNSProxy.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// EncryptedDNSProxy.swift -// MullvadRustRuntime -// -// Created by Emils on 24/09/2024. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadRustRuntimeProxy - -public enum EncryptedDnsProxyError: Error { - case start(err: Int32) -} - -public class EncryptedDNSProxy { - private var proxyConfig: ProxyHandle - private var stateLock = NSLock() - private var didStart = false - private let state: OpaquePointer - private let domain: String - - public init(domain: String) { - self.domain = domain - state = encrypted_dns_proxy_init(domain) - proxyConfig = ProxyHandle(context: nil, port: 0) - } - - public func localPort() -> UInt16 { - stateLock.lock() - defer { stateLock.unlock() } - return proxyConfig.port - } - - public func start() throws { - stateLock.lock() - defer { stateLock.unlock() } - guard didStart == false else { return } - - let err = encrypted_dns_proxy_start(state, &proxyConfig) - if err != 0 { - throw EncryptedDnsProxyError.start(err: err) - } - didStart = true - } - - public func stop() { - stateLock.lock() - defer { stateLock.unlock() } - guard didStart == true else { return } - didStart = false - - encrypted_dns_proxy_stop(&proxyConfig) - } - - deinit { - if didStart { - encrypted_dns_proxy_stop(&proxyConfig) - } - - encrypted_dns_proxy_free(state) - } -} diff --git a/ios/MullvadRustRuntime/ShadowSocksProxy.swift b/ios/MullvadRustRuntime/ShadowSocksProxy.swift deleted file mode 100644 index 4969352999..0000000000 --- a/ios/MullvadRustRuntime/ShadowSocksProxy.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// ShadowsocksProxy.swift -// MullvadREST -// -// Created by Emils on 19/04/2023. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import MullvadRustRuntimeProxy -import Network - -/// A Swift wrapper around a Rust implementation of Shadowsocks proxy instance -public class ShadowsocksProxy: @unchecked Sendable { - private var proxyConfig: ProxyHandle - private let forwardAddress: IPAddress - private let forwardPort: UInt16 - private let bridgeAddress: IPAddress - private let bridgePort: UInt16 - private let password: String - private let cipher: String - private var didStart = false - private let stateLock = NSLock() - - public init( - forwardAddress: IPAddress, - forwardPort: UInt16, - bridgeAddress: IPAddress, - bridgePort: UInt16, - password: String, - cipher: String - ) { - proxyConfig = ProxyHandle(context: nil, port: 0) - self.forwardAddress = forwardAddress - self.forwardPort = forwardPort - self.bridgeAddress = bridgeAddress - self.bridgePort = bridgePort - self.password = password - self.cipher = cipher - } - - /// The local port for the shadow socks proxy - /// - /// - Returns: The local port for the shadow socks proxy when it has started, 0 otherwise. - public func localPort() -> UInt16 { - stateLock.lock() - defer { stateLock.unlock() } - return proxyConfig.port - } - - deinit { - stop() - } - - /// Starts the socks proxy - public func start() { - stateLock.lock() - defer { stateLock.unlock() } - guard didStart == false else { return } - didStart = true - - // Get the raw bytes access to `proxyConfig` - _ = withUnsafeMutablePointer(to: &proxyConfig) { config in - start_shadowsocks_proxy( - forwardAddress.rawValue.map { $0 }, - UInt(forwardAddress.rawValue.count), - forwardPort, - bridgeAddress.rawValue.map { $0 }, - UInt(bridgeAddress.rawValue.count), - bridgePort, - password, - UInt(password.count), - cipher, - UInt(cipher.count), - config - ) - } - } - - /// Stops the socks proxy - public func stop() { - stateLock.lock() - defer { stateLock.unlock() } - guard didStart == true else { return } - didStart = false - - _ = withUnsafeMutablePointer(to: &proxyConfig) { config in - stop_shadowsocks_proxy(config) - } - } -} diff --git a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h index c8443c1fcf..2583d64c8d 100644 --- a/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h +++ b/ios/MullvadRustRuntime/include/mullvad_rust_runtime.h @@ -19,12 +19,6 @@ typedef uint8_t SwiftAccessMethodKind; typedef struct ApiContext ApiContext; -/** - * A thin wrapper around [`mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState`] that - * can start a local forwarder (see [`Self::start`]). - */ -typedef struct EncryptedDnsProxyState EncryptedDnsProxyState; - typedef struct ExchangeCancelToken ExchangeCancelToken; typedef struct Map Map; @@ -107,11 +101,6 @@ typedef struct SwiftProblemReportRequest { struct ProblemReportMetadata metadata; } SwiftProblemReportRequest; -typedef struct ProxyHandle { - void *context; - uint16_t port; -} ProxyHandle; - typedef struct DaitaParameters { uint8_t *machines; double max_padding_frac; @@ -132,6 +121,11 @@ typedef struct EphemeralPeerParameters { struct WgTcpConnectionFunctions funcs; } EphemeralPeerParameters; +typedef struct ProxyHandle { + void *context; + uint16_t port; +} ProxyHandle; + extern const uint16_t CONFIG_SERVICE_PORT; /** @@ -798,8 +792,6 @@ struct SwiftCancelHandle mullvad_ios_init_storekit_payment(struct SwiftApiContex * `retry_strategy` must have been created by a call to either of the following functions * `mullvad_api_retry_strategy_never`, `mullvad_api_retry_strategy_constant` or `mullvad_api_retry_strategy_exponential` * - * `account_number` must be a pointer to a null terminated string. - * * `body` must be a pointer to a contiguous memory segment * * `body_size` must be the size of the body @@ -809,56 +801,10 @@ struct SwiftCancelHandle mullvad_ios_init_storekit_payment(struct SwiftApiContex struct SwiftCancelHandle mullvad_ios_check_storekit_payment(struct SwiftApiContext api_context, void *completion_cookie, struct SwiftRetryStrategy retry_strategy, - const char *account_number, const uint8_t *body, uintptr_t body_size); /** - * Initializes a valid pointer to an instance of `EncryptedDnsProxyState`. - * - * # Safety - * - * * [domain_name] must not be non-null. - * - * * [domain_name] pointer must be [valid](core::ptr#safety) - * - * * The caller must ensure that the pointer to the [domain_name] string contains a nul terminator - * at the end of the string. - */ -struct EncryptedDnsProxyState *encrypted_dns_proxy_init(const char *domain_name); - -/** - * This must be called only once to deallocate `EncryptedDnsProxyState`. - * - * # Safety - * `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized - * by `encrypted_dns_proxy_init`. This function is not thread safe, and should only be called - * once. - */ -void encrypted_dns_proxy_free(struct EncryptedDnsProxyState *ptr); - -/** - * # Safety - * encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized - * by `encrypted_dns_proxy_init`. This function is not thread safe. - * `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`. This - * function is not thread safe, but it can be called repeatedly. Each successful invocation should - * clean up the resulting proxy via `[encrypted_dns_proxy_stop]`. - * - * `proxy_handle` will only contain valid values if the return value is zero. It is still valid to - * deallocate the memory. - */ -int32_t encrypted_dns_proxy_start(struct EncryptedDnsProxyState *encrypted_dns_proxy, - struct ProxyHandle *proxy_handle); - -/** - * # Safety - * `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by - * [`encrypted_dns_proxy_start`]. It should only ever be called once. - */ -int32_t encrypted_dns_proxy_stop(struct ProxyHandle *proxy_config); - -/** * To be called when ephemeral peer exchange has finished. All parameters except * `raw_packet_tunnel` are optional. * @@ -910,33 +856,6 @@ struct ExchangeCancelToken *request_ephemeral_peer(const uint8_t *public_key, int32_t tunnel_handle, struct EphemeralPeerParameters peer_parameters); -/** - * # Safety - * `addr`, `password`, `cipher` must be valid for the lifetime of this function call and they must - * be backed by the amount of bytes as stored in the respective `*_len` parameters. - * - * `proxy_config` must be pointing to a valid memory region for the size of a `ProxyHandle` - * instance. - */ -int32_t start_shadowsocks_proxy(const uint8_t *forward_address, - uintptr_t forward_address_len, - uint16_t forward_port, - const uint8_t *addr, - uintptr_t addr_len, - uint16_t port, - const uint8_t *password, - uintptr_t password_len, - const uint8_t *cipher, - uintptr_t cipher_len, - struct ProxyHandle *proxy_config); - -/** - * # Safety - * `proxy_config` must be pointing to a valid instance of a `ProxyInstance`, as instantiated by - * `start_shadowsocks_proxy`. - */ -int32_t stop_shadowsocks_proxy(struct ProxyHandle *proxy_config); - int32_t start_udp2tcp_obfuscator_proxy(const uint8_t *peer_address, uintptr_t peer_address_len, uint16_t peer_port, diff --git a/ios/MullvadTypes/Storekit2.swift b/ios/MullvadTypes/Storekit2.swift index 149d82f84b..affaa6c2b0 100644 --- a/ios/MullvadTypes/Storekit2.swift +++ b/ios/MullvadTypes/Storekit2.swift @@ -1,4 +1,4 @@ -public struct StorekitTransaction: Codable, Sendable { +public struct StoreKitTransaction: Codable, Sendable { let transaction: String public init(transaction: String) { @@ -6,7 +6,7 @@ public struct StorekitTransaction: Codable, Sendable { } } -public struct LegacyStorekitRequest: Codable, Sendable { +public struct LegacyStoreKitRequest: Codable, Sendable { let receiptString: Data public init(receiptString: Data) { diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 1efb5ca45f..770f6a8250 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -7,13 +7,12 @@ objects = { /* Begin PBXBuildFile section */ - 014449952CA293B100C0C2F2 /* EncryptedDNSProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */; }; 01B2FF862D70B914004AED35 /* MullvadRustRuntime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A992DA1D2C24709F00DE7CE5 /* MullvadRustRuntime.framework */; }; 01EF6F342B6A590700125696 /* libmullvad_api.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 01EF6F332B6A590700125696 /* libmullvad_api.a */; }; 062B45A328FD4CA700746E77 /* le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 06799AB428F98CE700ACD94E /* le_root_cert.cer */; }; 062B45BC28FD8C3B00746E77 /* RESTDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 062B45BB28FD8C3B00746E77 /* RESTDefaults.swift */; }; 063687BA28EB234F00BE7161 /* PacketTunnelAPITransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 063687B928EB234F00BE7161 /* PacketTunnelAPITransport.swift */; }; - 063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; }; + 063F026628FFE11C001FA09F /* StorePaymentOutcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* StorePaymentOutcome.swift */; }; 06799ACE28F98E1D00ACD94E /* MullvadREST.h in Headers */ = {isa = PBXBuildFile; fileRef = 06799ABE28F98E1D00ACD94E /* MullvadREST.h */; settings = {ATTRIBUTES = (Public, ); }; }; 06799AD128F98E1D00ACD94E /* MullvadREST.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; }; 06799AD228F98E1D00ACD94E /* MullvadREST.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 06799ABC28F98E1D00ACD94E /* MullvadREST.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -142,11 +141,11 @@ 583832292AC3DF1300EA2071 /* PacketTunnelActorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583832282AC3DF1300EA2071 /* PacketTunnelActorCommand.swift */; }; 5838322B2AC3EF9600EA2071 /* EventChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5838322A2AC3EF9600EA2071 /* EventChannel.swift */; }; 583D86482A2678DC0060D63B /* DeviceStateAccessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583D86472A2678DC0060D63B /* DeviceStateAccessor.swift */; }; + 583DA21425FA4B5C00318683 /* LocationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583DA21325FA4B5C00318683 /* LocationDataSource.swift */; }; + 583FE02429C1ACB3006E85F9 /* StorePaymentOutcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* StorePaymentOutcome.swift */; }; 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */; }; 58421032282E42B000F24E46 /* UpdateDeviceDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */; }; - 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; }; 5846227326E22A160035F7C2 /* StorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */; }; - 5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */; }; 584D26C4270C855B004EA533 /* VPNSettingsDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C3270C855A004EA533 /* VPNSettingsDataSource.swift */; }; 584D26C6270C8741004EA533 /* SettingsDNSTextCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584D26C5270C8741004EA533 /* SettingsDNSTextCell.swift */; }; 5859A55529CD9DD900F66591 /* changes.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5859A55429CD9DD800F66591 /* changes.txt */; }; @@ -475,6 +474,8 @@ 7A2E7B722D6C9FE5009EF2C3 /* APIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E7B6C2D6C9E53009EF2C3 /* APIRequest.swift */; }; 7A2E7B732D6C9FEB009EF2C3 /* APIRequestProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B6742D5DF86400687524 /* APIRequestProxy.swift */; }; 7A2E7B752D6CA0B1009EF2C3 /* APITransportProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E7B742D6CA0AC009EF2C3 /* APITransportProvider.swift */; }; + 7A2F41092EC38FD20013D3C5 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2F41082EC38FC00013D3C5 /* StoreSubscription.swift */; }; + 7A2F410A2EC38FD20013D3C5 /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2F41082EC38FC00013D3C5 /* StoreSubscription.swift */; }; 7A307ADB2A8F56DF0017618B /* Duration+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */; }; 7A3215722D3934E6005DF395 /* MullvadApiCompletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */; }; 7A33538F2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */; }; @@ -567,6 +568,8 @@ 7A8A19242CF4C9BF000BCB5B /* MultihopPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */; }; 7A8A19262CF4D37B000BCB5B /* DAITAPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19252CF4D373000BCB5B /* DAITAPage.swift */; }; 7A8A19282CF603EB000BCB5B /* SettingsViewControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */; }; + 7A9246AB2EB0C765008FE31A /* StorePaymentManagerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9246AA2EB0C75A008FE31A /* StorePaymentManagerInteractor.swift */; }; + 7A9246AC2EB0C765008FE31A /* StorePaymentManagerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9246AA2EB0C75A008FE31A /* StorePaymentManagerInteractor.swift */; }; 7A95B67B2D5F758300687524 /* relays.json in Resources */ = {isa = PBXBuildFile; fileRef = 7A95B67A2D5F758300687524 /* relays.json */; }; 7A95B67D2D5F7C5B00687524 /* DAITASettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */; }; 7A964BB92E699A3F00C6A4EC /* ShadowsocksObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A964BB72E6999A500C6A4EC /* ShadowsocksObfuscator.swift */; }; @@ -625,6 +628,8 @@ 7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */; }; 7AB931262D43D22F005FCEBA /* MullvadApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */; }; 7AB9312F2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */; }; + 7ABB9B932EB207CC006A0EDD /* LegacyStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABB9B922EB207BF006A0EDD /* LegacyStorePaymentManager.swift */; }; + 7ABB9B942EB207CC006A0EDD /* LegacyStorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABB9B922EB207BF006A0EDD /* LegacyStorePaymentManager.swift */; }; 7ABCA5B32A9349F20044A708 /* Routing.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; }; 7ABCA5B42A9349F20044A708 /* Routing.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7A88DCCE2A8FABBE00D2FF0E /* Routing.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7ABCA5B72A9353C60044A708 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58CAF9F72983D36800BE19F7 /* Coordinator.swift */; }; @@ -772,7 +777,7 @@ A9A5F9EB2ACB05160083449F /* CharacterSet+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587EB669270EFACB00123C75 /* CharacterSet+IPAddress.swift */; }; A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58E511E528DDDEAC00B0BCDE /* CodingErrors+CustomErrorDescription.swift */; }; A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */; }; - A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */; }; + A9A5F9EE2ACB05160083449F /* StorePaymentOutcome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE67828F83CA50033DD93 /* StorePaymentOutcome.swift */; }; A9A5F9EF2ACB05160083449F /* String+AccountFormatting.swift in Sources */ = {isa = PBXBuildFile; fileRef = E158B35F285381C60002F069 /* String+AccountFormatting.swift */; }; A9A5F9F02ACB05160083449F /* String+FuzzyMatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A09C98029D99215000C2CAC /* String+FuzzyMatch.swift */; }; A9A5F9F12ACB05160083449F /* String+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5807E2BF2432038B00F5FF30 /* String+Helpers.swift */; }; @@ -801,7 +806,6 @@ A9A5FA092ACB05160083449F /* SendStoreReceiptOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */; }; A9A5FA0A2ACB05160083449F /* StorePaymentEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27429093A310096FC88 /* StorePaymentEvent.swift */; }; A9A5FA0B2ACB05160083449F /* StorePaymentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */; }; - A9A5FA0C2ACB05160083449F /* StorePaymentManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */; }; A9A5FA0D2ACB05160083449F /* StorePaymentManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */; }; A9A5FA0E2ACB05160083449F /* StorePaymentObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */; }; A9A5FA0F2ACB05160083449F /* StoreSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5846227026E229F20035F7C2 /* StoreSubscription.swift */; }; @@ -870,7 +874,6 @@ A9C342C32ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C342C22ACC3EE90045F00E /* RelayCacheTracker+Stubs.swift */; }; A9C7B62C2EB9F71D002CABB1 /* LoggerBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C7B62B2EB9F71D002CABB1 /* LoggerBuilderTests.swift */; }; A9D4A4792C2DAB5F00F1E522 /* libmullvad_ios.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A9C75BC12C2D8C9E00B4CDF5 /* libmullvad_ios.a */; }; - A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */; }; A9D9A4B22C36D12D004088DD /* TunnelObfuscator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584023212A406BF5007B27AC /* TunnelObfuscator.swift */; }; A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */; }; A9D9A4C42C36D53C004088DD /* MullvadRustRuntime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A992DA1D2C24709F00DE7CE5 /* MullvadRustRuntime.framework */; }; @@ -1559,7 +1562,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedDNSProxy.swift; sourceTree = "<group>"; }; 01EF6F2D2B6A51B100125696 /* mullvad-api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "mullvad-api.h"; path = "../mullvad-api/include/mullvad-api.h"; sourceTree = "<group>"; }; 01EF6F332B6A590700125696 /* libmullvad_api.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmullvad_api.a; path = "../target/aarch64-apple-ios/debug/libmullvad_api.a"; sourceTree = "<group>"; }; 01F1FF1D29F0627D007083C3 /* libmullvad_ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libmullvad_ios.a; path = ../target/debug/libmullvad_ios.a; sourceTree = "<group>"; }; @@ -1589,8 +1591,8 @@ 06FAE67528F83CA40033DD93 /* RESTTaskIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RESTTaskIdentifier.swift; sourceTree = "<group>"; }; 06FAE67628F83CA40033DD93 /* RetryStrategy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RetryStrategy.swift; sourceTree = "<group>"; }; 06FAE67728F83CA40033DD93 /* ServerRelaysResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerRelaysResponse.swift; sourceTree = "<group>"; }; - 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RESTCreateApplePaymentResponse+Localization.swift"; sourceTree = "<group>"; }; 06FAE67A28F83CA50033DD93 /* DeviceHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceHandling.swift; sourceTree = "<group>"; }; + 06FAE67828F83CA50033DD93 /* StorePaymentOutcome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorePaymentOutcome.swift; sourceTree = "<group>"; }; 06FAE67B28F83CA50033DD93 /* REST.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = REST.swift; sourceTree = "<group>"; }; 44075DFA2CDA4F7400F61139 /* UDPOverTCPObfuscationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UDPOverTCPObfuscationSettingsViewModel.swift; sourceTree = "<group>"; }; 440870812D7A00B00038972F /* UIImage+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Assets.swift"; sourceTree = "<group>"; }; @@ -1729,9 +1731,7 @@ 5842102F282D8A3C00F24E46 /* UpdateAccountDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateAccountDataOperation.swift; sourceTree = "<group>"; }; 58421031282E42B000F24E46 /* UpdateDeviceDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateDeviceDataOperation.swift; sourceTree = "<group>"; }; 5846226426E0D9630035F7C2 /* ProductsRequestOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsRequestOperation.swift; sourceTree = "<group>"; }; - 5846227026E229F20035F7C2 /* StoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSubscription.swift; sourceTree = "<group>"; }; 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentObserver.swift; sourceTree = "<group>"; }; - 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerDelegate.swift; sourceTree = "<group>"; }; 584B26F3237434D00073B10E /* RelaySelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorTests.swift; sourceTree = "<group>"; }; 584D0111299134AB00531822 /* Version.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Version.xcconfig; sourceTree = "<group>"; }; 584D26BE270C550B004EA533 /* AnyIPAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyIPAddress.swift; sourceTree = "<group>"; }; @@ -2020,6 +2020,7 @@ 7A2E7B6C2D6C9E53009EF2C3 /* APIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequest.swift; sourceTree = "<group>"; }; 7A2E7B6E2D6C9ED9009EF2C3 /* APIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIError.swift; sourceTree = "<group>"; }; 7A2E7B742D6CA0AC009EF2C3 /* APITransportProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APITransportProvider.swift; sourceTree = "<group>"; }; + 7A2F41082EC38FC00013D3C5 /* StoreSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSubscription.swift; sourceTree = "<group>"; }; 7A307ADA2A8F56DF0017618B /* Duration+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extensions.swift"; sourceTree = "<group>"; }; 7A3215702D392F0B005DF395 /* MullvadApiCompletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiCompletion.swift; sourceTree = "<group>"; }; 7A33538E2AA9FF1600F0A71C /* SimulatorTunnelProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderManager.swift; sourceTree = "<group>"; }; @@ -2102,6 +2103,7 @@ 7A8A19232CF4C9B8000BCB5B /* MultihopPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultihopPage.swift; sourceTree = "<group>"; }; 7A8A19252CF4D373000BCB5B /* DAITAPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITAPage.swift; sourceTree = "<group>"; }; 7A8A19272CF603E3000BCB5B /* SettingsViewControllerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewControllerFactory.swift; sourceTree = "<group>"; }; + 7A9246AA2EB0C75A008FE31A /* StorePaymentManagerInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePaymentManagerInteractor.swift; sourceTree = "<group>"; }; 7A95B6742D5DF86400687524 /* APIRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIRequestProxy.swift; sourceTree = "<group>"; }; 7A95B67A2D5F758300687524 /* relays.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = relays.json; sourceTree = "<group>"; }; 7A95B67C2D5F7C5B00687524 /* DAITASettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITASettingsCoordinator.swift; sourceTree = "<group>"; }; @@ -2156,6 +2158,7 @@ 7AB931232D43C2C2005FCEBA /* MullvadApiContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiContext.swift; sourceTree = "<group>"; }; 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiResponse.swift; sourceTree = "<group>"; }; 7AB9312D2D4A5D0A005FCEBA /* MullvadApiNetworkOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadApiNetworkOperation.swift; sourceTree = "<group>"; }; + 7ABB9B922EB207BF006A0EDD /* LegacyStorePaymentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyStorePaymentManager.swift; sourceTree = "<group>"; }; 7ABE318C2A1CDD4500DF4963 /* UIFont+Weight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Weight.swift"; sourceTree = "<group>"; }; 7ABFB09D2BA316220074A49E /* RelayConstraintsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConstraintsTests.swift; sourceTree = "<group>"; }; 7AC8A3AD2ABC6FBB00DC4939 /* SettingsHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsHeaderView.swift; sourceTree = "<group>"; }; @@ -2444,7 +2447,6 @@ F0DA87462A9CB9A2006044F1 /* AccountExpiryRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountExpiryRow.swift; sourceTree = "<group>"; }; F0DA87482A9CBA9F006044F1 /* AccountDeviceRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeviceRow.swift; sourceTree = "<group>"; }; F0DA874A2A9CBACB006044F1 /* AccountNumberRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountNumberRow.swift; sourceTree = "<group>"; }; - F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowSocksProxy.swift; sourceTree = "<group>"; }; F0DDE4102B220458006B57A7 /* ShadowsocksConfigurationCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfigurationCache.swift; sourceTree = "<group>"; }; F0DDE4132B220458006B57A7 /* ShadowsocksConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShadowsocksConfiguration.swift; sourceTree = "<group>"; }; F0DDE4272B220A15006B57A7 /* Haversine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Haversine.swift; sourceTree = "<group>"; }; @@ -3366,7 +3368,6 @@ 587D9675288989DB00CD8F1C /* NSLayoutConstraint+Helpers.swift */, 7AE90B672C2D726000375A60 /* NSParagraphStyle+Extensions.swift */, 5871FB9F254C26BF0051A0A4 /* NSRegularExpression+IPAddress.swift */, - 06FAE67828F83CA50033DD93 /* RESTCreateApplePaymentResponse+Localization.swift */, 58B9EB142489139B00095626 /* RESTError+Display.swift */, 58A8EE592976BFBB009C0F8D /* SKError+Localized.swift */, 58FD5BEF24238EB300112C88 /* SKProduct+Formatting.swift */, @@ -3460,14 +3461,16 @@ 5846226F26E229CD0035F7C2 /* StorePaymentManager */ = { isa = PBXGroup; children = ( + 7ABB9B922EB207BF006A0EDD /* LegacyStorePaymentManager.swift */, 585E820227F3285E00939F0E /* SendStoreReceiptOperation.swift */, 5878A27629093A4F0096FC88 /* StorePaymentBlockObserver.swift */, 5878A27429093A310096FC88 /* StorePaymentEvent.swift */, 58DF28A42417CB4B00E836B0 /* StorePaymentManager.swift */, - 5846227626E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift */, 58FB865426E8BF3100F188BC /* StorePaymentManagerError.swift */, + 7A9246AA2EB0C75A008FE31A /* StorePaymentManagerInteractor.swift */, 5846227226E22A160035F7C2 /* StorePaymentObserver.swift */, - 5846227026E229F20035F7C2 /* StoreSubscription.swift */, + 06FAE67828F83CA50033DD93 /* StorePaymentOutcome.swift */, + 7A2F41082EC38FC00013D3C5 /* StoreSubscription.swift */, 58F70FE42AEA707800E6890E /* StoreTransactionLog.swift */, ); path = StorePaymentManager; @@ -4511,7 +4514,6 @@ A992DA1E2C24709F00DE7CE5 /* MullvadRustRuntime */ = { isa = PBXGroup; children = ( - 014449942CA293B100C0C2F2 /* EncryptedDNSProxy.swift */, A948809A2BC9308D0090A44C /* EphemeralPeerExchangeActor.swift */, A9EB4F9C2B7FAB21002A2D7A /* EphemeralPeerNegotiator.swift */, A9A557F42B7E3E5C0017ADA8 /* EphemeralPeerReceiver.swift */, @@ -4522,7 +4524,6 @@ 7AB931252D43D222005FCEBA /* MullvadApiResponse.swift */, A992DA1F2C24709F00DE7CE5 /* MullvadRustRuntime.h */, F0EEFB9E2D8D60E1007FE4B3 /* RustProblemReportRequest.swift */, - F0DDE40F2B220458006B57A7 /* ShadowSocksProxy.swift */, F0A89CB62D9D922300580C27 /* String+UnsafePointer.swift */, 584023212A406BF5007B27AC /* TunnelObfuscator.swift */, A96D0B442D675F0400DD6C59 /* MullvadConnectionModeProvider.swift */, @@ -5908,6 +5909,7 @@ F97C38DF2DEEDB0F006DCB08 /* Color+Mullvad.swift in Sources */, A9A5FA3A2ACB05910083449F /* UIEdgeInsets+Extensions.swift in Sources */, A9A5FA3B2ACB05910083449F /* UIMetrics.swift in Sources */, + 7A9246AB2EB0C765008FE31A /* StorePaymentManagerInteractor.swift in Sources */, 58B07C182AEFDD6C00A09625 /* StoreTransactionLog.swift in Sources */, A9A5FA382ACB05600083449F /* InputTextFormatter.swift in Sources */, F022EBA62CF0C6AE009484B9 /* ConsolidatedApplicationLog.swift in Sources */, @@ -5927,7 +5929,7 @@ F0A086902C22D6A700BF83E7 /* TunnelSettingsStrategyTests.swift in Sources */, A9A5F9EC2ACB05160083449F /* CodingErrors+CustomErrorDescription.swift in Sources */, A9A5F9ED2ACB05160083449F /* NSRegularExpression+IPAddress.swift in Sources */, - A9A5F9EE2ACB05160083449F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, + A9A5F9EE2ACB05160083449F /* StorePaymentOutcome.swift in Sources */, 7A9BE5A72B907EEC00E2A7D0 /* AllLocationDataSource.swift in Sources */, A9A5F9EF2ACB05160083449F /* String+AccountFormatting.swift in Sources */, F9EDB26C2EC4C0480015DE36 /* CustomListInteractorTests.swift in Sources */, @@ -5975,10 +5977,8 @@ A902E7A62D3FB0D9007F844A /* LogFileOutputStreamTests.swift in Sources */, A9A5FA0B2ACB05160083449F /* StorePaymentManager.swift in Sources */, F0FA16092D7F0425007E2546 /* FilterDescriptorTests.swift in Sources */, - A9A5FA0C2ACB05160083449F /* StorePaymentManagerDelegate.swift in Sources */, A9A5FA0D2ACB05160083449F /* StorePaymentManagerError.swift in Sources */, A9A5FA0E2ACB05160083449F /* StorePaymentObserver.swift in Sources */, - A9A5FA0F2ACB05160083449F /* StoreSubscription.swift in Sources */, 7A6811542DC8EC6E009CB61A /* UIFont+Weight.swift in Sources */, A9A5FA102ACB05160083449F /* PacketTunnelAPITransport.swift in Sources */, 7AD63A472CDA666100445268 /* UIntTests.swift in Sources */, @@ -5995,12 +5995,14 @@ F072D3CF2C07122400906F64 /* SettingsUpdaterTests.swift in Sources */, 7ACE19132C1C352100260BB6 /* RelayPickingTests.swift in Sources */, F09D04B52AE93CB6003D4F89 /* OutgoingConnectionProxy+Stub.swift in Sources */, + 7ABB9B932EB207CC006A0EDD /* LegacyStorePaymentManager.swift in Sources */, 58BE4B9D2B18A85B007EA1D3 /* NSAttributedString+Extensions.swift in Sources */, A9A5FA172ACB05160083449F /* SendTunnelProviderMessageOperation.swift in Sources */, 7A83A0C62B29A750008B5CE7 /* APIAccessMethodsTests.swift in Sources */, A9A5FA182ACB05160083449F /* SetAccountOperation.swift in Sources */, A9A5FA192ACB05160083449F /* StartTunnelOperation.swift in Sources */, 7ACE19152C1C429A00260BB6 /* MultihopDecisionFlowTests.swift in Sources */, + 7A2F410A2EC38FD20013D3C5 /* StoreSubscription.swift in Sources */, A9A5FA1A2ACB05160083449F /* StopTunnelOperation.swift in Sources */, 7A9BE5A52B90760C00E2A7D0 /* CustomListsDataSourceTests.swift in Sources */, A9A5FA1B2ACB05160083449F /* Tunnel.swift in Sources */, @@ -6246,7 +6248,6 @@ 7A11DD0B2A9495D400098CD8 /* AppRoutes.swift in Sources */, 5827B0902B0CAA0500CCBBA1 /* EditAccessMethodCoordinator.swift in Sources */, F97C38E52DEEDFD6006DCB08 /* Image+Assets.swift in Sources */, - 5846227126E229F20035F7C2 /* StoreSubscription.swift in Sources */, 58421030282D8A3C00F24E46 /* UpdateAccountDataOperation.swift in Sources */, 7A8A190A2CE5FFE9000BCB5B /* SettingsDAITAView.swift in Sources */, 449E9A6D2D283A2500F8574A /* ConnectionViewComponentPreview.swift in Sources */, @@ -6301,6 +6302,7 @@ F90A988E2E13C5490020F64F /* MullvadSecondaryTextField.swift in Sources */, 5827B0BD2B14AC9200CCBBA1 /* AccessViewModel+TestingStatus.swift in Sources */, 58F8AC0E25D3F8CE002BE0ED /* ProblemReportReviewViewController.swift in Sources */, + 7A9246AC2EB0C765008FE31A /* StorePaymentManagerInteractor.swift in Sources */, 58EFC7752AFB4CEF00E9F4CB /* AboutViewController.swift in Sources */, 5878A27129091CF20096FC88 /* AccountInteractor.swift in Sources */, F9C579C42E8FE08600C90C50 /* LocationListItem.swift in Sources */, @@ -6376,7 +6378,6 @@ 7A6F2FAD2AFD3DA7006D0856 /* CustomDNSViewController.swift in Sources */, F0E8CC0C2A4EE672007ED3B4 /* SetupAccountCompletedController.swift in Sources */, 7AD03E1D2E8E910E00270EAE /* RevokedDeviceView.swift in Sources */, - 5846227726E22A7C0035F7C2 /* StorePaymentManagerDelegate.swift in Sources */, 58EF581125D69DB400AEBA94 /* StatusImageView.swift in Sources */, 58EE2E3B272FF814003BFF93 /* SettingsDataSourceDelegate.swift in Sources */, 5823FA5426CE49F700283BF8 /* TunnelObserver.swift in Sources */, @@ -6432,7 +6433,7 @@ 5820676426E771DB00655B05 /* TunnelManagerErrors.swift in Sources */, 585B4B8726D9098900555C4C /* TunnelStatusNotificationProvider.swift in Sources */, F9276C622DBA2103006FE43D /* Font+Mullvad.swift in Sources */, - 063F026628FFE11C001FA09F /* RESTCreateApplePaymentResponse+Localization.swift in Sources */, + 063F026628FFE11C001FA09F /* StorePaymentOutcome.swift in Sources */, 58DF28A52417CB4B00E836B0 /* StorePaymentManager.swift in Sources */, F9C579C82E8FE10400C90C50 /* RelayItemView.swift in Sources */, F050AE602B73A41E003F4EDB /* AllLocationDataSource.swift in Sources */, @@ -6512,6 +6513,7 @@ 5827B0A82B0F49EF00CCBBA1 /* ProxyConfigurationInteractorProtocol.swift in Sources */, 7A5869B92B56E7F000640D27 /* IPOverrideViewControllerDelegate.swift in Sources */, 586C0D7A2B039CE300E7CDD7 /* ShadowsocksCipherPicker.swift in Sources */, + 7A2F41092EC38FD20013D3C5 /* StoreSubscription.swift in Sources */, 58B93A1326C3F13600A55733 /* TunnelState.swift in Sources */, F017F8E02D78AC020076EC01 /* RelayFilterDataSourceItem.swift in Sources */, 586C0D832B03D2FF00E7CDD7 /* ShadowsocksSectionHandler.swift in Sources */, @@ -6626,6 +6628,7 @@ F02F41A12B9723AF00625A4F /* AddLocationsDataSource.swift in Sources */, 58EFC76E2AFB3BDA00E9F4CB /* ListAccessMethodCoordinator.swift in Sources */, 5827B0B92B14A1C700CCBBA1 /* MethodTestingStatusCellContentConfiguration.swift in Sources */, + 7ABB9B942EB207CC006A0EDD /* LegacyStorePaymentManager.swift in Sources */, 7A42DEC92A05164100B209BE /* SettingsInputCell.swift in Sources */, F95A28332E8BBB7400C3F75D /* SelectLocationFilter.swift in Sources */, 5803B4B22940A48700C23744 /* TunnelStore.swift in Sources */, @@ -6665,6 +6668,7 @@ 58C7A45B2A8640030060C66F /* PacketTunnelPathObserver.swift in Sources */, 580D6B8E2AB33BBF00B2D6E0 /* BlockedStateErrorMapper.swift in Sources */, 06AC116228F94C450037AF9A /* ApplicationConfiguration.swift in Sources */, + 583FE02429C1ACB3006E85F9 /* StorePaymentOutcome.swift in Sources */, 58CE38C728992C8700A6D6E5 /* WireGuardAdapterError+Localization.swift in Sources */, 58E511E828DDDF2400B0BCDE /* CodingErrors+CustomErrorDescription.swift in Sources */, 58FDF2D92A0BA11A00C2B061 /* DeviceCheckOperation.swift in Sources */, @@ -6874,8 +6878,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - A9D9A4B12C36D10E004088DD /* ShadowSocksProxy.swift in Sources */, - 014449952CA293B100C0C2F2 /* EncryptedDNSProxy.swift in Sources */, F0A89CB52D9D864B00580C27 /* RustProblemReportRequest.swift in Sources */, 7AB931242D43C2CA005FCEBA /* MullvadApiContext.swift in Sources */, A9D9A4BB2C36D397004088DD /* EphemeralPeerNegotiator.swift in Sources */, diff --git a/ios/MullvadVPN/AppDelegate.swift b/ios/MullvadVPN/AppDelegate.swift index 8128961250..8824a166fe 100644 --- a/ios/MullvadVPN/AppDelegate.swift +++ b/ios/MullvadVPN/AppDelegate.swift @@ -19,9 +19,7 @@ import UIKit import UserNotifications @main -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, StorePaymentManagerDelegate, - @unchecked Sendable -{ +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, @unchecked Sendable { nonisolated(unsafe) private var logger: Logger! #if targetEnvironment(simulator) @@ -154,10 +152,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD storePaymentManager = StorePaymentManager( backgroundTaskProvider: backgroundTaskProvider, - queue: .default(), - apiProxy: apiProxy, - accountsProxy: accountsProxy, - transactionLog: .default + interactor: StorePaymentManagerInteractor( + tunnelManager: tunnelManager, + apiProxy: apiProxy, + accountProxy: accountsProxy + ) ) let apiRequestFactory = MullvadApiRequestFactory( @@ -178,7 +177,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) registerBackgroundTasks() - setupPaymentHandler() setupNotifications() addApplicationNotifications(application: application) @@ -445,11 +443,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) } - private func setupPaymentHandler() { - storePaymentManager.delegate = self - storePaymentManager.addPaymentObserver(tunnelManager) - } - private func setupNotifications() { NotificationManager.shared.notificationProviders = [ LatestChangesNotificationProvider(appPreferences: appPreferences), @@ -551,9 +544,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD self.logger.debug("Finished initialization.") NotificationManager.shared.updateNotifications() - self.storePaymentManager.start() - finish(nil) + Task { + await self.storePaymentManager.start() + finish(nil) + } } } } @@ -636,18 +631,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } - // MARK: - StorePaymentManagerDelegate - - nonisolated func storePaymentManager( - _ manager: StorePaymentManager, - didRequestAccountTokenFor payment: SKPayment - ) -> String? { - // Since we do not persist the relation between payment and account number between the - // app launches, we assume that all successful purchases belong to the active account - // number. - tunnelManager.deviceState.accountData?.number - } - // MARK: - UNUserNotificationCenterDelegate nonisolated func userNotificationCenter( diff --git a/ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift b/ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift index 118aa486f4..ae9c895cc7 100644 --- a/ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift +++ b/ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift @@ -13,7 +13,7 @@ extension Bundle { /// /// 1. Dev builds (debug): XXXX.YY-devZ /// 2. TestFlight builds: XXXX.YY-betaZ - /// 3. AppStore builds: XXXX.YY + /// 3. App Store builds: XXXX.YY /// /// Note: XXXX.YY is an app version (i.e 2020.5) and Z is a build number (i.e 1) var productVersion: String { diff --git a/ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift b/ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift index aa2dda62f5..a44859102a 100644 --- a/ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift +++ b/ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift @@ -10,7 +10,7 @@ import Foundation import MullvadTypes import StoreKit -extension StorePaymentManagerError: DisplayError { +extension LegacyStorePaymentManagerError: DisplayError { var displayErrorDescription: String? { switch self { case .noAccountSet: @@ -25,14 +25,14 @@ extension StorePaymentManagerError: DisplayError { case let .readReceipt(readReceiptError): if readReceiptError is StoreReceiptNotFound { - return NSLocalizedString("AppStore receipt is not found on disk.", comment: "") + return NSLocalizedString("App Store receipt is not found on disk.", comment: "") } else if let storeError = readReceiptError as? SKError { return String( - format: NSLocalizedString("Cannot refresh the AppStore receipt: %@", comment: ""), + format: NSLocalizedString("Cannot refresh the App Store receipt: %@", comment: ""), storeError.localizedDescription ) } else { - return NSLocalizedString("Cannot read the AppStore receipt from disk", comment: "") + return NSLocalizedString("Cannot read the App Store receipt from disk", comment: "") } case let .sendReceipt(error): diff --git a/ios/MullvadVPN/StorePaymentManager/LegacyStorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/LegacyStorePaymentManager.swift new file mode 100644 index 0000000000..37f36b19a9 --- /dev/null +++ b/ios/MullvadVPN/StorePaymentManager/LegacyStorePaymentManager.swift @@ -0,0 +1,476 @@ +// +// LegacyStorePaymentManager.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-10-29. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadLogging +import MullvadREST +import MullvadTypes +import Operations +@preconcurrency import StoreKit + +/// Manager responsible for handling AppStore payments and passing StoreKit receipts to the backend. +/// +/// - Warning: only interact with this object on the main queue. +final class LegacyStorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchecked Sendable { + private enum OperationCategory { + static let sendStoreReceipt = "StorePaymentManager.sendStoreReceipt" + static let productsRequest = "StorePaymentManager.productsRequest" + } + + private let logger = Logger(label: "LegacyStorePaymentManager") + private let operationQueue: OperationQueue = { + let queue = AsyncOperationQueue() + queue.name = "StorePaymentManagerQueue" + return queue + }() + + private let backgroundTaskProvider: BackgroundTaskProviding + private let paymentQueue: SKPaymentQueue + private var observerList = ObserverList<StorePaymentObserver>() + private let transactionLog: StoreTransactionLog + private let interactor: StorePaymentManagerInteractor + + /// A dictionary that maps each payment to account number. + private var paymentToAccountToken = [SKPayment: String]() + + /// Returns true if the device is able to make payments. + static var canMakePayments: Bool { + SKPaymentQueue.canMakePayments() + } + + /// Designated initializer + /// + /// - Parameters: + /// - backgroundTaskProvider: the background task provider. + /// - accountsProxy: the object implementing `RESTAccountHandling`. + /// - transactionLog: an instance of transaction log. Typically ``StoreTransactionLog/default``. + /// - interactor: interactor for communicating with API etc. + init( + backgroundTaskProvider: BackgroundTaskProviding, + queue: SKPaymentQueue, + transactionLog: StoreTransactionLog, + interactor: StorePaymentManagerInteractor + ) { + self.backgroundTaskProvider = backgroundTaskProvider + paymentQueue = queue + self.transactionLog = transactionLog + self.interactor = interactor + } + + func starts() { + // Load transaction log from file before starting the payment queue. + logger.debug("Load transaction log.") + transactionLog.read() + + logger.debug("Start payment queue monitoring") + paymentQueue.add(self) + } + + // MARK: - SKPaymentTransactionObserver + + func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { + // Ensure that all calls happen on main queue because StoreKit does not guarantee on which queue the delegate + // will be invoked. + Task { @MainActor in + await self.handleTransactions(transactions) + } + } + + // MARK: - Payment observation + + /// Add payment observer + /// - Parameter observer: an observer object. + func addPaymentObserver(_ observer: StorePaymentObserver) { + observerList.append(observer) + } + + // MARK: - Products and payments + + /// Fetch products from AppStore using product identifiers. + /// + /// - Parameters: + /// - productIdentifiers: a set of product identifiers. + /// - completionHandler: completion handler. Invoked on main queue. + /// - Returns: the request cancellation token + func requestProducts( + with productIdentifiers: Set<LegacyStoreSubscription>, + completionHandler: @escaping @Sendable (Result<SKProductsResponse, Error>) -> Void + ) -> Cancellable { + let productIdentifiers = productIdentifiers.productIdentifiersSet + let operation = ProductsRequestOperation( + productIdentifiers: productIdentifiers, + completionHandler: completionHandler + ) + operation.addCondition(MutuallyExclusive(category: OperationCategory.productsRequest)) + + operationQueue.addOperation(operation) + + return operation + } + + /// Add payment and associate it with the account number. + /// + /// Validates the user account with backend before adding the payment to the queue. + /// + /// - Parameters: + /// - payment: an instance of `SKPayment`. + /// - accountNumber: the account number to credit. + nonisolated func addPayment(_ payment: SKPayment, for accountNumber: String) async { + logger.debug("Validating account before the purchase.") + + let productIdentifier = payment.productIdentifier + let quantity = payment.quantity + let requestData = payment.requestData + let applicationUsername = payment.applicationUsername + let simulatesAskToBuyInSandbox = payment.simulatesAskToBuyInSandbox + + // Validate account token before adding new payment to the queue. + await validateAccount(accountNumber: accountNumber) { error in + // Reconstruct a new SKMutablePayment with the same fields + let cloned = SKMutablePayment() + cloned.productIdentifier = productIdentifier + cloned.quantity = quantity + cloned.requestData = requestData + cloned.applicationUsername = applicationUsername + cloned.simulatesAskToBuyInSandbox = simulatesAskToBuyInSandbox + + if let error { + self.logger.error("Failed to validate the account. Payment is ignored.") + let event = LegacyStorePaymentEvent.failure( + LegacyStorePaymentFailure( + transaction: nil, + payment: cloned, + accountNumber: accountNumber, + error: error + ) + ) + + self.notifyObservers(of: event) + } else { + self.logger.debug("Add payment to the queue.") + + self.associateAccountNumber(accountNumber, and: cloned) + self.paymentQueue.add(cloned) + } + } + } + + /// Restore purchases by sending the AppStore receipt to backend. + /// + /// - Parameters: + /// - accountNumber: the account number to credit. + /// - completionHandler: completion handler invoked on the main queue. + /// - Returns: the request cancellation token. + func restorePurchases( + for accountNumber: String, + completionHandler: @escaping @Sendable (Result<REST.CreateApplePaymentResponse, Error>) -> Void + ) async -> Cancellable { + logger.debug("Restore purchases.") + + return await sendStoreReceipt( + accountNumber: accountNumber, + forceRefresh: true, + completionHandler: completionHandler + ) + } + + // Returns time added, in seconds. + func timeFromProduct(id: String) -> TimeInterval { + let product = LegacyStoreSubscription(rawValue: id) + + return switch product { + case .thirtyDays: Duration.days(30).timeInterval + case .ninetyDays: Duration.days(90).timeInterval + case .none: 0 + } + } + + // MARK: - Private methods + + private func notifyObservers(of storeKitEvent: LegacyStorePaymentEvent) { + observerList.notify { observer in + Task { @MainActor in + observer.storePaymentManager(didReceiveEvent: storeKitEvent) + } + } + } + + private func transactionHasBeenProcessed(id: String) -> Bool { + transactionLog.contains(transactionIdentifier: id) + } + + private func addToProcessedTransactions(id: String) { + transactionLog.add(transactionIdentifier: id) + } + + /// Associate account number with the payment object. + /// + /// - Parameters: + /// - accountNumber: the account number that should be credited with the payment. + /// - payment: the payment object. + private func associateAccountNumber(_ accountNumber: String, and payment: SKPayment) { + paymentToAccountToken[payment] = accountNumber + } + + /// Remove association between the payment object and the account number. + /// + /// Since the association between account numbers and payments is not persisted, this method may consult the delegate to provide the account number to + /// credit. This can happen for dangling transactions that remain in the payment queue between the application restarts. In the future this association should be + /// solved by using `SKPaymentQueue.applicationUsername`. + /// + /// - Parameter payment: the payment object. + /// - Returns: The account number on success, otherwise `nil`. + private func deassociateAccountNumber(_ payment: SKPayment) async -> String? { + if let accountToken = paymentToAccountToken[payment] { + paymentToAccountToken.removeValue(forKey: payment) + return accountToken + } else { + return await interactor.accountNumber + } + } + + /// Validate account number. + /// + /// - Parameters: + /// - accountNumber: the account number + /// - completionHandler: completion handler invoked on main queue. The completion block Receives `nil` upon success, otherwise an error. + private func validateAccount( + accountNumber: String, + completionHandler: @escaping @Sendable (LegacyStorePaymentManagerError?) -> Void + ) async { + let accountProxy = await interactor.accountProxy + let accountOperation = ResultBlockOperation<Account>(dispatchQueue: .main) { finish in + accountProxy.getAccountData( + accountNumber: accountNumber, retryStrategy: .default, completion: finish) + } + + accountOperation.addObserver( + BackgroundObserver( + backgroundTaskProvider: backgroundTaskProvider, + name: "Validate account number", + cancelUponExpiration: false + )) + + accountOperation.completionQueue = .main + accountOperation.completionHandler = { result in + completionHandler(result.error.map { LegacyStorePaymentManagerError.validateAccount($0) }) + } + + operationQueue.addOperation(accountOperation) + } + + /// Send the AppStore receipt stored on device to the backend. + /// + /// - Parameters: + /// - accountNumber: the account number to credit. + /// - forceRefresh: indicates whether the receipt should be downloaded from AppStore even when it's present on device. + /// - completionHandler: a completion handler invoked on main queue. + /// - Returns: the request cancellation token. + private func sendStoreReceipt( + accountNumber: String, + forceRefresh: Bool, + completionHandler: @escaping @Sendable (Result<REST.CreateApplePaymentResponse, Error>) -> Void + ) async -> Cancellable { + let operation = SendStoreReceiptOperation( + apiProxy: await interactor.apiProxy, + accountNumber: accountNumber, + forceRefresh: forceRefresh, + receiptProperties: nil, + completionHandler: completionHandler + ) + + operation.addObserver( + BackgroundObserver( + backgroundTaskProvider: backgroundTaskProvider, + name: "Send AppStore receipt", + cancelUponExpiration: true + ) + ) + + operation.addCondition(MutuallyExclusive(category: OperationCategory.sendStoreReceipt)) + + operationQueue.addOperation(operation) + + return operation + } + + /// Handles an array of StoreKit transactions. + /// - Parameter transactions: an array of transactions + private func handleTransactions(_ transactions: [SKPaymentTransaction]) async { + for transaction in transactions { + await handleTransaction(transaction) + } + } + + /// Handle single StoreKit transaction. + /// - Parameter transaction: a transaction + private func handleTransaction(_ transaction: SKPaymentTransaction) async { + switch transaction.transactionState { + case .deferred: + logger.info("Deferred \(transaction.payment.productIdentifier)") + + case .failed: + let transactionError = transaction.error?.localizedDescription ?? "No error" + logger.error("Failed to purchase \(transaction.payment.productIdentifier): \(transactionError)") + + await didFailPurchase(transaction: transaction) + + case .purchased: + logger.info("Purchased \(transaction.payment.productIdentifier)") + + await didFinishOrRestorePurchase(transaction: transaction) + + case .purchasing: + logger.info("Purchasing \(transaction.payment.productIdentifier)") + + case .restored: + logger.info("Restored \(transaction.payment.productIdentifier)") + + await didFinishOrRestorePurchase(transaction: transaction) + + @unknown default: + logger.warning("Unknown transactionState = \(transaction.transactionState.rawValue)") + } + } + + /// Handle failed transaction by finishing it and notifying the observers. + /// + /// - Parameter transaction: the failed transaction. + private func didFailPurchase(transaction: SKPaymentTransaction) async { + paymentQueue.finishTransaction(transaction) + + let paymentFailure = + if let accountToken = await deassociateAccountNumber(transaction.payment) { + LegacyStorePaymentFailure( + transaction: transaction, + payment: transaction.payment, + accountNumber: accountToken, + error: .storePayment(transaction.error!) + ) + } else { + LegacyStorePaymentFailure( + transaction: transaction, + payment: transaction.payment, + accountNumber: nil, + error: .noAccountSet + ) + } + + notifyObservers(of: .failure(paymentFailure)) + } + + /// Handle successful transaction that's in purchased or restored state. + /// + /// - Consults with transaction log before handling the transaction. Transactions that are already processed are removed from the payment queue, + /// observers are not notified as they had already received the corresponding events. + /// - Keeps transaction in the queue if association between transaction payment and account number cannot be established. Notifies observers with the error. + /// - Sends the AppStore receipt to backend. + /// + /// - Parameter transaction: the transaction that's in purchased or restored state. + private func didFinishOrRestorePurchase(transaction: SKPaymentTransaction) async { + // Obtain transaction identifier which must be set on transactions with purchased or restored state. + guard let transactionIdentifier = transaction.transactionIdentifier else { + logger.warning("Purchased or restored transaction does not contain a transaction identifier!") + return + } + + // Check if transaction is already processed. + guard !transactionHasBeenProcessed(id: transactionIdentifier) else { + logger.debug("Found transaction that is already processed.") + paymentQueue.finishTransaction(transaction) + return + } + + // Find the account number associated with the payment. + guard let accountNumber = await deassociateAccountNumber(transaction.payment) else { + logger.debug("Cannot locate the account associated with the purchase. Keep transaction in the queue.") + + let event = LegacyStorePaymentEvent.failure( + LegacyStorePaymentFailure( + transaction: transaction, + payment: transaction.payment, + accountNumber: nil, + error: .noAccountSet + ) + ) + + notifyObservers(of: event) + return + } + + // Send the AppStore receipt to the backend. + await _ = sendStoreReceipt(accountNumber: accountNumber, forceRefresh: false) { result in + self.didSendStoreReceipt( + accountNumber: accountNumber, + transactionIdentifier: transactionIdentifier, + transaction: transaction, + result: result + ) + } + } + + /// Handles the result of uploading the AppStore receipt to the backend. + /// + /// If the server response is successful, this function adds the transaction identifier to the transaction log to make sure that the same transaction is not + /// processed twice, then finishes the transaction. + /// + /// This is important because the call to `SKPaymentQueue.finishTransaction()` may fail, causing the same transaction to re-appear on the payment + /// queue. Since the transaction was already processed, no action needs to be performed besides another attempt to finish it and hopefully remove it from + /// the payment queue for good. + /// + /// If the server response indicates an error, then this function keeps the transaction in the payment queue in order to process it again later. + /// + /// Finally, the ``StorePaymentEvent`` is produced and dispatched to observers to notify them on the progress. + /// + /// - Parameters: + /// - accountNumber: the account number to credit + /// - transactionIdentifier: the transaction identifier + /// - transaction: the transaction object + /// - result: the result of uploading the AppStore receipt to the backend. + private func didSendStoreReceipt( + accountNumber: String, + transactionIdentifier: String, + transaction: SKPaymentTransaction, + result: Result<REST.CreateApplePaymentResponse, Error> + ) { + var event: LegacyStorePaymentEvent? + + switch result { + case let .success(response): + // Save transaction identifier to identify it later if it resurrects on the payment queue. + addToProcessedTransactions(id: transactionIdentifier) + + // Finish transaction to remove it from the payment queue. + paymentQueue.finishTransaction(transaction) + + event = LegacyStorePaymentEvent.finished( + LegacyStorePaymentCompletion( + transaction: transaction, + accountNumber: accountNumber, + serverResponse: response + )) + + case let .failure(error as LegacyStorePaymentManagerError): + logger.debug("Failed to upload the receipt. Keep transaction in the queue.") + + event = LegacyStorePaymentEvent.failure( + LegacyStorePaymentFailure( + transaction: transaction, + payment: transaction.payment, + accountNumber: accountNumber, + error: error + )) + + default: + break + } + + if let event { + notifyObservers(of: event) + } + } +} diff --git a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift index c143f59215..45d920ebf1 100644 --- a/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift +++ b/ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift @@ -55,26 +55,26 @@ class SendStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse } override func main() { - // Pull receipt from AppStore if requested. + // Pull receipt from App Store if requested. guard !forceRefresh else { startRefreshRequest() return } - // Read AppStore receipt from disk. + // Read App Store receipt from disk. do { let data = try readReceiptFromDisk() sendReceipt(data) } catch is StoreReceiptNotFound { - // Pull receipt from AppStore if it's not cached locally. + // Pull receipt from App Store if it's not cached locally. startRefreshRequest() } catch { logger.error( error: error, - message: "Failed to read the AppStore receipt." + message: "Failed to read the App Store receipt." ) - finish(result: .failure(StorePaymentManagerError.readReceipt(error))) + finish(result: .failure(LegacyStorePaymentManagerError.readReceipt(error))) } } @@ -89,9 +89,9 @@ class SendStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse } catch { self.logger.error( error: error, - message: "Failed to read the AppStore receipt after refresh." + message: "Failed to read the App Store receipt after refresh." ) - self.finish(result: .failure(StorePaymentManagerError.readReceipt(error))) + self.finish(result: .failure(LegacyStorePaymentManagerError.readReceipt(error))) } } } @@ -100,9 +100,9 @@ class SendStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse dispatchQueue.async { self.logger.error( error: error, - message: "Failed to refresh the AppStore receipt." + message: "Failed to refresh the App Store receipt." ) - self.finish(result: .failure(StorePaymentManagerError.readReceipt(error))) + self.finish(result: .failure(LegacyStorePaymentManagerError.readReceipt(error))) } } @@ -133,16 +133,16 @@ class SendStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse } private func sendReceipt(_ receiptData: Data) { - submitReceiptTask = apiProxy.legacyStorekitPayment( + submitReceiptTask = apiProxy.legacyStoreKitPayment( accountNumber: accountNumber, - request: LegacyStorekitRequest(receiptString: receiptData), + request: LegacyStoreKitRequest(receiptString: receiptData), retryStrategy: .default, completionHandler: { result in switch result { case let .success(response): self.logger.info( """ - AppStore receipt was processed. \ + App Store receipt was processed. \ Time added: \(response.timeAdded), \ New expiry: \(response.newExpiry.logFormatted) """ @@ -156,9 +156,9 @@ class SendStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse } else { self.logger.error( error: error, - message: "Failed to send the AppStore receipt." + message: "Failed to send the App Store receipt." ) - self.finish(result: .failure(StorePaymentManagerError.sendReceipt(error))) + self.finish(result: .failure(LegacyStorePaymentManagerError.sendReceipt(error))) } } } @@ -168,6 +168,6 @@ class SendStoreReceiptOperation: ResultOperation<REST.CreateApplePaymentResponse struct StoreReceiptNotFound: LocalizedError { var errorDescription: String? { - "AppStore receipt file does not exist on disk." + "App Store receipt file does not exist on disk." } } diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift index 14c5cf93a9..822bfbe116 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift @@ -7,9 +7,10 @@ // import Foundation +import StoreKit final class StorePaymentBlockObserver: StorePaymentObserver { - typealias BlockHandler = @Sendable (StorePaymentManager, StorePaymentEvent) -> Void + typealias BlockHandler = @Sendable (LegacyStorePaymentEvent) -> Void private let blockHandler: BlockHandler @@ -17,10 +18,11 @@ final class StorePaymentBlockObserver: StorePaymentObserver { self.blockHandler = blockHandler } - func storePaymentManager( - _ manager: StorePaymentManager, - didReceiveEvent event: StorePaymentEvent - ) { - blockHandler(manager, event) + func storePaymentManager(didReceiveEvent event: LegacyStorePaymentEvent) { + blockHandler(event) + } + + func storePaymentManager(didReceiveEvent event: StorePaymentEvent) { + // Not used. } } diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift index 3b7f046309..599106ec4c 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift @@ -10,13 +10,64 @@ import Foundation import MullvadREST @preconcurrency import StoreKit +// MARK: StoreKit 2 flow + +enum StorePaymentEvent { + /// Successful payment + case successfulPayment(StorePaymentOutcome) + /// Use cancelled the purchase + case userCancelled + /// Payment was made but it is still being processed. This transaction can be processed and the receipt uploaded to the API later, when the transaction listener handles it. + case pending + /// Purchasing failed + case failed(StorePaymentError) +} + +enum StorePaymentError { + /// Purchase failed because the product being purchased is either unavailable or StoreKit services failed. + case storeKitError(StoreKitError) + /// Purchase failed because of a "purchase error". + case purchaseError(Product.PurchaseError) + /// User made a purchase, but we failed to verify the transaction. In this case, it is fine to not send the transaction to the API. + case verification(VerificationResult<Transaction>.VerificationError) + /// In this case, the user has initiated the payment but the app failed to fetch a payment token from the API. + /// No money has been spent and the payment has failed. + case getPaymentToken(Error) + /// In this case, the user has already spent money but we failed to upload the receipt to the API. + /// They should be fine as the API should , but we can still upload the receipt later + case receiptUpload(Error) + /// To handle errors we don't recognize, we need to, unfortunately, wrap them in an unkown error type. + case unknown(Error) + + var description: String? { + switch self { + case let .storeKitError(error): + error.localizedDescription + case let .purchaseError(error): + error.localizedDescription + case .verification: + NSLocalizedString("Failed to verify transaction receipt", comment: "") + case .getPaymentToken: + NSLocalizedString("Failed to reach Mullvad servers to initiate purchase", comment: "") + case let .unknown(error): + NSLocalizedString("Unexpected error occured: \(error)", comment: "") + case .receiptUpload: + NSLocalizedString( + "Failed to upload receipt to Mullvad servers. Try again later or contact support for help.", comment: "" + ) + } + } +} + +// MARK: Legacy StoreKit flow + /// The payment event received by observers implementing ``StorePaymentObserver``. -enum StorePaymentEvent: @unchecked Sendable { +enum LegacyStorePaymentEvent: @unchecked Sendable { /// The payment is successfully completed. - case finished(StorePaymentCompletion) + case finished(LegacyStorePaymentCompletion) /// Failure to complete the payment. - case failure(StorePaymentFailure) + case failure(LegacyStorePaymentFailure) /// An instance of `SKPayment` held in the associated value. var payment: SKPayment { @@ -30,19 +81,19 @@ enum StorePaymentEvent: @unchecked Sendable { } /// Successful payment metadata. -struct StorePaymentCompletion { +struct LegacyStorePaymentCompletion { /// Transaction object. let transaction: SKPaymentTransaction /// The account number credited. let accountNumber: String - /// The server response received after uploading the AppStore receipt. + /// The server response received after uploading the App Store receipt. let serverResponse: REST.CreateApplePaymentResponse } /// Failed payment metadata. -struct StorePaymentFailure: @unchecked Sendable { +struct LegacyStorePaymentFailure: @unchecked Sendable { /// Transaction object, if available. /// May not be available due to account validation failure. let transaction: SKPaymentTransaction? @@ -56,5 +107,5 @@ struct StorePaymentFailure: @unchecked Sendable { let accountNumber: String? /// The payment manager error. - let error: StorePaymentManagerError + let error: LegacyStorePaymentManagerError } diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift index 7eaa9f6605..1ae441e8c4 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift @@ -2,474 +2,340 @@ // StorePaymentManager.swift // MullvadVPN // -// Created by pronebird on 10/03/2020. +// Created by Jon Petersson on 2025-10-29. // Copyright © 2025 Mullvad VPN AB. All rights reserved. // import MullvadLogging import MullvadREST import MullvadTypes -import Operations @preconcurrency import StoreKit -import UIKit -/// Manager responsible for handling AppStore payments and passing StoreKit receipts to the backend. +/// Manager responsible for handling App Store payments and passing StoreKit receipts to the backend. /// /// - Warning: only interact with this object on the main queue. -final class StorePaymentManager: NSObject, SKPaymentTransactionObserver, @unchecked Sendable { - private enum OperationCategory { - static let sendStoreReceipt = "StorePaymentManager.sendStoreReceipt" - static let productsRequest = "StorePaymentManager.productsRequest" - } - +final actor StorePaymentManager: @unchecked Sendable { private let logger = Logger(label: "StorePaymentManager") - - private let operationQueue: OperationQueue = { - let queue = AsyncOperationQueue() - queue.name = "StorePaymentManagerQueue" - return queue - }() - - private let backgroundTaskProvider: BackgroundTaskProviding - private let paymentQueue: SKPaymentQueue - private let apiProxy: APIQuerying - private let accountsProxy: RESTAccountHandling private var observerList = ObserverList<StorePaymentObserver>() - private let transactionLog: StoreTransactionLog - - /// Payment manager's delegate. - weak var delegate: StorePaymentManagerDelegate? + private let interactor: StorePaymentManagerInteractor + private var processedTransactionIds: Set<UInt64> = [] + private var updateListenerTask: Task<Void, Never>? - /// A dictionary that maps each payment to account number. - private var paymentToAccountToken = [SKPayment: String]() - - /// Returns true if the device is able to make payments. - static var canMakePayments: Bool { - SKPaymentQueue.canMakePayments() - } + // Legacy payment manager, kept around until Store Kit 2 is fully migrated and tested. + private let legacyStorePaymentManager: LegacyStorePaymentManager /// Designated initializer /// /// - Parameters: /// - backgroundTaskProvider: the background task provider. - /// - queue: the payment queue. Typically `SKPaymentQueue.default()`. - /// - apiProxy: the object implement `APIQuerying` - /// - accountsProxy: the object implementing `RESTAccountHandling`. - /// - transactionLog: an instance of transaction log. Typically ``StoreTransactionLog/default``. - init( - backgroundTaskProvider: BackgroundTaskProviding, - queue: SKPaymentQueue, - apiProxy: APIQuerying, - accountsProxy: RESTAccountHandling, - transactionLog: StoreTransactionLog - ) { - self.backgroundTaskProvider = backgroundTaskProvider - paymentQueue = queue - self.apiProxy = apiProxy - self.accountsProxy = accountsProxy - self.transactionLog = transactionLog + /// - interactor: interactor for communicating with API etc. + init(backgroundTaskProvider: BackgroundTaskProviding, interactor: StorePaymentManagerInteractor) { + self.interactor = interactor + + legacyStorePaymentManager = LegacyStorePaymentManager( + backgroundTaskProvider: backgroundTaskProvider, + queue: .default(), + transactionLog: .default, + interactor: interactor + ) } - /// Loads transaction log from disk and starts monitoring payment queue. - func start() { - // Load transaction log from file before starting the payment queue. - logger.debug("Load transaction log.") - transactionLog.read() + /// Start listening for transaction updates. + func start() async { + logger.debug("Starting StoreKit 2 transaction listener.") - logger.debug("Start payment queue monitoring") - paymentQueue.add(self) - } + #if !DEBUG + legacyStorePaymentManager.start() + #endif + + _ = try? await processOutstandingTransactions() - // MARK: - SKPaymentTransactionObserver + updateListenerTask?.cancel() + updateListenerTask = Task { [weak self] in + guard let self else { return } + + // If the purchase was made out-of-band, we need not upload the receipt. + for await verification in Transaction.updates { + guard await shouldProcessPayment(verification: verification) else { + continue + } - func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { - // Ensure that all calls happen on main queue because StoreKit does not guarantee on which queue the delegate - // will be invoked. - DispatchQueue.main.async { - self.handleTransactions(transactions) + await updateAccountData() + } } } - // MARK: - Payment observation + // MARK: Notifications - /// Add payment observer - /// - Parameter observer: an observer object. + // If the call stack ever gets asynchronous, we should remove the nonisolation. func addPaymentObserver(_ observer: StorePaymentObserver) { + // assumeIsolated { isolatedSelf in + // } observerList.append(observer) - } - - /// Remove payment observer - /// - Parameter observer: an observer object. - func removePaymentObserver(_ observer: StorePaymentObserver) { - observerList.remove(observer) + legacyStorePaymentManager.addPaymentObserver(observer) } // MARK: - Products and payments - /// Fetch products from AppStore using product identifiers. - /// - /// - Parameters: - /// - productIdentifiers: a set of product identifiers. - /// - completionHandler: completion handler. Invoked on main queue. - /// - Returns: the request cancellation token - func requestProducts( - with productIdentifiers: Set<StoreSubscription>, - completionHandler: @escaping @Sendable (Result<SKProductsResponse, Error>) -> Void - ) -> Cancellable { - let productIdentifiers = productIdentifiers.productIdentifiersSet - let operation = ProductsRequestOperation( - productIdentifiers: productIdentifiers, - completionHandler: completionHandler - ) - operation.addCondition(MutuallyExclusive(category: OperationCategory.productsRequest)) + func products() async throws -> [Product] { + try await Product.products(for: StoreSubscription.allCases.map { $0.rawValue }) + } + + func purchase(product: Product) async { + let token: UUID + do { + token = try await self.getPaymentToken() + } catch { + didFailFetchingToken(error: error) + return + } - operationQueue.addOperation(operation) + let result: Product.PurchaseResult + do { + result = try await product.purchase( + options: [.appAccountToken(token)] + ) + } catch { + didFailPurchase(error: error) + return + } - return operation + switch result { + case let .success(.verified(transaction)): + await purchaseWasSuccessful(transaction: transaction) + case let .success(.unverified(transaction, verificationFailure)): + await didFailVerification(transaction: transaction, error: verificationFailure) + case .userCancelled: + userDidCancel() + case .pending: + didSuspendPurchase() + @unknown default: + fatalError("Unhandled purchase result \(result)") + } } - /// Add payment and associate it with the account number. - /// - /// Validates the user account with backend before adding the payment to the queue. - /// - /// - Parameters: - /// - payment: an instance of `SKPayment`. - /// - accountNumber: the account number to credit. - func addPayment(_ payment: SKPayment, for accountNumber: String) { - logger.debug("Validating account before the purchase.") + func processOutstandingTransactions() async throws -> StorePaymentOutcome { + var timeAdded: TimeInterval = 0 - let productIdentifier = payment.productIdentifier - let quantity = payment.quantity - let requestData = payment.requestData - let applicationUsername = payment.applicationUsername - let simulatesAskToBuyInSandbox = payment.simulatesAskToBuyInSandbox + for await verification in Transaction.unfinished { + guard shouldProcessPayment(verification: verification) else { + continue + } - // Validate account token before adding new payment to the queue. - validateAccount(accountNumber: accountNumber) { error in - // Reconstruct a new SKMutablePayment with the same fields - let cloned = SKMutablePayment() - cloned.productIdentifier = productIdentifier - cloned.quantity = quantity - cloned.requestData = requestData - cloned.applicationUsername = applicationUsername - cloned.simulatesAskToBuyInSandbox = simulatesAskToBuyInSandbox + try await uploadReceipt(verification: verification) - if let error { - self.logger.error("Failed to validate the account. Payment is ignored.") - let event = StorePaymentEvent.failure( - StorePaymentFailure( - transaction: nil, - payment: cloned, - accountNumber: accountNumber, - error: error - ) - ) + let payload = try verification.payloadValue + await payload.finish() - self.observerList.notify { observer in - observer.storePaymentManager(self, didReceiveEvent: event) - } - } else { - self.logger.debug("Add payment to the queue.") + addToProcessedTransactions(verification) - self.associateAccountNumber(accountNumber, and: cloned) - self.paymentQueue.add(cloned) - } + let isStoreKit2Transaction = StoreSubscription.allCases + .map { $0.rawValue } + .contains(payload.productID) + + timeAdded += + isStoreKit2Transaction + ? timeFromProduct(id: payload.productID) + : legacyStorePaymentManager.timeFromProduct(id: payload.productID) } - } - /// Restore purchases by sending the AppStore receipt to backend. - /// - /// - Parameters: - /// - accountNumber: the account number to credit. - /// - completionHandler: completion handler invoked on the main queue. - /// - Returns: the request cancellation token. - func restorePurchases( - for accountNumber: String, - completionHandler: @escaping @Sendable (Result<REST.CreateApplePaymentResponse, Error>) -> Void - ) -> Cancellable { - logger.debug("Restore purchases.") + await updateAccountData() - return sendStoreReceipt( - accountNumber: accountNumber, - forceRefresh: true, - completionHandler: completionHandler - ) + return if timeAdded > 0 { + .timeAdded(timeAdded) + } else { + .noTimeAdded + } } // MARK: - Private methods - /// Associate account number with the payment object. - /// - /// - Parameters: - /// - accountNumber: the account number that should be credited with the payment. - /// - payment: the payment object. - private func associateAccountNumber(_ accountNumber: String, and payment: SKPayment) { - dispatchPrecondition(condition: .onQueue(.main)) + private func getPaymentToken() async throws -> UUID { + let result = await interactor.initPayment() - paymentToAccountToken[payment] = accountNumber + switch result { + case .success(let token): return token + case .failure(let error): throw error + } } - /// Remove association between the payment object and the account number. - /// - /// Since the association between account numbers and payments is not persisted, this method may consult the delegate to provide the account number to - /// credit. This can happen for dangling transactions that remain in the payment queue between the application restarts. In the future this association should be - /// solved by using `SKPaymentQueue.applicationUsername`. - /// - /// - Parameter payment: the payment object. - /// - Returns: The account number on success, otherwise `nil`. - private func deassociateAccountNumber(_ payment: SKPayment) -> String? { - dispatchPrecondition(condition: .onQueue(.main)) + private func uploadReceipt(verification: VerificationResult<Transaction>) async throws { + let isStoreKit2Transaction = try StoreSubscription.allCases + .map { $0.rawValue } + .contains(verification.payloadValue.productID) - if let accountToken = paymentToAccountToken[payment] { - paymentToAccountToken.removeValue(forKey: payment) - return accountToken + let result: Result<Void, Error> + if isStoreKit2Transaction { + result = await interactor.checkPayment(jwsRepresentation: verification.jwsRepresentation) } else { - return delegate?.storePaymentManager(self, didRequestAccountTokenFor: payment) - } - } - - /// Validate account number. - /// - /// - Parameters: - /// - accountNumber: the account number - /// - completionHandler: completion handler invoked on main queue. The completion block Receives `nil` upon success, otherwise an error. - private func validateAccount( - accountNumber: String, - completionHandler: @escaping @Sendable (StorePaymentManagerError?) -> Void - ) { - let accountOperation = ResultBlockOperation<Account>(dispatchQueue: .main) { finish in - self.accountsProxy.getAccountData(accountNumber: accountNumber, retryStrategy: .default, completion: finish) + result = await interactor.legacySendReceipt() } - accountOperation.addObserver( - BackgroundObserver( - backgroundTaskProvider: backgroundTaskProvider, - name: "Validate account number", - cancelUponExpiration: false - )) - - accountOperation.completionQueue = .main - accountOperation.completionHandler = { result in - completionHandler(result.error.map { StorePaymentManagerError.validateAccount($0) }) + switch result { + case .success(): return + case .failure(let error): throw error } - - operationQueue.addOperation(accountOperation) } - /// Send the AppStore receipt stored on device to the backend. - /// - /// - Parameters: - /// - accountNumber: the account number to credit. - /// - forceRefresh: indicates whether the receipt should be downloaded from AppStore even when it's present on device. - /// - completionHandler: a completion handler invoked on main queue. - /// - Returns: the request cancellation token. - private func sendStoreReceipt( - accountNumber: String, - forceRefresh: Bool, - completionHandler: @escaping @Sendable (Result<REST.CreateApplePaymentResponse, Error>) -> Void - ) -> Cancellable { - let operation = SendStoreReceiptOperation( - apiProxy: apiProxy, - accountNumber: accountNumber, - forceRefresh: forceRefresh, - receiptProperties: nil, - completionHandler: completionHandler - ) - - operation.addObserver( - BackgroundObserver( - backgroundTaskProvider: backgroundTaskProvider, - name: "Send AppStore receipt", - cancelUponExpiration: true - ) - ) + private func purchaseWasSuccessful(transaction: Transaction) async { + let verification = VerificationResult<Transaction>.verified(transaction) - operation.addCondition(MutuallyExclusive(category: OperationCategory.sendStoreReceipt)) + do { + try await uploadReceipt(verification: verification) + await updateAccountData() - operationQueue.addOperation(operation) + try await verification.payloadValue.finish() - return operation - } - - /// Handles an array of StoreKit transactions. - /// - Parameter transactions: an array of transactions - private func handleTransactions(_ transactions: [SKPaymentTransaction]) { - transactions.forEach { transaction in - handleTransaction(transaction) + addToProcessedTransactions(verification) + didPurchaseMoreTime(outcome: .timeAdded(timeFromProduct(id: transaction.productID))) + } catch { + didFailUploadingReceipt(error: error) } } - /// Handle single StoreKit transaction. - /// - Parameter transaction: a transaction - private func handleTransaction(_ transaction: SKPaymentTransaction) { - switch transaction.transactionState { - case .deferred: - logger.info("Deferred \(transaction.payment.productIdentifier)") - - case .failed: - let transactionError = transaction.error?.localizedDescription ?? "No error" - logger.error("Failed to purchase \(transaction.payment.productIdentifier): \(transactionError)") - - didFailPurchase(transaction: transaction) + private func updateAccountData() async { + guard let accountNumber = await interactor.accountNumber else { + return + } - case .purchased: - logger.info("Purchased \(transaction.payment.productIdentifier)") + let result = await interactor.getAccountData(accountNumber: accountNumber) - didFinishOrRestorePurchase(transaction: transaction) + switch result { + case let .success(accountData): + logger.info("Successfully updated account data. New expiry: \(accountData.expiry.logFormatted)") + await interactor.updateAccountData(for: accountData) - case .purchasing: - logger.info("Purchasing \(transaction.payment.productIdentifier)") + case let .failure(error): + if !error.isOperationCancellationError { + logger.error(error: error, message: "Failed to update account data.") + } + } + } - case .restored: - logger.info("Restored \(transaction.payment.productIdentifier)") + private func transactionHasBeenProcessed(_ verificationResult: VerificationResult<Transaction>) -> Bool { + guard let transactionId = try? verificationResult.payloadValue.id else { + return true + } - didFinishOrRestorePurchase(transaction: transaction) + return processedTransactionIds.contains(transactionId) + } - @unknown default: - logger.warning("Unknown transactionState = \(transaction.transactionState.rawValue)") + private func addToProcessedTransactions(_ verificationResult: VerificationResult<Transaction>) { + guard let transactionId = try? verificationResult.payloadValue.id else { + return } - } - /// Handle failed transaction by finishing it and notifying the observers. - /// - /// - Parameter transaction: the failed transaction. - private func didFailPurchase(transaction: SKPaymentTransaction) { - paymentQueue.finishTransaction(transaction) + _ = processedTransactionIds.insert(transactionId) + } - let paymentFailure = - if let accountToken = deassociateAccountNumber(transaction.payment) { - StorePaymentFailure( - transaction: transaction, - payment: transaction.payment, - accountNumber: accountToken, - error: .storePayment(transaction.error!) - ) - } else { - StorePaymentFailure( - transaction: transaction, - payment: transaction.payment, - accountNumber: nil, - error: .noAccountSet - ) - } + // Returns time added, in seconds. + private func timeFromProduct(id: String) -> TimeInterval { + let product = StoreSubscription(rawValue: id) - observerList.notify { observer in - observer.storePaymentManager(self, didReceiveEvent: .failure(paymentFailure)) + return switch product { + case .thirtyDays: Duration.days(30).timeInterval + case .ninetyDays: Duration.days(90).timeInterval + case .none: 0 } } - /// Handle successful transaction that's in purchased or restored state. - /// - /// - Consults with transaction log before handling the transaction. Transactions that are already processed are removed from the payment queue, - /// observers are not notified as they had already received the corresponding events. - /// - Keeps transaction in the queue if association between transaction payment and account number cannot be established. Notifies observers with the error. - /// - Sends the AppStore receipt to backend. - /// - /// - Parameter transaction: the transaction that's in purchased or restored state. - private func didFinishOrRestorePurchase(transaction: SKPaymentTransaction) { - // Obtain transaction identifier which must be set on transactions with purchased or restored state. - guard let transactionIdentifier = transaction.transactionIdentifier else { - logger.warning("Purchased or restored transaction does not contain a transaction identifier!") - return + private func shouldProcessPayment(verification: VerificationResult<Transaction>) -> Bool { + guard case VerificationResult<Transaction>.verified = verification else { + return false } - // Check if transaction is already processed. - guard !transactionLog.contains(transactionIdentifier: transactionIdentifier) else { - logger.debug("Found transaction that is already processed.") - paymentQueue.finishTransaction(transaction) - return - } + let revocationDate = try? verification.payloadValue.revocationDate + return (revocationDate == nil) && !transactionHasBeenProcessed(verification) + } - // Find the account number associated with the payment. - guard let accountNumber = deassociateAccountNumber(transaction.payment) else { - logger.debug("Cannot locate the account associated with the purchase. Keep transaction in the queue.") + // MARK: Notifications - let event = StorePaymentEvent.failure( - StorePaymentFailure( - transaction: transaction, - payment: transaction.payment, - accountNumber: nil, - error: .noAccountSet - ) - ) + /// Purchase was successful. + private func didPurchaseMoreTime(outcome: StorePaymentOutcome) { + notifyObservers(of: .successfulPayment(outcome)) + } - observerList.notify { observer in - observer.storePaymentManager(self, didReceiveEvent: event) - } - return - } + /// User cancelled purchase before it was completed. + private func userDidCancel() { + notifyObservers(of: .userCancelled) + } - // Send the AppStore receipt to the backend. - _ = sendStoreReceipt(accountNumber: accountNumber, forceRefresh: false) { result in - self.didSendStoreReceipt( - accountNumber: accountNumber, - transactionIdentifier: transactionIdentifier, - transaction: transaction, - result: result - ) - } + /// Purchase is still pending, transaction may be delivered asynchronously. + private func didSuspendPurchase() { + notifyObservers(of: .pending) } - /// Handles the result of uploading the AppStore receipt to the backend. - /// - /// If the server response is successful, this function adds the transaction identifier to the transaction log to make sure that the same transaction is not - /// processed twice, then finishes the transaction. - /// - /// This is important because the call to `SKPaymentQueue.finishTransaction()` may fail, causing the same transaction to re-appear on the payment - /// queue. Since the transaction was already processed, no action needs to be performed besides another attempt to finish it and hopefully remove it from - /// the payment queue for good. - /// - /// If the server response indicates an error, then this function keeps the transaction in the payment queue in order to process it again later. + /// Handle failure to fetch a payment token /// - /// Finally, the ``StorePaymentEvent`` is produced and dispatched to observers to notify them on the progress. - /// - /// - Parameters: - /// - accountNumber: the account number to credit - /// - transactionIdentifier: the transaction identifier - /// - transaction: the transaction object - /// - result: the result of uploading the AppStore receipt to the backend. - private func didSendStoreReceipt( - accountNumber: String, - transactionIdentifier: String, - transaction: SKPaymentTransaction, - result: Result<REST.CreateApplePaymentResponse, Error> - ) { - var event: StorePaymentEvent? - - switch result { - case let .success(response): - // Save transaction identifier to transaction log to identify it later if it resurrects on the payment queue. - transactionLog.add(transactionIdentifier: transactionIdentifier) + /// - Parameter error: error thrown by the API client + private func didFailFetchingToken(error: Error) { + notifyObservers(of: .failed(.getPaymentToken(error))) + } - // Finish transaction to remove it from the payment queue. - paymentQueue.finishTransaction(transaction) + /// Handle failure to upload a payment receipt to the API. This transaction should be uploaded again. + /// + /// - Parameter error: error thrown by the API client + private func didFailUploadingReceipt(error: Error) { + notifyObservers(of: .failed(.receiptUpload(error))) + } - event = StorePaymentEvent.finished( - StorePaymentCompletion( - transaction: transaction, - accountNumber: accountNumber, - serverResponse: response - )) + /// Handle failure to verify the payment transaction. + /// + /// - Parameter error: error thrown by the API client + private func didFailVerification( + transaction: Transaction, + error: VerificationResult<Transaction>.VerificationError + ) async { + await transaction.finish() + notifyObservers(of: .failed(.verification(error))) + } - case let .failure(error as StorePaymentManagerError): - logger.debug("Failed to upload the receipt. Keep transaction in the queue.") + /// Handle an error thrown from the Product.purchase call + /// + /// - Parameter error: the error that was thrown by the Product.purchase call + private func didFailPurchase(error: Error) { + let failure: StorePaymentError + switch error { + case let storeKitError as StoreKitError: + failure = .storeKitError(storeKitError) - event = StorePaymentEvent.failure( - StorePaymentFailure( - transaction: transaction, - payment: transaction.payment, - accountNumber: accountNumber, - error: error - )) + case let purchaseError as Product.PurchaseError: + failure = .purchaseError(purchaseError) default: - break + logger.error("Caught unknown error during purchase call: \(error)") + failure = .unknown(error) } - if let event { - observerList.notify { observer in - observer.storePaymentManager(self, didReceiveEvent: event) + notifyObservers(of: .failed(failure)) + } + + private func notifyObservers(of storeKitEvent: StorePaymentEvent) { + observerList.notify { observer in + Task { @MainActor in + observer.storePaymentManager(didReceiveEvent: storeKitEvent) } } } } + +// Proxy functions for legacy payment +extension StorePaymentManager { + nonisolated func requestProducts( + with productIdentifiers: Set<LegacyStoreSubscription>, + completionHandler: @escaping @Sendable (Result<SKProductsResponse, Error>) -> Void + ) -> Cancellable { + legacyStorePaymentManager.requestProducts(with: productIdentifiers, completionHandler: completionHandler) + } + + nonisolated func addPayment(_ payment: SKPayment, for accountNumber: String) async { + await legacyStorePaymentManager.addPayment(payment, for: accountNumber) + } + + nonisolated func restorePurchases( + for accountNumber: String, + completionHandler: @escaping @Sendable (Result<REST.CreateApplePaymentResponse, Error>) -> Void + ) async -> Cancellable { + await legacyStorePaymentManager.restorePurchases(for: accountNumber, completionHandler: completionHandler) + } +} diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift deleted file mode 100644 index 0d89835548..0000000000 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// StorePaymentManagerDelegate.swift -// MullvadVPN -// -// Created by pronebird on 03/09/2021. -// Copyright © 2025 Mullvad VPN AB. All rights reserved. -// - -import Foundation -import StoreKit - -protocol StorePaymentManagerDelegate: AnyObject, Sendable { - /// Return the account number associated with the payment. - /// Usually called for unfinished transactions coming back after the app was restarted. - func storePaymentManager(_ manager: StorePaymentManager, didRequestAccountTokenFor payment: SKPayment) -> String? -} diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerError.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerError.swift index b64390a97d..ecfd40eef8 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerError.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerError.swift @@ -10,8 +10,8 @@ import Foundation import MullvadREST import MullvadTypes -/// An error type emitted by `StorePaymentManager`. -enum StorePaymentManagerError: LocalizedError, WrappingError { +/// An error type emitted by `LegacyStorePaymentManager`. +enum LegacyStorePaymentManagerError: LocalizedError, WrappingError { /// Failure to find the account token associated with the transaction. case noAccountSet @@ -21,10 +21,10 @@ enum StorePaymentManagerError: LocalizedError, WrappingError { /// Failure to handle payment transaction. Contains error returned by StoreKit. case storePayment(Error) - /// Failure to read the AppStore receipt. + /// Failure to read the App Store receipt. case readReceipt(Error) - /// Failure to send the AppStore receipt to backend. + /// Failure to send the App Store receipt to backend. case sendReceipt(Error) var errorDescription: String? { diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerInteractor.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerInteractor.swift new file mode 100644 index 0000000000..a08650b283 --- /dev/null +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentManagerInteractor.swift @@ -0,0 +1,128 @@ +// +// StorePaymentManagerInteractor.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-10-28. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST +import MullvadSettings +import MullvadTypes +import StoreKit + +final actor StorePaymentManagerInteractor { + private let tunnelManager: TunnelManager + private(set) var apiProxy: APIQuerying + private(set) var accountProxy: RESTAccountHandling + + var accountNumber: String? { + tunnelManager.deviceState.accountData?.number + } + + init(tunnelManager: TunnelManager, apiProxy: APIQuerying, accountProxy: RESTAccountHandling) { + self.tunnelManager = tunnelManager + self.apiProxy = apiProxy + self.accountProxy = accountProxy + } + + // MARK: Tunnel manager + + func updateAccountData(for account: Account) { + guard case .loggedIn(var storedAccountData, let deviceData) = tunnelManager.deviceState else { + return + } + + storedAccountData.expiry = account.expiry + let newDeviceState = DeviceState.loggedIn(storedAccountData, deviceData) + + tunnelManager.setDeviceState(newDeviceState, persist: true) + } + + // MARK: API proxy + + func initPayment() async -> Result<UUID, Error> { + guard let accountNumber = accountNumber else { + return .failure(NSError(domain: "User is not logged in", code: 0)) + } + + return await withCheckedContinuation { continuation in + _ = apiProxy.initStoreKitPayment( + accountNumber: accountNumber, + retryStrategy: .noRetry, + ) { result in + continuation.resume(returning: result) + } + } + } + + func checkPayment(jwsRepresentation: String) async -> Result<Void, Error> { + await withCheckedContinuation { continuation in + _ = apiProxy.checkStoreKitPayment( + transaction: StoreKitTransaction(transaction: jwsRepresentation), + retryStrategy: .noRetry, + ) { result in + continuation.resume(returning: result) + } + } + } + + func legacySendReceipt() async -> Result<Void, Error> { + guard let accountNumber = accountNumber else { + return .failure(NSError(domain: "User is not logged in", code: 0)) + } + + let receiptData: Data + do { + receiptData = try readReceiptFromDisk() + } catch { + return .failure(error) + } + + return await withCheckedContinuation { continuation in + _ = apiProxy.legacyStoreKitPayment( + accountNumber: accountNumber, + request: LegacyStoreKitRequest(receiptString: receiptData), + retryStrategy: .default, + ) { result in + switch result { + case .success: + continuation.resume(returning: .success(())) + case let .failure(error): + continuation.resume(returning: .failure(error)) + } + } + } + } + + // MARK: Account proxy + + func getAccountData(accountNumber: String) async -> Result<Account, Error> { + await withCheckedContinuation { continuation in + _ = self.accountProxy.getAccountData( + accountNumber: accountNumber, + retryStrategy: .default + ) { result in + continuation.resume(returning: result) + } + } + } + + // MARK: Private functions + + private func readReceiptFromDisk() throws -> Data { + guard let appStoreReceiptURL = Bundle.main.appStoreReceiptURL else { + throw StoreReceiptNotFound() + } + + do { + return try Data(contentsOf: appStoreReceiptURL) + } catch let error as CocoaError + where error.code == .fileReadNoSuchFile || error.code == .fileNoSuchFile + { + throw StoreReceiptNotFound() + } catch { + throw error + } + } +} diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift index a254a90e3e..bf2d568bf9 100644 --- a/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift @@ -7,10 +7,9 @@ // import Foundation +import StoreKit protocol StorePaymentObserver: AnyObject, Sendable { - func storePaymentManager( - _ manager: StorePaymentManager, - didReceiveEvent event: StorePaymentEvent - ) + @MainActor func storePaymentManager(didReceiveEvent event: StorePaymentEvent) + @MainActor func storePaymentManager(didReceiveEvent event: LegacyStorePaymentEvent) } diff --git a/ios/MullvadVPN/StorePaymentManager/StorePaymentOutcome.swift b/ios/MullvadVPN/StorePaymentManager/StorePaymentOutcome.swift new file mode 100644 index 0000000000..7edd59cdb1 --- /dev/null +++ b/ios/MullvadVPN/StorePaymentManager/StorePaymentOutcome.swift @@ -0,0 +1,80 @@ +// +// StorePaymentOutcome.swift +// MullvadVPN +// +// Created by Jon Petersson on 2025-10-29. +// Copyright © 2025 Mullvad VPN AB. All rights reserved. +// + +import MullvadREST + +enum StorePaymentOutcome { + case noTimeAdded + case timeAdded(_ timeAdded: TimeInterval) + + var timeAdded: TimeInterval { + switch self { + case .noTimeAdded: + return 0 + case let .timeAdded(timeAdded): + return timeAdded + } + } + + /// Returns a formatted string for the `timeAdded` interval, i.e "30 days" + var formattedTimeAdded: String? { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day] + formatter.unitsStyle = .full + + return formatter.string(from: timeAdded) + } + + func alertMessage(for context: Context) -> String { + switch context { + case .purchase: + return String( + format: NSLocalizedString("%@ have been added to your account.", comment: ""), + formattedTimeAdded ?? "" + ) + case .restoration: + switch self { + case .noTimeAdded: + return NSLocalizedString( + "Your previous purchases have already been added to this account.", + comment: "" + ) + case .timeAdded: + return NSLocalizedString( + "Your previous purchases have been added to your account.", + comment: "" + ) + } + } + } +} + +extension StorePaymentOutcome { + enum Context { + case purchase + case restoration + + var alertTitle: String { + switch self { + case .purchase: + return NSLocalizedString("Thanks for your purchase", comment: "") + case .restoration: + return NSLocalizedString("Restore purchases", comment: "") + } + } + + var errorTitle: String { + switch self { + case .purchase: + return NSLocalizedString("Cannot complete the purchase", comment: "") + case .restoration: + return NSLocalizedString("Cannot restore purchases", comment: "") + } + } + } +} diff --git a/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift b/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift index 76512558cc..d16ffed408 100644 --- a/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift +++ b/ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift @@ -6,11 +6,36 @@ // Copyright © 2025 Mullvad VPN AB. All rights reserved. // -import Foundation import StoreKit +// MARK: StoreKit 2 flow + enum StoreSubscription: String, CaseIterable { - /// Thirty days non-renewable subscription + case thirtyDays = "net.mullvad.MullvadVPN.subscription.storekit2.30days" + case ninetyDays = "net.mullvad.MullvadVPN.subscription.storekit2.90days" + + var localizedTitle: String { + switch self { + case .thirtyDays: + return NSLocalizedString("Add 30 days time (%@)", comment: "") + case .ninetyDays: + return NSLocalizedString("Add 90 days time (%@)", comment: "") + } + } +} + +extension Product { + var customLocalizedTitle: String? { + guard let localizedTitle = StoreSubscription(rawValue: id)?.localizedTitle else { + return nil + } + return String(format: localizedTitle, displayPrice) + } +} + +// MARK: Legacy StoreKit flow + +enum LegacyStoreSubscription: String, CaseIterable { case thirtyDays = "net.mullvad.MullvadVPN.subscription.30days" case ninetyDays = "net.mullvad.MullvadVPN.subscription.90days" @@ -26,7 +51,7 @@ enum StoreSubscription: String, CaseIterable { extension SKProduct { var customLocalizedTitle: String? { - guard let localizedTitle = StoreSubscription(rawValue: productIdentifier)?.localizedTitle, + guard let localizedTitle = LegacyStoreSubscription(rawValue: productIdentifier)?.localizedTitle, let localizedPrice else { return nil @@ -35,7 +60,7 @@ extension SKProduct { } } -extension Set<StoreSubscription> { +extension Set<LegacyStoreSubscription> { var productIdentifiersSet: Set<String> { Set<String>(map { $0.rawValue }) } diff --git a/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift b/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift index a3e32b069f..2ef3ea8afd 100644 --- a/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift +++ b/ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift @@ -14,7 +14,7 @@ import MullvadLogging /// This class is thread safe. final class StoreTransactionLog: @unchecked Sendable { private let logger = Logger(label: "StoreTransactionLog") - private var transactionIdentifiers: Set<String> = [] + private(set) var transactionIdentifiers: Set<String> = [] private let stateLock = NSLock() /// The location of the transaction log file on disk. @@ -65,6 +65,13 @@ final class StoreTransactionLog: @unchecked Sendable { } } + /// Get transaction identifiers from transaction log. + func getTransactionIdentifiers() -> Set<String> { + stateLock.withLock { + transactionIdentifiers + } + } + /// Read transaction log from file. func read() { stateLock.withLock { diff --git a/ios/MullvadVPN/TunnelManager/TunnelManager.swift b/ios/MullvadVPN/TunnelManager/TunnelManager.swift index fde0ec5216..5c92c93e62b 100644 --- a/ios/MullvadVPN/TunnelManager/TunnelManager.swift +++ b/ios/MullvadVPN/TunnelManager/TunnelManager.swift @@ -602,10 +602,7 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable { // MARK: - StorePaymentObserver - func storePaymentManager( - _ manager: StorePaymentManager, - didReceiveEvent event: StorePaymentEvent - ) { + func storePaymentManager(didReceiveEvent event: LegacyStorePaymentEvent) { guard case let .finished(paymentCompletion) = event else { return } @@ -628,6 +625,10 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable { ) } + func storePaymentManager(didReceiveEvent event: StorePaymentEvent) { + // Not used. + } + // MARK: - TunnelInteractor var isConfigurationLoaded: Bool { @@ -786,7 +787,7 @@ final class TunnelManager: StorePaymentObserver, @unchecked Sendable { } } - fileprivate func setDeviceState(_ deviceState: DeviceState, persist: Bool) { + func setDeviceState(_ deviceState: DeviceState, persist: Bool) { nslock.lock() defer { nslock.unlock() } diff --git a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift index 362609e876..f76d4e0838 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountContentView.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountContentView.swift @@ -16,12 +16,6 @@ class AccountContentView: UIView { return button }() - let storeKit2PurchaseButton: AppButton = { - let button = AppButton(style: .success) - button.setTitle(NSLocalizedString("Make a purchase with StoreKit2", comment: ""), for: .normal) - return button - }() - let storeKit2RefundButton: AppButton = { let button = AppButton(style: .success) button.setTitle(NSLocalizedString("Refund last purchase with StoreKit2", comment: ""), for: .normal) @@ -83,7 +77,6 @@ class AccountContentView: UIView { var arrangedSubviews = [UIView]() #if DEBUG arrangedSubviews.append(redeemVoucherButton) - arrangedSubviews.append(storeKit2PurchaseButton) arrangedSubviews.append(storeKit2RefundButton) #endif arrangedSubviews.append(contentsOf: [ diff --git a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift index 055a658621..2aebffb4ba 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountInteractor.swift @@ -61,38 +61,4 @@ final class AccountInteractor: Sendable { func logout() async { await tunnelManager.unsetAccount() } - - // This function is for testing only - func getPaymentToken(for accountNumber: String) async -> Result<String, Error> { - await withCheckedContinuation { continuation in - _ = - apiProxy - .initStorekitPayment( - accountNumber: accountNumber, - retryStrategy: .noRetry, - completionHandler: { result in - continuation.resume(returning: result) - } - ) - } - } - - // This function is for testing only - func sendStoreKitReceipt( - _ transaction: VerificationResult<Transaction>, - for accountNumber: String - ) async -> Result<Void, Error> { - await withCheckedContinuation { c in - _ = - apiProxy - .checkStorekitPayment( - accountNumber: accountNumber, - transaction: StorekitTransaction(transaction: transaction.jwsRepresentation), - retryStrategy: .noRetry, - completionHandler: { result in - c.resume(returning: result) - } - ) - } - } } diff --git a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift index 7979ad6f0b..b28621e5ab 100644 --- a/ios/MullvadVPN/View controllers/Account/AccountViewController.swift +++ b/ios/MullvadVPN/View controllers/Account/AccountViewController.swift @@ -39,7 +39,7 @@ class AccountViewController: UIViewController, @unchecked Sendable { private var isFetchingProducts = false private var paymentState: PaymentState = .none - private let storeKit2TestProduct = StoreSubscription.thirtyDays.rawValue + private let storeKit2TestProduct = LegacyStoreSubscription.thirtyDays.rawValue var actionHandler: ActionHandler? @@ -130,10 +130,6 @@ class AccountViewController: UIViewController, @unchecked Sendable { contentView.logoutButton.addTarget(self, action: #selector(logOut), for: .touchUpInside) contentView.deleteButton.addTarget(self, action: #selector(deleteAccount), for: .touchUpInside) - contentView.storeKit2PurchaseButton.addTarget( - self, action: #selector(handleStoreKit2Purchase), - for: .touchUpInside - ) contentView.storeKit2RefundButton.addTarget( self, action: #selector(handleStoreKit2Refund), for: .touchUpInside @@ -176,7 +172,6 @@ class AccountViewController: UIViewController, @unchecked Sendable { contentView.logoutButton.isEnabled = isInteractionEnabled contentView.redeemVoucherButton.isEnabled = isInteractionEnabled contentView.deleteButton.isEnabled = isInteractionEnabled - contentView.storeKit2PurchaseButton.isEnabled = isInteractionEnabled contentView.storeKit2RefundButton.isEnabled = isInteractionEnabled navigationItem.rightBarButtonItem?.isEnabled = isInteractionEnabled @@ -220,59 +215,7 @@ class AccountViewController: UIViewController, @unchecked Sendable { actionHandler?(.showRestorePurchases) } - // This function is for testing only - @objc private func handleStoreKit2Purchase() { - guard let accountData = interactor.deviceState.accountData else { - return - } - - setPaymentState(.makingStoreKit2Purchase, animated: true) - - Task { - do { - let product = try await Product.products( - for: [ - storeKit2TestProduct - ] - ).first! - let token = - switch await interactor - .getPaymentToken(for: accountData.number) - { - case let .success(token): - UUID(uuidString: token)! - case let .failure(error): - throw error - } - - let result = try await product.purchase( - options: [.appAccountToken(token)] - ) - - switch result { - case let .success(verification): - let transaction = try checkVerified(verification) - await sendReceiptToAPI( - accountNumber: accountData.number, - receipt: verification - ) - await transaction.finish() - case .userCancelled: - print("User cancelled the purchase") - case .pending: - print("Purchase is pending") - @unknown default: - print("Unknown purchase result") - } - } catch { - print("Error: \(error)") - errorPresenter.showAlertForStoreKitError(error, context: .purchase) - } - - setPaymentState(.none, animated: true) - } - } - + // For testing StoreKit 2 refunds only. @objc private func handleStoreKit2Refund() { setPaymentState(.makingStoreKit2Refund, animated: true) @@ -301,33 +244,10 @@ class AccountViewController: UIViewController, @unchecked Sendable { } } catch { print("Error: \(error)") - errorPresenter.showAlertForStoreKitError(error, context: .purchase) + errorPresenter.showAlertForRefundError(error, context: .purchase) } setPaymentState(.none, animated: true) } } - - private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T { - switch result { - case .unverified: - throw StoreKit2Error.verificationFailed - case let .verified(safe): - return safe - } - } - - private func sendReceiptToAPI(accountNumber: String, receipt: VerificationResult<Transaction>) async { - switch await interactor.sendStoreKitReceipt(receipt, for: accountNumber) { - case .success: - print("Receipt sent successfully") - case let .failure(error): - print("Error sending receipt: \(error)") - errorPresenter.showAlertForStoreKitError(error, context: .purchase) - } - } -} - -private enum StoreKit2Error: Error { - case verificationFailed } diff --git a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift index 8b44ea34ac..941896a08a 100644 --- a/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift +++ b/ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift @@ -13,11 +13,17 @@ import Routing struct PaymentAlertPresenter { let alertContext: any Presenting - func showAlertForRefund(completion: (@MainActor @Sendable () -> Void)? = nil) { + // MARK: StoreKit 2 flow + + func showAlertForOutcome( + _ outcome: StorePaymentOutcome, + context: StorePaymentOutcome.Context, + completion: (@MainActor @Sendable () -> Void)? = nil + ) { let presentation = AlertPresentation( - id: "payment-refund-alert", - title: NSLocalizedString("Refund successful", comment: ""), - message: NSLocalizedString("Your purchase was successfully refunded.", comment: ""), + id: "payment-outcome-alert", + title: context.alertTitle, + message: outcome.alertMessage(for: context), buttons: [ AlertAction( title: NSLocalizedString("Got it!", comment: ""), @@ -34,14 +40,14 @@ struct PaymentAlertPresenter { } func showAlertForError( - _ error: StorePaymentManagerError, - context: REST.CreateApplePaymentResponse.Context, + _ error: StorePaymentError, + context: StorePaymentOutcome.Context, completion: (@MainActor @Sendable () -> Void)? = nil ) { let presentation = AlertPresentation( id: "payment-error-alert", title: context.errorTitle, - message: error.displayErrorDescription, + message: error.description, buttons: [ AlertAction( title: NSLocalizedString("Got it!", comment: ""), @@ -57,15 +63,17 @@ struct PaymentAlertPresenter { presenter.showAlert(presentation: presentation, animated: true) } - func showAlertForStoreKitError( - _ error: any Error, - context: REST.CreateApplePaymentResponse.Context, - completion: (() -> Void)? = nil + // MARK: Legacy StoreKit flow + + func showAlertForError( + _ error: LegacyStorePaymentManagerError, + context: StorePaymentOutcome.Context, + completion: (@MainActor @Sendable () -> Void)? = nil ) { let presentation = AlertPresentation( id: "payment-error-alert", title: context.errorTitle, - message: "\(error)", + message: error.displayErrorDescription, buttons: [ AlertAction( title: NSLocalizedString("Got it!", comment: ""), @@ -81,20 +89,37 @@ struct PaymentAlertPresenter { presenter.showAlert(presentation: presentation, animated: true) } - func showAlertForResponse( - _ response: REST.CreateApplePaymentResponse, - context: REST.CreateApplePaymentResponse.Context, - completion: (@MainActor @Sendable () -> Void)? = nil - ) { - guard case .noTimeAdded = response else { - completion?() - return - } + // MARK: StoreKit 2 refunds + + func showAlertForRefund(completion: (@MainActor @Sendable () -> Void)? = nil) { + let presentation = AlertPresentation( + id: "payment-refund-alert", + title: NSLocalizedString("Refund successful", comment: ""), + message: NSLocalizedString("Your purchase was successfully refunded.", comment: ""), + buttons: [ + AlertAction( + title: NSLocalizedString("Got it!", comment: ""), + style: .default, + handler: { + completion?() + } + ) + ] + ) + + let presenter = AlertPresenter(context: alertContext) + presenter.showAlert(presentation: presentation, animated: true) + } + func showAlertForRefundError( + _ error: any Error, + context: StorePaymentOutcome.Context, + completion: (() -> Void)? = nil + ) { let presentation = AlertPresentation( - id: "payment-response-alert", - title: response.alertTitle(context: context), - message: response.alertMessage(context: context), + id: "payment-refund-error-alert", + title: context.errorTitle, + message: "\(error)", buttons: [ AlertAction( title: NSLocalizedString("Got it!", comment: ""), diff --git a/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift b/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift index 86e14af5fc..f95a300b5f 100644 --- a/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift +++ b/ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift @@ -16,7 +16,7 @@ protocol InAppPurchaseViewControllerDelegate: AnyObject { class InAppPurchaseInteractor: @unchecked Sendable { let storePaymentManager: StorePaymentManager - var didFinishPayment: ((InAppPurchaseInteractor, StorePaymentEvent) -> Void)? + var didFinishPayment: ((InAppPurchaseInteractor, LegacyStorePaymentEvent) -> Void)? weak var viewControllerDelegate: InAppPurchaseViewControllerDelegate? private var paymentObserver: StorePaymentObserver? @@ -27,20 +27,24 @@ class InAppPurchaseInteractor: @unchecked Sendable { } private func addObservers() { - let paymentObserver = StorePaymentBlockObserver { [weak self] _, event in + let paymentObserver = StorePaymentBlockObserver { [weak self] event in guard let self else { return } viewControllerDelegate?.didEndPayment() didFinishPayment?(self, event) } - storePaymentManager.addPaymentObserver(paymentObserver) - - self.paymentObserver = paymentObserver + Task { + await storePaymentManager.addPaymentObserver(paymentObserver) + self.paymentObserver = paymentObserver + } } func purchase(accountNumber: String, product: SKProduct) { let payment = SKPayment(product: product) - storePaymentManager.addPayment(payment, for: accountNumber) - viewControllerDelegate?.didBeginPayment() + + Task { @MainActor in + viewControllerDelegate?.didBeginPayment() + await storePaymentManager.addPayment(payment, for: accountNumber) + } } } diff --git a/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift b/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift index 46ad7a6ebe..90522fd90e 100644 --- a/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift +++ b/ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift @@ -32,9 +32,14 @@ class InAppPurchaseViewController: UIViewController, StorePaymentObserver { self.errorPresenter = errorPresenter self.paymentAction = paymentAction super.init(nibName: nil, bundle: nil) - self.storePaymentManager.addPaymentObserver(self) + + Task { + await self.storePaymentManager.addPaymentObserver(self) + } + modalPresentationStyle = .overFullScreen modalTransitionStyle = .crossDissolve + view.addConstrainedSubviews([spinnerView]) { spinnerView.centerXAnchor.constraint(equalTo: view.centerXAnchor) spinnerView.centerYAnchor.constraint(equalTo: view.centerYAnchor) @@ -48,60 +53,185 @@ class InAppPurchaseViewController: UIViewController, StorePaymentObserver { override func viewDidLoad() { spinnerView.startAnimating() - let productIdentifiers = Set(StoreSubscription.allCases) - switch paymentAction { + + Task { + #if DEBUG + await handlePaymentAction(paymentAction) + #else + // NOTE! When enabling or disabling legacy payments, make sure + // to also enable/disable them in StorePaymentManager.start(). + await handleLegacyPaymentAction(paymentAction) + #endif + } + } + + // MARK: StoreKit 2 flow + + func handlePaymentAction(_ action: PaymentAction) async { + switch action { case .purchase: - _ = storePaymentManager.requestProducts( - with: productIdentifiers - ) { result in - Task { @MainActor [weak self] in - guard let self else { return } - self.spinnerView.stopAnimating() - switch result { - case let .success(success): - let products = success.products - guard !products.isEmpty else { - return - } - self.showPurchaseOptions(for: products) - case let .failure(failure as StorePaymentManagerError): - self.errorPresenter.showAlertForError(failure, context: .purchase) { - self.didFinish?() + await startPaymentFlow() + case .restorePurchase: + do { + let outcome = try await storePaymentManager.processOutstandingTransactions() + spinnerView.stopAnimating() + errorPresenter.showAlertForOutcome(outcome, context: .restoration) { + self.didFinish?() + } + } catch { + spinnerView.stopAnimating() + errorPresenter.showAlertForError(.unknown(error), context: .restoration) { + self.didFinish?() + } + } + } + } + + func startPaymentFlow() async { + var products: [Product] + do { + products = try await storePaymentManager.products() + } catch { + spinnerView.stopAnimating() + didFinish?() + return + } + + spinnerView.stopAnimating() + + guard !products.isEmpty else { + return + } + + showPurchaseOptions(for: products) + } + + func showPurchaseOptions(for products: [Product]) { + let localizedString = NSLocalizedString("Add Time", comment: "") + + let sheetController = UIAlertController( + title: localizedString, + message: nil, + preferredStyle: UIDevice.current.userInterfaceIdiom == .pad ? .alert : .actionSheet + ) + sheetController.overrideUserInterfaceStyle = .dark + sheetController.view.tintColor = .AlertController.tintColor + + products.sorted { $0.price < $1.price }.forEach { product in + guard let title = product.customLocalizedTitle else { return } + + let action = UIAlertAction( + title: title, style: .default, + handler: { _ in + sheetController.dismiss( + animated: true, + completion: { + self.spinnerView.startAnimating() + + Task { + await self.storePaymentManager.purchase(product: product) + } } - case .failure: - self.didFinish?() - } + ) } + ) + + action.accessibilityIdentifier = action.accessibilityIdentifier + sheetController.addAction(action) + } + + let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in + self.didFinish?() + } + cancelAction.accessibilityIdentifier = "action-sheet-cancel-button" + + sheetController.addAction(cancelAction) + present(sheetController, animated: true) + } + + @MainActor func storePaymentManager(didReceiveEvent event: StorePaymentEvent) { + spinnerView.stopAnimating() + + switch event { + case let .successfulPayment(outcome): + errorPresenter.showAlertForOutcome(outcome, context: .purchase) { + self.didFinish?() + } + case .pending, .userCancelled: + self.didFinish?() + case let .failed(error): + errorPresenter.showAlertForError(error, context: .purchase) { + self.didFinish?() } + @unknown default: + self.didFinish?() + } + } + + // MARK: Legacy StoreKit flow + + nonisolated func handleLegacyPaymentAction(_ action: PaymentAction) async { + switch paymentAction { + case .purchase: + await startLegacyPaymentFlow() case .restorePurchase: - _ = storePaymentManager.restorePurchases(for: accountNumber) { result in + _ = await storePaymentManager.restorePurchases(for: accountNumber) { result in Task { @MainActor [weak self] in guard let self else { return } - self.spinnerView.stopAnimating() + + spinnerView.stopAnimating() + switch result { case let .success(success): - self.errorPresenter.showAlertForResponse(success, context: .restoration) { + let outcome = StorePaymentOutcome.timeAdded(success.timeAdded) + errorPresenter.showAlertForOutcome(outcome, context: .restoration) { self.didFinish?() } - case let .failure(failure as StorePaymentManagerError): - self.errorPresenter.showAlertForError(failure, context: .restoration) { + case let .failure(failure as LegacyStorePaymentManagerError): + errorPresenter.showAlertForError(failure, context: .restoration) { self.didFinish?() } case .failure: - self.didFinish?() + didFinish?() } } } + } } - func purchase(product: SKProduct) { - let payment = SKPayment(product: product) - storePaymentManager.addPayment(payment, for: accountNumber) + func startLegacyPaymentFlow() { + let productIdentifiers = Set(LegacyStoreSubscription.allCases) + + _ = storePaymentManager.requestProducts( + with: productIdentifiers + ) { result in + Task { @MainActor [weak self] in + guard let self else { return } + + spinnerView.stopAnimating() + + switch result { + case let .success(success): + let products = success.products + guard !products.isEmpty else { + return + } + legacyShowPurchaseOptions(for: products) + case let .failure(failure as LegacyStorePaymentManagerError): + errorPresenter.showAlertForError(failure, context: .purchase) { + self.didFinish?() + } + case .failure: + didFinish?() + } + } + } } - func showPurchaseOptions(for products: [SKProduct]) { - let localizedString = NSLocalizedString("Add time", comment: "") + func legacyShowPurchaseOptions(for products: [SKProduct]) { + let localizedString = NSLocalizedString("Add Time", comment: "") + let sheetController = UIAlertController( title: localizedString, message: nil, @@ -109,48 +239,59 @@ class InAppPurchaseViewController: UIViewController, StorePaymentObserver { ) sheetController.overrideUserInterfaceStyle = .dark sheetController.view.tintColor = .AlertController.tintColor + products.sortedByPrice().forEach { product in guard let title = product.customLocalizedTitle else { return } + let action = UIAlertAction( - title: title, style: .default, + title: title, + style: .default, handler: { _ in sheetController.dismiss( animated: true, completion: { - self.purchase(product: product) self.spinnerView.startAnimating() - }) - }) - action - .accessibilityIdentifier = action.accessibilityIdentifier + + Task { + await self.storePaymentManager.addPayment( + SKPayment(product: product), + for: self.accountNumber + ) + } + } + ) + } + ) + + action.accessibilityIdentifier = action.accessibilityIdentifier sheetController.addAction(action) } let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in self.didFinish?() } - cancelAction.accessibilityIdentifier = "actoin-sheet-cancel-button" + cancelAction.accessibilityIdentifier = "action-sheet-cancel-button" + sheetController.addAction(cancelAction) present(sheetController, animated: true) } - nonisolated func storePaymentManager(_ manager: StorePaymentManager, didReceiveEvent event: StorePaymentEvent) { - Task { @MainActor in - spinnerView.stopAnimating() - switch event { - case let .finished(completion): - errorPresenter.showAlertForResponse(completion.serverResponse, context: .purchase) { - self.didFinish?() - } + @MainActor func storePaymentManager(didReceiveEvent event: LegacyStorePaymentEvent) { + spinnerView.stopAnimating() - case let .failure(paymentFailure): - switch paymentFailure.error { - case .storePayment(SKError.paymentCancelled): + switch event { + case let .finished(completion): + let outcome = StorePaymentOutcome.timeAdded(completion.serverResponse.timeAdded) + errorPresenter.showAlertForOutcome(outcome, context: .purchase) { + self.didFinish?() + } + case let .failure(paymentFailure): + switch paymentFailure.error { + case .storePayment(SKError.paymentCancelled): + self.didFinish?() + default: + errorPresenter.showAlertForError(paymentFailure.error, context: .purchase) { self.didFinish?() - default: - errorPresenter.showAlertForError(paymentFailure.error, context: .purchase) { - self.didFinish?() - } } } } diff --git a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift index ce6729c82d..3233f4c343 100644 --- a/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift +++ b/ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift @@ -160,7 +160,7 @@ extension VPNSettingsViewController: @preconcurrency VPNSettingsDataSourceDelega : NSLocalizedString("Disabling", comment: "") let description = NSLocalizedString( """ - “%@ Local network sharing” requires restarting the VPN connection, \ + %@ “Local network sharing” requires restarting the VPN connection, \ which will disconnect you and briefly expose your traffic. To prevent this, manually enable Airplane Mode and turn off Wi-Fi before continuing. Would you like to continue to enable “Local network sharing”? diff --git a/ios/MullvadVPNTests/MullvadLogging/LoggerBuilderTests.swift b/ios/MullvadVPNTests/MullvadLogging/LoggerBuilderTests.swift index 5330605541..f963d9732f 100644 --- a/ios/MullvadVPNTests/MullvadLogging/LoggerBuilderTests.swift +++ b/ios/MullvadVPNTests/MullvadLogging/LoggerBuilderTests.swift @@ -7,6 +7,7 @@ // import Testing + @testable import MullvadLogging struct LoggerBuilderTests { diff --git a/ios/translation/scripts/relays-localization.sh b/ios/translation/scripts/relays-localization.sh index 17de89cde5..8ea8cb5a79 100755 --- a/ios/translation/scripts/relays-localization.sh +++ b/ios/translation/scripts/relays-localization.sh @@ -22,6 +22,8 @@ extract_key() { countries=$(extract_key "country") cities=$(extract_key "city") +echo "Updating '$(basename "$OUTPUT_FILE")'." + all_locations=$(printf "%s\n%s\n" "$countries" "$cities" | awk '!seen[tolower($0)]++') { @@ -29,7 +31,7 @@ all_locations=$(printf "%s\n%s\n" "$countries" "$cities" | awk '!seen[tolower($0 echo echo "import Foundation" echo - echo "let allLocations: [String: String] = [" + echo "private let relayLocationList: [String: String] = [" while read -r name; do [ -z "$name" ] && continue echo " \"$name\": NSLocalizedString(\"$name\", comment: \"\")," diff --git a/mullvad-api/src/lib.rs b/mullvad-api/src/lib.rs index e3c0c8b9e8..ad42473173 100644 --- a/mullvad-api/src/lib.rs +++ b/mullvad-api/src/lib.rs @@ -614,15 +614,13 @@ impl AccountsProxy { #[cfg(target_os = "ios")] pub async fn check_storekit_payment( &self, - account: AccountNumber, body: Vec<u8>, ) -> Result<rest::Response<Incoming>, rest::Error> { let request = self .handle .factory .post_json_bytes(&format!("{APPLE_PAYMENT_URL_PREFIX}/check"), body)? - .expected_status(&[StatusCode::OK]) - .account(account)?; + .expected_status(&[StatusCode::OK]); self.handle.service.request(request).await } diff --git a/mullvad-encrypted-dns-proxy/src/config_resolver.rs b/mullvad-encrypted-dns-proxy/src/config_resolver.rs index aded03ee7d..c132aa0519 100644 --- a/mullvad-encrypted-dns-proxy/src/config_resolver.rs +++ b/mullvad-encrypted-dns-proxy/src/config_resolver.rs @@ -110,6 +110,7 @@ pub async fn resolve_config_with_resolverconfig( log::trace!("IPv6 {addr} parsed into proxy config: {proxy_config:?}"); proxy_configs.push(proxy_config); } + Err(config::Error::XorV1Unsupported) => continue, // ignore deprecated configs Err(e) => log::error!("IPv6 {addr} fails to parse to a proxy config: {e}"), } } diff --git a/mullvad-ios/Cargo.toml b/mullvad-ios/Cargo.toml index 519f7852c9..8f6e47a969 100644 --- a/mullvad-ios/Cargo.toml +++ b/mullvad-ios/Cargo.toml @@ -35,13 +35,6 @@ serde_json = { workspace = true } mockito = "1.6.1" async-trait = "0.1" -shadowsocks-service = { workspace = true, features = [ - "local", - "stream-cipher", - "local-http", - "local-tunnel", -] } - [target.'cfg(target_os = "macos")'.build-dependencies] cbindgen = { version = "0.28.0", default-features = false } diff --git a/mullvad-ios/src/api_client/storekit.rs b/mullvad-ios/src/api_client/storekit.rs index 3b89a04543..961d274a3a 100644 --- a/mullvad-ios/src/api_client/storekit.rs +++ b/mullvad-ios/src/api_client/storekit.rs @@ -179,8 +179,6 @@ async fn mullvad_ios_init_storekit_payment_inner( /// `retry_strategy` must have been created by a call to either of the following functions /// `mullvad_api_retry_strategy_never`, `mullvad_api_retry_strategy_constant` or `mullvad_api_retry_strategy_exponential` /// -/// `account_number` must be a pointer to a null terminated string. -/// /// `body` must be a pointer to a contiguous memory segment /// /// `body_size` must be the size of the body @@ -191,7 +189,6 @@ pub unsafe extern "C" fn mullvad_ios_check_storekit_payment( api_context: SwiftApiContext, completion_cookie: *mut libc::c_void, retry_strategy: SwiftRetryStrategy, - account_number: *const c_char, body: *const u8, body_size: usize, ) -> SwiftCancelHandle { @@ -209,16 +206,12 @@ pub unsafe extern "C" fn mullvad_ios_check_storekit_payment( let completion = completion_handler.clone(); - // SAFETY: See param documentation for `account_number`. - let account_number = AccountNumber::from(unsafe { get_string(account_number) }); - // SAFETY: See param documentation for `body`. let body = unsafe { std::slice::from_raw_parts(body, body_size) }.to_vec(); let task = tokio_handle.spawn(async move { match mullvad_ios_check_storekit_payment_inner( api_context.rest_handle(), retry_strategy, - account_number, body, ) .await @@ -237,13 +230,11 @@ pub unsafe extern "C" fn mullvad_ios_check_storekit_payment( async fn mullvad_ios_check_storekit_payment_inner( rest_client: MullvadRestHandle, retry_strategy: RetryStrategy, - account_number: AccountNumber, body: Vec<u8>, ) -> Result<SwiftMullvadApiResponse, rest::Error> { let account_proxy = AccountsProxy::new(rest_client); - let future_factory = - || account_proxy.check_storekit_payment(account_number.clone(), body.clone()); + let future_factory = || account_proxy.check_storekit_payment(body.clone()); do_request(retry_strategy, future_factory).await } diff --git a/mullvad-ios/src/encrypted_dns_proxy.rs b/mullvad-ios/src/encrypted_dns_proxy.rs deleted file mode 100644 index d4e0f5b0f9..0000000000 --- a/mullvad-ios/src/encrypted_dns_proxy.rs +++ /dev/null @@ -1,183 +0,0 @@ -use crate::ProxyHandle; - -use libc::c_char; -use mullvad_encrypted_dns_proxy::{ - Forwarder, - state::{EncryptedDnsProxyState as State, FetchConfigError}, -}; -use std::{ - io, mem, - net::{Ipv4Addr, SocketAddr}, - ptr, -}; -use tokio::{net::TcpListener, task::JoinHandle}; - -use std::ffi::CStr; - -/// A thin wrapper around [`mullvad_encrypted_dns_proxy::state::EncryptedDnsProxyState`] that -/// can start a local forwarder (see [`Self::start`]). -pub struct EncryptedDnsProxyState { - state: State, - domain: String, -} - -#[derive(Debug)] -pub enum Error { - /// Failed to initialize tokio runtime. - TokioRuntime, - /// Failed to bind a local listening socket, the one that will be forwarded through the proxy. - BindLocalSocket(io::Error), - /// Failed to get local listening address of the local listening socket. - GetBindAddr(io::Error), - /// Failed to initialize forwarder. - Forwarder(io::Error), - /// Failed to fetch a proxy configuration over DNS. - FetchConfig(FetchConfigError), - /// Failed to initialize with a valid configuration. - NoConfigs, -} - -impl From<Error> for i32 { - fn from(err: Error) -> Self { - match err { - Error::TokioRuntime => -1, - Error::BindLocalSocket(_) => -2, - Error::GetBindAddr(_) => -3, - Error::Forwarder(_) => -4, - Error::FetchConfig(_) => -5, - Error::NoConfigs => -6, - } - } -} - -impl EncryptedDnsProxyState { - async fn start(&mut self) -> Result<ProxyHandle, Error> { - self.state - .fetch_configs(&self.domain) - .await - .map_err(Error::FetchConfig)?; - let proxy_configuration = self.state.next_configuration().ok_or(Error::NoConfigs)?; - - let local_socket = Self::bind_local_addr() - .await - .map_err(Error::BindLocalSocket)?; - let bind_addr = local_socket.local_addr().map_err(Error::GetBindAddr)?; - let forwarder = Forwarder::connect(&proxy_configuration) - .await - .map_err(Error::Forwarder)?; - let join_handle = Box::new(tokio::spawn(async move { - if let Ok((client_conn, _)) = local_socket.accept().await { - let _ = forwarder.forward(client_conn).await; - } - })); - - Ok(ProxyHandle { - context: Box::into_raw(join_handle).cast(), - port: bind_addr.port(), - }) - } - - async fn bind_local_addr() -> io::Result<TcpListener> { - let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), 0); - TcpListener::bind(bind_addr).await - } -} - -/// Initializes a valid pointer to an instance of `EncryptedDnsProxyState`. -/// -/// # Safety -/// -/// * [domain_name] must not be non-null. -/// -/// * [domain_name] pointer must be [valid](core::ptr#safety) -/// -/// * The caller must ensure that the pointer to the [domain_name] string contains a nul terminator -/// at the end of the string. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn encrypted_dns_proxy_init( - domain_name: *const c_char, -) -> *mut EncryptedDnsProxyState { - let domain = { - // SAFETY: domain_name points to a valid region of memory and contains a nul terminator. - let c_str = unsafe { CStr::from_ptr(domain_name) }; - String::from_utf8_lossy(c_str.to_bytes()) - }; - - let state = Box::new(EncryptedDnsProxyState { - state: State::default(), - domain: domain.into_owned(), - }); - Box::into_raw(state) -} - -/// This must be called only once to deallocate `EncryptedDnsProxyState`. -/// -/// # Safety -/// `ptr` must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized -/// by `encrypted_dns_proxy_init`. This function is not thread safe, and should only be called -/// once. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn encrypted_dns_proxy_free(ptr: *mut EncryptedDnsProxyState) { - // SAFETY: See notes above - let _ = unsafe { Box::from_raw(ptr) }; -} - -/// # Safety -/// encrypted_dns_proxy must be a valid, exclusive pointer to `EncryptedDnsProxyState`, initialized -/// by `encrypted_dns_proxy_init`. This function is not thread safe. -/// `proxy_handle` must be pointing to a valid memory region for the size of a `ProxyHandle`. This -/// function is not thread safe, but it can be called repeatedly. Each successful invocation should -/// clean up the resulting proxy via `[encrypted_dns_proxy_stop]`. -/// -/// `proxy_handle` will only contain valid values if the return value is zero. It is still valid to -/// deallocate the memory. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn encrypted_dns_proxy_start( - encrypted_dns_proxy: *mut EncryptedDnsProxyState, - proxy_handle: *mut ProxyHandle, -) -> i32 { - let handle = match crate::mullvad_ios_runtime() { - Ok(handle) => handle, - Err(err) => { - log::error!("Cannot instantiate a tokio runtime: {}", err); - return Error::TokioRuntime.into(); - } - }; - - // SAFETY: See notes above - let mut encrypted_dns_proxy = unsafe { Box::from_raw(encrypted_dns_proxy) }; - let proxy_result = handle.block_on(encrypted_dns_proxy.start()); - mem::forget(encrypted_dns_proxy); - - match proxy_result { - // SAFETY: `proxy_handle` is guaranteed to be a valid pointer - Ok(handle) => unsafe { ptr::write(proxy_handle, handle) }, - Err(err) => { - let empty_handle = ProxyHandle { - context: ptr::null_mut(), - port: 0, - }; - // SAFETY: `proxy_handle` is guaranteed to be a valid pointer - unsafe { ptr::write(proxy_handle, empty_handle) } - log::error!("Failed to create a proxy connection: {err:?}"); - return err.into(); - } - } - - 0 -} - -/// # Safety -/// `proxy_config` must be a valid pointer to a `ProxyHandle` as initialized by -/// [`encrypted_dns_proxy_start`]. It should only ever be called once. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn encrypted_dns_proxy_stop(proxy_config: *mut ProxyHandle) -> i32 { - // SAFETY: See notes above - let ptr = unsafe { (*proxy_config).context }; - if !ptr.is_null() { - // SAFETY: `ptr` is guaranteed to be non-null and valid - let handle: Box<JoinHandle<()>> = unsafe { Box::from_raw(ptr.cast()) }; - handle.abort(); - } - 0i32 -} diff --git a/mullvad-ios/src/lib.rs b/mullvad-ios/src/lib.rs index 305e044fd5..9104271c89 100644 --- a/mullvad-ios/src/lib.rs +++ b/mullvad-ios/src/lib.rs @@ -6,9 +6,7 @@ use std::sync::OnceLock; use tokio::runtime::{Builder, Handle, Runtime}; mod api_client; -mod encrypted_dns_proxy; mod ephemeral_peer_proxy; -mod shadowsocks_proxy; pub mod tunnel_obfuscator_proxy; #[repr(C)] diff --git a/mullvad-ios/src/shadowsocks_proxy/ffi.rs b/mullvad-ios/src/shadowsocks_proxy/ffi.rs deleted file mode 100644 index bd376e6caf..0000000000 --- a/mullvad-ios/src/shadowsocks_proxy/ffi.rs +++ /dev/null @@ -1,120 +0,0 @@ -use super::{ShadowsocksHandle, run_forwarding_proxy}; -use crate::ProxyHandle; -use crate::api_client::helpers::parse_ip_addr; -use std::net::SocketAddr; -#[cfg(any(target_os = "macos", target_os = "ios"))] -use std::sync::Once; - -#[cfg(any(target_os = "macos", target_os = "ios"))] -static INIT_LOGGING: Once = Once::new(); - -/// # Safety -/// `addr`, `password`, `cipher` must be valid for the lifetime of this function call and they must -/// be backed by the amount of bytes as stored in the respective `*_len` parameters. -/// -/// `proxy_config` must be pointing to a valid memory region for the size of a `ProxyHandle` -/// instance. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn start_shadowsocks_proxy( - forward_address: *const u8, - forward_address_len: usize, - forward_port: u16, - addr: *const u8, - addr_len: usize, - port: u16, - password: *const u8, - password_len: usize, - cipher: *const u8, - cipher_len: usize, - proxy_config: *mut ProxyHandle, -) -> i32 { - #[cfg(any(target_os = "macos", target_os = "ios"))] - INIT_LOGGING.call_once(|| { - let _ = oslog::OsLogger::new("net.mullvad.MullvadVPN.ShadowSocks") - .level_filter(log::LevelFilter::Info) - .init(); - }); - - let forward_ip = if let Some(forward_address) = - { unsafe { parse_ip_addr(forward_address, forward_address_len) } } - { - forward_address - } else { - return -1; - }; - - let forward_socket_addr = SocketAddr::new(forward_ip, forward_port); - - let bridge_ip = if let Some(addr) = { unsafe { parse_ip_addr(addr, addr_len) } } { - addr - } else { - return -1; - }; - - let bridge_socket_addr = SocketAddr::new(bridge_ip, port); - - // SAFETY: See notes for `parse_str` - let password = if let Some(password) = unsafe { parse_str(password, password_len) } { - password - } else { - return -1; - }; - - // SAFETY: See notes for `parse_str` - let cipher = if let Some(cipher) = unsafe { parse_str(cipher, cipher_len) } { - cipher - } else { - return -1; - }; - - let (port, handle) = - match run_forwarding_proxy(forward_socket_addr, bridge_socket_addr, &password, &cipher) { - Ok((port, handle)) => (port, handle), - Err(err) => { - log::error!("Failed to run HTTP proxy {}", err); - return err.raw_os_error().unwrap_or(-1); - } - }; - let handle = Box::new(handle); - - // SAFETY: `proxy_config` is guaranteed to be writeable for the duration of this call. - // It does not overlap with `handle` - unsafe { - std::ptr::write( - proxy_config, - ProxyHandle { - port, - context: Box::into_raw(handle) as *mut _, - }, - ) - } - - 0 -} -/// # Safety -/// `proxy_config` must be pointing to a valid instance of a `ProxyInstance`, as instantiated by -/// `start_shadowsocks_proxy`. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn stop_shadowsocks_proxy(proxy_config: *mut ProxyHandle) -> i32 { - // SAFETY: `proxy_config` is guaranteed to be a valid pointer - let context_ptr = unsafe { (*proxy_config).context }; - if context_ptr.is_null() { - return -1; - } - - // SAFETY: `context_ptr` is guaranteed to be a valid, non-null pointer - let proxy_handle: Box<ShadowsocksHandle> = unsafe { Box::from_raw(context_ptr as *mut _) }; - proxy_handle.stop(); - // SAFETY: `proxy_config` is guaranteed to be a valid pointer - unsafe { (*proxy_config).context = std::ptr::null_mut() }; - 0 -} - -/// Allocates a new string with the contents of `data` if it contains only valid UTF-8 bytes. -/// -/// SAFETY: `data` must be a valid pointer to `len` amount of bytes -unsafe fn parse_str(data: *const u8, len: usize) -> Option<String> { - // SAFETY: data pointer must be valid for the size of len - let bytes = unsafe { std::slice::from_raw_parts(data, len) }; - String::from_utf8(bytes.to_vec()).ok() -} diff --git a/mullvad-ios/src/shadowsocks_proxy/mod.rs b/mullvad-ios/src/shadowsocks_proxy/mod.rs deleted file mode 100644 index 3e3b7b151f..0000000000 --- a/mullvad-ios/src/shadowsocks_proxy/mod.rs +++ /dev/null @@ -1,121 +0,0 @@ -use super::mullvad_ios_runtime; -use shadowsocks_service::{ - config::{ - Config, ConfigType, LocalConfig, LocalInstanceConfig, ProtocolType, ServerInstanceConfig, - }, - local::Server, - shadowsocks::{config::ServerConfig, crypto::CipherKind}, -}; -use std::{ - io, - net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener}, - str::FromStr, -}; -use tokio::task::JoinHandle; -mod ffi; - -pub fn run_forwarding_proxy( - forward_socket_addr: SocketAddr, - bridge_socket_addr: SocketAddr, - password: &str, - cipher: &str, -) -> io::Result<(u16, ShadowsocksHandle)> { - let runtime = - ShadowsocksService::new(forward_socket_addr, bridge_socket_addr, password, cipher)?; - let port = runtime.port(); - let handle = runtime.run()?; - - Ok((port, handle)) -} - -struct ShadowsocksService { - config: Config, - local_port: u16, -} - -pub struct ShadowsocksHandle { - abort_handle: JoinHandle<()>, -} - -impl ShadowsocksHandle { - pub fn stop(self) { - self.abort_handle.abort(); - } -} - -impl ShadowsocksService { - pub fn new( - forward_socket_addr: SocketAddr, - bridge_socket_addr: SocketAddr, - password: &str, - cipher: &str, - ) -> io::Result<Self> { - let (config, local_port) = - Self::create_config(forward_socket_addr, bridge_socket_addr, password, cipher)?; - Ok(Self { config, local_port }) - } - - pub fn port(&self) -> u16 { - self.local_port - } - - pub fn run(self) -> io::Result<ShadowsocksHandle> { - let runtime = mullvad_ios_runtime().map_err(io::Error::other)?; - - let abort_handle = runtime.spawn(async move { - self.run_service_inner().await; - }); - - Ok(ShadowsocksHandle { abort_handle }) - } - - async fn run_service_inner(self) { - let Self { config, .. } = self; - - let _ = Server::new(config) - .await - .expect("Could not create Shadowsocks server") - .run() - .await; - } - - pub fn create_config( - forward_socket_addr: SocketAddr, - bridge_socket_addr: SocketAddr, - password: &str, - cipher: &str, - ) -> io::Result<(Config, u16)> { - let mut cfg = Config::new(ConfigType::Local); - let free_port = get_free_port()?; - let bind_addr = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), free_port); - - let mut local_config = LocalConfig::new_with_addr(bind_addr.into(), ProtocolType::Tunnel); - local_config.forward_addr = Some(forward_socket_addr.into()); - cfg.local = vec![LocalInstanceConfig::with_local_config(local_config)]; - - let cipher = match CipherKind::from_str(cipher) { - Ok(cipher) => cipher, - Err(err) => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid cipher specified: {err}"), - )); - } - }; - let server_config = ServerInstanceConfig::with_server_config(ServerConfig::new( - bridge_socket_addr, - password, - cipher, - )); - - cfg.server = vec![server_config]; - - Ok((cfg, free_port)) - } -} - -fn get_free_port() -> io::Result<u16> { - let bind_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); - let port = TcpListener::bind(bind_addr)?.local_addr()?.port(); - Ok(port) -} diff --git a/mullvad-relay-selector/src/constants.rs b/mullvad-relay-selector/src/constants.rs index 5e6b511195..11872d6b42 100644 --- a/mullvad-relay-selector/src/constants.rs +++ b/mullvad-relay-selector/src/constants.rs @@ -1,4 +1,4 @@ //! Constants used throughout the relay selector /// All the valid ports when using UDP2TCP obfuscation. -pub(crate) const UDP2TCP_PORTS: [u16; 2] = [80, 5001]; +pub(crate) const UDP2TCP_PORTS: [u16; 3] = [80, 443, 5001]; diff --git a/mullvad-relay-selector/tests/relay_selector.rs b/mullvad-relay-selector/tests/relay_selector.rs index 18016cebc4..feef5fb602 100644 --- a/mullvad-relay-selector/tests/relay_selector.rs +++ b/mullvad-relay-selector/tests/relay_selector.rs @@ -807,7 +807,7 @@ fn test_selecting_endpoint_with_auto_obfuscation() { /// all configurations contain a valid port. #[test] fn test_selected_endpoints_use_correct_port_ranges() { - const TCP2UDP_PORTS: [u16; 2] = [80, 5001]; + const TCP2UDP_PORTS: [u16; 3] = [80, 443, 5001]; let relay_selector = default_relay_selector(); // Note that we do *not* specify any port here! let query = RelayQueryBuilder::new().udp2tcp().build(); diff --git a/nix/desktop-devshell.nix b/nix/desktop-devshell.nix index 0a329b3fdd..960d5dc9e8 100644 --- a/nix/desktop-devshell.nix +++ b/nix/desktop-devshell.nix @@ -1,7 +1,10 @@ { pkgs, desktop-toolchain }: pkgs.devshell.mkShell { name = "mullvad-desktop-devshell"; - packages = desktop-toolchain.packages ++ [ pkgs.cargo-insta ]; + packages = desktop-toolchain.packages ++ [ + pkgs.cargo-insta + pkgs.cargo-deny + ]; env = import ./desktop-env.nix { inherit pkgs; diff --git a/scripts/ios-localization b/scripts/ios-localization new file mode 100755 index 0000000000..6e83395d4a --- /dev/null +++ b/scripts/ios-localization @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +iOS_LOCALIZATION_DIR="$(cd "$SCRIPT_DIR/../ios/translation" && pwd)" +cd "$SCRIPT_DIR" + +# shellcheck disable=SC1091 +source utils/log + +# shellcheck disable=SC1091 +source utils/localization_utils + +function main { + case ${1:-""} in + upload) upload_to_crowdin ;; + download) download_from_crowdin ;; + "") + echo "Available subcommands: upload and download" + ;; + *) + echo "Unknown parameter: $1" + exit 1 + ;; + esac +} + +function update_relay_locations { + log_header "Retrieving relay locations from server list and updating RelayLocationList.Swift" + pushd "$iOS_LOCALIZATION_DIR" + ./scripts/fetch-relay-locations.sh + ./scripts/relays-localization.sh + popd +} + +function prepare_localization_strings { + update_relay_locations + update_ios_strings export + commit_changes "Export iOS strings" +} + +function upload_to_crowdin { + ensure_crowdin_api_key + prepare_localization_strings + log_header "Uploading iOS translations to Crowdin" + pushd "$iOS_LOCALIZATION_DIR" + crowdin upload sources + crowdin upload translations + popd +} + +function download_from_crowdin { + ensure_crowdin_api_key + log_header "Downloading iOS translations from Crowdin" + pushd "$iOS_LOCALIZATION_DIR" + crowdin download translations + popd + update_ios_strings import + commit_changes "Import iOS translations" +} + +function update_ios_strings { + if [ $# -ne 1 ] || { [ "$1" != "export" ] && [ "$1" != "import" ]; }; then + echo "Usage: update_ios_strings [export|import]" >&2 + return 2 + fi + if [ "$1" = "export" ]; then + log_header "Extracting strings from iOS source code" + else + log_header "Updating strings into iOS source code with new translations" + fi + pushd "$iOS_LOCALIZATION_DIR" + ./scripts/localizations.sh "$1" + popd +} + +main "$@" diff --git a/scripts/localization b/scripts/localization index 965ca0ff17..da05b455bc 100755 --- a/scripts/localization +++ b/scripts/localization @@ -8,6 +8,9 @@ cd "$SCRIPT_DIR" # shellcheck disable=SC1091 source utils/log +# shellcheck disable=SC1091 +source utils/localization_utils + function main { case ${1:-""} in prepare) prepare_localization_strings;; @@ -50,12 +53,6 @@ function update_relay_locations_pot { popd } -function commit_changes { - if ! git diff-index --quiet HEAD --; then - git commit -a -S -m "$1" - fi -} - function prepare_localization_strings { sync_localizations commit_changes "Update messages.pot" @@ -64,10 +61,6 @@ function prepare_localization_strings { commit_changes "Update relay-locations.pot" } -function ensure_crowdin_api_key { - test ! -z "$CROWDIN_API_KEY" -} - function upload_to_crowdin { ensure_crowdin_api_key diff --git a/scripts/utils/localization_utils b/scripts/utils/localization_utils new file mode 100755 index 0000000000..ff4031edcb --- /dev/null +++ b/scripts/utils/localization_utils @@ -0,0 +1,9 @@ +function commit_changes { + if ! git diff-index --quiet HEAD --; then + git commit -a -S -m "$1" + fi +} + +function ensure_crowdin_api_key { + test ! -z "$CROWDIN_API_KEY" +} diff --git a/talpid-core/src/ffi.rs b/talpid-core/src/firewall/windows/ffi.rs index dda7de9bd7..125ddacc69 100644 --- a/talpid-core/src/ffi.rs +++ b/talpid-core/src/firewall/windows/ffi.rs @@ -1,3 +1,5 @@ +//! Misc FFI utilities. + /// Creates a new result type that returns the given result variant on error. #[macro_export] macro_rules! ffi_error { diff --git a/talpid-core/src/firewall/windows/mod.rs b/talpid-core/src/firewall/windows/mod.rs index bae10cc76d..7ff82be50a 100644 --- a/talpid-core/src/firewall/windows/mod.rs +++ b/talpid-core/src/firewall/windows/mod.rs @@ -14,6 +14,8 @@ use crate::dns::ResolvedDnsConfig; mod hyperv; mod winfw; +#[macro_use] +mod ffi; const HYPERV_LEAK_WARNING_MSG: &str = "Hyper-V (e.g. WSL machines) may leak in blocked states."; diff --git a/talpid-core/src/firewall/windows/winfw/sys.rs b/talpid-core/src/firewall/windows/winfw/sys.rs index a47c2b4192..03d9dfcb8c 100644 --- a/talpid-core/src/firewall/windows/winfw/sys.rs +++ b/talpid-core/src/firewall/windows/winfw/sys.rs @@ -5,6 +5,7 @@ use talpid_windows::string::multibyte_to_wide; use windows_sys::Win32::Globalization::CP_ACP; use super::{Error, WideCString}; +use crate::ffi_error; pub const LOGGING_CONTEXT: &CStr = c"WinFw"; diff --git a/talpid-core/src/lib.rs b/talpid-core/src/lib.rs index 2bc6c5b4bc..bdf6643487 100644 --- a/talpid-core/src/lib.rs +++ b/talpid-core/src/lib.rs @@ -3,11 +3,6 @@ #![deny(missing_docs)] #![recursion_limit = "1024"] -/// Misc FFI utilities. -#[cfg(windows)] -#[macro_use] -mod ffi; - /// Window API wrappers and utilities #[cfg(target_os = "windows")] pub mod window; diff --git a/talpid-wireguard/src/gotatun/mod.rs b/talpid-wireguard/src/gotatun/mod.rs index 7cc6331a84..1ec9557ef8 100644 --- a/talpid-wireguard/src/gotatun/mod.rs +++ b/talpid-wireguard/src/gotatun/mod.rs @@ -687,7 +687,13 @@ pub fn get_tunnel_for_userspace( // Route everything into the tunnel and have WireGuard act as a firewall when // blocking. These will not necessarily be the actual routes used by android. Those will // be generated at a later stage e.g. if Local Network Sharing is enabled. - tun_config.routes = vec!["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]; + // If IPv6 is not enabled in the tunnel we should not route IPv6 traffic as this + // leads to leaks. + tun_config.routes = if config.ipv6_gateway.is_some() { + vec!["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()] + } else { + vec!["0.0.0.0/0".parse().unwrap()] + }; const MAX_PREPARE_TUN_ATTEMPTS: usize = 4; |
