fix infloop when skipping queries; add debug logging
[captive-validator.git] / src / com / verisign / cl / DNSSECValTool.java
1 package com.verisign.cl;
2
3 import java.io.*;
4 import java.net.SocketTimeoutException;
5 import java.util.*;
6
7 import org.apache.log4j.BasicConfigurator;
8 import org.apache.log4j.Level;
9 import org.apache.log4j.Logger;
10 import org.xbill.DNS.*;
11
12 import com.verisign.tat.dnssec.CaptiveValidator;
13 import com.verisign.tat.dnssec.SecurityStatus;
14 import com.verisign.tat.dnssec.Util;
15
16 public class DNSSECValTool {
17
18     /**
19      * Invoke with java -jar dnssecreconciler.jar server=127.0.0.1 \
20      * query_file=queries.txt dnskey_query=net dnskey_query=edu
21      */
22     private CaptiveValidator validator;
23     private SimpleResolver resolver;
24
25     private BufferedReader queryStream;
26     private PrintStream errorStream;
27     private Set<Name> zoneNames;
28
29     // Options
30     public String server;
31     public String query;
32     public String queryFile;
33     public String dnskeyFile;
34     public List<String> dnskeyNames;
35     public String errorFile;
36     public long count = 0;
37     public boolean debug = false;
38
39     DNSSECValTool() {
40         validator = new CaptiveValidator();
41     }
42
43     /**
44      * Convert a query line of the form: <qname> <qtype> <flags> to a request
45      * message.
46      * 
47      * @param query_line
48      * @return A query message
49      * @throws TextParseException
50      * @throws NameTooLongException
51      */
52     private Message queryFromString(String query_line)
53             throws TextParseException, NameTooLongException {
54
55         String[] tokens = query_line.split("[ \t]+");
56
57         Name qname = null;
58         int qtype = -1;
59         int qclass = -1;
60
61         if (tokens.length < 1)
62             return null;
63         qname = Name.fromString(tokens[0]);
64         if (!qname.isAbsolute()) {
65             qname = Name.concatenate(qname, Name.root);
66         }
67
68         for (int i = 1; i < tokens.length; i++) {
69             if (tokens[i].startsWith("+")) {
70                 // For now, we ignore flags as uninteresting
71                 // All queries will get the DO bit anyway
72                 continue;
73             }
74
75             int type = Type.value(tokens[i]);
76             if (type > 0) {
77                 qtype = type;
78                 continue;
79             }
80             int cls = DClass.value(tokens[i]);
81             if (cls > 0) {
82                 qclass = cls;
83                 continue;
84             }
85         }
86         if (qtype < 0) {
87             qtype = Type.A;
88         }
89         if (qclass < 0) {
90             qclass = DClass.IN;
91         }
92
93         Message query = Message.newQuery(Record.newRecord(qname, qtype, qclass));
94
95         return query;
96     }
97
98     /**
99      * Fetch the next query from either the command line or the query file
100      * 
101      * @return a query Message, or null if the query list is exhausted
102      * @throws IOException
103      */
104     private Message nextQuery() throws IOException {
105         if (query != null) {
106             Message res = queryFromString(query);
107             query = null;
108             return res;
109         }
110
111         if (queryStream == null && queryFile != null) {
112             queryStream = new BufferedReader(new FileReader(queryFile));
113         }
114
115         if (queryStream != null) {
116             String line = queryStream.readLine();
117
118             if (line == null)
119                 return null;
120
121             return queryFromString(line);
122         }
123
124         return null;
125
126     }
127
128     /**
129      * Figure out the correct zone from the query by comparing the qname to the
130      * list of trusted DNSKEY owner names.
131      * 
132      * @param query
133      * @return a zone name
134      * @throws IOException
135      */
136     private Name zoneFromQuery(Message query) throws IOException {
137
138         if (zoneNames == null) {
139             zoneNames = new HashSet<Name>();
140             for (String key : validator.listTrustedKeys()) {
141                 String[] components = key.split("/");
142                 Name keyname = Name.fromString(components[0]);
143                 if (!keyname.isAbsolute()) {
144                     keyname = Name.concatenate(keyname, Name.root);
145                 }
146                 zoneNames.add(keyname);
147             }
148         }
149
150         Name qname = query.getQuestion().getName();
151         for (Name n : zoneNames) {
152             if (qname.subdomain(n)) {
153                 return n;
154             }
155         }
156
157         return null;
158     }
159
160     private Message resolve(Message query) {
161
162         try {
163             return resolver.send(query);
164         } catch (SocketTimeoutException e) {
165             System.err.println("Error: timed out querying " + server + " for "
166                     + queryToString(query));
167         } catch (IOException e) {
168             System.err.println("Error: error querying " + server + " for "
169                     + queryToString(query) + ":" + e.getMessage());
170         }
171         return null;
172     }
173
174     private String queryToString(Message query) {
175         if (query == null)
176             return null;
177         Record question = query.getQuestion();
178         return question.getName() + "/" + Type.string(question.getType()) + "/"
179                 + DClass.string(question.getDClass());
180     }
181
182     public void execute() throws IOException {
183         // Configure our resolver
184         resolver = new SimpleResolver(server);
185         resolver.setEDNS(0, 4096, Flags.DO, null);
186
187         // Create our DNSSEC error stream
188         if (errorFile != null) {
189             errorStream = new PrintStream(new FileOutputStream(errorFile, true));
190         } else {
191             errorStream = System.out;
192         }
193
194         // Prime the validator
195         if (dnskeyFile != null) {
196             validator.addTrustedKeysFromFile(dnskeyFile);
197         } else {
198             for (String name : dnskeyNames) {
199                 Message query = queryFromString(name + " DNSKEY");
200                 Message response = resolve(query);
201                 validator.addTrustedKeysFromResponse(response);
202             }
203         }
204
205         // Log our set of trusted keys
206         for (String key : validator.listTrustedKeys()) {
207             System.out.println("Trusted Key: " + key);
208         }
209
210         // Iterate over all queries
211         Message query = nextQuery();
212         long total = 0;
213         long validCount = 0;
214         long errorCount = 0;
215
216         while (query != null) {
217
218             Name zone = zoneFromQuery(query);
219             // Skip queries in zones that we don't have keys for
220             if (zone == null) {
221                 if (debug) {
222                     System.out.println("DEBUG: skipping query " + queryToString(query));
223                 }
224                 query = nextQuery();
225                 continue;
226             }
227
228             if (debug) {
229                 System.out.println("DEBUG: querying for: " + queryToString(query));
230             }
231
232             Message response = resolve(query);
233             if (response == null) {
234                 System.out.println("ERROR: No response for query: " + queryToString(query));
235                 continue;
236             }
237             byte result = validator.validateMessage(response, zone.toString());
238
239             switch (result) {
240             case SecurityStatus.BOGUS:
241             case SecurityStatus.INVALID:
242                 errorStream.println("BOGUS Answer:");
243                 errorStream.println("Query: " + queryToString(query));
244                 errorStream.println("Response:\n" + response);
245                 for (String err : validator.getErrorList()) {
246                     errorStream.println("Error: " + err);
247                 }
248                 errorStream.println("");
249                 errorCount++;
250                 break;
251             case SecurityStatus.INSECURE:
252             case SecurityStatus.INDETERMINATE:
253             case SecurityStatus.UNCHECKED:
254                 errorStream.println("Insecure Answer:");
255                 errorStream.println("Query: " + queryToString(query));
256                 errorStream.println("Response:\n" + response);
257                 for (String err : validator.getErrorList()) {
258                     errorStream.println("Error: " + err);
259                 }
260                 errorCount++;
261                 break;
262             case SecurityStatus.SECURE:
263                 if (debug) System.out.println("DEBUG: response for " + queryToString(query) + " was valid.");
264                 validCount++;
265                 break;
266             }
267
268             if (++total % 1000 == 0) {
269                 System.out.println("Completed " + total + " queries: "
270                         + validCount + " valid, " + errorCount + " errors.");
271             }
272          
273             if (count > 0 && total >= count) {
274                 if (debug) System.out.println("DEBUG: reached maximum number of queries, exiting");
275                 break;
276             }
277             
278             query = nextQuery();
279         }
280
281         System.out.println("Completed " + total
282                 + (total > 1 ? " queries" : " query") +
283                 ": " + validCount + " valid, " + errorCount + " errors.");
284     }
285
286     private static void usage() {
287         System.err.println("usage: java -jar dnssecvaltool.jar [..options..]");
288         System.err.println("       server:       the DNS server to query.");
289         System.err.println("       query:        a name [type [flags]] string.");
290         System.err.println("       query_file:   a list of queries, one query per line.");
291         System.err.println("       count:        send up to'count' queries, then stop.");
292         System.err.println("       dnskey_file:  a file containing DNSKEY RRs to trust.");
293         System.err.println("       dnskey_query: query 'server' for DNSKEY at given name to trust, may repeat.");
294         System.err.println("       error_file:   write DNSSEC validation failure details to this file.");
295     }
296
297     public static void main(String[] argv) {
298
299         // Set up Log4J to just log to console.
300         BasicConfigurator.configure();
301         // And raise the log level quite high
302         Logger rootLogger = Logger.getRootLogger();
303         rootLogger.setLevel(Level.FATAL);
304
305         DNSSECValTool dr = new DNSSECValTool();
306
307         try {
308             // Parse the command line options
309             for (String arg : argv) {
310
311                 if (arg.indexOf('=') < 0) {
312                     System.err.println("Unrecognized option: " + arg);
313                     usage();
314                     System.exit(1);
315                 }
316
317                 String[] split_arg = arg.split("=", 2);
318                 String opt = split_arg[0];
319                 String optarg = split_arg[1];
320
321                 if (opt.equals("server")) {
322                     dr.server = optarg;
323                 } else if (opt.equals("query")) {
324                     dr.query = optarg;
325                 } else if (opt.equals("query_file")) {
326                     dr.queryFile = optarg;
327                 } else if (opt.equals("count")) {
328                     dr.count = Util.parseInt(optarg, 0);
329                 } else if (opt.equals("error_file")) {
330                     dr.errorFile = optarg;
331                 } else if (opt.equals("dnskey_file")) {
332                     dr.dnskeyFile = optarg;
333                 } else if (opt.equals("dnskey_query")) {
334                     if (dr.dnskeyNames == null) {
335                         dr.dnskeyNames = new ArrayList<String>();
336                     }
337                     dr.dnskeyNames.add(optarg);
338                 } else if (opt.equals("debug")) {
339                     dr.debug = Boolean.parseBoolean(optarg);
340                 } else {
341                     System.err.println("Unrecognized option: " + opt);
342                     usage();
343                     System.exit(1);
344                 }
345             }
346
347             // Check for minimum usage
348             if (dr.server == null) {
349                 System.err.println("'server' must be specified");
350                 usage();
351                 System.exit(1);
352             }
353             if (dr.query == null && dr.queryFile == null) {
354                 System.err.println("Either 'query' or 'query_file' must be specified");
355                 usage();
356                 System.exit(1);
357             }
358             if (dr.dnskeyFile == null && dr.dnskeyNames == null) {
359                 System.err.println("Either 'dnskey_file' or 'dnskey_query' must be specified");
360                 usage();
361                 System.exit(1);
362             }
363
364             // Execute the job
365             dr.execute();
366
367         } catch (Exception e) {
368             e.printStackTrace();
369             System.exit(1);
370         }
371     }
372 }