/*******************************************************************************
* gMix open source project - https://svs.informatik.uni-hamburg.de/gmix/
* Copyright (C) 2014 SVS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*******************************************************************************/
package userGeneratedContent.testbedPlugIns.layerPlugIns.layer2recodingScheme.encDNS_v0_001;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.lang3.ArrayUtils;
import staticContent.framework.EncDnsServer;
import staticContent.framework.controller.Implementation;
import staticContent.framework.interfaces.Layer2RecodingSchemeMix;
import staticContent.framework.message.Reply;
import staticContent.framework.message.Request;
import staticContent.framework.userDatabase.User;
import staticContent.framework.util.Util;
import com.github.encdns.LibSodiumWrapper;
/**
* This is the EncDNS server proxy (also known as remote proxy). It should be
* executed on the same computer as the remote recursive nameserver or at least
* be connected to it via a trustworthy connection. The server proxy will
* decrypt encrypted EncDNS queries it receives and pass the decrypted
* standard DNS query on to the remote recursive nameserver. It will also
* encrypt the standard DNS responses received from the remote recursive
* nameserver and pass the EncDNS response on to the local recursive nameserver.
*/
public class MixPlugIn extends Implementation implements Layer2RecodingSchemeMix {
/** size of cache for intermediate shared secrets */
public int CACHESIZE;
/** maximum number of threads */
private int _maxThreads;
private byte[] _magicString;
private LibSodiumWrapper _libSodium;
private byte[] _zoneName;
private Map<EncDNSKey, byte[]> _cache;
private boolean _encryption;
private byte[] _sk;
private static boolean displayByteThroughput;
private static boolean displayPacketThroughput;
private static boolean displayThreadStatus;
private static int displayThreadInterval;
private static AtomicInteger decryptingThreads = new AtomicInteger(0);
private static AtomicInteger resolvingThreads = new AtomicInteger(0);
private static AtomicInteger encryptingThreads = new AtomicInteger(0);
private static AtomicInteger sendingThreads = new AtomicInteger(0);
private static AtomicInteger idleThreads = new AtomicInteger(0);
private static int threads;
private userGeneratedContent.testbedPlugIns.layerPlugIns.layer1network.encDNS_v0_001.MixPlugIn layer1;
private DatagramSocket layer1socket;
private userGeneratedContent.testbedPlugIns.layerPlugIns.layer5application.encDNS_v0_001.MixPlugIn layer5;
@Override
public void constructor() {
_encryption = EncDnsServer.encryption;
if (_encryption)
_libSodium = new LibSodiumWrapper();
CACHESIZE = EncDnsServer.cachesize;
if (CACHESIZE == 0)
System.out.println("key-caching is disabled");
_zoneName = EncDNSHelper.parseZoneNameString(EncDnsServer.zoneurl);
_magicString = new byte[]{0x20, 0x45, 0x5e};
_cache = Collections.synchronizedMap(new SharedSecretCache(CACHESIZE));
_maxThreads = EncDnsServer.maxThreads;
threads = _maxThreads;
if (_encryption) {
//_pk = EncDNSHelper.readByteArrayFromFile(pkPath);
_sk = EncDNSHelper.readByteArrayFromFile(EncDnsServer.skPath);
if (Util.toHex(_sk).equals("59A79183CC11D725C64DF8005E784B8ABE6908F053E1AC77BB022DA250D36049"))
System.err.println("WARNING: you are using a publicly known test key for this EncDNS Server; never use this key to transmit sensitive data!");
}
displayThreadInterval = EncDnsServer.displayThreadStatusInt;
displayThreadStatus = EncDnsServer.displayThreadStatusBool;
displayByteThroughput = EncDnsServer.displayThroughputBool;
displayPacketThroughput = EncDnsServer.displayThroughputBool;
if (displayThreadStatus) {
new Thread(
new Runnable() {
public void run() {
while (true) {
try {
Thread.sleep(displayThreadInterval);
} catch (InterruptedException e) {
e.printStackTrace();
continue;
}
String threadStatus = "\nThreadStatus (of "+threads +" threads):\n";
threadStatus += " idle threads: " +idleThreads.get() +"\n";
threadStatus += " decrypting threads: " +decryptingThreads.get() +"\n";
threadStatus += " resolvingThreads threads: " +resolvingThreads.get() +"\n";
threadStatus += " encryptingThreads threads: " +encryptingThreads.get() +"\n";
System.out.println(threadStatus);
}
}
}
).start();
}
if (EncDnsServer.displayThroughputBool) {
StatisticsRecorder.init(EncDnsServer.displayThroughputBool, EncDnsServer.displayThroughputBool, displayThreadInterval, threads);
}
}
@Override
public void initialize() {
}
@Override
public void begin() {
this.layer1 = ((userGeneratedContent.testbedPlugIns.layerPlugIns.layer1network.encDNS_v0_001.MixPlugIn)networkLayerMix.getImplementation());
this.layer1socket = layer1.getSocket();
this.layer5 = ((userGeneratedContent.testbedPlugIns.layerPlugIns.layer5application.encDNS_v0_001.MixPlugIn)applicationLayerMix.getImplementations()[0]);
for (int i=0; i<_maxThreads; i++) {
new Thread(new ServerMessageHandlerThread()).start();
}
}
@Override
public int getMaxSizeOfNextReply() {
return EncDnsServer.MAX_MSG_SIZE;
}
@Override
public int getMaxSizeOfNextRequest() {
return EncDnsServer.MAX_MSG_SIZE;
}
/**
* This thread will send a query to the remote recursive nameserver, wait
* for a response and pass the decrypted response on to the local recursive
* nameserver.
*
* This is encapsulated in a thread to allow parallelized processing of
* queries.
*/
private class ServerMessageHandlerThread implements Runnable {
//private DatagramPacket _rcvPkt;
private byte[] _k;
private int threadId;
/**
* Constructor for ServerMessageHandlerThread
* @param rcvPkt (hopefully) an EncDNS query received from a loccal
* recursive nameserver
*/
ServerMessageHandlerThread(/*DatagramPacket rcvPkt*/) {
this.threadId = StatisticsRecorder.getThreadId();
}
public void run() {
/*if(EncDnsServer.verbosity >= 1) {
System.out.println("ServerMessageHandler started");
}*/
while (true) {
byte[] rcvPkt = new byte[EncDnsServer.MAX_MSG_SIZE];
DatagramPacket _rcvPkt = new DatagramPacket(rcvPkt, rcvPkt.length);
if (displayThreadStatus) {
sendingThreads.decrementAndGet();
idleThreads.incrementAndGet();
}
// reiceive:
synchronized (layer1) {
try {
layer1socket.receive(_rcvPkt);
if (displayPacketThroughput || displayByteThroughput)
StatisticsRecorder.addRequestThroughputRecord(_rcvPkt.getLength(), threadId);
} catch (IOException e) {
if(EncDnsServer.verbosity >= 1) {
System.err.println("Failed to receive message:");
e.printStackTrace();
}
continue;
}
}
if(EncDnsServer.verbosity >= 1) {
System.out.println("Received message");
}
if (displayThreadStatus) {
idleThreads.decrementAndGet();
decryptingThreads.incrementAndGet();
}
// Copy received query into a byte[]
byte[] rcvDNS = new byte[_rcvPkt.getLength()];
System.arraycopy(_rcvPkt.getData(), _rcvPkt.getOffset(), rcvDNS, 0,
_rcvPkt.getLength());
int qNameEnd = EncDNSHelper.findQuestionNameEnd(rcvDNS);
byte[] decMsg;
if(_encryption) {
// If encryption is enabled, decrypt the message
decMsg = decryptQuery(rcvDNS, qNameEnd);
} else {
// else just pass the unencrypted message on
decMsg = Arrays.copyOf(rcvDNS, rcvDNS.length);
}
if (decMsg == null) {
// If the decrypted message is null, decryption failed (probably due to a manipulated message)
if(EncDnsServer.verbosity >= 1) {
System.out.println("Decryption of EncDNS message failed.");
}
continue;
}
if (displayThreadStatus) {
decryptingThreads.decrementAndGet();
resolvingThreads.incrementAndGet();
}
byte[] ansReply = layer5.sendQueryAndListenForReply(decMsg);
if (displayThreadStatus) {
resolvingThreads.decrementAndGet();
encryptingThreads.incrementAndGet();
}
byte[] qID = Arrays.copyOfRange(decMsg, 0, 2);
byte[] rID = (ansReply == null) ? null : Arrays.copyOfRange(ansReply, 0, 2);
byte[] encReply;
// Check for empty responses and compare query and response IDs
if (ansReply != null && Arrays.equals(qID, rID)) {
if(_encryption) {
// If encryption is enabled, encrypt the response
encReply = encryptResponse(rcvDNS, qNameEnd, ansReply, _k);
} else {
// Else just pass the unencrypted response on
encReply = Arrays.copyOf(ansReply, ansReply.length);
}
} else {
// If we received an empty response or the response ID does not
// match the query ID, something went wrong -> generate a
// SERVFAIL message
if(_encryption) {
encReply = encryptResponse(rcvDNS, qNameEnd, EncDNSHelper.generateServfail(rcvDNS), _k);
} else {
encReply = EncDNSHelper.generateServfail(rcvDNS);
}
}
if (displayThreadStatus) {
encryptingThreads.decrementAndGet();
sendingThreads.incrementAndGet();
}
// Send response to local recursive nameserver
DatagramPacket sendPacket = new DatagramPacket(encReply, encReply.length, _rcvPkt.getAddress(), _rcvPkt.getPort());
//synchronized (_udpSock53) {
try {
if (displayPacketThroughput || displayByteThroughput)
StatisticsRecorder.addReplyThroughputRecord(encReply.length, threadId);
layer1socket.send(sendPacket);
} catch (IOException e) {
if(EncDnsServer.verbosity >= 1) {
System.err.println("Failed to send message:");
System.err.println(e);
}
}
//}
}
}
/**
* Decrypts a query.
* @param rcv encrypted query
* @param qNameEnd position of the question name's end
* @return the decrypted query
*/
private byte[] decryptQuery(byte[] rcv, int qNameEnd) {
// expected start position of EncDNS zone name
int msgZoneNameStart = qNameEnd + 1 - _zoneName.length;
// check whether the EncDNS zone name in the query is correct
byte[] rcvZoneName = Arrays.copyOfRange(rcv, msgZoneNameStart, qNameEnd + 1);
if ((msgZoneNameStart > rcv.length) || (!Arrays.equals(_zoneName, rcvZoneName))) {
if(EncDnsServer.verbosity >= 1) {
System.err.println("DNS message was not destined for this server! " +new String(rcvZoneName) +" != " +new String(_zoneName));
}
return null;
}
// check for the magic string
if (rcv[12] < _magicString.length || (!Arrays.equals(_magicString, Arrays.copyOfRange(rcv, 13, 13 + _magicString.length)))) {
if(EncDnsServer.verbosity >= 1) {
System.err.println("Message not in EncDNS format!");
System.err.println(rcv[12] +" <? " +_magicString.length);
System.err.println(Util.toHex(_magicString) +" ==? " +Util.toHex(Arrays.copyOfRange(rcv, 13, 13 + _magicString.length)));
}
return null;
}
// copy the crypto information from the query
ArrayList<Byte> cryptList = new ArrayList<Byte>();
for (int mpos = 12; mpos < msgZoneNameStart;) {
int llen = rcv[mpos];
mpos++;
for (int i = 0; i < llen; i++) {
cryptList.add(rcv[mpos]);
mpos++;
}
}
byte[] cryptoStuff = ArrayUtils.toPrimitive(cryptList.toArray(new Byte[0]));
// split cryptographic information
int pkAndNonceEnd = _magicString.length + _libSodium.PKBYTES + (_libSodium.NONCEBYTES / 2);
byte[] rpk = Arrays.copyOfRange(cryptoStuff, _magicString.length, _magicString.length + _libSodium.PKBYTES);
byte[] n = new byte[_libSodium.NONCEBYTES];
System.arraycopy(cryptoStuff, _magicString.length + _libSodium.PKBYTES, n, 0, _libSodium.NONCEBYTES / 2);
byte[] cbox = Arrays.copyOfRange(cryptoStuff, pkAndNonceEnd, cryptoStuff.length);
// check whether we've got the corresponding intermediate shared
// secret in the cache
EncDNSKey rpkobj = new EncDNSKey(rpk);
_k = _cache.get(rpkobj);
if (_k == null) {
if(EncDnsServer.verbosity >= 1) {
System.out.println("Key not found in cache. Calculating intermediate shared secret...");
}
_k = _libSodium.cryptoBoxBeforenm(rpk, _sk);
_cache.put(rpkobj, _k);
} else {
if(EncDnsServer.verbosity >= 1) {
System.out.println("Key found in cache.");
}
}
// decrypt and return the query
byte[] decCbox = _libSodium.openCryptoBoxAfternm(cbox, _k, n);
return decCbox;
}
}
/**
* Encrypts a standard DNS response.
* @param encQuery EncDNS query
* @param qNameEnd position of the question name's end
* @param response standard DNS response to encrypt
* @param k intermediate shared secret to use for encryption
* @return the encrypted EndDNS response containing the specified standard DNS response
*/
private byte[] encryptResponse(byte[] encQuery, int qNameEnd, byte[] response, byte[] k) {
// TODO This uses 65 kB of memory even if the resultig message is a lot
// smaller ... :-(
ByteBuffer buf = ByteBuffer.allocate(EncDnsServer.MAX_MSG_SIZE);
// --- DNS HEADER ---
// ID must match the query ID
byte[] id = Arrays.copyOfRange(encQuery, 0, 2);
buf.put(id);
buf.put((byte) 0x81); // QR=1 (Response), OpCode=0000 (Standard), AA=0, TC=0, RD=1
buf.put((byte) 0); // RA=0, Z,AD,CD=0, RCode=0000 (no error)
buf.putShort((short) 1); // question count = 1
buf.putShort((short) 1); // answer count = 1
buf.putShort((short) 0); // authority count = 0
buf.putShort((short) 0); // additional count = 0
// --- DNS QUESTION SECTION ---
// copy the question from the query
buf.put(Arrays.copyOfRange(encQuery, 12, qNameEnd + 5));
// --- DNS ANSWER SECTION ---
buf.put((byte) 0xc0); // c00c is the address of the question sections name
buf.put((byte) 0x0c);
buf.putShort((short) 16); // TYPE=TXT
buf.putShort((short) 1); // CLASS = IN
buf.putInt(0); // TTL = 0
// copy client nonce
byte[] nonce = new byte[_libSodium.NONCEBYTES];
System.arraycopy(encQuery, 13 + _magicString.length + _libSodium.PKBYTES, nonce, 0, _libSodium.NONCEBYTES / 2);
// Generate and add server nonce: 8 byte timer+4 byte random value
byte[] sn = new byte[_libSodium.NONCEBYTES/2];
ByteBuffer timeBuffer = ByteBuffer.allocate(8);
timeBuffer.putLong(System.nanoTime());
byte[] time = timeBuffer.array();
byte[] random = new byte[(_libSodium.NONCEBYTES/2)-time.length];
EncDnsServer.rnd.nextBytes(random);
System.arraycopy(time, 0, sn, 0, time.length);
System.arraycopy(random, 0, sn, time.length, random.length);
System.arraycopy(sn, 0, nonce, _libSodium.NONCEBYTES/2, _libSodium.NONCEBYTES/2);
// encrypt the message
byte[] cbox = _libSodium.makeCryptoBoxAfternm(response, k, nonce);
byte[] cryptoStuff = ArrayUtils.addAll(nonce, cbox);
// The contents of a TXT RR must be split into <character-string>s
// of a length of less than 256 bytes. Each of these has a length byte,
// so we'll need space for those in the RR.
short txtlen = (short) (cryptoStuff.length + (cryptoStuff.length / 255) + 1);
buf.putShort(txtlen);
// Put nonce and cbox into TXT RR.
int cpos = 0;
while (true) {
if ((cpos + 255) < cryptoStuff.length) {
// remaining part > 255 byte -> add 255-byte <character-string>
buf.put((byte) 255);
buf.put(cryptoStuff, cpos, 255);
cpos += 255;
} else {
// remaining part of cbox <= 63 byte -> add remaining part
short remainingBytes = (short) (cryptoStuff.length - cpos);
buf.put((byte) remainingBytes);
buf.put(cryptoStuff, cpos, remainingBytes);
break;
}
}
// TODO We may want to copy the payload size from the request. As the
// value is fixed in the current client implementation, we'll just
// assume the fixed value here. Also, there is currently no check
// to prevent messages exceeding this size to be sent.
// --- DNS ADDITIONAL SECTION - OPT PSEUDO-RR ---
buf.put((byte) 0); // Domain name = root
buf.putShort((short) 41); // RR TYPE=OPT(41)
buf.putShort((short) EncDnsServer.MAX_MSG_SIZE); // sender max UDP payload=65535
buf.put((byte) 0); // RCode extension=0
buf.put((byte) 0); // EDNS version=0
buf.put((byte) 0); // DNSSEC OK=0, Z=0
buf.put((byte) 0); // Z=0
buf.putShort((short) 0); // RDATA length
// convert to byte[]
byte[] out = new byte[buf.position()];
buf.position(0);
buf.get(out);
return out;
}
@Override
public Request generateDummy(int[] route, User user) {
throw new RuntimeException("not supported");
}
@Override
public Request generateDummy(User user) {
throw new RuntimeException("not supported");
}
@Override
public Reply generateDummyReply(int[] route, User user) {
throw new RuntimeException("not supported");
}
@Override
public Reply generateDummyReply(User user) {
throw new RuntimeException("not supported");
}
}