fold in the next changes entry
[tor.git] / contrib / exitlist
blob3fd26b51664cd1fb634195563ed6a7334ad910d0
1 #!/usr/bin/python
2 # Copyright 2005-2006 Nick Mathewson
3 # See the LICENSE file in the Tor distribution for licensing information.
5 # Requires Python 2.2 or later.
7 """
8 exitlist -- Given a Tor directory on stdin, lists the Tor servers
9 that accept connections to given addreses.
11 example usage:
13 cat ~/.tor/cached-descriptors* | python exitlist 18.244.0.188:80
15 You should look at the "FetchUselessDescriptors" and "FetchDirInfoEarly"
16 config options in the man page.
18 Note that this script won't give you a perfect list of IP addresses
19 that might connect to you using Tor.
20 False negatives:
21 - Some Tor servers might exit from other addresses than the one they
22 publish in their descriptor.
23 False positives:
24 - This script just looks at the descriptor lists, so it counts relays
25 that were running a day in the past and aren't running now (or are
26 now running at a different address).
28 See https://check.torproject.org/ for an alternative (more accurate!)
29 approach.
31 """
34 # Change this to True if you want more verbose output. By default, we
35 # only print the IPs of the servers that accept any the listed
36 # addresses, one per line.
38 VERBOSE = False
41 # Change this to True if you want to reverse the output, and list the
42 # servers that accept *none* of the listed addresses.
44 INVERSE = False
47 # Change this list to contain all of the target services you are interested
48 # in. It must contain one entry per line, each consisting of an IPv4 address,
49 # a colon, and a port number. This default is only used if we don't learn
50 # about any addresses from the command-line.
52 ADDRESSES_OF_INTEREST = """
53 1.2.3.4:80
54 """
58 # YOU DO NOT NEED TO EDIT AFTER THIS POINT.
61 import sys
62 import re
63 import getopt
64 import socket
65 import struct
66 import time
68 assert sys.version_info >= (2,2)
71 def maskIP(ip,mask):
72 return "".join([chr(ord(a) & ord(b)) for a,b in zip(ip,mask)])
74 def maskFromLong(lng):
75 return struct.pack("!L", lng)
77 def maskByBits(n):
78 return maskFromLong(0xffffffffl ^ ((1L<<(32-n))-1))
80 class Pattern:
81 """
82 >>> import socket
83 >>> ip1 = socket.inet_aton("192.169.64.11")
84 >>> ip2 = socket.inet_aton("192.168.64.11")
85 >>> ip3 = socket.inet_aton("18.244.0.188")
87 >>> print Pattern.parse("18.244.0.188")
88 18.244.0.188/255.255.255.255:1-65535
89 >>> print Pattern.parse("18.244.0.188/16:*")
90 18.244.0.0/255.255.0.0:1-65535
91 >>> print Pattern.parse("18.244.0.188/2.2.2.2:80")
92 2.0.0.0/2.2.2.2:80-80
93 >>> print Pattern.parse("192.168.0.1/255.255.00.0:22-25")
94 192.168.0.0/255.255.0.0:22-25
95 >>> p1 = Pattern.parse("192.168.0.1/255.255.00.0:22-25")
96 >>> import socket
97 >>> p1.appliesTo(ip1, 22)
98 False
99 >>> p1.appliesTo(ip2, 22)
100 True
101 >>> p1.appliesTo(ip2, 25)
102 True
103 >>> p1.appliesTo(ip2, 26)
104 False
106 def __init__(self, ip, mask, portMin, portMax):
107 self.ip = maskIP(ip,mask)
108 self.mask = mask
109 self.portMin = portMin
110 self.portMax = portMax
112 def __str__(self):
113 return "%s/%s:%s-%s"%(socket.inet_ntoa(self.ip),
114 socket.inet_ntoa(self.mask),
115 self.portMin,
116 self.portMax)
118 def parse(s):
119 if ":" in s:
120 addrspec, portspec = s.split(":",1)
121 else:
122 addrspec, portspec = s, "*"
124 if addrspec == '*':
125 ip,mask = "\x00\x00\x00\x00","\x00\x00\x00\x00"
126 elif '/' not in addrspec:
127 ip = socket.inet_aton(addrspec)
128 mask = "\xff\xff\xff\xff"
129 else:
130 ip,mask = addrspec.split("/",1)
131 ip = socket.inet_aton(ip)
132 if "." in mask:
133 mask = socket.inet_aton(mask)
134 else:
135 mask = maskByBits(int(mask))
137 if portspec == '*':
138 portMin = 1
139 portMax = 65535
140 elif '-' not in portspec:
141 portMin = portMax = int(portspec)
142 else:
143 portMin, portMax = map(int,portspec.split("-",1))
145 return Pattern(ip,mask,portMin,portMax)
147 parse = staticmethod(parse)
149 def appliesTo(self, ip, port):
150 return ((maskIP(ip,self.mask) == self.ip) and
151 (self.portMin <= port <= self.portMax))
153 class Policy:
155 >>> import socket
156 >>> ip1 = socket.inet_aton("192.169.64.11")
157 >>> ip2 = socket.inet_aton("192.168.64.11")
158 >>> ip3 = socket.inet_aton("18.244.0.188")
160 >>> pol = Policy.parseLines(["reject *:80","accept 18.244.0.188:*"])
161 >>> print str(pol).strip()
162 reject 0.0.0.0/0.0.0.0:80-80
163 accept 18.244.0.188/255.255.255.255:1-65535
164 >>> pol.accepts(ip1,80)
165 False
166 >>> pol.accepts(ip3,80)
167 False
168 >>> pol.accepts(ip3,81)
169 True
172 def __init__(self, lst):
173 self.lst = lst
175 def parseLines(lines):
176 r = []
177 for item in lines:
178 a,p=item.split(" ",1)
179 if a == 'accept':
180 a = True
181 elif a == 'reject':
182 a = False
183 else:
184 raise ValueError("Unrecognized action %r",a)
185 p = Pattern.parse(p)
186 r.append((p,a))
187 return Policy(r)
189 parseLines = staticmethod(parseLines)
191 def __str__(self):
192 r = []
193 for pat, accept in self.lst:
194 rule = accept and "accept" or "reject"
195 r.append("%s %s\n"%(rule,pat))
196 return "".join(r)
198 def accepts(self, ip, port):
199 for pattern,accept in self.lst:
200 if pattern.appliesTo(ip,port):
201 return accept
202 return True
204 class Server:
205 def __init__(self, name, ip, policy, published, fingerprint):
206 self.name = name
207 self.ip = ip
208 self.policy = policy
209 self.published = published
210 self.fingerprint = fingerprint
212 def uniq_sort(lst):
213 d = {}
214 for item in lst: d[item] = 1
215 lst = d.keys()
216 lst.sort()
217 return lst
219 def run():
220 global VERBOSE
221 global INVERSE
222 global ADDRESSES_OF_INTEREST
224 if len(sys.argv) > 1:
225 try:
226 opts, pargs = getopt.getopt(sys.argv[1:], "vx")
227 except getopt.GetoptError, e:
228 print """
229 usage: cat ~/.tor/cached-routers* | %s [-v] [-x] [host:port [host:port [...]]]
230 -v verbose output
231 -x invert results
232 """ % sys.argv[0]
233 sys.exit(0)
235 for o, a in opts:
236 if o == "-v":
237 VERBOSE = True
238 if o == "-x":
239 INVERSE = True
240 if len(pargs):
241 ADDRESSES_OF_INTEREST = "\n".join(pargs)
243 servers = []
244 policy = []
245 name = ip = None
246 published = 0
247 fp = ""
248 for line in sys.stdin.xreadlines():
249 if line.startswith('router '):
250 if name:
251 servers.append(Server(name, ip, Policy.parseLines(policy),
252 published, fp))
253 _, name, ip, rest = line.split(" ", 3)
254 policy = []
255 published = 0
256 fp = ""
257 elif line.startswith('fingerprint') or \
258 line.startswith('opt fingerprint'):
259 elts = line.strip().split()
260 if elts[0] == 'opt': del elts[0]
261 assert elts[0] == 'fingerprint'
262 del elts[0]
263 fp = "".join(elts)
264 elif line.startswith('accept ') or line.startswith('reject '):
265 policy.append(line.strip())
266 elif line.startswith('published '):
267 date = time.strptime(line[len('published '):].strip(),
268 "%Y-%m-%d %H:%M:%S")
269 published = time.mktime(date)
271 if name:
272 servers.append(Server(name, ip, Policy.parseLines(policy), published,
273 fp))
275 targets = []
276 for line in ADDRESSES_OF_INTEREST.split("\n"):
277 line = line.strip()
278 if not line: continue
279 p = Pattern.parse(line)
280 targets.append((p.ip, p.portMin))
282 # remove all but the latest server of each IP/Nickname pair.
283 latest = {}
284 for s in servers:
285 if (not latest.has_key((s.fingerprint))
286 or s.published > latest[(s.fingerprint)]):
287 latest[s.fingerprint] = s
288 servers = latest.values()
290 accepters, rejecters = {}, {}
291 for s in servers:
292 for ip,port in targets:
293 if s.policy.accepts(ip,port):
294 accepters[s.ip] = s
295 break
296 else:
297 rejecters[s.ip] = s
299 # If any server at IP foo accepts, the IP does not reject.
300 for k in accepters.keys():
301 if rejecters.has_key(k):
302 del rejecters[k]
304 if INVERSE:
305 printlist = rejecters.values()
306 else:
307 printlist = accepters.values()
309 ents = []
310 if VERBOSE:
311 ents = uniq_sort([ "%s\t%s"%(s.ip,s.name) for s in printlist ])
312 else:
313 ents = uniq_sort([ s.ip for s in printlist ])
314 for e in ents:
315 print e
317 def _test():
318 import doctest, exitparse
319 return doctest.testmod(exitparse)
320 #_test()
322 run()