/* * 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") } } } } }