Skip to content

Instantly share code, notes, and snippets.

@mon-compte-github
Created February 26, 2021 12:43
Show Gist options
  • Save mon-compte-github/6636c4b3ec33871bb495de1153f5d411 to your computer and use it in GitHub Desktop.
Save mon-compte-github/6636c4b3ec33871bb495de1153f5d411 to your computer and use it in GitHub Desktop.
Quick & Dirty DNS Client

DNSClient

Quick and dirty implementation of a simple DNSClient. Uncomment DNS_SERVER_ADDRESS to change the DNS server (root, domain or recursive).

Run

When querying a standard DNS server (1.1.1.1 for example).

% javac DNSClient.java && java DNSClient www.heroku.com
Found 3 answer(s)
 * www.heroku.com. - CNAME : mysterious-spire-9566.polar-eyrie-3327.herokuspace.com.
 * mysterious-spire-9566.polar-eyrie-3327.herokuspace.com. - IP : 54.156.19.143
 * mysterious-spire-9566.polar-eyrie-3327.herokuspace.com. - IP : 34.197.145.217

% java DNSClient www.mit.edu   
Found 3 answer(s)
 * www.mit.edu. - CNAME : www.mit.edu.edgekey.net.
 * www.mit.edu.edgekey.net. - CNAME : e9566.dscb.akamaiedge.net.
 * e9566.dscb.akamaiedge.net. - IP : 23.43.211.181

When querying a root DNS server :

$ java DNSClient com           
Found 13 authoritative nameserver(s)
 * com. - NS : a.gtld-servers.net.
 * com. - NS : b.gtld-servers.net.
 * com. - NS : c.gtld-servers.net.
 * com. - NS : d.gtld-servers.net.
 * com. - NS : e.gtld-servers.net.
 * com. - NS : f.gtld-servers.net.
 * com. - NS : g.gtld-servers.net.
 * com. - NS : h.gtld-servers.net.
 * com. - NS : i.gtld-servers.net.
 * com. - NS : j.gtld-servers.net.
 * com. - NS : k.gtld-servers.net.
 * com. - NS : l.gtld-servers.net.
 * com. - NS : m.gtld-servers.net.
Found 15 additional record(s)
 * a.gtld-servers.net. - IP : 192.5.6.30
 * b.gtld-servers.net. - IP : 192.33.14.30
 * c.gtld-servers.net. - IP : 192.26.92.30
 * d.gtld-servers.net. - IP : 192.31.80.30
 * e.gtld-servers.net. - IP : 192.12.94.30
 * f.gtld-servers.net. - IP : 192.35.51.30
 * g.gtld-servers.net. - IP : 192.42.93.30
 * h.gtld-servers.net. - IP : 192.54.112.30
 * i.gtld-servers.net. - IP : 192.43.172.30
 * j.gtld-servers.net. - IP : 192.48.79.30
 * k.gtld-servers.net. - IP : 192.52.178.30
 * l.gtld-servers.net. - IP : 192.41.162.30
 * m.gtld-servers.net. - IP : 192.55.83.30
 * a.gtld-servers.net. - AAAA : 2001:503:a83e:0:0:0:2:30
 * b.gtld-servers.net. - AAAA : 2001:503:231d:0:0:0:2:30

Protocol

DNS protocol is very simple, the only tricky part is the (de)compression algorithm. Implementation provided here is reaaaally ugly ... but runs properly ;-). See https://www2.cs.duke.edu/courses/fall16/compsci356/DNS/DNS-primer.pdf for explanations.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class DNSClient {
private static final int A_TYPE = 1;
private static final int NS_TYPE = 2;
private static final int CNAME_TYPE = 5;
private static final int SOA_TYPE = 6;
private static final int MX_TYPE = 15;
private static final int TXT_TYPE = 16;
private static final int AAAA_TYPE = 28;
// cloudflare
private static final String DNS_SERVER_ADDRESS = "1.1.1.1";
// sfr
//private static final String DNS_SERVER_ADDRESS = "89.2.0.1";
// root server - when querying a root server with a fqdn, only the tld is taken into account
//private static final String DNS_SERVER_ADDRESS = "198.41.0.4";
// e.nic.fr - when querying a tld server with a fqdn, only the tld and domain are taken into account
//private static final String DNS_SERVER_ADDRESS = "193.176.144.22";
private static final int DNS_SERVER_PORT = 53;
//
// Helpers
//
/**
* Reads string from buffer, starting at offset.
* Last character can be '\0' or a pointer.
* @returns The whole string.
*/
private static String readStringAt(byte[] buffer, int offset) {
String result = "";
byte b = buffer[offset++];
while(b != 0) {
if((b & 0xC0) == 0xC0) {
// it's a pointer => inception !
offset = (b & 0x3F) << 8 | buffer[offset++];
} else {
// immediate value, b is string length
result += new String(buffer, offset, b, StandardCharsets.UTF_8) + ".";
offset += b;
}
b = buffer[offset++];
}
return result;
}
/**
* Reads a string from stream. Can be a literal value,
* a pointer or a literal ending with a pointer.
*/
private static String readStringFrom(DataInputStream din, byte[] buffer) throws IOException {
String result = "";
byte b = din.readByte();
while(b != 0) {
if((b & 0xC0) == 0xC0) {
// it's a pointer !
int offset = ((b & 0x3F) << 8 | (0xFF & din.readByte()));
result += readStringAt(buffer, offset);
b = 0; // break
} else {
byte[] temp = new byte[b];
din.read(temp);
result += new String(temp, StandardCharsets.UTF_8) + ".";
b = din.readByte();
}
}
return result;
}
//
// main
//
public static void main(String[] args) throws IOException {
if(args.length == 0) {
System.err.println("usage: " + DNSClient.class.getName() + " domain");
return;
}
final InetAddress ipAddress = InetAddress.getByName(DNS_SERVER_ADDRESS);
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
final DataOutputStream dos = new DataOutputStream(baos);
// DataOutputStream est Big Endian, ça tombe bien
// puisque c'est en network byte order ^^
// l'identifiant va permettre de relier la réponse à la requête
// ici on peut utiliser une valeur en dur puisqu'une seule requête
final short uniqId = 1337;
// ID
dos.writeShort(uniqId);
// FLAGS
short flags = 0;
// QR 0 = query, 1 = response
flags |= (0 << 15);
// OP 0 = std query
flags |= (0 << 11);
// AA (reponse only)
// TR (truncated)
// RD 1 = recursion desired
flags |= (1 << 8);
// RA (response only)
// Z (reserved)
// RCODE (response only)
dos.writeShort(flags);
// QDCOUNT
dos.writeShort(0x01);
// ANCOUNT
dos.writeShort(0x00);
// NSCOUNT
dos.writeShort(0x00);
// ARCOUNT
dos.writeShort(0x00);
// QUESTION
// we should use compression here :-S
// QNAME
final String[] parts = args[0].split("\\.");
for(String part : parts) {
dos.writeByte(part.length());
final byte[] str = part.getBytes(StandardCharsets.UTF_8);
dos.write(str, 0, str.length);
}
dos.writeByte(0x00);
// QTYPE
dos.writeShort(A_TYPE);
// QCLASS (1=Internet)
dos.writeShort(0x01);
final byte[] dnsFrame = baos.toByteArray();
// --- SEND --- >
final DatagramSocket socket = new DatagramSocket();
final DatagramPacket dnsReqPacket = new DatagramPacket(dnsFrame, dnsFrame.length, ipAddress, DNS_SERVER_PORT);
socket.send(dnsReqPacket);
// (><) --------- (><)
// ->---- (><) / \
// \ / \
// (><) -------- (><) \
// (><)------->--
// <--- RECV ---
// https://serverfault.com/questions/587625/why-dns-through-udp-has-a-512-bytes-limit
// Why DNS through UDP has a 512 bytes limit?
final byte[] buf = new byte[512];
final DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
final DataInputStream din = new DataInputStream(new ByteArrayInputStream(buf));
if(din.readShort() != uniqId) { /* ... */ }
final short respFlags = din.readShort();
// maybe we should check the truncated flag ¯\_(ツ)_/¯
if((respFlags & 0x07) != 0) {
System.err.println("Erreur " + (respFlags & 0x07) + " ?!? ");
} else {
final int qdCount = din.readShort();
final int anCount = din.readShort();
final int nsCount = din.readShort();
final int arCount = din.readShort();
// skip queries
for(int k=0; k<qdCount; k++) {
// skip queries (qname, qtype, qclass)
String dummy = readStringFrom(din, buf);
din.readShort();
din.readShort();
}
// record format : [ NAME | TYPE | CLASS | TTL | RDLENGTH | RDATA ]
// The compression scheme allows a domain name in a message to be represented as either:
// • a sequence of labels ending in a zero octet
// • a pointer
// • a sequence of labels ending with a pointer
for(int k=0; k<(anCount + nsCount + arCount); k++) {
if(anCount != 0 && k == 0) {
System.out.println("Found " + anCount + " answer(s)");
} else if(nsCount != 0 && k == anCount) {
System.out.println("Found " + nsCount + " authoritative nameserver(s)");
} else if(arCount != 0 && k == (anCount + nsCount)) {
System.out.println("Found " + arCount + " additional record(s)");
}
String domain = readStringFrom(din, buf);
short recordType = din.readShort();
// class is always 1 (internet)
short clazz = din.readShort();
int ttl = din.readInt();
short dataLen = din.readShort();
// rdata depends on type
switch(recordType) {
case A_TYPE:
final int ip = din.readInt();
System.out.println(" * " + domain + " - IP : " + String.format("%d.%d.%d.%d",
(ip >> 24 & 0xff), (ip >> 16 & 0xff), (ip >> 8 & 0xff), (ip >> 0 & 0xff)));
break;
case NS_TYPE:
final String hostname = readStringFrom(din, buf);
System.out.println(" * " + domain + " - NS : " + hostname);
break;
case CNAME_TYPE:
final String cname = readStringFrom(din, buf);
System.out.println(" * " + domain + " - CNAME : " + cname);
break;
case SOA_TYPE:
final String primaryNameServer = readStringFrom(din, buf);
final String mailbox = readStringFrom(din, buf);
din.readInt(); // serial number
din.readInt(); // refresh interval
din.readInt(); // retry interval
din.readInt(); // expire limit
din.readInt(); // minimum ttl
System.out.println(" * " + domain + " - SOA : " + primaryNameServer + "; " + mailbox);
break;
case MX_TYPE:
final short preference = din.readShort();
final String mailServer = readStringFrom(din, buf);
System.out.println(" * " + domain + " - MX : " + mailServer);
break;
case TXT_TYPE:
final String text = readStringFrom(din, buf);
System.out.println(" * " + domain + " - TXT : " + text);
break;
case AAAA_TYPE:
System.out.print(" * " + domain + " - AAAA : ");
// how are multiple IPs stored ? concatenated ?
for(int m=0; m<(dataLen>>1); m++) {
System.out.print(String.format("%x", din.readShort()));
if(m % 8 != 7) System.out.print(":");
if(m > 0 && m % 8 == 0) {
System.out.print(" | ");
}
}
System.out.println();
break;
default:
System.err.println("Type #" + String.format("%x", recordType) + " not yet supported");
din.skipBytes(dataLen);
break;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment