├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── test.py ├── tunnel.py └── tunneld.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | 3 | MAINTAINER akiel 4 | 5 | LABEL version='1.0' 6 | LABEL description='tunneld' 7 | 8 | RUN apt update && apt upgrade -y && apt install python -y 9 | 10 | COPY tunneld.py /home/ 11 | 12 | EXPOSE 80 13 | 14 | CMD ["/usr/bin/python","/home/tunneld.py","-p 443"] 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ==================== 3 | 4 | Copyright © 2015 Vu Minh Khue, http://khuevu.github.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HTTP TUNNEL 2 | ========== 3 | 4 | A program to tunnel TCP connection through HTTP connection 5 | 6 | ## Usage: 7 | 8 | The program is useful when you has a client behind a firewall which only allows 9 | HTTP connections or connections to standard ports such as port 80 of HTTP. You 10 | will need a remote server outside of the firewall. 11 | 12 | ### Tunnel Server 13 | 14 | Start the tunneld server on a remote machine. The server listens on a port 15 | specified by parameter `-p` for HTTP connection from a client program. 16 | 17 | python tunneld.py -p 18 | 19 | The server then read the HTTP payload and send it to the target using TCP 20 | connection. The target is specified by the client when establishing the tunnel. 21 | 22 | Usually, tunneling will actually be useful when you use the default HTTP port 23 | 80 so that the connection from tunnel client to tunnel server is not blocked by 24 | firewall. 25 | 26 | ### Tunnel Client 27 | 28 | Start the tunnel client which listen on the local machine. The command needs 29 | local port parameter, the remote tunnel server and its port, and the target that the client want to connect to. 30 | 31 | python tunnel.py -p -r : : 32 | 33 | When this command is executed, the client sends a http request to establish the 34 | tunnel with the remote tunnel server. The tunnel server will then establish 35 | a TCP connection with the target server. 36 | 37 | ### Example 38 | 39 | To connect to irc server using the tunnel: 40 | 41 | # 1. on server machine (remote_host) 42 | python tunneld.py -p 80 43 | 44 | # 2. on client machine: 45 | python tunnel.py -p 8765 -r remote_host:80 irc.freenode.net:6667 46 | 47 | # 3. on the client machine, test the tunnel connection using netcat 48 | nc 127.0.0.1 8765 49 | # send irc NICK command to the connection and see the response back from 50 | the irc server 51 | NICK abc 52 | 53 | ## Credit: 54 | 55 | This project was inspired by [supertunnel 56 | project](https://code.google.com/p/supertunnel/) 57 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 4 | s.connect(('localhost', 8889)) 5 | 6 | commands = ["CAP LS", "NICK abc", "USER abc abc irc.freenode.net :abc", "CAP REQ :identify-msg", "CAP END", "NOTICE frigg :.VERSION xchat 2.8.8 Ubuntu"] 7 | 8 | order = 0 9 | s.sendall("NICK abcxyz\r\n") 10 | s.sendall("USER abcxyz abcxyz irc.freenode.net :abcxyz\r\n") 11 | while True: 12 | data = s.recv(1024) 13 | print data 14 | 15 | #import httplib, urllib 16 | #conn = httplib.HTTPConnection('localhost', 8889) 17 | #conn.request("GET", "http://www.google.com") 18 | #response = conn.getresponse() 19 | #print response.status, response.reason 20 | #print response.read() 21 | 22 | -------------------------------------------------------------------------------- /tunnel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import socket, time 3 | import httplib, urllib 4 | from uuid import uuid4 5 | import threading 6 | import argparse 7 | import sys 8 | 9 | BUFFER = 1024 * 50 10 | 11 | 12 | class Connection(): 13 | 14 | def __init__(self, connection_id, remote_addr, proxy_addr): 15 | self.id = connection_id 16 | conn_dest = proxy_addr if proxy_addr else remote_addr 17 | print "Establishing connection with remote tunneld at %s:%s" % (conn_dest['host'], conn_dest['port']) 18 | self.http_conn = httplib.HTTPConnection(conn_dest['host'], conn_dest['port']) 19 | self.remote_addr = remote_addr 20 | 21 | def _url(self, url): 22 | return "http://{host}:{port}{url}".format(host=self.remote_addr['host'], port=self.remote_addr['port'], url=url) 23 | 24 | def create(self, target_addr): 25 | params = urllib.urlencode({"host": target_addr['host'], "port": target_addr['port']}) 26 | headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "text/plain"} 27 | 28 | self.http_conn.request("POST", self._url("/" + self.id), params, headers) 29 | 30 | response = self.http_conn.getresponse() 31 | response.read() 32 | if response.status == 200: 33 | print 'Successfully create connection' 34 | return True 35 | else: 36 | print 'Fail to establish connection: status %s because %s' % (response.status, response.reason) 37 | return False 38 | 39 | def send(self, data): 40 | params = urllib.urlencode({"data": data}) 41 | headers = {"Content-Type": "application/x-www-form-urlencoded", "Accept": "text/plain"} 42 | try: 43 | self.http_conn.request("PUT", self._url("/" + self.id), params, headers) 44 | response = self.http_conn.getresponse() 45 | response.read() 46 | print response.status 47 | except (httplib.HTTPResponse, socket.error) as ex: 48 | print "Error Sending Data: %s" % ex 49 | 50 | def receive(self): 51 | try: 52 | self.http_conn.request("GET", "/" + self.id) 53 | response = self.http_conn.getresponse() 54 | data = response.read() 55 | if response.status == 200: 56 | return data 57 | else: 58 | return None 59 | except (httplib.HTTPResponse, socket.error) as ex: 60 | print "Error Receiving Data: %s" % ex 61 | return None 62 | 63 | def close(self): 64 | print "Close connection to target at remote tunnel" 65 | self.http_conn.request("DELETE", "/" + self.id) 66 | self.http_conn.getresponse() 67 | 68 | class SendThread(threading.Thread): 69 | 70 | """ 71 | Thread to send data to remote host 72 | """ 73 | 74 | def __init__(self, client, connection): 75 | threading.Thread.__init__(self, name="Send-Thread") 76 | self.client = client 77 | self.socket = client.socket 78 | self.conn = connection 79 | self._stop = threading.Event() 80 | 81 | def run(self): 82 | while not self.stopped(): 83 | print "Getting data from client to send" 84 | try: 85 | data = self.socket.recv(BUFFER) 86 | if data == '': 87 | print "Client's socket connection broken" 88 | # There should be a nicer way to stop receiver 89 | self.client.receiver.stop() 90 | self.client.receiver.join() 91 | self.conn.close() 92 | return 93 | 94 | print "Sending data ... %s " % data 95 | self.conn.send(data) 96 | except socket.timeout: 97 | print "time out" 98 | 99 | def stop(self): 100 | self._stop.set() 101 | 102 | def stopped(self): 103 | return self._stop.isSet() 104 | 105 | class ReceiveThread(threading.Thread): 106 | 107 | """ 108 | Thread to receive data from remote host 109 | """ 110 | 111 | def __init__(self, client, connection): 112 | threading.Thread.__init__(self, name="Receive-Thread") 113 | self.client = client 114 | self.socket = client.socket 115 | self.conn = connection 116 | self._stop = threading.Event() 117 | 118 | def run(self): 119 | while not self.stopped(): 120 | print "Retreiving data from remote tunneld" 121 | data = self.conn.receive() 122 | if data: 123 | sent = self.socket.sendall(data) 124 | else: 125 | print "No data received" 126 | # sleep for sometime before trying to get data again 127 | time.sleep(1) 128 | 129 | def stop(self): 130 | self._stop.set() 131 | 132 | def stopped(self): 133 | return self._stop.isSet() 134 | 135 | class ClientWorker(object): 136 | 137 | def __init__(self, socket, remote_addr, target_addr, proxy_addr): 138 | #threading.Thread.__init__(self) 139 | self.socket = socket 140 | self.remote_addr = remote_addr 141 | self.target_addr = target_addr 142 | self.proxy_addr = proxy_addr 143 | 144 | def start(self): 145 | #generate unique connection ID 146 | connection_id = str(uuid4()) 147 | #main connection for create and close 148 | self.connection = Connection(connection_id, self.remote_addr, self.proxy_addr) 149 | 150 | if self.connection.create(self.target_addr): 151 | self.sender = SendThread(self, Connection(connection_id, self.remote_addr, self.proxy_addr) 152 | ) 153 | self.receiver = ReceiveThread(self, Connection(connection_id, self.remote_addr, self.proxy_addr) 154 | ) 155 | self.sender.start() 156 | self.receiver.start() 157 | 158 | def stop(self): 159 | #stop read and send threads 160 | self.sender.stop() 161 | self.receiver.stop() 162 | #send close signal to remote server 163 | self.connection.close() 164 | #wait for read and send threads to stop and close local socket 165 | self.sender.join() 166 | self.receiver.join() 167 | self.socket.close() 168 | 169 | 170 | 171 | 172 | def start_tunnel(listen_port, remote_addr, target_addr, proxy_addr): 173 | """Start tunnel""" 174 | listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 175 | listen_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 176 | listen_sock.settimeout(None) 177 | listen_sock.bind(('', int(listen_port))) 178 | listen_sock.listen(1) 179 | print "waiting for connection" 180 | workers = [] 181 | try: 182 | while True: 183 | c_sock, addr = listen_sock.accept() 184 | c_sock.settimeout(20) 185 | print "connected by ", addr 186 | worker = ClientWorker(c_sock, remote_addr, target_addr, proxy_addr) 187 | workers.append(worker) 188 | worker.start() 189 | except (KeyboardInterrupt, SystemExit): 190 | listen_sock.close() 191 | for w in workers: 192 | w.stop() 193 | for w in workers: 194 | w.join() 195 | sys.exit() 196 | 197 | if __name__ == "__main__": 198 | """Parse argument from command line and start tunnel""" 199 | 200 | parser = argparse.ArgumentParser(description='Start Tunnel') 201 | parser.add_argument('-p', default=8889, dest='listen_port', help='Port the tunnel listens to, (default to 8889)', type=int) 202 | parser.add_argument('target', metavar='Target Address', help='Specify the host and port of the target address in format Host:Port') 203 | parser.add_argument('-r', default='localhost:9999', dest='remote', help='Specify the host and port of the remote server to tunnel to (Default to localhost:9999)') 204 | parser.add_argument('-o', default='', dest='proxy', help='Specify the host and port of the proxy server(host:port)') 205 | 206 | args = parser.parse_args() 207 | 208 | target_addr = {"host": args.target.split(":")[0], "port": args.target.split(":")[1]} 209 | remote_addr = {"host": args.remote.split(":")[0], "port": args.remote.split(":")[1]} 210 | proxy_addr = {"host": args.proxy.split(":")[0], "port": args.proxy.split(":")[1]} if (args.proxy) else {} 211 | start_tunnel(args.listen_port, remote_addr, target_addr, proxy_addr) 212 | -------------------------------------------------------------------------------- /tunneld.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from BaseHTTPServer import BaseHTTPRequestHandler,HTTPServer 3 | import socket, select 4 | import cgi 5 | import argparse 6 | 7 | class ProxyRequestHandler(BaseHTTPRequestHandler): 8 | 9 | sockets = {} 10 | BUFFER = 1024 * 50 11 | SOCKET_TIMEOUT = 50 12 | 13 | def _get_connection_id(self): 14 | return self.path.split('/')[-1] 15 | 16 | def _get_socket(self): 17 | """get the socket which connects to the target address for this connection""" 18 | id = self._get_connection_id() 19 | return self.sockets.get(id, None) 20 | 21 | def _close_socket(self): 22 | """ close the current socket""" 23 | id = self._get_connection_id() 24 | s = self.sockets[id] 25 | if s: 26 | s.close() 27 | del self.sockets[id] 28 | 29 | def do_GET(self): 30 | """GET: Read data from TargetAddress and return to client through http response""" 31 | s = self._get_socket() 32 | if s: 33 | # check if the socket is ready to be read 34 | to_reads, to_writes, in_errors = select.select([s], [], [], 5) 35 | if len(to_reads) > 0: 36 | to_read_socket = to_reads[0] 37 | try: 38 | print "Getting data from target address" 39 | data = to_read_socket.recv(self.BUFFER) 40 | print data 41 | self.send_response(200) 42 | self.end_headers() 43 | if data: 44 | self.wfile.write(data) 45 | except socket.error as ex: 46 | print 'Error getting data from target socket: %s' % ex 47 | self.send_response(503) 48 | self.end_headers() 49 | else: 50 | print 'No content available from socket' 51 | self.send_response(204) # no content had be retrieved 52 | self.end_headers() 53 | else: 54 | print 'Connection With ID %s has not been established' % self._get_connection_id() 55 | self.send_response(400) 56 | self.end_headers() 57 | 58 | 59 | def do_POST(self): 60 | """POST: Create TCP Connection to the TargetAddress""" 61 | id = self._get_connection_id() 62 | print 'Initializing connection with ID %s' % id 63 | length = int(self.headers.getheader('content-length')) 64 | req_data = self.rfile.read(length) 65 | params = cgi.parse_qs(req_data, keep_blank_values=1) 66 | target_host = params['host'][0] 67 | target_port = int(params['port'][0]) 68 | 69 | print 'Connecting to target address: %s % s' % (target_host, target_port) 70 | # open socket connection to remote server 71 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | # use non-blocking socket 73 | s.setblocking(0) 74 | s.connect_ex((target_host, target_port)) 75 | 76 | #save socket reference 77 | self.sockets[id] = s 78 | try: 79 | self.send_response(200) 80 | self.end_headers() 81 | except socket.error, e: 82 | print e 83 | 84 | def do_PUT(self): 85 | """Read data from HTTP Request and send to TargetAddress""" 86 | id = self._get_connection_id() 87 | s = self.sockets[id] 88 | if not s: 89 | print "Connection with id %s doesn't exist" % id 90 | self.send_response(400) 91 | self.end_headers() 92 | return 93 | length = int(self.headers.getheader('content-length')) 94 | data = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1)['data'][0] 95 | 96 | # check if the socket is ready to write 97 | to_reads, to_writes, in_errors = select.select([], [s], [], 5) 98 | if len(to_writes) > 0: 99 | print 'Sending data .... %s' % data 100 | to_write_socket = to_writes[0] 101 | try: 102 | to_write_socket.sendall(data) 103 | self.send_response(200) 104 | except socket.error as ex: 105 | print 'Error sending data from target socket: %s' % ex 106 | self.send_response(503) 107 | else: 108 | print 'Socket is not ready to write' 109 | self.send_response(504) 110 | self.end_headers() 111 | 112 | def do_DELETE(self): 113 | self._close_socket() 114 | self.send_response(200) 115 | self.end_headers() 116 | 117 | def run_server(port, server_class=HTTPServer, handler_class=ProxyRequestHandler): 118 | server_address = ('', port) 119 | httpd = server_class(server_address, handler_class) 120 | httpd.serve_forever() 121 | 122 | 123 | if __name__ == "__main__": 124 | parser = argparse.ArgumentParser(description="Start Tunnel Server") 125 | parser.add_argument("-p", default=9999, dest='port', help='Specify port number server will listen to', type=int) 126 | args = parser.parse_args() 127 | run_server(args.port) 128 | --------------------------------------------------------------------------------