rocksock: fix compat with py3
[rofl0r-nat-tunnel.git] / natsrv.py
blob64eaae0e9dec64ad6dae4a9f7cbc4292f5afefaa
1 import socket, select, os, threading, hashlib, rocksock, time, sys
3 NONCE_LEN = 8
5 def _get_nonce():
6 return os.urandom(NONCE_LEN).encode('hex')
8 def _hash(str):
9 return hashlib.sha256(str).hexdigest()
11 def _format_addr(addr):
12 ip, port = addr
13 return "%s:%d"%(ip, port)
15 def _timestamp():
16 return time.strftime('[%Y-%m-%d %H:%M:%S] ', time.localtime(time.time()))
18 class Tunnel():
19 def __init__(self, fds, fdc, caddr):
20 self.fds = fds
21 self.fdc = fdc
22 self.done = threading.Event()
23 self.t = None
24 def _cleanup(self):
25 if self.fdc: self.fdc.close()
26 if self.fds: self.fds.close()
27 self.fdc = None
28 self.fds = None
29 def _threadfunc(self):
30 while True:
31 a,b,c = select.select([self.fds, self.fdc], [], [])
32 try:
33 buf = a[0].recv(1024)
34 except:
35 buf = ''
36 if len(buf) == 0:
37 break
38 try:
39 if a[0] == self.fds:
40 self.fdc.send(buf)
41 else:
42 self.fds.send(buf)
43 except:
44 break
45 self._cleanup()
46 self.done.set()
47 def start(self):
48 self.t = threading.Thread(target=self._threadfunc)
49 self.t.daemon = True
50 self.t.start()
51 def finished(self):
52 return self.done.is_set()
53 def reap(self):
54 self.t.join()
56 class NATClient():
57 def __init__(self, secret, upstream_ip, upstream_port, localserv_ip, localserv_port):
58 self.secret = secret
59 self.localserv_ip = localserv_ip
60 self.localserv_port = localserv_port
61 self.upstream_ip = upstream_ip
62 self.upstream_port = upstream_port
63 self.controlsock = None
64 self.next_csock = None
65 self.threads = []
67 def _setup_sock(self, cmd):
68 sock = rocksock.Rocksock(host=self.upstream_ip, port=self.upstream_port)
69 sock.connect()
70 nonce = sock.recv(NONCE_LEN*2 + 1).rstrip('\n')
71 sock.send(_hash(cmd + self.secret + nonce) + '\n')
72 return sock
74 def setup(self):
75 self.controlsock = self._setup_sock('adm')
76 self.next_csock = self._setup_sock('skt')
78 def doit(self):
79 while True:
80 i = 0
81 while i < len(self.threads):
82 if self.threads[i].finished():
83 self.threads[i].reap()
84 self.threads.pop(i)
85 else:
86 i += 1
88 l = self.controlsock.recvline()
89 print(_timestamp() + l.rstrip('\n'))
90 if l.startswith('CONN:'):
91 addr=l.rstrip('\n').split(':')[1]
92 local_conn = rocksock.Rocksock(host=self.localserv_ip, port=self.localserv_port)
93 local_conn.connect()
94 thread = Tunnel(local_conn.sock, self.next_csock.sock, addr)
95 thread.start()
96 self.threads.append(thread)
97 self.next_csock = self._setup_sock('skt')
100 class NATSrv():
101 def _isnumericipv4(self, ip):
102 try:
103 a,b,c,d = ip.split('.')
104 if int(a) < 256 and int(b) < 256 and int(c) < 256 and int(d) < 256:
105 return True
106 return False
107 except:
108 return False
110 def _resolve(self, host, port, want_v4=True):
111 if self._isnumericipv4(host):
112 return socket.AF_INET, (host, port)
113 for res in socket.getaddrinfo(host, port, \
114 socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
115 af, socktype, proto, canonname, sa = res
116 if want_v4 and af != socket.AF_INET: continue
117 if af != socket.AF_INET and af != socket.AF_INET6: continue
118 else: return af, sa
120 return None, None
122 def __init__(self, secret, upstream_listen_ip, upstream_port, client_listen_ip, client_port):
123 self.up_port = upstream_port
124 self.up_ip = upstream_listen_ip
125 self.client_port = client_port
126 self.client_ip = client_listen_ip
127 self.secret = secret
128 self.threads = []
129 self.su = None
130 self.sc = None
131 self.control_socket = None
132 self.next_upstream_socket = None
133 self.hashlen = len(_hash(""))
135 def _setup_listen_socket(self, listenip, port):
136 af, sa = self._resolve(listenip, port)
137 s = socket.socket(af, socket.SOCK_STREAM)
138 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
139 s.bind((sa[0], sa[1]))
140 s.listen(1)
141 return s
143 def setup(self):
144 self.su = self._setup_listen_socket(self.up_ip, self.up_port)
145 self.sc = self._setup_listen_socket(self.client_ip, self.client_port)
147 def wait_conn_up(self):
148 conn, addr = self.su.accept()
149 nonce = _get_nonce()
150 sys.stdout.write(_timestamp() + "CONN: %s (nonce: %s) ... "%(_format_addr(addr), nonce))
151 conn.send(nonce + '\n')
152 cmd = conn.recv(1 + self.hashlen).rstrip('\n')
153 if cmd == _hash('adm' + self.secret + nonce):
154 if self.control_socket:
155 self.control_socket.close()
156 self.control_socket = conn
157 print("OK (admin)")
158 elif cmd == _hash('skt' + self.secret + nonce):
159 print("OK (tunnel)")
160 if not self.control_socket:
161 conn.close()
162 else:
163 self.next_upstream_socket = conn
164 else:
165 print("rejected!")
166 conn.close()
168 def wait_conn_client(self):
169 conn, addr = self.sc.accept()
170 self.control_socket.send("CONN:%s\n"%_format_addr(addr))
171 thread = Tunnel(self.next_upstream_socket, conn, addr)
172 thread.start()
173 self.threads.append(thread)
174 self.next_upstream_socket = None
176 def doit(self):
177 while True:
178 i = 0
179 while i < len(self.threads):
180 if self.threads[i].finished():
181 self.threads[i].reap()
182 self.threads.pop(i)
183 else:
184 i += 1
185 if not self.control_socket:
186 self.wait_conn_up()
187 if not self.next_upstream_socket:
188 self.wait_conn_up()
189 if self.control_socket and self.next_upstream_socket:
190 a,b,c = select.select([self.sc, self.control_socket, ], [], [])
191 if self.control_socket in a:
192 print("lost control socket")
193 self.control_socket.close()
194 self.control_socket = None
195 continue
196 if self.next_upstream_socket in a:
197 print("lost spare upstream socket")
198 self.next_upstream_socket.close()
199 self.next_upstream_socket = None
200 continue
201 if self.sc in a:
202 self.wait_conn_client()
205 if __name__ == "__main__":
206 import argparse
207 desc=(
208 "NAT Tunnel v0.01\n"
209 "----------------\n"
210 "If you have access to a server with public IP and unfiltered ports\n"
211 "you can run NAT Tunnel (NT) server on the server, and NT client\n"
212 "on your box behind NAT.\n"
213 "the server requires 2 open ports: one for communication with the\n"
214 "NT client (--admin), the other for regular clients to connect to\n"
215 "(--public: this is the port you want your users to use).\n"
216 "\n"
217 "The NT client opens a connection to the server's admin ip/port.\n"
218 "As soon as the server receives a new connection, it signals the\n"
219 "NT client, which then creates a new tunnel connection to the\n"
220 "server, which is then connected to the desired service on the\n"
221 "NT client's side (--local)\n"
222 "\n"
223 "The connection between NT Client and NT Server on the admin\n"
224 "interface is protected by a shared secret against unauthorized use.\n"
225 "An adversary who can intercept packets could crack the secret\n"
226 "if it's of insufficient complexity. At least 10 random\n"
227 "characters and numbers are recommended.\n"
228 "\n"
229 "Example:\n"
230 "You have a HTTP server listening on your local machine on port 80.\n"
231 "You want to make it available on your cloud server/VPS/etc's public\n"
232 "IP on port 7000.\n"
233 "We use port 8000 on the cloud server for the control channel.\n"
234 "\n"
235 "Server:\n"
236 " %s --mode server --secret s3cretP4ss --public 0.0.0.0:7000 --admin 0.0.0.0:8000\n"
237 "Client:\n"
238 " %s --mode client --secret s3cretP4ss --local localhost:80 --admin example.com:8000\n"
239 ) % (sys.argv[0], sys.argv[0])
240 if len(sys.argv) < 2 or (sys.argv[1] == '-h' or sys.argv[1] == '--help'):
241 print(desc)
242 parser = argparse.ArgumentParser(description='')
243 parser.add_argument('--secret', help='shared secret between natserver/client', type=str, default='', required=True)
244 parser.add_argument('--mode', help='work mode: server or client', type=str, default='server', required=True)
245 parser.add_argument('--public', help='(server only) ip:port where we will listen for regular clients', type=str, default='0.0.0.0:8080', required=False)
246 parser.add_argument('--local', help='(client only) ip:port of the local target service', type=str, default="localhost:80", required=False)
247 parser.add_argument('--admin', help='ip:port tuple for admin/upstream/control connection', type=str, default="0.0.0.0:8081", required=False)
248 args = parser.parse_args()
249 adminip, adminport = args.admin.split(':')
250 if args.mode == 'server':
251 clientip, clientport = args.public.split(':')
252 srv = NATSrv(args.secret, adminip, int(adminport), clientip, int(clientport))
253 srv.setup()
254 srv.doit()
255 else:
256 localip, localport = args.local.split(':')
257 cl = NATClient(args.secret, adminip, int(adminport), localip, int(localport))
258 cl.setup()
259 cl.doit()