From 7d26a29a0073f415f0a119fd0fd515ffaf838640 Mon Sep 17 00:00:00 2001
From: PeratX <1215714524@qq.com>
Date: Tue, 2 May 2017 21:18:55 +0800
Subject: [PATCH 1/2] Modularized dns provider
---
.../daedalus/provider/DnsProvider.java | 43 ++
.../daedalus/provider/UdpDnsProvider.java | 410 +++++++++++++++++
.../daedalus/service/DaedalusVpnService.java | 412 ++----------------
app/src/main/res/values-zh/strings.xml | 1 +
app/src/main/res/values/strings.xml | 1 +
app/src/main/res/xml/perf_settings.xml | 5 +
6 files changed, 486 insertions(+), 386 deletions(-)
create mode 100644 app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java
create mode 100644 app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java
diff --git a/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java b/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java
new file mode 100644
index 0000000..090c7dd
--- /dev/null
+++ b/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java
@@ -0,0 +1,43 @@
+package org.itxtech.daedalus.provider;
+
+import android.os.ParcelFileDescriptor;
+import org.itxtech.daedalus.service.DaedalusVpnService;
+
+/**
+ * Daedalus Project
+ *
+ * @author iTXTech
+ * @link https://itxtech.org
+ *
+ * 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, version 3.
+ */
+public abstract class DnsProvider {
+ ParcelFileDescriptor descriptor;
+ DaedalusVpnService service;
+ boolean running = false;
+ static long dnsQueryTimes = 0;
+
+ DnsProvider(ParcelFileDescriptor descriptor, DaedalusVpnService service) {
+ this.descriptor = descriptor;
+ this.service = service;
+ dnsQueryTimes = 0;
+ }
+
+ public final long getDnsQueryTimes() {
+ return dnsQueryTimes;
+ }
+
+ public abstract void process();
+
+ public final void start() {
+ running = true;
+ }
+
+ public final void shutdown() {
+ running = false;
+ }
+
+ public abstract void stop();
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java b/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java
new file mode 100644
index 0000000..8fd35e2
--- /dev/null
+++ b/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java
@@ -0,0 +1,410 @@
+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 de.measite.minidns.DNSMessage;
+import de.measite.minidns.Record;
+import de.measite.minidns.record.A;
+import org.itxtech.daedalus.service.DaedalusVpnService;
+import org.itxtech.daedalus.util.DnsServerHelper;
+import org.itxtech.daedalus.util.HostsResolver;
+import org.pcap4j.packet.*;
+import org.pcap4j.packet.factory.PacketFactoryPropertiesLoader;
+import org.pcap4j.util.PropertiesLoader;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.*;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Daedalus Project
+ *
+ * @author iTXTech
+ * @link https://itxtech.org
+ *
+ * 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, version 3.
+ */
+public class UdpDnsProvider extends DnsProvider {
+ private static final String TAG = "UdpDnsProvider";
+
+ private final WospList dnsIn = new WospList();
+ private FileDescriptor mBlockFd = null;
+ private FileDescriptor mInterruptFd = null;
+ private final Queue deviceWrites = new LinkedList<>();
+
+ /**
+ * Number of iterations since we last cleared the pcap4j cache
+ */
+ private int pcap4jFactoryClearCacheCounter = 0;
+
+ public UdpDnsProvider(ParcelFileDescriptor descriptor, DaedalusVpnService service) {
+ super(descriptor, service);
+ }
+
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+ public void stop() {
+ try {
+ if (mInterruptFd != null) {
+ Os.close(mInterruptFd);
+ }
+ if (mBlockFd != null) {
+ Os.close(mBlockFd);
+ }
+ if (this.descriptor != null) {
+ this.descriptor.close();
+ this.descriptor = null;
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ private void queueDeviceWrite(IpPacket ipOutPacket) {
+ dnsQueryTimes++;
+ Log.i(TAG, "QT " + dnsQueryTimes);
+ deviceWrites.add(ipOutPacket.getRawData());
+ }
+
+ @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 (WaitingOnSocketPacket wosp : dnsIn) {
+ i++;
+ StructPollfd pollFd = polls[2 + i] = new StructPollfd();
+ pollFd.fd = ParcelFileDescriptor.fromDatagramSocket(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 iter = dnsIn.iterator();
+ while (iter.hasNext()) {
+ i++;
+ WaitingOnSocketPacket wosp = iter.next();
+ if ((polls[i + 2].revents & OsConstants.POLLIN) != 0) {
+ Log.d(TAG, "Read from 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);
+ }
+
+ // pcap4j has some sort of properties cache in the packet factory. This cache leaks, so
+ // we need to clean it up.
+ if (++pcap4jFactoryClearCacheCounter % 1024 == 0) {
+ try {
+ PacketFactoryPropertiesLoader l = PacketFactoryPropertiesLoader.getInstance();
+ Field field = l.getClass().getDeclaredField("loader");
+ field.setAccessible(true);
+ PropertiesLoader loader = (PropertiesLoader) field.get(l);
+ Log.d(TAG, "Cleaning cache");
+ loader.clearCache();
+ } catch (NoSuchFieldException e) {
+ Log.e(TAG, "Cannot find declared loader field", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Cannot get declared loader field", e);
+ }
+ }
+ service.providerLoopCallback();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ private void writeToDevice(FileOutputStream outFd) throws DaedalusVpnService.VpnNetworkException {
+ try {
+ outFd.write(deviceWrites.poll());
+ } catch (IOException e) {
+ // TODO: Make this more specific, only for: "File descriptor closed"
+ throw new DaedalusVpnService.VpnNetworkException("Outgoing VPN output stream closed");
+ }
+ }
+
+ private void readPacketFromDevice(FileInputStream inputStream, byte[] packet) throws DaedalusVpnService.VpnNetworkException, SocketException {
+ // Read the outgoing packet from the input stream.
+ int length;
+
+ try {
+ length = inputStream.read(packet);
+ } catch (IOException e) {
+ throw new DaedalusVpnService.VpnNetworkException("Cannot read from device", e);
+ }
+
+
+ if (length == 0) {
+ // TODO: Possibly change to exception
+ Log.w(TAG, "Got empty packet!");
+ return;
+ }
+
+ final byte[] readPacket = Arrays.copyOfRange(packet, 0, length);
+
+ handleDnsRequest(readPacket);
+ }
+
+ private void forwardPacket(DatagramPacket outPacket, IpPacket parsedPacket) throws DaedalusVpnService.VpnNetworkException {
+ DatagramSocket dnsSocket;
+ try {
+ // Packets to be sent to the real DNS server will need to be protected from the VPN
+ dnsSocket = new DatagramSocket();
+
+ service.protect(dnsSocket);
+
+ dnsSocket.send(outPacket);
+
+ if (parsedPacket != null) {
+ dnsIn.add(new 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, DatagramSocket dnsSocket) throws IOException {
+ byte[] datagramData = new byte[1024];
+ DatagramPacket replyPacket = new DatagramPacket(datagramData, datagramData.length);
+ dnsSocket.receive(replyPacket);
+ handleDnsResponse(parsedPacket, datagramData);
+ }
+
+
+ /**
+ * Handles a responsePayload from an upstream DNS server
+ *
+ * @param requestPacket The original request packet
+ * @param responsePayload The payload of the response
+ */
+ private void handleDnsResponse(IpPacket requestPacket, byte[] responsePayload) {
+ UdpPacket udpOutPacket = (UdpPacket) requestPacket.getPayload();
+ UdpPacket.Builder payLoadBuilder = new UdpPacket.Builder(udpOutPacket)
+ .srcPort(udpOutPacket.getHeader().getDstPort())
+ .dstPort(udpOutPacket.getHeader().getSrcPort())
+ .srcAddr(requestPacket.getHeader().getDstAddr())
+ .dstAddr(requestPacket.getHeader().getSrcAddr())
+ .correctChecksumAtBuild(true)
+ .correctLengthAtBuild(true)
+ .payloadBuilder(
+ new UnknownPacket.Builder()
+ .rawData(responsePayload)
+ );
+
+
+ IpPacket ipOutPacket;
+ if (requestPacket instanceof IpV4Packet) {
+ ipOutPacket = new IpV4Packet.Builder((IpV4Packet) requestPacket)
+ .srcAddr((Inet4Address) requestPacket.getHeader().getDstAddr())
+ .dstAddr((Inet4Address) requestPacket.getHeader().getSrcAddr())
+ .correctChecksumAtBuild(true)
+ .correctLengthAtBuild(true)
+ .payloadBuilder(payLoadBuilder)
+ .build();
+
+ } else {
+ ipOutPacket = new IpV6Packet.Builder((IpV6Packet) requestPacket)
+ .srcAddr((Inet6Address) requestPacket.getHeader().getDstAddr())
+ .dstAddr((Inet6Address) requestPacket.getHeader().getSrcAddr())
+ .correctLengthAtBuild(true)
+ .payloadBuilder(payLoadBuilder)
+ .build();
+ }
+
+ queueDeviceWrite(ipOutPacket);
+ }
+
+ /**
+ * Handles a DNS request, by either blocking it or forwarding it to the remote location.
+ *
+ * @param packetData The packet data to read
+ * @throws DaedalusVpnService.VpnNetworkException If some network error occurred
+ */
+ private void handleDnsRequest(byte[] packetData) throws DaedalusVpnService.VpnNetworkException {
+
+ IpPacket parsedPacket;
+ try {
+ parsedPacket = (IpPacket) IpSelector.newPacket(packetData, 0, packetData.length);
+ //TODO: get rid of pcap4j
+ } catch (Exception e) {
+ Log.i(TAG, "handleDnsRequest: Discarding invalid IP packet", e);
+ return;
+ }
+
+ if (!(parsedPacket.getPayload() instanceof UdpPacket)) {
+ Log.i(TAG, "handleDnsRequest: Discarding unknown packet type " + parsedPacket.getPayload());
+ return;
+ }
+
+ InetAddress destAddr = parsedPacket.getHeader().getDstAddr();
+ if (destAddr == null)
+ return;
+
+ UdpPacket parsedUdp = (UdpPacket) parsedPacket.getPayload();
+
+
+ if (parsedUdp.getPayload() == null) {
+ Log.i(TAG, "handleDnsRequest: Sending UDP packet without payload: " + parsedUdp);
+
+ // Let's be nice to Firefox. Firefox uses an empty UDP packet to
+ // the gateway to reduce the RTT. For further details, please see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=888268
+ DatagramPacket outPacket = new DatagramPacket(new byte[0], 0, 0, destAddr,
+ DnsServerHelper.getPortOrDefault(destAddr, parsedUdp.getHeader().getDstPort().valueAsInt()));
+ forwardPacket(outPacket, null);
+ return;
+ }
+
+ byte[] dnsRawData = (parsedUdp).getPayload().getRawData();
+ DNSMessage dnsMsg;
+ try {
+ dnsMsg = new DNSMessage(dnsRawData);
+ } catch (IOException e) {
+ Log.i(TAG, "handleDnsRequest: Discarding non-DNS or invalid packet", e);
+ return;
+ }
+ if (dnsMsg.getQuestion() == null) {
+ Log.i(TAG, "handleDnsRequest: Discarding DNS packet with no query " + dnsMsg);
+ return;
+ }
+ String dnsQueryName = dnsMsg.getQuestion().name.toString();
+
+ try {
+ if (HostsResolver.canResolve(dnsQueryName)) {
+ String response = HostsResolver.resolve(dnsQueryName);
+ Log.i(TAG, "handleDnsRequest: DNS Name " + dnsQueryName + " address " + response + ", using local hosts to resolve.");
+ DNSMessage.Builder builder = dnsMsg.asBuilder();
+ int[] ip = new int[4];
+ byte i = 0;
+ for (String block : response.split("\\.")) {
+ ip[i] = Integer.parseInt(block);
+ i++;
+ }
+ builder.addAnswer(new Record<>(dnsQueryName, Record.TYPE.getType(A.class), 1, 64, new A(ip[0], ip[1], ip[2], ip[3])));
+ handleDnsResponse(parsedPacket, builder.build().toArray());
+ } else {
+ Log.i(TAG, "handleDnsRequest: DNS Name " + dnsQueryName + " , sending to " + destAddr);
+ DatagramPacket outPacket = new DatagramPacket(dnsRawData, 0, dnsRawData.length, destAddr,
+ DnsServerHelper.getPortOrDefault(destAddr, parsedUdp.getHeader().getDstPort().valueAsInt()));
+ forwardPacket(outPacket, parsedPacket);
+ }
+ } catch (Exception e) {
+ Log.e(TAG, e.toString());
+ }
+ }
+
+ /**
+ * Helper class holding a socket, the packet we are waiting the answer for, and a time
+ */
+ private static class WaitingOnSocketPacket {
+ final DatagramSocket socket;
+ final IpPacket packet;
+ private final long time;
+
+ WaitingOnSocketPacket(DatagramSocket 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 {
+ private final LinkedList list = new LinkedList<>();
+
+ void add(WaitingOnSocketPacket wosp) {
+ 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);
+ }
+
+ public Iterator iterator() {
+ return list.iterator();
+ }
+
+ int size() {
+ return list.size();
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java b/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java
index aee5fb4..71d2849 100644
--- a/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java
+++ b/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java
@@ -10,35 +10,18 @@ import android.net.VpnService;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.support.v7.app.NotificationCompat;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.system.OsConstants;
-import android.system.StructPollfd;
import android.util.Log;
-import de.measite.minidns.DNSMessage;
-import de.measite.minidns.Record;
-import de.measite.minidns.record.A;
import de.measite.minidns.util.InetAddressUtil;
import org.itxtech.daedalus.Daedalus;
import org.itxtech.daedalus.R;
import org.itxtech.daedalus.activity.MainActivity;
+import org.itxtech.daedalus.provider.DnsProvider;
+import org.itxtech.daedalus.provider.UdpDnsProvider;
import org.itxtech.daedalus.receiver.StatusBarBroadcastReceiver;
import org.itxtech.daedalus.util.DnsServerHelper;
import org.itxtech.daedalus.util.HostsResolver;
-import org.pcap4j.packet.*;
-import org.pcap4j.packet.factory.PacketFactoryPropertiesLoader;
-import org.pcap4j.util.PropertiesLoader;
-import java.io.FileDescriptor;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.net.*;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.Queue;
+import java.net.Inet4Address;
/**
* Daedalus Project
@@ -63,22 +46,12 @@ public class DaedalusVpnService extends VpnService implements Runnable {
private static NotificationCompat.Builder notification = null;
- private static long dnsQueryTimes = 0;
- private static boolean localHostsResolve = false;
-
- private Thread mThread = null;
- private ParcelFileDescriptor descriptor;
private boolean running = false;
private long lastUpdate = 0;
+ private boolean statisticQuery;
+ private DnsProvider provider;
- private final WospList dnsIn = new WospList();
- private FileDescriptor mBlockFd = null;
- private FileDescriptor mInterruptFd = null;
- private final Queue deviceWrites = new LinkedList<>();
- /**
- * Number of iterations since we last cleared the pcap4j cache
- */
- private int pcap4jFactoryClearCacheCounter = 0;
+ private Thread mThread = null;
@Override
public void onCreate() {
@@ -123,7 +96,6 @@ public class DaedalusVpnService extends VpnService implements Runnable {
Daedalus.initHostsResolver();
DnsServerHelper.buildPortCache();
- dnsQueryTimes = 0;
if (this.mThread == null) {
this.mThread = new Thread(this, "DaedalusVpn");
this.running = true;
@@ -148,21 +120,12 @@ public class DaedalusVpnService extends VpnService implements Runnable {
private void stopThread() {
boolean shouldRefresh = false;
try {
- if (this.descriptor != null) {
- this.descriptor.close();
- this.descriptor = null;
- }
- if (this.mThread != null) {
+ if (mThread != null) {
+ running = false;
shouldRefresh = true;
- this.running = false;
- this.mThread.interrupt();
- if (mInterruptFd != null) {
- Os.close(mInterruptFd);
- }
- if (mBlockFd != null) {
- Os.close(mBlockFd);
- }
- this.mThread = null;
+ provider.shutdown();
+ mThread.interrupt();
+ provider.stop();
}
if (notification != null) {
NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
@@ -180,7 +143,6 @@ public class DaedalusVpnService extends VpnService implements Runnable {
Daedalus.updateShortcut(getApplicationContext());
}
- dnsQueryTimes = 0;
HostsResolver.clean();
DnsServerHelper.cleanPortCache();
}
@@ -206,13 +168,9 @@ public class DaedalusVpnService extends VpnService implements Runnable {
format = prefix;
break;
}
- if (this.descriptor != null) {
- this.descriptor.close();
- }
boolean advanced = Daedalus.getPrefs().getBoolean("settings_advanced_switch", false);
- boolean statisticQuery = Daedalus.getPrefs().getBoolean("settings_count_query_times", false);
- localHostsResolve = Daedalus.getPrefs().getBoolean("settings_local_host_resolution", false);
+ statisticQuery = Daedalus.getPrefs().getBoolean("settings_count_query_times", false);
Log.d(TAG, "tun0 add " + format + " pServ " + primaryServer + " sServ " + secondaryServer);
Inet4Address primaryDNSServer = InetAddressUtil.ipv4From(primaryServer);
Inet4Address secondaryDNSServer = InetAddressUtil.ipv4From(secondaryServer);
@@ -229,96 +187,12 @@ public class DaedalusVpnService extends VpnService implements Runnable {
.setBlocking(true);
}
- this.descriptor = builder.establish();
+ ParcelFileDescriptor descriptor = builder.establish();
if (advanced) {
- 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 (WaitingOnSocketPacket wosp : dnsIn) {
- i++;
- StructPollfd pollFd = polls[2 + i] = new StructPollfd();
- pollFd.fd = ParcelFileDescriptor.fromDatagramSocket(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 iter = dnsIn.iterator();
- while (iter.hasNext()) {
- i++;
- WaitingOnSocketPacket wosp = iter.next();
- if ((polls[i + 2].revents & OsConstants.POLLIN) != 0) {
- Log.d(TAG, "Read from 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);
- }
-
- // pcap4j has some sort of properties cache in the packet factory. This cache leaks, so
- // we need to clean it up.
- if (++pcap4jFactoryClearCacheCounter % 1024 == 0) {
- try {
- PacketFactoryPropertiesLoader l = PacketFactoryPropertiesLoader.getInstance();
- Field field = l.getClass().getDeclaredField("loader");
- field.setAccessible(true);
- PropertiesLoader loader = (PropertiesLoader) field.get(l);
- Log.d(TAG, "Cleaning cache");
- loader.clearCache();
- } catch (NoSuchFieldException e) {
- Log.e(TAG, "Cannot find declared loader field", e);
- } catch (IllegalAccessException e) {
- Log.e(TAG, "Cannot get declared loader field", e);
- }
- }
-
- if (statisticQuery) {
- updateUserInterface();
- }
- }
+ provider = new UdpDnsProvider(descriptor, this);
+ provider.start();
+ provider.process();
} else {
while (running) {
Thread.sleep(1000);
@@ -332,268 +206,34 @@ public class DaedalusVpnService extends VpnService implements Runnable {
}
}
+ public void providerLoopCallback() {
+ if (statisticQuery) {
+ updateUserInterface();
+ }
+ }
+
private void updateUserInterface() {
long time = System.currentTimeMillis();
if (time - lastUpdate >= 1000) {
lastUpdate = time;
Log.i(TAG, "notify");
- notification.setContentTitle(getResources().getString(R.string.notification_queries) + " " + String.valueOf(dnsQueryTimes));
+ notification.setContentTitle(getResources().getString(R.string.notification_queries) + " " + String.valueOf(provider.getDnsQueryTimes()));
NotificationManager manager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(NOTIFICATION_ACTIVATED, notification.build());
}
}
- private void writeToDevice(FileOutputStream outFd) throws VpnNetworkException {
- try {
- outFd.write(deviceWrites.poll());
- } catch (IOException e) {
- // TODO: Make this more specific, only for: "File descriptor closed"
- throw new VpnNetworkException("Outgoing VPN output stream closed");
- }
- }
- private void readPacketFromDevice(FileInputStream inputStream, byte[] packet) throws VpnNetworkException, SocketException {
- // Read the outgoing packet from the input stream.
- int length;
-
- try {
- length = inputStream.read(packet);
- } catch (IOException e) {
- throw new VpnNetworkException("Cannot read from device", e);
- }
-
-
- if (length == 0) {
- // TODO: Possibly change to exception
- Log.w(TAG, "Got empty packet!");
- return;
- }
-
- final byte[] readPacket = Arrays.copyOfRange(packet, 0, length);
-
- handleDnsRequest(readPacket);
- }
-
- private void forwardPacket(DatagramPacket outPacket, IpPacket parsedPacket) throws VpnNetworkException {
- DatagramSocket dnsSocket;
- try {
- // Packets to be sent to the real DNS server will need to be protected from the VPN
- dnsSocket = new DatagramSocket();
-
- this.protect(dnsSocket);
-
- dnsSocket.send(outPacket);
-
- if (parsedPacket != null) {
- dnsIn.add(new 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 VpnNetworkException("Cannot send message:", e);
- }
- }
- Log.w(TAG, "handleDnsRequest: Could not send packet to upstream", e);
- }
- }
-
- private void queueDeviceWrite(IpPacket ipOutPacket) {
- dnsQueryTimes++;
- Log.i(TAG, "QT " + dnsQueryTimes);
- deviceWrites.add(ipOutPacket.getRawData());
- }
-
- private void handleRawDnsResponse(IpPacket parsedPacket, DatagramSocket dnsSocket) throws IOException {
- byte[] datagramData = new byte[1024];
- DatagramPacket replyPacket = new DatagramPacket(datagramData, datagramData.length);
- dnsSocket.receive(replyPacket);
- handleDnsResponse(parsedPacket, datagramData);
- }
-
-
- /**
- * Handles a responsePayload from an upstream DNS server
- *
- * @param requestPacket The original request packet
- * @param responsePayload The payload of the response
- */
- private void handleDnsResponse(IpPacket requestPacket, byte[] responsePayload) {
- UdpPacket udpOutPacket = (UdpPacket) requestPacket.getPayload();
- UdpPacket.Builder payLoadBuilder = new UdpPacket.Builder(udpOutPacket)
- .srcPort(udpOutPacket.getHeader().getDstPort())
- .dstPort(udpOutPacket.getHeader().getSrcPort())
- .srcAddr(requestPacket.getHeader().getDstAddr())
- .dstAddr(requestPacket.getHeader().getSrcAddr())
- .correctChecksumAtBuild(true)
- .correctLengthAtBuild(true)
- .payloadBuilder(
- new UnknownPacket.Builder()
- .rawData(responsePayload)
- );
-
-
- IpPacket ipOutPacket;
- if (requestPacket instanceof IpV4Packet) {
- ipOutPacket = new IpV4Packet.Builder((IpV4Packet) requestPacket)
- .srcAddr((Inet4Address) requestPacket.getHeader().getDstAddr())
- .dstAddr((Inet4Address) requestPacket.getHeader().getSrcAddr())
- .correctChecksumAtBuild(true)
- .correctLengthAtBuild(true)
- .payloadBuilder(payLoadBuilder)
- .build();
-
- } else {
- ipOutPacket = new IpV6Packet.Builder((IpV6Packet) requestPacket)
- .srcAddr((Inet6Address) requestPacket.getHeader().getDstAddr())
- .dstAddr((Inet6Address) requestPacket.getHeader().getSrcAddr())
- .correctLengthAtBuild(true)
- .payloadBuilder(payLoadBuilder)
- .build();
- }
-
- queueDeviceWrite(ipOutPacket);
- }
-
- /**
- * Handles a DNS request, by either blocking it or forwarding it to the remote location.
- *
- * @param packetData The packet data to read
- * @throws VpnNetworkException If some network error occurred
- */
- private void handleDnsRequest(byte[] packetData) throws VpnNetworkException {
-
- IpPacket parsedPacket;
- try {
- parsedPacket = (IpPacket) IpSelector.newPacket(packetData, 0, packetData.length);
- //TODO: get rid of pcap4j
- } catch (Exception e) {
- Log.i(TAG, "handleDnsRequest: Discarding invalid IP packet", e);
- return;
- }
-
- if (!(parsedPacket.getPayload() instanceof UdpPacket)) {
- Log.i(TAG, "handleDnsRequest: Discarding unknown packet type " + parsedPacket.getPayload());
- return;
- }
-
- InetAddress destAddr = parsedPacket.getHeader().getDstAddr();
- if (destAddr == null)
- return;
-
- UdpPacket parsedUdp = (UdpPacket) parsedPacket.getPayload();
-
-
- if (parsedUdp.getPayload() == null) {
- Log.i(TAG, "handleDnsRequest: Sending UDP packet without payload: " + parsedUdp);
-
- // Let's be nice to Firefox. Firefox uses an empty UDP packet to
- // the gateway to reduce the RTT. For further details, please see
- // https://bugzilla.mozilla.org/show_bug.cgi?id=888268
- DatagramPacket outPacket = new DatagramPacket(new byte[0], 0, 0, destAddr,
- DnsServerHelper.getPortOrDefault(destAddr, parsedUdp.getHeader().getDstPort().valueAsInt()));
- forwardPacket(outPacket, null);
- return;
- }
-
- byte[] dnsRawData = (parsedUdp).getPayload().getRawData();
- DNSMessage dnsMsg;
- try {
- dnsMsg = new DNSMessage(dnsRawData);
- } catch (IOException e) {
- Log.i(TAG, "handleDnsRequest: Discarding non-DNS or invalid packet", e);
- return;
- }
- if (dnsMsg.getQuestion() == null) {
- Log.i(TAG, "handleDnsRequest: Discarding DNS packet with no query " + dnsMsg);
- return;
- }
- String dnsQueryName = dnsMsg.getQuestion().name.toString();
-
- try {
- if (localHostsResolve && HostsResolver.canResolve(dnsQueryName)) {
- String response = HostsResolver.resolve(dnsQueryName);
- Log.i(TAG, "handleDnsRequest: DNS Name " + dnsQueryName + " address " + response + ", using local hosts to resolve.");
- DNSMessage.Builder builder = dnsMsg.asBuilder();
- int[] ip = new int[4];
- byte i = 0;
- for (String block : response.split("\\.")) {
- ip[i] = Integer.parseInt(block);
- i++;
- }
- builder.addAnswer(new Record<>(dnsQueryName, Record.TYPE.getType(A.class), 1, 64, new A(ip[0], ip[1], ip[2], ip[3])));
- handleDnsResponse(parsedPacket, builder.build().toArray());
- } else {
- Log.i(TAG, "handleDnsRequest: DNS Name " + dnsQueryName + " , sending to " + destAddr);
- DatagramPacket outPacket = new DatagramPacket(dnsRawData, 0, dnsRawData.length, destAddr,
- DnsServerHelper.getPortOrDefault(destAddr, parsedUdp.getHeader().getDstPort().valueAsInt()));
- forwardPacket(outPacket, parsedPacket);
- }
- } catch (Exception e) {
- Log.e(TAG, e.toString());
- }
- }
-
- static class VpnNetworkException extends Exception {
- VpnNetworkException(String s) {
+ public static class VpnNetworkException extends Exception {
+ public VpnNetworkException(String s) {
super(s);
}
- VpnNetworkException(String s, Throwable t) {
+ public VpnNetworkException(String s, Throwable t) {
super(s, t);
}
}
- /**
- * Helper class holding a socket, the packet we are waiting the answer for, and a time
- */
- private static class WaitingOnSocketPacket {
- final DatagramSocket socket;
- final IpPacket packet;
- private final long time;
-
- WaitingOnSocketPacket(DatagramSocket 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 {
- private final LinkedList list = new LinkedList<>();
-
- void add(WaitingOnSocketPacket wosp) {
- 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);
- }
-
- public Iterator iterator() {
- return list.iterator();
- }
-
- int size() {
- return list.size();
- }
-
- }
}
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index c1bcbf4..6238146 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -63,4 +63,5 @@
删除
应用
请填写所有配置项。
+ DNS over TCP
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 00f4da9..0d5f912 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -63,4 +63,5 @@
Delete
Apply
Please fill in all configuration items.
+ DNS over TCP
diff --git a/app/src/main/res/xml/perf_settings.xml b/app/src/main/res/xml/perf_settings.xml
index 0da1ae3..cfa7c1e 100644
--- a/app/src/main/res/xml/perf_settings.xml
+++ b/app/src/main/res/xml/perf_settings.xml
@@ -59,6 +59,11 @@
android:title="@string/settings_hosts_pan_domain_resolution"
android:defaultValue="false"
android:enabled="false"/>
+
Date: Wed, 3 May 2017 22:44:16 +0800
Subject: [PATCH 2/2] Implemented DNS over TCP (very slow)
---
.../java/org/itxtech/daedalus/Daedalus.java | 3 +-
.../activity/DnsServerConfigActivity.java | 3 +-
.../daedalus/activity/MainActivity.java | 3 +-
.../daedalus/fragment/AboutFragment.java | 3 +-
.../fragment/DnsServerConfigFragment.java | 3 +-
.../daedalus/fragment/DnsServersFragment.java | 3 +-
.../daedalus/fragment/DnsTestFragment.java | 3 +-
.../daedalus/fragment/HostsFragment.java | 3 +-
.../daedalus/fragment/MainFragment.java | 3 +-
.../daedalus/fragment/SettingsFragment.java | 3 +-
.../daedalus/provider/DnsProvider.java | 3 +-
.../daedalus/provider/TcpDnsProvider.java | 264 ++++++++++++++++++
.../daedalus/provider/UdpDnsProvider.java | 54 ++--
.../receiver/BootBroadcastReceiver.java | 3 +-
.../receiver/StatusBarBroadcastReceiver.java | 3 +-
.../daedalus/service/DaedalusVpnService.java | 10 +-
.../itxtech/daedalus/util/Configurations.java | 3 +-
.../daedalus/util/CustomDnsServer.java | 3 +-
.../org/itxtech/daedalus/util/DnsServer.java | 3 +-
.../daedalus/util/DnsServerHelper.java | 3 +-
.../itxtech/daedalus/util/HostsProvider.java | 3 +-
.../itxtech/daedalus/util/HostsResolver.java | 3 +-
.../view/ClearAutoCompleteTextView.java | 3 +-
.../daedalus/view/ClickPreference.java | 6 +-
24 files changed, 346 insertions(+), 48 deletions(-)
create mode 100644 app/src/main/java/org/itxtech/daedalus/provider/TcpDnsProvider.java
diff --git a/app/src/main/java/org/itxtech/daedalus/Daedalus.java b/app/src/main/java/org/itxtech/daedalus/Daedalus.java
index 2b3aa94..b3aaf21 100644
--- a/app/src/main/java/org/itxtech/daedalus/Daedalus.java
+++ b/app/src/main/java/org/itxtech/daedalus/Daedalus.java
@@ -38,7 +38,8 @@ import java.util.List;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class Daedalus extends Application {
private static final String SHORTCUT_ID_ACTIVATE = "shortcut_activate";
diff --git a/app/src/main/java/org/itxtech/daedalus/activity/DnsServerConfigActivity.java b/app/src/main/java/org/itxtech/daedalus/activity/DnsServerConfigActivity.java
index 097f533..8eedada 100644
--- a/app/src/main/java/org/itxtech/daedalus/activity/DnsServerConfigActivity.java
+++ b/app/src/main/java/org/itxtech/daedalus/activity/DnsServerConfigActivity.java
@@ -21,7 +21,8 @@ import org.itxtech.daedalus.fragment.DnsServerConfigFragment;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DnsServerConfigActivity extends AppCompatActivity {
public static final String LAUNCH_ACTION_CUSTOM_DNS_SERVER_ID = "org.itxtech.daedalus.activity.DnsServerConfigActivity.LAUNCH_ACTION_CUSTOM_DNS_SERVER_ID";
diff --git a/app/src/main/java/org/itxtech/daedalus/activity/MainActivity.java b/app/src/main/java/org/itxtech/daedalus/activity/MainActivity.java
index 3429f98..bc8108d 100644
--- a/app/src/main/java/org/itxtech/daedalus/activity/MainActivity.java
+++ b/app/src/main/java/org/itxtech/daedalus/activity/MainActivity.java
@@ -34,7 +34,8 @@ import org.itxtech.daedalus.fragment.*;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
public static final String LAUNCH_ACTION = "org.itxtech.daedalus.activity.MainActivity.LAUNCH_ACTION";
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/AboutFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/AboutFragment.java
index 184a70c..60733ff 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/AboutFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/AboutFragment.java
@@ -25,7 +25,8 @@ import java.util.Locale;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class AboutFragment extends Fragment {
private WebView mWebView = null;
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/DnsServerConfigFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/DnsServerConfigFragment.java
index 43d0435..ab90a1c 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/DnsServerConfigFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/DnsServerConfigFragment.java
@@ -25,7 +25,8 @@ import org.itxtech.daedalus.util.DnsServer;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DnsServerConfigFragment extends PreferenceFragment implements Toolbar.OnMenuItemClickListener {
private Intent intent = null;
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/DnsServersFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/DnsServersFragment.java
index 99664ed..03b29ea 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/DnsServersFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/DnsServersFragment.java
@@ -24,7 +24,8 @@ import org.itxtech.daedalus.util.CustomDnsServer;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DnsServersFragment extends Fragment {
private DnsServersFragment.DnsServerAdapter adapter;
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/DnsTestFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/DnsTestFragment.java
index e471fd8..a4f1d59 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/DnsTestFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/DnsTestFragment.java
@@ -37,7 +37,8 @@ import java.util.Set;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DnsTestFragment extends Fragment {
private static final String TAG = "DServerTest";
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/HostsFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/HostsFragment.java
index 68c7ac3..0f0c196 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/HostsFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/HostsFragment.java
@@ -30,7 +30,8 @@ import java.util.Date;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class HostsFragment extends Fragment {
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/MainFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/MainFragment.java
index 658270e..be0c052 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/MainFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/MainFragment.java
@@ -26,7 +26,8 @@ import org.itxtech.daedalus.util.DnsServerHelper;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class MainFragment extends Fragment {
diff --git a/app/src/main/java/org/itxtech/daedalus/fragment/SettingsFragment.java b/app/src/main/java/org/itxtech/daedalus/fragment/SettingsFragment.java
index f0626c7..4f5183f 100644
--- a/app/src/main/java/org/itxtech/daedalus/fragment/SettingsFragment.java
+++ b/app/src/main/java/org/itxtech/daedalus/fragment/SettingsFragment.java
@@ -21,7 +21,8 @@ import org.itxtech.daedalus.util.DnsServerHelper;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class SettingsFragment extends PreferenceFragment {
private View view = null;
diff --git a/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java b/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java
index 090c7dd..12e0178 100644
--- a/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java
+++ b/app/src/main/java/org/itxtech/daedalus/provider/DnsProvider.java
@@ -11,7 +11,8 @@ import org.itxtech.daedalus.service.DaedalusVpnService;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public abstract class DnsProvider {
ParcelFileDescriptor descriptor;
diff --git a/app/src/main/java/org/itxtech/daedalus/provider/TcpDnsProvider.java b/app/src/main/java/org/itxtech/daedalus/provider/TcpDnsProvider.java
new file mode 100644
index 0000000..ca5af94
--- /dev/null
+++ b/app/src/main/java/org/itxtech/daedalus/provider/TcpDnsProvider.java
@@ -0,0 +1,264 @@
+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 de.measite.minidns.DNSMessage;
+import org.itxtech.daedalus.service.DaedalusVpnService;
+import org.itxtech.daedalus.util.DnsServerHelper;
+import org.pcap4j.packet.*;
+
+import java.io.*;
+import java.net.*;
+import java.nio.channels.SocketChannel;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * @author PeratX
+ */
+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 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();
+
+ Log.d(TAG, String.valueOf(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) throws IOException, IllegalRawDataException {
+ if (!dnsSocket.isClosed()) {
+ 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);
+ }
+ }
+
+
+ /**
+ * Handles a responsePayload from an upstream DNS server
+ *
+ * @param requestPacket The original request packet
+ * @param responsePayload The payload of the response
+ */
+ private void handleDnsResponse(IpPacket requestPacket, byte[] responsePayload) throws IllegalRawDataException, IOException {
+ DNSMessage message = new DNSMessage(responsePayload);
+ Log.d(TAG, message.toString());
+
+ UdpPacket udpOutPacket = (UdpPacket) requestPacket.getPayload();
+ UdpPacket.Builder payLoadBuilder = new UdpPacket.Builder(udpOutPacket)
+ .srcPort(udpOutPacket.getHeader().getDstPort())
+ .dstPort(udpOutPacket.getHeader().getSrcPort())
+ .srcAddr(requestPacket.getHeader().getDstAddr())
+ .dstAddr(requestPacket.getHeader().getSrcAddr())
+ .correctChecksumAtBuild(true)
+ .correctLengthAtBuild(true)
+ .payloadBuilder(
+ new UnknownPacket.Builder()
+ .rawData(responsePayload)
+ );
+
+
+ IpPacket ipOutPacket;
+ if (requestPacket instanceof IpV4Packet) {
+ ipOutPacket = new IpV4Packet.Builder((IpV4Packet) requestPacket)
+ .srcAddr((Inet4Address) requestPacket.getHeader().getDstAddr())
+ .dstAddr((Inet4Address) requestPacket.getHeader().getSrcAddr())
+ .correctChecksumAtBuild(true)
+ .correctLengthAtBuild(true)
+ .payloadBuilder(payLoadBuilder)
+ .build();
+
+ } else {
+ ipOutPacket = new IpV6Packet.Builder((IpV6Packet) requestPacket)
+ .srcAddr((Inet6Address) requestPacket.getHeader().getDstAddr())
+ .dstAddr((Inet6Address) requestPacket.getHeader().getSrcAddr())
+ .correctLengthAtBuild(true)
+ .payloadBuilder(payLoadBuilder)
+ .build();
+ }
+
+ queueDeviceWrite(ipOutPacket);
+ }
+
+ /**
+ * 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 {
+ private final LinkedList 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 iterator() {
+ return list.iterator();
+ }
+
+ int size() {
+ return list.size();
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java b/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java
index 8fd35e2..7d4b4b9 100644
--- a/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java
+++ b/app/src/main/java/org/itxtech/daedalus/provider/UdpDnsProvider.java
@@ -37,15 +37,16 @@ import java.util.Queue;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class UdpDnsProvider extends DnsProvider {
private static final String TAG = "UdpDnsProvider";
private final WospList dnsIn = new WospList();
- private FileDescriptor mBlockFd = null;
- private FileDescriptor mInterruptFd = null;
- private final Queue deviceWrites = new LinkedList<>();
+ FileDescriptor mBlockFd = null;
+ FileDescriptor mInterruptFd = null;
+ final Queue deviceWrites = new LinkedList<>();
/**
* Number of iterations since we last cleared the pcap4j cache
@@ -73,7 +74,7 @@ public class UdpDnsProvider extends DnsProvider {
}
}
- private void queueDeviceWrite(IpPacket ipOutPacket) {
+ void queueDeviceWrite(IpPacket ipOutPacket) {
dnsQueryTimes++;
Log.i(TAG, "QT " + dnsQueryTimes);
deviceWrites.add(ipOutPacket.getRawData());
@@ -132,7 +133,7 @@ public class UdpDnsProvider extends DnsProvider {
i++;
WaitingOnSocketPacket wosp = iter.next();
if ((polls[i + 2].revents & OsConstants.POLLIN) != 0) {
- Log.d(TAG, "Read from DNS socket" + wosp.socket);
+ Log.d(TAG, "Read from UDP DNS socket" + wosp.socket);
iter.remove();
handleRawDnsResponse(wosp.packet, wosp.socket);
wosp.socket.close();
@@ -148,22 +149,7 @@ public class UdpDnsProvider extends DnsProvider {
readPacketFromDevice(inputStream, packet);
}
- // pcap4j has some sort of properties cache in the packet factory. This cache leaks, so
- // we need to clean it up.
- if (++pcap4jFactoryClearCacheCounter % 1024 == 0) {
- try {
- PacketFactoryPropertiesLoader l = PacketFactoryPropertiesLoader.getInstance();
- Field field = l.getClass().getDeclaredField("loader");
- field.setAccessible(true);
- PropertiesLoader loader = (PropertiesLoader) field.get(l);
- Log.d(TAG, "Cleaning cache");
- loader.clearCache();
- } catch (NoSuchFieldException e) {
- Log.e(TAG, "Cannot find declared loader field", e);
- } catch (IllegalAccessException e) {
- Log.e(TAG, "Cannot get declared loader field", e);
- }
- }
+ checkCache();
service.providerLoopCallback();
}
} catch (Exception e) {
@@ -171,8 +157,26 @@ public class UdpDnsProvider extends DnsProvider {
}
}
+ void checkCache() {
+ // pcap4j has some sort of properties cache in the packet factory. This cache leaks, so
+ // we need to clean it up.
+ if (++pcap4jFactoryClearCacheCounter % 1024 == 0) {
+ try {
+ PacketFactoryPropertiesLoader l = PacketFactoryPropertiesLoader.getInstance();
+ Field field = l.getClass().getDeclaredField("loader");
+ field.setAccessible(true);
+ PropertiesLoader loader = (PropertiesLoader) field.get(l);
+ Log.d(TAG, "Cleaning cache");
+ loader.clearCache();
+ } catch (NoSuchFieldException e) {
+ Log.e(TAG, "Cannot find declared loader field", e);
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Cannot get declared loader field", e);
+ }
+ }
+ }
- private void writeToDevice(FileOutputStream outFd) throws DaedalusVpnService.VpnNetworkException {
+ void writeToDevice(FileOutputStream outFd) throws DaedalusVpnService.VpnNetworkException {
try {
outFd.write(deviceWrites.poll());
} catch (IOException e) {
@@ -181,7 +185,7 @@ public class UdpDnsProvider extends DnsProvider {
}
}
- private void readPacketFromDevice(FileInputStream inputStream, byte[] packet) throws DaedalusVpnService.VpnNetworkException, SocketException {
+ void readPacketFromDevice(FileInputStream inputStream, byte[] packet) throws DaedalusVpnService.VpnNetworkException, SocketException {
// Read the outgoing packet from the input stream.
int length;
@@ -203,7 +207,7 @@ public class UdpDnsProvider extends DnsProvider {
handleDnsRequest(readPacket);
}
- private void forwardPacket(DatagramPacket outPacket, IpPacket parsedPacket) throws DaedalusVpnService.VpnNetworkException {
+ void forwardPacket(DatagramPacket outPacket, IpPacket parsedPacket) throws DaedalusVpnService.VpnNetworkException {
DatagramSocket dnsSocket;
try {
// Packets to be sent to the real DNS server will need to be protected from the VPN
diff --git a/app/src/main/java/org/itxtech/daedalus/receiver/BootBroadcastReceiver.java b/app/src/main/java/org/itxtech/daedalus/receiver/BootBroadcastReceiver.java
index f073998..deadd93 100644
--- a/app/src/main/java/org/itxtech/daedalus/receiver/BootBroadcastReceiver.java
+++ b/app/src/main/java/org/itxtech/daedalus/receiver/BootBroadcastReceiver.java
@@ -17,7 +17,8 @@ import org.itxtech.daedalus.util.DnsServerHelper;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class BootBroadcastReceiver extends BroadcastReceiver {
@Override
diff --git a/app/src/main/java/org/itxtech/daedalus/receiver/StatusBarBroadcastReceiver.java b/app/src/main/java/org/itxtech/daedalus/receiver/StatusBarBroadcastReceiver.java
index 3ba26ed..a642e5a 100644
--- a/app/src/main/java/org/itxtech/daedalus/receiver/StatusBarBroadcastReceiver.java
+++ b/app/src/main/java/org/itxtech/daedalus/receiver/StatusBarBroadcastReceiver.java
@@ -17,7 +17,8 @@ import java.lang.reflect.Method;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class StatusBarBroadcastReceiver extends BroadcastReceiver {
public static String STATUS_BAR_BTN_DEACTIVATE_CLICK_ACTION = "org.itxtech.daedalus.receiver.StatusBarBroadcastReceiver.STATUS_BAR_BTN_DEACTIVATE_CLICK_ACTION";
diff --git a/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java b/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java
index 5ed1055..14df70b 100644
--- a/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java
+++ b/app/src/main/java/org/itxtech/daedalus/service/DaedalusVpnService.java
@@ -16,6 +16,7 @@ import org.itxtech.daedalus.Daedalus;
import org.itxtech.daedalus.R;
import org.itxtech.daedalus.activity.MainActivity;
import org.itxtech.daedalus.provider.DnsProvider;
+import org.itxtech.daedalus.provider.TcpDnsProvider;
import org.itxtech.daedalus.provider.UdpDnsProvider;
import org.itxtech.daedalus.receiver.StatusBarBroadcastReceiver;
import org.itxtech.daedalus.util.DnsServerHelper;
@@ -31,7 +32,8 @@ import java.net.Inet4Address;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DaedalusVpnService extends VpnService implements Runnable {
public static final String ACTION_ACTIVATE = "org.itxtech.daedalus.service.DaedalusVpnService.ACTION_ACTIVATE";
@@ -191,7 +193,11 @@ public class DaedalusVpnService extends VpnService implements Runnable {
ParcelFileDescriptor descriptor = builder.establish();
if (advanced) {
- provider = new UdpDnsProvider(descriptor, this);
+ if (Daedalus.getPrefs().getBoolean("settings_dns_over_tcp", false)) {
+ provider = new TcpDnsProvider(descriptor, this);
+ } else {
+ provider = new UdpDnsProvider(descriptor, this);
+ }
provider.start();
provider.process();
} else {
diff --git a/app/src/main/java/org/itxtech/daedalus/util/Configurations.java b/app/src/main/java/org/itxtech/daedalus/util/Configurations.java
index 3460092..c2de7a7 100644
--- a/app/src/main/java/org/itxtech/daedalus/util/Configurations.java
+++ b/app/src/main/java/org/itxtech/daedalus/util/Configurations.java
@@ -18,7 +18,8 @@ import java.util.ArrayList;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class Configurations {
private static final int CUSTOM_DNS_ID_START = 32;
diff --git a/app/src/main/java/org/itxtech/daedalus/util/CustomDnsServer.java b/app/src/main/java/org/itxtech/daedalus/util/CustomDnsServer.java
index 2be822c..f171826 100644
--- a/app/src/main/java/org/itxtech/daedalus/util/CustomDnsServer.java
+++ b/app/src/main/java/org/itxtech/daedalus/util/CustomDnsServer.java
@@ -10,7 +10,8 @@ import org.itxtech.daedalus.Daedalus;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class CustomDnsServer {
private String name;
diff --git a/app/src/main/java/org/itxtech/daedalus/util/DnsServer.java b/app/src/main/java/org/itxtech/daedalus/util/DnsServer.java
index 98d1525..6bc375b 100644
--- a/app/src/main/java/org/itxtech/daedalus/util/DnsServer.java
+++ b/app/src/main/java/org/itxtech/daedalus/util/DnsServer.java
@@ -10,7 +10,8 @@ import android.content.Context;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DnsServer {
public static int DNS_SERVER_DEFAULT_PORT = 53;
diff --git a/app/src/main/java/org/itxtech/daedalus/util/DnsServerHelper.java b/app/src/main/java/org/itxtech/daedalus/util/DnsServerHelper.java
index 61eee35..707d8da 100644
--- a/app/src/main/java/org/itxtech/daedalus/util/DnsServerHelper.java
+++ b/app/src/main/java/org/itxtech/daedalus/util/DnsServerHelper.java
@@ -15,7 +15,8 @@ import java.util.HashMap;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class DnsServerHelper {
private static HashMap portCache = null;
diff --git a/app/src/main/java/org/itxtech/daedalus/util/HostsProvider.java b/app/src/main/java/org/itxtech/daedalus/util/HostsProvider.java
index 4a56495..c20085f 100644
--- a/app/src/main/java/org/itxtech/daedalus/util/HostsProvider.java
+++ b/app/src/main/java/org/itxtech/daedalus/util/HostsProvider.java
@@ -12,7 +12,8 @@ import java.util.ArrayList;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class HostsProvider {
private String name;
diff --git a/app/src/main/java/org/itxtech/daedalus/util/HostsResolver.java b/app/src/main/java/org/itxtech/daedalus/util/HostsResolver.java
index c80e016..60992de 100644
--- a/app/src/main/java/org/itxtech/daedalus/util/HostsResolver.java
+++ b/app/src/main/java/org/itxtech/daedalus/util/HostsResolver.java
@@ -16,7 +16,8 @@ import java.util.HashMap;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class HostsResolver implements Runnable {
private static final String TAG = "DHostsResolver";
diff --git a/app/src/main/java/org/itxtech/daedalus/view/ClearAutoCompleteTextView.java b/app/src/main/java/org/itxtech/daedalus/view/ClearAutoCompleteTextView.java
index aee739c..260ae1c 100644
--- a/app/src/main/java/org/itxtech/daedalus/view/ClearAutoCompleteTextView.java
+++ b/app/src/main/java/org/itxtech/daedalus/view/ClearAutoCompleteTextView.java
@@ -20,7 +20,8 @@ import org.itxtech.daedalus.R;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class ClearAutoCompleteTextView extends AutoCompleteTextView implements View.OnTouchListener, View.OnFocusChangeListener, TextWatcher {
diff --git a/app/src/main/java/org/itxtech/daedalus/view/ClickPreference.java b/app/src/main/java/org/itxtech/daedalus/view/ClickPreference.java
index e8c7b67..b0fcce5 100644
--- a/app/src/main/java/org/itxtech/daedalus/view/ClickPreference.java
+++ b/app/src/main/java/org/itxtech/daedalus/view/ClickPreference.java
@@ -1,6 +1,8 @@
package org.itxtech.daedalus.view;
+import android.annotation.TargetApi;
import android.content.Context;
+import android.os.Build;
import android.preference.ListPreference;
import android.util.AttributeSet;
@@ -12,10 +14,12 @@ import android.util.AttributeSet;
*
* 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, version 3.
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
*/
public class ClickPreference extends ListPreference {
+ @TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ClickPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}