diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt index 158e23e15118..e0da5f1b620b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/LocationInfo.kt @@ -33,6 +33,7 @@ private fun PreviewLocationInfo() { isVisible = true, isExpanded = true, location = null, + isUsingDaita = false, inAddress = null, outAddress = "" ) @@ -48,6 +49,7 @@ fun LocationInfo( isVisible: Boolean, isExpanded: Boolean, location: GeoIpLocation?, + isUsingDaita: Boolean, inAddress: Triple?, outAddress: String ) { @@ -61,15 +63,12 @@ fun LocationInfo( .then(modifier) ) { Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = location?.hostname ?: "", - color = - if (isExpanded) { - colorExpanded - } else { - colorCollapsed - }, - style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold) + RelayHostname( + hostname = location?.hostname, + isUsingDaita = isUsingDaita, + isExpanded = isExpanded, + colorExpanded = colorExpanded, + colorCollapsed = colorCollapsed, ) Chevron( isExpanded = isExpanded, @@ -119,3 +118,38 @@ fun LocationInfo( ) } } + +@Composable +private fun RelayHostname( + hostname: String?, + isUsingDaita: Boolean, + isExpanded: Boolean, + colorExpanded: Color, + colorCollapsed: Color +) { + val hostnameTitle = + when { + hostname != null && isUsingDaita -> { + stringResource( + id = R.string.connected_using_daita, + hostname, + stringResource( + id = R.string.daita, + ) + ) + } + hostname != null -> hostname + else -> "" + } + + Text( + text = hostnameTitle, + color = + if (isExpanded) { + colorExpanded + } else { + colorCollapsed + }, + style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold), + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaInfoDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaInfoDialog.kt new file mode 100644 index 000000000000..a7e2b0b78ebd --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/DaitaInfoDialog.kt @@ -0,0 +1,35 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.dropUnlessResumed +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator +import com.ramcosta.composedestinations.spec.DestinationStyle +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.lib.theme.AppTheme + +@Preview +@Composable +private fun PreviewDaitaInfoDialog() { + AppTheme { DaitaInfo(EmptyDestinationsNavigator) } +} + +@Destination(style = DestinationStyle.Dialog::class) +@Composable +fun DaitaInfo(navigator: DestinationsNavigator) { + InfoDialog( + message = + stringResource( + id = R.string.daita_info, + stringResource(id = R.string.daita), + stringResource(id = R.string.daita_full), + ), + additionalInfo = + stringResource(id = R.string.daita_warning, stringResource(id = R.string.daita)), + onDismiss = dropUnlessResumed { navigator.navigateUp() }, + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt index c1b42c9415e3..417ddbef978e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/RelayItemPreviewData.kt @@ -51,6 +51,7 @@ private fun generateRelayItemRelay( cityCode: GeoLocationId.City, hostName: String, active: Boolean = true, + daita: Boolean = true, ) = RelayItem.Location.Relay( id = @@ -60,6 +61,7 @@ private fun generateRelayItemRelay( ), active = active, provider = Provider(ProviderId("Provider"), Ownership.MullvadOwned), + daita = daita ) private fun String.generateCountryCode() = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt index 7045cc45dcf6..050ae32d8262 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/TunnelStatePreviewData.kt @@ -18,14 +18,14 @@ object TunnelStatePreviewData { fun generateConnectingState(featureIndicators: Int, quantumResistant: Boolean) = TunnelState.Connecting( - endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant), + endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant, daita = false), location = generateLocation(), featureIndicators = generateFeatureIndicators(featureIndicators) ) fun generateConnectedState(featureIndicators: Int, quantumResistant: Boolean) = TunnelState.Connected( - endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant), + endpoint = generateTunnelEndpoint(quantumResistant = quantumResistant, daita = true), location = generateLocation(), featureIndicators = generateFeatureIndicators(featureIndicators) ) @@ -39,7 +39,7 @@ object TunnelStatePreviewData { ) } -private fun generateTunnelEndpoint(quantumResistant: Boolean): TunnelEndpoint = +private fun generateTunnelEndpoint(quantumResistant: Boolean, daita: Boolean): TunnelEndpoint = TunnelEndpoint( endpoint = generateEndpoint(TransportProtocol.Udp), quantumResistant = quantumResistant, @@ -47,7 +47,8 @@ private fun generateTunnelEndpoint(quantumResistant: Boolean): TunnelEndpoint = ObfuscationEndpoint( endpoint = generateEndpoint(TransportProtocol.Tcp), ObfuscationType.Udp2Tcp - ) + ), + daita = daita ) private fun generateEndpoint(transportProtocol: TransportProtocol) = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt index 466aa0580b0b..5a34cd812295 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt @@ -365,6 +365,7 @@ private fun ConnectionInfo(state: ConnectUiState) { isVisible = state.showLocationInfo, isExpanded = expanded, location = state.location, + isUsingDaita = state.tunnelState.isUsingDaita(), inAddress = state.inAddress, outAddress = state.outAddress, modifier = diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt index 2a5157319807..0a49d2ab94b5 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt @@ -36,6 +36,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AutoConnectAndLockdownModeDestination import com.ramcosta.composedestinations.generated.destinations.ContentBlockersInfoDestination import com.ramcosta.composedestinations.generated.destinations.CustomDnsInfoDestination +import com.ramcosta.composedestinations.generated.destinations.DaitaInfoDestination import com.ramcosta.composedestinations.generated.destinations.DnsDestination import com.ramcosta.composedestinations.generated.destinations.LocalNetworkSharingInfoDestination import com.ramcosta.composedestinations.generated.destinations.MalwareInfoDestination @@ -224,6 +225,7 @@ fun VpnSettings( }, navigateToLocalNetworkSharingInfo = dropUnlessResumed { navigator.navigate(LocalNetworkSharingInfoDestination) }, + navigateToDaitaInfo = dropUnlessResumed { navigator.navigate(DaitaInfoDestination) }, navigateToServerIpOverrides = dropUnlessResumed { navigator.navigate(ServerIpOverridesDestination) }, onToggleBlockTrackers = vm::onToggleBlockTrackers, @@ -231,6 +233,7 @@ fun VpnSettings( onToggleBlockMalware = vm::onToggleBlockMalware, onToggleAutoConnect = vm::onToggleAutoConnect, onToggleLocalNetworkSharing = vm::onToggleLocalNetworkSharing, + onToggleDaita = vm::onToggleDaita, onToggleBlockAdultContent = vm::onToggleBlockAdultContent, onToggleBlockGambling = vm::onToggleBlockGambling, onToggleBlockSocialMedia = vm::onToggleBlockSocialMedia, @@ -273,6 +276,7 @@ fun VpnSettingsScreen( navigateUdp2TcpInfo: () -> Unit = {}, navigateToWireguardPortInfo: (availablePortRanges: List) -> Unit = {}, navigateToLocalNetworkSharingInfo: () -> Unit = {}, + navigateToDaitaInfo: () -> Unit = {}, navigateToWireguardPortDialog: () -> Unit = {}, navigateToServerIpOverrides: () -> Unit = {}, onToggleBlockTrackers: (Boolean) -> Unit = {}, @@ -280,6 +284,7 @@ fun VpnSettingsScreen( onToggleBlockMalware: (Boolean) -> Unit = {}, onToggleAutoConnect: (Boolean) -> Unit = {}, onToggleLocalNetworkSharing: (Boolean) -> Unit = {}, + onToggleDaita: (Boolean) -> Unit = {}, onToggleBlockAdultContent: (Boolean) -> Unit = {}, onToggleBlockGambling: (Boolean) -> Unit = {}, onToggleBlockSocialMedia: (Boolean) -> Unit = {}, @@ -497,8 +502,19 @@ fun VpnSettingsScreen( ) } - itemWithDivider { + item { Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) + HeaderSwitchComposeCell( + title = "DAITA", + isToggled = state.isDaitaEnabled, + isEnabled = true, + onCellClicked = { newValue -> onToggleDaita(newValue) }, + onInfoClicked = navigateToDaitaInfo + ) + Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) + } + + itemWithDivider { InformationComposeCell( title = stringResource(id = R.string.wireguard_port_title), onInfoClicked = { navigateToWireguardPortInfo(state.availablePortRanges) }, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt index 17eb69d380e8..00b86b3a4f60 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt @@ -13,6 +13,7 @@ data class VpnSettingsUiState( val mtu: Mtu?, val isAutoConnectEnabled: Boolean, val isLocalNetworkSharingEnabled: Boolean, + val isDaitaEnabled: Boolean, val isCustomDnsEnabled: Boolean, val customDnsItems: List, val contentBlockersOptions: DefaultDnsOptions, @@ -31,6 +32,7 @@ data class VpnSettingsUiState( mtu: Mtu? = null, isAutoConnectEnabled: Boolean = false, isLocalNetworkSharingEnabled: Boolean = false, + isDaitaEnabled: Boolean = false, isCustomDnsEnabled: Boolean = false, customDnsItems: List = emptyList(), contentBlockersOptions: DefaultDnsOptions = DefaultDnsOptions(), @@ -46,6 +48,7 @@ data class VpnSettingsUiState( mtu, isAutoConnectEnabled, isLocalNetworkSharingEnabled, + isDaitaEnabled, isCustomDnsEnabled, customDnsItems, contentBlockersOptions, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 929d1e3b9981..ab428bdd0304 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -138,7 +138,7 @@ val uiModule = module { single { FilterCustomListsRelayItemUseCase(get(), get()) } single { CustomListsRelayItemUseCase(get(), get()) } single { CustomListRelayItemsUseCase(get(), get()) } - single { FilteredRelayListUseCase(get(), get()) } + single { FilteredRelayListUseCase(get(), get(), get()) } single { LastKnownLocationUseCase(get()) } single { InAppNotificationController(get(), get(), get(), get(), MainScope()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt index a3758b25fef9..19abfcf6082d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/relaylist/RelayItemExtensions.kt @@ -53,27 +53,29 @@ private fun RelayItem.Location.hasProvider(providersConstraint: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.CustomList { val newLocations = locations.mapNotNull { when (it) { is RelayItem.Location.Country -> - it.filterOnOwnershipAndProvider(ownership, providers) - is RelayItem.Location.City -> it.filterOnOwnershipAndProvider(ownership, providers) - is RelayItem.Location.Relay -> it.filterOnOwnershipAndProvider(ownership, providers) + it.applyFilters(ownership, providers, isDaitaEnabled) + is RelayItem.Location.City -> it.applyFilters(ownership, providers, isDaitaEnabled) + is RelayItem.Location.Relay -> it.applyFilters(ownership, providers, isDaitaEnabled) } } return copy(locations = newLocations) } -fun RelayItem.Location.Country.filterOnOwnershipAndProvider( +fun RelayItem.Location.Country.applyFilters( ownership: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.Location.Country? { - val cities = cities.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + val cities = cities.mapNotNull { it.applyFilters(ownership, providers, isDaitaEnabled) } return if (cities.isNotEmpty()) { this.copy(cities = cities) } else { @@ -81,11 +83,12 @@ fun RelayItem.Location.Country.filterOnOwnershipAndProvider( } } -private fun RelayItem.Location.City.filterOnOwnershipAndProvider( +private fun RelayItem.Location.City.applyFilters( ownership: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.Location.City? { - val relays = relays.mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + val relays = relays.mapNotNull { it.applyFilters(ownership, providers, isDaitaEnabled) } return if (relays.isNotEmpty()) { this.copy(relays = relays) } else { @@ -93,11 +96,18 @@ private fun RelayItem.Location.City.filterOnOwnershipAndProvider( } } -private fun RelayItem.Location.Relay.filterOnOwnershipAndProvider( +private fun RelayItem.Location.Relay.hasMatchingDaitaSetting(isDaitaEnabled: Boolean): Boolean { + return if (isDaitaEnabled) daita else true +} + +private fun RelayItem.Location.Relay.applyFilters( ownership: Constraint, - providers: Constraint + providers: Constraint, + isDaitaEnabled: Boolean ): RelayItem.Location.Relay? { - return if (hasOwnership(ownership) && hasProvider(providers)) { + return if ( + hasMatchingDaitaSetting(isDaitaEnabled) && hasOwnership(ownership) && hasProvider(providers) + ) { this } else { null diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt index 7a9be0303a13..790e3b0c7162 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt @@ -10,6 +10,7 @@ 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.CustomDnsOptions +import net.mullvad.mullvadvpn.lib.model.DaitaSettings import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.DnsOptions import net.mullvad.mullvadvpn.lib.model.DnsState @@ -70,4 +71,7 @@ class SettingsRepository( suspend fun setLocalNetworkSharing(isEnabled: Boolean) = managementService.setAllowLan(isEnabled) + + suspend fun setDaitaSettings(daitaSettings: DaitaSettings) = + managementService.setDaitaSettings(daitaSettings) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt index c4e05ccc92cc..8b5a8a11aa3c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/FilteredRelayListUseCase.kt @@ -5,28 +5,33 @@ import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.RelayItem -import net.mullvad.mullvadvpn.relaylist.filterOnOwnershipAndProvider +import net.mullvad.mullvadvpn.relaylist.applyFilters import net.mullvad.mullvadvpn.repository.RelayListFilterRepository import net.mullvad.mullvadvpn.repository.RelayListRepository +import net.mullvad.mullvadvpn.repository.SettingsRepository class FilteredRelayListUseCase( private val relayListRepository: RelayListRepository, - private val relayListFilterRepository: RelayListFilterRepository + private val relayListFilterRepository: RelayListFilterRepository, + private val settingsRepository: SettingsRepository ) { operator fun invoke() = combine( relayListRepository.relayList, relayListFilterRepository.selectedOwnership, relayListFilterRepository.selectedProviders, - ) { relayList, selectedOwnership, selectedProviders -> - relayList.filterOnOwnershipAndProvider( + settingsRepository.settingsUpdates + ) { relayList, selectedOwnership, selectedProviders, settings -> + relayList.applyFilters( selectedOwnership, selectedProviders, + isDaitaEnabled = settings?.tunnelOptions?.wireguard?.daita ?: false ) } - private fun List.filterOnOwnershipAndProvider( + private fun List.applyFilters( ownership: Constraint, - providers: Constraint - ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + providers: Constraint, + isDaitaEnabled: Boolean + ) = mapNotNull { it.applyFilters(ownership, providers, isDaitaEnabled) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt index f82d9eed5e20..671035a377e6 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/usecase/customlists/FilterCustomListsRelayItemUseCase.kt @@ -6,7 +6,7 @@ import net.mullvad.mullvadvpn.lib.model.Constraint import net.mullvad.mullvadvpn.lib.model.Ownership import net.mullvad.mullvadvpn.lib.model.Providers import net.mullvad.mullvadvpn.lib.model.RelayItem -import net.mullvad.mullvadvpn.relaylist.filterOnOwnershipAndProvider +import net.mullvad.mullvadvpn.relaylist.applyFilters import net.mullvad.mullvadvpn.repository.RelayListFilterRepository class FilterCustomListsRelayItemUseCase( @@ -26,5 +26,5 @@ class FilterCustomListsRelayItemUseCase( private fun List.filterOnOwnershipAndProvider( ownership: Constraint, providers: Constraint - ) = mapNotNull { it.filterOnOwnershipAndProvider(ownership, providers) } + ) = mapNotNull { it.applyFilters(ownership, providers, isDaitaEnabled = false) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt index 1e9a335951d3..6c0d1905073d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.VpnSettingsUiState import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.DaitaSettings import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.DnsState import net.mullvad.mullvadvpn.lib.model.Port @@ -64,6 +65,7 @@ class VpnSettingsViewModel( mtuValue = settings?.tunnelOptions?.wireguard?.mtu, isAutoConnectEnabled = settings?.autoConnect ?: false, isLocalNetworkSharingEnabled = settings?.allowLan ?: false, + isDaitaEnabled = settings?.tunnelOptions?.wireguard?.daita ?: false, isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false, customDnsList = settings?.addresses()?.asStringAddressList() ?: listOf(), contentBlockersOptions = @@ -124,6 +126,14 @@ class VpnSettingsViewModel( } } + fun onToggleDaita(isEnabled: Boolean) { + viewModelScope.launch(dispatcher) { + repository + .setDaitaSettings(if (isEnabled) DaitaSettings.On else DaitaSettings.Off) + .onLeft { _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError) } + } + } + fun onDnsDialogDismissed() { if (vmState.value.customDnsList.isEmpty()) { onToggleCustomDns(enable = false) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt index d8be8d1cf27e..38979fd4b955 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt @@ -13,6 +13,7 @@ data class VpnSettingsViewModelState( val mtuValue: Mtu?, val isAutoConnectEnabled: Boolean, val isLocalNetworkSharingEnabled: Boolean, + val isDaitaEnabled: Boolean, val isCustomDnsEnabled: Boolean, val customDnsList: List, val contentBlockersOptions: DefaultDnsOptions, @@ -29,6 +30,7 @@ data class VpnSettingsViewModelState( mtuValue, isAutoConnectEnabled, isLocalNetworkSharingEnabled, + isDaitaEnabled, isCustomDnsEnabled, customDnsList, contentBlockersOptions, @@ -47,6 +49,7 @@ data class VpnSettingsViewModelState( mtuValue = null, isAutoConnectEnabled = false, isLocalNetworkSharingEnabled = false, + isDaitaEnabled = false, isCustomDnsEnabled = false, customDnsList = listOf(), contentBlockersOptions = DefaultDnsOptions(), diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt index add2ee8580d5..8b6618b7e19c 100644 --- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt +++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt @@ -145,7 +145,8 @@ class VpnSettingsViewModelTest { wireguard = WireguardTunnelOptions( mtu = null, - quantumResistant = QuantumResistantState.Off + quantumResistant = QuantumResistantState.Off, + daita = false ), dnsOptions = mockk(relaxed = true) ) diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt index c10f3b58e661..1a574dd6fc67 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt @@ -63,6 +63,7 @@ import net.mullvad.mullvadvpn.lib.model.CustomList as ModelCustomList import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.DaitaSettings import net.mullvad.mullvadvpn.lib.model.DeleteCustomListError import net.mullvad.mullvadvpn.lib.model.DeleteDeviceError import net.mullvad.mullvadvpn.lib.model.Device @@ -104,6 +105,7 @@ import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation import net.mullvad.mullvadvpn.lib.model.SetAllowLanError import net.mullvad.mullvadvpn.lib.model.SetApiAccessMethodError import net.mullvad.mullvadvpn.lib.model.SetAutoConnectError +import net.mullvad.mullvadvpn.lib.model.SetDaitaSettingsError import net.mullvad.mullvadvpn.lib.model.SetDnsOptionsError import net.mullvad.mullvadvpn.lib.model.SetObfuscationOptionsError import net.mullvad.mullvadvpn.lib.model.SetRelayLocationError @@ -501,6 +503,13 @@ class ManagementService( .mapLeft(SetAllowLanError::Unknown) .mapEmpty() + suspend fun setDaitaSettings( + daitaSettings: DaitaSettings + ): Either = + Either.catch { grpc.setDaitaSettings(daitaSettings.toDomain()) } + .mapLeft(SetDaitaSettingsError::Unknown) + .mapEmpty() + suspend fun setRelayLocation(location: ModelRelayItemId): Either = Either.catch { val currentRelaySettings = getSettings().relaySettings diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt index ca4e924b6cdf..85ce44ff20eb 100644 --- a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt @@ -25,6 +25,7 @@ import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions import net.mullvad.mullvadvpn.lib.model.CustomList import net.mullvad.mullvadvpn.lib.model.CustomListId import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.DaitaSettings import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions import net.mullvad.mullvadvpn.lib.model.Device import net.mullvad.mullvadvpn.lib.model.DeviceId @@ -68,6 +69,7 @@ import net.mullvad.mullvadvpn.lib.model.TunnelState import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings import net.mullvad.mullvadvpn.lib.model.WireguardConstraints import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData +import net.mullvad.mullvadvpn.lib.model.WireguardRelayEndpointData import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions import org.joda.time.Instant @@ -159,7 +161,8 @@ internal fun ManagementInterface.TunnelEndpoint.toDomain(): TunnelEndpoint = obfuscation.toDomain() } else { null - } + }, + daita = daita ) internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndpoint = @@ -372,6 +375,7 @@ internal fun ManagementInterface.TunnelOptions.WireguardOptions.toDomain(): Wire WireguardTunnelOptions( mtu = if (hasMtu()) Mtu(mtu) else null, quantumResistant = quantumResistant.toDomain(), + daita = daita.enabled ) internal fun ManagementInterface.QuantumResistantState.toDomain(): QuantumResistantState = @@ -423,6 +427,16 @@ internal fun QuantumResistantState.toDomain(): ManagementInterface.QuantumResist ) .build() +internal fun DaitaSettings.toDomain(): ManagementInterface.DaitaSettings = + ManagementInterface.DaitaSettings.newBuilder() + .setEnabled( + when (this) { + DaitaSettings.On -> true + DaitaSettings.Off -> false + } + ) + .build() + internal fun ManagementInterface.AppVersionInfo.toDomain(): AppVersionInfo = AppVersionInfo( supported = supported, @@ -442,7 +456,12 @@ internal fun ManagementInterface.RelayList.toDomain(): RelayList = RelayList(countriesList.toDomain(), wireguard.toDomain()) internal fun ManagementInterface.WireguardEndpointData.toDomain(): WireguardEndpointData = - WireguardEndpointData(portRangesList.map { it.toDomain() }) + WireguardEndpointData( + portRangesList.map { it.toDomain() }, + ) + +internal fun ManagementInterface.WireguardRelayEndpointData.toDomain(): WireguardRelayEndpointData = + WireguardRelayEndpointData(daita) internal fun ManagementInterface.PortRange.toDomain(): PortRange = PortRange(first..last) @@ -494,7 +513,13 @@ internal fun ManagementInterface.Relay.toDomain( Provider( ProviderId(provider), ownership = if (owned) Ownership.MullvadOwned else Ownership.Rented - ) + ), + daita = + if ( + hasEndpointData() && endpointType == ManagementInterface.Relay.RelayType.WIREGUARD + ) { + ManagementInterface.WireguardRelayEndpointData.parseFrom(endpointData.value).daita + } else false ) internal fun ManagementInterface.Device.toDomain(): Device = @@ -598,11 +623,11 @@ internal fun ManagementInterface.FeatureIndicator.toDomain() = ManagementInterface.FeatureIndicator.SERVER_IP_OVERRIDE -> FeatureIndicator.SERVER_IP_OVERRIDE ManagementInterface.FeatureIndicator.CUSTOM_MTU -> FeatureIndicator.CUSTOM_MTU + ManagementInterface.FeatureIndicator.DAITA -> FeatureIndicator.DAITA ManagementInterface.FeatureIndicator.LOCKDOWN_MODE, ManagementInterface.FeatureIndicator.SHADOWSOCKS, ManagementInterface.FeatureIndicator.MULTIHOP, ManagementInterface.FeatureIndicator.BRIDGE_MODE, ManagementInterface.FeatureIndicator.CUSTOM_MSS_FIX, - ManagementInterface.FeatureIndicator.DAITA, ManagementInterface.FeatureIndicator.UNRECOGNIZED -> error("Feature not supported") } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DaitaSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DaitaSettings.kt new file mode 100644 index 000000000000..c33c61eb9cdd --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DaitaSettings.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class DaitaSettings { + On, + Off +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt index 7ad0b3ab6998..d11f40586950 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/FeatureIndicator.kt @@ -9,12 +9,12 @@ enum class FeatureIndicator { CUSTOM_DNS, SERVER_IP_OVERRIDE, CUSTOM_MTU, + DAITA, // Currently not supported // LOCKDOWN_MODE, // SHADOWSOCKS, // MULTIHOP, // BRIDGE_MODE, // CUSTOM_MSS_FIX, - // DAITA, // UNRECOGNIZED, } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt index 17bc563a8ddd..8bfa6c1f64f4 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt @@ -60,6 +60,7 @@ sealed interface RelayItem { override val id: GeoLocationId.Hostname, val provider: Provider, override val active: Boolean, + val daita: Boolean ) : Location { override val name: String = id.code override val hasChildren: Boolean = false diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDaitaSettingsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDaitaSettingsError.kt new file mode 100644 index 000000000000..f636267c0914 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDaitaSettingsError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetDaitaSettingsError { + data class Unknown(val throwable: Throwable) : SetDaitaSettingsError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt index d715f1676610..8b61ab1fd77d 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt @@ -3,5 +3,6 @@ package net.mullvad.mullvadvpn.lib.model data class TunnelEndpoint( val endpoint: Endpoint, val quantumResistant: Boolean, - val obfuscation: ObfuscationEndpoint? + val obfuscation: ObfuscationEndpoint?, + val daita: Boolean ) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt index 8ed43bd294b2..1751e9f80360 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt @@ -38,4 +38,12 @@ sealed class TunnelState { is Error -> this.errorState.isBlocking } } + + fun isUsingDaita(): Boolean { + return when (this) { + is Connected -> endpoint.daita + is Connecting -> endpoint?.daita ?: false + else -> false + } + } } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardRelayEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardRelayEndpointData.kt new file mode 100644 index 000000000000..9e328b92e636 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardRelayEndpointData.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class WireguardRelayEndpointData(val daita: Boolean) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt index 573f08213ef0..0bdb19e70181 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt @@ -1,3 +1,7 @@ package net.mullvad.mullvadvpn.lib.model -data class WireguardTunnelOptions(val mtu: Mtu?, val quantumResistant: QuantumResistantState) +data class WireguardTunnelOptions( + val mtu: Mtu?, + val quantumResistant: QuantumResistantState, + val daita: Boolean +) diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index c43acdb496d9..28e9ea75a953 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -385,4 +385,7 @@ Failed to set to current - Unknown reason %s was removed from \"%s\" \"%s\" was created + %s (%s) hides patterns in your encrypted VPN traffic. If anyone is monitoring your connection, this makes it significantly harder for them to identify what websites you are visiting. It does this by carefully adding network noise and making all network packets the same size. + Attention: Since this increases your total network traffic, be cautious if you have a limited data plan. It can also negatively impact your network speed. Please consider this if you want to enable %s. + %s using %s diff --git a/android/lib/resource/src/main/res/values/strings_non_translatable.xml b/android/lib/resource/src/main/res/values/strings_non_translatable.xml index 110e112e99f3..9cf571171a8b 100644 --- a/android/lib/resource/src/main/res/values/strings_non_translatable.xml +++ b/android/lib/resource/src/main/res/values/strings_non_translatable.xml @@ -14,4 +14,6 @@
  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 169.254.0.0/16
  • fe80::/10
  • fc00::/7
  • ]]>
    + DAITA + Defence against AI-guided Traffic Analysis diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/FileResourceExtractor.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/FileResourceExtractor.kt deleted file mode 100644 index 71a05e674385..000000000000 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/FileResourceExtractor.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.mullvad.mullvadvpn.service - -import android.content.Context -import java.io.File -import java.io.FileOutputStream - -class FileResourceExtractor(val context: Context) { - fun extract(asset: String, force: Boolean = false) { - val destination = File(context.filesDir, asset) - - if (!destination.exists() || force) { - extractFile(asset, destination) - } - } - - private fun extractFile(asset: String, destination: File) { - val destinationStream = FileOutputStream(destination) - - context.assets.open(asset).copyTo(destinationStream) - - destinationStream.close() - } -} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt index 992326510711..abee0d2c7532 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadVpnService.kt @@ -31,13 +31,14 @@ import net.mullvad.mullvadvpn.service.migration.MigrateSplitTunneling import net.mullvad.mullvadvpn.service.notifications.ForegroundNotificationManager import net.mullvad.mullvadvpn.service.notifications.NotificationChannelFactory import net.mullvad.mullvadvpn.service.notifications.NotificationManager +import net.mullvad.mullvadvpn.service.util.AssetToFilesDirExtractor import net.mullvad.talpid.TalpidVpnService import org.koin.android.ext.android.getKoin import org.koin.core.context.loadKoinModules import org.koin.core.qualifier.named -private const val RELAYS_FILE = "relays.json" -private const val MAYBENOT_MACHINES_FILE = "maybenot_machines" +private const val RELAY_LIST_ASSET_NAME = "relays.json" +private const val MAYBENOT_MACHINES_ASSET_NAME = "maybenot_machines" class MullvadVpnService : TalpidVpnService() { @@ -230,27 +231,12 @@ class MullvadVpnService : TalpidVpnService() { } private fun Context.prepareFiles() { - prepareRelayList() - prepareMaybenotMachines() - } - - private fun Context.prepareRelayList() { - val shouldOverwriteRelayList = - lastUpdatedTime() > File(filesDir, RELAYS_FILE).lastModified() - FileResourceExtractor(this).apply { extract(RELAYS_FILE, shouldOverwriteRelayList) } - } - - private fun Context.prepareMaybenotMachines() { - val shouldOverwriteMaybenotMachines = - lastUpdatedTime() > File(filesDir, RELAYS_FILE).lastModified() - FileResourceExtractor(this).apply { - extract(MAYBENOT_MACHINES_FILE, shouldOverwriteMaybenotMachines) + AssetToFilesDirExtractor(this).apply { + extract(RELAY_LIST_ASSET_NAME, overwriteFileIfAssetMoreRecent = true) + extract(MAYBENOT_MACHINES_ASSET_NAME, overwriteFileIfAssetMoreRecent = true) } } - private fun Context.lastUpdatedTime(): Long = - packageManager.getPackageInfo(packageName, 0).lastUpdateTime - companion object { init { System.loadLibrary("mullvad_jni") diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/AssetToFilesDirExtractor.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/AssetToFilesDirExtractor.kt new file mode 100644 index 000000000000..038371522de5 --- /dev/null +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/util/AssetToFilesDirExtractor.kt @@ -0,0 +1,31 @@ +package net.mullvad.mullvadvpn.service.util + +import android.content.Context +import java.io.File +import java.io.FileOutputStream + +class AssetToFilesDirExtractor(val context: Context) { + fun extract(assetName: String, overwriteFileIfAssetMoreRecent: Boolean = false) { + val forceOverwrite = + if (overwriteFileIfAssetMoreRecent) { + context.lastUpdatedTime() > File(context.filesDir, assetName).lastModified() + } else false + + val destination = File(context.filesDir, assetName) + + if (!destination.exists() || forceOverwrite) { + extractFile(assetName, destination) + } + } + + private fun Context.lastUpdatedTime(): Long = + packageManager.getPackageInfo(packageName, 0).lastUpdateTime + + private fun extractFile(asset: String, destination: File) { + val destinationStream = FileOutputStream(destination) + + context.assets.open(asset).copyTo(destinationStream) + + destinationStream.close() + } +}