4
## This uses a custom txtorcon.IStreamAttacher to force streams to use
5
## circuits that exit in the same country (as supplied by GeoIP) and
6
## builds such a circuit if one isn't available yet.
8
## Note that you can do something very similar to this with Tor's
9
## config file as well by setting something like:
11
## ExitNodes {us},{ca}
13
## ...in your torrc. The above just exits from those countries, not
14
## the one in which the Web server is located, however. So, this is a
15
## little redundant, but gives you the idea of how to do these sorts
18
## Another thing to note is that the DNS lookup is a stream before the
19
## name is looked up, so the DNS lookup may occur from whatever stream
20
## Tor chose for that (we return None, which causes the attacher to
21
## tell Tor to attach that stream itself). This presents a problem for
22
## sites which optimize the server they deliver based on DNS -- if you
23
## lookup from X you'll get a server near/in X, which for our next
24
## step will make "the site" appear to be there.
26
## The only "solution" for this would be to do the lookup locally, but
27
## that defeats the purpose of Tor.
32
from twisted.python import log
33
from twisted.internet import reactor, defer
34
from zope.interface import implements
39
class MyStreamListener(txtorcon.StreamListenerMixin):
41
def stream_new(self, stream):
42
print "new stream:", stream.id, stream.target_host
44
def stream_succeeded(self, stream):
45
print "successful stream:", stream.id, stream.target_host
47
def stream_attach(self, stream, circuit):
48
print "stream", stream.id, " attached to circuit", circuit.id,
49
print "with path:", '->'.join(map(lambda x: x.location.countrycode, circuit.path))
52
class MyAttacher(txtorcon.CircuitListenerMixin):
53
implements(txtorcon.IStreamAttacher)
55
def __init__(self, state):
56
## pointer to our TorState object
58
## circuits for which we are awaiting completion so we can
59
## finish our attachment to them.
60
self.waiting_circuits = []
62
def waiting_on(self, circuit):
63
for (circid, d, stream_cc) in self.waiting_circuits:
64
if circuit.id == circid:
68
def circuit_extend(self, circuit, router):
70
if circuit.purpose != 'GENERAL':
72
# only output for circuits we're waiting on
73
if self.waiting_on(circuit):
74
print " circuit %d (%s). Path now %s" % (circuit.id, router.id_hex,
75
'->'.join(map(lambda x: x.location.countrycode,
78
def circuit_built(self, circuit):
80
if circuit.purpose != 'GENERAL':
83
print "circuit built", circuit.id, '->'.join(map(lambda r:
84
r.location.countrycode,
86
for (circid, d, stream_cc) in self.waiting_circuits:
87
if circid == circuit.id:
88
self.waiting_circuits.remove((circid, d, stream_cc))
91
def circuit_failed(self, circuit, kw):
92
if self.waiting_on(circuit):
93
print "A circuit we requested", circuit.id, "has failed. Reason:", kw['REASON']
95
circid, d, stream_cc = None, None, None
96
for x in self.waiting_circuits:
97
if x[0] == circuit.id:
98
circid, d, stream_cc = x
100
raise Exception("Expected to find circuit.")
102
self.waiting_circuits.remove((circid, d, stream_cc))
103
print "Trying a new circuit build for", circid
104
self.request_circuit_build(stream_cc, d)
106
def attach_stream(self, stream, circuits):
110
if not stream.target_host in self.state.addrmap.addr:
111
print "No AddrMap entry for", stream.target_host,
112
print "so I don't know where it exits; get Tor to attach stream."
115
ip = str(self.state.addrmap.addr[stream.target_host].ip)
116
stream_cc = txtorcon.util.NetLocation(ip).countrycode
117
print "Stream to", ip, "exiting in", stream_cc
119
if stream_cc is None:
120
## returning None tells TorState to ask Tor to select a
122
print " unknown country, Tor will assign stream"
125
for circ in circuits.values():
126
if circ.state != 'BUILT' or circ.purpose != 'GENERAL':
129
circuit_cc = circ.path[-1].location.countrycode
130
if circuit_cc is None:
131
print "warning: don't know where circuit", circ.id, "exits"
133
if circuit_cc == stream_cc:
134
print " found suitable circuit:", circ
137
## if we get here, we haven't found a circuit that exits in
138
## the country GeoIP claims our target server is in, so we
139
## need to build one.
140
print "Didn't find a circuit, building one"
142
## we need to return a Deferred which will callback with our
143
## circuit, however built_circuit only callbacks with the
144
## message from Tor saying it heard about our request. So when
145
## that happens, we push our real Deferred into the
146
## waiting_circuits list which will get pop'd at some point
147
## when the circuit_built() listener callback happens.
150
self.request_circuit_build(stream_cc, d)
153
def request_circuit_build(self, stream_cc, deferred_to_callback):
154
## for exits, we can select from any router that's in the
156
last = filter(lambda x: x.location.countrycode == stream_cc,
157
self.state.routers.values())
159
## start with an entry guard, put anything in the middle and
160
## put one of our exits at the end.
161
path = [random.choice(self.state.entry_guards.values()),
162
random.choice(self.state.routers.values()),
165
print " requesting a circuit:", '->'.join(map(lambda r:
166
r.location.countrycode,
170
def __init__(self, attacher, d, stream_cc):
171
self.attacher = attacher
173
self.stream_cc = stream_cc
175
def __call__(self, circ):
177
return from build_circuit is a Circuit. However, we
178
want to wait until it is built before we can issue an
179
attach on it and callback to the Deferred we issue
182
print " my circuit is in progress", circ.id
183
self.attacher.waiting_circuits.append((circ.id, self.d,
186
return self.state.build_circuit(path).addCallback(AppendWaiting(self, deferred_to_callback, stream_cc)).addErrback(log.err)
190
print "Connected to a Tor version", state.protocol.version
192
attacher = MyAttacher(state)
193
state.set_attacher(attacher, reactor)
194
state.add_circuit_listener(attacher)
196
state.add_stream_listener(MyStreamListener())
198
print "Existing state when we connected:"
200
for s in state.streams.values():
204
print "General-purpose circuits:"
205
for c in filter(lambda x: x.purpose == 'GENERAL', state.circuits.values()):
206
print ' ', c.id, '->'.join(map(lambda x: x.location.countrycode,
210
def setup_failed(arg):
211
print "SETUP FAILED", arg
214
d = txtorcon.build_local_tor_connection(reactor)
215
d.addCallback(do_setup).addErrback(setup_failed)