package org.itxtech.daedalus.provider; import android.annotation.TargetApi; import android.os.Build; import android.os.ParcelFileDescriptor; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.system.StructPollfd; import android.util.Log; import org.itxtech.daedalus.service.DaedalusVpnService; import org.itxtech.daedalus.util.DnsServerHelper; import org.pcap4j.packet.IpPacket; import java.io.*; import java.net.DatagramPacket; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.nio.channels.SocketChannel; import java.util.Iterator; import java.util.LinkedList; /** * Daedalus Project * * @author iTX Technologies * @link https://itxtech.org * <p> * 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. */ public class TcpDnsProvider extends UdpDnsProvider { private static final String TAG = "TcpDnsProvider"; private final TcpDnsProvider.WospList dnsIn = new TcpDnsProvider.WospList(); public TcpDnsProvider(ParcelFileDescriptor descriptor, DaedalusVpnService service) { super(descriptor, service); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void process() { try { Log.d(TAG, "Starting advanced DNS proxy."); FileDescriptor[] pipes = Os.pipe(); mInterruptFd = pipes[0]; mBlockFd = pipes[1]; FileInputStream inputStream = new FileInputStream(descriptor.getFileDescriptor()); FileOutputStream outputStream = new FileOutputStream(descriptor.getFileDescriptor()); byte[] packet = new byte[32767]; while (running) { StructPollfd deviceFd = new StructPollfd(); deviceFd.fd = inputStream.getFD(); deviceFd.events = (short) OsConstants.POLLIN; StructPollfd blockFd = new StructPollfd(); blockFd.fd = mBlockFd; blockFd.events = (short) (OsConstants.POLLHUP | OsConstants.POLLERR); if (!deviceWrites.isEmpty()) deviceFd.events |= (short) OsConstants.POLLOUT; StructPollfd[] polls = new StructPollfd[2 + dnsIn.size()]; polls[0] = deviceFd; polls[1] = blockFd; { int i = -1; for (TcpDnsProvider.WaitingOnSocketPacket wosp : dnsIn) { i++; StructPollfd pollFd = polls[2 + i] = new StructPollfd(); pollFd.fd = ParcelFileDescriptor.fromSocket(wosp.socket).getFileDescriptor(); pollFd.events = (short) OsConstants.POLLIN; } } Log.d(TAG, "doOne: Polling " + polls.length + " file descriptors"); Os.poll(polls, -1); if (blockFd.revents != 0) { Log.i(TAG, "Told to stop VPN"); running = false; return; } // Need to do this before reading from the device, otherwise a new insertion there could // invalidate one of the sockets we want to read from either due to size or time out // constraints { int i = -1; Iterator<TcpDnsProvider.WaitingOnSocketPacket> iter = dnsIn.iterator(); while (iter.hasNext()) { i++; TcpDnsProvider.WaitingOnSocketPacket wosp = iter.next(); if ((polls[i + 2].revents & OsConstants.POLLIN) != 0) { Log.d(TAG, "Read from TCP DNS socket" + wosp.socket); iter.remove(); handleRawDnsResponse(wosp.packet, wosp.socket); wosp.socket.close(); } } } if ((deviceFd.revents & OsConstants.POLLOUT) != 0) { Log.d(TAG, "Write to device"); writeToDevice(outputStream); } if ((deviceFd.revents & OsConstants.POLLIN) != 0) { Log.d(TAG, "Read from device"); readPacketFromDevice(inputStream, packet); } checkCache(); service.providerLoopCallback(); } } catch (Exception e) { e.printStackTrace(); } } private byte[] processUdpPacket(DatagramPacket outPacket, IpPacket parsedPacket) { if (parsedPacket == null) { return new byte[0]; } return outPacket.getData(); } void forwardPacket(DatagramPacket outPacket, IpPacket parsedPacket) throws DaedalusVpnService.VpnNetworkException { Socket dnsSocket; try { // Packets to be sent to the real DNS server will need to be protected from the VPN dnsSocket = SocketChannel.open().socket(); service.protect(dnsSocket); SocketAddress address = new InetSocketAddress(outPacket.getAddress(), DnsServerHelper.getPortOrDefault(outPacket.getAddress(), outPacket.getPort())); dnsSocket.connect(address, 5000); dnsSocket.setSoTimeout(5000); Log.d(TAG, "Sending"); DataOutputStream dos = new DataOutputStream(dnsSocket.getOutputStream()); byte[] packet = processUdpPacket(outPacket, parsedPacket); dos.writeShort(packet.length); dos.write(packet); dos.flush(); if (parsedPacket != null) { dnsIn.add(new TcpDnsProvider.WaitingOnSocketPacket(dnsSocket, parsedPacket)); } else { dnsSocket.close(); } } catch (IOException e) { if (e.getCause() instanceof ErrnoException) { ErrnoException errnoExc = (ErrnoException) e.getCause(); if ((errnoExc.errno == OsConstants.ENETUNREACH) || (errnoExc.errno == OsConstants.EPERM)) { throw new DaedalusVpnService.VpnNetworkException("Cannot send message:", e); } } Log.w(TAG, "handleDnsRequest: Could not send packet to upstream", e); } } private void handleRawDnsResponse(IpPacket parsedPacket, Socket dnsSocket) { try { DataInputStream stream = new DataInputStream(dnsSocket.getInputStream()); int length = stream.readUnsignedShort(); Log.d(TAG, "Reading length: " + String.valueOf(length)); byte[] data = new byte[length]; stream.read(data); dnsSocket.close(); handleDnsResponse(parsedPacket, data); } catch (Exception ignored) { } } /** * Helper class holding a socket, the packet we are waiting the answer for, and a time */ private static class WaitingOnSocketPacket { final Socket socket; final IpPacket packet; private final long time; WaitingOnSocketPacket(Socket socket, IpPacket packet) { this.socket = socket; this.packet = packet; this.time = System.currentTimeMillis(); } long ageSeconds() { return (System.currentTimeMillis() - time) / 1000; } } /** * Queue of WaitingOnSocketPacket, bound on time and space. */ private static class WospList implements Iterable<TcpDnsProvider.WaitingOnSocketPacket> { private final LinkedList<TcpDnsProvider.WaitingOnSocketPacket> list = new LinkedList<>(); void add(TcpDnsProvider.WaitingOnSocketPacket wosp) { try { if (list.size() > 1024) { Log.d(TAG, "Dropping socket due to space constraints: " + list.element().socket); list.element().socket.close(); list.remove(); } while (!list.isEmpty() && list.element().ageSeconds() > 10) { Log.d(TAG, "Timeout on socket " + list.element().socket); list.element().socket.close(); list.remove(); } list.add(wosp); } catch (Exception ignored) { } } public Iterator<TcpDnsProvider.WaitingOnSocketPacket> iterator() { return list.iterator(); } int size() { return list.size(); } } }