From 9d55978063947d5865bb3fa4e0c2ebef78f78812 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Mon, 6 Nov 2023 08:14:27 +0000 Subject: epic18: Manage VPN services for Tor or Tracker control --- .../advancedprivacy/trackers/service/DNSBlocker.kt | 104 ++++++++++++++++++++ .../trackers/service/TrackersService.kt | 58 +++++++++++ .../trackers/service/TrackersSupervisorEos.kt | 70 +++++++++++++ .../trackers/service/VpnSupervisorUseCaseEos.kt | 108 +++++++++++++++++++++ 4 files changed, 340 insertions(+) create mode 100644 trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt create mode 100644 trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt create mode 100644 trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersSupervisorEos.kt create mode 100644 trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/VpnSupervisorUseCaseEos.kt (limited to 'trackersserviceeos/src/main/java/foundation/e/advancedprivacy') diff --git a/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt new file mode 100644 index 0000000..6a2b218 --- /dev/null +++ b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/DNSBlocker.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2022 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.advancedprivacy.trackers.service + +import android.net.LocalServerSocket +import android.system.ErrnoException +import android.system.Os +import android.system.OsConstants +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import timber.log.Timber +import java.io.BufferedReader +import java.io.InputStreamReader +import java.io.PrintWriter + +class DNSBlocker( + val filterHostnameUseCase: FilterHostnameUseCase +) { + private var resolverReceiver: LocalServerSocket? = null + + companion object { + private const val SOCKET_NAME = "foundation.e.advancedprivacy" + } + + private fun closeSocket() { + // Known bug and workaround that LocalServerSocket::close is not working well + // https://issuetracker.google.com/issues/36945762 + if (resolverReceiver != null) { + try { + Os.shutdown(resolverReceiver!!.fileDescriptor, OsConstants.SHUT_RDWR) + resolverReceiver!!.close() + resolverReceiver = null + } catch (e: ErrnoException) { + if (e.errno != OsConstants.EBADF) { + Timber.w("Socket already closed") + } else { + Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") + } + } catch (e: Exception) { + Timber.e(e, "Exception: cannot close DNS port on stop $SOCKET_NAME !") + } + } + } + + fun listenJob(scope: CoroutineScope): Job = scope.launch(Dispatchers.IO) { + val resolverReceiver = runSuspendCatching { + LocalServerSocket(SOCKET_NAME) + }.getOrElse { + Timber.e(it, "Exception: cannot open DNS port on $SOCKET_NAME") + return@launch + } + + this@DNSBlocker.resolverReceiver = resolverReceiver + Timber.d("DNSFilterProxy running on port $SOCKET_NAME") + + while (isActive) { + runSuspendCatching { + val socket = resolverReceiver.accept() + val reader = BufferedReader(InputStreamReader(socket.inputStream)) + val line = reader.readLine() + val params = line.split(",").toTypedArray() + val output = socket.outputStream + val writer = PrintWriter(output, true) + val domainName = params[0] + val appUid = params[1].toInt() + if (filterHostnameUseCase.shouldBlock(domainName, appUid)) { + writer.println("block") + } else { + writer.println("pass") + } + socket.close() + }.onFailure { + if (it is CancellationException) { + closeSocket() + throw it + } else { + Timber.w(it, "Exception while listening DNS resolver") + } + } + } + } +} diff --git a/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt new file mode 100644 index 0000000..5f573b0 --- /dev/null +++ b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 MURENA SAS + * Copyright (C) 2021 E FOUNDATION + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package foundation.e.advancedprivacy.trackers.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import org.koin.java.KoinJavaComponent.get + +class TrackersService : Service() { + companion object { + const val ACTION_START = "foundation.e.privacymodules.trackers.intent.action.START" + + var coroutineScope = CoroutineScope(Dispatchers.IO) + } + + override fun onBind(intent: Intent): IBinder? { + throw UnsupportedOperationException("Not yet implemented") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (ACTION_START == intent?.action) { + stop() + start() + } + return START_REDELIVER_INTENT + } + + private fun start() { + coroutineScope = CoroutineScope(Dispatchers.IO) + get(DNSBlocker::class.java).apply { + filterHostnameUseCase.writeLogJob(coroutineScope) + listenJob(coroutineScope) + } + } + + private fun stop() { + kotlin.runCatching { coroutineScope.cancel() } + } +} diff --git a/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersSupervisorEos.kt b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersSupervisorEos.kt new file mode 100644 index 0000000..71a4fc4 --- /dev/null +++ b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersSupervisorEos.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package foundation.e.advancedprivacy.trackers.service + +import android.content.Context +import android.content.Intent +import foundation.e.advancedprivacy.domain.entities.FeatureState +import foundation.e.advancedprivacy.domain.usecases.VpnSupervisorUseCase +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersSupervisor +import foundation.e.advancedprivacy.trackers.service.TrackersService.Companion.ACTION_START +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.isActive +import org.koin.core.module.dsl.bind +import org.koin.core.module.dsl.factoryOf +import org.koin.core.module.dsl.singleOf +import org.koin.core.qualifier.named +import org.koin.dsl.module + +class TrackersSupervisorEos(private val context: Context) : TrackersSupervisor { + + override val state: StateFlow = MutableStateFlow(FeatureState.ON) + + override fun start(): Boolean { + val intent = Intent(context, TrackersService::class.java) + intent.action = ACTION_START + return context.startService(intent) != null + } + + override fun stop(): Boolean { + return context.stopService(Intent(context, TrackersService::class.java)) + } + + override fun isRunning(): Boolean { + return TrackersService.coroutineScope.isActive + } + + override val dnsFilterForIpScrambling = null +} + +val trackerServiceModule = module { + factoryOf(::DNSBlocker) + singleOf(::TrackersSupervisorEos) { + bind() + } + single { + VpnSupervisorUseCaseEos( + localStateRepository = get(), + orbotSupervisor = get(), + trackersSupervisor = get(), + appDesc = get(named("AdvancedPrivacy")), + permissionsPrivacyModule = get(), + scope = get(), + ) + } +} diff --git a/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/VpnSupervisorUseCaseEos.kt b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/VpnSupervisorUseCaseEos.kt new file mode 100644 index 0000000..976a2b7 --- /dev/null +++ b/trackersserviceeos/src/main/java/foundation/e/advancedprivacy/trackers/service/VpnSupervisorUseCaseEos.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 MURENA SAS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package foundation.e.advancedprivacy.trackers.service + +import foundation.e.advancedprivacy.domain.entities.ApplicationDescription +import foundation.e.advancedprivacy.domain.entities.FeatureState +import foundation.e.advancedprivacy.domain.entities.MainFeatures +import foundation.e.advancedprivacy.domain.entities.MainFeatures.IpScrambling +import foundation.e.advancedprivacy.domain.entities.MainFeatures.TrackersControl +import foundation.e.advancedprivacy.domain.repositories.LocalStateRepository +import foundation.e.advancedprivacy.domain.usecases.VpnSupervisorUseCase +import foundation.e.advancedprivacy.externalinterfaces.permissions.IPermissionsPrivacyModule +import foundation.e.advancedprivacy.ipscrambler.OrbotSupervisor +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersSupervisor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.launch + +class VpnSupervisorUseCaseEos( + private val localStateRepository: LocalStateRepository, + private val orbotSupervisor: OrbotSupervisor, + private val trackersSupervisor: TrackersSupervisor, + private val appDesc: ApplicationDescription, + private val permissionsPrivacyModule: IPermissionsPrivacyModule, + private val scope: CoroutineScope, +) : VpnSupervisorUseCase { + + override fun listenSettings() { + trackersSupervisor.start() + + scope.launch(Dispatchers.IO) { + localStateRepository.ipScramblingSetting.collect { + applySettings(it) + } + } + + scope.launch(Dispatchers.IO) { + localStateRepository.blockTrackers.drop(1).dropWhile { !it }.collect { + localStateRepository.emitStartVpnDisclaimer(TrackersControl()) + } + } + } + + private suspend fun applySettings(isIpScramblingEnabled: Boolean) { + val currentMode = localStateRepository.internetPrivacyMode.value + when { + ( + isIpScramblingEnabled && + currentMode in setOf(FeatureState.OFF, FeatureState.STOPPING) + ) -> { + applyStartIpScrambling() + } + ( + !isIpScramblingEnabled && + currentMode in setOf(FeatureState.ON, FeatureState.STARTING) + ) -> { + orbotSupervisor.stop() + } + else -> {} + } + } + + private suspend fun applyStartIpScrambling() { + if (orbotSupervisor.prepareAndroidVpn() != null) { + permissionsPrivacyModule.setVpnPackageAuthorization(appDesc.packageName) + val alwaysOnVpnPackage = permissionsPrivacyModule.getAlwaysOnVpnPackage() + if (alwaysOnVpnPackage != null) { + localStateRepository.emitOtherVpnRunning( + permissionsPrivacyModule.getApplicationDescription( + packageName = alwaysOnVpnPackage, + withIcon = false + ) + ) + localStateRepository.setIpScramblingSetting(enabled = false) + return + } + } + + localStateRepository.emitStartVpnDisclaimer(IpScrambling()) + startVpnService(IpScrambling()) + } + + override fun startVpnService(feature: MainFeatures) { + localStateRepository.internetPrivacyMode.value = FeatureState.STARTING + orbotSupervisor.setDNSFilter(null) + orbotSupervisor.start() + } + + override fun cancelStartVpnService(feature: MainFeatures) { + localStateRepository.setIpScramblingSetting(enabled = false) + } +} -- cgit v1.2.1