Merge branch 'dns-over-tcp'

This commit is contained in:
PeratX 2017-05-03 22:51:14 +08:00
commit ee3d041605
27 changed files with 804 additions and 406 deletions

View File

@ -38,7 +38,8 @@ import java.util.List;
* <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, 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";

View File

@ -21,7 +21,8 @@ import org.itxtech.daedalus.fragment.DnsServerConfigFragment;
* <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, 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";

View File

@ -34,7 +34,8 @@ import org.itxtech.daedalus.fragment.*;
* <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, 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";

View File

@ -25,7 +25,8 @@ import java.util.Locale;
* <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, 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;

View File

@ -25,7 +25,8 @@ import org.itxtech.daedalus.util.DnsServer;
* <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, 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;

View File

@ -24,7 +24,8 @@ import org.itxtech.daedalus.util.CustomDnsServer;
* <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, 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;

View File

@ -37,7 +37,8 @@ import java.util.Set;
* <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, 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";

View File

@ -30,7 +30,8 @@ import java.util.Date;
* <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, version 3.
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
public class HostsFragment extends Fragment {

View File

@ -26,7 +26,8 @@ import org.itxtech.daedalus.util.DnsServerHelper;
* <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, version 3.
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
public class MainFragment extends Fragment {

View File

@ -21,7 +21,8 @@ import org.itxtech.daedalus.util.DnsServerHelper;
* <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, 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;

View File

@ -0,0 +1,44 @@
package org.itxtech.daedalus.provider;
import android.os.ParcelFileDescriptor;
import org.itxtech.daedalus.service.DaedalusVpnService;
/**
* Daedalus Project
*
* @author iTXTech
* @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 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();
}

View File

@ -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<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();
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<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();
}
}
}

View File

@ -0,0 +1,414 @@
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
* <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 UdpDnsProvider extends DnsProvider {
private static final String TAG = "UdpDnsProvider";
private final WospList dnsIn = new WospList();
FileDescriptor mBlockFd = null;
FileDescriptor mInterruptFd = null;
final Queue<byte[]> 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) {
}
}
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<WaitingOnSocketPacket> iter = dnsIn.iterator();
while (iter.hasNext()) {
i++;
WaitingOnSocketPacket wosp = iter.next();
if ((polls[i + 2].revents & OsConstants.POLLIN) != 0) {
Log.d(TAG, "Read from UDP 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();
}
}
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);
}
}
}
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");
}
}
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);
}
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<WaitingOnSocketPacket> {
private final LinkedList<WaitingOnSocketPacket> 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<WaitingOnSocketPacket> iterator() {
return list.iterator();
}
int size() {
return list.size();
}
}
}

View File

@ -17,7 +17,8 @@ import org.itxtech.daedalus.util.DnsServerHelper;
* <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, 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

View File

@ -17,7 +17,8 @@ import java.lang.reflect.Method;
* <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, 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";

View File

@ -10,35 +10,19 @@ 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.TcpDnsProvider;
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
@ -48,7 +32,8 @@ import java.util.Queue;
* <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, 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";
@ -63,22 +48,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<byte[]> 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() {
@ -124,7 +99,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;
@ -149,21 +123,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);
@ -181,7 +146,6 @@ public class DaedalusVpnService extends VpnService implements Runnable {
Daedalus.updateShortcut(getApplicationContext());
}
dnsQueryTimes = 0;
HostsResolver.clean();
DnsServerHelper.cleanPortCache();
}
@ -207,13 +171,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);
@ -230,96 +190,16 @@ 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<WaitingOnSocketPacket> 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();
}
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 {
while (running) {
Thread.sleep(1000);
@ -333,268 +213,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<WaitingOnSocketPacket> {
private final LinkedList<WaitingOnSocketPacket> 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<WaitingOnSocketPacket> iterator() {
return list.iterator();
}
int size() {
return list.size();
}
}
}

View File

@ -18,7 +18,8 @@ import java.util.ArrayList;
* <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, 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;

View File

@ -10,7 +10,8 @@ import org.itxtech.daedalus.Daedalus;
* <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, 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;

View File

@ -10,7 +10,8 @@ import android.content.Context;
* <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, 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;

View File

@ -15,7 +15,8 @@ import java.util.HashMap;
* <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, 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<String, Integer> portCache = null;

View File

@ -12,7 +12,8 @@ import java.util.ArrayList;
* <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, 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;

View File

@ -16,7 +16,8 @@ import java.util.HashMap;
* <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, 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";

View File

@ -20,7 +20,8 @@ import org.itxtech.daedalus.R;
* <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, 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 {

View File

@ -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;
* <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, 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);
}

View File

@ -63,4 +63,5 @@
<string name="delete">删除</string>
<string name="apply">应用</string>
<string name="notice_fill_in_all">请填写所有配置项。</string>
<string name="settings_dns_over_tcp">DNS over TCP</string>
</resources>

View File

@ -63,4 +63,5 @@
<string name="delete">Delete</string>
<string name="apply">Apply</string>
<string name="notice_fill_in_all">Please fill in all configuration items.</string>
<string name="settings_dns_over_tcp">DNS over TCP</string>
</resources>

View File

@ -59,6 +59,11 @@
android:title="@string/settings_hosts_pan_domain_resolution"
android:defaultValue="false"
android:enabled="false"/>
<SwitchPreference
android:key="settings_dns_over_tcp"
android:title="@string/settings_dns_over_tcp"
android:defaultValue="false"
android:enabled="false"/>
</PreferenceCategory>
<PreferenceCategory