summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2025-11-20 11:29:17 +0100
committerEmīls <emils@mullvad.net>2025-11-20 11:29:17 +0100
commit0b7747f64607f8f65e862135acf58118c6b4d29b (patch)
tree45cd158f9a083e9fe8d210664ae6f21be8a83464
parent8beacc616749851c1e6d1fb18fc4a493f3c961f6 (diff)
parent15ba64661b9ebb172c5baa3710b21105e611b805 (diff)
downloadmullvadvpn-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
-rw-r--r--CHANGELOG.md5
-rw-r--r--Cargo.lock115
-rw-r--r--android/CHANGELOG.md14
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/connectioninfo/ConnectionDetailPanel.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/WireguardConstant.kt2
-rwxr-xr-xbuild-windows-modules.sh7
-rw-r--r--desktop/packages/mullvad-vpn/src/renderer/components/views/udp-over-tcp-settings/components/udp-over-tcp-port-setting/UdpOverTcpPortSetting.tsx2
-rw-r--r--desktop/packages/mullvad-vpn/test/e2e/installed/state-dependent/tunnel-state.spec.ts4
-rw-r--r--dist-assets/android-version-code.txt2
-rw-r--r--dist-assets/android-version-name.txt2
-rw-r--r--dist-assets/desktop-product-version.txt2
-rw-r--r--docs/relay-selector.md2
-rw-r--r--ios/Assets/Localizable.xcstrings33
-rw-r--r--ios/Assets/RelayLocationList.swift2
-rw-r--r--ios/MullvadMockData/MullvadREST/APIProxy+Stubs.swift24
-rw-r--r--ios/MullvadREST/ApiHandlers/RESTAPIProxy.swift253
-rw-r--r--ios/MullvadREST/Assets/relays.json2
-rw-r--r--ios/MullvadREST/MullvadAPI/APIHandlers/MullvadAPIProxy.swift29
-rw-r--r--ios/MullvadREST/MullvadAPI/APIRequest/APIRequest.swift10
-rw-r--r--ios/MullvadREST/MullvadAPI/MullvadApiRequestFactory.swift2
-rw-r--r--ios/MullvadREST/RetryStrategy/RetryStrategy.swift24
-rw-r--r--ios/MullvadRustRuntime/EncryptedDNSProxy.swift63
-rw-r--r--ios/MullvadRustRuntime/ShadowSocksProxy.swift91
-rw-r--r--ios/MullvadRustRuntime/include/mullvad_rust_runtime.h91
-rw-r--r--ios/MullvadTypes/Storekit2.swift4
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj52
-rw-r--r--ios/MullvadVPN/AppDelegate.swift37
-rw-r--r--ios/MullvadVPN/Extensions/Bundle+ProductVersion.swift2
-rw-r--r--ios/MullvadVPN/Extensions/StorePaymentManagerError+Display.swift8
-rw-r--r--ios/MullvadVPN/StorePaymentManager/LegacyStorePaymentManager.swift476
-rw-r--r--ios/MullvadVPN/StorePaymentManager/SendStoreReceiptOperation.swift30
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentBlockObserver.swift14
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentEvent.swift65
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManager.swift610
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManagerDelegate.swift16
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManagerError.swift8
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentManagerInteractor.swift128
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentObserver.swift7
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StorePaymentOutcome.swift80
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StoreSubscription.swift33
-rw-r--r--ios/MullvadVPN/StorePaymentManager/StoreTransactionLog.swift9
-rw-r--r--ios/MullvadVPN/TunnelManager/TunnelManager.swift11
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountContentView.swift7
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountInteractor.swift34
-rw-r--r--ios/MullvadVPN/View controllers/Account/AccountViewController.swift86
-rw-r--r--ios/MullvadVPN/View controllers/Account/PaymentAlertPresenter.swift73
-rw-r--r--ios/MullvadVPN/View controllers/CreationAccount/InAppPurchaseInteractor.swift18
-rw-r--r--ios/MullvadVPN/View controllers/InAppPurchase/InAppPurchaseViewController.swift251
-rw-r--r--ios/MullvadVPN/View controllers/VPNSettings/VPNSettingsViewController.swift2
-rw-r--r--ios/MullvadVPNTests/MullvadLogging/LoggerBuilderTests.swift1
-rwxr-xr-xios/translation/scripts/relays-localization.sh4
-rw-r--r--mullvad-api/src/lib.rs4
-rw-r--r--mullvad-encrypted-dns-proxy/src/config_resolver.rs1
-rw-r--r--mullvad-ios/Cargo.toml7
-rw-r--r--mullvad-ios/src/api_client/storekit.rs11
-rw-r--r--mullvad-ios/src/encrypted_dns_proxy.rs183
-rw-r--r--mullvad-ios/src/lib.rs2
-rw-r--r--mullvad-ios/src/shadowsocks_proxy/ffi.rs120
-rw-r--r--mullvad-ios/src/shadowsocks_proxy/mod.rs121
-rw-r--r--mullvad-relay-selector/src/constants.rs2
-rw-r--r--mullvad-relay-selector/tests/relay_selector.rs2
-rw-r--r--nix/desktop-devshell.nix5
-rwxr-xr-xscripts/ios-localization78
-rwxr-xr-xscripts/localization13
-rwxr-xr-xscripts/utils/localization_utils9
-rw-r--r--talpid-core/src/firewall/windows/ffi.rs (renamed from talpid-core/src/ffi.rs)2
-rw-r--r--talpid-core/src/firewall/windows/mod.rs2
-rw-r--r--talpid-core/src/firewall/windows/winfw/sys.rs1
-rw-r--r--talpid-core/src/lib.rs5
-rw-r--r--talpid-wireguard/src/gotatun/mod.rs8
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;