
commit c1092dcf4b4bc5e95d2d37d69feb3a2c62cd85ab Author: Harry Bock <hbock@ele.uri.edu> Date: Fri Sep 3 18:53:14 2010 -0400 Specify and implement narrow exit policy check. A "narrow" port is a port that a router explicitly rejects our active tester but could possibly accept other traffic; e.g., from an exit enclave. Add a NarrowPorts data field in TorBEL exports. Implement the check, export, and import functionality. --- doc/data-spec.txt | 5 ++++ query.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++-- router.py | 30 ++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 6 deletions(-) diff --git a/doc/data-spec.txt b/doc/data-spec.txt index 58dbb7e..c5f4014 100644 --- a/doc/data-spec.txt +++ b/doc/data-spec.txt @@ -43,6 +43,11 @@ Status: Draft * FailedPorts: A list of ports that were not reachable by TorBEL, either due to its exit policy or a temporary failure, when it was last tested. + * NarrowPorts: A list of ports that were not reachable by TorBEL due to + a narrow exit policy that does not include the TorBEL + test server. Consumers should treat NarrowPorts as + working according to the currently published exit policy + for the router. The following data are supplied for convenience to the consumer: diff --git a/query.py b/query.py index 2f223d7..f1d0046 100644 --- a/query.py +++ b/query.py @@ -44,6 +44,11 @@ class Router: self.exit_policy = self._build_exit_policy(data["ExitPolicy"]) self.working_ports = data["WorkingPorts"] self.failed_ports = data["FailedPorts"] + self.narrow_ports = data["NarrowPorts"] + + @property + def exit_policy_string(self): + return "; ".join(map(str, self.exit_policy)) def _build_exit_policy(self, policy_list): return [ExitPolicyRule(line) for line in policy_list] @@ -58,7 +63,24 @@ class Router: # will be accepted." return True - def will_exit_to(self, ip, port): + def is_narrow_exit(self, ip, port): + """ Returns True if this router accepts exit traffic to port + on some IP addresses but rejects traffic to ip. This can be + used to detect exit enclaves. """ + can_accept = False + for line in self.exit_policy: + if line.reject and line.network == _nulladdr and (port >= line.port_low and port <= line.port_high): + can_accept = False + break + + if line.accept and (port >= line.port_low and port <= line.port_high): + can_accept = True + break + + match = self.exit_policy_match(ip, port) + return can_accept and not match + + def will_exit_to(self, ip, port, check_narrow_policy = True): # FIXME: In the case that TorBEL has not actually tested this exit, # we will trust the router's exit policy. This may or may not be # the Right Thing To Do. @@ -69,7 +91,7 @@ class Router: return True elif port in self.failed_ports: return False - + # Treat NarrowPorts the same as not tested. return self.exit_policy_match(ip, port) def will_exit_to_ports(self, ip, port_list): @@ -87,6 +109,7 @@ class ParseError(Exception): _exitline = re.compile(r"^(accept|reject) (.+)") _portspec = re.compile(r"^\d{1,5}|\d{1,5}-\d{1,5}$") _addrspec = re.compile(r"^\[?([\d:.]+)\]?(/[\d:.]+)?$") +_nulladdr = ipaddr.IPAddress("0.0.0.0") class ExitPolicyRule: def __init__(self, line): self.port_low, self.port_high = -1, -1 @@ -139,6 +162,31 @@ class ExitPolicyRule: return True elif self.address and ipaddr.IPAddress(ip) == self.address: return True + + def __str__(self): + # 0.0.0.0/0.0.0.0 => * + if self.network == _nulladdr:# and line.netmask == 0: + ip = "*" + else: + # ipaddr.IPv4Network always converts to CIDR. + if self.network: + ip = str(self.network) + else: + ip = str(self.address) + + # Convert 0-65535 to * + if self.port_low == 0 and self.port_high == 0xffff: + port = "*" + # Use 8 instead of 8-8 + elif self.port_low == self.port_high: + port = str(self.port_low) + else: + port = "%d-%d" % (self.port_low, self.port_high) + + if self.accept: + return "accept " + ip + ":" + port + else: + return "reject " + ip + ":" + port class ExitList: def __init__(self, filename, status_filename = None): @@ -248,7 +296,8 @@ class ExitList: "InConsensus": r[4] == "True", "ExitPolicy": r[5].split(";"), "WorkingPorts": port_list_from_string(r[6]), - "FailedPorts": port_list_from_string(r[7]) + "FailedPorts": port_list_from_string(r[7]), + "NarrowPorts": port_list_from_string(r[8]), } self.add_record(data) @@ -349,6 +398,15 @@ if __name__ == "__main__": output.close() + elif command == "test": + port = int(sys.argv[2]) + e = ExitList("torbel_export.csv") + print "start" + count = 0 + for r in e.cache_ip.itervalues(): + if r.is_narrow_exit(ipaddr.IPAddress("131.128.160.242"), port): + count += 1 + print count, r.nickname, r.exit_policy_string else: usage() sys.exit(1) diff --git a/router.py b/router.py index c77e178..bc71e86 100644 --- a/router.py +++ b/router.py @@ -16,6 +16,7 @@ class RouterRecord(_OldRouterClass): self.test_ports = set(ports) self.working_ports = set() self.failed_ports = set() + self.narrow_ports = set() self.circ_failed = False def passed(self, port): @@ -24,6 +25,9 @@ class RouterRecord(_OldRouterClass): def failed(self, port): self.failed_ports.add(port) + def narrow(self, port): + self.narrow_ports.add(port) + def start(self): self.start_time = time.time() return self @@ -33,7 +37,8 @@ class RouterRecord(_OldRouterClass): return self def is_complete(self): - return self.test_ports <= (self.working_ports | self.failed_ports) + return self.test_ports <= \ + (self.working_ports | self.failed_ports | self.narrow_ports) def __init__(self, *args, **kwargs): _OldRouterClass.__init__(self, *args, **kwargs) @@ -68,6 +73,23 @@ class RouterRecord(_OldRouterClass): return len(self.exitpolicy) == 1 and \ (ep.ip, ep.netmask, ep.port_low, ep.port_high) == (0, 0, 0, 0xffff) + def is_narrow_exit(self, ip, port): + """ Returns True if this router accepts exit traffic to port + on some IP addresses but rejects traffic to ip. This can be + used to detect exit enclaves. """ + can_accept = False + for line in self.exitpolicy: + if not line.match and line.ip == 0 and \ + (port >= line.port_low and port <= line.port_high): + can_accept = False + break + + if line.match and (port >= line.port_low and port <= line.port_high): + can_accept = True + break + + return can_accept and not self.will_exit_to(ip, port) + def should_export(self): """ Returns True if we have found working exit ports, or if we have not found working test ports, if this router has a non-reject-all @@ -171,7 +193,8 @@ class RouterRecord(_OldRouterClass): "LastTestedTimestamp": int(self.last_test.end_time), "ExitPolicy": self.exit_policy_list(), "WorkingPorts": list(self.last_test.working_ports), - "FailedPorts": list(self.last_test.failed_ports) } + "FailedPorts": list(self.last_test.failed_ports), + "NarrowPorts": list(self.last_test.narrow_ports) } def export_csv(self, out): """ Export record in CSV format, given a Python csv.writer instance. """ @@ -186,7 +209,8 @@ class RouterRecord(_OldRouterClass): not self.stale, # InConsensus self.exit_policy_string(), # ExitPolicy list(self.last_test.working_ports), # WorkingPorts - list(self.last_test.failed_ports)]) # FailedPorts + list(self.last_test.failed_ports), # FailedPorts + list(self.last_test.narrow_ports)]) # NarrowPorts def __str__(self): return "%s (%s)" % (self.idhex, self.nickname)