30f22170ef
Reviewed-by: vtewari, rriggs
364 lines
12 KiB
Java
364 lines
12 KiB
Java
/*
|
|
* Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This code 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
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
import sun.security.util.HexDumpEncoder;
|
|
|
|
import java.io.IOException;
|
|
import java.net.DatagramPacket;
|
|
import java.net.DatagramSocket;
|
|
import java.net.InetAddress;
|
|
import java.net.SocketException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.file.Paths;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Scanner;
|
|
import java.util.regex.MatchResult;
|
|
|
|
/*
|
|
* A dummy DNS server.
|
|
*
|
|
* Loads a sequence of DNS messages from a capture file into its cache.
|
|
* It listens for DNS UDP requests, finds match request in cache and sends the
|
|
* corresponding DNS responses.
|
|
*
|
|
* The capture file contains an DNS protocol exchange in the hexadecimal
|
|
* dump format emitted by HexDumpEncoder:
|
|
*
|
|
* xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................
|
|
*
|
|
* Typically, DNS protocol exchange is generated by DNSTracer who captures
|
|
* communication messages between DNS application program and real DNS server
|
|
*/
|
|
public class DNSServer extends Thread implements Server {
|
|
|
|
public class Pair<F, S> {
|
|
private F first;
|
|
private S second;
|
|
|
|
public Pair(F first, S second) {
|
|
this.first = first;
|
|
this.second = second;
|
|
}
|
|
|
|
public void setFirst(F first) {
|
|
this.first = first;
|
|
}
|
|
|
|
public void setSecond(S second) {
|
|
this.second = second;
|
|
}
|
|
|
|
public F getFirst() {
|
|
return first;
|
|
}
|
|
|
|
public S getSecond() {
|
|
return second;
|
|
}
|
|
}
|
|
|
|
public static final int DNS_HEADER_SIZE = 12;
|
|
public static final int DNS_PACKET_SIZE = 512;
|
|
|
|
static HexDumpEncoder encoder = new HexDumpEncoder();
|
|
|
|
private DatagramSocket socket;
|
|
private String filename;
|
|
private boolean loop;
|
|
private final List<Pair<byte[], byte[]>> cache = new ArrayList<>();
|
|
private ByteBuffer reqBuffer = ByteBuffer.allocate(DNS_PACKET_SIZE);
|
|
private volatile boolean isRunning;
|
|
|
|
public DNSServer(String filename) throws SocketException {
|
|
this(filename, false);
|
|
}
|
|
|
|
public DNSServer(String filename, boolean loop) throws SocketException {
|
|
this.socket = new DatagramSocket(0, InetAddress.getLoopbackAddress());
|
|
this.filename = filename;
|
|
this.loop = loop;
|
|
}
|
|
|
|
public void run() {
|
|
try {
|
|
isRunning = true;
|
|
System.out.println(
|
|
"DNSServer: Loading DNS cache data from : " + filename);
|
|
loadCaptureFile(filename);
|
|
|
|
System.out.println(
|
|
"DNSServer: listening on port " + socket.getLocalPort());
|
|
|
|
System.out.println("DNSServer: loop playback: " + loop);
|
|
|
|
int playbackIndex = 0;
|
|
|
|
while (playbackIndex < cache.size()) {
|
|
DatagramPacket reqPacket = receiveQuery();
|
|
|
|
if (!verifyRequestMsg(reqPacket, playbackIndex)) {
|
|
if (playbackIndex > 0 && verifyRequestMsg(reqPacket,
|
|
playbackIndex - 1)) {
|
|
System.out.println(
|
|
"DNSServer: received retry query, resend");
|
|
playbackIndex--;
|
|
} else {
|
|
throw new RuntimeException(
|
|
"DNSServer: Error: Failed to verify DNS request. "
|
|
+ "Not identical request message : \n"
|
|
+ encoder.encodeBuffer(
|
|
Arrays.copyOf(reqPacket.getData(),
|
|
reqPacket.getLength())));
|
|
}
|
|
}
|
|
|
|
sendResponse(reqPacket, playbackIndex);
|
|
|
|
playbackIndex++;
|
|
if (loop && playbackIndex >= cache.size()) {
|
|
playbackIndex = 0;
|
|
}
|
|
}
|
|
|
|
System.out.println(
|
|
"DNSServer: Done for all cached messages playback");
|
|
|
|
System.out.println(
|
|
"DNSServer: Still listening for possible retry query");
|
|
while (true) {
|
|
DatagramPacket reqPacket = receiveQuery();
|
|
|
|
// here we only handle the retry query for last one
|
|
if (!verifyRequestMsg(reqPacket, playbackIndex - 1)) {
|
|
throw new RuntimeException(
|
|
"DNSServer: Error: Failed to verify DNS request. "
|
|
+ "Not identical request message : \n"
|
|
+ encoder.encodeBuffer(
|
|
Arrays.copyOf(reqPacket.getData(),
|
|
reqPacket.getLength())));
|
|
}
|
|
|
|
sendResponse(reqPacket, playbackIndex - 1);
|
|
}
|
|
} catch (Exception e) {
|
|
if (isRunning) {
|
|
System.err.println("DNSServer: Error: " + e);
|
|
e.printStackTrace();
|
|
} else {
|
|
System.out.println("DNSServer: Exit");
|
|
}
|
|
}
|
|
}
|
|
|
|
private DatagramPacket receiveQuery() throws IOException {
|
|
DatagramPacket reqPacket = new DatagramPacket(reqBuffer.array(),
|
|
reqBuffer.array().length);
|
|
socket.receive(reqPacket);
|
|
|
|
System.out.println("DNSServer: received query message from " + reqPacket
|
|
.getSocketAddress());
|
|
|
|
return reqPacket;
|
|
}
|
|
|
|
private void sendResponse(DatagramPacket reqPacket, int playbackIndex)
|
|
throws IOException {
|
|
byte[] payload = generateResponsePayload(reqPacket, playbackIndex);
|
|
socket.send(new DatagramPacket(payload, payload.length,
|
|
reqPacket.getSocketAddress()));
|
|
System.out.println("DNSServer: send response message to " + reqPacket
|
|
.getSocketAddress());
|
|
}
|
|
|
|
/*
|
|
* Load a capture file containing an DNS protocol exchange in the
|
|
* hexadecimal dump format emitted by sun.misc.HexDumpEncoder:
|
|
*
|
|
* xxxx: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff ................
|
|
*/
|
|
private void loadCaptureFile(String filename) throws IOException {
|
|
StringBuilder hexString = new StringBuilder();
|
|
String pattern = "(....): (..) (..) (..) (..) (..) (..) (..) (..) "
|
|
+ "(..) (..) (..) (..) (..) (..) (..) (..).*";
|
|
|
|
try (Scanner fileScanner = new Scanner(Paths.get(filename))) {
|
|
while (fileScanner.hasNextLine()) {
|
|
|
|
try (Scanner lineScanner = new Scanner(
|
|
fileScanner.nextLine())) {
|
|
if (lineScanner.findInLine(pattern) == null) {
|
|
continue;
|
|
}
|
|
MatchResult result = lineScanner.match();
|
|
for (int i = 1; i <= result.groupCount(); i++) {
|
|
String digits = result.group(i);
|
|
if (digits.length() == 4) {
|
|
if (digits.equals("0000")) { // start-of-message
|
|
if (hexString.length() > 0) {
|
|
addToCache(hexString.toString());
|
|
hexString.delete(0, hexString.length());
|
|
}
|
|
}
|
|
continue;
|
|
} else if (digits.equals(" ")) { // short message
|
|
continue;
|
|
}
|
|
hexString.append(digits);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
addToCache(hexString.toString());
|
|
}
|
|
|
|
/*
|
|
* Add an DNS encoding to the cache (by request message key).
|
|
*/
|
|
private void addToCache(String hexString) {
|
|
byte[] encoding = parseHexBinary(hexString);
|
|
if (encoding.length < DNS_HEADER_SIZE) {
|
|
throw new RuntimeException("Invalid DNS message : " + hexString);
|
|
}
|
|
|
|
if (getQR(encoding) == 0) {
|
|
// a query message, create entry in cache
|
|
cache.add(new Pair<>(encoding, null));
|
|
System.out.println(
|
|
" adding DNS query message with ID " + getID(encoding)
|
|
+ " to the cache");
|
|
} else {
|
|
// a response message, attach it to the query entry
|
|
if (!cache.isEmpty() && (getID(getLatestCacheEntry().getFirst())
|
|
== getID(encoding))) {
|
|
getLatestCacheEntry().setSecond(encoding);
|
|
System.out.println(
|
|
" adding DNS response message associated to ID "
|
|
+ getID(encoding) + " in the cache");
|
|
} else {
|
|
throw new RuntimeException(
|
|
"Invalid DNS message : " + hexString);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* ID: A 16 bit identifier assigned by the program that generates any
|
|
* kind of query. This identifier is copied the corresponding reply and
|
|
* can be used by the requester to match up replies to outstanding queries.
|
|
*/
|
|
private static int getID(byte[] encoding) {
|
|
return ByteBuffer.wrap(encoding, 0, 2).getShort();
|
|
}
|
|
|
|
/*
|
|
* QR: A one bit field that specifies whether this message is
|
|
* a query (0), or a response (1) after ID
|
|
*/
|
|
private static int getQR(byte[] encoding) {
|
|
return encoding[2] & (0x01 << 7);
|
|
}
|
|
|
|
private Pair<byte[], byte[]> getLatestCacheEntry() {
|
|
return cache.get(cache.size() - 1);
|
|
}
|
|
|
|
private boolean verifyRequestMsg(DatagramPacket packet, int playbackIndex) {
|
|
byte[] cachedRequest = cache.get(playbackIndex).getFirst();
|
|
return Arrays.equals(Arrays
|
|
.copyOfRange(packet.getData(), 2, packet.getLength()),
|
|
Arrays.copyOfRange(cachedRequest, 2, cachedRequest.length));
|
|
}
|
|
|
|
private byte[] generateResponsePayload(DatagramPacket packet,
|
|
int playbackIndex) {
|
|
byte[] resMsg = cache.get(playbackIndex).getSecond();
|
|
byte[] payload = Arrays.copyOf(resMsg, resMsg.length);
|
|
|
|
// replace the ID with same with real request
|
|
payload[0] = packet.getData()[0];
|
|
payload[1] = packet.getData()[1];
|
|
|
|
return payload;
|
|
}
|
|
|
|
public static byte[] parseHexBinary(String s) {
|
|
|
|
final int len = s.length();
|
|
|
|
// "111" is not a valid hex encoding.
|
|
if (len % 2 != 0) {
|
|
throw new IllegalArgumentException(
|
|
"hexBinary needs to be even-length: " + s);
|
|
}
|
|
|
|
byte[] out = new byte[len / 2];
|
|
|
|
for (int i = 0; i < len; i += 2) {
|
|
int h = hexToBin(s.charAt(i));
|
|
int l = hexToBin(s.charAt(i + 1));
|
|
if (h == -1 || l == -1) {
|
|
throw new IllegalArgumentException(
|
|
"contains illegal character for hexBinary: " + s);
|
|
}
|
|
|
|
out[i / 2] = (byte) (h * 16 + l);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
private static int hexToBin(char ch) {
|
|
if ('0' <= ch && ch <= '9') {
|
|
return ch - '0';
|
|
}
|
|
if ('A' <= ch && ch <= 'F') {
|
|
return ch - 'A' + 10;
|
|
}
|
|
if ('a' <= ch && ch <= 'f') {
|
|
return ch - 'a' + 10;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
@Override public void stopServer() {
|
|
isRunning = false;
|
|
if (socket != null) {
|
|
try {
|
|
socket.close();
|
|
} catch (Exception e) {
|
|
// ignore
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override public int getPort() {
|
|
if (socket != null) {
|
|
return socket.getLocalPort();
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
}
|