def main(): addr = config.tun_addr or config.uri_addr port = config.tun_port or config.uri_port uri = config.uri factory = WebSocketServerFactory(uri) factory.protocol = WSTunServerProtocol factory.server = '' # hide Server field of handshake HTTP header factory.autoPingInterval = 400 # only used to clear half-open connections factory.autoPingTimeout = 30 factory.openHandshakeTimeout = 8 # timeout after TCP established and before succeeded WS handshake try: server = loop.run_until_complete( loop.create_server(factory, addr, port)) except OSError: die('wstan server failed to bind on %s:%d' % (addr, port)) so = server.sockets[0] if len(server.sockets) == 1 and so.family == socket.AF_INET6 and hasattr( socket, 'IPPROTO_IPV6'): # force user to specify URI in wstan server is a bad design, this try to fix # inconvenience in dual stack server so.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # default 1 in Linux loop.set_exception_handler(silent_timeout_err_handler) async_(clean_seen_nonce()) print('wstan server -- listening on %s:%d' % (addr, port)) try: loop.run_forever() except KeyboardInterrupt: pass finally: server.close() loop.close()
def main(): addr = config.tun_addr or config.uri_addr port = config.tun_port or config.uri_port uri = config.uri factory = WebSocketServerFactory(uri) factory.protocol = WSTunServerProtocol factory.server = '' # hide Server field of handshake HTTP header factory.autoPingInterval = 400 # only used to clear half-open connections factory.autoPingTimeout = 30 factory.openHandshakeTimeout = 8 # timeout after TCP established and before succeeded WS handshake try: server = loop.run_until_complete(loop.create_server(factory, addr, port)) except OSError: die('wstan server failed to bind on %s:%d' % (addr, port)) so = server.sockets[0] if len(server.sockets) == 1 and so.family == socket.AF_INET6 and hasattr(socket, 'IPPROTO_IPV6'): # force user to specify URI in wstan server is a bad design, this try to fix # inconvenience in dual stack server so.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # default 1 in Linux loop.set_exception_handler(silent_timeout_err_handler) async_(clean_seen_nonce()) print('wstan server -- listening on %s:%d' % (addr, port)) try: loop.run_forever() except KeyboardInterrupt: pass finally: server.close() loop.close()
def main(): addr = config.tun_addr or config.uri_addr port = config.tun_port or config.uri_port try: c = loop.create_server_tfo if config.tfo else loop.create_server server = loop.run_until_complete(c(factory, addr, port)) except OSError: die('wstan server failed to bind on %s:%d' % (addr, port)) so = server.sockets[0] if len(server.sockets) == 1 and so.family == socket.AF_INET6 and hasattr( socket, 'IPPROTO_IPV6'): # force user to specify URI in wstan server is a bad design, this try to fix # inconvenience in dual stack server so.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) # default 1 in Linux loop.set_exception_handler(silent_timeout_err_handler) async_(clean_seen_nonce()) print('wstan server -- listening on %s:%d' % (addr, port)) try: loop.run_forever() except KeyboardInterrupt: pass finally: server.close() loop.close()
def onConnect(self, request): # ----- init decryptor ----- if not config.tun_ssl: if config.compatible: cookie = request.headers['cookie'] if cookie.count(';') > 0: raise ConnectionDeny(400) if not cookie.startswith(config.cookie_key + '='): raise ConnectionDeny(400) nonceB64 = cookie.lstrip(config.cookie_key + '=') else: nonceB64 = request.headers['sec-websocket-key'] try: nonce = base64.b64decode(nonceB64) self.initCipher(nonce, decryptor=True) except Exception as e: logging.error('failed to initialize cipher: %s' % e) raise ConnectionDeny(400) else: nonceB64 = nonce = None # for decrypting # ----- extract header ----- path = self.http_request_path try: if path.startswith(factory.path): path = path[len(factory.path):] dat = base64.urlsafe_b64decode( path[1:] if path.startswith('/') else path) cmd = ord(self.decrypt(dat[:1])) addr, port, remainData, timestamp = self.parseRelayHeader(dat) if cmd != self.CMD_REQ: raise ValueError('wrong command %s' % cmd) except (ValueError, Base64Error) as e: logging.error('invalid request: %s (from %s), path: %s' % (e, self.peer, path)) raise ConnectionDeny(400) if not config.tun_ssl: # filter replay attack seen = seenNonceByTime[timestamp // 10] if nonce in seen: logging.warning('replay attack detected (from %s)' % self.peer) raise ConnectionDeny(400) seen.add(nonce) if config.compatible: # avoid generating a new random nonce for encrypting, and client will do same # calculating to get this nonce encNonce = get_sha1(nonce)[:16] else: # repeat calculation in websocket library so that key in WS handshake reply # is the same as this one encNonce = get_sha1( nonceB64.encode() + b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")[:16] self.initCipher(encNonce, encryptor=True) self.tunOpen.set_result(None) self.connectTargetTask = async_( self.connectTarget(addr, port, remainData))
def onMessage(self, dat, isBinary): if not isBinary: logging.error('non binary ws message received (from %s)' % self.clientInfo) return self.sendClose(3000) cmd = ord(self.decrypt(dat[:1])) if cmd == self.CMD_RST: try: msg = self.parseResetMessage(dat) except ValueError as e: logging.error('invalid reset message: %s (from %s)' % (e, self.clientInfo)) return self.sendClose(3000) if not msg.startswith(' '): logging.info('tunnel abnormal reset: %s' % msg) self.onResetTunnel() elif cmd == self.CMD_REQ: try: if self.tunState != self.TUN_STATE_IDLE: raise Exception('reset received when not idle') addr, port, remainData, __ = self.parseRelayHeader(dat) except Exception as e: logging.error('invalid request in reused tun: %s (from %s)' % (e, self.clientInfo)) return self.sendClose(3000) self.connectTargetTask = async_(self.connectTarget(addr, port, remainData)) elif cmd == self.CMD_DAT: dat = self.decrypt(dat[1:]) if self.tunState == self.TUN_STATE_RESETTING: return if self.connectTargetTask: self._dataToTarget += dat return self._writer.write(dat) else: logging.error('wrong command: %s (from %s)' % (cmd, self.clientInfo)) self.sendClose(3000)
def startProxy(cls, addrHeader, dat, reader, writer): canErr = can_return_error_page(dat) if cls.pool: logging.debug('reuse tunnel from pool (total %s)' % len(cls.pool)) tun = cls.pool[0] tun.checkTimeoutTask.cancel() tun.checkTimeoutTask = None tun.tryRemoveFromPool() tun.setAutoPing(cls.TUN_AUTO_PING_INTERVAL, cls.TUN_AUTO_PING_TIMEOUT) tun.sendMessage(tun.makeRelayHeader(addrHeader, dat), True) else: try: sock = None if config.proxy: sock = yield from setup_http_tunnel() tun = (yield from loop.create_connection( factory, None if sock else config.uri_addr, None if sock else config.uri_port, server_hostname=config.uri_addr if config.tun_ssl else None, sock=sock, ssl=config.tun_ssl))[1] # lower latency by sending relay header and data in ws handshake tun.customUriPath = '/' + base64.urlsafe_b64encode( tun.makeRelayHeader(addrHeader, dat)).decode() async_(tun.restartHandshake()) yield from wait_for(tun.tunOpen, tun.openHandshakeTimeout) except Exception as e: if isinstance(e, (asyncio.TimeoutError, asyncio.CancelledError)): # sometimes reason can be None in extremely poor network msg = tun.wasNotCleanReason or '' else: msg = str(e) msg = translate_err_msg(msg) logging.error("can't connect to server: %s" % msg) if canErr: writer.write( gen_error_page("can't connect to wstan server", msg)) return writer.close() tun.canReturnErrorPage = canErr tun.setProxy(reader, writer)
def addToPool(self): assert self.tunState == self.TUN_STATE_IDLE if len(self.pool) >= self.POOL_MAX_SIZE: self.sendClose(1000) else: assert not self.checkTimeoutTask self.checkTimeoutTask = async_(self._checkTimeout()) self.inPool = True self.setAutoPing(self.POOL_AUTO_PING_INTERVAL, self.POOL_AUTO_PING_TIMEOUT) self.pool.append(self)
def onConnect(self, request): self.clientInfo = '{0}:{1}'.format(*self.transport.get_extra_info('peername')) # ----- init decryptor ----- if not config.tun_ssl: if config.compatible: cookie = request.headers['cookie'] if cookie.count(';') > 0: raise ConnectionDeny(400) if not cookie.startswith(config.cookie_key + '='): raise ConnectionDeny(400) nonceB64 = cookie.lstrip(config.cookie_key + '=') else: nonceB64 = request.headers['sec-websocket-key'] try: nonce = base64.b64decode(nonceB64) self.initCipher(nonce, decryptor=True) except Exception as e: logging.error('failed to initialize cipher: %s' % e) raise ConnectionDeny(400) else: nonceB64 = nonce = None # for decrypting # ----- extract header ----- try: dat = base64.urlsafe_b64decode(self.http_request_path[1:]) cmd = ord(self.decrypt(dat[:1])) addr, port, remainData, timestamp = self.parseRelayHeader(dat) if cmd != self.CMD_REQ: raise ValueError('wrong command %s' % cmd) except (ValueError, Base64Error) as e: logging.error('invalid request: %s (from %s), path: %s' % (e, self.clientInfo, self.http_request_path)) raise ConnectionDeny(400) if not config.tun_ssl: # filter replay attack seen = seenNonceByTime[timestamp // 10] if nonce in seen: logging.warning('replay attack detected (from %s)' % self.clientInfo) raise ConnectionDeny(400) seen.add(nonce) if config.compatible: # avoid generating a new random nonce for encrypting, and client will do same # calculating to get this nonce encNonce = get_sha1(nonce)[:16] else: # repeat calculation in websocket library so that key in WS handshake reply # is the same as this one encNonce = get_sha1(nonceB64.encode() + b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")[:16] self.initCipher(encNonce, encryptor=True) self.tunOpen.set_result(None) self.connectTargetTask = async_(self.connectTarget(addr, port, remainData))
def startProxy(cls, addrHeader, dat, reader, writer): if not dat: logging.debug('startProxy with no data') canErr = can_return_error_page(dat) if cls.pool: logging.debug('reuse tunnel from pool (total %s)' % len(cls.pool)) tun = cls.pool[0] tun.checkTimeoutTask.cancel() tun.checkTimeoutTask = None tun.tryRemoveFromPool() tun.setAutoPing(cls.TUN_AUTO_PING_INTERVAL, cls.TUN_AUTO_PING_TIMEOUT) tun.canReturnErrorPage = canErr tun.setProxy(reader, writer) tun.sendMessage(tun.makeRelayHeader(addrHeader, dat), True) else: try: sock = None if config.proxy: sock = yield from setup_http_tunnel() tun = (yield from loop.create_connection( factory, None if sock else config.uri_addr, None if sock else config.uri_port, server_hostname=config.uri_addr if config.tun_ssl else None, sock=sock, ssl=config.tun_ssl))[1] # Lower latency by sending relay header and data in ws handshake tun.customUriPath = factory.path + base64.urlsafe_b64encode( tun.makeRelayHeader(addrHeader, dat)).decode() tun.canReturnErrorPage = canErr tun.setProxy(reader, writer, startPushLoop=False) async_(tun.restartHandshake()) # Data may arrive before setProxy if wait for onOpen here # and then set proxy. except Exception as e: msg = translate_err_msg(str(e)) logging.error("can't connect to server: %s" % msg) if canErr: writer.write(gen_error_page("can't connect to wstan server", msg)) return writer.close()
def http_proxy_handler(dat, reader, writer): # get request line and header while True: # the line is not likely to be that long if b'\r\n\r\n' in dat: break r = yield from reader.read(1024) if not r: return writer.close() dat += r rl_end = dat.find(b'\r\n') req_line, rest_dat = dat[:rl_end], dat[rl_end:] method, url, ver = req_line.split() if method == b'CONNECT': # e.g. g.cn:443 host, port = url.split(b':') port = int(port) elif url.startswith(b'http'): # e.g. http://g.cn/aa parsed = urlparse.urlparse(url) path, host, port = parsed.path, parsed.hostname, parsed.port or 80 else: writer.write(gen_log_view_page()) return writer.close() addr_header = make_socks_addr(host, port) if method == b'CONNECT': writer.write(b'HTTP/1.1 200 Connection Established\r\n\r\n') try: dat = yield from wait_for(reader.read(2048), 0.01) if not dat: return writer.close() except asyncio.TimeoutError: dat = None else: dat = method + b' ' + path + b' ' + ver + rest_dat dat = http_die_soon(dat) # let target know keep-alive is not supported logging.info('requesting %s:%d' % (host.decode(), port)) async_(WSTunClientProtocol.startProxy(addr_header, dat, reader, writer))
def startProxy(cls, addrHeader, dat, reader, writer): canErr = can_return_error_page(dat) if cls.pool: logging.debug('reuse tunnel from pool (total %s)' % len(cls.pool)) tun = cls.pool[0] tun.checkTimeoutTask.cancel() tun.checkTimeoutTask = None tun.tryRemoveFromPool() tun.setAutoPing(cls.TUN_AUTO_PING_INTERVAL, cls.TUN_AUTO_PING_TIMEOUT) tun.sendMessage(tun.makeRelayHeader(addrHeader, dat), True) else: try: sock = None if config.proxy: sock = yield from setup_http_tunnel() tun = (yield from loop.create_connection( factory, None if sock else config.uri_addr, None if sock else config.uri_port, server_hostname=config.uri_addr if config.tun_ssl else None, sock=sock, ssl=config.tun_ssl))[1] # lower latency by sending relay header and data in ws handshake tun.customUriPath = '/' + base64.urlsafe_b64encode(tun.makeRelayHeader(addrHeader, dat)).decode() async_(tun.restartHandshake()) yield from wait_for(tun.tunOpen, tun.openHandshakeTimeout) except Exception as e: if isinstance(e, (asyncio.TimeoutError, asyncio.CancelledError)): # sometimes reason can be None in extremely poor network msg = tun.wasNotCleanReason or '' else: msg = str(e) msg = translate_err_msg(msg) logging.error("can't connect to server: %s" % msg) if canErr: writer.write(gen_error_page("can't connect to wstan server", msg)) return writer.close() tun.canReturnErrorPage = canErr tun.setProxy(reader, writer)
def socks5_tcp_handler(dat, reader, writer): # handle auth method selection if len(dat) < 2 or len(dat) != dat[1] + 2: logging.warning('bad SOCKS v5 request') return writer.close() writer.write(b'\x05\x00') # \x00 == NO AUTHENTICATION REQUIRED # handle relay request dat = yield from reader.read(262) try: cmd, addr_header = dat[1], dat[2:] target_addr, target_port = parse_socks_addr(addr_header) except (ValueError, IndexError): logging.warning('invalid SOCKS v5 relay request') return writer.close() if cmd != 0x01: # CONNECT writer.write(b'\x05\x07\x00\x01' + b'\x00' * 6) # \x07 == COMMAND NOT SUPPORTED return writer.close() # Delay can be lowered (of a round-trip) by accepting request before connected to target. # But SOCKS client can't get real reason when error happens (not a big problem, Firefox always # display connection reset error). Dirty solution: generate a HTML page when a HTTP request failed writer.write(b'\x05\x00\x00\x01' + b'\x01' * 6) # \x00 == SUCCEEDED try: dat = yield from wait_for(reader.read(2048), 0.02) if not dat: return writer.close() except asyncio.TimeoutError: # 20ms passed and no data received, rare but legal behavior. # timeout may always happen if set to 10ms, and enable asyncio library debug mode will "fix" it # e.g. Old SSH client will wait for server after conn established dat = None async_( WSTunClientProtocol.openTunnel((target_addr, target_port), dat, reader, writer))
def onMessage(self, dat, isBinary): if not isBinary: logging.error('non binary ws message received (from %s)' % self.clientInfo) return self.sendClose(3000) cmd = ord(self.decrypt(dat[:1])) if cmd == self.CMD_RST: try: msg = self.parseResetMessage(dat) except ValueError as e: logging.error('invalid reset message: %s (from %s)' % (e, self.clientInfo)) return self.sendClose(3000) if not msg.startswith(' '): logging.info('tunnel abnormal reset: %s' % msg) self.onResetTunnel() elif cmd == self.CMD_REQ: try: if self.tunState != self.TUN_STATE_IDLE: raise Exception('reset received when not idle') addr, port, remainData, __ = self.parseRelayHeader(dat) except Exception as e: logging.error('invalid request in reused tun: %s (from %s)' % (e, self.clientInfo)) return self.sendClose(3000) self.connectTargetTask = async_(self.connectTarget(addr, port, remainData)) elif cmd == self.CMD_DAT: dat = self.decrypt(dat[1:]) if self.tunState == self.TUN_STATE_RESETTING: return if self.connectTargetTask: logging.debug('data received when connectTargetTask running') # will order of messages be changed by waiting? yield from wait_for(self.connectTargetTask, None) self._writer.write(dat) else: logging.error('wrong command: %s (from %s)' % (cmd, self.clientInfo)) self.sendClose(3000)
def onOpen(self): self.tunOpen.set_result(None) now = time.time() self.lastIdleTime = now # measure RTT rtt = now - self.handshakeSentTime if WSTunClientProtocol.rtt is None: WSTunClientProtocol.rtt = rtt else: WSTunClientProtocol.rtt = 0.8 * WSTunClientProtocol.rtt + 0.2 * rtt assert not self._pushToTunTask self._pushToTunTask = async_(self._pushToTunnelLoop()) self.setAutoPing(self.TUN_AUTO_PING_INTERVAL, self.TUN_AUTO_PING_TIMEOUT) if not config.debug: self.customUriPath = None # save memory if not config.tun_ssl: if config.compatible: nonce = get_sha1(base64.b64decode(self.customWsKey))[:16] else: # SHA-1 has 20 bytes nonce = base64.b64decode(self.http_headers['sec-websocket-accept'])[:16] self.initCipher(nonce, decryptor=True)
def _onMessageEnd(self): res = self.onMessageEnd() if yields(res): asyncio.async_(res)
def _onMessage(self, payload, isBinary): res = self.onMessage(payload, isBinary) if yields(res): asyncio.async_(res)
def setProxy(self, reader, writer): self.tunState = self.TUN_STATE_USING self._reader, self._writer = reader, writer self._pushToTunTask = async_(self._pushToTunnelLoop())
def _onConnect(self, response): res = self.onConnect(response) if yields(res): asyncio.async_(res)
def _onMessageBegin(self, isBinary): res = self.onMessageBegin(isBinary) if yields(res): asyncio.async_(res)
def startPushToTunLoop(self): assert not self._pushToTunTask self._pushToTunTask = async_(self._pushToTunnelLoop()) self._pushToTunTask.add_done_callback(_on_pushToTunTaskDone)
def _onMessageFrame(self, payload): res = self.onMessageFrame(payload) if yields(res): asyncio.async_(res)
def _onMessageFrameBegin(self, length): res = self.onMessageFrameBegin(length) if yields(res): asyncio.async_(res)
def _onPong(self, payload): res = self.onPong(payload) if yields(res): asyncio.async_(res)
def _onClose(self, wasClean, code, reason): res = self.onClose(wasClean, code, reason) if yields(res): asyncio.async_(res)
def openTunnel(cls, target, dat, reader, writer, retryCount=0): logging.info('requesting %s:%d' % target) if not dat: logging.debug('openTunnel with no data') canErr = can_return_error_page(dat) if cls.pool: # reuse from pool logging.debug('reuse tunnel from pool (total %s)' % len(cls.pool)) tun = cls.pool[0] tun.checkTimeoutTask.cancel() tun.checkTimeoutTask = None tun.tryRemoveFromPool() tun.setAutoPing(cls.TUN_AUTO_PING_INTERVAL, cls.TUN_AUTO_PING_TIMEOUT) tun.canReturnErrorPage = canErr tun.setProxy(reader, writer) tun.sendMessage(tun.makeRelayHeader(target, dat), True) return # new tunnel try: if retryCount >= cls.MAX_RETRY_COUNT: raise ConnectionResetError( 'run into tcp reset, all retries failed') sock = None if config.proxy: sock = yield from setup_http_tunnel() tun = factory() # Lower latency by sending relay header and data in ws handshake tun.customUriPath = factory.path + base64.urlsafe_b64encode( tun.makeRelayHeader(target, dat)).decode() tun.canReturnErrorPage = canErr # Data may arrive before setProxy if wait for tunOpen here and then set proxy. tun.setProxy(reader, writer, startPushLoop=False) # push loop will start in onOpen if config.tfo: assert not config.proxy and not config.tun_ssl tun.noSendHandshake = True tun.startHandshake() # tfo is meaningless if handshake data can't fit into TCP SYN packet # switch back to normal sock_connect just in case my Windows tfo extension has bug tfoDat = tun.http_request_data if len( tun.http_request_data) <= 1400 else None sock = yield from my_sock_connect(config.uri_addr, config.uri_port, tfo_dat=tfoDat) # it will return after SYN,ACK received regardless of TFO if not tfoDat: loop.sock_sendall(sock, tun.http_request_data) yield from loop.create_connection( lambda: tun, None if sock else config.uri_addr, None if sock else config.uri_port, server_hostname=config.uri_addr if config.tun_ssl else None, sock=sock, ssl=config.tun_ssl) except Exception as e: msg = translate_err_msg(str(e)) dest = 'proxy' if config.proxy and not sock else 'wstan server' logging.error("can't connect to %s: %s" % (dest, msg)) if canErr: writer.write(gen_error_page("can't connect to " + dest, msg)) return writer.close() try: yield from wait_for(tun.tunOpen, None) except CancelledError: # sometimes reason can be None in extremely poor network msg = tun.wasNotCleanReason or '' if isinstance(tun.connLostReason, ConnectionResetError): # GFW random reset HTTP stream it can't recognize, just retry return async_( cls.openTunnel(target, dat, reader, writer, retryCount + 1)) msg = translate_err_msg(msg) logging.error("can't connect to server: %s" % msg) if tun.wasNotCleanReason and tun.canReturnErrorPage: # write before closing writer writer.write( gen_error_page("can't connect to wstan server", msg)) return writer.close() if retryCount > 0: logging.debug('tcp reset happen, retried %d times' % retryCount)
def _onOpen(self): res = self.onOpen() if yields(res): asyncio.async_(res)
def setProxy(self, reader, writer, startPushLoop=True): self.tunState = self.TUN_STATE_USING self._reader, self._writer = reader, writer if startPushLoop: self._pushToTunTask = async_(self._pushToTunnelLoop())