summaryrefslogtreecommitdiff
path: root/trackersservicestandalone
diff options
context:
space:
mode:
authorGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-10-11 16:36:02 +0000
committerGuillaume Jacquart <guillaume.jacquart@hoodbrains.com>2023-10-11 16:36:02 +0000
commit95e68cbbe748f81af1113753c5b99929e3db9ea2 (patch)
tree776b2e1fb3d5c16a48beb1c947eef887b2475db8 /trackersservicestandalone
parent41c0e48498979ec8310961e40c89aa6b3c0d8e0f (diff)
epic18: Trackers control on standalone app (without Ipscrambling).
Diffstat (limited to 'trackersservicestandalone')
-rw-r--r--trackersservicestandalone/.gitignore1
-rw-r--r--trackersservicestandalone/build.gradle43
-rw-r--r--trackersservicestandalone/consumer-rules.pro0
-rw-r--r--trackersservicestandalone/proguard-rules.pro21
-rw-r--r--trackersservicestandalone/src/main/AndroidManifest.xml39
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/Config.kt44
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersService.kt127
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TrackersServiceSupervisorImpl.kt64
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt167
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/NetworkDNSAddressRepository.kt59
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/data/RequestDNSRepository.kt48
-rw-r--r--trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/usecases/ResolveDNSUseCase.kt54
12 files changed, 667 insertions, 0 deletions
diff --git a/trackersservicestandalone/.gitignore b/trackersservicestandalone/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/trackersservicestandalone/.gitignore
@@ -0,0 +1 @@
+/build \ No newline at end of file
diff --git a/trackersservicestandalone/build.gradle b/trackersservicestandalone/build.gradle
new file mode 100644
index 0000000..d5df422
--- /dev/null
+++ b/trackersservicestandalone/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdk 32
+
+ defaultConfig {
+ minSdk 26
+ targetSdk 32
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+ implementation project(":core")
+ implementation project(":trackers")
+
+ implementation(
+ libs.androidx.core.ktx,
+ libs.bundles.koin,
+ libs.bundles.pcap4j,
+ libs.kotlinx.coroutines,
+ libs.timber,
+ )
+}
diff --git a/trackersservicestandalone/consumer-rules.pro b/trackersservicestandalone/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/trackersservicestandalone/consumer-rules.pro
diff --git a/trackersservicestandalone/proguard-rules.pro b/trackersservicestandalone/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/trackersservicestandalone/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile \ No newline at end of file
diff --git a/trackersservicestandalone/src/main/AndroidManifest.xml b/trackersservicestandalone/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4bfa4eb
--- /dev/null
+++ b/trackersservicestandalone/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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/>.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="foundation.e.advancedprivacy.trackers.service">
+
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:usesCleartextTraffic="true"
+ >
+ <service android:name=".TrackersService"
+ android:enabled="true"
+ android:permission="android.permission.BIND_VPN_SERVICE"
+ android:exported="true"
+ >
+ <intent-filter>
+ <action android:name="android.net.VpnService"/>
+ </intent-filter>
+ </service>
+
+ </application>
+</manifest> \ No newline at end of file
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)
+ }
+}