/*******************************************************************************
* 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 staticContent.evaluation.encDnsPingTool;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingOptionException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.DirectSchedulerFactory;
import userGeneratedContent.testbedPlugIns.layerPlugIns.layer2recodingScheme.encDNS_v0_001.EncDNSHelper;
/**
* The DNSPing utility will send queries to a specified nameserver and measure
* the time it takes for a corresponding answer to arrive. It will then output
* minimum, maximum, average, 25th, 50th (median) and 75th percentile of the
* round trip times.
*
* @author Jens Lindemann
*/
public class DNSPing {
/** maximum DNS message size */
public static final int MAX_MSG_SIZE = 65535;
private final int NUM_QUERIES;
/** interval between queries in msecs */
private final int INTERVAL;
/** timeout for queries */
protected final int TIMEOUT;
/** number of queries to be ignored at start of measurement */
private final int IGNORE;
private int _qSent;
private int _qErr;
private int _qTimeout;
private int _repliesRcv;
private ArrayList<Long> _rtts;
//private ArrayList<Long> _sendTimes;
private int _ignore;
private byte[] _qname;
private short _qtype;
private short _qclass;
private int _qNum;
/** target nameserver's address/IP */
protected final InetAddress _targetNSAddress;
/** target nameserver's UDP port */
protected final short _targetNSPort;
private Random _rnd;
private static Options _opt;
/**
* Constructor for DNSPing class
* @param numQueries number of queries to send
* @param interval interval between individual queries (in ms)
* @param ignore number of queries to ignore at start of measurement
* @param ns nameserver's address / IP
* @param port nameserver's UDP port
* @param timeout timeout for queries
* @param qname query name (will be prefixed by a counter to avoid caching by recursive nameservers)
* @param qtype query type
* @param qclass query class
* @throws UnknownHostException @see UnknownHostException will be thrown if nameserver's address / IP is unknown
*/
public DNSPing(int numQueries, int interval, int ignore, String ns, int port, int timeout, byte[] qname, short qtype, short qclass) throws UnknownHostException {
NUM_QUERIES = numQueries;
INTERVAL = interval;
IGNORE = ignore;
_ignore = IGNORE;
// initialize counters
_qSent = 0;
_qErr = 0;
_qTimeout = 0;
_repliesRcv = 0;
TIMEOUT = timeout;
_rtts = new ArrayList<Long>(NUM_QUERIES);
//_sendTimes = new ArrayList<Long>(NUM_QUERIES);
_rnd = new Random();
_targetNSAddress = InetAddress.getByName(ns);
_targetNSPort = (short)port;
_qname = qname;
_qtype = qtype;
_qclass = qclass;
_qNum = 0;
sendQueryQuartz();
}
/**
* Increments the counter for number of queries sent.
*/
protected synchronized void querySent(long time) {
_qSent++;
//_sendTimes.add(time);
}
/**
* Increments the counter for number of responses received and records round
* trip time
* @param tdiff round trip time (nanoseconds)
*/
protected synchronized void responseReceived(long tdiff) {
_repliesRcv++;
_rtts.add(tdiff);
}
/**
* Increments the counter for number of query errros.
*/
protected synchronized void queryError() {
_qErr++;
}
/**
* Increments the counter for number of query timeouts.
*/
protected synchronized void queryTimedOut() {
_qTimeout++;
}
/**
* This method will be called when a new SendJob is being started. It will
* return to the caller whether the query is to be ignored for measurement
* purposes and will decrement the ignore counter.
*
* @return true if query is to be ignored, false otherwise
*/
protected synchronized boolean reportSendJobStart() {
if(_ignore == 0) {
return false;
} else {
_ignore--;
return true;
}
}
/**
* Sends the queries using the Quartz scheduling framework.
*/
private void sendQueryQuartz() {
try {
DirectSchedulerFactory df = DirectSchedulerFactory.getInstance();
df.createVolatileScheduler((TIMEOUT/INTERVAL)+2); // create sufficient number of threads to be able to handle all queries, even if all of them time out
Scheduler sched = df.getScheduler();
sched.start();
// Create a JobDataMap and put a reference to this class into it,
// so the jobs can access it.
JobDataMap jdm = new JobDataMap();
jdm.put("mainclassobj", this);
JobDetail job = JobBuilder.newJob(SendJob.class)
.withIdentity("sendJob")
.usingJobData(jdm)
.build();
// Create a trigger which will automatically start the SendJobs
// NUM_QUERIES times with INTERVAL ms between calls.
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("sendTrigger")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMilliseconds(INTERVAL)
.withRepeatCount(NUM_QUERIES-1))
.build();
sched.scheduleJob(job, trigger);
// Wait until all queries have finished or timed out.
Thread.sleep((NUM_QUERIES*INTERVAL)+1000);
while((_qSent < NUM_QUERIES) || ((_repliesRcv + _qErr + IGNORE) < NUM_QUERIES)) {
Thread.sleep(1000);
}
sched.shutdown();
// Calculate and output statistics.
System.out.println("Queries sent: " + _qSent);
System.out.println("Queries ignored: " + IGNORE);
System.out.println("Replies received: " + _repliesRcv);
System.out.println("Queries timed out: " + _qTimeout);
System.out.println("Query errors: " + _qErr);
Collections.sort(_rtts);
double min = (double)_rtts.get(0)/1000000;
double max = (double)_rtts.get(_rtts.size()-1)/1000000;
double median = (double)_rtts.get((int)Math.round(0.5*(_rtts.size()-1)))/1000000;
double perc25 = (double)_rtts.get((int)Math.round(0.25*(_rtts.size()-1)))/1000000;
double perc75 = (double)_rtts.get((int)Math.round(0.75*(_rtts.size()-1)))/1000000;
double[] dblRTTs = new double[_rtts.size()];
for(int i = 0; i < _rtts.size(); i++) {
dblRTTs[i] = _rtts.get(i);
}
Mean meanCalc = new Mean();
double mean = meanCalc.evaluate(dblRTTs)/1000000;
StandardDeviation sdCalc = new StandardDeviation();
double sd = sdCalc.evaluate(dblRTTs)/1000000;
System.out.println("Min RTT: " + min + " ms");
System.out.println("Max RTT: " + max + " ms");
System.out.println("RTT Median: " + median + " ms");
System.out.println("RTT 25-percentile: " + perc25 + " ms");
System.out.println("RTT 75-percentile: " + perc75 + " ms");
System.out.println("Mean RTT: " + mean + " ms");
System.out.println("RTT SD: " + sd + " ms");
/*try {
// TODO Write to file
PrintWriter out = new PrintWriter("dnsping.stats");
out.println("Min RTT: " + min + " ms");
out.println("Max RTT: " + max + " ms");
out.println("RTT Median: " + median + " ms");
out.println("RTT 25-percentile: " + perc25 + " ms");
out.println("RTT 75-percentile: " + perc75 + " ms");
out.println("Mean RTT: " + mean + " ms");
out.println("RTT SD: " + sd + " ms");
out.close();
out = new PrintWriter("dnsping.sendtimes");
for(int i = 0; i < _sendTimes.size(); i++) {
out.println(_sendTimes.get(i));
}
out.close();
out = new PrintWriter("dnsping.rtt");
for(int i = 0; i < _rtts.size(); i++) {
out.println(_rtts.get(i));
}
out.close();
} catch (FileNotFoundException ex) {
// TODO error handling
System.err.println(ex);
}*/
} catch(SchedulerException e) {
System.err.println(e);
} catch(InterruptedException e) {
System.err.println(e);
}
}
/**
* Builds the query message
* @param qName query name
* @param type query type
* @param qClass query class
* @return query message
*/
private byte[] buildQuery(byte[] qName, short type, short qClass) {
ByteBuffer buf = ByteBuffer.allocate(MAX_MSG_SIZE);
// --- DNS HEADER ---
// Choose random ID
byte[] id = new byte[2];
_rnd.nextBytes(id);
buf.put(id);
buf.put((byte) 1); // QR=0 (Query), OpCode=0000 (Query), AA=0, TC=0, RD=1
buf.put((byte) 0); // RA,Z,AD,CD,RCode=0
buf.putShort((short) 1); // question count = 1
buf.putShort((short) 0); // answer count = 0
buf.putShort((short) 0); // authority count = 0
buf.putShort((short) 0); // additional count = 0
// --- DNS QUESTION SECTION ---
buf.put(qName); // NAME
buf.putShort(type); // TYPE
buf.putShort(qClass); // CLASS
// convert to byte[]
byte[] out = new byte[buf.position()];
buf.position(0);
buf.get(out);
return out;
}
/**
* Creates a new query message with a unique prefix.
* @return query message
*/
public byte[] getQueryBytes() {
byte[] label1length = {36};
byte[] label1 = ("a123456789a123456789a123456789a12345".getBytes());
byte[] prefix = new byte[19];
prefix[0] = 18;
prefix[1] = 'a';
prefix[2] = '1';
prefix[3] = '2';
prefix[4] = '3';
prefix[5] = '4';
prefix[6] = '5';
prefix[7] = '6';
prefix[8] = '7';
prefix[9] = '8';
prefix[10] = 'w';
String numstr = String.format("%08d", _qNum);
if(_qNum>=99999999) {
_qNum = 0;
} else {
_qNum++;
}
for(int i = 11; i < prefix.length; i++) {
prefix[i] = (byte)numstr.charAt(i-11);
}
byte[] qname = ArrayUtils.addAll(prefix, label1length);
qname = ArrayUtils.addAll(qname, label1);
//qname = ArrayUtils.addAll(qname, label1length);
//qname = ArrayUtils.addAll(qname, label1);
qname = ArrayUtils.addAll(qname, _qname);
//byte[] qname = ArrayUtils.addAll(prefix, _qname);
byte[] query = buildQuery(qname, _qtype, _qclass);
return query;
}
public static void main(String[] args) {
try {
_opt = new Options();
_opt.addOption("h", "help", false, "prints this message");
_opt.addOption("n", "queries", true, "number of queries (default: 100)");
_opt.addOption("i", "interval", true, "interval between queries (msecs) (default: 100)");
_opt.addOption("w", "ignore", true, "number of queries to ignore at start of measurement (default: 0)");
_opt.addOption("a", "nameserver", true, "nameserver's IP address or hostname (default: 127.0.0.1)");
_opt.addOption("p", "port", true, "nameserver's port (default: 53)");
_opt.addOption("t", "timeout", true, "query timeout (msecs) (default: 5000)");
Option qnameOpt = new Option("q", "qname", true, "query name suffix (will be prefixed with wnnnnnnnn.) (REQUIRED)");
qnameOpt.setRequired(true);
_opt.addOption(qnameOpt);
_opt.addOption("T", "qtype", true, "query type (QTYPE) (default: 1 (A))");
_opt.addOption("c", "qclass", true, "query class (default: 1 (IN))");
Options helpOpts = new Options();
helpOpts.addOption("h", "help", false, "prints this message");
CommandLineParser parser = new BasicParser();
// We need to check for the help parameter first, as parsing all
// simultaneously will throw an exception if a required option
// is missing.
CommandLine cmd = parser.parse(helpOpts, args, true);
if (cmd.hasOption("help")) {
HelpFormatter hf = new HelpFormatter();
hf.printHelp("DNSPing", _opt);
return;
}
// Now we can parse all options
cmd = parser.parse(_opt, args);
// Get parameters from CLI
String numQueriesStr = cmd.getOptionValue("n");
String intervalStr = cmd.getOptionValue("i");
String ignoreStr = cmd.getOptionValue("w");
String nsStr = cmd.getOptionValue("a");
String portStr = cmd.getOptionValue("port");
String timeoutStr = cmd.getOptionValue("timeout");
String qnameStr = cmd.getOptionValue("q");
String qtypeStr = cmd.getOptionValue("qtype");
String qclassStr = cmd.getOptionValue("c");
// Set values from CLI or use defaults, if not present
int numQueries = 100;
if(numQueriesStr != null) numQueries = Integer.parseInt(numQueriesStr);
int interval = 100;
if(intervalStr != null) interval = Integer.parseInt(intervalStr);
int ignore = 0;
if(ignoreStr != null) ignore = Integer.parseInt(ignoreStr);
if(nsStr == null) nsStr = "127.0.0.1";
int port = 53;
if(portStr != null) port = Integer.parseInt(portStr);
int timeout = 5000;
if(timeoutStr != null) timeout = Integer.parseInt(timeoutStr);
short qtype = 1;
// This must be parsed as an Integer as otherwise values having a
// highest-value bit of 1 would be considered out of range
if(qtypeStr != null) {
int qtypeInt = Integer.parseInt(qtypeStr);
if(qtypeInt>65535) {
System.err.println("Query type must be <= 65535!");
return;
}
qtype = (short)qtypeInt;
}
short qclass = 1;
if (qclassStr != null) qclass = Short.parseShort(qclassStr);
byte[] qname = EncDNSHelper.parseZoneNameString(qnameStr);
new DNSPing(numQueries, interval, ignore, nsStr, port, timeout, qname, qtype, qclass);
} catch (MissingOptionException e) {
// Print error message and help if required option is missing
System.out.println("Missing required option: " + e.getMessage());
HelpFormatter hf = new HelpFormatter();
hf.printHelp("DNSPing", _opt);
} catch (UnknownHostException ex) {
// Print error message if nameserver address/IP is invalid
System.err.println("Invalid destination address!");
} catch (ParseException e) {
System.err.println(e);
}
}
}