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