diff options
Diffstat (limited to 'trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt')
-rw-r--r-- | trackersservicestandalone/src/main/java/foundation/e/advancedprivacy/trackers/service/TunLooper.kt | 167 |
1 files changed, 167 insertions, 0 deletions
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") + } + } +} |