Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement device ip setting #7763

Merged
merged 5 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Line wrap the file at 100 chars. Th

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

### Changed
- Disable Wireguard port setting when a obfuscation is selected since it is not used when an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_CUSTOM_PORT_TEXT_
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_OBFUSCATION_TITLE_TEST_TAG
import net.mullvad.mullvadvpn.compose.test.LAZY_LIST_WIREGUARD_PORT_ITEM_X_TEST_TAG
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
Expand Down Expand Up @@ -72,6 +73,7 @@ class VpnSettingsScreenTest {
navigateToShadowSocksSettings: () -> Unit = {},
navigateToUdp2TcpSettings: () -> Unit = {},
onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = {},
onSelectDeviceIpVersion: (Constraint<IpVersion>) -> Unit = {},
) {
setContentWithTheme {
VpnSettingsScreen(
Expand Down Expand Up @@ -103,6 +105,7 @@ class VpnSettingsScreenTest {
navigateToShadowSocksSettings = navigateToShadowSocksSettings,
navigateToUdp2TcpSettings = navigateToUdp2TcpSettings,
onToggleAutoStartAndConnectOnBoot = onToggleAutoStartAndConnectOnBoot,
onSelectDeviceIpVersion = onSelectDeviceIpVersion,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import net.mullvad.mullvadvpn.compose.util.OnNavResultValue
import net.mullvad.mullvadvpn.compose.util.showSnackbarImmediately
import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
Expand Down Expand Up @@ -140,6 +141,7 @@ private fun PreviewVpnSettings(
navigateToLocalNetworkSharingInfo = {},
navigateToWireguardPortDialog = {},
navigateToServerIpOverrides = {},
onSelectDeviceIpVersion = {},
)
}
}
Expand Down Expand Up @@ -268,6 +270,7 @@ fun VpnSettings(
navigateToUdp2TcpSettings =
dropUnlessResumed { navigator.navigate(Udp2TcpSettingsDestination) },
onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot,
onSelectDeviceIpVersion = vm::onDeviceIpVersionSelected,
)
}

Expand Down Expand Up @@ -304,6 +307,7 @@ fun VpnSettingsScreen(
navigateToShadowSocksSettings: () -> Unit,
navigateToUdp2TcpSettings: () -> Unit,
onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit,
onSelectDeviceIpVersion: (ipVersion: Constraint<IpVersion>) -> Unit,
) {
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
val biggerPadding = 54.dp
Expand Down Expand Up @@ -651,6 +655,32 @@ fun VpnSettingsScreen(
Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing))
}

itemWithDivider {
InformationComposeCell(title = stringResource(R.string.device_ip_version_title))
}
itemWithDivider {
SelectableCell(
title = stringResource(id = R.string.automatic),
isSelected = state.deviceIpVersion == Constraint.Any,
onCellClicked = { onSelectDeviceIpVersion(Constraint.Any) },
)
}
itemWithDivider {
SelectableCell(
title = stringResource(id = R.string.device_ip_version_ipv4),
isSelected = state.deviceIpVersion.getOrNull() == IpVersion.IPV4,
onCellClicked = { onSelectDeviceIpVersion(Constraint.Only(IpVersion.IPV4)) },
)
}
item {
SelectableCell(
title = stringResource(id = R.string.device_ip_version_ipv6),
isSelected = state.deviceIpVersion.getOrNull() == IpVersion.IPV6,
onCellClicked = { onSelectDeviceIpVersion(Constraint.Only(IpVersion.IPV6)) },
)
Spacer(modifier = Modifier.height(Dimens.cellVerticalSpacing))
}

item {
MtuComposeCell(mtuValue = state.mtu, onEditMtu = { navigateToMtuDialog(state.mtu) })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.compose.state

import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
Expand All @@ -24,6 +25,7 @@ data class VpnSettingsUiState(
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
val autoStartAndConnectOnBoot: Boolean,
val deviceIpVersion: Constraint<IpVersion>,
) {
val isCustomWireguardPort =
selectedWireguardPort is Constraint.Only &&
Expand All @@ -48,6 +50,7 @@ data class VpnSettingsUiState(
availablePortRanges: List<PortRange> = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
autoStartAndConnectOnBoot: Boolean = false,
deviceIpVersion: Constraint<IpVersion> = Constraint.Any,
) =
VpnSettingsUiState(
mtu,
Expand All @@ -64,6 +67,7 @@ data class VpnSettingsUiState(
availablePortRanges,
systemVpnSettingsAvailable,
autoStartAndConnectOnBoot,
deviceIpVersion,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.RelayItemId

Expand All @@ -26,4 +27,7 @@ class WireguardConstraintsRepository(

suspend fun setEntryLocation(relayItemId: RelayItemId) =
managementService.setEntryLocation(relayItemId)

suspend fun setDeviceIpVersion(ipVersion: Constraint<IpVersion>) =
managementService.setDeviceIpVersion(ipVersion)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import net.mullvad.mullvadvpn.constant.WIREGUARD_PRESET_PORTS
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.DnsState
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
Expand All @@ -46,7 +47,7 @@ sealed interface VpnSettingsSideEffect {
@Suppress("TooManyFunctions")
class VpnSettingsViewModel(
private val repository: SettingsRepository,
private val relayListRepository: RelayListRepository,
relayListRepository: RelayListRepository,
private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase,
private val autoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository,
private val wireguardConstraintsRepository: WireguardConstraintsRepository,
Expand Down Expand Up @@ -83,6 +84,7 @@ class VpnSettingsViewModel(
availablePortRanges = portRanges,
systemVpnSettingsAvailable = systemVpnSettingsUseCase(),
autoStartAndConnectOnBoot = autoStartAndConnectOnBoot,
deviceIpVersion = settings?.getDeviceIpVersion() ?: Constraint.Any,
)
}
.stateIn(
Expand Down Expand Up @@ -122,14 +124,6 @@ class VpnSettingsViewModel(
}
}

fun onToggleDaita(enable: Boolean) {
viewModelScope.launch(dispatcher) {
repository.setDaitaEnabled(enable).onLeft {
_uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
}
}
}

fun onDnsDialogDismissed() {
if (vmState.value.customDnsList.isEmpty()) {
onToggleCustomDns(enable = false)
Expand Down Expand Up @@ -251,6 +245,14 @@ class VpnSettingsViewModel(
}
}

fun onDeviceIpVersionSelected(ipVersion: Constraint<IpVersion>) {
viewModelScope.launch(dispatcher) {
wireguardConstraintsRepository.setDeviceIpVersion(ipVersion).onLeft {
_uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
}
}
}

private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) =
viewModelScope.launch(dispatcher) {
repository
Expand All @@ -265,7 +267,7 @@ class VpnSettingsViewModel(
private fun List<String>.asInetAddressList(): List<InetAddress> {
return try {
map { InetAddress.getByName(it) }
} catch (ex: UnknownHostException) {
} catch (_: UnknownHostException) {
Logger.e("Error parsing the DNS address list.")
emptyList()
}
Expand All @@ -290,6 +292,9 @@ class VpnSettingsViewModel(
private fun Settings.getWireguardPort() =
relaySettings.relayConstraints.wireguardConstraints.port

private fun Settings.getDeviceIpVersion() =
relaySettings.relayConstraints.wireguardConstraints.ipVersion

private fun InetAddress.isLocalAddress(): Boolean {
return isLinkLocalAddress || isSiteLocalAddress
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel
import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.ObfuscationMode
import net.mullvad.mullvadvpn.lib.model.Port
Expand All @@ -24,6 +25,7 @@ data class VpnSettingsViewModelState(
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
val autoStartAndConnectOnBoot: Boolean,
val deviceIpVersion: Constraint<IpVersion>,
) {
val isCustomWireguardPort =
selectedWireguardPort is Constraint.Only &&
Expand All @@ -45,6 +47,7 @@ data class VpnSettingsViewModelState(
availablePortRanges,
systemVpnSettingsAvailable,
autoStartAndConnectOnBoot,
deviceIpVersion,
)

companion object {
Expand All @@ -64,6 +67,7 @@ data class VpnSettingsViewModelState(
availablePortRanges = emptyList(),
systemVpnSettingsAvailable = false,
autoStartAndConnectOnBoot = false,
deviceIpVersion = Constraint.Any,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class SelectedLocationUseCaseTest {
isMultihopEnabled = true,
entryLocation = entryLocation,
port = Constraint.Any,
ipVersion = Constraint.Any,
)
selectedLocation.value = exitLocation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class MultihopViewModelTest {
isMultihopEnabled = true,
entryLocation = Constraint.Any,
port = Constraint.Any,
ipVersion = Constraint.Any,
)

// Act, Assert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ class SettingsViewModelTest {
isMultihopEnabled = true,
entryLocation = Constraint.Any,
port = Constraint.Any,
ipVersion = Constraint.Any,
)

// Act, Assert
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import arrow.core.right
import io.mockk.Awaits
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
Expand All @@ -18,10 +19,10 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import mullvad_daemon.management_interface.daitaSettings
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.model.Constraint
import net.mullvad.mullvadvpn.lib.model.DaitaSettings
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.Mtu
import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.PortRange
Expand Down Expand Up @@ -163,6 +164,7 @@ class VpnSettingsViewModelTest {
every { mockRelaySettings.relayConstraints } returns mockRelayConstraints
every { mockRelayConstraints.wireguardConstraints } returns mockWireguardConstraints
every { mockWireguardConstraints.port } returns expectedPort
every { mockWireguardConstraints.ipVersion } returns Constraint.Any
every { mockSettings.tunnelOptions } returns
TunnelOptions(
wireguard =
Expand Down Expand Up @@ -193,6 +195,7 @@ class VpnSettingsViewModelTest {
port = wireguardPort,
isMultihopEnabled = false,
entryLocation = Constraint.Any,
ipVersion = Constraint.Any,
)
coEvery { mockWireguardConstraintsRepository.setWireguardPort(any()) } returns
Unit.right()
Expand Down Expand Up @@ -249,4 +252,42 @@ class VpnSettingsViewModelTest {
mockAutoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(targetState)
}
}

@Test
fun `when device ip version is IPv6 then UiState should be IPv6`() = runTest {
// Arrange
val ipVersion = Constraint.Only(IpVersion.IPV6)
val mockSettings = mockk<Settings>(relaxed = true)
every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.ipVersion } returns
ipVersion
every { mockSettings.tunnelOptions.wireguard } returns
WireguardTunnelOptions(
mtu = Mtu(0),
quantumResistant = QuantumResistantState.Off,
daitaSettings = DaitaSettings(enabled = false, directOnly = false),
)
every { mockSettings.relaySettings.relayConstraints.wireguardConstraints.port } returns
Constraint.Any

// Act, Assert
viewModel.uiState.test {
// Default value
awaitItem()
mockSettingsUpdate.value = mockSettings
assertEquals(ipVersion, awaitItem().deviceIpVersion)
}
}

@Test
fun `calling onDeviceIpVersionSelected should call setDeviceIpVersion`() = runTest {
// Arrange
val targetState = Constraint.Only(IpVersion.IPV4)
coEvery { mockWireguardConstraintsRepository.setDeviceIpVersion(targetState) } just Awaits

// Act
viewModel.onDeviceIpVersionSelected(targetState)

// Assert
coVerify(exactly = 1) { mockWireguardConstraintsRepository.setDeviceIpVersion(targetState) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import net.mullvad.mullvadvpn.lib.model.GetAccountHistoryError
import net.mullvad.mullvadvpn.lib.model.GetDeviceListError
import net.mullvad.mullvadvpn.lib.model.GetDeviceStateError
import net.mullvad.mullvadvpn.lib.model.GetVersionInfoError
import net.mullvad.mullvadvpn.lib.model.IpVersion
import net.mullvad.mullvadvpn.lib.model.LoginAccountError
import net.mullvad.mullvadvpn.lib.model.LogoutAccountError
import net.mullvad.mullvadvpn.lib.model.NameAlreadyExists
Expand Down Expand Up @@ -125,6 +126,7 @@ import net.mullvad.mullvadvpn.lib.model.addresses
import net.mullvad.mullvadvpn.lib.model.customOptions
import net.mullvad.mullvadvpn.lib.model.enabled
import net.mullvad.mullvadvpn.lib.model.entryLocation
import net.mullvad.mullvadvpn.lib.model.ipVersion
import net.mullvad.mullvadvpn.lib.model.isMultihopEnabled
import net.mullvad.mullvadvpn.lib.model.location
import net.mullvad.mullvadvpn.lib.model.ownership
Expand Down Expand Up @@ -784,6 +786,22 @@ class ManagementService(
.mapLeft(SetWireguardConstraintsError::Unknown)
.mapEmpty()

suspend fun setDeviceIpVersion(
ipVersion: Constraint<IpVersion>
): Either<SetWireguardConstraintsError, Unit> =
Either.catch {
val relaySettings = getSettings().relaySettings
val updated =
RelaySettings.relayConstraints.wireguardConstraints.ipVersion.set(
relaySettings,
ipVersion,
)
grpc.setRelaySettings(updated.fromDomain())
}
.onLeft { Logger.e("Set multihop error") }
.mapLeft(SetWireguardConstraintsError::Unknown)
.mapEmpty()

private fun <A> Either<A, Empty>.mapEmpty() = map {}

private inline fun <B, C> Either<Throwable, B>.mapLeftStatus(
Expand Down
Loading