[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[or-cvs] r20617: {} Initial checkin of a script that tells you whether some IP a (in projects/archives/trunk: . exonerator)
Author: kloesing
Date: 2009-09-19 12:16:41 -0400 (Sat, 19 Sep 2009)
New Revision: 20617
Added:
projects/archives/trunk/exonerator/
projects/archives/trunk/exonerator/ExoneraTor.java
projects/archives/trunk/exonerator/HOWTO
projects/archives/trunk/exonerator/LICENSE
Log:
Initial checkin of a script that tells you whether some IP address was a Tor relay.
Added: projects/archives/trunk/exonerator/ExoneraTor.java
===================================================================
--- projects/archives/trunk/exonerator/ExoneraTor.java (rev 0)
+++ projects/archives/trunk/exonerator/ExoneraTor.java 2009-09-19 16:16:41 UTC (rev 20617)
@@ -0,0 +1,347 @@
+/* Copyright 2009 The Tor Project
+ * See LICENSE for licensing information */
+
+import java.io.*;
+import java.math.*;
+import java.text.*;
+import java.util.*;
+import org.bouncycastle.util.encoders.Base64;
+
+public final class ExoneraTor {
+
+ public static void main(final String[] args) throws Exception {
+
+ // check parameters
+ if (args.length < 4 || args.length > 5) {
+ System.err.println("\nUsage: java "
+ + ExoneraTor.class.getSimpleName()
+ + " <descriptor archive directory> <IP address in question> "
+ + "<timestamp, in UTC, formatted as YYYY-MM-DD hh:mm:ss> "
+ + "[<target address>[:<target port>]]\n");
+ return;
+ }
+ File archiveDirectory = new File(args[0]);
+ if (!archiveDirectory.exists() || !archiveDirectory.isDirectory()) {
+ System.err.println("\nDescriptor archive directory + "
+ + archiveDirectory.getAbsolutePath()
+ + " does not exist or is not a directory.\n");
+ return;
+ }
+ String relayIP = args[1];
+ String timestampStr = args[2] + " " + args[3];
+ SimpleDateFormat timeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd HH:mm:ss");
+ timeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ long timestamp = timeFormat.parse(timestampStr).getTime();
+ String target = null, targetIP = null, targetPort = null;
+ String[] targetIPParts = null;
+ if (args.length > 4) {
+ target = args[4];
+ if (target.contains(":")) {
+ targetIP = target.split(":")[0];
+ targetPort = target.split(":")[1];
+ } else {
+ targetIP = target;
+ }
+ targetIPParts = targetIP.replace(".", " ").split(" ");
+ }
+ String DELIMITER = "--------------------------------------------------"
+ + "-------------------------";
+ System.out.println("\nTrying to find out whether " + relayIP + " was "
+ + "running as a Tor relay at " + timestampStr
+ + (target != null ? " permitting exiting to " + target : "")
+ + "...\n\n" + DELIMITER);
+
+ // look for consensus files
+ long timestampTooOld = timestamp - 300 * 60 * 1000;
+ long timestampFrom = timestamp - 180 * 60 * 1000;
+ long timestampTooNew = timestamp + 120 * 60 * 1000;
+ Calendar calTooOld = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ Calendar calFrom = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ Calendar calTooNew = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
+ calTooOld.setTimeInMillis(timestampTooOld);
+ calFrom.setTimeInMillis(timestampFrom);
+ calTooNew.setTimeInMillis(timestampTooNew);
+ System.out.printf("%nLooking for relevant consensuses between "
+ + "%tF %<tT and %s%n", calFrom, timestampStr);
+ SortedSet<File> tooOldConsensuses = new TreeSet<File>();
+ SortedSet<File> relevantConsensuses = new TreeSet<File>();
+ SortedSet<File> tooNewConsensuses = new TreeSet<File>();
+ Stack<File> directoriesLeftToParse = new Stack<File>();
+ directoriesLeftToParse.push(archiveDirectory);
+ SimpleDateFormat consensusTimeFormat = new SimpleDateFormat(
+ "yyyy-MM-dd-HH:mm:ss");
+ consensusTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ while (!directoriesLeftToParse.isEmpty()) {
+ File directoryOrFile = directoriesLeftToParse.pop();
+ if (directoryOrFile.isDirectory()) {
+ for (File fileInDir : directoryOrFile.listFiles()) {
+ directoriesLeftToParse.push(fileInDir);
+ }
+ continue;
+ } else {
+ String filename = directoryOrFile.getName();
+ if (filename.endsWith("consensus")) {
+ long consensusTime = consensusTimeFormat.parse(
+ filename.substring(0, 19)).getTime();
+ if (consensusTime >= timestampTooOld &&
+ consensusTime < timestampFrom)
+ tooOldConsensuses.add(directoryOrFile);
+ else if (consensusTime >= timestampFrom &&
+ consensusTime <= timestamp)
+ relevantConsensuses.add(directoryOrFile);
+ else if (consensusTime > timestamp &&
+ consensusTime <= timestampTooNew)
+ tooNewConsensuses.add(directoryOrFile);
+ }
+ }
+ }
+ SortedSet<File> allConsensuses = new TreeSet<File>();
+ allConsensuses.addAll(tooOldConsensuses);
+ allConsensuses.addAll(relevantConsensuses);
+ allConsensuses.addAll(tooNewConsensuses);
+ if (allConsensuses.isEmpty()) {
+ System.out.println(" None found!\n\n" + DELIMITER + "\n\nResult is "
+ + "INDECISIVE!\n\nWe cannot make any statement about IP address "
+ + relayIP + "being a relay at " + timestampStr + " or not! We "
+ + "did not find any relevant consensuses preceding the given "
+ + "time. This either means that you did not download and "
+ + "extract the consensus archives preceding the hours before "
+ + "the given time, or (in rare cases) that the directory "
+ + "archives are missing the hours before the timestamp. Please "
+ + "check that your directory archives contain consensus files "
+ + "of the interval 5:00 hours before and 2:00 hours after the "
+ + "time you are looking for.\n");
+ return;
+ }
+ for (File f : relevantConsensuses)
+ System.out.println(" " + f.getAbsolutePath());
+
+ // parse consensuses to find descriptors belonging to the IP address
+ System.out.println("\nLooking for descriptor identifiers referenced "
+ + "in \"r \" lines in these consensuses containing IP address "
+ + relayIP + "...");
+ SortedSet<File> positiveConsensusesNoTarget = new TreeSet<File>();
+ Set<String> addressesInSameNetwork = new HashSet<String>();
+ SortedMap<String, Set<File>> relevantDescriptors =
+ new TreeMap<String, Set<File>>();
+ for (File consensus : allConsensuses) {
+ if (relevantConsensuses.contains(consensus))
+ System.out.println(" " + consensus.getAbsolutePath());
+ BufferedReader br = new BufferedReader(new FileReader(consensus));
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (!line.startsWith("r "))
+ continue;
+ String[] parts = line.split(" ");
+ String address = parts[6];
+ if (address.equals(relayIP)) {
+ byte[] result = Base64.decode(parts[3] + "==");
+ String hex = new BigInteger(1, Base64.decode(parts[3] +
+ "==")).toString(16).substring(0, 40);
+ if (!relevantDescriptors.containsKey(hex))
+ relevantDescriptors.put(hex, new HashSet<File>());
+ relevantDescriptors.get(hex).add(consensus);
+ positiveConsensusesNoTarget.add(consensus);
+ if (relevantConsensuses.contains(consensus))
+ System.out.println(" \"" + line + "\" references "
+ + "descriptor " + hex);
+ } else {
+ if (relayIP.startsWith(address.substring(0,
+ address.lastIndexOf(".")))) {
+ addressesInSameNetwork.add(address);
+ }
+ }
+ }
+ br.close();
+ }
+ if (relevantDescriptors.isEmpty()) {
+ System.out.printf(" None found!\n\n" + DELIMITER + "\n\nResult is "
+ + "NEGATIVE with moderate certainty!\n\nWe did not find IP "
+ + "address " + relayIP + " in any of the consensuses that were "
+ + "published between %tF %<tT and %tF %<tT.\n\nA possible "
+ + "reason for false negatives is that the relay is using a "
+ + "different IP address when generating a descriptor than for "
+ + "exiting to the Internet. We hope to provide better checks "
+ + "for this case in the future.", calTooOld, calTooNew);
+ if (!addressesInSameNetwork.isEmpty()) {
+ System.out.println("\n\nThe following other IP addresses of Tor "
+ + "relays were found in the mentioned consensus files that "
+ + "are in the same /24 network and that could be related to "
+ + "IP address " + relayIP + ":");
+ for (String s : addressesInSameNetwork) {
+ System.out.println(" " + s);
+ }
+ }
+ System.out.println();
+ return;
+ }
+
+ // parse router descriptors to check exit policies
+ SortedSet<File> positiveConsensuses = new TreeSet<File>();
+ Set<String> missingDescriptors = new HashSet<String>();
+ if (target != null) {
+ System.out.println("\nChecking if referenced descriptors permit "
+ + "exiting to " + target + "...");
+ Set<String> descriptors = relevantDescriptors.keySet();
+ missingDescriptors.addAll(relevantDescriptors.keySet());
+ directoriesLeftToParse.clear();
+ directoriesLeftToParse.push(archiveDirectory);
+ while (!directoriesLeftToParse.isEmpty()) {
+ File directoryOrFile = directoriesLeftToParse.pop();
+ if (directoryOrFile.isDirectory()) {
+ for (File fileInDir : directoryOrFile.listFiles()) {
+ directoriesLeftToParse.push(fileInDir);
+ }
+ continue;
+ } else {
+ String filename = directoryOrFile.getName();
+ for (String descriptor : descriptors) {
+ if (filename.equals(descriptor)) {
+ missingDescriptors.remove(descriptor);
+ BufferedReader br = new BufferedReader(
+ new FileReader(directoryOrFile));
+ String line;
+ while ((line = br.readLine()) != null) {
+ if (line.startsWith("reject ") ||
+ line.startsWith("accept ")) {
+ boolean ruleAccept = line.split(" ")[0].equals("accept");
+ String ruleAddress = line.split(" ")[1].split(":")[0];
+ if (!ruleAddress.equals("*")) {
+ if (!ruleAddress.contains("/") &&
+ !ruleAddress.equals(targetIP))
+ continue; // IP address does not match
+ String[] ruleIPParts = ruleAddress.split("/")[0].
+ replace(".", " ").split(" ");
+ int ruleNetwork = Integer.parseInt(
+ ruleAddress.split("/")[1]);
+ for (int i = 0; i < 4; i++) {
+ if (ruleNetwork == 0) {
+ break;
+ } else if (ruleNetwork >= 8) {
+ if (ruleIPParts[i].equals(targetIPParts[i]))
+ ruleNetwork -= 8;
+ else
+ break;
+ } else {
+ int mask = 255 ^ 255 >>> ruleNetwork;
+ if ((Integer.parseInt(ruleIPParts[i]) & mask) ==
+ (Integer.parseInt(targetIPParts[i]) & mask))
+ ruleNetwork = 0;
+ break;
+ }
+ }
+ if (ruleNetwork > 0)
+ continue; // IP address does not match
+ }
+ String rulePort = line.split(" ")[1].split(":")[1];
+ if (targetPort == null && !ruleAccept &&
+ !rulePort.equals("*"))
+ continue; // with no port given, we only consider
+ // reject :* rules as matching
+ if (targetPort != null) {
+ if (!rulePort.equals("*") &&
+ !targetPort.equals(rulePort))
+ continue; // ports do not match
+ }
+ boolean relevantMatch = false;
+ for (File f : relevantDescriptors.get(descriptor))
+ if (relevantConsensuses.contains(f))
+ relevantMatch = true;
+ if (relevantMatch)
+ System.out.println(" "
+ + directoryOrFile.getAbsolutePath() + " "
+ + (ruleAccept ? "permits" : "does not permit")
+ + " exiting to " + target + " according to rule \""
+ + line + "\"");
+ if (ruleAccept)
+ positiveConsensuses.addAll(
+ relevantDescriptors.get(descriptor));
+ break;
+ }
+ }
+ br.close();
+ }
+ }
+ }
+ }
+ }
+
+ // print out result
+ Set<File> matches = (target != null) ? positiveConsensuses
+ : positiveConsensusesNoTarget;
+ if (matches.contains(relevantConsensuses.last())) {
+ System.out.println("\n" + DELIMITER + "\n\nResult is POSITIVE with "
+ + "high certainty!\n\nWe found one or more relays on IP address "
+ + relayIP
+ + (target != null ? " permitting exit to " + target : "")
+ + " in the most recent consensus preceding " + timestampStr
+ + " that clients were likely to know.\n");
+ return;
+ }
+ boolean inOtherRelevantConsensus = false, inTooOldConsensuses = false,
+ inTooNewConsensuses = false;
+ for (File f : matches)
+ if (relevantConsensuses.contains(f))
+ inOtherRelevantConsensus = true;
+ else if (tooOldConsensuses.contains(f))
+ inTooOldConsensuses = true;
+ else if (tooNewConsensuses.contains(f))
+ inTooNewConsensuses = true;
+ if (inOtherRelevantConsensus) {
+ System.out.println("\n" + DELIMITER + "\n\nResult is POSITIVE with "
+ + "moderate certainty!\n\nWe found one or more relays on IP "
+ + "address " + relayIP
+ + (target != null ? " permitting exit to " + target : "")
+ + ", but not in the consensus immediately preceding "
+ + timestampStr + ". A possible reason for the relay being "
+ + "missing in the last consensus preceding the given time might "
+ + "be that some of the directory authorities had difficulties "
+ + "connecting to the relay. However, clients might still have "
+ + "used the relay.");
+ } else {
+ System.out.println("Result is NEGATIVE with high certainty!\n\nWe "
+ + "did not find any relay on IP address " + relayIP
+ + (target != null ? " permitting exit to " + target : "")
+ + "in the consensuses 3:00 hours preceding " + timestampStr
+ + ".");
+ if (inTooOldConsensuses || inTooNewConsensuses) {
+ if (inTooOldConsensuses && !inTooNewConsensuses)
+ System.out.println("\nNote that we found a matching relay in "
+ + "consensuses that were published between 5:00 and 3:00 "
+ + "hours before " + timestampStr + ". ");
+ else if (!inTooOldConsensuses && inTooNewConsensuses)
+ System.out.println("\nNote that we found a matching relay in "
+ + "consensuses that were published up to 2:00 hours after "
+ + timestampStr + ". ");
+ else
+ System.out.println("\nNote that we found a matching relay in "
+ + "consensuses that were published between 5:00 and 3:00 "
+ + "hours before and in consensuses that were published up "
+ + "to 2:00 hours after " + timestampStr + ". ");
+ System.out.println("Make sure that the timestamp you provided is "
+ + "in the correct timezone: UTC (or GMT).");
+ }
+ }
+ if (target != null) {
+ if (positiveConsensuses.isEmpty() &&
+ !positiveConsensusesNoTarget.isEmpty())
+ System.out.println("\nNote that although the found relay(s) did "
+ + "not permit exiting to " + target + ", there have been one "
+ + "or more relays running at the given time.");
+ if (!missingDescriptors.isEmpty()) {
+ System.out.println("\nNote that not all referenced descriptors "
+ + "could be found. We cannot make any good statement about "
+ + "exit relays without these descriptors. Make sure you "
+ + "downloaded and extracted the server descriptors of the "
+ + "given time. (In rare cases it also happens that we are "
+ + "missing single descriptors in the archives.) The following "
+ + "descriptors are missing:");
+ for (String desc : missingDescriptors)
+ System.out.println(" " + desc);
+ }
+ }
+ System.out.println();
+ }
+}
+
Property changes on: projects/archives/trunk/exonerator/ExoneraTor.java
___________________________________________________________________
Added: svn:mergeinfo
+
Added: projects/archives/trunk/exonerator/HOWTO
===================================================================
--- projects/archives/trunk/exonerator/HOWTO (rev 0)
+++ projects/archives/trunk/exonerator/HOWTO 2009-09-19 16:16:41 UTC (rev 20617)
@@ -0,0 +1,110 @@
+ExoneraTor
+ or: a script that tells you whether some IP address was a Tor relay
+
+---------------------------------------------------------------------------
+
+Introduction:
+
+Some people have expressed the desire to learn whether a given IP address
+has been a Tor relay at a certain time. In addition to that, these people
+might want to know whether the IP address permitted exit to a given address
+and port.
+
+Answering these questions can be important for Tor relay operators to show
+to the authorities that an anonymous user might have conducted bad things
+with their IP address. Likewise, police investigators might be interested
+in the answer to these questions, too, in order to decide whether to
+proceed with their investigations or not.
+
+We can answer the above questions from looking at the descriptor archives
+that are available since late 2007 (or even beyond, but this script only
+works with the data format that was produced starting in October 2007).
+This script parses the directory archives to print out the answer whether
+a certain IP address was a Tor relay at a given time. The script further
+prints out all intermediate steps in answering this, so that users can
+confirm the correctness of the result themselves.
+
+---------------------------------------------------------------------------
+
+Quick Start:
+
+In order to run this script, you need to install and download the following
+software and data (please note that all instructions are written for Linux;
+commands for Windows or Mac OS X may vary):
+
+- Install Java 6 or higher.
+
+- Download the BouncyCastle provider that includes Base 64 decoding from
+ http://www.bouncycastle.org/download/bcprov-jdk16-143.jar and put it in
+ your working directory, e.g. /home/you/exonerator/ .
+
+- Copy the consensuses-* and server-descriptors-* files of the relevant
+ time from http://archive.torproject.org/tor-directory-authority-archive/
+ and extract them to a directory in your working directory, e.g.
+ /home/you/exonerator/data/ .
+
+ Note that all files are touched by the script at least once. You might
+ want to avoid putting the archives of more than, say, two months in that
+ directory for your evaluation. You may just temporarily move the
+ irrelevant archives away to another directory, e.g., data-tmp/ in your
+ working directory.
+
+ Also note that you only need the server-descriptors-* files if you want
+ to learn whether a given IP address permits exiting to a given target. If
+ you only want to learn whether that IP address was a Tor relay, you
+ don't need them.
+
+- Compile the (single) Java class using this command:
+
+ $ javac -cp bcprov-jdk16-143.jar ExoneraTor.java
+
+- Run the script, providing it with the parameters it needs:
+
+ java -cp .:bcprov-jdk16-143.jar ExoneraTor
+ <descriptor archive directory>
+ <IP address in question>
+ <timestamp, in UTC, formatted as YYYY-MM-DD hh:mm:ss>
+ [<target address>[:<target port>]]
+
+ Make sure that the timestamp is provided in UTC, which is similar to GMT,
+ and not in your local timezone! Otherwise, results will very likely be
+ wrong.
+
+ A sample invocation might be (without line break):
+
+ $ java -cp .:bcprov-jdk16-143.jar ExoneraTor data/ 209.17.171.104
+ 2009-08-15 16:05:00 209.85.129.104:80
+
+---------------------------------------------------------------------------
+
+Test cases:
+
+The following test cases work with the August 2009 archives and can be used
+to check whether this script works correctly (line breaks have been added
+only for formatting reasons here):
+
+- Positive result of echelon1+2 being a relay:
+
+ $ java -cp .:bcprov-jdk16-141.jar ExoneraTor data/ 209.17.171.104
+ 2009-08-15 16:05:00
+
+- Positive result of echelon1+2 exiting to google.com on any port
+
+ $ java -cp .:bcprov-jdk16-141.jar ExoneraTor data/ 209.17.171.104
+ 2009-08-15 16:05:00 209.85.129.104
+
+- Positive result of echelon1+2 exiting to google.com on port 80
+
+ $ java -cp .:bcprov-jdk16-141.jar ExoneraTor data/ 209.17.171.104
+ 2009-08-15 16:05:00 209.85.129.104:80
+
+- Negative result of echelon1+2 exiting to google.com, but not on port 25
+
+ $ java -cp .:bcprov-jdk16-141.jar ExoneraTor data/ 209.17.171.104
+ 2009-08-15 16:05:00 209.85.129.104:25
+
+- Negative result with IP address of echelon1+2 changed in the last octet
+
+ $ java -cp .:bcprov-jdk16-141.jar ExoneraTor data/ 209.17.171.50
+ 2009-08-15 16:05:00
+
Property changes on: projects/archives/trunk/exonerator/HOWTO
___________________________________________________________________
Added: svn:mergeinfo
+
Added: projects/archives/trunk/exonerator/LICENSE
===================================================================
--- projects/archives/trunk/exonerator/LICENSE (rev 0)
+++ projects/archives/trunk/exonerator/LICENSE 2009-09-19 16:16:41 UTC (rev 20617)
@@ -0,0 +1,30 @@
+Copyright 2009 The Tor Project
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following disclaimer
+ in the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the names of the copyright owners nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
Property changes on: projects/archives/trunk/exonerator/LICENSE
___________________________________________________________________
Added: svn:mergeinfo
+