]> code.delx.au - monosys/blob - scripts/tcp-proxy
Rearranged, tidied, deleted things, moved things
[monosys] / scripts / tcp-proxy
1 #!/usr/bin/python2
2
3 """
4 Proxy Utility
5 -------------
6
7 With mode=basic any incoming connections on listen_port will be proxied
8 to host:port. The proxy will only accept connections from hosts in the
9 [allowed] section.
10
11 With mode=proxy the first two lines of incoming connections must be
12 'host\nport\n'. Once again only connections from hosts in the [allowed]
13 section will be accepted. The proxy will connect to host:port and pass
14 bytes in both directions.
15
16 The final mode, mode=interceptor is designed to be combined with a firewall
17 rule and another instance running mode=proxy on another computer.
18 Proxies running in interceptor mode listen on all interfaces. They make
19 connections to the host:port specified in the config file, passing the
20 'capturedhost\ncapturedport\n' onto the destination. They then pass bytes
21 in both directions.
22
23
24 Example - Basic Forwarder
25 -------------------------
26
27 Config to forward all packets from port 8000 on localhost to google.com:80
28 Connections will be accepted from whatever IP address alpha.example.com
29 and beta.example.com point to.
30
31 [proxy]
32 mode = basic
33 listen_port = 8000
34 host = google.com
35 port = 80
36
37 [allowed]
38 host1 = alpha.example.com
39 host2 = beta.example.com
40
41
42
43 Example - Interceptor Proxy Combo
44 ---------------------------------
45
46 Capture all packets destined for port 1935 and send them to an interceptor
47 configured to listen on example.com:9997.
48 On Linux:
49 # iptables -t nat -A PREROUTING -p tcp --dport 1935
50 -j REDIRECT --to-ports 9997
51 # iptables -t nat -A OUTPUT -p tcp --dport 1935
52 -j REDIRECT --to-ports 9997
53
54 On Mac OS X:
55 # ipfw add 50000 fwd 127.0.0.1,9997 tcp from any to any dst-port 1935
56
57 Config to forward these connections to proxy.example.com
58
59 [proxy]
60 mode = interceptor
61 listen_port = 9997
62 host = proxy.example.com
63 port = 9997
64
65
66
67 Config file for proxy.example.com
68
69 [proxy]
70 mode = proxy
71 listen_port = 9997
72
73 [allowed]
74 host1 = alpha.example.com
75 host2 = beta.example.com
76
77
78 """
79
80
81 import asyncore
82 import ConfigParser
83 import os
84 import socket
85 import struct
86 import sys
87 import traceback
88
89
90 if sys.platform == "linux2":
91 try:
92 socket.SO_ORIGINAL_DST
93 except AttributeError:
94 # There is a missing const in the socket module... So we will add it now
95 socket.SO_ORIGINAL_DST = 80
96
97 def get_original_dest(sock):
98 '''Gets the original destination address for connection that has been
99 redirected by netfilter.'''
100 # struct sockaddr_in {
101 # short sin_family; // e.g. AF_INET
102 # unsigned short sin_port; // e.g. htons(3490)
103 # struct in_addr sin_addr; // see struct in_addr, below
104 # char sin_zero[8]; // zero this if you want to
105 # };
106 # struct in_addr {
107 # unsigned long s_addr; // load with inet_aton()
108 # };
109 # getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, (struct sockaddr_in *)&dstaddr, &dstlen);
110
111 data = sock.getsockopt(socket.SOL_IP, socket.SO_ORIGINAL_DST, 16)
112 _, port, a1, a2, a3, a4 = struct.unpack("!HHBBBBxxxxxxxx", data)
113 address = "%d.%d.%d.%d" % (a1, a2, a3, a4)
114 return address, port
115
116
117 elif sys.platform == "darwin":
118 def get_original_dest(sock):
119 '''Gets the original destination address for connection that has been
120 redirected by ipfw.'''
121 return sock.getsockname()
122
123
124
125 class Proxy(asyncore.dispatcher):
126 def __init__(self, arg):
127 if isinstance(arg, tuple):
128 asyncore.dispatcher.__init__(self)
129 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
130 self.connect(arg)
131 else:
132 asyncore.dispatcher.__init__(self, arg)
133 self.init()
134
135 def init(self):
136 self.end = False
137 self.other = None
138 self.buffer = ""
139
140 def meet(self, other):
141 self.other = other
142 other.other = self
143
144 def handle_error(self):
145 print >>sys.stderr, "Proxy error:", traceback.format_exc()
146 self.close()
147
148 def handle_read(self):
149 data = self.recv(8192)
150 if len(data) > 0:
151 self.other.buffer += data
152
153 def handle_write(self):
154 sent = self.send(self.buffer)
155 self.buffer = self.buffer[sent:]
156 if len(self.buffer) == 0 and self.end:
157 self.close()
158
159 def writable(self):
160 return len(self.buffer) > 0
161
162 def handle_close(self):
163 if not self.other:
164 return
165 print >>sys.stderr, "Proxy closed"
166 self.close()
167 if len(self.other.buffer) == 0:
168 self.other.close()
169 self.other.end = True
170 self.other = None
171
172 class ConnectProxy(asyncore.dispatcher):
173 def __init__(self, sock):
174 asyncore.dispatcher.__init__(self, sock)
175 self.buffer = ""
176
177 def handle_error(self):
178 print >>sys.stderr, "ConnectProxy error:", traceback.format_exc()
179 self.close()
180
181 def handle_read(self):
182 self.buffer += self.recv(8192)
183 pos1 = self.buffer.find("\n")
184 if pos1 < 0:
185 return
186 host = self.buffer[:pos1].strip()
187 pos1 += 1
188 pos2 = self.buffer[pos1:].find("\n")
189 if pos2 < 0:
190 return
191 pos2 += pos1
192 port = int(self.buffer[pos1:pos2].strip())
193
194 self.buffer = self.buffer[pos2+1:]
195 self.done(host, port)
196
197 def handle_write(self):
198 pass
199
200 def handle_close(self):
201 print >>sys.stderr, "Proxy closed"
202 self.close()
203
204 def done(self, host, port):
205 print >>sys.stderr, "Forwarding connection", host, port
206
207 # Create server proxy
208 server = Proxy((host, port))
209 server.buffer = self.buffer
210
211 # Morph and connect
212 self.__class__ = Proxy
213 self.init()
214 server.meet(self)
215
216
217 class BasicForwarder(asyncore.dispatcher):
218 def __init__(self, listen_port, host, port, allowed):
219 asyncore.dispatcher.__init__(self)
220 self.host = host
221 self.port = port
222 self.allowed = allowed
223 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
224 self.set_reuse_addr()
225 self.bind(("", listen_port))
226 self.listen(50)
227 print >>sys.stderr, "BasicForwarder bound on", listen_port
228
229 def handle_error(self):
230 print >>sys.stderr, "BasicForwarder error:", traceback.format_exc()
231
232 def handle_accept(self):
233 client_connection, source_addr = self.accept()
234 if source_addr[0] not in map(socket.gethostbyname, self.allowed):
235 print >>sys.stderr, "Rejected connection from", source_addr
236 client_connection.close()
237 return
238
239 print >>sys.stderr, "Accepted connection from", source_addr
240
241 # Hook the sockets up to the event loop
242 client = Proxy(client_connection)
243 server = Proxy((self.host, self.port))
244 server.meet(client)
245
246 class Forwarder(asyncore.dispatcher):
247 def __init__(self, listen_port, allowed):
248 asyncore.dispatcher.__init__(self)
249 self.allowed = allowed
250 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
251 self.set_reuse_addr()
252 self.bind(("", listen_port))
253 self.listen(50)
254 print >>sys.stderr, "Forwarder bound on", listen_port
255
256 def handle_error(self):
257 print >>sys.stderr, "Forwarder error:", traceback.format_exc()
258
259 def handle_accept(self):
260 client_connection, source_addr = self.accept()
261 if source_addr[0] not in map(socket.gethostbyname, self.allowed):
262 print >>sys.stderr, "Rejected connection from", source_addr
263 client_connection.close()
264 return
265
266 print >>sys.stderr, "Accepted connection from", source_addr
267 ConnectProxy(client_connection)
268
269 class Interceptor(asyncore.dispatcher):
270 def __init__(self, listen_port, host, port):
271 asyncore.dispatcher.__init__(self)
272 self.host = host
273 self.port = port
274 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
275 self.set_reuse_addr()
276 self.bind(("0.0.0.0", listen_port))
277 self.listen(50)
278 print >>sys.stderr, "Interceptor bound on", listen_port
279
280 def handle_error(self):
281 print >>sys.stderr, "Interceptor error!", traceback.format_exc()
282
283 def handle_accept(self):
284 # Get sockets
285 client_connection, source_addr = self.accept()
286 dest = get_original_dest(client_connection)
287 print >>sys.stderr, "Accepted connection from", source_addr
288
289 # Hook them up to the event loop
290 client = Proxy(client_connection)
291 server = Proxy((self.host, self.port))
292 server.buffer += "%s\n%d\n" % dest
293 server.meet(client)
294
295
296 def main(listen_port, host, port, mode, allowed):
297 if mode == "basic":
298 proxy = BasicForwarder(listen_port, host, port, allowed)
299 elif mode == "proxy":
300 proxy = Forwarder(listen_port, allowed)
301 elif mode == "interceptor":
302 proxy = Interceptor(listen_port, host, port)
303 else:
304 print >>sys.stderr, "Unknown mode:", mode
305 return 1
306 asyncore.loop()
307
308
309 if __name__ == "__main__":
310 try:
311 if sys.argv[1] == "-d":
312 daemon = True
313 config = sys.argv[2]
314 else:
315 daemon = False
316 config = sys.argv[1]
317 except (IndexError, ValueError):
318 print >>sys.stderr, "Usage: %s [-d] config" % sys.argv[0]
319 sys.exit(1)
320
321 try:
322 c = ConfigParser.RawConfigParser()
323 c.read(config)
324 except:
325 print >>sys.stderr, "Error parsing config!"
326 sys.exit(1)
327
328 def guard(func, message):
329 try:
330 return func()
331 except:
332 print >>sys.stderr, "Error:", message
333 raise
334 sys.exit(1)
335
336 mode = guard(lambda:c.get("proxy", "mode").lower(),
337 "mode is a required field")
338
339 listen_port = guard(lambda:c.getint("proxy", "listen_port"),
340 "listen_port is a required field")
341
342 if mode in ["basic", "interceptor"]:
343 text = "%%s is a required field for mode=%s" % mode
344 host = guard(lambda:c.get("proxy", "host"), text % "host")
345 port = guard(lambda:c.getint("proxy", "port"), text % "port")
346 else:
347 host = None
348 port = None
349
350 if mode in ["basic", "proxy"]:
351 allowed = guard(lambda:c.items("allowed"),
352 "[allowed] section is required for mode=%s" % mode)
353 allowed = [h for _,h in c.items("allowed")]
354 else:
355 allowed = None
356
357
358 if not daemon:
359 try:
360 main(listen_port, host, port, mode, allowed)
361 except KeyboardInterrupt:
362 print
363 else:
364 os.close(0)
365 os.close(1)
366 os.close(2)
367 os.open("/dev/null", os.O_RDONLY)
368 os.open("/dev/null", os.O_RDWR)
369 os.dup(1)
370
371 if os.fork() == 0:
372 # We are the child
373 try:
374 sys.exit(main(listen_port, host, port, mode, allowed))
375 except KeyboardInterrupt:
376 print
377 sys.exit(0)
378