Skip to content

Commit a6543aa

Browse files
committed
Merge branch 'implement-device-ip-setting'
2 parents cae71a3 + c17b6fa commit a6543aa

File tree

37 files changed

+202
-49
lines changed

37 files changed

+202
-49
lines changed

android/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Line wrap the file at 100 chars. Th
2525

2626
### Added
2727
- Prompt password manager to store new account number on account creation.
28+
- Add the ability to force the ip version used to connect to a relay.
2829

2930
### Changed
3031
- Disable Wireguard port setting when a obfuscation is selected since it is not used when an

android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_
2323
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG
2424
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG
2525
import net.mullvad.mullvadvpn.lib.model.Constraint
26+
import net.mullvad.mullvadvpn.lib.model.IpVersion
2627
import net.mullvad.mullvadvpn.lib.model.Mtu
2728
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
2829
import net.mullvad.mullvadvpn.lib.model.Port
@@ -72,6 +73,7 @@ class VpnSettingsScreenTest {
7273
navigateToShadowSocksSettings: () -> Unit = {},
7374
navigateToUdp2TcpSettings: () -> Unit = {},
7475
onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = {},
76+
onSelectDeviceIpVersion: (Constraint<IpVersion>) -> Unit = {},
7577
) {
7678
setContentWithTheme {
7779
VpnSettingsScreen(
@@ -103,6 +105,7 @@ class VpnSettingsScreenTest {
103105
navigateToShadowSocksSettings = navigateToShadowSocksSettings,
104106
navigateToUdp2TcpSettings = navigateToUdp2TcpSettings,
105107
onToggleAutoStartAndConnectOnBoot = onToggleAutoStartAndConnectOnBoot,
108+
onSelectDeviceIpVersion = onSelectDeviceIpVersion,
106109
)
107110
}
108111
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt

+30
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
9393
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
9494
import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
9595
import net.mullvad.mullvadvpn.lib.model.Constraint
96+
import net.mullvad.mullvadvpn.lib.model.IpVersion
9697
import net.mullvad.mullvadvpn.lib.model.Mtu
9798
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
9899
import net.mullvad.mullvadvpn.lib.model.Port
@@ -140,6 +141,7 @@ private fun PreviewVpnSettings(
140141
navigateToLocalNetworkSharingInfo = {},
141142
navigateToWireguardPortDialog = {},
142143
navigateToServerIpOverrides = {},
144+
onSelectDeviceIpVersion = {},
143145
)
144146
}
145147
}
@@ -268,6 +270,7 @@ fun VpnSettings(
268270
navigateToUdp2TcpSettings =
269271
dropUnlessResumed { navigator.navigate(Udp2TcpSettingsDestination) },
270272
onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot,
273+
onSelectDeviceIpVersion = vm::onDeviceIpVersionSelected,
271274
)
272275
}
273276

@@ -304,6 +307,7 @@ fun VpnSettingsScreen(
304307
navigateToShadowSocksSettings: () -> Unit,
305308
navigateToUdp2TcpSettings: () -> Unit,
306309
onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit,
310+
onSelectDeviceIpVersion: (ipVersion: Constraint<IpVersion>) -> Unit,
307311
) {
308312
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
309313
val biggerPadding = 54.dp
@@ -651,6 +655,32 @@ fun VpnSettingsScreen(
651655
Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing))
652656
}
653657

658+
itemWithDivider {
659+
InformationComposeCell(title = stringResource(R.string.device_ip_version_title))
660+
}
661+
itemWithDivider {
662+
SelectableCell(
663+
title = stringResource(id = R.string.automatic),
664+
isSelected = state.deviceIpVersion == Constraint.Any,
665+
onCellClicked = { onSelectDeviceIpVersion(Constraint.Any) },
666+
)
667+
}
668+
itemWithDivider {
669+
SelectableCell(
670+
title = stringResource(id = R.string.device_ip_version_ipv4),
671+
isSelected = state.deviceIpVersion.getOrNull() == IpVersion.IPV4,
672+
onCellClicked = { onSelectDeviceIpVersion(Constraint.Only(IpVersion.IPV4)) },
673+
)
674+
}
675+
item {
676+
SelectableCell(
677+
title = stringResource(id = R.string.device_ip_version_ipv6),
678+
isSelected = state.deviceIpVersion.getOrNull() == IpVersion.IPV6,
679+
onCellClicked = { onSelectDeviceIpVersion(Constraint.Only(IpVersion.IPV6)) },
680+
)
681+
Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing))
682+
}
683+
654684
item {
655685
MtuComposeCell(mtuValue = state.mtu, onEditMtu = { navigateToMtuDialog(state.mtu) })
656686
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.compose.state
22

33
import net.mullvad.mullvadvpn.lib.model.Constraint
44
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
5+
import net.mullvad.mullvadvpn.lib.model.IpVersion
56
import net.mullvad.mullvadvpn.lib.model.Mtu
67
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
78
import net.mullvad.mullvadvpn.lib.model.Port
@@ -24,6 +25,7 @@ data class VpnSettingsUiState(
2425
val availablePortRanges: List<PortRange>,
2526
val systemVpnSettingsAvailable: Boolean,
2627
val autoStartAndConnectOnBoot: Boolean,
28+
val deviceIpVersion: Constraint<IpVersion>,
2729
) {
2830
val isCustomWireguardPort =
2931
selectedWireguardPort is Constraint.Only &&
@@ -48,6 +50,7 @@ data class VpnSettingsUiState(
4850
availablePortRanges: List<PortRange> = emptyList(),
4951
systemVpnSettingsAvailable: Boolean = false,
5052
autoStartAndConnectOnBoot: Boolean = false,
53+
deviceIpVersion: Constraint<IpVersion> = Constraint.Any,
5154
) =
5255
VpnSettingsUiState(
5356
mtu,
@@ -64,6 +67,7 @@ data class VpnSettingsUiState(
6467
availablePortRanges,
6568
systemVpnSettingsAvailable,
6669
autoStartAndConnectOnBoot,
70+
deviceIpVersion,
6771
)
6872
}
6973
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/WireguardConstraintsRepository.kt

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.mapNotNull
88
import kotlinx.coroutines.flow.stateIn
99
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
1010
import net.mullvad.mullvadvpn.lib.model.Constraint
11+
import net.mullvad.mullvadvpn.lib.model.IpVersion
1112
import net.mullvad.mullvadvpn.lib.model.Port
1213
import net.mullvad.mullvadvpn.lib.model.RelayItemId
1314

@@ -26,4 +27,7 @@ class WireguardConstraintsRepository(
2627

2728
suspend fun setEntryLocation(relayItemId: RelayItemId) =
2829
managementService.setEntryLocation(relayItemId)
30+
31+
suspend fun setDeviceIpVersion(ipVersion: Constraint<IpVersion>) =
32+
managementService.setDeviceIpVersion(ipVersion)
2933
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt

+15-10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
2323
import net.mullvad.mullvadvpn.lib.model.Constraint
2424
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
2525
import net.mullvad.mullvadvpn.lib.model.DnsState
26+
import net.mullvad.mullvadvpn.lib.model.IpVersion
2627
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
2728
import net.mullvad.mullvadvpn.lib.model.Port
2829
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
@@ -46,7 +47,7 @@ sealed interface VpnSettingsSideEffect {
4647
@Suppress("TooManyFunctions")
4748
class VpnSettingsViewModel(
4849
private val repository: SettingsRepository,
49-
private val relayListRepository: RelayListRepository,
50+
relayListRepository: RelayListRepository,
5051
private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase,
5152
private val autoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository,
5253
private val wireguardConstraintsRepository: WireguardConstraintsRepository,
@@ -83,6 +84,7 @@ class VpnSettingsViewModel(
8384
availablePortRanges = portRanges,
8485
systemVpnSettingsAvailable = systemVpnSettingsUseCase(),
8586
autoStartAndConnectOnBoot = autoStartAndConnectOnBoot,
87+
deviceIpVersion = settings?.getDeviceIpVersion() ?: Constraint.Any,
8688
)
8789
}
8890
.stateIn(
@@ -122,14 +124,6 @@ class VpnSettingsViewModel(
122124
}
123125
}
124126

125-
fun onToggleDaita(enable: Boolean) {
126-
viewModelScope.launch(dispatcher) {
127-
repository.setDaitaEnabled(enable).onLeft {
128-
_uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
129-
}
130-
}
131-
}
132-
133127
fun onDnsDialogDismissed() {
134128
if (vmState.value.customDnsList.isEmpty()) {
135129
onToggleCustomDns(enable = false)
@@ -251,6 +245,14 @@ class VpnSettingsViewModel(
251245
}
252246
}
253247

248+
fun onDeviceIpVersionSelected(ipVersion: Constraint<IpVersion>) {
249+
viewModelScope.launch(dispatcher) {
250+
wireguardConstraintsRepository.setDeviceIpVersion(ipVersion).onLeft {
251+
_uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
252+
}
253+
}
254+
}
255+
254256
private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) =
255257
viewModelScope.launch(dispatcher) {
256258
repository
@@ -265,7 +267,7 @@ class VpnSettingsViewModel(
265267
private fun List<String>.asInetAddressList(): List<InetAddress> {
266268
return try {
267269
map { InetAddress.getByName(it) }
268-
} catch (ex: UnknownHostException) {
270+
} catch (_: UnknownHostException) {
269271
Logger.e("Error parsing the DNS address list.")
270272
emptyList()
271273
}
@@ -290,6 +292,9 @@ class VpnSettingsViewModel(
290292
private fun Settings.getWireguardPort() =
291293
relaySettings.relayConstraints.wireguardConstraints.port
292294

295+
private fun Settings.getDeviceIpVersion() =
296+
relaySettings.relayConstraints.wireguardConstraints.ipVersion
297+
293298
private fun InetAddress.isLocalAddress(): Boolean {
294299
return isLinkLocalAddress || isSiteLocalAddress
295300
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel
33
import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
44
import net.mullvad.mullvadvpn.lib.model.Constraint
55
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
6+
import net.mullvad.mullvadvpn.lib.model.IpVersion
67
import net.mullvad.mullvadvpn.lib.model.Mtu
78
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
89
import net.mullvad.mullvadvpn.lib.model.Port
@@ -24,6 +25,7 @@ data class VpnSettingsViewModelState(
2425
val availablePortRanges: List<PortRange>,
2526
val systemVpnSettingsAvailable: Boolean,
2627
val autoStartAndConnectOnBoot: Boolean,
28+
val deviceIpVersion: Constraint<IpVersion>,
2729
) {
2830
val isCustomWireguardPort =
2931
selectedWireguardPort is Constraint.Only &&
@@ -45,6 +47,7 @@ data class VpnSettingsViewModelState(
4547
availablePortRanges,
4648
systemVpnSettingsAvailable,
4749
autoStartAndConnectOnBoot,
50+
deviceIpVersion,
4851
)
4952

5053
companion object {
@@ -64,6 +67,7 @@ data class VpnSettingsViewModelState(
6467
availablePortRanges = emptyList(),
6568
systemVpnSettingsAvailable = false,
6669
autoStartAndConnectOnBoot = false,
70+
deviceIpVersion = Constraint.Any,
6771
)
6872
}
6973
}

android/app/src/test/kotlin/net/mullvad/mullvadvpn/usecase/SelectedLocationUseCaseTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class SelectedLocationUseCaseTest {
4848
isMultihopEnabled = true,
4949
entryLocation = entryLocation,
5050
port = Constraint.Any,
51+
ipVersion = Constraint.Any,
5152
)
5253
selectedLocation.value = exitLocation
5354

android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/MultihopViewModelTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class MultihopViewModelTest {
4848
isMultihopEnabled = true,
4949
entryLocation = Constraint.Any,
5050
port = Constraint.Any,
51+
ipVersion = Constraint.Any,
5152
)
5253

5354
// Act, Assert

android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SettingsViewModelTest.kt

+1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class SettingsViewModelTest {
108108
isMultihopEnabled = true,
109109
entryLocation = Constraint.Any,
110110
port = Constraint.Any,
111+
ipVersion = Constraint.Any,
111112
)
112113

113114
// Act, Assert

android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt

+42-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel
33
import androidx.lifecycle.viewModelScope
44
import app.cash.turbine.test
55
import arrow.core.right
6+
import io.mockk.Awaits
67
import io.mockk.Runs
78
import io.mockk.coEvery
89
import io.mockk.coVerify
@@ -18,10 +19,10 @@ import kotlinx.coroutines.cancel
1819
import kotlinx.coroutines.flow.MutableStateFlow
1920
import kotlinx.coroutines.test.UnconfinedTestDispatcher
2021
import kotlinx.coroutines.test.runTest
21-
import mullvad_daemon.management_interface.daitaSettings
2222
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
2323
import net.mullvad.mullvadvpn.lib.model.Constraint
2424
import net.mullvad.mullvadvpn.lib.model.DaitaSettings
25+
import net.mullvad.mullvadvpn.lib.model.IpVersion
2526
import net.mullvad.mullvadvpn.lib.model.Mtu
2627
import net.mullvad.mullvadvpn.lib.model.Port
2728
import net.mullvad.mullvadvpn.lib.model.PortRange
@@ -163,6 +164,7 @@ class VpnSettingsViewModelTest {
163164
every { mockRelaySettings.relayConstraints } returns mockRelayConstraints
164165
every { mockRelayConstraints.wireguardConstraints } returns mockWireguardConstraints
165166
every { mockWireguardConstraints.port } returns expectedPort
167+
every { mockWireguardConstraints.ipVersion } returns Constraint.Any
166168
every { mockSettings.tunnelOptions } returns
167169
TunnelOptions(
168170
wireguard =
@@ -193,6 +195,7 @@ class VpnSettingsViewModelTest {
193195
port = wireguardPort,
194196
isMultihopEnabled = false,
195197
entryLocation = Constraint.Any,
198+
ipVersion = Constraint.Any,
196199
)
197200
coEvery { mockWireguardConstraintsRepository.setWireguardPort(any()) } returns
198201
Unit.right()
@@ -249,4 +252,42 @@ class VpnSettingsViewModelTest {
249252
mockAutoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(targetState)
250253
}
251254
}
255+
256+
@Test
257+
fun `when device ip version is IPv6 then UiState should be IPv6`() = runTest {
258+
// Arrange
259+
val ipVersion = Constraint.Only(IpVersion.IPV6)
260+
val mockSettings = mockk<Settings>(relaxed = true)
261+
every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.ipVersion } returns
262+
ipVersion
263+
every { mockSettings.tunnelOptions.wireguard } returns
264+
WireguardTunnelOptions(
265+
mtu = Mtu(0),
266+
quantumResistant = QuantumResistantState.Off,
267+
daitaSettings = DaitaSettings(enabled = false, directOnly = false),
268+
)
269+
every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.port } returns
270+
Constraint.Any
271+
272+
// Act, Assert
273+
viewModel.uiState.test {
274+
// Default value
275+
awaitItem()
276+
mockSettingsUpdate.value = mockSettings
277+
assertEquals(ipVersion, awaitItem().deviceIpVersion)
278+
}
279+
}
280+
281+
@Test
282+
fun `calling onDeviceIpVersionSelected should call setDeviceIpVersion`() = runTest {
283+
// Arrange
284+
val targetState = Constraint.Only(IpVersion.IPV4)
285+
coEvery { mockWireguardConstraintsRepository.setDeviceIpVersion(targetState) } just Awaits
286+
287+
// Act
288+
viewModel.onDeviceIpVersionSelected(targetState)
289+
290+
// Assert
291+
coVerify(exactly = 1) { mockWireguardConstraintsRepository.setDeviceIpVersion(targetState) }
292+
}
252293
}

android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt

+18
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import net.mullvad.mullvadvpn.lib.model.GetAccountHistoryError
7676
import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
7777
import net.mullvad.mullvadvpn.lib.model.GetDeviceStateError
7878
import net.mullvad.mullvadvpn.lib.model.GetVersionInfoError
79+
import net.mullvad.mullvadvpn.lib.model.IpVersion
7980
import net.mullvad.mullvadvpn.lib.model.LoginAccountError
8081
import net.mullvad.mullvadvpn.lib.model.LogoutAccountError
8182
import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
@@ -125,6 +126,7 @@ import net.mullvad.mullvadvpn.lib.model.addresses
125126
import net.mullvad.mullvadvpn.lib.model.customOptions
126127
import net.mullvad.mullvadvpn.lib.model.enabled
127128
import net.mullvad.mullvadvpn.lib.model.entryLocation
129+
import net.mullvad.mullvadvpn.lib.model.ipVersion
128130
import net.mullvad.mullvadvpn.lib.model.isMultihopEnabled
129131
import net.mullvad.mullvadvpn.lib.model.location
130132
import net.mullvad.mullvadvpn.lib.model.ownership
@@ -784,6 +786,22 @@ class ManagementService(
784786
.mapLeft(SetWireguardConstraintsError::Unknown)
785787
.mapEmpty()
786788

789+
suspend fun setDeviceIpVersion(
790+
ipVersion: Constraint<IpVersion>
791+
): Either<SetWireguardConstraintsError, Unit> =
792+
Either.catch {
793+
val relaySettings = getSettings().relaySettings
794+
val updated =
795+
RelaySettings.relayConstraints.wireguardConstraints.ipVersion.set(
796+
relaySettings,
797+
ipVersion,
798+
)
799+
grpc.setRelaySettings(updated.fromDomain())
800+
}
801+
.onLeft { Logger.e("Set multihop error") }
802+
.mapLeft(SetWireguardConstraintsError::Unknown)
803+
.mapEmpty()
804+
787805
private fun <A> Either<A, Empty>.mapEmpty() = map {}
788806

789807
private inline fun <B, C> Either<Throwable, B>.mapLeftStatus(

0 commit comments

Comments
 (0)