diff options
Diffstat (limited to 'trackersservicestandalone/src/main/java/foundation/e')
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) +    } +} | 
