diff options
| author | Jonathan <jonathan@mullvad.net> | 2023-06-29 15:14:40 +0200 |
|---|---|---|
| committer | Jonathan <jonathan@mullvad.net> | 2023-06-29 15:14:40 +0200 |
| commit | d3805f501cea6aa352de2c160f3a2f7bc8b93821 (patch) | |
| tree | ba94b7e0829f2fd57a9974b4590af59e9937e5be | |
| parent | 9e7bb470758385159ade1e22d5093b535a5e667c (diff) | |
| parent | 5f61ba4058337e6985cee99661a59a98ebff6dd0 (diff) | |
| download | mullvadvpn-d3805f501cea6aa352de2c160f3a2f7bc8b93821.tar.xz mullvadvpn-d3805f501cea6aa352de2c160f3a2f7bc8b93821.zip | |
Merge branch 'custom-lists'
43 files changed, 2171 insertions, 336 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ff6d74fa..973b96b0d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,9 @@ Line wrap the file at 100 chars. Th - Add settings view button in main view in the desktop app. - Add time left and device name in the header bar in the desktop app. +- Add customizable relay lists to the CLI on desktop. Custom lists can be managed through + `mullvad custom-lists` and can be selected through `mullvad relay set` and `mullvad bridge set`. + #### Android - Add UDP-over-TCP. - Prevent incoming connections from outside the VPN in Android 11+ when Local Network Sharing diff --git a/Cargo.lock b/Cargo.lock index 66d0b516e3..eba13a2ea9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2248,6 +2248,7 @@ dependencies = [ "regex", "serde", "talpid-types", + "uuid", ] [[package]] @@ -4467,6 +4468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ "getrandom 0.2.3", + "serde", ] [[package]] diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt index b2af9c989d..3ab59000ae 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt @@ -5,7 +5,7 @@ import android.os.Messenger import java.net.InetAddress import kotlinx.parcelize.Parcelize import net.mullvad.mullvadvpn.model.DnsOptions -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint import net.mullvad.mullvadvpn.model.ObfuscationSettings import net.mullvad.mullvadvpn.model.QuantumResistantState import net.mullvad.mullvadvpn.model.WireguardConstraints @@ -73,7 +73,8 @@ sealed class Request : Message.RequestMessage() { @Parcelize data class SetEnableSplitTunneling(val enable: Boolean) : Request() - @Parcelize data class SetRelayLocation(val relayLocation: LocationConstraint?) : Request() + @Parcelize + data class SetRelayLocation(val relayLocation: GeographicLocationConstraint?) : Request() @Parcelize data class SetWireGuardMtu(val mtu: Int?) : Request() diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt new file mode 100644 index 0000000000..04f92a72ac --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt @@ -0,0 +1,28 @@ +package net.mullvad.mullvadvpn.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +sealed class GeographicLocationConstraint : Parcelable { + abstract val location: GeoIpLocation + + @Parcelize + data class Country(val countryCode: String) : GeographicLocationConstraint() { + override val location: GeoIpLocation + get() = GeoIpLocation(null, null, countryCode, null, null) + } + + @Parcelize + data class City(val countryCode: String, val cityCode: String) : + GeographicLocationConstraint() { + override val location: GeoIpLocation + get() = GeoIpLocation(null, null, countryCode, cityCode, null) + } + + @Parcelize + data class Hostname(val countryCode: String, val cityCode: String, val hostname: String) : + GeographicLocationConstraint() { + override val location: GeoIpLocation + get() = GeoIpLocation(null, null, countryCode, cityCode, hostname) + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt index 2820a449b8..de7dd4e99b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt @@ -4,24 +4,7 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize sealed class LocationConstraint : Parcelable { - abstract val location: GeoIpLocation - - @Parcelize - data class Country(val countryCode: String) : LocationConstraint() { - override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, null, null) - } - - @Parcelize - data class City(val countryCode: String, val cityCode: String) : LocationConstraint() { - override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, cityCode, null) - } - @Parcelize - data class Hostname(val countryCode: String, val cityCode: String, val hostname: String) : - LocationConstraint() { - override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, cityCode, hostname) - } + data class Location(val location: GeographicLocationConstraint) : LocationConstraint() + @Parcelize data class CustomList(val listId: String) : LocationConstraint() } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt index 7afb2249d2..6f7b6760b0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt @@ -1,12 +1,13 @@ package net.mullvad.mullvadvpn.relaylist -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint data class Relay(val city: RelayCity, override val name: String, override val active: Boolean) : RelayItem { override val code = name override val type = RelayItemType.Relay - override val location = LocationConstraint.Hostname(city.country.code, city.code, name) + override val location = + GeographicLocationConstraint.Hostname(city.country.code, city.code, name) override val hasChildren = false override val visibleChildCount = 0 diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt index 9500c43795..c6244101f6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt @@ -1,6 +1,6 @@ package net.mullvad.mullvadvpn.relaylist -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint class RelayCity( val country: RelayCountry, @@ -10,7 +10,7 @@ class RelayCity( val relays: List<Relay> ) : RelayItem { override val type = RelayItemType.City - override val location = LocationConstraint.City(country.code, code) + override val location = GeographicLocationConstraint.City(country.code, code) override val active get() = relays.any { relay -> relay.active } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt index 447cc25ff2..d8424cacad 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt @@ -1,6 +1,6 @@ package net.mullvad.mullvadvpn.relaylist -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint class RelayCountry( override val name: String, @@ -9,7 +9,7 @@ class RelayCountry( val cities: List<RelayCity> ) : RelayItem { override val type = RelayItemType.Country - override val location = LocationConstraint.Country(code) + override val location = GeographicLocationConstraint.Country(code) override val active get() = cities.any { city -> city.active } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt index e5f28acee6..fde283fcdf 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt @@ -1,12 +1,12 @@ package net.mullvad.mullvadvpn.relaylist -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint interface RelayItem { val type: RelayItemType val name: String val code: String - val location: LocationConstraint + val location: GeographicLocationConstraint val active: Boolean val hasChildren: Boolean val visibleChildCount: Int diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt index b5aaed028a..60cbdd46cf 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt @@ -1,7 +1,7 @@ package net.mullvad.mullvadvpn.relaylist import net.mullvad.mullvadvpn.model.Constraint -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint class RelayList { val countries: List<RelayCountry> @@ -41,7 +41,7 @@ class RelayList { } fun findItemForLocation( - constraint: Constraint<LocationConstraint>, + constraint: Constraint<GeographicLocationConstraint>, expand: Boolean = false ): RelayItem? { when (constraint) { @@ -50,10 +50,10 @@ class RelayList { val location = constraint.value when (location) { - is LocationConstraint.Country -> { + is GeographicLocationConstraint.Country -> { return countries.find { country -> country.code == location.countryCode } } - is LocationConstraint.City -> { + is GeographicLocationConstraint.City -> { val country = countries.find { country -> country.code == location.countryCode } @@ -63,7 +63,7 @@ class RelayList { return country?.cities?.find { city -> city.code == location.cityCode } } - is LocationConstraint.Hostname -> { + is GeographicLocationConstraint.Hostname -> { val country = countries.find { country -> country.code == location.countryCode } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt index 7cc43925d7..7b0d419b45 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt @@ -19,6 +19,7 @@ import net.mullvad.mullvadvpn.model.GeoIpLocation import net.mullvad.mullvadvpn.model.RelaySettings import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.util.ExponentialBackoff +import net.mullvad.mullvadvpn.util.toGeographicLocationConstraint import net.mullvad.talpid.tunnel.ActionAfterDisconnect class LocationInfoCache(private val endpoint: ServiceEndpoint) { @@ -131,6 +132,6 @@ class LocationInfoCache(private val endpoint: ServiceEndpoint) { val settings = relaySettings as? RelaySettings.Normal val constraint = settings?.relayConstraints?.location as? Constraint.Only - selectedRelayLocation = constraint?.value?.location + selectedRelayLocation = constraint?.value?.toGeographicLocationConstraint()?.location } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt index 4fa531eeb4..7f4d274d6f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.trySendBlocking import net.mullvad.mullvadvpn.ipc.Event import net.mullvad.mullvadvpn.ipc.Request import net.mullvad.mullvadvpn.model.Constraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint import net.mullvad.mullvadvpn.model.LocationConstraint import net.mullvad.mullvadvpn.model.RelayConstraintsUpdate import net.mullvad.mullvadvpn.model.RelayList @@ -29,7 +30,7 @@ class RelayListListener(endpoint: ServiceEndpoint) { private val daemon = endpoint.intermittentDaemon private var selectedRelayLocation by - observable<LocationConstraint?>(null) { _, _, _ -> + observable<GeographicLocationConstraint?>(null) { _, _, _ -> commandChannel.trySendBlocking(Command.SetRelayLocation) } private var selectedWireguardConstraints by @@ -93,7 +94,10 @@ class RelayListListener(endpoint: ServiceEndpoint) { private suspend fun updateRelayConstraints() { val location: Constraint<LocationConstraint> = - selectedRelayLocation?.let { location -> Constraint.Only(location) } ?: Constraint.Any() + selectedRelayLocation?.let { location -> + Constraint.Only(LocationConstraint.Location(location)) + } + ?: Constraint.Any() val wireguardConstraints: WireguardConstraints? = selectedWireguardConstraints val update = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt index 46cf492d01..c6142694b9 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt @@ -5,13 +5,14 @@ import net.mullvad.mullvadvpn.ipc.Event import net.mullvad.mullvadvpn.ipc.EventDispatcher import net.mullvad.mullvadvpn.ipc.Request import net.mullvad.mullvadvpn.model.Constraint -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint import net.mullvad.mullvadvpn.model.PortRange import net.mullvad.mullvadvpn.model.RelayConstraints import net.mullvad.mullvadvpn.model.RelaySettings import net.mullvad.mullvadvpn.model.WireguardConstraints import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.relaylist.RelayList +import net.mullvad.mullvadvpn.util.toGeographicLocationConstraint class RelayListListener( private val connection: Messenger, @@ -25,12 +26,12 @@ class RelayListListener( var selectedRelayItem: RelayItem? = null private set - var selectedRelayLocation: LocationConstraint? + var selectedRelayLocation: GeographicLocationConstraint? get() { val settings = relaySettings as? RelaySettings.Normal val location = settings?.relayConstraints?.location as? Constraint.Only - return location?.value + return location?.value?.toGeographicLocationConstraint() } set(value) { connection.send(Request.SetRelayLocation(value).message) @@ -119,7 +120,10 @@ class RelayListListener( is RelaySettings.Normal -> { val location = relaySettings.relayConstraints.location - return relayList?.findItemForLocation(location, true) + return relayList?.findItemForLocation( + location.toGeographicLocationConstraint(), + true + ) } else -> { /* NOOP */ diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LocationConstraintExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LocationConstraintExtensions.kt new file mode 100644 index 0000000000..2637028111 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LocationConstraintExtensions.kt @@ -0,0 +1,22 @@ +package net.mullvad.mullvadvpn.util + +import net.mullvad.mullvadvpn.model.Constraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint +import net.mullvad.mullvadvpn.model.LocationConstraint + +fun LocationConstraint.toGeographicLocationConstraint(): GeographicLocationConstraint? = + when (this) { + is LocationConstraint.Location -> this.location + is LocationConstraint.CustomList -> null + } + +fun Constraint<LocationConstraint>.toGeographicLocationConstraint(): + Constraint<GeographicLocationConstraint> = + when (this) { + is Constraint.Only -> + when (this.value) { + is LocationConstraint.Location -> Constraint.Only(this.value.location) + is LocationConstraint.CustomList -> Constraint.Any() + } + is Constraint.Any -> Constraint.Any() + } diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt index fbb5008eb7..ba7ea30c41 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.TestCoroutineRule import net.mullvad.mullvadvpn.assertLists import net.mullvad.mullvadvpn.compose.state.SelectLocationUiState -import net.mullvad.mullvadvpn.model.LocationConstraint +import net.mullvad.mullvadvpn.model.GeographicLocationConstraint import net.mullvad.mullvadvpn.relaylist.RelayCountry import net.mullvad.mullvadvpn.relaylist.RelayItem import net.mullvad.mullvadvpn.relaylist.RelayList @@ -120,7 +120,7 @@ class SelectLocationViewModelTest { fun testSelectRelayAndClose() = runTest { // Arrange val mockRelayItem: RelayItem = mockk() - val mockLocation: LocationConstraint.Country = mockk(relaxed = true) + val mockLocation: GeographicLocationConstraint.Country = mockk(relaxed = true) val connectionProxyMock: ConnectionProxy = mockk(relaxUnitFun = true) every { mockRelayItem.location } returns mockLocation every { mockServiceConnectionManager.relayListListener() } returns mockRelayListListener diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index f3b744e522..f4d47c7916 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -1228,15 +1228,23 @@ function convertFromConnectionConfig( } } -function convertFromLocation(location: grpcTypes.RelayLocation.AsObject): RelayLocation { - if (location.hostname) { - return { hostname: [location.country, location.city, location.hostname] }; +function convertFromLocation(location: grpcTypes.LocationConstraint.AsObject): RelayLocation { + // FIXME: This is a hack that assumes that the LocationConstraint is not a custom list. + // If it is we just set the country to "any" even if that isn't correct. + if (location.location == undefined) { + return { country: 'any' }; } - if (location.city) { - return { city: [location.country, location.city] }; + const loc = location.location; + + if (loc.hostname) { + return { hostname: [loc.country, loc.city, loc.hostname] }; + } + + if (loc.city) { + return { city: [loc.country, loc.city] }; } - return { country: location.country }; + return { country: loc.country }; } function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObject): ITunnelOptions { @@ -1455,24 +1463,24 @@ function convertToNormalBridgeSettings( function convertToLocation( constraint: RelayLocation | undefined, -): grpcTypes.RelayLocation | undefined { +): grpcTypes.LocationConstraint | undefined { + const locationConstraint = new grpcTypes.LocationConstraint(); const location = new grpcTypes.RelayLocation(); if (constraint && 'hostname' in constraint) { const [countryCode, cityCode, hostname] = constraint.hostname; location.setCountry(countryCode); location.setCity(cityCode); location.setHostname(hostname); - return location; } else if (constraint && 'city' in constraint) { location.setCountry(constraint.city[0]); location.setCity(constraint.city[1]); - return location; } else if (constraint && 'country' in constraint) { location.setCountry(constraint.country); - return location; } else { return undefined; } + locationConstraint.setLocation(location); + return locationConstraint; } function convertToTunnelTypeConstraint( diff --git a/mullvad-cli/src/cmds/bridge.rs b/mullvad-cli/src/cmds/bridge.rs index 3a4061d9ca..3cc1a95b37 100644 --- a/mullvad-cli/src/cmds/bridge.rs +++ b/mullvad-cli/src/cmds/bridge.rs @@ -11,8 +11,7 @@ use mullvad_types::{ use std::net::{IpAddr, SocketAddr}; use talpid_types::net::openvpn::{self, SHADOWSOCKS_CIPHERS}; -use super::relay::find_relay_by_hostname; -use super::relay_constraints::LocationArgs; +use super::{relay::find_relay_by_hostname, relay_constraints::LocationArgs}; #[derive(Subcommand, Debug)] pub enum Bridge { @@ -53,6 +52,10 @@ pub enum SetCommands { )] Location(LocationArgs), + /// Set custom list to select relays from. Use the 'custom-lists list' + /// command to show available alternatives. + CustomList { custom_list_name: String }, + /// Set hosting provider(s) to select relays from. The 'list' /// command shows the available relays and their providers. Provider { @@ -150,27 +153,32 @@ impl Bridge { } async fn set(subcmd: SetCommands) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; match subcmd { SetCommands::State { policy } => { - let mut rpc = MullvadProxyClient::new().await?; rpc.set_bridge_state(policy).await?; println!("Updated bridge state"); Ok(()) } SetCommands::Location(location) => { - let mut rpc = MullvadProxyClient::new().await?; let countries = rpc.get_relay_locations().await?.countries; - let location_constraint = + let location = if let Some(relay) = find_relay_by_hostname(&countries, &location.country) { Constraint::Only(relay) } else { Constraint::from(location) }; - - Self::update_bridge_settings(Some(location_constraint), None, None).await + let location = location.map(LocationConstraint::Location); + Self::update_bridge_settings(&mut rpc, Some(location), None, None).await + } + SetCommands::CustomList { custom_list_name } => { + let list = rpc.get_custom_list(custom_list_name).await?; + let location = + Constraint::Only(LocationConstraint::CustomList { list_id: list.id }); + Self::update_bridge_settings(&mut rpc, Some(location), None, None).await } SetCommands::Ownership { ownership } => { - Self::update_bridge_settings(None, None, Some(ownership)).await + Self::update_bridge_settings(&mut rpc, None, None, Some(ownership)).await } SetCommands::Provider { providers } => { let providers = if providers[0].eq_ignore_ascii_case("any") { @@ -178,7 +186,7 @@ impl Bridge { } else { Constraint::Only(Providers::new(providers.into_iter()).unwrap()) }; - Self::update_bridge_settings(None, Some(providers), None).await + Self::update_bridge_settings(&mut rpc, None, Some(providers), None).await } SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await, } @@ -272,7 +280,9 @@ impl Bridge { } }, BridgeSettings::Normal(constraints) => { - println!("Bridge constraints: {constraints}") + let mut buf = String::new(); + let _ = constraints.format(&mut buf, &settings.custom_lists); + println!("Bridge constraints: {buf}") } }; Ok(()) @@ -360,12 +370,11 @@ impl Bridge { } async fn update_bridge_settings( + rpc: &mut MullvadProxyClient, location: Option<Constraint<LocationConstraint>>, providers: Option<Constraint<Providers>>, ownership: Option<Constraint<Ownership>>, ) -> Result<()> { - let mut rpc = MullvadProxyClient::new().await?; - let constraints = match rpc.get_settings().await?.bridge_settings { BridgeSettings::Normal(mut constraints) => { if let Some(new_location) = location { @@ -380,7 +389,9 @@ impl Bridge { constraints } _ => BridgeConstraints { - location: location.unwrap_or(Constraint::Any), + location: location + .unwrap_or(Constraint::Any) + .map(LocationConstraint::from), providers: providers.unwrap_or(Constraint::Any), ownership: ownership.unwrap_or(Constraint::Any), }, diff --git a/mullvad-cli/src/cmds/custom_lists.rs b/mullvad-cli/src/cmds/custom_lists.rs new file mode 100644 index 0000000000..9c0770a442 --- /dev/null +++ b/mullvad-cli/src/cmds/custom_lists.rs @@ -0,0 +1,110 @@ +use super::relay_constraints::LocationArgs; +use anyhow::Result; +use clap::Subcommand; +use mullvad_management_interface::MullvadProxyClient; +use mullvad_types::{ + custom_list::CustomListLocationUpdate, + relay_constraints::{Constraint, GeographicLocationConstraint}, +}; + +#[derive(Subcommand, Debug)] +pub enum CustomList { + /// Get names of custom lists + List, + + /// Retrieve a custom list by its name + Get { name: String }, + + /// Create a new custom list + Create { name: String }, + + /// Add a location to the list + Add { + name: String, + #[command(flatten)] + location: LocationArgs, + }, + + /// Remove a location from the list + Remove { + name: String, + #[command(flatten)] + location: LocationArgs, + }, + + /// Delete the custom list + Delete { name: String }, + + /// Rename a custom list to a new name + Rename { name: String, new_name: String }, +} + +impl CustomList { + pub async fn handle(self) -> Result<()> { + match self { + CustomList::List => Self::list().await, + CustomList::Get { name } => Self::get(name).await, + CustomList::Create { name } => Self::create_list(name).await, + CustomList::Add { name, location } => Self::add_location(name, location).await, + CustomList::Remove { name, location } => Self::remove_location(name, location).await, + CustomList::Delete { name } => Self::delete_list(name).await, + CustomList::Rename { name, new_name } => Self::rename_list(name, new_name).await, + } + } + + async fn list() -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + for custom_list in rpc.list_custom_lists().await? { + Self::print_custom_list(&custom_list); + } + Ok(()) + } + + async fn get(name: String) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let custom_list = rpc.get_custom_list(name).await?; + Self::print_custom_list(&custom_list); + Ok(()) + } + + async fn create_list(name: String) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.create_custom_list(name).await?; + Ok(()) + } + + async fn add_location(name: String, location_args: LocationArgs) -> Result<()> { + let location = Constraint::<GeographicLocationConstraint>::from(location_args); + let update = CustomListLocationUpdate::Add { name, location }; + let mut rpc = MullvadProxyClient::new().await?; + rpc.update_custom_list_location(update).await?; + Ok(()) + } + + async fn remove_location(name: String, location_args: LocationArgs) -> Result<()> { + let location = Constraint::<GeographicLocationConstraint>::from(location_args); + let update = CustomListLocationUpdate::Remove { name, location }; + let mut rpc = MullvadProxyClient::new().await?; + rpc.update_custom_list_location(update).await?; + Ok(()) + } + + async fn delete_list(name: String) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.delete_custom_list(name).await?; + Ok(()) + } + + async fn rename_list(name: String, new_name: String) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + rpc.rename_custom_list(name, new_name).await?; + Ok(()) + } + + fn print_custom_list(custom_list: &mullvad_types::custom_list::CustomList) { + println!("{}", custom_list.name); + for location in &custom_list.locations { + println!("\t{}", location); + } + } +} diff --git a/mullvad-cli/src/cmds/mod.rs b/mullvad-cli/src/cmds/mod.rs index 7adcf6c65b..8b7ca1a6be 100644 --- a/mullvad-cli/src/cmds/mod.rs +++ b/mullvad-cli/src/cmds/mod.rs @@ -5,6 +5,7 @@ pub mod account; pub mod auto_connect; pub mod beta_program; pub mod bridge; +pub mod custom_lists; pub mod dns; pub mod lan; pub mod lockdown; diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index 6e213726d9..83f4ad4fdd 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -5,9 +5,9 @@ use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{ location::Location, relay_constraints::{ - Constraint, LocationConstraint, Match, OpenVpnConstraints, Ownership, Provider, Providers, - RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, TransportPort, - WireguardConstraints, + Constraint, GeographicLocationConstraint, LocationConstraint, Match, OpenVpnConstraints, + Ownership, Provider, Providers, RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, + TransportPort, WireguardConstraints, }, relay_list::{RelayEndpointData, RelayListCountry}, ConnectionConfig, CustomTunnelEndpoint, @@ -64,6 +64,13 @@ pub enum SetCommands { )] Location(LocationArgs), + /// Set custom list to select relays from. Use the 'custom-lists list' + /// command to show available alternatives. + CustomList { + /// Name of the custom list to use + custom_list_name: String, + }, + /// Set hosting provider(s) to select relays from. The 'list' /// command shows the available relays and their providers. Provider { @@ -149,6 +156,8 @@ pub enum EntryLocation { \tmullvad relay set tunnel wireguard entry-location se-got-wg-004" )] EntryLocation(LocationArgs), + /// Name of custom list to use to pick entry endpoint. + CustomList { custom_list_name: String }, } #[derive(Subcommand, Debug, Clone)] @@ -203,8 +212,11 @@ impl Relay { async fn get() -> Result<()> { let mut rpc = MullvadProxyClient::new().await?; - let relay_settings = rpc.get_settings().await?.relay_settings; - println!("Current constraints: {relay_settings}"); + let settings = rpc.get_settings().await?; + let relay_settings = settings.relay_settings; + let mut buf = String::new(); + let _ = relay_settings.format(&mut buf, &settings.custom_lists); + println!("Current constraints: \n{}", buf); Ok(()) } @@ -302,6 +314,9 @@ impl Relay { match subcmd { SetCommands::Custom(subcmd) => Self::set_custom(subcmd).await, SetCommands::Location(location) => Self::set_location(location).await, + SetCommands::CustomList { custom_list_name } => { + Self::set_custom_list(custom_list_name).await + } SetCommands::Provider { providers } => Self::set_providers(providers).await, SetCommands::Ownership { ownership } => Self::set_ownership(ownership).await, SetCommands::Tunnel(subcmd) => Self::set_tunnel(subcmd).await, @@ -435,9 +450,10 @@ impl Relay { // The country field is assumed to be hostname due to CLI argument parsing find_relay_by_hostname(&countries, &location_constraint_args.country) { - Constraint::Only(relay) + Constraint::Only(LocationConstraint::Location(relay)) } else { - let location_constraint = Constraint::from(location_constraint_args); + let location_constraint: Constraint<GeographicLocationConstraint> = + Constraint::from(location_constraint_args); match &location_constraint { Constraint::Any => (), Constraint::Only(constraint) => { @@ -452,7 +468,7 @@ impl Relay { } } } - location_constraint + location_constraint.map(LocationConstraint::Location) }; Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { @@ -462,6 +478,17 @@ impl Relay { .await } + async fn set_custom_list(custom_list_name: String) -> Result<()> { + let mut rpc = MullvadProxyClient::new().await?; + let list_id = rpc.get_custom_list(custom_list_name).await?.id; + rpc.update_relay_settings(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + location: Some(Constraint::Only(LocationConstraint::CustomList { list_id })), + ..Default::default() + })) + .await?; + Ok(()) + } + async fn set_providers(providers: Vec<String>) -> Result<()> { let providers = if providers[0].eq_ignore_ascii_case("any") { Constraint::Any @@ -543,15 +570,23 @@ impl Relay { if let Some(use_multihop) = use_multihop { wireguard_constraints.use_multihop = *use_multihop; } - if let Some(EntryLocation::EntryLocation(entry)) = entry_location { - let countries = Self::get_filtered_relays().await?; - // The country field is assumed to be hostname due to CLI argument parsing - wireguard_constraints.entry_location = - if let Some(relay) = find_relay_by_hostname(&countries, &entry.country) { - Constraint::Only(relay) - } else { - Constraint::from(entry) - }; + match entry_location { + Some(EntryLocation::EntryLocation(entry)) => { + let countries = Self::get_filtered_relays().await?; + // The country field is assumed to be hostname due to CLI argument parsing + wireguard_constraints.entry_location = + if let Some(relay) = find_relay_by_hostname(&countries, &entry.country) { + Constraint::Only(LocationConstraint::Location(relay)) + } else { + Constraint::from(entry) + }; + } + Some(EntryLocation::CustomList { custom_list_name }) => { + let list = rpc.get_custom_list(custom_list_name).await?; + wireguard_constraints.entry_location = + Constraint::Only(LocationConstraint::CustomList { list_id: list.id }); + } + None => (), } Self::update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { @@ -613,7 +648,7 @@ fn parse_transport_port( pub fn find_relay_by_hostname( countries: &[RelayListCountry], hostname: &str, -) -> Option<LocationConstraint> { +) -> Option<GeographicLocationConstraint> { countries .iter() .flat_map(|country| country.cities.clone()) @@ -626,7 +661,7 @@ pub fn find_relay_by_hostname( city_code, .. }| { - LocationConstraint::Hostname(country_code, city_code, relay.hostname) + GeographicLocationConstraint::Hostname(country_code, city_code, relay.hostname) }, ) }) diff --git a/mullvad-cli/src/cmds/relay_constraints.rs b/mullvad-cli/src/cmds/relay_constraints.rs index 1fc5073a4d..626754edcb 100644 --- a/mullvad-cli/src/cmds/relay_constraints.rs +++ b/mullvad-cli/src/cmds/relay_constraints.rs @@ -1,7 +1,7 @@ use clap::Args; use mullvad_types::{ location::{CityCode, CountryCode, Hostname}, - relay_constraints::{Constraint, LocationConstraint}, + relay_constraints::{Constraint, GeographicLocationConstraint, LocationConstraint}, }; #[derive(Args, Debug, Clone)] @@ -14,21 +14,41 @@ pub struct LocationArgs { pub hostname: Option<Hostname>, } -impl From<LocationArgs> for Constraint<LocationConstraint> { +impl From<LocationArgs> for Constraint<GeographicLocationConstraint> { fn from(value: LocationArgs) -> Self { if value.country.eq_ignore_ascii_case("any") { return Constraint::Any; } match (value.country, value.city, value.hostname) { - (country, None, None) => Constraint::Only(LocationConstraint::Country(country)), + (country, None, None) => { + Constraint::Only(GeographicLocationConstraint::Country(country)) + } (country, Some(city), None) => { - Constraint::Only(LocationConstraint::City(country, city)) + Constraint::Only(GeographicLocationConstraint::City(country, city)) } + (country, Some(city), Some(hostname)) => Constraint::Only( + GeographicLocationConstraint::Hostname(country, city, hostname), + ), + _ => unreachable!("invalid location arguments"), + } + } +} + +impl From<LocationArgs> for Constraint<LocationConstraint> { + fn from(value: LocationArgs) -> Self { + if value.country.eq_ignore_ascii_case("any") { + return Constraint::Any; + } + + let location = match (value.country, value.city, value.hostname) { + (country, None, None) => GeographicLocationConstraint::Country(country), + (country, Some(city), None) => GeographicLocationConstraint::City(country, city), (country, Some(city), Some(hostname)) => { - Constraint::Only(LocationConstraint::Hostname(country, city, hostname)) + GeographicLocationConstraint::Hostname(country, city, hostname) } _ => unreachable!("invalid location arguments"), - } + }; + Constraint::Only(LocationConstraint::Location(location)) } } diff --git a/mullvad-cli/src/cmds/tunnel_state.rs b/mullvad-cli/src/cmds/tunnel_state.rs index 6e115c5ba1..76393091c5 100644 --- a/mullvad-cli/src/cmds/tunnel_state.rs +++ b/mullvad-cli/src/cmds/tunnel_state.rs @@ -2,8 +2,7 @@ use crate::format; use anyhow::{anyhow, Result}; use futures::{Stream, StreamExt}; use mullvad_management_interface::{client::DaemonEvent, MullvadProxyClient}; -use mullvad_types::device::DeviceState; -use mullvad_types::states::TunnelState; +use mullvad_types::{device::DeviceState, states::TunnelState}; pub async fn connect(wait: bool) -> Result<()> { let mut rpc = MullvadProxyClient::new().await?; diff --git a/mullvad-cli/src/main.rs b/mullvad-cli/src/main.rs index a36ec58a81..e383c0025d 100644 --- a/mullvad-cli/src/main.rs +++ b/mullvad-cli/src/main.rs @@ -112,6 +112,10 @@ enum Cli { /// Reset settings, caches, and logs FactoryReset, + + /// Manage custom lists + #[clap(subcommand)] + CustomLists(custom_lists::CustomList), } #[tokio::main] @@ -137,6 +141,7 @@ async fn main() -> Result<()> { #[cfg(any(target_os = "windows", target_os = "linux"))] Cli::SplitTunnel(cmd) => cmd.handle().await, Cli::Status { cmd, args } => status::handle(cmd, args).await, + Cli::CustomLists(cmd) => cmd.handle().await, #[cfg(all(unix, not(target_os = "android")))] Cli::ShellCompletions { shell, dir } => { diff --git a/mullvad-daemon/src/custom_lists.rs b/mullvad-daemon/src/custom_lists.rs new file mode 100644 index 0000000000..ce75e19bc5 --- /dev/null +++ b/mullvad-daemon/src/custom_lists.rs @@ -0,0 +1,330 @@ +use crate::{new_selector_config, settings, Daemon, EventListener}; +use mullvad_types::{ + custom_list::{CustomList, CustomListLocationUpdate, Id}, + relay_constraints::{ + BridgeSettings, BridgeState, Constraint, LocationConstraint, RelaySettings, + }, +}; +use talpid_types::net::TunnelType; + +#[derive(err_derive::Error, Debug)] +pub enum Error { + /// Custom list already exists + #[error(display = "A list with that name already exists")] + ListExists, + /// Custom list does not exist + #[error(display = "A list with that name does not exist")] + ListNotFound, + /// Can not add any to a custom list + #[error(display = "Can not add or remove 'any' to or from a custom list")] + CannotAddOrRemoveAny, + /// Location already exists in the custom list + #[error(display = "Location already exists in the custom list")] + LocationExists, + /// Location was not found in the list + #[error(display = "Location was not found in the list")] + LocationNotFoundInlist, + /// Custom list settings error + #[error(display = "Settings error")] + Settings(#[error(source)] settings::Error), +} + +impl<L> Daemon<L> +where + L: EventListener + Clone + Send + 'static, +{ + pub async fn delete_custom_list(&mut self, name: String) -> Result<(), Error> { + let custom_list = self.settings.custom_lists.get_custom_list_with_name(&name); + match &custom_list { + None => Err(Error::ListNotFound), + Some(custom_list) => { + let id = custom_list.id.clone(); + + let settings_changed = self + .settings + .update(|settings| { + let index = settings + .custom_lists + .custom_lists + .iter() + .position(|custom_list| custom_list.id == id) + .unwrap(); + // NOTE: Not using swap remove because it would make user output slightly + // more confusing and the cost is so small. + settings.custom_lists.custom_lists.remove(index); + }) + .await + .map_err(Error::Settings); + + if let Ok(true) = settings_changed { + let need_to_reconnect = self.change_should_cause_reconnect(&id); + + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector + .set_config(new_selector_config(&self.settings, &self.app_version_info)); + + if need_to_reconnect { + log::info!( + "Initiating tunnel restart because a selected custom list was deleted" + ); + self.reconnect_tunnel(); + } + } + + settings_changed.map(|_| ()) + } + } + } + + pub async fn create_custom_list(&mut self, name: String) -> Result<(), Error> { + if self + .settings + .custom_lists + .get_custom_list_with_name(&name) + .is_some() + { + return Err(Error::ListExists); + } + + let settings_changed = self + .settings + .update(|settings| { + let custom_list = CustomList::new(name); + settings.custom_lists.custom_lists.push(custom_list); + }) + .await + .map_err(Error::Settings); + + if let Ok(true) = settings_changed { + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector + .set_config(new_selector_config(&self.settings, &self.app_version_info)); + } + + settings_changed.map(|_| ()) + } + + pub async fn update_custom_list_location( + &mut self, + update: CustomListLocationUpdate, + ) -> Result<(), Error> { + match update { + CustomListLocationUpdate::Add { + name, + location: new_location, + } => { + if new_location.is_any() { + return Err(Error::CannotAddOrRemoveAny); + } + + if let Some(custom_list) = + self.settings.custom_lists.get_custom_list_with_name(&name) + { + let id = custom_list.id.clone(); + let new_location = new_location.unwrap(); + + let settings_changed = self + .settings + .update(|settings| { + let locations = &mut settings + .custom_lists + .custom_lists + .iter_mut() + .find(|custom_list| custom_list.id == id) + .unwrap() + .locations; + + if !locations.iter().any(|location| new_location == *location) { + locations.push(new_location); + } + }) + .await + .map_err(Error::Settings); + + if let Ok(true) = settings_changed { + let should_reconnect = self.change_should_cause_reconnect(&id); + + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector.set_config(new_selector_config( + &self.settings, + &self.app_version_info, + )); + + if should_reconnect { + log::info!( + "Initiating tunnel restart because a selected custom list changed" + ); + self.reconnect_tunnel(); + } + } else if let Ok(false) = settings_changed { + return Err(Error::LocationExists); + } + + settings_changed.map(|_| ()) + } else { + Err(Error::ListNotFound) + } + } + CustomListLocationUpdate::Remove { + name, + location: location_to_remove, + } => { + if location_to_remove.is_any() { + return Err(Error::CannotAddOrRemoveAny); + } + + if let Some(custom_list) = + self.settings.custom_lists.get_custom_list_with_name(&name) + { + let id = custom_list.id.clone(); + let location_to_remove = location_to_remove.unwrap(); + + let settings_changed = self + .settings + .update(|settings| { + let locations = &mut settings + .custom_lists + .custom_lists + .iter_mut() + .find(|custom_list| custom_list.id == id) + .unwrap() + .locations; + if let Some(index) = locations + .iter() + .position(|location| location == &location_to_remove) + { + locations.remove(index); + } + }) + .await + .map_err(Error::Settings); + + if let Ok(true) = settings_changed { + let should_reconnect = self.change_should_cause_reconnect(&id); + + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector.set_config(new_selector_config( + &self.settings, + &self.app_version_info, + )); + + if should_reconnect { + log::info!( + "Initiating tunnel restart because a selected custom list changed" + ); + self.reconnect_tunnel(); + } + } else if let Ok(false) = settings_changed { + return Err(Error::LocationNotFoundInlist); + } + + settings_changed.map(|_| ()) + } else { + Err(Error::ListNotFound) + } + } + } + } + + pub async fn rename_custom_list( + &mut self, + name: String, + new_name: String, + ) -> Result<(), Error> { + if self + .settings + .custom_lists + .get_custom_list_with_name(&new_name) + .is_some() + { + Err(Error::ListExists) + } else { + match self.settings.custom_lists.get_custom_list_with_name(&name) { + Some(custom_list) => { + let id = custom_list.id.clone(); + + let settings_changed = self + .settings + .update(|settings| { + settings + .custom_lists + .custom_lists + .iter_mut() + .find(|custom_list| custom_list.id == id) + .unwrap() + .name = new_name; + }) + .await; + + if let Ok(true) = settings_changed { + self.event_listener + .notify_settings(self.settings.to_settings()); + self.relay_selector.set_config(new_selector_config( + &self.settings, + &self.app_version_info, + )); + } + + Ok(()) + } + None => Err(Error::ListNotFound), + } + } + } + + fn change_should_cause_reconnect(&self, custom_list_id: &Id) -> bool { + use mullvad_types::states::TunnelState; + let mut need_to_reconnect = false; + + if let RelaySettings::Normal(relay_settings) = &self.settings.relay_settings { + if let Constraint::Only(LocationConstraint::CustomList { list_id }) = + &relay_settings.location + { + need_to_reconnect |= list_id == custom_list_id; + } + + if let TunnelState::Connecting { + endpoint, + location: _, + } + | TunnelState::Connected { + endpoint, + location: _, + } = &self.tunnel_state + { + match endpoint.tunnel_type { + TunnelType::Wireguard => { + if relay_settings.wireguard_constraints.use_multihop { + if let Constraint::Only(LocationConstraint::CustomList { list_id }) = + &relay_settings.wireguard_constraints.entry_location + { + need_to_reconnect |= list_id == custom_list_id; + } + } + } + + TunnelType::OpenVpn => { + if !matches!(self.settings.bridge_state, BridgeState::Off) { + if let BridgeSettings::Normal(bridge_settings) = + &self.settings.bridge_settings + { + if let Constraint::Only(LocationConstraint::CustomList { + list_id, + }) = &bridge_settings.location + { + need_to_reconnect |= list_id == custom_list_id; + } + } + } + } + } + } + } + + need_to_reconnect + } +} diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index 946c10a1f8..46ee8b25df 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -8,6 +8,7 @@ pub mod account_history; mod api; #[cfg(not(target_os = "android"))] mod cleanup; +mod custom_lists; pub mod device; mod dns; pub mod exception_logging; @@ -42,6 +43,7 @@ use mullvad_relay_selector::{ use mullvad_types::{ account::{AccountData, AccountToken, VoucherSubmission}, auth_failed::AuthFailed, + custom_list::{CustomList, CustomListLocationUpdate}, device::{Device, DeviceEvent, DeviceEventCause, DeviceId, DeviceState, RemoveDeviceEvent}, location::GeoIpLocation, relay_constraints::{BridgeSettings, BridgeState, ObfuscationSettings, RelaySettingsUpdate}, @@ -163,6 +165,9 @@ pub enum Error { #[error(display = "Tunnel state machine error")] TunnelError(#[error(source)] tunnel_state_machine::Error), + #[error(display = "Custom list error")] + CustomListError(#[error(source)] custom_lists::Error), + #[cfg(target_os = "macos")] #[error(display = "Failed to set exclusion group")] GroupIdError(#[error(source)] io::Error), @@ -242,6 +247,18 @@ pub enum DaemonCommand { RotateWireguardKey(ResponseTx<(), Error>), /// Return a public key of the currently set wireguard private key, if there is one GetWireguardKey(ResponseTx<Option<PublicKey>, Error>), + /// List custom lists + ListCustomLists(ResponseTx<Vec<CustomList>, Error>), + /// Get custom list + GetCustomList(ResponseTx<CustomList, Error>, String), + /// Create custom list + CreateCustomList(ResponseTx<(), Error>, String), + /// Delete custom list + DeleteCustomList(ResponseTx<(), Error>, String), + /// Update a custom list by adding or removing a location + UpdateCustomListLocation(ResponseTx<(), Error>, CustomListLocationUpdate), + /// Rename a custom list from the old name to a new name + RenameCustomList(ResponseTx<(), Error>, String, String), /// Get information about the currently running and latest app versions GetVersionInfo(oneshot::Sender<Option<AppVersionInfo>>), /// Return whether the daemon is performing post-upgrade tasks @@ -1016,6 +1033,16 @@ where GetSettings(tx) => self.on_get_settings(tx), RotateWireguardKey(tx) => self.on_rotate_wireguard_key(tx).await, GetWireguardKey(tx) => self.on_get_wireguard_key(tx).await, + ListCustomLists(tx) => self.on_list_custom_lists(tx), + GetCustomList(tx, name) => self.on_get_custom_list(tx, name), + CreateCustomList(tx, name) => self.on_create_custom_list(tx, name).await, + DeleteCustomList(tx, name) => self.on_delete_custom_list(tx, name).await, + UpdateCustomListLocation(tx, update) => { + self.on_update_custom_list_location(tx, update).await + } + RenameCustomList(tx, name, new_name) => { + self.on_rename_custom_list(tx, name, new_name).await + } GetVersionInfo(tx) => self.on_get_version_info(tx).await, IsPerformingPostUpgrade(tx) => self.on_is_performing_post_upgrade(tx), GetCurrentVersion(tx) => self.on_get_current_version(tx), @@ -2213,6 +2240,62 @@ where Self::oneshot_send(tx, result, "get_wireguard_key response"); } + fn on_list_custom_lists(&mut self, tx: ResponseTx<Vec<CustomList>, Error>) { + let result = self.settings.custom_lists.custom_lists.clone(); + Self::oneshot_send(tx, Ok(result), "list_custom_lists response"); + } + + fn on_get_custom_list(&mut self, tx: ResponseTx<CustomList, Error>, name: String) { + let result = self + .settings + .custom_lists + .get_custom_list_with_name(&name) + .cloned() + .ok_or(Error::CustomListError(custom_lists::Error::ListNotFound)); + Self::oneshot_send(tx, result, "create_custom_list response"); + } + + async fn on_create_custom_list(&mut self, tx: ResponseTx<(), Error>, name: String) { + let result = self + .create_custom_list(name) + .await + .map_err(Error::CustomListError); + Self::oneshot_send(tx, result, "create_custom_list response"); + } + + async fn on_delete_custom_list(&mut self, tx: ResponseTx<(), Error>, name: String) { + let result = self + .delete_custom_list(name) + .await + .map_err(Error::CustomListError); + Self::oneshot_send(tx, result, "delete_custom_list response"); + } + + async fn on_update_custom_list_location( + &mut self, + tx: ResponseTx<(), Error>, + update: CustomListLocationUpdate, + ) { + let result = self + .update_custom_list_location(update) + .await + .map_err(Error::CustomListError); + Self::oneshot_send(tx, result, "update_custom_list_location response"); + } + + async fn on_rename_custom_list( + &mut self, + tx: ResponseTx<(), Error>, + name: String, + new_name: String, + ) { + let result = self + .rename_custom_list(name, new_name) + .await + .map_err(Error::CustomListError); + Self::oneshot_send(tx, result, "rename_custom_list response"); + } + fn on_get_settings(&self, tx: oneshot::Sender<Settings>) { Self::oneshot_send(tx, self.settings.to_settings(), "get_settings response"); } @@ -2372,10 +2455,11 @@ fn new_selector_config( }; SelectorConfig { - relay_settings: settings.get_relay_settings(), + relay_settings: settings.relay_settings.clone(), bridge_state: settings.bridge_state, bridge_settings: settings.bridge_settings.clone(), obfuscation_settings: settings.obfuscation_settings.clone(), default_tunnel_type, + custom_lists: settings.custom_lists.clone(), } } diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index 76ebc56612..aa46c91584 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -1,4 +1,7 @@ -use crate::{account_history, device, settings, DaemonCommand, DaemonCommandSender, EventListener}; +use crate::{ + account_history, custom_lists, device, settings, DaemonCommand, DaemonCommandSender, + EventListener, +}; use futures::{ channel::{mpsc, oneshot}, StreamExt, @@ -581,6 +584,91 @@ impl ManagementService for ManagementServiceImpl { } } + // Custom lists + // + + async fn list_custom_lists( + &self, + _: Request<()>, + ) -> ServiceResult<mullvad_management_interface::types::CustomLists> { + log::debug!("list_custom_lists"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::ListCustomLists(tx))?; + self.wait_for_result(rx) + .await? + .map(|custom_lists| { + Response::new(mullvad_management_interface::types::CustomLists::from( + custom_lists, + )) + }) + .map_err(map_daemon_error) + } + + async fn get_custom_list( + &self, + request: Request<String>, + ) -> ServiceResult<mullvad_management_interface::types::CustomList> { + log::debug!("get_custom_list"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetCustomList(tx, request.into_inner()))?; + self.wait_for_result(rx) + .await? + .map(|custom_list| { + Response::new(mullvad_management_interface::types::CustomList::from( + custom_list, + )) + }) + .map_err(map_daemon_error) + } + + async fn create_custom_list(&self, request: Request<String>) -> ServiceResult<()> { + log::debug!("create_custom_list"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::CreateCustomList(tx, request.into_inner()))?; + self.wait_for_result(rx) + .await? + .map(Response::new) + .map_err(map_daemon_error) + } + + async fn delete_custom_list(&self, request: Request<String>) -> ServiceResult<()> { + log::debug!("delete_custom_list"); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::DeleteCustomList(tx, request.into_inner()))?; + self.wait_for_result(rx) + .await? + .map(Response::new) + .map_err(map_daemon_error) + } + + async fn update_custom_list_location( + &self, + request: Request<types::CustomListLocationUpdate>, + ) -> ServiceResult<()> { + log::debug!("update_custom_list_location"); + let custom_list = + mullvad_types::custom_list::CustomListLocationUpdate::try_from(request.into_inner())?; + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::UpdateCustomListLocation(tx, custom_list))?; + self.wait_for_result(rx) + .await? + .map(Response::new) + .map_err(map_daemon_error) + } + async fn rename_custom_list( + &self, + request: Request<types::CustomListRename>, + ) -> ServiceResult<()> { + log::debug!("rename_custom_list"); + let names: (String, String) = From::from(request.into_inner()); + let (tx, rx) = oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::RenameCustomList(tx, names.0, names.1))?; + self.wait_for_result(rx) + .await? + .map(Response::new) + .map_err(map_daemon_error) + } + // Split tunneling // @@ -937,6 +1025,7 @@ fn map_daemon_error(error: crate::Error) -> Status { DaemonError::NoAccountToken | DaemonError::NoAccountTokenHistory => { Status::unauthenticated(error.to_string()) } + DaemonError::CustomListError(error) => map_custom_list_error(error), error => Status::unknown(error.to_string()), } } @@ -1017,6 +1106,36 @@ fn map_account_history_error(error: account_history::Error) -> Status { } } +/// Converts an instance of [`mullvad_daemon::account_history::Error`] into a tonic status. +fn map_custom_list_error(error: custom_lists::Error) -> Status { + match error { + custom_lists::Error::ListExists => Status::with_details( + Code::AlreadyExists, + error.to_string(), + mullvad_management_interface::CUSTOM_LIST_LIST_EXISTS_DETAILS.into(), + ), + custom_lists::Error::ListNotFound => Status::with_details( + Code::NotFound, + error.to_string(), + mullvad_management_interface::CUSTOM_LIST_LIST_NOT_FOUND_DETAILS.into(), + ), + custom_lists::Error::CannotAddOrRemoveAny => { + Status::new(Code::InvalidArgument, error.to_string()) + } + custom_lists::Error::LocationExists => Status::with_details( + Code::AlreadyExists, + error.to_string(), + mullvad_management_interface::CUSTOM_LIST_LOCATION_EXISTS_DETAILS.into(), + ), + custom_lists::Error::LocationNotFoundInlist => Status::with_details( + Code::NotFound, + error.to_string(), + mullvad_management_interface::CUSTOM_LIST_LOCATION_NOT_FOUND_DETAILS.into(), + ), + custom_lists::Error::Settings(error) => map_settings_error(error), + } +} + fn map_protobuf_type_err(err: types::FromProtobufTypeError) -> Status { match err { types::FromProtobufTypeError::InvalidArgument(err) => Status::invalid_argument(err), diff --git a/mullvad-daemon/src/migrations/v6.rs b/mullvad-daemon/src/migrations/v6.rs index 54dc2268e4..3a842923f0 100644 --- a/mullvad-daemon/src/migrations/v6.rs +++ b/mullvad-daemon/src/migrations/v6.rs @@ -36,16 +36,55 @@ pub fn migrate(settings: &mut serde_json::Value) -> Result<()> { migrate_udp2tcp_port_443(settings); - // TODO - // log::info!("Migrating settings format to V7"); + migrate_location_constraint(settings)?; - // Note: Not incrementing the version number yet, since this migration is still open - // for future modification. - // settings["settings_version"] = serde_json::json!(SettingsVersion::V7); + log::info!("Migrating settings format to V7"); + + settings["settings_version"] = serde_json::json!(SettingsVersion::V7); + + Ok(()) +} + +fn migrate_location_constraint(settings: &mut serde_json::Value) -> Result<()> { + if let Some(location) = settings + .get_mut("relay_settings") + .and_then(|relay_settings| relay_settings.get_mut("normal")) + .and_then(|normal_relay_settings| normal_relay_settings.get_mut("location")) + { + wrap_location(location)?; + } + + if let Some(location) = settings + .get_mut("relay_settings") + .and_then(|relay_settings| relay_settings.get_mut("normal")) + .and_then(|normal_relay_settings| normal_relay_settings.get_mut("wireguard_constraints")) + .and_then(|normal_relay_settings| normal_relay_settings.get_mut("entry_location")) + { + wrap_location(location)?; + } + + if let Some(location) = settings + .get_mut("bridge_settings") + .and_then(|relay_settings| relay_settings.get_mut("normal")) + .and_then(|normal_relay_settings| normal_relay_settings.get_mut("location")) + { + wrap_location(location)?; + } Ok(()) } +fn wrap_location(location: &mut serde_json::Value) -> Result<()> { + if let Some(only) = location.get_mut("only") { + only["location"] = only.clone(); + let only = only.as_object_mut().ok_or(Error::InvalidSettingsContent)?; + only.remove("country"); + only.remove("city"); + only.remove("hostname"); + } + Ok(()) +} + fn migrate_pq_setting(settings: &mut serde_json::Value) -> Result<()> { if let Some(tunnel_options) = settings .get_mut("tunnel_options") @@ -89,7 +128,7 @@ fn version_matches(settings: &mut serde_json::Value) -> bool { #[cfg(test)] mod test { - use super::{migrate, migrate_pq_setting, version_matches}; + use super::{migrate, migrate_location_constraint, migrate_pq_setting, version_matches}; pub const V6_SETTINGS: &str = r#" { @@ -175,7 +214,9 @@ mod test { "normal": { "location": { "only": { - "country": "se" + "location": { + "country": "se" + } } }, "tunnel_protocol": "any", @@ -241,7 +282,7 @@ mod test { } } }, - "settings_version": 6 + "settings_version": 7 } "#; @@ -256,6 +297,252 @@ mod test { assert_eq!(&old_settings, &new_settings); } + /// For relay settings + /// location: { only: { country : "se" } } should be replaced with + /// location: { only: { location: { country: "se" } } } + #[test] + fn test_from_relay_settings_location_constraint_country() { + let mut migrated_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "location": { + "only": { + "country": "se" + } + } + } + } + } + "#, + ) + .unwrap(); + migrate_location_constraint(&mut migrated_settings).unwrap(); + + let expected_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "location": { + "only": { + "location": { + "country": "se" + } + } + } + } + } + } + "#, + ) + .unwrap(); + + assert_eq!(migrated_settings, expected_settings); + } + + /// For relay settings + /// location: { only: { country : "se", city: "got" } } should be replaced with + /// location: { only: { location: { country: "se", city: "got" } } } + #[test] + fn test_from_relay_settings_location_constraint_city() { + let mut migrated_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "location": { + "only": { + "country": "se", + "city": "got" + } + } + } + } + } + "#, + ) + .unwrap(); + migrate_location_constraint(&mut migrated_settings).unwrap(); + + let expected_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "location": { + "only": { + "location": { + "country": "se", + "city": "got" + } + } + } + } + } + } + "#, + ) + .unwrap(); + + assert_eq!(migrated_settings, expected_settings); + } + + /// For relay settings + /// location: { only: { country : "se", city: "got", hostname: "se-got-wg-001" } } should be + /// replaced with location: { only: { location: { country: "se", city: "got", hostname: + /// "se-got-wg-001" } } } + #[test] + fn test_from_relay_settings_location_constraint_hostname() { + let mut migrated_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "location": { + "only": { + "country": "se", + "city": "got", + "hostname": "se-got-wg-001" + } + } + } + } + } + "#, + ) + .unwrap(); + migrate_location_constraint(&mut migrated_settings).unwrap(); + + let expected_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "location": { + "only": { + "location": { + "country": "se", + "city": "got", + "hostname": "se-got-wg-001" + } + } + } + } + } + } + "#, + ) + .unwrap(); + + assert_eq!(migrated_settings, expected_settings); + } + + /// For bridge settings + /// location: { only: { country : "se", city: "got", hostname: "se-got-wg-001" } } should be + /// replaced with location: { only: { location: { country: "se", city: "got", hostname: + /// "se-got-wg-001" } } } + #[test] + fn test_from_bridge_location_constraint_hostname() { + let mut migrated_settings: serde_json::Value = serde_json::from_str( + r#" + { + "bridge_settings": { + "normal": { + "location": { + "only": { + "country": "se", + "city": "got", + "hostname": "se-got-wg-001" + } + } + } + } + } + "#, + ) + .unwrap(); + migrate_location_constraint(&mut migrated_settings).unwrap(); + + let expected_settings: serde_json::Value = serde_json::from_str( + r#" + { + "bridge_settings": { + "normal": { + "location": { + "only": { + "location": { + "country": "se", + "city": "got", + "hostname": "se-got-wg-001" + } + } + } + } + } + } + "#, + ) + .unwrap(); + + assert_eq!(migrated_settings, expected_settings); + } + + /// For wireguard constraints + /// location: { only: { country : "se", city: "got", hostname: "se-got-wg-001" } } should be + /// replaced with location: { only: { location: { country: "se", city: "got", hostname: + /// "se-got-wg-001" } } } + #[test] + fn test_from_wireguard_constraint_location_constraint_hostname() { + let mut migrated_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "wireguard_constraints": { + "entry_location": { + "only": { + "country": "se", + "city": "got", + "hostname": "se-got-wg-001" + } + } + } + } + } + } + "#, + ) + .unwrap(); + migrate_location_constraint(&mut migrated_settings).unwrap(); + + let expected_settings: serde_json::Value = serde_json::from_str( + r#" + { + "relay_settings": { + "normal": { + "wireguard_constraints": { + "entry_location": { + "only": { + "location": { + "country": "se", + "city": "got", + "hostname": "se-got-wg-001" + } + } + } + } + } + } + } + "#, + ) + .unwrap(); + + assert_eq!(migrated_settings, expected_settings); + } + /// use_pq_safe_psk=false should be replaced with quantum_resistant=null #[test] fn test_from_pq_safe_psk_false() { diff --git a/mullvad-daemon/src/settings.rs b/mullvad-daemon/src/settings.rs index 5e90067166..794b0381ca 100644 --- a/mullvad-daemon/src/settings.rs +++ b/mullvad-daemon/src/settings.rs @@ -377,7 +377,9 @@ mod test { "normal": { "location": { "only": { - "country": "gb" + "location": { + "country": "gb" + } } }, "tunnel_protocol": { @@ -414,7 +416,10 @@ mod test { } }, "settings_version": 5, - "show_beta_releases": false + "show_beta_releases": false, + "custom_lists": { + "custom_lists": [] + } }"#; let _ = SettingsPersister::load_from_bytes(settings).unwrap(); diff --git a/mullvad-jni/src/classes.rs b/mullvad-jni/src/classes.rs index e7d34f7966..88b5f6d938 100644 --- a/mullvad-jni/src/classes.rs +++ b/mullvad-jni/src/classes.rs @@ -20,14 +20,18 @@ pub const CLASSES: &[&str] = &[ "net/mullvad/mullvadvpn/model/DeviceState$LoggedOut", "net/mullvad/mullvadvpn/model/DeviceState$Revoked", "net/mullvad/mullvadvpn/model/RemoveDeviceEvent", + "net/mullvad/mullvadvpn/model/GeographicLocationConstraint", + "net/mullvad/mullvadvpn/model/GeographicLocationConstraint$City", + "net/mullvad/mullvadvpn/model/GeographicLocationConstraint$Country", + "net/mullvad/mullvadvpn/model/GeographicLocationConstraint$Hostname", "net/mullvad/mullvadvpn/model/GeoIpLocation", "net/mullvad/mullvadvpn/model/GetAccountDataResult$Ok", "net/mullvad/mullvadvpn/model/GetAccountDataResult$InvalidAccount", "net/mullvad/mullvadvpn/model/GetAccountDataResult$RpcError", "net/mullvad/mullvadvpn/model/GetAccountDataResult$OtherError", - "net/mullvad/mullvadvpn/model/LocationConstraint$City", - "net/mullvad/mullvadvpn/model/LocationConstraint$Country", - "net/mullvad/mullvadvpn/model/LocationConstraint$Hostname", + "net/mullvad/mullvadvpn/model/LocationConstraint", + "net/mullvad/mullvadvpn/model/LocationConstraint$Location", + "net/mullvad/mullvadvpn/model/LocationConstraint$CustomList", "net/mullvad/mullvadvpn/model/ObfuscationSettings", "net/mullvad/mullvadvpn/model/PublicKey", "net/mullvad/mullvadvpn/model/QuantumResistantState", diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto index 9f967c7531..f82f55078a 100644 --- a/mullvad-management-interface/proto/management_interface.proto +++ b/mullvad-management-interface/proto/management_interface.proto @@ -68,6 +68,14 @@ service ManagementService { rpc RotateWireguardKey(google.protobuf.Empty) returns (google.protobuf.Empty) {} rpc GetWireguardKey(google.protobuf.Empty) returns (PublicKey) {} + // Custom lists + rpc ListCustomLists(google.protobuf.Empty) returns (CustomLists) {} + rpc GetCustomList(google.protobuf.StringValue) returns (CustomList) {} + rpc CreateCustomList(google.protobuf.StringValue) returns (google.protobuf.Empty) {} + rpc DeleteCustomList(google.protobuf.StringValue) returns (google.protobuf.Empty) {} + rpc UpdateCustomListLocation(CustomListLocationUpdate) returns (google.protobuf.Empty) {} + rpc RenameCustomList(CustomListRename) returns (google.protobuf.Empty) {} + // Split tunneling (Linux) rpc GetSplitTunnelProcesses(google.protobuf.Empty) returns (stream google.protobuf.Int32Value) {} rpc AddSplitTunnelProcess(google.protobuf.Int32Value) returns (google.protobuf.Empty) {} @@ -248,7 +256,7 @@ enum Ownership { message BridgeSettings { message BridgeConstraints { - RelayLocation location = 1; + LocationConstraint location = 1; repeated string providers = 2; Ownership ownership = 3; } @@ -279,6 +287,13 @@ message BridgeSettings { } } +message LocationConstraint { + oneof type { + string customList = 1; + RelayLocation location = 2; + } +} + message RelayLocation { string country = 1; string city = 2; @@ -306,6 +321,31 @@ message ObfuscationSettings { Udp2TcpObfuscationSettings udp2tcp = 2; } +message CustomListRename { + string name = 1; + string new_name = 2; +} + +message CustomListLocationUpdate { + enum State { + ADD = 0; + REMOVE = 1; + } + State state = 1; + string name = 2; + RelayLocation location = 3; +} + +message CustomList { + string id = 1; + string name = 2; + repeated RelayLocation locations = 3; +} + +message CustomLists { repeated CustomList custom_lists = 1; } + +message CustomListSettings { repeated CustomList custom_lists = 1; } + message Settings { RelaySettings relay_settings = 1; BridgeSettings bridge_settings = 2; @@ -317,6 +357,7 @@ message Settings { bool show_beta_releases = 8; SplitTunnelSettings split_tunnel = 9; ObfuscationSettings obfuscation_settings = 10; + CustomListSettings custom_lists = 11; } message SplitTunnelSettings { @@ -334,7 +375,7 @@ message RelaySettings { message TunnelTypeConstraint { TunnelType tunnel_type = 1; } message NormalRelaySettings { - RelayLocation location = 1; + LocationConstraint location = 1; repeated string providers = 2; TunnelTypeConstraint tunnel_type = 3; WireguardConstraints wireguard_constraints = 4; @@ -344,7 +385,7 @@ message NormalRelaySettings { // Constraints are only updated for fields that are provided message NormalRelaySettingsUpdate { - RelayLocation location = 1; + LocationConstraint location = 1; ProviderUpdate providers = 2; TunnelTypeUpdate tunnel_type = 3; WireguardConstraints wireguard_constraints = 4; @@ -376,7 +417,7 @@ message WireguardConstraints { uint32 port = 1; IpVersionConstraint ip_version = 2; bool use_multihop = 3; - RelayLocation entry_location = 4; + LocationConstraint entry_location = 4; } message CustomRelaySettings { diff --git a/mullvad-management-interface/src/client.rs b/mullvad-management-interface/src/client.rs index b15f076c46..b8c0b32085 100644 --- a/mullvad-management-interface/src/client.rs +++ b/mullvad-management-interface/src/client.rs @@ -4,6 +4,7 @@ use crate::types; use futures::{Stream, StreamExt}; use mullvad_types::{ account::{AccountData, AccountToken, VoucherSubmission}, + custom_list::{CustomList, CustomListLocationUpdate}, device::{Device, DeviceEvent, DeviceId, DeviceState, RemoveDeviceEvent}, location::GeoIpLocation, relay_constraints::{BridgeSettings, BridgeState, ObfuscationSettings, RelaySettingsUpdate}, @@ -429,6 +430,65 @@ impl MullvadProxyClient { PublicKey::try_from(key).map_err(Error::InvalidResponse) } + pub async fn list_custom_lists(&mut self) -> Result<Vec<CustomList>> { + let result = self + .0 + .list_custom_lists(()) + .await + .map_err(map_custom_list_error)? + .into_inner() + .try_into() + .map_err(Error::InvalidResponse)?; + Ok(result) + } + + pub async fn get_custom_list(&mut self, name: String) -> Result<CustomList> { + let result = self + .0 + .get_custom_list(name) + .await + .map_err(map_custom_list_error)? + .into_inner() + .try_into() + .map_err(Error::InvalidResponse)?; + Ok(result) + } + + pub async fn create_custom_list(&mut self, name: String) -> Result<()> { + self.0 + .create_custom_list(name) + .await + .map_err(map_custom_list_error)?; + Ok(()) + } + + pub async fn delete_custom_list(&mut self, name: String) -> Result<()> { + self.0 + .delete_custom_list(name) + .await + .map_err(map_custom_list_error)?; + Ok(()) + } + + pub async fn update_custom_list_location( + &mut self, + custom_list_update: CustomListLocationUpdate, + ) -> Result<()> { + self.0 + .update_custom_list_location(types::CustomListLocationUpdate::from(custom_list_update)) + .await + .map_err(map_custom_list_error)?; + Ok(()) + } + + pub async fn rename_custom_list(&mut self, name: String, new_name: String) -> Result<()> { + self.0 + .rename_custom_list(types::CustomListRename::from((name, new_name))) + .await + .map_err(map_custom_list_error)?; + Ok(()) + } + #[cfg(target_os = "linux")] pub async fn get_split_tunnel_processes(&mut self) -> Result<Vec<i32>> { use futures::TryStreamExt; @@ -550,3 +610,30 @@ fn map_location_error(status: Status) -> Error { _other => Error::Rpc(status), } } + +fn map_custom_list_error(status: Status) -> Error { + match status.code() { + Code::NotFound => { + let details = status.details(); + if details == crate::CUSTOM_LIST_LOCATION_NOT_FOUND_DETAILS { + Error::LocationNotFoundInCustomlist + } else if details == crate::CUSTOM_LIST_LIST_NOT_FOUND_DETAILS { + Error::CustomListListNotFound + } else { + Error::Rpc(status) + } + } + Code::AlreadyExists => { + let details = status.details(); + if details == crate::CUSTOM_LIST_LOCATION_EXISTS_DETAILS { + Error::LocationExistsInCustomList + } else if details == crate::CUSTOM_LIST_LIST_EXISTS_DETAILS { + Error::CustomListExists + } else { + Error::Rpc(status) + } + } + Code::InvalidArgument => Error::CustomListCannotAddOrRemoveAny, + _other => Error::Rpc(status), + } +} diff --git a/mullvad-management-interface/src/lib.rs b/mullvad-management-interface/src/lib.rs index 002d122435..7009de0740 100644 --- a/mullvad-management-interface/src/lib.rs +++ b/mullvad-management-interface/src/lib.rs @@ -26,6 +26,11 @@ lazy_static::lazy_static! { .ok(); } +pub const CUSTOM_LIST_LOCATION_NOT_FOUND_DETAILS: &[u8] = b"custom_list_location_not_found"; +pub const CUSTOM_LIST_LIST_NOT_FOUND_DETAILS: &[u8] = b"custom_list_list_not_found"; +pub const CUSTOM_LIST_LOCATION_EXISTS_DETAILS: &[u8] = b"custom_list_location_exists"; +pub const CUSTOM_LIST_LIST_EXISTS_DETAILS: &[u8] = b"custom_list_list_exists"; + #[derive(err_derive::Error, Debug)] #[error(no_from)] pub enum Error { @@ -88,6 +93,21 @@ pub enum Error { #[error(display = "Location data is unavailable")] NoLocationData, + + #[error(display = "A custom list with that name already exists")] + CustomListExists, + + #[error(display = "A custom list with that name does not exist")] + CustomListListNotFound, + + #[error(display = "Can not add or remove 'any' to or from a custom list")] + CustomListCannotAddOrRemoveAny, + + #[error(display = "Location already exists in the custom list")] + LocationExistsInCustomList, + + #[error(display = "Location was not found in the custom list")] + LocationNotFoundInCustomlist, } #[deprecated(note = "Prefer MullvadProxyClient")] diff --git a/mullvad-management-interface/src/types/conversions/custom_list.rs b/mullvad-management-interface/src/types/conversions/custom_list.rs new file mode 100644 index 0000000000..61e4ce9478 --- /dev/null +++ b/mullvad-management-interface/src/types/conversions/custom_list.rs @@ -0,0 +1,200 @@ +use crate::types::{proto, FromProtobufTypeError}; +use mullvad_types::{custom_list::Id, relay_constraints::GeographicLocationConstraint}; +use proto::RelayLocation; + +impl From<(String, String)> for proto::CustomListRename { + fn from(names: (String, String)) -> Self { + proto::CustomListRename { + name: names.0, + new_name: names.1, + } + } +} + +impl From<proto::CustomListRename> for (String, String) { + fn from(names: proto::CustomListRename) -> Self { + (names.name, names.new_name) + } +} + +impl From<&mullvad_types::custom_list::CustomListsSettings> for proto::CustomListSettings { + fn from(settings: &mullvad_types::custom_list::CustomListsSettings) -> Self { + Self { + custom_lists: settings + .custom_lists + .iter() + .map(|custom_list| proto::CustomList::from(custom_list.clone())) + .collect(), + } + } +} + +impl TryFrom<proto::CustomListSettings> for mullvad_types::custom_list::CustomListsSettings { + type Error = FromProtobufTypeError; + + fn try_from(settings: proto::CustomListSettings) -> Result<Self, Self::Error> { + Ok(Self { + custom_lists: settings + .custom_lists + .into_iter() + .map(mullvad_types::custom_list::CustomList::try_from) + .collect::<Result<Vec<_>, _>>()?, + }) + } +} + +impl From<mullvad_types::custom_list::CustomListLocationUpdate> + for proto::CustomListLocationUpdate +{ + fn from(custom_list: mullvad_types::custom_list::CustomListLocationUpdate) -> Self { + use mullvad_types::relay_constraints::Constraint; + match custom_list { + mullvad_types::custom_list::CustomListLocationUpdate::Add { name, location } => { + let location = match location { + Constraint::Any => None, + Constraint::Only(location) => Some(RelayLocation::from(location)), + }; + Self { + state: i32::from(proto::custom_list_location_update::State::Add), + name, + location, + } + } + mullvad_types::custom_list::CustomListLocationUpdate::Remove { name, location } => { + let location = match location { + Constraint::Any => None, + Constraint::Only(location) => Some(RelayLocation::from(location)), + }; + Self { + state: i32::from(proto::custom_list_location_update::State::Remove), + name, + location, + } + } + } + } +} + +impl TryFrom<proto::CustomListLocationUpdate> + for mullvad_types::custom_list::CustomListLocationUpdate +{ + type Error = FromProtobufTypeError; + + fn try_from(custom_list: proto::CustomListLocationUpdate) -> Result<Self, Self::Error> { + use mullvad_types::relay_constraints::Constraint; + let location: Constraint<GeographicLocationConstraint> = + Constraint::<GeographicLocationConstraint>::from( + custom_list + .location + .ok_or(FromProtobufTypeError::InvalidArgument("missing location"))?, + ); + match proto::custom_list_location_update::State::from_i32(custom_list.state) { + Some(proto::custom_list_location_update::State::Add) => Ok(Self::Add { + name: custom_list.name, + location, + }), + Some(proto::custom_list_location_update::State::Remove) => Ok(Self::Remove { + name: custom_list.name, + location, + }), + None => Err(FromProtobufTypeError::InvalidArgument("incorrect state")), + } + } +} + +impl From<mullvad_types::custom_list::CustomList> for proto::CustomList { + fn from(custom_list: mullvad_types::custom_list::CustomList) -> Self { + let locations = custom_list + .locations + .into_iter() + .map(proto::RelayLocation::from) + .collect(); + Self { + id: custom_list.id.to_string(), + name: custom_list.name, + locations, + } + } +} + +impl TryFrom<proto::CustomList> for mullvad_types::custom_list::CustomList { + type Error = FromProtobufTypeError; + + fn try_from(custom_list: proto::CustomList) -> Result<Self, Self::Error> { + let locations: Result<Vec<GeographicLocationConstraint>, _> = custom_list + .locations + .into_iter() + .map(GeographicLocationConstraint::try_from) + .collect(); + let locations = locations.map_err(|_| { + FromProtobufTypeError::InvalidArgument("Could not convert custom list from proto") + })?; + Ok(Self { + id: Id::try_from(custom_list.id.as_str()).map_err(|_| { + FromProtobufTypeError::InvalidArgument("Id could not be parsed to a uuid") + })?, + name: custom_list.name, + locations, + }) + } +} + +impl TryFrom<proto::RelayLocation> for GeographicLocationConstraint { + type Error = FromProtobufTypeError; + + fn try_from(relay_location: proto::RelayLocation) -> Result<Self, Self::Error> { + match ( + relay_location.country.as_ref(), + relay_location.city.as_ref(), + relay_location.hostname.as_ref(), + ) { + ("", ..) => Err(FromProtobufTypeError::InvalidArgument( + "Relay location formatted incorrectly", + )), + (_country, "", "") => Ok(GeographicLocationConstraint::Country( + relay_location.country, + )), + (_country, _city, "") => Ok(GeographicLocationConstraint::City( + relay_location.country, + relay_location.city, + )), + (_country, city, _hostname) => { + if city.is_empty() { + Err(FromProtobufTypeError::InvalidArgument( + "Relay location must contain a city if hostname is included", + )) + } else { + Ok(GeographicLocationConstraint::Hostname( + relay_location.country, + relay_location.city, + relay_location.hostname, + )) + } + } + } + } +} + +impl From<Vec<mullvad_types::custom_list::CustomList>> for proto::CustomLists { + fn from(custom_lists: Vec<mullvad_types::custom_list::CustomList>) -> Self { + let custom_lists = custom_lists + .into_iter() + .map(proto::CustomList::from) + .collect(); + proto::CustomLists { custom_lists } + } +} + +impl TryFrom<proto::CustomLists> for Vec<mullvad_types::custom_list::CustomList> { + type Error = FromProtobufTypeError; + + fn try_from(custom_lists: proto::CustomLists) -> Result<Self, Self::Error> { + let mut new_custom_lists = Vec::with_capacity(custom_lists.custom_lists.len()); + for custom_list in custom_lists.custom_lists { + new_custom_lists.push(mullvad_types::custom_list::CustomList::try_from( + custom_list, + )?); + } + Ok(new_custom_lists) + } +} diff --git a/mullvad-management-interface/src/types/conversions/mod.rs b/mullvad-management-interface/src/types/conversions/mod.rs index 51c53ef76b..d2e8b60265 100644 --- a/mullvad-management-interface/src/types/conversions/mod.rs +++ b/mullvad-management-interface/src/types/conversions/mod.rs @@ -1,6 +1,7 @@ use std::str::FromStr; mod account; +mod custom_list; mod custom_tunnel; mod device; mod location; diff --git a/mullvad-management-interface/src/types/conversions/relay_constraints.rs b/mullvad-management-interface/src/types/conversions/relay_constraints.rs index 04904e0191..e987ca102b 100644 --- a/mullvad-management-interface/src/types/conversions/relay_constraints.rs +++ b/mullvad-management-interface/src/types/conversions/relay_constraints.rs @@ -1,5 +1,8 @@ use crate::types::{conversions::option_from_proto_string, proto, FromProtobufTypeError}; -use mullvad_types::relay_constraints::{Constraint, RelaySettingsUpdate}; +use mullvad_types::{ + custom_list::Id, + relay_constraints::{Constraint, RelaySettingsUpdate}, +}; use talpid_types::net::TunnelType; impl TryFrom<&proto::WireguardConstraints> @@ -37,7 +40,12 @@ impl TryFrom<&proto::WireguardConstraints> entry_location: constraints .entry_location .clone() - .map(Constraint::<mullvad_types::relay_constraints::LocationConstraint>::from) + .and_then(|loc| { + Constraint::<mullvad_types::relay_constraints::LocationConstraint>::try_from( + loc, + ) + .ok() + }) .unwrap_or(Constraint::Any), }) } @@ -94,7 +102,7 @@ impl TryFrom<proto::RelaySettings> for mullvad_types::relay_constraints::RelaySe proto::relay_settings::Endpoint::Normal(settings) => { let location = settings .location - .map(Constraint::<mullvad_types::relay_constraints::LocationConstraint>::from) + .and_then(|loc| Constraint::<mullvad_types::relay_constraints::LocationConstraint>::try_from(loc).ok()) .unwrap_or(Constraint::Any); let providers = try_providers_constraint_from_proto(&settings.providers)?; let ownership = try_ownership_constraint_from_i32(settings.ownership)?; @@ -136,7 +144,14 @@ impl From<RelaySettingsUpdate> for proto::RelaySettingsUpdate { RelaySettingsUpdate::Normal(constraints) => proto::RelaySettingsUpdate { r#type: Some(proto::relay_settings_update::Type::Normal( proto::NormalRelaySettingsUpdate { - location: constraints.location.map(proto::RelayLocation::from), + location: constraints + .location + .and_then(|constraint| match constraint { + Constraint::Any => None, + Constraint::Only(location) => { + Some(proto::LocationConstraint::from(location)) + } + }), providers: constraints .providers .map(|constraint| proto::ProviderUpdate { @@ -176,7 +191,7 @@ impl From<RelaySettingsUpdate> for proto::RelaySettingsUpdate { entry_location: wireguard_constraints .entry_location .option() - .map(proto::RelayLocation::from), + .map(proto::LocationConstraint::from), }, ), openvpn_constraints: constraints.openvpn_constraints.map( @@ -239,9 +254,12 @@ impl TryFrom<proto::RelaySettingsUpdate> for mullvad_types::relay_constraints::R // If `location` isn't provided, no changes are made. // If `location` is provided, but is an empty vector, // then the constraint is set to `Constraint::Any`. - let location = settings - .location - .map(Constraint::<mullvad_types::relay_constraints::LocationConstraint>::from); + let location = settings.location.and_then(|loc| { + Constraint::<mullvad_types::relay_constraints::LocationConstraint>::try_from( + loc, + ) + .ok() + }); let providers = if let Some(ref provider_update) = settings.providers { Some(try_providers_constraint_from_proto( &provider_update.providers, @@ -351,7 +369,7 @@ impl From<mullvad_types::relay_constraints::BridgeSettings> for proto::BridgeSet .location .clone() .option() - .map(proto::RelayLocation::from), + .map(proto::LocationConstraint::from), providers: convert_providers_constraint(&constraints.providers), ownership: convert_ownership_constraint(&constraints.ownership) as i32, }) @@ -408,7 +426,7 @@ impl From<mullvad_types::relay_constraints::RelaySettings> for proto::RelaySetti location: constraints .location .option() - .map(proto::RelayLocation::from), + .map(proto::LocationConstraint::from), providers: convert_providers_constraint(&constraints.providers), ownership: convert_ownership_constraint(&constraints.ownership) as i32, tunnel_type: match constraints.tunnel_protocol { @@ -437,7 +455,7 @@ impl From<mullvad_types::relay_constraints::RelaySettings> for proto::RelaySetti .wireguard_constraints .entry_location .option() - .map(proto::RelayLocation::from), + .map(proto::LocationConstraint::from), }), openvpn_constraints: Some(proto::OpenvpnConstraints { @@ -466,40 +484,69 @@ impl From<mullvad_types::relay_constraints::TransportPort> for proto::TransportP } } -impl - From< - mullvad_types::relay_constraints::Constraint< - mullvad_types::relay_constraints::LocationConstraint, - >, - > for proto::RelayLocation -{ - fn from( - location: mullvad_types::relay_constraints::Constraint< - mullvad_types::relay_constraints::LocationConstraint, - >, - ) -> Self { - location - .option() - .map(proto::RelayLocation::from) - .unwrap_or_default() +impl From<mullvad_types::relay_constraints::LocationConstraint> for proto::LocationConstraint { + fn from(location: mullvad_types::relay_constraints::LocationConstraint) -> Self { + use mullvad_types::relay_constraints::LocationConstraint; + match location { + LocationConstraint::Location(location) => Self { + r#type: Some(proto::location_constraint::Type::Location( + proto::RelayLocation::from(location), + )), + }, + LocationConstraint::CustomList { list_id } => Self { + r#type: Some(proto::location_constraint::Type::CustomList(list_id)), + }, + } } } -impl From<mullvad_types::relay_constraints::LocationConstraint> for proto::RelayLocation { - fn from(location: mullvad_types::relay_constraints::LocationConstraint) -> Self { +impl TryFrom<proto::LocationConstraint> + for Constraint<mullvad_types::relay_constraints::LocationConstraint> +{ + type Error = FromProtobufTypeError; + + fn try_from(location: proto::LocationConstraint) -> Result<Self, Self::Error> { use mullvad_types::relay_constraints::LocationConstraint; + match location.r#type { + Some(proto::location_constraint::Type::Location(location)) => { + let location = Constraint::< + mullvad_types::relay_constraints::GeographicLocationConstraint, + >::from(location); + match location { + Constraint::Any => Ok(Constraint::Any), + Constraint::Only(location) => { + Ok(Constraint::Only(LocationConstraint::Location(location))) + } + } + } + Some(proto::location_constraint::Type::CustomList(list_id)) => { + let location = LocationConstraint::CustomList { + list_id: Id::try_from(list_id.as_str()).map_err(|_| { + FromProtobufTypeError::InvalidArgument("Id could not be parsed to a uuid") + })?, + }; + Ok(Constraint::Only(location)) + } + None => Ok(Constraint::Any), + } + } +} + +impl From<mullvad_types::relay_constraints::GeographicLocationConstraint> for proto::RelayLocation { + fn from(location: mullvad_types::relay_constraints::GeographicLocationConstraint) -> Self { + use mullvad_types::relay_constraints::GeographicLocationConstraint; match location { - LocationConstraint::Country(country) => Self { + GeographicLocationConstraint::Country(country) => Self { country, ..Default::default() }, - LocationConstraint::City(country, city) => Self { + GeographicLocationConstraint::City(country, city) => Self { country, city, ..Default::default() }, - LocationConstraint::Hostname(country, city, hostname) => Self { + GeographicLocationConstraint::Hostname(country, city, hostname) => Self { country, city, hostname, @@ -509,21 +556,21 @@ impl From<mullvad_types::relay_constraints::LocationConstraint> for proto::Relay } impl From<proto::RelayLocation> - for Constraint<mullvad_types::relay_constraints::LocationConstraint> + for Constraint<mullvad_types::relay_constraints::GeographicLocationConstraint> { fn from(location: proto::RelayLocation) -> Self { - use mullvad_types::relay_constraints::LocationConstraint; + use mullvad_types::relay_constraints::GeographicLocationConstraint; if let Some(hostname) = option_from_proto_string(location.hostname) { - Constraint::Only(LocationConstraint::Hostname( + Constraint::Only(GeographicLocationConstraint::Hostname( location.country, location.city, hostname, )) } else if let Some(city) = option_from_proto_string(location.city) { - Constraint::Only(LocationConstraint::City(location.country, city)) + Constraint::Only(GeographicLocationConstraint::City(location.country, city)) } else if let Some(country) = option_from_proto_string(location.country) { - Constraint::Only(LocationConstraint::Country(country)) + Constraint::Only(GeographicLocationConstraint::Country(country)) } else { Constraint::Any } @@ -545,9 +592,9 @@ impl TryFrom<proto::BridgeSettings> for mullvad_types::relay_constraints::Bridge proto::bridge_settings::Type::Normal(constraints) => { let location = match constraints.location { None => Constraint::Any, - Some(location) => { - Constraint::<mullvad_constraints::LocationConstraint>::from(location) - } + Some(location) => Constraint::< + mullvad_types::relay_constraints::LocationConstraint, + >::try_from(location)?, }; let providers = try_providers_constraint_from_proto(&constraints.providers)?; let ownership = try_ownership_constraint_from_i32(constraints.ownership)?; diff --git a/mullvad-management-interface/src/types/conversions/settings.rs b/mullvad-management-interface/src/types/conversions/settings.rs index 54dc98c9cd..699dedf155 100644 --- a/mullvad-management-interface/src/types/conversions/settings.rs +++ b/mullvad-management-interface/src/types/conversions/settings.rs @@ -39,6 +39,7 @@ impl From<&mullvad_types::settings::Settings> for proto::Settings { &settings.obfuscation_settings, )), split_tunnel, + custom_lists: Some(proto::CustomListSettings::from(&settings.custom_lists)), } } } @@ -134,6 +135,12 @@ impl TryFrom<proto::Settings> for mullvad_types::settings::Settings { .ok_or(FromProtobufTypeError::InvalidArgument( "missing obfuscation settings", ))?; + let custom_lists_settings = + settings + .custom_lists + .ok_or(FromProtobufTypeError::InvalidArgument( + "missing custom lists settings", + ))?; #[cfg(windows)] let split_tunnel = settings .split_tunnel @@ -164,6 +171,9 @@ impl TryFrom<proto::Settings> for mullvad_types::settings::Settings { // NOTE: This field is set based on mullvad-types. It's not based on the actual settings // version. settings_version: CURRENT_SETTINGS_VERSION, + custom_lists: mullvad_types::custom_list::CustomListsSettings::try_from( + custom_lists_settings, + )?, }) } } diff --git a/mullvad-relay-selector/src/lib.rs b/mullvad-relay-selector/src/lib.rs index f752d220d3..e62c63f3db 100644 --- a/mullvad-relay-selector/src/lib.rs +++ b/mullvad-relay-selector/src/lib.rs @@ -4,13 +4,14 @@ use chrono::{DateTime, Local}; use ipnetwork::IpNetwork; use mullvad_types::{ + custom_list::CustomListsSettings, endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, location::{Coordinates, Location}, relay_constraints::{ BridgeSettings, BridgeState, Constraint, InternalBridgeConstraints, LocationConstraint, Match, ObfuscationSettings, OpenVpnConstraints, Ownership, Providers, RelayConstraints, - RelaySettings, SelectedObfuscation, Set, TransportPort, Udp2TcpObfuscationSettings, - WireguardConstraints, + RelaySettings, ResolvedLocationConstraint, SelectedObfuscation, Set, TransportPort, + Udp2TcpObfuscationSettings, }, relay_list::{BridgeEndpointData, Relay, RelayEndpointData, RelayList}, CustomTunnelEndpoint, @@ -171,6 +172,7 @@ pub struct SelectorConfig { pub bridge_settings: BridgeSettings, pub obfuscation_settings: ObfuscationSettings, pub default_tunnel_type: TunnelType, + pub custom_lists: CustomListsSettings, } #[derive(Clone)] @@ -238,6 +240,7 @@ impl RelaySelector { config.bridge_state, retry_attempt, config.default_tunnel_type, + &config.custom_lists, )?; let bridge = match relay.endpoint { MullvadEndpoint::OpenVpn(endpoint) @@ -248,7 +251,7 @@ impl RelaySelector { .location .as_ref() .expect("Relay has no location set"); - self.get_bridge_for(&config, location, retry_attempt)? + self.get_bridge_for(&config, location, retry_attempt, &config.custom_lists)? } _ => None, }; @@ -278,36 +281,36 @@ impl RelaySelector { bridge_state: BridgeState, retry_attempt: u32, default_tunnel_type: TunnelType, + custom_lists: &CustomListsSettings, ) -> Result<NormalSelectedRelay, Error> { match relay_constraints.tunnel_protocol { Constraint::Only(TunnelType::OpenVpn) => self.get_openvpn_endpoint( - &relay_constraints.location, - &relay_constraints.providers, - &relay_constraints.ownership, - relay_constraints.openvpn_constraints, + relay_constraints, bridge_state, retry_attempt, + custom_lists, ), - Constraint::Only(TunnelType::Wireguard) => self.get_wireguard_endpoint( - &relay_constraints.location, - &relay_constraints.providers, - &relay_constraints.ownership, - &relay_constraints.wireguard_constraints, - retry_attempt, - ), + Constraint::Only(TunnelType::Wireguard) => { + self.get_wireguard_endpoint(relay_constraints, retry_attempt, custom_lists) + } Constraint::Any => self.get_any_tunnel_endpoint( relay_constraints, bridge_state, retry_attempt, default_tunnel_type, + custom_lists, ), } } /// Returns the average location of relays that match the given constraints. /// This returns none if the location is `any` or if no relays match the constraints. - pub fn get_relay_midpoint(&self, relay_constraints: &RelayConstraints) -> Option<Coordinates> { + pub fn get_relay_midpoint( + &self, + relay_constraints: &RelayConstraints, + custom_lists: &CustomListsSettings, + ) -> Option<Coordinates> { if relay_constraints.location.is_any() { return None; } @@ -320,7 +323,12 @@ impl RelaySelector { ) }; - let matcher = RelayMatcher::new(relay_constraints.clone(), openvpn_data, wireguard_data); + let matcher = RelayMatcher::new( + relay_constraints.clone(), + openvpn_data, + wireguard_data, + custom_lists, + ); let parsed_relays = self.parsed_relays.lock(); let mut matching_locations: Vec<Location> = matcher @@ -340,19 +348,20 @@ impl RelaySelector { /// protocol as only OpenVPN. fn get_openvpn_endpoint( &self, - location: &Constraint<LocationConstraint>, - providers: &Constraint<Providers>, - ownership: &Constraint<Ownership>, - openvpn_constraints: OpenVpnConstraints, + relay_constraints: &RelayConstraints, bridge_state: BridgeState, retry_attempt: u32, + custom_lists: &CustomListsSettings, ) -> Result<NormalSelectedRelay, Error> { let mut relay_matcher = RelayMatcher { - location: location.clone(), - providers: providers.clone(), - ownership: *ownership, + locations: ResolvedLocationConstraint::from_constraint( + relay_constraints.location.clone(), + custom_lists, + ), + providers: relay_constraints.providers.clone(), + ownership: relay_constraints.ownership, endpoint_matcher: OpenVpnMatcher::new( - openvpn_constraints, + relay_constraints.openvpn_constraints, self.parsed_relays.lock().locations.openvpn.clone(), ), }; @@ -402,16 +411,18 @@ impl RelaySelector { fn get_wireguard_multi_hop_endpoint( &self, mut entry_matcher: RelayMatcher<WireguardMatcher>, - exit_location: Constraint<LocationConstraint>, + exit_locations: Constraint<LocationConstraint>, + custom_lists: &CustomListsSettings, ) -> Result<NormalSelectedRelay, Error> { let mut exit_matcher = RelayMatcher { - location: exit_location, + locations: ResolvedLocationConstraint::from_constraint(exit_locations, custom_lists), + providers: entry_matcher.providers.clone(), + ownership: entry_matcher.ownership, endpoint_matcher: self.wireguard_exit_matcher(), - ..entry_matcher.clone() }; let (exit_relay, entry_relay, exit_endpoint, mut entry_endpoint) = - if entry_matcher.location.is_subset(&exit_matcher.location) { + if entry_matcher.locations.is_subset(&exit_matcher.locations) { let (entry_relay, entry_endpoint) = self.get_entry_endpoint(&entry_matcher)?; exit_matcher.set_peer(entry_relay.clone()); let exit_result = self.get_tunnel_endpoint_internal(&exit_matcher)?; @@ -455,46 +466,73 @@ impl RelaySelector { /// tunnel protocol as only WireGuard. fn get_wireguard_endpoint( &self, - location: &Constraint<LocationConstraint>, - providers: &Constraint<Providers>, - ownership: &Constraint<Ownership>, - wireguard_constraints: &WireguardConstraints, + relay_constraints: &RelayConstraints, retry_attempt: u32, + custom_lists: &CustomListsSettings, ) -> Result<NormalSelectedRelay, Error> { - let mut entry_relay_matcher = RelayMatcher { - location: location.clone(), - providers: providers.clone(), - ownership: *ownership, - endpoint_matcher: WireguardMatcher::new( - wireguard_constraints.clone(), - self.parsed_relays.lock().locations.wireguard.clone(), - ), - }; + let wg_endpoint_data = self.parsed_relays.lock().locations.wireguard.clone(); - let mut preferred_matcher: RelayMatcher<WireguardMatcher> = entry_relay_matcher.clone(); - preferred_matcher.endpoint_matcher.port = preferred_matcher - .endpoint_matcher - .port - .or(Self::preferred_wireguard_port(retry_attempt)); + // NOTE: If not using multihop then `location` is set as the only location constraint. + // If using multihop then location is the exit constraint and + // `wireguard_constraints.entry_location` is set as the entry location constraint. + if !relay_constraints.wireguard_constraints.use_multihop { + let relay_matcher = RelayMatcher { + locations: ResolvedLocationConstraint::from_constraint( + relay_constraints.location.clone(), + custom_lists, + ), + providers: relay_constraints.providers.clone(), + ownership: relay_constraints.ownership, + endpoint_matcher: WireguardMatcher::new( + relay_constraints.wireguard_constraints.clone(), + wg_endpoint_data, + ), + }; - if !wireguard_constraints.use_multihop { - return self - .get_tunnel_endpoint_internal(&preferred_matcher) - .or_else(|_| self.get_tunnel_endpoint_internal(&entry_relay_matcher)); - } + // Nightly clippy seems wrong about this being a redundant clone + #[allow(clippy::redundant_clone)] + let mut preferred_matcher: RelayMatcher<WireguardMatcher> = relay_matcher.clone(); + preferred_matcher.endpoint_matcher.port = preferred_matcher + .endpoint_matcher + .port + .or(Self::preferred_wireguard_port(retry_attempt)); + + self.get_tunnel_endpoint_internal(&preferred_matcher) + .or_else(|_| self.get_tunnel_endpoint_internal(&relay_matcher)) + } else { + let mut entry_relay_matcher = RelayMatcher { + locations: ResolvedLocationConstraint::from_constraint( + relay_constraints + .wireguard_constraints + .entry_location + .clone(), + custom_lists, + ), + providers: relay_constraints.providers.clone(), + ownership: relay_constraints.ownership, + endpoint_matcher: WireguardMatcher::new( + relay_constraints.wireguard_constraints.clone(), + wg_endpoint_data, + ), + }; + entry_relay_matcher.endpoint_matcher.port = entry_relay_matcher + .endpoint_matcher + .port + .or(Self::preferred_wireguard_port(retry_attempt)); - entry_relay_matcher.location = wireguard_constraints.entry_location.clone(); - entry_relay_matcher.endpoint_matcher.port = entry_relay_matcher - .endpoint_matcher - .port - .or(Self::preferred_wireguard_port(retry_attempt)); - self.get_wireguard_multi_hop_endpoint(entry_relay_matcher, location.clone()) + self.get_wireguard_multi_hop_endpoint( + entry_relay_matcher, + relay_constraints.location.clone(), + custom_lists, + ) + } } /// Like [Self::get_tunnel_endpoint_internal] but also selects an entry endpoint if applicable. fn get_multihop_tunnel_endpoint_internal( &self, relay_constraints: &RelayConstraints, + custom_lists: &CustomListsSettings, ) -> Result<NormalSelectedRelay, Error> { let (openvpn_data, wireguard_data) = { let relays = self.parsed_relays.lock(); @@ -503,28 +541,33 @@ impl RelaySelector { relays.locations.wireguard.clone(), ) }; - let mut matcher = - RelayMatcher::new(relay_constraints.clone(), openvpn_data, wireguard_data); + let mut matcher = RelayMatcher::new( + relay_constraints.clone(), + openvpn_data, + wireguard_data, + custom_lists, + ); let mut selected_entry_relay = None; let mut selected_entry_endpoint = None; let mut entry_matcher = RelayMatcher { - location: relay_constraints - .wireguard_constraints - .entry_location - .clone(), - ..matcher.clone() + locations: ResolvedLocationConstraint::from_constraint( + relay_constraints + .wireguard_constraints + .entry_location + .clone(), + custom_lists, + ), + providers: relay_constraints.providers.clone(), + ownership: relay_constraints.ownership, + endpoint_matcher: matcher.endpoint_matcher.clone(), } .into_wireguard_matcher(); // Pick the entry relay first if its location constraint is a subset of the exit location. if relay_constraints.wireguard_constraints.use_multihop { matcher.endpoint_matcher.wireguard = self.wireguard_exit_matcher(); - if relay_constraints - .wireguard_constraints - .entry_location - .is_subset(&matcher.location) - { + if entry_matcher.locations.is_subset(&matcher.locations) { if let Ok((entry_relay, entry_endpoint)) = self.get_entry_endpoint(&entry_matcher) { matcher.endpoint_matcher.wireguard.peer = Some(entry_relay.clone()); selected_entry_relay = Some(entry_relay); @@ -540,11 +583,7 @@ impl RelaySelector { if matches!(selected_relay.endpoint, MullvadEndpoint::Wireguard(..)) && relay_constraints.wireguard_constraints.use_multihop { - if !relay_constraints - .wireguard_constraints - .entry_location - .is_subset(&matcher.location) - { + if !entry_matcher.locations.is_subset(&matcher.locations) { entry_matcher.endpoint_matcher.peer = Some(selected_relay.exit_relay.clone()); if let Ok((entry_relay, entry_endpoint)) = self.get_entry_endpoint(&entry_matcher) { selected_entry_relay = Some(entry_relay); @@ -585,28 +624,36 @@ impl RelaySelector { bridge_state: BridgeState, retry_attempt: u32, default_tunnel_type: TunnelType, + custom_lists: &CustomListsSettings, ) -> Result<NormalSelectedRelay, Error> { let preferred_constraints = self.preferred_constraints( relay_constraints, bridge_state, retry_attempt, default_tunnel_type, + custom_lists, ); - if let Ok(result) = self.get_multihop_tunnel_endpoint_internal(&preferred_constraints) { + if let Ok(result) = + self.get_multihop_tunnel_endpoint_internal(&preferred_constraints, custom_lists) + { log::debug!( "Relay matched on highest preference for retry attempt {}", retry_attempt ); Ok(result) - } else if let Ok(result) = self.get_multihop_tunnel_endpoint_internal(relay_constraints) { + } else if let Ok(result) = + self.get_multihop_tunnel_endpoint_internal(relay_constraints, custom_lists) + { log::debug!( "Relay matched on second preference for retry attempt {}", retry_attempt ); Ok(result) } else { - log::warn!("No relays matching {}", &relay_constraints); + let mut relay_constraints_string = String::new(); + let _ = relay_constraints.format(&mut relay_constraints_string, custom_lists); + log::warn!("No relays matching {}", &relay_constraints_string); Err(Error::NoRelay) } } @@ -618,12 +665,17 @@ impl RelaySelector { bridge_state: BridgeState, retry_attempt: u32, default_tunnel_type: TunnelType, + custom_lists: &CustomListsSettings, ) -> RelayConstraints { + let location = ResolvedLocationConstraint::from_constraint( + original_constraints.location.clone(), + custom_lists, + ); let (preferred_port, preferred_protocol, preferred_tunnel) = self .preferred_tunnel_constraints( retry_attempt, default_tunnel_type, - &original_constraints.location, + &location, &original_constraints.providers, &original_constraints.ownership, ); @@ -726,6 +778,7 @@ impl RelaySelector { config: &MutexGuard<'_, SelectorConfig>, location: &mullvad_types::location::Location, retry_attempt: u32, + custom_lists: &CustomListsSettings, ) -> Result<Option<SelectedBridge>, Error> { match &config.bridge_settings { BridgeSettings::Normal(settings) => { @@ -739,7 +792,7 @@ impl RelaySelector { match config.bridge_state { BridgeState::On => { let (settings, relay) = self - .get_proxy_settings(&bridge_constraints, Some(location)) + .get_proxy_settings(&bridge_constraints, Some(location), custom_lists) .ok_or(Error::NoBridge)?; Ok(Some(SelectedBridge::Normal(NormalSelectedBridge { settings, @@ -747,7 +800,7 @@ impl RelaySelector { }))) } BridgeState::Auto if Self::should_use_bridge(retry_attempt) => Ok(self - .get_proxy_settings(&bridge_constraints, Some(location)) + .get_proxy_settings(&bridge_constraints, Some(location), custom_lists) .map(|(settings, relay)| { SelectedBridge::Normal(NormalSelectedBridge { settings, relay }) })), @@ -769,7 +822,9 @@ impl RelaySelector { let config = self.config.lock(); let near_location = match &config.relay_settings { - RelaySettings::Normal(settings) => self.get_relay_midpoint(settings), + RelaySettings::Normal(settings) => { + self.get_relay_midpoint(settings, &config.custom_lists) + } _ => None, }; @@ -788,7 +843,7 @@ impl RelaySelector { }, }; - self.get_proxy_settings(&constraints, near_location) + self.get_proxy_settings(&constraints, near_location, &config.custom_lists) .map(|(settings, _relay)| settings) } @@ -806,9 +861,13 @@ impl RelaySelector { &self, constraints: &InternalBridgeConstraints, location: Option<T>, + custom_lists: &CustomListsSettings, ) -> Option<(ProxySettings, Relay)> { let matcher = RelayMatcher { - location: constraints.location.clone(), + locations: ResolvedLocationConstraint::from_constraint( + constraints.location.clone(), + custom_lists, + ), providers: constraints.providers.clone(), ownership: constraints.ownership, endpoint_matcher: BridgeMatcher(()), @@ -970,7 +1029,7 @@ impl RelaySelector { &self, retry_attempt: u32, default_tunnel_type: TunnelType, - location_constraint: &Constraint<LocationConstraint>, + location_constraint: &Constraint<ResolvedLocationConstraint>, providers_constraint: &Constraint<Providers>, ownership_constraint: &Constraint<Ownership>, ) -> (Constraint<u16>, TransportProtocol, TunnelType) { @@ -1239,8 +1298,10 @@ impl NormalSelectedRelay { mod test { use super::*; use mullvad_types::{ + custom_list::CustomListsSettings, relay_constraints::{ - BridgeConstraints, RelayConstraints, RelayConstraintsUpdate, RelaySettingsUpdate, + BridgeConstraints, GeographicLocationConstraint, RelayConstraints, + RelayConstraintsUpdate, RelaySettingsUpdate, WireguardConstraints, }, relay_list::{ OpenVpnEndpoint, OpenVpnEndpointData, Relay, RelayListCity, RelayListCountry, @@ -1396,7 +1457,9 @@ mod test { ))), config: Arc::new(Mutex::new(SelectorConfig { relay_settings: RelaySettings::Normal(RelayConstraints { - location: Constraint::Only(LocationConstraint::Country("se".to_owned())), + location: Constraint::Only(LocationConstraint::from( + GeographicLocationConstraint::Country("se".to_owned()), + )), ..Default::default() }), bridge_settings: BridgeSettings::Normal(BridgeConstraints::default()), @@ -1406,6 +1469,7 @@ mod test { }, bridge_state: BridgeState::Auto, default_tunnel_type: default_tunnel_type(), + custom_lists: CustomListsSettings::default(), })), } } @@ -1419,13 +1483,13 @@ mod test { let relay_selector = new_relay_selector(); // Prefer WG if the location only supports it - let location = LocationConstraint::Hostname( + let location = GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), "se9-wireguard".to_string(), ); let relay_constraints = RelayConstraints { - location: Constraint::Only(location), + location: Constraint::Only(LocationConstraint::from(location)), tunnel_protocol: Constraint::Any, ..RelayConstraints::default() }; @@ -1435,6 +1499,7 @@ mod test { BridgeState::Off, 0, TunnelType::Wireguard, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1448,18 +1513,19 @@ mod test { BridgeState::Off, attempt, TunnelType::Wireguard, + &CustomListsSettings::default() ) .is_ok()); } // Prefer OpenVPN if the location only supports it - let location = LocationConstraint::Hostname( + let location = GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), "se-got-001".to_string(), ); let relay_constraints = RelayConstraints { - location: Constraint::Only(location), + location: Constraint::Only(LocationConstraint::from(location)), tunnel_protocol: Constraint::Any, ..RelayConstraints::default() }; @@ -1469,6 +1535,7 @@ mod test { BridgeState::Off, 0, TunnelType::Wireguard, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1482,6 +1549,7 @@ mod test { BridgeState::Off, attempt, TunnelType::Wireguard, + &CustomListsSettings::default() ) .is_ok()); } @@ -1496,6 +1564,7 @@ mod test { BridgeState::Off, attempt, TunnelType::OpenVpn, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1506,6 +1575,7 @@ mod test { BridgeState::Off, attempt, TunnelType::OpenVpn, + &CustomListsSettings::default(), ) { Ok(result) if matches!(result.endpoint, MullvadEndpoint::OpenVpn(_)) => (), _ => panic!("OpenVPN endpoint was not selected"), @@ -1518,25 +1588,26 @@ mod test { fn test_wg_entry_hostname_collision() { let relay_selector = new_relay_selector(); - let location1 = LocationConstraint::Hostname( + let location1 = GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), "se9-wireguard".to_string(), ); - let location2 = LocationConstraint::Hostname( + let location2 = GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), "se10-wireguard".to_string(), ); let mut relay_constraints = RelayConstraints { - location: Constraint::Only(location1.clone()), + location: Constraint::Only(LocationConstraint::from(location1.clone())), tunnel_protocol: Constraint::Only(TunnelType::Wireguard), ..RelayConstraints::default() }; relay_constraints.wireguard_constraints.use_multihop = true; - relay_constraints.wireguard_constraints.entry_location = Constraint::Only(location1); + relay_constraints.wireguard_constraints.entry_location = + Constraint::Only(LocationConstraint::from(location1)); // The same host cannot be used for entry and exit assert!(relay_selector @@ -1545,10 +1616,12 @@ mod test { BridgeState::Off, 0, TunnelType::Wireguard, + &CustomListsSettings::default() ) .is_err()); - relay_constraints.wireguard_constraints.entry_location = Constraint::Only(location2); + relay_constraints.wireguard_constraints.entry_location = + Constraint::Only(LocationConstraint::from(location2)); // If the entry and exit differ, this should succeed assert!(relay_selector @@ -1557,6 +1630,7 @@ mod test { BridgeState::Off, 0, TunnelType::Wireguard, + &CustomListsSettings::default() ) .is_ok()); } @@ -1567,12 +1641,15 @@ mod test { let specific_hostname = "se10-wireguard"; - let location_general = LocationConstraint::City("se".to_string(), "got".to_string()); - let location_specific = LocationConstraint::Hostname( + let location_general = LocationConstraint::from(GeographicLocationConstraint::City( + "se".to_string(), + "got".to_string(), + )); + let location_specific = LocationConstraint::from(GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), specific_hostname.to_string(), - ); + )); let mut relay_constraints = RelayConstraints { location: Constraint::Only(location_general.clone()), @@ -1586,7 +1663,13 @@ mod test { // The exit must not equal the entry let exit_relay = relay_selector - .get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, TunnelType::OpenVpn) + .get_tunnel_endpoint( + &relay_constraints, + BridgeState::Off, + 0, + TunnelType::OpenVpn, + &CustomListsSettings::default(), + ) .map_err(|error| error.to_string())? .exit_relay; @@ -1606,6 +1689,7 @@ mod test { BridgeState::Off, 0, TunnelType::Wireguard, + &CustomListsSettings::default(), ) .map_err(|error| error.to_string())?; @@ -1722,6 +1806,7 @@ mod test { BridgeState::Auto, retry_attempt, default_tunnel_type(), + &CustomListsSettings::default(), ); println!("relay: {relay:?}, constraints: {relay_constraints:?}"); @@ -1750,11 +1835,11 @@ mod test { fn test_bridge_constraints() -> Result<(), String> { let relay_selector = new_relay_selector(); - let location = LocationConstraint::Hostname( + let location = LocationConstraint::from(GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), "se-got-001".to_string(), - ); + )); let mut relay_constraints = RelayConstraints { location: Constraint::Only(location), tunnel_protocol: Constraint::Any, @@ -1770,6 +1855,7 @@ mod test { BridgeState::On, 0, TunnelType::Wireguard, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1785,11 +1871,11 @@ mod test { ); // Ignore bridge state where WireGuard is used - let location = LocationConstraint::Hostname( + let location = LocationConstraint::from(GeographicLocationConstraint::Hostname( "se".to_string(), "got".to_string(), "se10-wireguard".to_string(), - ); + )); let relay_constraints = RelayConstraints { location: Constraint::Only(location), tunnel_protocol: Constraint::Any, @@ -1800,6 +1886,7 @@ mod test { BridgeState::On, 0, TunnelType::Wireguard, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1823,6 +1910,7 @@ mod test { BridgeState::On, 0, TunnelType::Wireguard, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1834,6 +1922,7 @@ mod test { BridgeState::On, 2, TunnelType::Wireguard, + &CustomListsSettings::default(), ); assert_eq!( preferred.tunnel_protocol, @@ -1865,7 +1954,7 @@ mod test { let relay_selector = new_relay_selector(); - let result = relay_selector.get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, default_tunnel_type()) + let result = relay_selector.get_tunnel_endpoint(&relay_constraints, BridgeState::Off, 0, default_tunnel_type(), &CustomListsSettings::default()) .expect("Failed to get relay when tunnel constraints are set to Any and retrying the selection"); // Windows will ignore WireGuard until WireGuard is supported well enough // TODO: Remove this caveat once Windows defaults to using WireGuard @@ -1917,7 +2006,7 @@ mod test { fn test_selecting_wireguard_location_will_consider_multihop() { let relay_selector = new_relay_selector(); - let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_MULTIHOP_CONSTRAINTS, BridgeState::Off, 0, default_tunnel_type()) + let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_MULTIHOP_CONSTRAINTS, BridgeState::Off, 0, default_tunnel_type(), &CustomListsSettings::default()) .expect("Failed to get relay when tunnel constraints are set to default WireGuard multihop constraints"); assert!(result.entry_relay.is_some()); @@ -1928,7 +2017,7 @@ mod test { fn test_selecting_wg_endpoint_with_udp2tcp_obfuscation() { let relay_selector = new_relay_selector(); - let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_SINGLEHOP_CONSTRAINTS, BridgeState::Off, 0, default_tunnel_type()) + let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_SINGLEHOP_CONSTRAINTS, BridgeState::Off, 0, default_tunnel_type(), &CustomListsSettings::default()) .expect("Failed to get relay when tunnel constraints are set to default WireGuard constraints"); assert!(result.entry_relay.is_none()); @@ -1957,7 +2046,7 @@ mod test { fn test_selecting_wg_endpoint_with_auto_obfuscation() { let relay_selector = new_relay_selector(); - let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_SINGLEHOP_CONSTRAINTS, BridgeState::Off, 0, default_tunnel_type()) + let result = relay_selector.get_tunnel_endpoint(&WIREGUARD_SINGLEHOP_CONSTRAINTS, BridgeState::Off, 0, default_tunnel_type(), &CustomListsSettings::default()) .expect("Failed to get relay when tunnel constraints are set to default WireGuard constraints"); assert!(result.entry_relay.is_none()); @@ -2002,6 +2091,7 @@ mod test { BridgeState::Off, attempt, TunnelType::Wireguard, + &CustomListsSettings::default(), ) .expect("Failed to select a WireGuard relay"); assert!(result.entry_relay.is_none()); @@ -2038,7 +2128,13 @@ mod test { for i in 0..10 { constraints.ownership = Constraint::Only(Ownership::MullvadOwned); let relay = relay_selector - .get_tunnel_endpoint(&constraints, BridgeState::Auto, i, TunnelType::Wireguard) + .get_tunnel_endpoint( + &constraints, + BridgeState::Auto, + i, + TunnelType::Wireguard, + &CustomListsSettings::default(), + ) .unwrap(); assert!(matches!( relay, @@ -2050,7 +2146,13 @@ mod test { constraints.ownership = Constraint::Only(Ownership::Rented); let relay = relay_selector - .get_tunnel_endpoint(&constraints, BridgeState::Auto, i, TunnelType::Wireguard) + .get_tunnel_endpoint( + &constraints, + BridgeState::Auto, + i, + TunnelType::Wireguard, + &CustomListsSettings::default(), + ) .unwrap(); assert!(matches!( relay, @@ -2077,8 +2179,8 @@ mod test { config.relay_settings = config.relay_settings.merge(RelaySettingsUpdate::Normal( RelayConstraintsUpdate { tunnel_protocol: Some(tunnel_protocol), - location: Some(Constraint::Only(LocationConstraint::Country( - "se".to_string(), + location: Some(Constraint::Only(LocationConstraint::from( + GeographicLocationConstraint::Country("se".to_string()), ))), ..Default::default() }, @@ -2123,7 +2225,13 @@ mod test { Providers::new(EXPECTED_PROVIDERS.into_iter().map(|p| p.to_owned())).unwrap(), ); let relay = relay_selector - .get_tunnel_endpoint(&constraints, BridgeState::Auto, i, TunnelType::Wireguard) + .get_tunnel_endpoint( + &constraints, + BridgeState::Auto, + i, + TunnelType::Wireguard, + &CustomListsSettings::default(), + ) .unwrap(); assert!( EXPECTED_PROVIDERS.contains(&relay.exit_relay.provider.as_str()), diff --git a/mullvad-relay-selector/src/matcher.rs b/mullvad-relay-selector/src/matcher.rs index d141e9900b..05b3799dd8 100644 --- a/mullvad-relay-selector/src/matcher.rs +++ b/mullvad-relay-selector/src/matcher.rs @@ -1,8 +1,9 @@ +use crate::CustomListsSettings; use mullvad_types::{ endpoint::{MullvadEndpoint, MullvadWireguardEndpoint}, relay_constraints::{ - Constraint, LocationConstraint, Match, OpenVpnConstraints, Ownership, Providers, - RelayConstraints, WireguardConstraints, + Constraint, Match, OpenVpnConstraints, Ownership, Providers, RelayConstraints, + ResolvedLocationConstraint, WireguardConstraints, }, relay_list::{ OpenVpnEndpoint, OpenVpnEndpointData, Relay, RelayEndpointData, WireguardEndpointData, @@ -17,7 +18,9 @@ use talpid_types::net::{all_of_the_internet, wireguard, Endpoint, IpVersion, Tun #[derive(Clone)] pub struct RelayMatcher<T: EndpointMatcher> { - pub location: Constraint<LocationConstraint>, + /// Locations allowed to be picked from. In the case of custom lists this may be multiple + /// locations. In normal circumstances this contains only 1 location. + pub locations: Constraint<ResolvedLocationConstraint>, pub providers: Constraint<Providers>, pub ownership: Constraint<Ownership>, pub endpoint_matcher: T, @@ -28,9 +31,13 @@ impl RelayMatcher<AnyTunnelMatcher> { constraints: RelayConstraints, openvpn_data: OpenVpnEndpointData, wireguard_data: WireguardEndpointData, + custom_lists: &CustomListsSettings, ) -> Self { Self { - location: constraints.location, + locations: ResolvedLocationConstraint::from_constraint( + constraints.location, + custom_lists, + ), providers: constraints.providers, ownership: constraints.ownership, endpoint_matcher: AnyTunnelMatcher { @@ -44,7 +51,7 @@ impl RelayMatcher<AnyTunnelMatcher> { pub fn into_wireguard_matcher(self) -> RelayMatcher<WireguardMatcher> { RelayMatcher { endpoint_matcher: self.endpoint_matcher.wireguard, - location: self.location, + locations: self.locations, providers: self.providers, ownership: self.ownership, } @@ -78,13 +85,13 @@ impl<T: EndpointMatcher> RelayMatcher<T> { relay.active && self.providers.matches(relay) && self.ownership.matches(relay) - && self.location.matches_with_opts(relay, true) + && self.locations.matches_with_opts(relay, true) && self.endpoint_matcher.is_matching_relay(relay) } /// Filter a relay based on constraints and endpoint type, 2nd pass. fn post_filter_matching_relay(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { - self.location + self.locations .matches_with_opts(relay, ignore_include_in_country) } diff --git a/mullvad-types/Cargo.toml b/mullvad-types/Cargo.toml index 2aa8324a83..abc182383c 100644 --- a/mullvad-types/Cargo.toml +++ b/mullvad-types/Cargo.toml @@ -16,6 +16,7 @@ log = "0.4" regex = "1" serde = { version = "1.0", features = ["derive"] } rand = "0.8" +uuid = { version = "0.8", features = ["v4", "serde"] } talpid-types = { path = "../talpid-types" } diff --git a/mullvad-types/src/custom_list.rs b/mullvad-types/src/custom_list.rs new file mode 100644 index 0000000000..ee4f914f75 --- /dev/null +++ b/mullvad-types/src/custom_list.rs @@ -0,0 +1,52 @@ +use crate::relay_constraints::{Constraint, GeographicLocationConstraint}; +#[cfg(target_os = "android")] +use jnix::{FromJava, IntoJava}; +use serde::{Deserialize, Serialize}; + +pub type Id = String; + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(target_os = "android", derive(FromJava, IntoJava))] +#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] +pub struct CustomListsSettings { + pub custom_lists: Vec<CustomList>, +} + +impl CustomListsSettings { + pub fn get_custom_list_with_name(&self, name: &String) -> Option<&CustomList> { + self.custom_lists + .iter() + .find(|custom_list| &custom_list.name == name) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum CustomListLocationUpdate { + Add { + name: String, + location: Constraint<GeographicLocationConstraint>, + }, + Remove { + name: String, + location: Constraint<GeographicLocationConstraint>, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(target_os = "android", derive(FromJava, IntoJava))] +#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] +pub struct CustomList { + pub id: Id, + pub name: String, + pub locations: Vec<GeographicLocationConstraint>, +} + +impl CustomList { + pub fn new(name: String) -> Self { + CustomList { + id: uuid::Uuid::new_v4().to_string(), + name, + locations: Vec::new(), + } + } +} diff --git a/mullvad-types/src/lib.rs b/mullvad-types/src/lib.rs index d157f55467..bfac631f82 100644 --- a/mullvad-types/src/lib.rs +++ b/mullvad-types/src/lib.rs @@ -2,6 +2,7 @@ pub mod account; pub mod auth_failed; +pub mod custom_list; pub mod device; pub mod endpoint; pub mod location; diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index eb7d3d6900..ab78ceca70 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -2,6 +2,7 @@ //! updated as well. use crate::{ + custom_list::{CustomListsSettings, Id}, location::{CityCode, CountryCode, Hostname}, relay_list::Relay, CustomTunnelEndpoint, @@ -9,7 +10,7 @@ use crate::{ #[cfg(target_os = "android")] use jnix::{jni::objects::JObject, FromJava, IntoJava, JnixEnv}; use serde::{Deserialize, Serialize}; -use std::{collections::HashSet, fmt, str::FromStr}; +use std::{collections::HashSet, fmt, fmt::Write, str::FromStr}; use talpid_types::net::{openvpn::ProxySettings, IpVersion, TransportProtocol, TunnelType}; pub trait Match<T> { @@ -203,19 +204,23 @@ pub enum RelaySettings { Normal(RelayConstraints), } -impl fmt::Display for RelaySettings { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +impl RelaySettings { + pub fn format( + &self, + s: &mut String, + custom_lists: &CustomListsSettings, + ) -> Result<(), fmt::Error> { match self { RelaySettings::CustomTunnelEndpoint(endpoint) => { - write!(f, "custom endpoint {endpoint}") + write!(s, "custom endpoint {endpoint}") } - RelaySettings::Normal(constraints) => constraints.fmt(f), + RelaySettings::Normal(constraints) => constraints.format(s, custom_lists), } } } impl RelaySettings { - pub fn merge(&mut self, update: RelaySettingsUpdate) -> Self { + pub fn merge(&self, update: RelaySettingsUpdate) -> Self { match update { RelaySettingsUpdate::CustomTunnelEndpoint(relay) => { RelaySettings::CustomTunnelEndpoint(relay) @@ -230,6 +235,121 @@ impl RelaySettings { } } +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(target_os = "android", derive(IntoJava, FromJava))] +#[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] +pub enum LocationConstraint { + Location(GeographicLocationConstraint), + CustomList { list_id: Id }, +} + +#[derive(Debug, Clone)] +pub enum ResolvedLocationConstraint { + Location(GeographicLocationConstraint), + Locations(Vec<GeographicLocationConstraint>), +} + +impl ResolvedLocationConstraint { + pub fn from_constraint( + location: Constraint<LocationConstraint>, + custom_lists: &CustomListsSettings, + ) -> Constraint<ResolvedLocationConstraint> { + match location { + Constraint::Any => Constraint::Any, + Constraint::Only(LocationConstraint::Location(location)) => { + Constraint::Only(Self::Location(location)) + } + Constraint::Only(LocationConstraint::CustomList { list_id }) => custom_lists + .custom_lists + .iter() + .find(|custom_list| custom_list.id == list_id) + .map(|custom_list| Constraint::Only(Self::Locations(custom_list.locations.clone()))) + .unwrap_or(Constraint::Any), + } + } +} + +impl From<GeographicLocationConstraint> for LocationConstraint { + fn from(location: GeographicLocationConstraint) -> Self { + Self::Location(location) + } +} + +impl Set<Constraint<ResolvedLocationConstraint>> for Constraint<ResolvedLocationConstraint> { + fn is_subset(&self, other: &Self) -> bool { + match self { + Constraint::Any => other.is_any(), + Constraint::Only(ResolvedLocationConstraint::Location(location)) => match other { + Constraint::Any => true, + Constraint::Only(ResolvedLocationConstraint::Location(other_location)) => { + location.is_subset(other_location) + } + Constraint::Only(ResolvedLocationConstraint::Locations(other_locations)) => { + other_locations + .iter() + .any(|other_location| location.is_subset(other_location)) + } + }, + Constraint::Only(ResolvedLocationConstraint::Locations(locations)) => match other { + Constraint::Any => true, + Constraint::Only(ResolvedLocationConstraint::Location(other_location)) => locations + .iter() + .all(|location| location.is_subset(other_location)), + Constraint::Only(ResolvedLocationConstraint::Locations(other_locations)) => { + for location in locations { + if !other_locations + .iter() + .any(|other_location| location.is_subset(other_location)) + { + return false; + } + } + true + } + }, + } + } +} + +impl Constraint<ResolvedLocationConstraint> { + pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { + match self { + Constraint::Any => true, + Constraint::Only(ResolvedLocationConstraint::Location(location)) => { + location.matches_with_opts(relay, ignore_include_in_country) + } + Constraint::Only(ResolvedLocationConstraint::Locations(locations)) => locations + .iter() + .any(|loc| loc.matches_with_opts(relay, ignore_include_in_country)), + } + } +} + +impl LocationConstraint { + fn format(&self, f: &mut String, custom_lists: &CustomListsSettings) -> Result<(), fmt::Error> { + match self { + Self::Location(location) => writeln!(f, "location - {location}"), + Self::CustomList { list_id } => match custom_lists + .custom_lists + .iter() + .find(|custom_list| &custom_list.id == list_id) + { + Some(list) => { + writeln!(f, "custom list - {}", list.name)?; + for location in &list.locations { + writeln!(f, "\t{}", location)?; + } + Ok(()) + } + None => { + writeln!(f, "custom list - list not found") + } + }, + } + } +} + /// Limits the set of [`crate::relay_list::Relay`]s that a `RelaySelector` may select. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] #[serde(default)] @@ -280,40 +400,49 @@ impl RelayConstraints { } } -impl fmt::Display for RelayConstraints { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +impl RelayConstraints { + pub fn format( + &self, + f: &mut String, + custom_lists: &CustomListsSettings, + ) -> Result<(), fmt::Error> { match self.tunnel_protocol { - Constraint::Any => write!( - f, - "Any tunnel protocol with OpenVPN through {} and WireGuard through {}", - &self.openvpn_constraints, &self.wireguard_constraints, - )?, + Constraint::Any => { + writeln!( + f, + "Tunnel protocol: Any\nOpenVPN constraints: {}\nWireguard constraints: ", + &self.openvpn_constraints, + )?; + self.wireguard_constraints.format(f, custom_lists)?; + } Constraint::Only(ref tunnel_protocol) => { - tunnel_protocol.fmt(f)?; + writeln!(f, "Tunnel protocol: {}", tunnel_protocol)?; match tunnel_protocol { TunnelType::Wireguard => { - write!(f, " over {}", &self.wireguard_constraints)?; + writeln!(f, "Wireguard constraints: ")?; + self.wireguard_constraints.format(f, custom_lists)?; } TunnelType::OpenVpn => { - write!(f, " over {}", &self.openvpn_constraints)?; + writeln!(f, "OpenVPN constraints: {}", &self.openvpn_constraints)?; } }; } } - write!(f, " in ")?; match self.location { - Constraint::Any => write!(f, "any location")?, - Constraint::Only(ref location_constraint) => location_constraint.fmt(f)?, + Constraint::Any => writeln!(f, "Location: Any")?, + Constraint::Only(ref location_constraint) => { + write!(f, "Location: ")?; + location_constraint.format(f, custom_lists)?; + } } - write!(f, " using ")?; match self.providers { - Constraint::Any => write!(f, "any provider")?, - Constraint::Only(ref constraint) => constraint.fmt(f)?, + Constraint::Any => writeln!(f, "Provider: Any")?, + Constraint::Only(ref constraint) => writeln!(f, "Provider: {}", constraint)?, } match self.ownership { Constraint::Any => Ok(()), Constraint::Only(ref constraint) => { - write!(f, " and {constraint}") + write!(f, "Constraints: {constraint}") } } } @@ -325,7 +454,7 @@ impl fmt::Display for RelayConstraints { #[serde(rename_all = "snake_case")] #[cfg_attr(target_os = "android", derive(FromJava, IntoJava))] #[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] -pub enum LocationConstraint { +pub enum GeographicLocationConstraint { /// A country is represented by its two letter country code. Country(CountryCode), /// A city is composed of a country code and a city code. @@ -334,22 +463,22 @@ pub enum LocationConstraint { Hostname(CountryCode, CityCode, Hostname), } -impl LocationConstraint { +impl GeographicLocationConstraint { pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { match self { - LocationConstraint::Country(ref country) => { + GeographicLocationConstraint::Country(ref country) => { relay .location .as_ref() .map_or(false, |loc| loc.country_code == *country) && (ignore_include_in_country || relay.include_in_country) } - LocationConstraint::City(ref country, ref city) => { + GeographicLocationConstraint::City(ref country, ref city) => { relay.location.as_ref().map_or(false, |loc| { loc.country_code == *country && loc.city_code == *city }) } - LocationConstraint::Hostname(ref country, ref city, ref hostname) => { + GeographicLocationConstraint::Hostname(ref country, ref city, ref hostname) => { relay.location.as_ref().map_or(false, |loc| { loc.country_code == *country && loc.city_code == *city @@ -360,7 +489,18 @@ impl LocationConstraint { } } -impl Constraint<LocationConstraint> { +impl Constraint<Vec<GeographicLocationConstraint>> { + pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { + match self { + Constraint::Only(constraint) => constraint + .iter() + .any(|loc| loc.matches_with_opts(relay, ignore_include_in_country)), + Constraint::Any => true, + } + } +} + +impl Constraint<GeographicLocationConstraint> { pub fn matches_with_opts(&self, relay: &Relay, ignore_include_in_country: bool) -> bool { match self { Constraint::Only(constraint) => { @@ -371,28 +511,52 @@ impl Constraint<LocationConstraint> { } } -impl Match<Relay> for LocationConstraint { +impl Match<Relay> for GeographicLocationConstraint { fn matches(&self, relay: &Relay) -> bool { self.matches_with_opts(relay, false) } } -impl Set<LocationConstraint> for LocationConstraint { +impl Set<GeographicLocationConstraint> for GeographicLocationConstraint { /// Returns whether `self` is equal to or a subset of `other`. fn is_subset(&self, other: &Self) -> bool { match self { - LocationConstraint::Country(_) => self == other, - LocationConstraint::City(ref country, ref _city) => match other { - LocationConstraint::Country(ref other_country) => country == other_country, - LocationConstraint::City(..) => self == other, + GeographicLocationConstraint::Country(_) => self == other, + GeographicLocationConstraint::City(ref country, ref _city) => match other { + GeographicLocationConstraint::Country(ref other_country) => { + country == other_country + } + GeographicLocationConstraint::City(..) => self == other, _ => false, }, - LocationConstraint::Hostname(ref country, ref city, ref _hostname) => match other { - LocationConstraint::Country(ref other_country) => country == other_country, - LocationConstraint::City(ref other_country, ref other_city) => { - country == other_country && city == other_city + GeographicLocationConstraint::Hostname(ref country, ref city, ref _hostname) => { + match other { + GeographicLocationConstraint::Country(ref other_country) => { + country == other_country + } + GeographicLocationConstraint::City(ref other_country, ref other_city) => { + country == other_country && city == other_city + } + GeographicLocationConstraint::Hostname(..) => self == other, } - LocationConstraint::Hostname(..) => self == other, + } + } + } +} + +impl Set<Constraint<Vec<GeographicLocationConstraint>>> + for Constraint<Vec<GeographicLocationConstraint>> +{ + fn is_subset(&self, other: &Self) -> bool { + match self { + Constraint::Any => other.is_any(), + Constraint::Only(locations) => match other { + Constraint::Any => true, + Constraint::Only(other_locations) => locations.iter().all(|location| { + other_locations + .iter() + .any(|other_location| location.is_subset(other_location)) + }), }, } } @@ -497,12 +661,14 @@ impl fmt::Display for Providers { } } -impl fmt::Display for LocationConstraint { +impl fmt::Display for GeographicLocationConstraint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { - LocationConstraint::Country(country) => write!(f, "country {country}"), - LocationConstraint::City(country, city) => write!(f, "city {city}, {country}"), - LocationConstraint::Hostname(country, city, hostname) => { + GeographicLocationConstraint::Country(country) => write!(f, "country {country}"), + GeographicLocationConstraint::City(country, city) => { + write!(f, "city {city}, {country}") + } + GeographicLocationConstraint::Hostname(country, city, hostname) => { write!(f, "city {city}, {country}, hostname {hostname}") } } @@ -583,21 +749,23 @@ where } } -impl fmt::Display for WireguardConstraints { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +impl WireguardConstraints { + fn format(&self, f: &mut String, custom_lists: &CustomListsSettings) -> Result<(), fmt::Error> { match self.port { - Constraint::Any => write!(f, "any port")?, - Constraint::Only(port) => write!(f, "port {port}")?, + Constraint::Any => writeln!(f, "Port: Any")?, + Constraint::Only(port) => writeln!(f, "port {port}")?, } - write!(f, " over ")?; match self.ip_version { - Constraint::Any => write!(f, "IPv4 or IPv6")?, - Constraint::Only(protocol) => write!(f, "{protocol}")?, + Constraint::Any => writeln!(f, "Protocol: IPv4 or IPv6")?, + Constraint::Only(protocol) => writeln!(f, "Protocol: {protocol}")?, } if self.use_multihop { match &self.entry_location { - Constraint::Any => write!(f, " (via any location)"), - Constraint::Only(location) => write!(f, " (via {location})"), + Constraint::Any => writeln!(f, "Entry location: Any"), + Constraint::Only(location) => { + write!(f, "Wireguard entry ")?; + location.format(f, custom_lists) + } } } else { Ok(()) @@ -715,16 +883,22 @@ pub struct BridgeConstraints { pub ownership: Constraint<Ownership>, } -impl fmt::Display for BridgeConstraints { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +impl BridgeConstraints { + pub fn format( + &self, + f: &mut String, + custom_lists: &CustomListsSettings, + ) -> Result<(), fmt::Error> { match self.location { Constraint::Any => write!(f, "any location")?, - Constraint::Only(ref location_constraint) => location_constraint.fmt(f)?, + Constraint::Only(ref location_constraint) => { + location_constraint.format(f, custom_lists)? + } } write!(f, " using ")?; match self.providers { Constraint::Any => write!(f, "any provider")?, - Constraint::Only(ref constraint) => constraint.fmt(f)?, + Constraint::Only(ref constraint) => write!(f, "{}", constraint)?, } match self.ownership { Constraint::Any => Ok(()), diff --git a/mullvad-types/src/settings/mod.rs b/mullvad-types/src/settings/mod.rs index ec4adb109a..12f5d831e1 100644 --- a/mullvad-types/src/settings/mod.rs +++ b/mullvad-types/src/settings/mod.rs @@ -1,8 +1,9 @@ use crate::{ + custom_list::CustomListsSettings, relay_constraints::{ - BridgeConstraints, BridgeSettings, BridgeState, Constraint, LocationConstraint, - ObfuscationSettings, RelayConstraints, RelaySettings, RelaySettingsUpdate, - SelectedObfuscation, WireguardConstraints, + BridgeConstraints, BridgeSettings, BridgeState, Constraint, GeographicLocationConstraint, + LocationConstraint, ObfuscationSettings, RelayConstraints, RelaySettings, + RelaySettingsUpdate, SelectedObfuscation, WireguardConstraints, }, wireguard, }; @@ -20,7 +21,7 @@ mod dns; /// latest version that exists in `SettingsVersion`. /// This should be bumped when a new version is introduced along with a migration /// being added to `mullvad-daemon`. -pub const CURRENT_SETTINGS_VERSION: SettingsVersion = SettingsVersion::V6; +pub const CURRENT_SETTINGS_VERSION: SettingsVersion = SettingsVersion::V7; #[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy)] #[repr(u32)] @@ -30,6 +31,7 @@ pub enum SettingsVersion { V4 = 4, V5 = 5, V6 = 6, + V7 = 7, } impl<'de> Deserialize<'de> for SettingsVersion { @@ -43,6 +45,7 @@ impl<'de> Deserialize<'de> for SettingsVersion { v if v == SettingsVersion::V4 as u32 => Ok(SettingsVersion::V4), v if v == SettingsVersion::V5 as u32 => Ok(SettingsVersion::V5), v if v == SettingsVersion::V6 as u32 => Ok(SettingsVersion::V6), + v if v == SettingsVersion::V7 as u32 => Ok(SettingsVersion::V7), v => Err(serde::de::Error::custom(format!( "{v} is not a valid SettingsVersion" ))), @@ -71,6 +74,9 @@ pub struct Settings { pub obfuscation_settings: ObfuscationSettings, #[cfg_attr(target_os = "android", jnix(skip))] pub bridge_state: BridgeState, + /// All of the custom relay lists + #[cfg_attr(target_os = "android", jnix(skip))] + pub custom_lists: CustomListsSettings, /// If the daemon should allow communication with private (LAN) networks. pub allow_lan: bool, /// Extra level of kill switch. When this setting is on, the disconnected state will block @@ -117,9 +123,13 @@ impl Default for Settings { fn default() -> Self { Settings { relay_settings: RelaySettings::Normal(RelayConstraints { - location: Constraint::Only(LocationConstraint::Country("se".to_owned())), + location: Constraint::Only(LocationConstraint::Location( + GeographicLocationConstraint::Country("se".to_owned()), + )), wireguard_constraints: WireguardConstraints { - entry_location: Constraint::Only(LocationConstraint::Country("se".to_owned())), + entry_location: Constraint::Only(LocationConstraint::Location( + GeographicLocationConstraint::Country("se".to_owned()), + )), ..Default::default() }, ..Default::default() @@ -139,6 +149,7 @@ impl Default for Settings { #[cfg(windows)] split_tunnel: SplitTunnelSettings::default(), settings_version: CURRENT_SETTINGS_VERSION, + custom_lists: CustomListsSettings::default(), } } } @@ -155,10 +166,18 @@ impl Settings { if !update_supports_bridge && BridgeState::On == self.bridge_state { self.bridge_state = BridgeState::Auto; } + + let mut old_settings_string = String::new(); + let _ = self + .relay_settings + .format(&mut old_settings_string, &self.custom_lists); + let mut new_settings_string = String::new(); + let _ = new_settings.format(&mut new_settings_string, &self.custom_lists); + log::debug!( "Changing relay settings:\n\tfrom: {}\n\tto: {}", - self.relay_settings, - new_settings + old_settings_string, + new_settings_string, ); self.relay_settings = new_settings; |
