diff options
author | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-10-11 16:36:02 +0000 |
---|---|---|
committer | Guillaume Jacquart <guillaume.jacquart@hoodbrains.com> | 2023-10-11 16:36:02 +0000 |
commit | 95e68cbbe748f81af1113753c5b99929e3db9ea2 (patch) | |
tree | 776b2e1fb3d5c16a48beb1c947eef887b2475db8 /trackersservicestandalone/src/main/java | |
parent | 41c0e48498979ec8310961e40c89aa6b3c0d8e0f (diff) |
epic18: Trackers control on standalone app (without Ipscrambling).
Diffstat (limited to 'trackersservicestandalone/src/main/java')
7 files changed, 563 insertions, 0 deletions
diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt new file mode 100644 index 0000000..d079e22 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt @@ -0,0 +1,44 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service + +internal object Config { + const val SESSION_NAME = "TrackersService" + + const val FALLBACK_DNS = "1.1.1.1" + const val VERBOSE = true + + const val VIRTUALDNS_IPV4 = "10.10.10.10" + const val VIRTUALDNS_IPV6 = "fdc8:1095:91e1:aaaa:aaaa:aaaa:aaaa:aaa1" + const val ADDRESS_IPV4 = "10.0.2.15" + const val ADDRESS_IPV6 = "fdc8:1095:91e1:aaaa:aaaa:aaaa:aaaa:aaa2" + + const val BLOCKED_IPV4 = "127.0.0.1" + const val BLOCKED_IPV6 = "::1" + + const val MTU = 3000 + const val LOCAL_RESOLVER_TTL = 60 + + const val MAX_RESOLVER_COUNT = 100 + + val DNS_SERVER_TO_CATCH_IPV4 = listOf( + "8.8.8.8", "8.8.4.4", "1.1.1.1" + ) + val DNS_SERVER_TO_CATCH_IPV6 = listOf( + "2001:4860:4860::8888", "2001:4860:4860::8844" + ) +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt new file mode 100644 index 0000000..918977f --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt @@ -0,0 +1,127 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service + +import android.content.Context +import android.content.Intent +import android.net.VpnService +import android.os.Build +import android.os.ParcelFileDescriptor +import foundation.e.advancedprivacy.core.utils.notificationBuilder +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState +import foundation.e.advancedprivacy.domain.entities.NOTIFICATION_TRACKER_FLAG +import foundation.e.advancedprivacy.domain.entities.NotificationContent +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor +import foundation.e.advancedprivacy.trackers.service.Config.DNS_SERVER_TO_CATCH_IPV4 +import foundation.e.advancedprivacy.trackers.service.Config.DNS_SERVER_TO_CATCH_IPV6 +import foundation.e.advancedprivacy.trackers.service.Config.SESSION_NAME +import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.koin.core.qualifier.named +import org.koin.java.KoinJavaComponent.get +import timber.log.Timber + +class TrackersService : VpnService() { + companion object { + var coroutineScope = CoroutineScope(Dispatchers.IO) + + fun start(context: Context) { + prepare(context) + val intent = Intent(context, TrackersService::class.java) + context.startService(intent) + } + } + + private val networkDNSAddressRepository: NetworkDNSAddressRepository = get(NetworkDNSAddressRepository::class.java) + private val trackersServiceSupervisor: TrackersServiceSupervisorImpl = get( + TrackersServiceSupervisor::class.java + ) as TrackersServiceSupervisorImpl + + private val notificationTrackerFlag: NotificationContent = get(NotificationContent::class.java, named("notificationTrackerFlag")) + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startVPN() + + startForeground( + NOTIFICATION_TRACKER_FLAG, + notificationBuilder( + context = this, + content = notificationTrackerFlag + ).build() + ) + trackersServiceSupervisor.state.value = FeatureServiceState.ON + + return START_STICKY + } + + override fun onDestroy() { + networkDNSAddressRepository.stop() + trackersServiceSupervisor.state.value = FeatureServiceState.OFF + super.onDestroy() + } + + private fun startVPN() { + val vpnInterface = initVPN() + + if (vpnInterface != null) { + networkDNSAddressRepository.start() + + coroutineScope = CoroutineScope(Dispatchers.IO) + get<TunLooper>(TunLooper::class.java).apply { + listenJob(vpnInterface, coroutineScope) + } + } else { + Timber.e("Cannot get VPN interface") + } + } + + private fun initVPN(): ParcelFileDescriptor? { + val builder = Builder() + builder.setSession(SESSION_NAME) + // IPV4: + builder + .addAddress(Config.ADDRESS_IPV4, 24) + .addDnsServer(Config.VIRTUALDNS_IPV4) + .addRoute(Config.VIRTUALDNS_IPV4, 32) + + // IPV6 + builder + .addAddress(Config.ADDRESS_IPV6, 48) + .addDnsServer(Config.VIRTUALDNS_IPV6) + .addRoute(Config.VIRTUALDNS_IPV6, 128) + + DNS_SERVER_TO_CATCH_IPV4.forEach { + builder.addRoute(it, 32) + } + DNS_SERVER_TO_CATCH_IPV6.forEach { + builder.addRoute(it, 128) + } + + // TODO: block private DNS. + // TODO 20230821: seen in privateDNSFilter, bypass filter for google apps on Android 7/8 + + builder.addDisallowedApplication(packageName) + builder.setBlocking(true) + builder.setMtu(Config.MTU) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + builder.setMetered(false) // take over defaults from underlying network + } + + return builder.establish() + } +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt new file mode 100644 index 0000000..25d3e2d --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt @@ -0,0 +1,64 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service + +import android.content.Context +import android.content.Intent +import foundation.e.advancedprivacy.domain.entities.FeatureServiceState +import foundation.e.advancedprivacy.trackers.domain.externalinterfaces.TrackersServiceSupervisor +import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository +import foundation.e.advancedprivacy.trackers.service.data.RequestDNSRepository +import foundation.e.advancedprivacy.trackers.service.usecases.ResolveDNSUseCase +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import org.koin.core.module.dsl.singleOf +import org.koin.dsl.module + +class TrackersServiceSupervisorImpl(private val context: Context) : TrackersServiceSupervisor { + internal val state: MutableStateFlow<FeatureServiceState> = MutableStateFlow(FeatureServiceState.OFF) + + override fun start(): Boolean { + return if (!isRunning()) { + state.value = FeatureServiceState.STARTING + TrackersService.start(context) + true + } else false + } + + override fun stop(): Boolean { + return when (state.value) { + FeatureServiceState.ON -> { + state.value = FeatureServiceState.STOPPING + kotlin.runCatching { TrackersService.coroutineScope.cancel() } + context.stopService(Intent(context, TrackersService::class.java)) + true + } + else -> false + } + } + + override fun isRunning(): Boolean { + return state.value != FeatureServiceState.OFF + } +} + +val trackerServiceModule = module { + singleOf(::NetworkDNSAddressRepository) + singleOf(::RequestDNSRepository) + singleOf(::ResolveDNSUseCase) + singleOf(::TunLooper) +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt new file mode 100644 index 0000000..7813c67 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt @@ -0,0 +1,167 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service + +import android.os.ParcelFileDescriptor +import foundation.e.advancedprivacy.trackers.service.usecases.ResolveDNSUseCase +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 org.pcap4j.packet.DnsPacket +import org.pcap4j.packet.IpPacket +import org.pcap4j.packet.IpSelector +import org.pcap4j.packet.IpV4Packet +import org.pcap4j.packet.IpV6Packet +import org.pcap4j.packet.UdpPacket +import org.pcap4j.packet.namednumber.IpNumber +import org.pcap4j.packet.namednumber.UdpPort +import timber.log.Timber +import java.io.DataOutputStream +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.net.Inet6Address +import java.util.Arrays + +class TunLooper( + private val resolveDNSUseCase: ResolveDNSUseCase, +) { + private var vpnInterface: ParcelFileDescriptor? = null + private var fileInputStream: FileInputStream? = null + private var dataOutputStream: DataOutputStream? = null + + private fun closeStreams() { + fileInputStream?.close() + fileInputStream = null + + dataOutputStream?.close() + dataOutputStream = null + + vpnInterface?.close() + vpnInterface = null + } + + fun listenJob( + vpnInterface: ParcelFileDescriptor, + scope: CoroutineScope + ): Job = scope.launch(Dispatchers.IO) { + this@TunLooper.vpnInterface = vpnInterface + val fis = FileInputStream(vpnInterface.fileDescriptor) + this@TunLooper.fileInputStream = fis + dataOutputStream = DataOutputStream(FileOutputStream(vpnInterface.fileDescriptor)) + + while (isActive) { + runCatching { + val buffer = ByteArray(Config.MTU) + val pLen = fis.read(buffer) + + if (pLen > 0) { + scope.launch { handleIpPacket(buffer, pLen) } + } + }.onFailure { + if (it is CancellationException) { + closeStreams() + throw it + } else { + Timber.w(it, "while reading from VPN fd") + } + } + } + } + + private suspend fun handleIpPacket(buffer: ByteArray, pLen: Int) { + val pdata = Arrays.copyOf(buffer, pLen) + try { + val packet = IpSelector.newPacket(pdata, 0, pdata.size) + if (packet is IpPacket) { + val ipPacket = packet + if (isPacketDNS(ipPacket)) { + handleDnsPacket(ipPacket) + } + } + } catch (e: Exception) { + Timber.w(e, "Can't parse packet, ignore it.") + } + } + + private fun isPacketDNS(p: IpPacket): Boolean { + if (p.header.protocol === IpNumber.UDP) { + val up = p.payload as UdpPacket + return up.header.dstPort === UdpPort.DOMAIN + } + return false + } + + private suspend fun handleDnsPacket(ipPacket: IpPacket) { + try { + val udpPacket = ipPacket.payload as UdpPacket + val dnsRequest = udpPacket.payload as DnsPacket + val dnsResponse = resolveDNSUseCase.processDNS(dnsRequest) + + if (dnsResponse != null) { + val dnsBuilder = dnsResponse.builder + + val udpBuilder = UdpPacket.Builder(udpPacket) + .srcPort(udpPacket.header.dstPort) + .dstPort(udpPacket.header.srcPort) + .srcAddr(ipPacket.getHeader().getDstAddr()) + .dstAddr(ipPacket.getHeader().getSrcAddr()) + .correctChecksumAtBuild(true) + .correctLengthAtBuild(true) + .payloadBuilder(dnsBuilder) + + val respPacket: IpPacket? = if (ipPacket is IpV4Packet) { + val ipV4Packet = ipPacket + val ipv4Builder = IpV4Packet.Builder() + ipv4Builder + .version(ipV4Packet.header.version) + .protocol(ipV4Packet.header.protocol) + .tos(ipV4Packet.header.tos) + .srcAddr(ipV4Packet.header.dstAddr) + .dstAddr(ipV4Packet.header.srcAddr) + .correctChecksumAtBuild(true) + .correctLengthAtBuild(true) + .dontFragmentFlag(ipV4Packet.header.dontFragmentFlag) + .reservedFlag(ipV4Packet.header.reservedFlag) + .moreFragmentFlag(ipV4Packet.header.moreFragmentFlag) + .ttl(Integer.valueOf(64).toByte()) + .payloadBuilder(udpBuilder) + ipv4Builder.build() + } else if (ipPacket is IpV6Packet) { + IpV6Packet.Builder(ipPacket as IpV6Packet?) + .srcAddr(ipPacket.getHeader().getDstAddr() as Inet6Address) + .dstAddr(ipPacket.getHeader().getSrcAddr() as Inet6Address) + .payloadBuilder(udpBuilder) + .build() + } else null + + respPacket?.let { + try { + dataOutputStream?.write(it.rawData) + } catch (e: IOException) { + Timber.e(e, "error writing to VPN fd") + } + } + } + } catch (ioe: java.lang.Exception) { + Timber.e(ioe, "could not parse DNS packet") + } + } +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt new file mode 100644 index 0000000..7c36ed2 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt @@ -0,0 +1,59 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service.data + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import foundation.e.advancedprivacy.trackers.service.Config +import java.net.InetAddress + +class NetworkDNSAddressRepository(private val context: Context) { + private val connectivityManager: ConnectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + + fun start() { + connectivityManager.registerNetworkCallback( + NetworkRequest.Builder().build(), + networkCallback + ) + } + + fun stop() { + kotlin.runCatching { + connectivityManager.unregisterNetworkCallback(networkCallback) + } + } + + var dnsAddress: InetAddress = InetAddress.getByName(Config.FALLBACK_DNS) + private set + + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + super.onAvailable(network) + connectivityManager.getLinkProperties(network) + ?.dnsServers?.firstOrNull { + it.hostAddress.let { + it != Config.VIRTUALDNS_IPV4 && it != Config.VIRTUALDNS_IPV6 + } + }?.let { + dnsAddress = InetAddress.getByName(it.hostAddress) + } + } + } +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt new file mode 100644 index 0000000..d9370be --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt @@ -0,0 +1,48 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service.data + +import foundation.e.advancedprivacy.core.utils.runSuspendCatching +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.pcap4j.packet.DnsPacket +import timber.log.Timber +import java.net.DatagramPacket +import java.net.DatagramSocket + +class RequestDNSRepository { + + suspend fun processDNS(request: DatagramPacket): DnsPacket? = withContext(Dispatchers.IO) { + runSuspendCatching { + var response: DnsPacket? = null + val datagramSocket = DatagramSocket() + datagramSocket.send(request) + + // Await response from DNS server + val buf = ByteArray(1024) + val packet = DatagramPacket(buf, buf.size) + datagramSocket.receive(packet) + val dnsResp = packet.data + if (dnsResp != null) { + response = DnsPacket.newPacket(dnsResp, 0, dnsResp.size) + } + response + }.onFailure { + Timber.w(it, "Can't make DNS request.") + }.getOrNull() + } +} diff --git a/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt new file mode 100644 index 0000000..ac8aee0 --- /dev/null +++ b/trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt @@ -0,0 +1,54 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ +package foundation.e.advancedprivacy.trackers.service.usecases + +import foundation.e.advancedprivacy.trackers.domain.usecases.FilterHostnameUseCase +import foundation.e.advancedprivacy.trackers.service.data.NetworkDNSAddressRepository +import foundation.e.advancedprivacy.trackers.service.data.RequestDNSRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import org.pcap4j.packet.DnsPacket +import org.pcap4j.packet.namednumber.DnsRCode +import java.net.DatagramPacket + +@OptIn(DelicateCoroutinesApi::class) +class ResolveDNSUseCase( + private val networkDNSAddressRepository: NetworkDNSAddressRepository, + private val filterHostnameUseCase: FilterHostnameUseCase, + private val requestDNSRepository: RequestDNSRepository, + private val scope: CoroutineScope = GlobalScope +) { + private val DNS_PORT = 53 + + init { + filterHostnameUseCase.writeLogJob(scope) + } + + suspend fun processDNS(dnsRequest: DnsPacket): DnsPacket? { + val host = dnsRequest.header.questions[0].qName.name + if (filterHostnameUseCase.shouldBlock(host)) { + return dnsRequest.builder + .rCode(DnsRCode.NX_DOMAIN) + .response(true).build() + } + + val payload = dnsRequest.rawData + val packet = DatagramPacket(payload, payload.size, networkDNSAddressRepository.dnsAddress, DNS_PORT) + return requestDNSRepository.processDNS(packet) + } +} |