]> code.delx.au - monosys/commitdiff
Epic proxy update - added extra awesome
authorJames Bunton <jamesbunton@fastmail.fm>
Mon, 23 Mar 2009 05:29:21 +0000 (16:29 +1100)
committerJames Bunton <jamesbunton@fastmail.fm>
Mon, 23 Mar 2009 05:29:21 +0000 (16:29 +1100)
scripts/proxy.py

index ad49bc2bbce0d847e0129a49f32fbf52d236a6ce..586459104405e4f9febc0a7b9caf44f016a530c5 100755 (executable)
 #!/usr/bin/env python
 
-# Sample config file:
+"""
+Proxy Utility
+-------------
 
-# [proxy]
-# listen_port = 8000
-# host = google.com
-# port = 80
-# 
-# [allowed]
-# host1 = delx.cjb.net
-# host2 = 333.333.333.333
+With mode=basic any incoming connections on listen_port will be proxied
+to host:port. The proxy will only accept connections from hosts in the
+[allowed] section.
+
+With mode=proxy the first two lines of incoming connections must be
+'host\nport\n'. Once again only connections from hosts in the [allowed]
+section will be accepted. The proxy will connect to host:port and pass
+bytes in both directions.
+
+The final mode, mode=interceptor is designed to be combined with a firewall
+rule and another instance running mode=proxy on another computer.
+Proxies running in interceptor mode listen on all interfaces. They make
+connections to the host:port specified in the config file, passing the
+'capturedhost\ncapturedport\n' onto the destination. They then pass bytes
+in both directions.
+
+
+Example - Basic Forwarder
+-------------------------
+
+Config to forward all packets from port 8000 on localhost to google.com:80
+Connections will be accepted from whatever IP address alpha.example.com
+and beta.example.com point to.
+
+[proxy]
+mode = basic
+listen_port = 8000
+host = google.com
+port = 80
+
+[allowed]
+host1 = alpha.example.com
+host2 = beta.example.com
+
+
+
+Example - Interceptor Proxy Combo
+---------------------------------
+
+Capture all packets destined for port 1935 and send them to an interceptor
+configured to listen on example.com:9997.
+On Linux:
+  # iptables -t nat -A PREROUTING -p tcp --dport 1935 
+      -j REDIRECT --to-ports 9997
+  # iptables -t nat -A OUTPUT -p tcp --dport 1935 
+      -j REDIRECT --to-ports 9997
+
+On Mac OS X:
+  # ipfw add 50000 fwd 127.0.0.1,9997 tcp from any to any dst-port 1935
+
+Config to forward these connections to proxy.example.com
+
+[proxy]
+mode = interceptor
+listen_port = 9997
+host = proxy.example.com
+port = 9997
+
+
+
+Config file for proxy.example.com
+
+[proxy]
+mode = proxy
+listen_port = 9997
+
+[allowed]
+host1 = alpha.example.com
+host2 = beta.example.com
+
+
+"""
 
 
 import asyncore
 import ConfigParser
 import os
 import socket
+import struct
 import sys
 
+
+if sys.platform == "linux2":
+       try:
+               socket.SO_ORIGINAL_DST
+       except AttributeError:
+               # There is a missing const in the socket module... So we will add it now
+               socket.SO_ORIGINAL_DST = 80
+
+       def get_original_dest(sock):
+               '''Gets the original destination address for connection that has been
+               redirected by netfilter.'''
+               # struct sockaddr_in {
+               #     short            sin_family;   // e.g. AF_INET
+               #     unsigned short   sin_port;     // e.g. htons(3490)
+               #     struct in_addr   sin_addr;     // see struct in_addr, below
+               #     char             sin_zero[8];  // zero this if you want to
+               # };
+               # struct in_addr {
+               #     unsigned long s_addr;  // load with inet_aton()
+               # };
+               # getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, (struct sockaddr_in *)&dstaddr, &dstlen);
+
+               data = sock.getsockopt(socket.SOL_IP, socket.SO_ORIGINAL_DST, 16)
+               _, port, a1, a2, a3, a4 = struct.unpack("!HHBBBBxxxxxxxx", data)
+               address = "%d.%d.%d.%d" % (a1, a2, a3, a4)
+               return address, port
+
+
+elif sys.platform == "darwin":
+       def get_original_dest(sock):
+               '''Gets the original destination address for connection that has been
+               redirected by ipfw.'''
+               return sock.getsockname()
+
+
+
 class Proxy(asyncore.dispatcher):
        def __init__(self, sock):
                asyncore.dispatcher.__init__(self, sock)
                self.other = None
                self.buffer = ""
-       
+
        def meet(self, other):
                self.other = other
                other.other = self
-       
+
        def handle_error(self):
-               print >>sys.stderr, "Connection closed..."
+               print >>sys.stderr, "Proxy error:", sys.exc_info()
                self.handle_close()
 
        def handle_read(self):
                data = self.recv(8192)
-               self.other.buffer += data
-       
+               if len(data) > 0:
+                       self.other.buffer += data
+
        def handle_write(self):
                sent = self.send(self.buffer)
                self.buffer = self.buffer[sent:]
-       
+
+       def writable(self):
+               return len(self.buffer) > 0
+
        def handle_close(self):
                if not self.other:
                        return
+               print >>sys.stderr, "Proxy closed"
                self.other.close()
                self.close()
                self.other = None
 
-class Forwarder(asyncore.dispatcher):
+class ConnectProxy(asyncore.dispatcher):
+       def __init__(self, sock):
+               asyncore.dispatcher.__init__(self, sock)
+               self.buffer = ""
+
+       def handle_error(self):
+               print >>sys.stderr, "ConnectProxy error:", sys.exc_info()
+               self.handle_close()
+
+       def handle_read(self):
+               self.buffer += self.recv(8192)
+               pos1 = self.buffer.find("\n")
+               if pos1 < 0:
+                       return
+               host = self.buffer[:pos1].strip()
+               pos1 += 1
+               pos2 = self.buffer[pos1:].find("\n")
+               if pos2 < 0:
+                       return
+               pos2 += pos1
+               port = int(self.buffer[pos1:pos2].strip())
+
+               self.buffer = self.buffer[pos2+1:]
+               self.done(host, port)
+
+       def handle_write(self):
+               pass
+
+       def handle_close(self):
+               print >>sys.stderr, "Proxy closed"
+               self.close()
+
+       def done(self, host, port):
+               print >>sys.stderr, "Forwarding connection", host, port
+
+               # Create server proxy
+               server_connection = socket.socket()
+               server_connection.connect((host, port))
+               server = Proxy(server_connection)
+
+               # Morph and connect
+               self.__class__ = Proxy
+               server.meet(self)
+               server.buffer = self.buffer
+               self.buffer = ""
+
+
+class BasicForwarder(asyncore.dispatcher):
        def __init__(self, listen_port, host, port, allowed):
                asyncore.dispatcher.__init__(self)
                self.host = host
@@ -56,10 +211,10 @@ class Forwarder(asyncore.dispatcher):
                self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
                self.bind(("", listen_port))
                self.listen(5)
-       
+
        def handle_error(self):
-               print >>sys.stderr, "Connection error!", sys.exc_info()
-       
+               print >>sys.stderr, "BasicForwarder error:", sys.exc_info()
+
        def handle_accept(self):
                client_connection, (addr, port) = self.accept()
                if addr not in map(socket.gethostbyname, self.allowed):
@@ -76,10 +231,67 @@ class Forwarder(asyncore.dispatcher):
                server = Proxy(server_connection)
                server.meet(client)
 
-def main(listen_port, host, port, allowed):
-       proxy = Forwarder(listen_port, host, port, allowed)
+class Forwarder(asyncore.dispatcher):
+       def __init__(self, listen_port, allowed):
+               asyncore.dispatcher.__init__(self)
+               self.allowed = allowed
+               self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+               self.bind(("", listen_port))
+               self.listen(5)
+
+       def handle_error(self):
+               print >>sys.stderr, "Forwarder error:", sys.exc_info()
+
+       def handle_accept(self):
+               client_connection, (addr, port) = self.accept()
+               if addr not in map(socket.gethostbyname, self.allowed):
+                       print >>sys.stderr, "Rejected connection from", addr
+                       client_connection.close()
+                       return
+
+               print >>sys.stderr, "Accepted connection from", addr, port
+               ConnectProxy(client_connection)
+
+class Interceptor(asyncore.dispatcher):
+       def __init__(self, listen_port, host, port):
+               asyncore.dispatcher.__init__(self)
+               self.host = host
+               self.port = port
+               self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+               self.bind(("0.0.0.0", listen_port))
+               self.listen(5)
+
+       def handle_error(self):
+               print >>sys.stderr, "Interceptor error!", sys.exc_info()
+
+       def handle_accept(self):
+               # Get sockets
+               client_connection, addr = self.accept()
+               dest = get_original_dest(client_connection)
+               print >>sys.stderr, "Accepted connection from", addr, port
+               server_connection = socket.socket()
+               server_connection.connect((self.host, self.port))
+
+               # Hook them up to the event loop
+               client = Proxy(client_connection)
+               server = Proxy(server_connection)
+               server.buffer += "%s\n%d\n" % dest
+               server.meet(client)
+
+
+def main(listen_port, host, port, mode, allowed):
+       if mode == "basic":
+               proxy = BasicForwarder(listen_port, host, port, allowed)
+       elif mode == "proxy":
+               proxy = Forwarder(listen_port, allowed)
+       elif mode == "interceptor":
+               proxy = Interceptor(listen_port, host, port)
+       else:
+               print >>sys.stderr, "Unknown mode:", mode
+               return 1
        asyncore.loop()
 
+
 if __name__ == "__main__":
        try:
                if sys.argv[1] == "-d":
@@ -95,18 +307,43 @@ if __name__ == "__main__":
        try:
                c = ConfigParser.RawConfigParser()
                c.read(config)
-               listen_port = c.getint("proxy", "listen_port")
-               host = c.get("proxy", "host")
-               port = c.getint("proxy", "port")
-               allowed = c.items("allowed")
-               allowed = [h for _,h in c.items("allowed")]
        except:
                print >>sys.stderr, "Error parsing config!"
                sys.exit(1)
 
+       def guard(func, message):
+               try:
+                       return func()
+               except:
+                       print >>sys.stderr, "Error:", message
+                       raise
+                       sys.exit(1)
+
+       mode = guard(lambda:c.get("proxy", "mode").lower(),
+               "mode is a required field")
+
+       listen_port = guard(lambda:c.getint("proxy", "listen_port"),
+               "listen_port is a required field")
+
+       if mode in ["basic", "interceptor"]:
+               text = "%%s is a required field for mode=%s" % mode
+               host = guard(lambda:c.get("proxy", "host"), text % "host")
+               port = guard(lambda:c.getint("proxy", "port"), text % "port")
+       else:
+               host = None
+               port = None
+
+       if mode in ["basic", "proxy"]:
+               allowed = guard(lambda:c.items("allowed"),
+                       "[allowed] section is required for mode=%s" % mode)
+               allowed = [h for _,h in c.items("allowed")]
+       else:
+               allowed = None
+
+
        if not daemon:
                try:
-                       main(listen_port, host, port, allowed)
+                       main(listen_port, host, port, mode, allowed)
                except KeyboardInterrupt:
                        print
        else:
@@ -120,7 +357,7 @@ if __name__ == "__main__":
                if os.fork() == 0:
                        # We are the child
                        try:
-                               main(listen_port, host, port, allowed)
+                               sys.exit(main(listen_port, host, port, allowed))
                        except KeyboardInterrupt:
                                print
                        sys.exit(0)