Merge branch 'bug8956_tweak'
[pluggable-transports/obfsproxy.git] / obfsproxy / network / extended_orport.py
1 import os
2
3 from twisted.internet import reactor
4
5 import obfsproxy.common.log as logging
6 import obfsproxy.common.serialize as srlz
7 import obfsproxy.common.hmac_sha256 as hmac_sha256
8 import obfsproxy.common.rand as rand
9
10 import obfsproxy.network.network as network
11
12 log = logging.get_obfslogger()
13
14 # Authentication states:
15 STATE_WAIT_FOR_AUTH_TYPES = 1
16 STATE_WAIT_FOR_SERVER_NONCE = 2
17 STATE_WAIT_FOR_AUTH_RESULTS = 3
18 STATE_WAIT_FOR_OKAY = 4
19 STATE_OPEN = 5
20
21 # Authentication protocol parameters:
22 AUTH_PROTOCOL_HEADER_LEN = 4
23
24 # Safe-cookie authentication parameters:
25 AUTH_SERVER_TO_CLIENT_CONST = "ExtORPort authentication server-to-client hash"
26 AUTH_CLIENT_TO_SERVER_CONST = "ExtORPort authentication client-to-server hash"
27 AUTH_NONCE_LEN = 32
28 AUTH_HASH_LEN = 32
29
30 # Extended ORPort commands:
31 # Transport-to-Bridge
32 EXT_OR_CMD_TB_DONE = 0x0000
33 EXT_OR_CMD_TB_USERADDR = 0x0001
34 EXT_OR_CMD_TB_TRANSPORT = 0x0002
35
36 # Bridge-to-Transport
37 EXT_OR_CMD_BT_OKAY = 0x1000
38 EXT_OR_CMD_BT_DENY = 0x1001
39 EXT_OR_CMD_BT_CONTROL = 0x1002
40
41 # Authentication cookie parameters
42 AUTH_COOKIE_LEN = 32
43 AUTH_COOKIE_HEADER_LEN = 32
44 AUTH_COOKIE_FILE_LEN = AUTH_COOKIE_LEN + AUTH_COOKIE_HEADER_LEN
45 AUTH_COOKIE_HEADER = "! Extended ORPort Auth Cookie !\x0a"
46
47 def _read_auth_cookie(cookie_path):
48 """
49 Read an Extended ORPort authentication cookie from 'cookie_path' and return it.
50 Throw CouldNotReadCookie if we couldn't read the cookie.
51 """
52
53 # Check if file exists.
54 if not os.path.exists(cookie_path):
55 raise CouldNotReadCookie("'%s' doesn't exist" % cookie_path)
56
57 # Check its size and make sure it's correct before opening.
58 auth_cookie_file_size = os.path.getsize(cookie_path)
59 if auth_cookie_file_size != AUTH_COOKIE_FILE_LEN:
60 raise CouldNotReadCookie("Cookie '%s' is the wrong size (%i bytes instead of %d)" % \
61 (cookie_path, auth_cookie_file_size, AUTH_COOKIE_FILE_LEN))
62
63 try:
64 with file(cookie_path, 'rb', 0) as f:
65 header = f.read(AUTH_COOKIE_HEADER_LEN) # first 32 bytes are the header
66
67 if header != AUTH_COOKIE_HEADER:
68 raise CouldNotReadCookie("Corrupted cookie file header '%s'." % header)
69
70 return f.read(AUTH_COOKIE_LEN) # nexta 32 bytes should be the cookie.
71
72 except IOError, exc:
73 raise CouldNotReadCookie("Unable to read '%s' (%s)" % (cookie_path, exc))
74
75 class ExtORPortProtocol(network.GenericProtocol):
76 """
77 Represents a connection to the Extended ORPort. It begins by
78 completing the Extended ORPort authentication, then sending some
79 Extended ORPort commands, and finally passing application-data
80 like it would do to an ORPort.
81
82 Specifically, after completing the Extended ORPort authentication
83 we send a USERADDR command with the address of our client, a
84 TRANSPORT command with the name of the pluggable transport, and a
85 DONE command to signal that we are done with the Extended ORPort
86 protocol. Then we wait for an OKAY command back from the server to
87 start sending application-data.
88
89 Attributes:
90 state: The protocol state the connections is currently at.
91 ext_orport_addr: The address of the Extended ORPort.
92
93 peer_addr: The address of the client, in the other side of the
94 circuit, that connected to our downstream side.
95 cookie_file: Path to the Extended ORPort authentication cookie.
96 client_nonce: A random nonce used in the Extended ORPort
97 authentication protocol.
98 client_hash: Our hash which is used to verify our knowledge of the
99 authentication cookie in the Extended ORPort Authentication
100 protocol.
101 """
102 def __init__(self, circuit, ext_orport_addr, cookie_file, peer_addr, transport_name):
103 self.state = STATE_WAIT_FOR_AUTH_TYPES
104 self.name = "ext_%s" % hex(id(self))
105
106 self.ext_orport_addr = ext_orport_addr
107 self.peer_addr = peer_addr
108 self.cookie_file = cookie_file
109
110 self.client_nonce = rand.random_bytes(AUTH_NONCE_LEN)
111 self.client_hash = None
112
113 self.transport_name = transport_name
114
115 network.GenericProtocol.__init__(self, circuit)
116
117 def connectionMade(self):
118 pass
119
120 def dataReceived(self, data_rcvd):
121 """
122 We got some data, process it according to our current state.
123 """
124
125 self.buffer.write(data_rcvd)
126
127 if self.state == STATE_WAIT_FOR_AUTH_TYPES:
128 try:
129 self._handle_auth_types()
130 except NeedMoreData:
131 return
132 except UnsupportedAuthTypes, err:
133 log.warning("Extended ORPort Cookie Authentication failed: %s" % err)
134 self.close()
135 return
136
137 self.state = STATE_WAIT_FOR_SERVER_NONCE
138
139 if self.state == STATE_WAIT_FOR_SERVER_NONCE:
140 try:
141 self._handle_server_nonce_and_hash()
142 except NeedMoreData:
143 return
144 except (CouldNotReadCookie, RcvdInvalidAuth) as err:
145 log.warning("Extended ORPort Cookie Authentication failed: %s" % err)
146 self.close()
147 return
148
149 self.state = STATE_WAIT_FOR_AUTH_RESULTS
150
151 if self.state == STATE_WAIT_FOR_AUTH_RESULTS:
152 try:
153 self._handle_auth_results()
154 except NeedMoreData:
155 return
156 except AuthFailed, err:
157 log.warning("Extended ORPort Cookie Authentication failed: %s" % err)
158 self.close()
159 return
160
161 # We've finished the Extended ORPort authentication
162 # protocol. Now send all the Extended ORPort commands we
163 # want to send.
164 try:
165 self._send_ext_orport_commands()
166 except CouldNotWriteExtCommand:
167 self.close()
168 return
169
170 self.state = STATE_WAIT_FOR_OKAY
171
172 if self.state == STATE_WAIT_FOR_OKAY:
173 try:
174 self._handle_okay()
175 except NeedMoreData:
176 return
177 except ExtORPortProtocolFailed as err:
178 log.warning("Extended ORPort Cookie Authentication failed: %s" % err)
179 self.close()
180 return
181
182 self.state = STATE_OPEN
183
184 if self.state == STATE_OPEN:
185 # We are done with the Extended ORPort protocol, we now
186 # treat the Extended ORPort as a normal ORPort.
187 if not self.circuit.circuitIsReady():
188 self.circuit.setUpstreamConnection(self)
189 self.circuit.dataReceived(self.buffer, self)
190
191 def _send_ext_orport_commands(self):
192 """
193 Send all the Extended ORPort commands we want to send.
194
195 Throws CouldNotWriteExtCommand.
196 """
197
198 # Send the actual IP address of our client to the Extended
199 # ORPort, then signal that we are done and that we want to
200 # start transferring application-data.
201 self._write_ext_orport_command(EXT_OR_CMD_TB_USERADDR, '%s:%s' % (self.peer_addr.host, self.peer_addr.port))
202 self._write_ext_orport_command(EXT_OR_CMD_TB_TRANSPORT, '%s' % self.transport_name)
203 self._write_ext_orport_command(EXT_OR_CMD_TB_DONE, '')
204
205 def _handle_auth_types(self):
206 """
207 Read authentication types that the server supports, select
208 one, and send it to the server.
209
210 Throws NeedMoreData and UnsupportedAuthTypes.
211 """
212
213 if len(self.buffer) < 2:
214 raise NeedMoreData('Not enough data')
215
216 data = self.buffer.peek()
217 if '\x00' not in data: # haven't received EndAuthTypes yet
218 log.debug("%s: Got some auth types data but no EndAuthTypes yet." % self.name)
219 raise NeedMoreData('Not EndAuthTypes.')
220
221 # Drain all data up to (and including) the EndAuthTypes.
222 log.debug("%s: About to drain %d bytes from %d." % \
223 (self.name, data.index('\x00')+1, len(self.buffer)))
224 data = self.buffer.read(data.index('\x00')+1)
225
226 if '\x01' not in data:
227 raise UnsupportedAuthTypes("%s: Could not find supported auth type (%s)." % (self.name, repr(data)))
228
229 # Send back chosen auth type.
230 self.write("\x01") # Static, since we only support auth type '1' atm.
231
232 # Since we are doing the safe-cookie protocol, now send our
233 # nonce.
234 # XXX This will need to be refactored out of this function in
235 # the future, when we have more than one auth types.
236 self.write(self.client_nonce)
237
238 def _handle_server_nonce_and_hash(self):
239 """
240 Get the server's nonce and hash, validate them and send our own hash.
241
242 Throws NeedMoreData and RcvdInvalidAuth and CouldNotReadCookie.
243 """
244
245 if len(self.buffer) < AUTH_HASH_LEN + AUTH_NONCE_LEN:
246 raise NeedMoreData('Need more data')
247
248 server_hash = self.buffer.read(AUTH_HASH_LEN)
249 server_nonce = self.buffer.read(AUTH_NONCE_LEN)
250 auth_cookie = _read_auth_cookie(self.cookie_file)
251
252 proper_server_hash = hmac_sha256.hmac_sha256_digest(auth_cookie,
253 AUTH_SERVER_TO_CLIENT_CONST + self.client_nonce + server_nonce)
254
255 log.debug("%s: client_nonce: %s\nserver_nonce: %s\nserver_hash: %s\nproper_server_hash: %s\n" % \
256 (self.name, repr(self.client_nonce), repr(server_nonce), repr(server_hash), repr(proper_server_hash)))
257
258 if proper_server_hash != server_hash:
259 raise RcvdInvalidAuth("%s: Invalid server hash. Authentication failed." % (self.name))
260
261 client_hash = hmac_sha256.hmac_sha256_digest(auth_cookie,
262 AUTH_CLIENT_TO_SERVER_CONST + self.client_nonce + server_nonce)
263
264 # Send our hash.
265 self.write(client_hash)
266
267 def _handle_auth_results(self):
268 """
269 Get the authentication results. See if the authentication
270 succeeded or failed, and take appropriate actions.
271
272 Throws NeedMoreData and AuthFailed.
273 """
274 if len(self.buffer) < 1:
275 raise NeedMoreData("Not enough data for body.")
276
277 result = self.buffer.read(1)
278 if result != '\x01':
279 raise AuthFailed("%s: Authentication failed (%s)!" % (self.name, repr(result)))
280
281 log.debug("%s: Authentication successful!" % self.name)
282
283 def _handle_okay(self):
284 """
285 We've sent a DONE command to the Extended ORPort and we
286 now check if the Extended ORPort liked it or not.
287
288 Throws NeedMoreData and ExtORPortProtocolFailed.
289 """
290
291 cmd, _ = self._get_ext_orport_command(self.buffer)
292 if cmd != EXT_OR_CMD_BT_OKAY:
293 raise ExtORPortProtocolFailed("%s: Unexpected command received (%d) after sending DONE." % (self.name, cmd))
294
295 def _get_ext_orport_command(self, buf):
296 """
297 Reads an Extended ORPort command from 'buf'. Returns (command,
298 body) if it was well-formed, where 'command' is the Extended
299 ORPort command type, and 'body' is its body.
300
301 Throws NeedMoreData.
302 """
303 if len(buf) < AUTH_PROTOCOL_HEADER_LEN:
304 raise NeedMoreData("Not enough data for header.")
305
306 header = buf.peek(AUTH_PROTOCOL_HEADER_LEN)
307 cmd = srlz.ntohs(header[:2])
308 bodylen = srlz.ntohs(header[2:4])
309
310 if (bodylen > len(buf) - AUTH_PROTOCOL_HEADER_LEN): # Not all here yet
311 raise NeedMoreData("Not enough data for body.")
312
313 # We have a whole command. Drain the header.
314 buf.drain(4)
315 body = buf.read(bodylen)
316
317 return (cmd, body)
318
319 def _write_ext_orport_command(self, command, body):
320 """
321 Serialize 'command' and 'body' to an Extended ORPort command
322 and send it to the Extended ORPort.
323
324 Throws CouldNotWriteExtCommand
325 """
326 payload = ''
327
328 if len(body) > 65535: # XXX split instead of quitting?
329 log.warning("Obfsproxy was asked to send Extended ORPort command with more than "
330 "65535 bytes of body. This is not supported by the Extended ORPort "
331 "protocol. Please file a bug.")
332 raise CouldNotWriteExtCommand("Too large body.")
333 if command > 65535:
334 raise CouldNotWriteExtCommand("Not supported command type.")
335
336 payload += srlz.htons(command)
337 payload += srlz.htons(len(body))
338 payload += body # body might be absent (empty string)
339 self.write(payload)
340
341
342 class ExtORPortClientFactory(network.StaticDestinationClientFactory):
343 def __init__(self, circuit, cookie_file, peer_addr, transport_name):
344 self.circuit = circuit
345 self.peer_addr = peer_addr
346 self.cookie_file = cookie_file
347 self.transport_name = transport_name
348
349 self.name = "fact_ext_c_%s" % hex(id(self))
350
351 def buildProtocol(self, addr):
352 return ExtORPortProtocol(self.circuit, addr, self.cookie_file, self.peer_addr, self.transport_name)
353
354 class ExtORPortServerFactory(network.StaticDestinationClientFactory):
355 def __init__(self, ext_or_addrport, ext_or_cookie_file, transport_name, transport_class, pt_config):
356 self.ext_or_host = ext_or_addrport[0]
357 self.ext_or_port = ext_or_addrport[1]
358 self.cookie_file = ext_or_cookie_file
359
360 self.transport_name = transport_name
361 self.transport_class = transport_class
362 self.pt_config = pt_config
363
364 self.name = "fact_ext_s_%s" % hex(id(self))
365
366 def startFactory(self):
367 log.debug("%s: Starting up Extended ORPort server factory." % self.name)
368
369 def buildProtocol(self, addr):
370 log.debug("%s: New connection from %s:%d." % (self.name, log.safe_addr_str(addr.host), addr.port))
371
372 circuit = network.Circuit(self.transport_class())
373
374 # XXX instantiates a new factory for each client
375 clientFactory = ExtORPortClientFactory(circuit, self.cookie_file, addr, self.transport_name)
376 reactor.connectTCP(self.ext_or_host, self.ext_or_port, clientFactory)
377
378 return network.StaticDestinationProtocol(circuit, 'server', addr)
379
380 # XXX Exceptions need more thought and work. Most of these can be generalized.
381 class RcvdInvalidAuth(Exception): pass
382 class AuthFailed(Exception): pass
383 class UnsupportedAuthTypes(Exception): pass
384 class ExtORPortProtocolFailed(Exception): pass
385 class CouldNotWriteExtCommand(Exception): pass
386 class CouldNotReadCookie(Exception): pass
387 class NeedMoreData(Exception): pass