Welcome, guest | Sign In | My Account | Store | Cart

This forward the TCP traffic from your machine to another host, and back in the the other way. It uses asynchronous socket thanks to ye olde asyncore module, which was used by Zope up until recently (they integrated the Twisted reactor). As a consequence, it should be able to handle a great number of connections without crumbling under the weight of many threads.

Python, 96 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import socket,asyncore

class forwarder(asyncore.dispatcher):
    def __init__(self, ip, port, remoteip,remoteport,backlog=5):
        asyncore.dispatcher.__init__(self)
        self.remoteip=remoteip
        self.remoteport=remoteport
        self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((ip,port))
        self.listen(backlog)

    def handle_accept(self):
        conn, addr = self.accept()
        # print '--- Connect --- '
        sender(receiver(conn),self.remoteip,self.remoteport)

class receiver(asyncore.dispatcher):
    def __init__(self,conn):
        asyncore.dispatcher.__init__(self,conn)
        self.from_remote_buffer=''
        self.to_remote_buffer=''
        self.sender=None

    def handle_connect(self):
        pass

    def handle_read(self):
        read = self.recv(4096)
        # print '%04i -->'%len(read)
        self.from_remote_buffer += read

    def writable(self):
        return (len(self.to_remote_buffer) > 0)

    def handle_write(self):
        sent = self.send(self.to_remote_buffer)
        # print '%04i <--'%sent
        self.to_remote_buffer = self.to_remote_buffer[sent:]

    def handle_close(self):
        self.close()
        if self.sender:
            self.sender.close()

class sender(asyncore.dispatcher):
    def __init__(self, receiver, remoteaddr,remoteport):
        asyncore.dispatcher.__init__(self)
        self.receiver=receiver
        receiver.sender=self
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((remoteaddr, remoteport))

    def handle_connect(self):
        pass

    def handle_read(self):
        read = self.recv(4096)
        # print '<-- %04i'%len(read)
        self.receiver.to_remote_buffer += read

    def writable(self):
        return (len(self.receiver.from_remote_buffer) > 0)

    def handle_write(self):
        sent = self.send(self.receiver.from_remote_buffer)
        # print '--> %04i'%sent
        self.receiver.from_remote_buffer = self.receiver.from_remote_buffer[sent:]

    def handle_close(self):
        self.close()
        self.receiver.close()

if __name__=='__main__':
    import optparse
    parser = optparse.OptionParser()

    parser.add_option(
        '-l','--local-ip',
        dest='local_ip',default='127.0.0.1',
        help='Local IP address to bind to')
    parser.add_option(
        '-p','--local-port',
        type='int',dest='local_port',default=80,
        help='Local port to bind to')
    parser.add_option(
        '-r','--remote-ip',dest='remote_ip',
        help='Local IP address to bind to')
    parser.add_option(
        '-P','--remote-port',
        type='int',dest='remote_port',default=80,
        help='Remote port to bind to')
    options, args = parser.parse_args()

    forwarder(options.local_ip,options.local_port,options.remote_ip,options.remote_port)
    asyncore.loop()

See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/483730 for a threaded version. Their UI is better, with a configuration file parser, but with one thread per server socket and two threads per connection, the Python runtime could be under heavy stress as soon as you have more than a few hundred simultaneous connections.

I've tested my version with 252 simultaneous connections (using three Apache Benchmark processes each with 64 concurrent connections) and it handles the load like a charm. asyncore is really an impressive module...

Note that if you need to run more than one forwarder at a time, you just have to build as many forwarder instance as you want before calling asyncore.loop(). In fact, instead of a configuration file, you could just replace the __main__ part with something like :

if __name__ == '__main__': forwarder('127.0.0.1',80,'www.foobar.com',80) forwarder('127.0.0.1',443,'www.foobar.com',443) forwarder('127.0.0.1',81,'www.anotherone.com',80) forwarder('127.0.0.1',444,'www.anotherone.com',443) asyncore.loop()

4 comments

Stephen Chappell 17 years, 12 months ago  # | flag

Nice! We create solutions for which we hope there is a problem. :)

Nicolas Lehuen (author) 17 years, 12 months ago  # | flag

Indeed :). I guess 99.99% of the time a port forwarder is used with very few connections, so the threaded version does not have any problems. I've put this asynchronous version as a tribute to the asyncore module, since I've wrote this little port forwarder as I needed one and thought "what the heck, let's do it asynchronously" :)

Dwight Walker 17 years, 4 months ago  # | flag

closed connection for receiver. There is a bug. The receiver connection is closed by the sender.handle_close() so if the receiver is slow (asynchronous connection) the receiver connection is closed before it has a chance to send the data. So check in sender.handle_close() if to_remote_buffer is empty first then close otherwise let receiver close it using its own receiver.handle_close(). This was after much checking of buffers and realising the ovezealous sender had closed the receiver connection in haste!!

change: class sender(asyncore.dispatcher): def handle_close(self): self.close() if len(self.receiver.to_remote_buffer) == 0: self.receiver.close()

Alex 11 years, 5 months ago  # | flag

Can I change remoteip, remoteport on the fly? If the fly remoteip, remoteport not possible to change how koektno stop?