]> code.delx.au - monosys/blob - hacks/tcp-proxy
Rename all the things
[monosys] / hacks / tcp-proxy
1 #!/usr/bin/env python3
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 = b""
139
140 def meet(self, other):
141 self.other = other
142 other.other = self
143
144 def handle_error(self):
145 print("Proxy error:", traceback.format_exc(), file=sys.stderr)
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("Proxy closed", file=sys.stderr)
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 = b""
176
177 def handle_error(self):
178 print("ConnectProxy error:", traceback.format_exc(), file=sys.stderr)
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("Proxy closed", file=sys.stderr)
202 self.close()
203
204 def done(self, host, port):
205 print("Forwarding connection", host, port, file=sys.stderr)
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((b"", listen_port))
226 self.listen(50)
227 print("BasicForwarder bound on", listen_port, file=sys.stderr)
228
229 def handle_error(self):
230 print("BasicForwarder error:", traceback.format_exc(), file=sys.stderr)
231
232 def handle_accept(self):
233 client_connection, source_addr = self.accept()
234 if not self.is_connected_allowed(source_addr):
235 print("Rejected connection from", source_addr, file=sys.stderr)
236 client_connection.close()
237 return
238
239 print("Accepted connection from", source_addr, file=sys.stderr)
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 def is_connected_allowed(self, source_addr):
247 if len(self.allowed) == 1 and self.allowed[0].lower() == "all":
248 return True
249
250 if source_addr[0] in list(map(socket.gethostbyname, self.allowed)):
251 return True
252
253 return False
254
255 class Forwarder(asyncore.dispatcher):
256 def __init__(self, listen_port, allowed):
257 asyncore.dispatcher.__init__(self)
258 self.allowed = allowed
259 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
260 self.set_reuse_addr()
261 self.bind((b"", listen_port))
262 self.listen(50)
263 print("Forwarder bound on", listen_port, file=sys.stderr)
264
265 def handle_error(self):
266 print("Forwarder error:", traceback.format_exc(), file=sys.stderr)
267
268 def handle_accept(self):
269 client_connection, source_addr = self.accept()
270 if source_addr[0] not in list(map(socket.gethostbyname, self.allowed)):
271 print("Rejected connection from", source_addr, file=sys.stderr)
272 client_connection.close()
273 return
274
275 print("Accepted connection from", source_addr, file=sys.stderr)
276 ConnectProxy(client_connection)
277
278 class Interceptor(asyncore.dispatcher):
279 def __init__(self, listen_port, host, port):
280 asyncore.dispatcher.__init__(self)
281 self.host = host
282 self.port = port
283 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
284 self.set_reuse_addr()
285 self.bind(("0.0.0.0", listen_port))
286 self.listen(50)
287 print("Interceptor bound on", listen_port, file=sys.stderr)
288
289 def handle_error(self):
290 print("Interceptor error!", traceback.format_exc(), file=sys.stderr)
291
292 def handle_accept(self):
293 # Get sockets
294 client_connection, source_addr = self.accept()
295 dest = get_original_dest(client_connection)
296 print("Accepted connection from", source_addr, file=sys.stderr)
297
298 # Hook them up to the event loop
299 client = Proxy(client_connection)
300 server = Proxy((self.host, self.port))
301 server.buffer += "%s\n%d\n" % dest
302 server.meet(client)
303
304
305 def main(listen_port, host, port, mode, allowed):
306 if mode == "basic":
307 proxy = BasicForwarder(listen_port, host, port, allowed)
308 elif mode == "proxy":
309 proxy = Forwarder(listen_port, allowed)
310 elif mode == "interceptor":
311 proxy = Interceptor(listen_port, host, port)
312 else:
313 print("Unknown mode:", mode, file=sys.stderr)
314 return 1
315 asyncore.loop()
316
317
318 if __name__ == "__main__":
319 try:
320 if sys.argv[1] == "-d":
321 daemon = True
322 config = sys.argv[2]
323 else:
324 daemon = False
325 config = sys.argv[1]
326 except (IndexError, ValueError):
327 print("Usage: %s [-d] config" % sys.argv[0], file=sys.stderr)
328 sys.exit(1)
329
330 try:
331 c = configparser.RawConfigParser()
332 c.read(config)
333 except:
334 print("Error parsing config!", file=sys.stderr)
335 sys.exit(1)
336
337 def guard(func, message):
338 try:
339 return func()
340 except:
341 print("Error:", message, file=sys.stderr)
342 raise
343 sys.exit(1)
344
345 mode = guard(lambda:c.get("proxy", "mode").lower(),
346 "mode is a required field")
347
348 listen_port = guard(lambda:c.getint("proxy", "listen_port"),
349 "listen_port is a required field")
350
351 if mode in ["basic", "interceptor"]:
352 text = "%%s is a required field for mode=%s" % mode
353 host = guard(lambda:c.get("proxy", "host"), text % "host")
354 port = guard(lambda:c.getint("proxy", "port"), text % "port")
355 else:
356 host = None
357 port = None
358
359 if mode in ["basic", "proxy"]:
360 allowed = guard(lambda:c.items("allowed"),
361 "[allowed] section is required for mode=%s" % mode)
362 allowed = [h for _,h in c.items("allowed")]
363 else:
364 allowed = None
365
366
367 if not daemon:
368 try:
369 main(listen_port, host, port, mode, allowed)
370 except KeyboardInterrupt:
371 print()
372 else:
373 os.close(0)
374 os.close(1)
375 os.close(2)
376 os.open("/dev/null", os.O_RDONLY)
377 os.open("/dev/null", os.O_RDWR)
378 os.dup(1)
379
380 if os.fork() == 0:
381 # We are the child
382 try:
383 sys.exit(main(listen_port, host, port, mode, allowed))
384 except KeyboardInterrupt:
385 print()
386 sys.exit(0)
387