001: /* VirtualDNS - A modular DNS server.
002: * Copyright (C) 2000 Eric Kidd
003: * Copyright (C) 1999 Brian Wellington
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: */
019:
020: package CustomDNS.VirtualDNS;
021:
022: import java.lang.reflect.*;
023: import java.io.*;
024: import java.net.*;
025: import java.util.*;
026: import org.xbill.DNS.*;
027: import org.xbill.DNS.utils.*;
028:
029: import CustomDNS.VirtualDNS.Response;
030: import CustomDNS.VirtualDNS.ErrorMessages;
031:
032: /*************************************************************************
033: * A very restricted DNS server which supports a single virtual domain.
034: *************************************************************************
035: * This DNS server only supports one zone. It refuses all queries for
036: * information outside that zone. It doesn't support zone transfers,
037: * subzones or referrals.
038: *
039: * The names in the zone are divided into two categories: static and
040: * dynamic. The static names are served from the specified master file.
041: * The dynamic names are supplied by subclasses (which should override
042: * findDynamicData).
043: *
044: * This class is based on jnamed by
045: * Brian Wellington <bwelling@xbill.org>.
046: */
047:
048: public class VirtualDNS {
049:
050: // Timeout values, in milliseconds. These are pretty arbitrary. We're
051: // just trying to shut down unused connections promptly.
052: static private final int TCP_TIMEOUT = 60000;
053:
054: // Instance variables.
055: Zone zone;
056:
057: /*********************************************************************
058: * Create and run a new DNS server on port 53.
059: *********************************************************************
060: * @param zonefile A master file for the zone to serve.
061: */
062: public VirtualDNS(String zonefile) throws IOException {
063: this (zonefile, (short) 53);
064: }
065:
066: /*********************************************************************
067: * Create and run a new DNS server.
068: *********************************************************************
069: * @param zonefile A master file for the zone to serve.
070: * @param port The port on which to run the server.
071: */
072: public VirtualDNS(String zonefile, short port) throws IOException {
073: // Load our master file.
074: zone = new Zone(zonefile, null);
075:
076: // Start our two server processes.
077: addUDP(port);
078: addTCP(port);
079: }
080:
081: // Create a new TCP listener process.
082: private void addTCP(final short port) {
083: Thread t;
084: t = new Thread(new Runnable() {
085: public void run() {
086: serveTCP(port);
087: }
088: });
089: t.start();
090: }
091:
092: // Create a new UDP listener process.
093: private void addUDP(final short port) {
094: Thread t;
095: t = new Thread(new Runnable() {
096: public void run() {
097: serveUDP(port);
098: }
099: });
100: t.start();
101: }
102:
103: // Our TCP listener process.
104: private void serveTCP(short port) {
105: try {
106: ServerSocket sock = new ServerSocket(port);
107: while (true) {
108: final Socket s = sock.accept();
109: Thread t = new Thread(new Runnable() {
110: public void run() {
111: processTCP(s);
112: }
113: });
114: t.start();
115: }
116: } catch (IOException e) {
117: System.out.println("serveTCP failed: " + e);
118: }
119: }
120:
121: // Read and respond to a TCP request.
122: private void processTCP(Socket s) {
123: try {
124: int inLength;
125: DataInputStream dataIn;
126: DataOutputStream dataOut;
127: byte[] in;
128:
129: s.setSoTimeout(TCP_TIMEOUT);
130:
131: try {
132: InputStream is = s.getInputStream();
133: dataIn = new DataInputStream(is);
134: inLength = dataIn.readUnsignedShort();
135: in = new byte[inLength];
136: dataIn.readFully(in);
137: } catch (InterruptedIOException e) {
138: s.close();
139: return;
140: }
141: Message query, response;
142: try {
143: query = new Message(in);
144: response = generateReply(query, in, s);
145: if (response == null)
146: return;
147: } catch (IOException e) {
148: response = ErrorMessages.makeFormatErrorMessage(in);
149: }
150: byte[] out = response.toWire();
151: dataOut = new DataOutputStream(s.getOutputStream());
152: dataOut.writeShort(out.length);
153: dataOut.write(out);
154:
155: } catch (IOException e) {
156: System.out.println("processTCP: " + e);
157: } finally {
158: try {
159: s.close();
160: } catch (IOException e) {
161: }
162: }
163: }
164:
165: // Our UDP listener process.
166: private void serveUDP(short port) {
167: try {
168: DatagramSocket sock = new DatagramSocket(port);
169: while (true) {
170: short udpLength = 512;
171: DatagramPacket dp = new DatagramPacket(new byte[512],
172: 512);
173: try {
174: sock.receive(dp);
175: } catch (InterruptedIOException e) {
176: continue;
177: }
178: byte[] in = new byte[dp.getLength()];
179: System.arraycopy(dp.getData(), 0, in, 0, in.length);
180: Message query, response;
181: try {
182: query = new Message(in);
183: response = generateReply(query, in, null);
184: if (response == null)
185: continue;
186: } catch (IOException e) {
187: response = ErrorMessages.makeFormatErrorMessage(in);
188: }
189: byte[] out = response.toWire();
190:
191: dp = new DatagramPacket(out, out.length, dp
192: .getAddress(), dp.getPort());
193: sock.send(dp);
194: }
195: } catch (IOException e) {
196: System.out.println("serveUDP: " + e);
197: }
198: }
199:
200: // Check to see if a given name falls within our zone.
201: private boolean isNameInZone(Name name) {
202: // Starting with the name itself, walk back up the namespace and
203: // look for our zone's name.
204: Name zonename = zone.getOrigin();
205: while (!name.equals(Name.root)) {
206: if (name.equals(zonename))
207: return true;
208: name = new Name(name, 1);
209: }
210: return false;
211: }
212:
213: // Construct a proper reply packet.
214: private Message generateReply(Message query, byte[] in, Socket s) {
215:
216: // Refuse everything but standard DNS queries.
217: if (query.getHeader().getOpcode() != Opcode.QUERY)
218: return ErrorMessages.makeErrorMessage(query, Rcode.NOTIMPL);
219:
220: // Get some useful information from the query.
221: // XXX - What should we do if we get the wrong query version?
222: Record queryRecord = query.getQuestion();
223: OPTRecord queryOPT = query.getOPT();
224: Name name = queryRecord.getName();
225: short type = queryRecord.getType();
226: short dclass = queryRecord.getDClass();
227:
228: // XXX - Is this the right thing to do with TSIGs?
229: if (query.getTSIG() != null)
230: return ErrorMessages.makeErrorMessage(query, Rcode.NOTIMPL);
231:
232: // Calculate the maximum allowable response size.
233: int maxLength;
234: if (s != null)
235: maxLength = 65535;
236: else if (queryOPT != null)
237: maxLength = queryOPT.getPayloadSize();
238: else
239: maxLength = 512;
240:
241: // Start building our response packet.
242: Response response = new Response();
243: response.getHeader().setID(query.getHeader().getID());
244: response.getHeader().setFlag(Flags.QR);
245: response.addRecord(queryRecord, Section.QUESTION);
246:
247: // Reject zone transfer requests--our zone is dynamic.
248: if (type == Type.AXFR && s != null)
249: return ErrorMessages.makeErrorMessage(query, Rcode.REFUSED);
250:
251: // Return an error if we get asked for unsupported record types.
252: if (!Type.isRR(type) && type != Type.ANY)
253: return ErrorMessages.makeErrorMessage(query, Rcode.NOTIMPL);
254: if (type == Type.SIG)
255: return ErrorMessages.makeErrorMessage(query, Rcode.NOTIMPL);
256:
257: // Reject names outside our zone.
258: // XXX - We should refer the client to other name servers here.
259: if (!isNameInZone(name))
260: return ErrorMessages.makeErrorMessage(query, Rcode.NOTIMPL);
261:
262: // We're authoritative for this zone.
263: response.getHeader().setFlag(Flags.AA);
264:
265: // Look up the records. If we can't find any, send an
266: // authoritative error response.
267: SetResponse zr = findMatchingRecords(name, type);
268: if (zr.isNXDOMAIN())
269: response.getHeader().setRcode(Rcode.NXDOMAIN);
270:
271: // Report all the CNAMEs we traversed.
272: // We only keep this around so admins can fool around
273: // with in-zone CNAMES. It will only work for static names.
274: Vector backtrace = zr.backtrace();
275: if (backtrace != null) {
276: Enumeration e = backtrace.elements();
277: while (e.hasMoreElements()) {
278: Record cname = (Record) e.nextElement();
279: response.addRecord(cname, Section.ANSWER);
280: }
281: }
282:
283: // Copy the records we found into the response.
284: if (zr.isSuccessful()) {
285: RRset[] rrsets = zr.answers();
286: for (int i = 0; i < rrsets.length; i++)
287: response.addRRset(name, rrsets[i]);
288: }
289:
290: // Attach our extra section, truncate, and return.
291: addAdditional(response);
292: truncateResponse(response, maxLength);
293: return response;
294: }
295:
296: // Attempt to truncate a response to wire size (if needed).
297: private void truncateResponse(Response response, int maxLength) {
298: // XXX - What happens if we fail? Hmm.
299: try {
300: if (response.wireLength() > maxLength) {
301: response.truncate(maxLength);
302: }
303: } catch (IOException e) {
304: }
305: }
306:
307: // Find all records matching a given name. We try our static zone
308: // first and then our dynamic information.
309: private SetResponse findMatchingRecords(Name name, short type) {
310:
311: // Return the static data if we have it. Note that we only fall
312: // through if the result in NXDOMAIN--i.e., the name server could
313: // authoritatively decide the host doesn't exit. Otherwise, we
314: // return the answer unchanged.
315: SetResponse staticResponse = zone.findRecords(name, type);
316: if (!staticResponse.isNXDOMAIN())
317: return staticResponse;
318:
319: // Try to find some dynamic data.
320: SetResponse dynamic = findDynamicData(zone.getOrigin(), name,
321: type);
322: if (dynamic != null)
323: return dynamic;
324:
325: // Give up.
326: return staticResponse;
327: }
328:
329: /*********************************************************************
330: * Look up dynamic DNS data for a given hostname.
331: *********************************************************************
332: * Override this method to return either a valid SetResponse for the
333: * given name, or null (to decline). You probably don't want to return
334: * any records that will require glue (MX, CNAME, etc.).
335: * XXX - Fix this to use an RRset. This will require refactoring other
336: * code.
337: * @param zoneName The DNS name of the zone containing the host.
338: * @param queryName The DNS name of the host.
339: * @param queryType The type of records to return.
340: * @return A SetResponse containing the records an error code.
341: * @see org.xbill.DNS.SetResponse
342: */
343: protected SetResponse findDynamicData(Name zoneName,
344: Name queryName, short queryType) {
345: return null;
346: }
347:
348: // Try to add glue names.
349: // We only keep this around so that we can response to in-zone MX
350: // queries in the polite fashion.
351: private void addAdditional(Message response) {
352: addAdditional2(response, Section.ANSWER);
353: addAdditional2(response, Section.AUTHORITY);
354: }
355:
356: // Add all the glue names for a given section. See addAditional.
357: private void addAdditional2(Message response, int section) {
358: Enumeration e = response.getSection(section);
359: while (e.hasMoreElements()) {
360: Record r = (Record) e.nextElement();
361: try {
362: Method m = r.getClass().getMethod("getTarget", null);
363: Name glueName = (Name) m.invoke(r, null);
364: addGlue(response, glueName);
365: } catch (Exception ex) {
366: }
367: }
368: }
369:
370: // Add address records for hosts mentioned in the response.
371: // See addAditional.
372: private void addGlue(Message response, Name name) {
373: RRset a = findExactMatch(name, Type.A, DClass.IN);
374: Enumeration e = a.rrs();
375: while (e.hasMoreElements()) {
376: Record r = (Record) e.nextElement();
377: if (response.findRecord(r) == false)
378: response.addRecord(r, Section.ADDITIONAL);
379: }
380: }
381:
382: // Look for some records to use as glue. See addAditional.
383: private RRset findExactMatch(Name name, short type, short dclass) {
384: if (!isNameInZone(name))
385: return null;
386: return zone.findExactMatch(name, type);
387: }
388:
389: /*********************************************************************
390: * Test this class.
391: *********************************************************************
392: * A simple test program allowing the VirtualDNS to be run by
393: * itself. Pass it a regular DNS master file.
394: * @param args Command-line arguments.
395: */
396: public static void main(String[] args) {
397: if (args.length > 1) {
398: System.out.println("usage: VirtualDNS [conf]");
399: System.exit(1);
400: }
401: try {
402: String zonefile;
403: if (args.length == 1)
404: zonefile = args[0];
405: else
406: zonefile = "primary.zone";
407: VirtualDNS s = new VirtualDNS(zonefile);
408: } catch (IOException e) {
409: System.out.println(e);
410: }
411: }
412: }
|