diff options
13 files changed, 232 insertions, 3 deletions
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 c3eb19b270..13b5e7a2db 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 @@ -105,8 +105,9 @@ val uiModule = module { androidContext().getSharedPreferences(APP_PREFERENCES_NAME, Context.MODE_PRIVATE) ) } - single { SettingsRepository(get()) } + single { SettingsRepository(get(), get()) } single { MullvadProblemReport(get()) } + single { RelayOverridesRepository(get(), get()) } single { CustomListsRepository(get(), get(), get()) } single { AccountExpiryNotificationUseCase(get()) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt new file mode 100644 index 0000000000..835cab4710 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/RelayOverridesRepository.kt @@ -0,0 +1,44 @@ +package net.mullvad.mullvadvpn.repository + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import net.mullvad.mullvadvpn.lib.ipc.MessageHandler +import net.mullvad.mullvadvpn.lib.ipc.Request +import net.mullvad.mullvadvpn.model.RelayOverride +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager +import net.mullvad.mullvadvpn.ui.serviceconnection.settingsListener +import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier +import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault + +class RelayOverridesRepository( + private val serviceConnectionManager: ServiceConnectionManager, + private val messageHandler: MessageHandler, + dispatcher: CoroutineDispatcher = Dispatchers.IO, +) { + fun clearAllOverrides() { + messageHandler.trySendRequest(Request.ClearAllRelayOverrides) + } + + val relayOverrides: StateFlow<List<RelayOverride>?> = + serviceConnectionManager.connectionState + .flatMapReadyConnectionOrDefault(flowOf()) { state -> + callbackFlowFromNotifier(state.container.settingsListener.settingsNotifier) + } + .mapNotNull { it?.relayOverrides?.toList() } + .onStart { + serviceConnectionManager + .settingsListener() + ?.settingsNotifier + ?.latestEvent + ?.relayOverrides + ?.toList() + } + .stateIn(CoroutineScope(dispatcher), SharingStarted.Eagerly, 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 81c4b85b88..7d61feaf0c 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 @@ -4,11 +4,18 @@ import java.net.InetAddress import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import net.mullvad.mullvadvpn.lib.ipc.Event.ApplyJsonSettingsResult +import net.mullvad.mullvadvpn.lib.ipc.MessageHandler +import net.mullvad.mullvadvpn.lib.ipc.Request +import net.mullvad.mullvadvpn.lib.ipc.events import net.mullvad.mullvadvpn.model.CustomDnsOptions import net.mullvad.mullvadvpn.model.DefaultDnsOptions import net.mullvad.mullvadvpn.model.DnsOptions @@ -24,7 +31,8 @@ import net.mullvad.mullvadvpn.util.flatMapReadyConnectionOrDefault class SettingsRepository( private val serviceConnectionManager: ServiceConnectionManager, - dispatcher: CoroutineDispatcher = Dispatchers.IO + private val messageHandler: MessageHandler, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) { val settingsUpdates: StateFlow<Settings?> = serviceConnectionManager.connectionState @@ -92,4 +100,11 @@ class SettingsRepository( fun setLocalNetworkSharing(isEnabled: Boolean) { serviceConnectionManager.settingsListener()?.allowLan = isEnabled } + + suspend fun applySettingsPatch(json: String) = + withContext(dispatcher) { + val deferred = async { messageHandler.events<ApplyJsonSettingsResult>().first() } + messageHandler.trySendRequest(Request.ApplyJsonSettings(json)) + deferred.await() + } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt index d996c432ad..e2ccc2e470 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/serviceconnection/SettingsListener.kt @@ -68,4 +68,8 @@ class SettingsListener(private val connection: Messenger, eventDispatcher: Event settings = newSettings } + + fun applySettingsPatch(json: String) { + connection.send(Request.ApplyJsonSettings(json).message) + } } diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt index cce2ab1f87..36ea17036e 100644 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt +++ b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt @@ -14,6 +14,7 @@ import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult import net.mullvad.mullvadvpn.model.RelayList import net.mullvad.mullvadvpn.model.RemoveDeviceResult import net.mullvad.mullvadvpn.model.Settings +import net.mullvad.mullvadvpn.model.SettingsPatchError import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.model.UpdateCustomListResult @@ -71,6 +72,10 @@ sealed class Event : Message.EventMessage() { @Parcelize data class UpdateCustomListResultEvent(val result: UpdateCustomListResult) : Event() + @Parcelize data class ExportJsonSettingsResult(val json: String) : Event() + + @Parcelize data class ApplyJsonSettingsResult(val error: SettingsPatchError?) : Event() + companion object { private const val MESSAGE_KEY = "event" diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt index fe9d3b46d9..4bcf871acc 100644 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt +++ b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt @@ -13,6 +13,7 @@ import net.mullvad.mullvadvpn.model.Ownership import net.mullvad.mullvadvpn.model.PlayPurchase import net.mullvad.mullvadvpn.model.Providers import net.mullvad.mullvadvpn.model.QuantumResistantState +import net.mullvad.mullvadvpn.model.RelayOverride import net.mullvad.mullvadvpn.model.WireguardConstraints // Requests that the service can handle @@ -117,6 +118,14 @@ sealed class Request : Message.RequestMessage() { @Parcelize data class UpdateCustomList(val customList: CustomList) : Request() + @Parcelize data object ClearAllRelayOverrides : Request() + + @Parcelize data class ApplyJsonSettings(val json: String) : Request() + + @Parcelize data object ExportJsonSettings : Request() + + @Parcelize data class SetRelayOverride(val override: RelayOverride) : Request() + companion object { private const val MESSAGE_KEY = "request" diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt new file mode 100644 index 0000000000..f738218ee7 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt @@ -0,0 +1,12 @@ +package net.mullvad.mullvadvpn.model + +import android.os.Parcelable +import java.net.InetAddress +import kotlinx.parcelize.Parcelize + +@Parcelize +data class RelayOverride( + val hostname: String, + val ipv4AddressIn: InetAddress?, + val ipv6AddressIn: InetAddress? +) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt index 304edc404a..847b80cd70 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt @@ -11,5 +11,6 @@ data class Settings( val allowLan: Boolean, val autoConnect: Boolean, val tunnelOptions: TunnelOptions, - val showBetaReleases: Boolean + val relayOverrides: ArrayList<RelayOverride>, + val showBetaReleases: Boolean, ) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt new file mode 100644 index 0000000000..5e3cb29911 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt @@ -0,0 +1,24 @@ +package net.mullvad.mullvadvpn.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class SettingsPatchError : Parcelable { + // E.g hostname is number instead of String + data class InvalidOrMissingValue(val value: String) : SettingsPatchError() + + // E.g. Unexpected top-level key? + data class UnknownOrProhibitedKey(val value: String) : SettingsPatchError() + + // Bad JSON + data object ParsePatch : SettingsPatchError() + + data object RecursionLimit : SettingsPatchError() + + // Patch was deserialized but was not valid domain data? + data object DeserializePatched : SettingsPatchError() + + // Failed to apply patch + data object ApplyPatch : SettingsPatchError() +} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt index f99d36c679..1d87987cf3 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadDaemon.kt @@ -20,10 +20,12 @@ import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult import net.mullvad.mullvadvpn.model.QuantumResistantState import net.mullvad.mullvadvpn.model.RelayList +import net.mullvad.mullvadvpn.model.RelayOverride import net.mullvad.mullvadvpn.model.RelaySettings import net.mullvad.mullvadvpn.model.RemoveDeviceEvent import net.mullvad.mullvadvpn.model.RemoveDeviceResult import net.mullvad.mullvadvpn.model.Settings +import net.mullvad.mullvadvpn.model.SettingsPatchError import net.mullvad.mullvadvpn.model.TunnelState import net.mullvad.mullvadvpn.model.UpdateCustomListResult import net.mullvad.mullvadvpn.model.VoucherSubmissionResult @@ -202,6 +204,15 @@ class MullvadDaemon( fun updateCustomList(customList: CustomList): UpdateCustomListResult = updateCustomList(daemonInterfaceAddress, customList) + fun clearAllRelayOverrides() = clearAllRelayOverrides(daemonInterfaceAddress) + + fun applyJsonSettings(json: String) = applyJsonSettings(daemonInterfaceAddress, json) + + fun exportJsonSettings(): String = exportJsonSettings(daemonInterfaceAddress) + + fun setRelayOverride(relayOverride: RelayOverride) = + setRelayOverride(daemonInterfaceAddress, relayOverride) + fun onDestroy() { onSettingsChange.unsubscribeAll() onTunnelStateChange.unsubscribeAll() @@ -323,6 +334,20 @@ class MullvadDaemon( customList: CustomList ): UpdateCustomListResult + private external fun clearAllRelayOverrides(daemonInterfaceAddress: Long) + + private external fun applyJsonSettings( + daemonInterfaceAddress: Long, + json: String + ): SettingsPatchError + + private external fun exportJsonSettings(daemonInterfaceAddress: Long): String + + private external fun setRelayOverride( + daemonInterfaceAddress: Long, + relayOverride: RelayOverride + ) + @Suppress("unused") private fun notifyAppVersionInfoEvent(appVersionInfo: AppVersionInfo) { onAppVersionInfoChange?.invoke(appVersionInfo) diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt new file mode 100644 index 0000000000..65d7b6cff0 --- /dev/null +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/JsonSettings.kt @@ -0,0 +1,48 @@ +package net.mullvad.mullvadvpn.service.endpoint + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.lib.ipc.Event +import net.mullvad.mullvadvpn.lib.ipc.Request + +class JsonSettings( + private val endpoint: ServiceEndpoint, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + private val daemon + get() = endpoint.intermittentDaemon + + init { + scope.launch { + endpoint.dispatcher.parsedMessages + .filterIsInstance<Request.ApplyJsonSettings>() + .collect { applyJsonSettings(it.json) } + } + + scope.launch { + endpoint.dispatcher.parsedMessages + .filterIsInstance<Request.ExportJsonSettings>() + .collect { exportJsonSettings() } + } + } + + private suspend fun applyJsonSettings(json: String) { + val result = daemon.await().applyJsonSettings(json) + endpoint.sendEvent(Event.ApplyJsonSettingsResult(result)) + } + + private suspend fun exportJsonSettings() { + val json = daemon.await().exportJsonSettings() + endpoint.sendEvent(Event.ExportJsonSettingsResult(json)) + } + + fun onDestroy() { + scope.cancel() + } +} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt new file mode 100644 index 0000000000..cda7a5b94b --- /dev/null +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/RelayOverrides.kt @@ -0,0 +1,37 @@ +package net.mullvad.mullvadvpn.service.endpoint + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.lib.ipc.Request + +class RelayOverrides( + private val endpoint: ServiceEndpoint, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) + private val daemon + get() = endpoint.intermittentDaemon + + init { + scope.launch { + endpoint.dispatcher.parsedMessages + .filterIsInstance<Request.SetRelayOverride>() + .collect { daemon.await().setRelayOverride(it.override) } + } + + scope.launch { + endpoint.dispatcher.parsedMessages + .filterIsInstance<Request.ClearAllRelayOverrides>() + .collect { daemon.await().clearAllRelayOverrides() } + } + } + + fun onDestroy() { + scope.cancel() + } +} diff --git a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt index 5485c528b0..f8fc6aaf64 100644 --- a/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt +++ b/android/service/src/main/kotlin/net/mullvad/mullvadvpn/service/endpoint/ServiceEndpoint.kt @@ -46,6 +46,8 @@ class ServiceEndpoint( val appVersionInfoCache = AppVersionInfoCache(this) val authTokenCache = AuthTokenCache(this) val customDns = CustomDns(this) + val relayOverrides = RelayOverrides(this) + val jsonSettings = JsonSettings(this) val relayListListener = RelayListListener(this) val splitTunneling = SplitTunneling(SplitTunnelingPersistence(context), this) val voucherRedeemer = VoucherRedeemer(this, accountCache) @@ -83,6 +85,8 @@ class ServiceEndpoint( voucherRedeemer.onDestroy() playPurchaseHandler.onDestroy() customLists.onDestroy() + relayOverrides.onDestroy() + jsonSettings.onDestroy() } internal fun sendEvent(event: Event) { |
