|
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; |
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |