summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonathan <jonathan@mullvad.net>2023-06-29 15:14:40 +0200
committerJonathan <jonathan@mullvad.net>2023-06-29 15:14:40 +0200
commitd3805f501cea6aa352de2c160f3a2f7bc8b93821 (patch)
treeba94b7e0829f2fd57a9974b4590af59e9937e5be
parent9e7bb470758385159ade1e22d5093b535a5e667c (diff)
parent5f61ba4058337e6985cee99661a59a98ebff6dd0 (diff)
downloadmullvadvpn-d3805f501cea6aa352de2c160f3a2f7bc8b93821.tar.xz
mullvadvpn-d3805f501cea6aa352de2c160f3a2f7bc8b93821.zip
Merge branch 'custom-lists'
-rw-r--r--CHANGELOG.md3
-rw-r--r--Cargo.lock2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/Request.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt28
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt21
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/Relay.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCity.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayCountry.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItem.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayList.kt10
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/LocationInfoCache.kt3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayListListener.kt8
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/RelayListListener.kt12
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/LocationConstraintExtensions.kt22
-rw-r--r--android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SelectLocationViewModelTest.kt4
-rw-r--r--gui/src/main/daemon-rpc.ts28
-rw-r--r--mullvad-cli/src/cmds/bridge.rs37
-rw-r--r--mullvad-cli/src/cmds/custom_lists.rs110
-rw-r--r--mullvad-cli/src/cmds/mod.rs1
-rw-r--r--mullvad-cli/src/cmds/relay.rs73
-rw-r--r--mullvad-cli/src/cmds/relay_constraints.rs32
-rw-r--r--mullvad-cli/src/cmds/tunnel_state.rs3
-rw-r--r--mullvad-cli/src/main.rs5
-rw-r--r--mullvad-daemon/src/custom_lists.rs330
-rw-r--r--mullvad-daemon/src/lib.rs86
-rw-r--r--mullvad-daemon/src/management_interface.rs121
-rw-r--r--mullvad-daemon/src/migrations/v6.rs303
-rw-r--r--mullvad-daemon/src/settings.rs9
-rw-r--r--mullvad-jni/src/classes.rs10
-rw-r--r--mullvad-management-interface/proto/management_interface.proto49
-rw-r--r--mullvad-management-interface/src/client.rs87
-rw-r--r--mullvad-management-interface/src/lib.rs20
-rw-r--r--mullvad-management-interface/src/types/conversions/custom_list.rs200
-rw-r--r--mullvad-management-interface/src/types/conversions/mod.rs1
-rw-r--r--mullvad-management-interface/src/types/conversions/relay_constraints.rs127
-rw-r--r--mullvad-management-interface/src/types/conversions/settings.rs10
-rw-r--r--mullvad-relay-selector/src/lib.rs332
-rw-r--r--mullvad-relay-selector/src/matcher.rs21
-rw-r--r--mullvad-types/Cargo.toml1
-rw-r--r--mullvad-types/src/custom_list.rs52
-rw-r--r--mullvad-types/src/lib.rs1
-rw-r--r--mullvad-types/src/relay_constraints.rs288
-rw-r--r--mullvad-types/src/settings/mod.rs35
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;