package javaforce.service;
/**
* Basic DNS Server
*
* Supports : A,CNAME,MX,AAAA
*
* Note : IP6 must be in full notation : xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
*
* @author pquiring
*
* Created : Nov 17, 2013
*/
import java.io.*;
import java.nio.*;
import java.net.*;
import java.util.*;
import javaforce.*;
import javaforce.jbus.*;
public class DNS extends Thread {
public final static String busPack = "net.sf.jfdns";
public static String getConfigFile() {
return JF.getConfigPath() + "/jfdns.cfg";
}
public static String getLogFile() {
return JF.getLogPath() + "/jfdns.log";
}
private DatagramSocket ds;
private static int maxmtu = 512; //standard
private String uplink;
private ArrayList<String> records = new ArrayList<String>();
public void run() {
JFLog.append(getLogFile(), false);
try {
loadConfig();
busClient = new JBusClient(busPack, new JBusMethods());
busClient.setPort(getBusPort());
busClient.start();
for(int a=0;a<5;a++) {
try {
ds = new DatagramSocket(53);
} catch (Exception e) {
if (a == 4) {
JFLog.log(e);
return;
}
JF.sleep(1000);
continue;
}
break;
}
while (true) {
byte data[] = new byte[maxmtu];
DatagramPacket packet = new DatagramPacket(data, maxmtu);
ds.receive(packet);
new Worker(packet).start();
}
} catch (Exception e) {
JFLog.log(e);
}
}
public void close() {
try {
ds.close();
} catch (Exception e) {}
}
enum Section {None, Global, Records};
private final static String defaultConfig
= "[global]\n"
+ "uplink=8.8.8.8\n"
+ "[records]\n"
+ "#name,type,ttl,value\n"
+ "#mydomain.com,cname,3600,www.mydomain.com\n"
+ "#www.mydomain.com,a,3600,192.168.0.2\n"
+ "#mydomain.com,mx,3600,50,mail.mydomain.com\n"
+ "#mail.mydomain.com,a,3600,192.168.0.3\n"
+ "#www.mydomain.com,aaaa,3600,1234:1234:1234:1234:1234:1234:1234:1234\n";
private void loadConfig() {
Section section = Section.None;
try {
BufferedReader br = new BufferedReader(new FileReader(getConfigFile()));
StringBuilder cfg = new StringBuilder();
while (true) {
String ln = br.readLine();
if (ln == null) break;
cfg.append(ln);
cfg.append("\n");
ln = ln.trim().toLowerCase();
int idx = ln.indexOf('#');
if (idx != -1) ln = ln.substring(0, idx).trim();
if (ln.length() == 0) continue;
if (ln.equals("[global]")) {
section = Section.Global;
continue;
}
if (ln.equals("[records]")) {
section = Section.Records;
continue;
}
switch (section) {
case Global:
if (ln.startsWith("uplink=")) {
uplink = ln.substring(7);
}
break;
case Records:
records.add(ln);
break;
}
}
config = cfg.toString();
} catch (FileNotFoundException e) {
//create default config
uplink = "8.8.8.8";
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(defaultConfig.getBytes());
fos.close();
config = defaultConfig;
} catch (Exception e2) {
JFLog.log(e2);
}
} catch (Exception e) {
JFLog.log(e);
}
}
//flags
private static final int QR = 0x8000; //1=response (0=query)
//4 bits opcode
private static final int OPCODE_MASK = 0x7800;
private static final int OPCODE_QUERY = 0x0000;
private static final int OPCODE_IQUERY = 0x4000; //(8)
private static final int OPCODE_UPDATE = 0x2800; //(5)
private static final int AA = 0x0400; //auth answer
private static final int TC = 0x0200; //truncated (if packet > 512 bytes)
private static final int RD = 0x0100; //recursive desired
private static final int RA = 0x0080; //recursive available
private static final int Z = 0x0040; //???
private static final int AD = 0x0020; //auth data???
private static final int CD = 0x0010; //checking disabled???
//4 bits result code (0=no error)
private static final int ERR_NO_ERROR = 0x0000; //no error
private static final int ERR_NO_SUCH_NAME = 0x0003; //404
private static final int A = 1;
private static final int CNAME = 5;
private static final int SOA = 6; //start of auth
private static final int PTR = 12;
private static final int MX = 15;
private static final int AAAA = 28;
private class Worker extends Thread {
private DatagramPacket packet;
private byte data[];
private int nameLength; //bytes used decoding name
private byte reply[];
private int replyOffset;
private ByteBuffer bb;
private ByteBuffer replyBuffer;
private short id, flgs;
private int opcode;
public Worker(DatagramPacket packet) {
this.packet = packet;
}
public void run() {
try {
data = packet.getData();
bb = ByteBuffer.wrap(data);
bb.order(ByteOrder.BIG_ENDIAN);
id = bb.getShort(0);
flgs = bb.getShort(2);
if ((flgs & QR) != 0) {
throw new Exception("response sent to server");
}
opcode = flgs & OPCODE_MASK;
switch (opcode) {
default:
throw new Exception("opcode not supported:" + opcode);
case OPCODE_IQUERY:
throw new Exception("inverse query not supported");
case OPCODE_UPDATE:
throw new Exception("update not supported");
case OPCODE_QUERY:
doQuery();
break;
}
} catch (Exception e) {
JFLog.log(e);
}
}
private void doQuery() throws Exception {
short cndQ = bb.getShort(4);
if (cndQ != 1) throw new Exception("only 1 question supported");
short cndA = bb.getShort(6);
if (cndA != 0) throw new Exception("query with answers?");
short cndS = bb.getShort(8);
if (cndS != 0) throw new Exception("query with auth names?");
short cndAdd = bb.getShort(10);
if (cndAdd != 0) throw new Exception("query with adds?");
int offset = 12;
for(int a=0;a < cndQ; a++) {
String domain = getName(data, offset);
offset += nameLength;
int type = bb.getShort(offset);
offset += 2;
int cls = bb.getShort(offset);
if (cls != 1) throw new Exception("only internet class supported");
offset += 2;
if (domain.endsWith(".in-addr.arpa")) {
//reverse IPv4 query (just send bogus info)
sendReply(domain, "*.in-addr.arpa,ptr,1440,localdomain", type, id);
continue;
}
if (domain.endsWith(".ip6.arpa")) {
//reverse IPv6 query (just send bogus info)
sendReply(domain, "*.ip6.arpa,ptr,1440,localdomain", type, id);
continue;
}
if (queryLocal(domain, type, id)) continue;
queryRemote(domain, type);
}
}
private void doUpdate() throws Exception {
}
private String getName(byte data[], int offset) {
StringBuilder name = new StringBuilder();
boolean jump = false;
nameLength = 0;
do {
if (!jump) nameLength++;
int length = ((int)data[offset++]) & 0xff;
if (length == 0) break;
if (length >= 0xc0) {
//pointer
if (!jump) nameLength++;
jump = true;
int newOffset = (length & 0x3f) << 8;
newOffset += data[offset] & 0xff;
offset = newOffset;
} else {
if (!jump) nameLength += length;
if (name.length() != 0) name.append(".");
name.append(new String(data, offset, length));
offset += length;
}
} while (true);
return name.toString();
}
private int encodeName(String domain) {
//TODO : compression
String p[] = domain.split("[.]");
int length = 0;
int strlen;
for(int a=0;a<p.length;a++) {
strlen = p[a].length();
reply[replyOffset++] = (byte)strlen;
length++;
System.arraycopy(p[a].getBytes(), 0, reply, replyOffset, strlen);
replyOffset += strlen;
length += strlen;
}
//zero length part ends string
reply[replyOffset++] = 0;
length++;
return length;
}
private boolean queryLocal(String domain, int type, int id) {
//TODO : query derby and add answer if available
//NOTE : set aa if found in local db and do not add anything to nameservers
int rc = records.size();
String match = null;
switch (type) {
case A: match = domain + ",a,"; break;
case CNAME: match = domain + ",cname,"; break;
case MX: match = domain + ",mx,"; break;
case AAAA: match = domain + ",aaaa,"; break;
}
if (match == null) return false;
match = match.toLowerCase();
for(int a=0;a<rc;a++) {
String record = records.get(a);
if (record.startsWith(match)) {
//type,name,ttl,values...
sendReply(domain, record, type, id);
return true;
}
}
return false;
}
private void queryRemote(String domain, int type) {
//query remote DNS server and simple relay the reply "as is"
//TODO : need to actually remove AA flag if present and fill in other sections as needed
try {
DatagramPacket out = new DatagramPacket(data, data.length);
out.setAddress(InetAddress.getByName(uplink));
out.setPort(53);
DatagramSocket sock = new DatagramSocket(); //bind to anything
sock.send(out);
reply = new byte[maxmtu];
DatagramPacket in = new DatagramPacket(reply, reply.length);
sock.receive(in);
sendReply(reply, in.getLength());
} catch (Exception e) {
JFLog.log(e);
}
}
private void sendReply(byte outData[], int outDataLength) {
try {
DatagramPacket out = new DatagramPacket(outData, outDataLength);
out.setAddress(packet.getAddress());
out.setPort(packet.getPort());
ds.send(out);
} catch (Exception e) {
JFLog.log(e);
}
}
private void sendReply(String query, String record, int type, int id) {
int rdataOffset, rdataLength;
//record = type,name,ttl,value
String f[] = record.split(",");
int ttl = JF.atoi(f[2]);
reply = new byte[maxmtu];
replyBuffer = ByteBuffer.wrap(reply);
replyBuffer.order(ByteOrder.BIG_ENDIAN);
replyOffset = 0;
putShort((short)id);
putShort((short)(QR | AA | RA));
putShort((short)1); //questions
putShort((short)1); //answers
putShort((short)0); //name servers
putShort((short)0); //additionals
encodeName(query);
putShort((short)type);
putShort((short)1); //class
switch(type) {
case A:
encodeName(query);
putShort((short)type);
putShort((short)1); //class
putInt(ttl);
putShort((short)4); //Rdata length
putIP4(f[3]); //Rdata
break;
case CNAME:
case PTR:
encodeName(query);
putShort((short)type);
putShort((short)1); //class
putInt(ttl);
rdataOffset = replyOffset;
putShort((short)0); //Rdata length (patch 2 lines down)
rdataLength = encodeName(f[3]);
replyBuffer.putShort(rdataOffset, (short)rdataLength);
break;
case MX:
encodeName(query);
putShort((short)type);
putShort((short)1); //class
putInt(ttl);
rdataOffset = replyOffset;
putShort((short)0); //Rdata length (patch 3 lines down)
putShort((short)JF.atoi(f[3])); //preference (1-99)
rdataLength = encodeName(f[4]); //cname
replyBuffer.putShort(rdataOffset, (short)(rdataLength + 2));
break;
case AAAA:
encodeName(query);
putShort((short)type);
putShort((short)1); //class
putInt(ttl);
putShort((short)16); //Rdata length
putIP6(f[3]); //Rdata
break;
}
sendReply(reply, replyOffset);
}
private void putIP4(String ip) {
String p[] = ip.split("[.]");
for(int a=0;a<4;a++) {
reply[replyOffset++] = (byte)JF.atoi(p[a]);
}
}
private void putIP6(String ip) {
String p[] = ip.split(":");
for(int a=0;a<8;a++) {
putShort((short)JF.atox(p[a]));
}
}
private void putShort(short value) {
replyBuffer.putShort(replyOffset, value);
replyOffset += 2;
}
private void putInt(int value) {
replyBuffer.putInt(replyOffset, value);
replyOffset += 4;
}
}
private static JBusServer busServer;
private JBusClient busClient;
private String config;
public class JBusMethods {
public void getConfig(String pack) {
busClient.call(pack, "getConfig", busClient.quote(busClient.encodeString(config)));
}
public void setConfig(String cfg) {
//write new file
try {
FileOutputStream fos = new FileOutputStream(getConfigFile());
fos.write(JBusClient.decodeString(cfg).getBytes());
fos.close();
} catch (Exception e) {
JFLog.log(e);
}
}
public void restart() {
dns.close();
dns = new DNS();
dns.start();
}
}
public static int getBusPort() {
if (JF.isWindows()) {
return 33005;
} else {
return 777;
}
}
public static void main(String args[]) {
serviceStart(args);
}
//Win32 Service
private static DNS dns;
public static void serviceStart(String args[]) {
if (JF.isWindows()) {
busServer = new JBusServer(getBusPort());
busServer.start();
while (!busServer.ready) {
JF.sleep(10);
}
}
dns = new DNS();
dns.start();
}
public static void serviceStop() {
JFLog.log("Stopping service");
busServer.close();
dns.close();
}
}
/*
struct Packet {
short id;
short flgs;
short queries_cnt;
short anwsers_cnt;
short auth_servers_cnt;
short additional_cnt;
Queries[];
AnwserResources[];
AuthServerResources[];
AdditionalResources[];
};
struct Query {
DNSName query;
short type;
short cls;
};
struct Resource {
DNSName name;
short type;
short cls;
int ttl;
short rdataLength;
byte rdata[rdataLength];
};
DNS Names compression:
[3]www[6]google[3]com[0]
Any length that starts with 11 binary is a 2 byte (14bits) pointer from the first byte of the packet.
[4]mail[pointer:33] -> assuming 33 points to [6]google[3]com[0] this would => mail.google.com
Types: 1=IP4 5=CNAME 15=MX 28=IP6 etc.
Cls : 1=Internet
*/