package javaforce.service; /** * DHCP Server (support IP4 only) * * @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 DHCP extends Thread { public final static String busPack = "net.sf.jfdhcp"; public static String getConfigFile() { return JF.getConfigPath() + "/jfdhcp.cfg"; } public static String getLogFile() { return JF.getLogPath() + "/jfdhcp.log"; } private static int maxmtu = 1500 - 20 - 8; //IP=20 UDP=8 private static enum OptionType {string, ip4, integer}; private static class Option { public int num; public OptionType type; public String data; } private static class Pool { public Object lock = new Object(); public String name; public String server_ip; //dhcp server ip public int server_ip_int; public String bind_ip; //bind ip (0.0.0.0 = all interfaces) public int bind_ip_int; public String pool_first; //pool first ip public int pool_first_int; //pool first ip (as int) public String pool_last; //pool last ip public int pool_last_int; //pool last ip (as int) public String mask; //subnet mask public int mask_int; //subnet mask (as int) public int host_mask_int; //inverse subnet mask (host bits) public int count = 0; //# of IPs in pool public long pool_time[]; //timestamp of issue (0=not in use) public int pool_hwlen[]; //hwaddr len of client public byte pool_hwaddr[][]; //hwaddr of client public int next = 0; //offset public String router; public String dns = "8.8.8.8"; public int leaseTime = 3600 * 24; //in seconds public ArrayList<Option> options = new ArrayList<Option>(); } private static ArrayList<Pool> pools = new ArrayList<Pool>(); private static Pool global = new Pool(); private static Inet4Address broadcastAddress; private static class Host { public String ip; public int ip_int; public DatagramSocket ds; } private static ArrayList<Host> hosts = new ArrayList<Host>(); private static Object close = new Object(); public static enum State {Loading, Running, Error, Stopped}; public static State state = State.Loading; public static Object stateMonitor = new Object(); public void run() { JFLog.append(getLogFile(), true); try { loadConfig(); busClient = new JBusClient(busPack, new JBusMethods()); busClient.setPort(getBusPort()); busClient.start(); if (!validConfig()) { throw new Exception("invalid config"); } broadcastAddress = (Inet4Address)Inet4Address.getByName("255.255.255.255"); for(int a=0;a<hosts.size();a++) { new HostWorker(hosts.get(a)).start(); } setState(State.Running); //wait for close request synchronized(close) { close.wait(); } setState(State.Stopped); } catch (Exception e) { JFLog.log(e); setState(State.Error); } } public void setState(State newState) { synchronized(stateMonitor) { state = newState; stateMonitor.notify(); } } public void close() { int cnt = hosts.size(); for(int a=0;a<cnt;a++) { DatagramSocket ds; ds = hosts.get(a).ds; if (ds != null) ds.close(); } if (busClient != null) busClient.close(); synchronized(close) { close.notify(); } } private static class HostWorker extends Thread { private Host host; public HostWorker(Host host) { this.host = host; } public void run() { JFLog.log("Starting HostWorker on : " + host.ip); try { host.ds = new DatagramSocket(67, Inet4Address.getByName(host.ip)); while (true) { byte data[] = new byte[maxmtu]; DatagramPacket packet = new DatagramPacket(data, maxmtu); host.ds.receive(packet); new RequestWorker(packet, host).start(); } } catch (Exception e) { JFLog.log(e); } } public void close() { try { host.ds.close(); host.ds = null; } catch (Exception e) { JFLog.log(e); } } } enum Section {None, Global, Pool}; private final static String defaultConfig = "#comments start with a # symbol\n" + "[global]\n" + "#dns=8.8.8.8\n" + "\n" + "#[pool_192_168_0_x]\n" + "#server_ip=192.168.0.2\n" + "#bind_ip=192.168.0.2\n" + "#pool_first=192.168.0.100\n" + "#pool_last=192.168.0.199\n" + "#mask=255.255.255.0\n" + "#router=192.168.0.1\n" + "#dns=8.8.8.8\n" + "#lease=86400 #24 hrs (1hr=min 24hrs=max)\n" + "#option=128:string:your_string\n" + "#option=129:ip4:1.2.3.4\n" + "#option=130:int:1234\n" + "\n" + "#[pool_192_168_1_x]\n" + "#server_ip=192.168.1.2\n" + "#bind_ip=192.168.1.2\n" + "#pool_first=192.168.1.100\n" + "#pool_last=192.168.1.250\n" + "#mask=255.255.255.0\n" + "#router=192.168.1.1\n" + "#dns=8.8.8.8\n" + "#lease=7200 #2 hrs\n" + "\n" + "#[pool_10_1_1_x_for_relay_agents_only]\n" + "#server_ip=192.168.1.2\n" + "#bind_ip=0.0.0.0 #bind to all interfaces\n" + "#pool_first=10.1.1.100\n" + "#pool_last=10.1.1.250\n" + "#mask=255.255.255.0\n" + "#router=10.1.1.1\n" ; private void addOption(Pool pool, String str) { int i1 = str.indexOf(":"); if (i1 == -1) { JFLog.log("bad option:" + str); return; } int i2 = str.substring(i1+1).indexOf(":"); if (i2 == -1) { JFLog.log("bad option:" + str); return; } i2 += i1 + 1; String num = str.substring(0, i1); String type = str.substring(i1+1, i2); String data = str.substring(i2+1); Option opt = new Option(); opt.num = Integer.valueOf(num); if (type.equals("int")) { opt.type = OptionType.integer; } else if (type.equals("ip4")) { opt.type = OptionType.ip4; } else { opt.type = OptionType.string; } opt.data = data; pool.options.add(opt); } private void loadConfig() { pools.clear(); hosts.clear(); Pool pool = null; try { StringBuilder cfg = new StringBuilder(); BufferedReader br = new BufferedReader(new FileReader(getConfigFile())); while (true) { String ln = br.readLine(); if (ln == null) break; cfg.append(ln); cfg.append("\n"); ln = ln.trim(); int idx = ln.indexOf('#'); if (idx != -1) ln = ln.substring(0, idx).trim(); if (ln.length() == 0) continue; if (ln.startsWith("[") && ln.endsWith("]")) { String sectionName = ln.substring(1,ln.length() - 1); if (sectionName.equals("global")) { pool = global; } else { pool = new Pool(); pool.name = sectionName; pools.add(pool); } continue; } idx = ln.indexOf("="); if (idx == -1) continue; String key = ln.substring(0, idx).toLowerCase().trim(); String value = ln.substring(idx+1).trim(); if (key.equals("pool_first")) pool.pool_first = value; else if (key.equals("pool_last")) pool.pool_last = value; else if (key.equals("server_ip")) pool.server_ip = value; else if (key.equals("bind_ip")) pool.bind_ip = value; else if (key.equals("mask")) pool.mask = value; else if (key.equals("dns")) pool.dns = value; else if (key.equals("router")) pool.router = value; else if (key.equals("lease")) { pool.leaseTime = JF.atoi(value); if (pool.leaseTime < 3600) pool.leaseTime = 3600; //1hr min if (pool.leaseTime > 86400) pool.leaseTime = 86400; //1 day max } else if (key.equals("option")) addOption(pool, value); } config = cfg.toString(); } catch (FileNotFoundException e) { //create default config 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); } } private boolean validConfig() { try { if (pools.size() == 0) throw new Exception("no pools defined"); if (global.server_ip != null) { if (!validIP4(global.server_ip)) throw new Exception("global : invalid server_ip"); global.server_ip_int = IP4toInt(global.server_ip); } if (global.bind_ip == null) global.bind_ip = "0.0.0.0"; global.bind_ip_int = IP4toInt(global.bind_ip); int cnt = pools.size(); for(int a=0;a<cnt;a++) { Pool pool = pools.get(a); if (pool.server_ip == null) pool.server_ip = global.server_ip; if (pool.router == null) pool.router = global.router; if (pool.dns == null) pool.dns = global.dns; if (pool.bind_ip == null) pool.bind_ip = global.bind_ip; if (!validIP4(pool.server_ip)) throw new Exception(pool.name + " : invalid server_ip"); if (!validIP4(pool.pool_first)) throw new Exception(pool.name + " : invalid pool_first"); if (!validIP4(pool.pool_last)) throw new Exception(pool.name + " : invalid pool_last"); if (!validIP4(pool.router)) throw new Exception(pool.name + " : invalid router"); if (!validIP4(pool.mask)) throw new Exception(pool.name + " : invalid mask"); if (!validIP4(pool.dns)) throw new Exception(pool.name + " : invalid dns"); if (pool.leaseTime < 3600 || pool.leaseTime > 3600 * 24) { JFLog.log(pool.name + " : leaseTime invalid, using 24 hrs"); pool.leaseTime = 3600 * 24; } pool.server_ip_int = IP4toInt(pool.server_ip); pool.bind_ip_int = IP4toInt(pool.bind_ip); pool.pool_first_int = IP4toInt(pool.pool_first); pool.pool_last_int = IP4toInt(pool.pool_last); pool.mask_int = IP4toInt(pool.mask); if ((pool.pool_first_int & pool.mask_int) != (pool.pool_last_int & pool.mask_int)) { throw new Exception(pool.name + " : invalid pool range : " + pool.pool_first + "-" + pool.pool_last + ",mask=" + pool.mask); } pool.host_mask_int = pool.mask_int ^ 0xffffffff; pool.count = (pool.pool_last_int & pool.host_mask_int) - (pool.pool_first_int & pool.host_mask_int) + 1; pool.pool_time = new long[pool.count]; pool.pool_hwlen = new int[pool.count]; pool.pool_hwaddr = new byte[pool.count][16]; JFLog.log("pool:" + IP4toString(pool.pool_first_int) + "-" + IP4toString(pool.pool_last_int) + ":" + pool.count + " IPs"); } for(int a=0;a<cnt;a++) { Pool poola = pools.get(a); for(int b=0;b<cnt;b++) { if (b == a) continue; Pool poolb = pools.get(b); if ((poola.pool_first_int & poola.mask_int) == (poolb.pool_first_int & poolb.mask_int)) { throw new Exception("multiple pools overlap"); } } } for(int a=0;a<cnt;a++) { Pool pool = pools.get(a); boolean hasHost = false; for(int b=0;b<hosts.size();b++) { Host host = hosts.get(b); if (host.ip == pool.bind_ip) {hasHost = true; break;} } if (!hasHost) { Host host = new Host(); host.ip = pool.bind_ip; host.ip_int = IP4toInt(host.ip); hosts.add(host); } } } catch (Exception e) { JFLog.log(e); return false; } return true; } private boolean validIP4(String ip) { if (ip == null) return false; String o[] = ip.split("[.]"); if (o.length != 4) return false; for(int a=0;a<4;a++) { int v = Integer.valueOf(o[a]); if (v < 0) return false; if (v > 255) return false; } return true; } private static int IP4toInt(String ip) { String o[] = ip.split("[.]"); int ret = 0; for(int a=0;a<4;a++) { ret <<= 8; ret += (JF.atoi(o[a])); } return ret; } private static byte[] IP4toByteArray(String ip) { String o[] = ip.split("[.]"); byte ret[] = new byte[4]; for(int a=0;a<4;a++) { ret[a] = (byte)JF.atoi(o[a]); } return ret; } private static byte[] IP4toByteArray(int ip) { return IP4toByteArray(IP4toString(ip)); } private static String IP4toString(int ip) { return String.format("%d.%d.%d.%d", ip >>> 24, (ip >> 16) & 0xff, (ip >> 8) & 0xff, ip & 0xff); } private static String IP4toString(byte ip[]) { return IP4toString(BE.getuint32(ip, 0)); } public static String IP4toString(byte ip[], int offset) { return IP4toString(BE.getuint32(ip, offset)); } private static final int cookie = 0x63825363; public static final int DHCP_OPCODE_REQUEST = 1; public static final int DHCP_OPCODE_REPLY = 2; public static final int DHCPDISCOVER = 1; public static final int DHCPOFFER = 2; public static final int DHCPREQUEST = 3; public static final int DHCPDECLINE = 4; public static final int DHCPACK = 5; public static final int DHCPNAK = 6; public static final int DHCPRELEASE = 7; public static final int DHCPINFORM = 8; private static String getMsgType(int type) { switch (type) { case DHCPDISCOVER: return "DHCPDISCOVER"; case DHCPOFFER: return "DHCPOFFER"; case DHCPREQUEST: return "DHCPREQUEST"; case DHCPDECLINE: return "DHCPDECLINE"; case DHCPACK: return "DHCPACK"; case DHCPNAK: return "DHCPNAK"; case DHCPRELEASE: return "DHCPRELEASE"; case DHCPINFORM: return "DHCPINFORM"; } return "???"; } private static final byte OPT_PAD = 0; private static final byte OPT_SUBNET_MASK = 1; private static final byte OPT_ROUTER = 3; private static final byte OPT_DNS = 6; private static final byte OPT_REQUEST_IP = 50; private static final byte OPT_LEASE_TIME = 51; private static final byte OPT_DHCP_MSG_TYPE = 53; private static final byte OPT_DHCP_SERVER_IP = 54; private static final byte OPT_RENEWAL_TIME = 58; private static final byte OPT_REBINDING_TIME = 59; private static final byte OPT_END = -1; //255 private static class RequestWorker extends Thread { private DatagramPacket packet; private Host host; private byte req[]; private byte reply[]; private int replyOffset; //offset while encoding name private ByteBuffer replyBuffer; private Pool pool; public RequestWorker(DatagramPacket packet, Host host) { this.packet = packet; this.host = host; } public void run() { try { JFLog.log("Received request from:" + packet.getSocketAddress()); req = packet.getData(); ByteBuffer bb = ByteBuffer.wrap(req); bb.order(ByteOrder.BIG_ENDIAN); byte opcode = req[0]; if (opcode != DHCP_OPCODE_REQUEST) throw new Exception("not a request"); byte hwtype = req[1]; // if (hwtype != 1) throw new Exception("not ethernet"); byte hwlen = req[2]; // if (hwlen != 6) throw new Exception("bad hardware length"); byte hop = req[3]; int id = bb.getInt(4); short seconds = bb.getShort(8); short flags = bb.getShort(10); int cip = bb.getInt(12); //client ip int yip = bb.getInt(16); //your ip int sip = bb.getInt(20); //server ip int rip = bb.getInt(24); //relay ip int msgType = -1; int yipOffset = -1; //detect pool int cnt = pools.size(); int from_ip = rip; if (from_ip == 0) { String src = packet.getAddress().getHostAddress(); from_ip = IP4toInt(src); } if (from_ip == 0) { from_ip = host.ip_int; } if (from_ip == 0 && pools.size() == 1) { from_ip = pools.get(0).server_ip_int; } if (from_ip == 0) { throw new Exception("can not determine pool for request"); } JFLog.log("ip=" + IP4toString(from_ip)); for(int a=0;a<cnt;a++) { pool = pools.get(a); if ((pool.pool_first_int & pool.mask_int) == (from_ip & pool.mask_int)) { break; } pool = null; } if (pool == null) throw new Exception("no pool for request"); if (yip !=0 && (yip & pool.mask_int) == (pool.pool_first_int & pool.mask_int)) { yipOffset = yip - pool.pool_first_int; } //28 = 16 bytes = client hardware address (ethernet : 6 bytes) //44 = 64 bytes = server host name (ignored) //108 = 128 bytes = boot filename (ignored) //236 = 4 bytes = cookie (0x63825363) //240 = options... int offset = 240; while (true) { byte opt = req[offset++]; if (opt == OPT_PAD) continue; byte len = req[offset++]; switch (opt) { case OPT_DHCP_MSG_TYPE: if (len != 1) throw new Exception("bad dhcp msg type (size != 1)"); msgType = req[offset]; break; case OPT_REQUEST_IP: if (len != 4) throw new Exception("bad request ip (size != 4)"); yip = bb.getInt(offset); if ((yip & pool.mask_int) == (pool.pool_first_int & pool.mask_int)) { yipOffset = yip - pool.pool_first_int; } break; } if (opt == OPT_END) break; offset += len; } if (msgType == -1) throw new Exception("no dhcp msg type"); JFLog.log("MsgType=" + getMsgType(msgType)); long now = System.currentTimeMillis(); switch (msgType) { case DHCPDISCOVER: synchronized(pool.lock) { int i = pool.next++; int c = 0; while (c < pool.count) { if (pool.pool_time[i] != 0) { if (pool.pool_time[i] < now) pool.pool_time[i] = 0; } if (pool.pool_time[i] == 0) { //check with ping (Java 5 required) byte addr[] = IP4toByteArray(pool.pool_first_int + i); Inet4Address inet = (Inet4Address)Inet4Address.getByAddress(addr); if (inet.isReachable(1000)) { //IP still in use! //this could happen if DHCP service is restarted since leases are only saved in memory pool.pool_time[i] = now + (pool.leaseTime * 1000); JFLog.log("warning:IP in use but not in database:" + IP4toString(addr)); } else { //offer this sendReply(addr, DHCPOFFER, id, pool, rip); pool.next = i+1; if (pool.next == pool.count) pool.next = 0; return; } } c++; i++; if (i == pool.count) i = 0; } } //nothing left in pool to send an offer (ignore request) JFLog.log("no free IPs in pool for request"); break; case DHCPREQUEST: //mark ip as used and send ack or nak if already in use if (yipOffset < 0 || yipOffset >= pool.count) { JFLog.log("request out of range"); break; } synchronized(pool.lock) { byte addr[] = IP4toByteArray(pool.pool_first); addr[3] += yipOffset; if (pool.pool_time[yipOffset] != 0) { //check if hwaddr is the same boolean same = true; for(int a=0;a<pool.pool_hwlen[yipOffset];a++) { if (pool.pool_hwaddr[yipOffset][a] != req[28 + a]) {same = false; break;} } if (same) pool.pool_time[yipOffset] = 0; } if (pool.pool_time[yipOffset] == 0) { //send ACK pool.pool_time[yipOffset] = now + (pool.leaseTime * 1000); pool.pool_hwlen[yipOffset] = hwlen; System.arraycopy(req, 28, pool.pool_hwaddr[yipOffset], 0, 16); sendReply(addr, DHCPACK, id, pool, rip); } else { //send NAK sendReply(addr, DHCPNAK, id, pool, rip); } } break; case DHCPRELEASE: //mark ip as unused if (yipOffset < 0 || yipOffset >= pool.count) { JFLog.log("release out of range"); break; } synchronized(pool.lock) { if (pool.pool_time[yipOffset] != 0) { //check if hwaddr is the same boolean same = true; for(int a=0;a<pool.pool_hwlen[yipOffset];a++) { if (pool.pool_hwaddr[yipOffset][a] != req[28 + a]) {same = false; break;} } if (!same) break; pool.pool_time[yipOffset] = 0; } } break; default: throw new Exception("unsupported dhcp msg type"); } } catch (Exception e) { JFLog.log(e); } } private void sendReply(byte outData[], int outDataLength, int rip) { try { DatagramPacket out = new DatagramPacket(outData, outDataLength); if (rip == 0) out.setAddress(broadcastAddress); else out.setAddress(Inet4Address.getByAddress(new byte[] {(byte)(rip >> 24), (byte)((rip >> 16) & 0xff), (byte)((rip >> 8) & 0xff), (byte)(rip & 0xff)})); int port = packet.getPort(); out.setPort(port); host.ds.send(out); JFLog.log("ReplyTo:" + IP4toString(rip) + ":" + port); } catch (Exception e) { JFLog.log(e); } } private void sendReply(byte yip[], int msgType /*offer,ack,nak*/, int id, Pool pool, int rip) { JFLog.log("ReplyFor:" + IP4toString(yip) + ":" + getMsgType(msgType)); reply = new byte[maxmtu]; replyBuffer = ByteBuffer.wrap(reply); replyBuffer.order(ByteOrder.BIG_ENDIAN); replyOffset = 0; reply[replyOffset++] = DHCP_OPCODE_REPLY; //reply opcode reply[replyOffset++] = req[1]; //hwtype reply[replyOffset++] = req[2]; //hwlen reply[replyOffset++] = 0; //hops putInt(id); putShort((short)0); //seconds putShort((short)0x8000); //flags (bit 15 = broadcast) putInt(0); //client IP putByteArray(yip); //your IP putIP4(pool.server_ip); //server ip putInt(rip); //relay ip System.arraycopy(req, replyOffset, reply, replyOffset, 16); //client hwaddr replyOffset += 16; replyOffset += 64; //server name replyOffset += 128; //boot filename (legacy BOOTP) //add cookie putInt(cookie); //add options reply[replyOffset++] = OPT_DHCP_MSG_TYPE; reply[replyOffset++] = 1; reply[replyOffset++] = (byte)msgType; reply[replyOffset++] = OPT_SUBNET_MASK; reply[replyOffset++] = 4; putIP4(pool.mask); reply[replyOffset++] = OPT_DNS; reply[replyOffset++] = 4; putIP4(pool.dns); reply[replyOffset++] = OPT_ROUTER; reply[replyOffset++] = 4; putIP4(pool.router); reply[replyOffset++] = OPT_DHCP_SERVER_IP; reply[replyOffset++] = 4; putIP4(pool.server_ip); reply[replyOffset++] = OPT_RENEWAL_TIME; reply[replyOffset++] = 4; putInt(pool.leaseTime - 1800); //30 mins early reply[replyOffset++] = OPT_REBINDING_TIME; reply[replyOffset++] = 4; putInt(pool.leaseTime - 900); //15 mins early reply[replyOffset++] = OPT_LEASE_TIME; reply[replyOffset++] = 4; putInt(pool.leaseTime); //add custom options for(int a=0;a<pool.options.size();a++) { Option opt = pool.options.get(a); reply[replyOffset++] = (byte)opt.num; switch (opt.type) { case string: reply[replyOffset++] = (byte)opt.data.length(); putByteArray(opt.data.getBytes()); break; case integer: reply[replyOffset++] = 4; putInt(Integer.valueOf(opt.data)); break; case ip4: reply[replyOffset++] = 4; putIP4(opt.data); break; } } reply[replyOffset++] = OPT_END; sendReply(reply, replyOffset, rip); } private void putByteArray(byte ba[]) { for(int a=0;a<ba.length;a++) { reply[replyOffset++] = ba[a]; } } 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() { dhcp.close(); dhcp = new DHCP(); dhcp.start(); } } public static int getBusPort() { if (JF.isWindows()) { return 33004; } else { return 777; } } public static void main(String args[]) { serviceStart(args); } //Win32 Service private static DHCP dhcp; public static void serviceStart(String args[]) { if (JF.isWindows()) { busServer = new JBusServer(getBusPort()); busServer.start(); while (!busServer.ready) { JF.sleep(10); } } dhcp = new DHCP(); dhcp.start(); } public static void serviceStop() { JFLog.log("Stopping service"); dhcp.close(); } }