From e06819c124d025c1a63d61c20ad37dbbf63cc534 Mon Sep 17 00:00:00 2001 From: Falseen Date: Thu, 2 Mar 2017 21:20:04 +0800 Subject: [PATCH] add simple ss-tunnel to shadowsocks for dns forward (#759) * add vscode to .gitignore * add config.json to gitignore * add simple ss-tunnel to shadowsocks for dns forward 1.add tunnel.py file to shadowoscks for dns forward (seem ss-tunnel of ss-libev) 2.add add_header to common.py for add socks5 request header 3.add dns_service dns_server dns_server_port dns_server_port dns_local_port to shell.py and config.json 4.update to udprelay for ss-tunnel (dns forward) 5.update to local.py for ss-tunnel * add config.json.example to shadowsocks * add tunnel_udp_server.close to local.py and tunnel.py * fix error about pep8 and pyflakes for travis * update rename rename add "tunnel_" to dns_service dns_server dns_server_port dns_local_port * fix tunnel for run tunnel alone * fix pep8 for travis * update the config name for tunnel ord name: tunnel_service tunnel_dns_server tunnel_dns_server_port tunnel_dns_local_port new name: both_tunnel_local tunnel_remote tunnel_remote_port tunnel_port * update for tunnel update to local.py shell.py tunnel.py for tunnel * update pep8 for travis * update config to _config for tunnel and fix pep8 * changed "add socks5 header to data" to "add ss header to data" and changed "remove socks5 header" to "remove ss header" * add tcp forward to tunnel and fix a bug for pack_addr from common.py 1. update tunnel to tcprelay.py 2. add tunnel_tcp_server to tunnel.py 3. add tunnel_tcp_server to local.py 4. add `address = to_bytes(address)` to `pack_addr` from common.py (fix a error when address is a domain) * fix pep8 for travis again * remove ss_header from tcprelay.py and update the "header_length" to udprelay.py 1. Remove unnecessary "add ss_header" from tcprelay.py 2. update "data[7:]" to "data[header_length:]" * remove "both_tunnel_local" and fix some error for tunnel * update * update add_header * rename is_tunnel to _is_tunnel https://github.com/shadowsocks/shadowsocks/pull/759 --- .gitignore | 7 ++++ config.json.example | 13 +++++++ shadowsocks/common.py | 8 +++++ shadowsocks/shell.py | 18 ++++++++++ shadowsocks/tcprelay.py | 76 ++++++++++++++++++++++++++--------------- shadowsocks/tunnel.py | 74 +++++++++++++++++++++++++++++++++++++++ shadowsocks/udprelay.py | 33 ++++++++++++++---- 7 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 config.json.example create mode 100644 shadowsocks/tunnel.py diff --git a/.gitignore b/.gitignore index 4907974..0b36d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,10 @@ htmlcov #Emacs .#* + +#vscode +.idea +.vscode + +#ss +config.json diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..4006656 --- /dev/null +++ b/config.json.example @@ -0,0 +1,13 @@ +{ + "server":"127.0.0.1", + "server_port":8388, + "local_port":1080, + "password":"password", + "timeout":600, + "method":"aes-256-cfb", + "local_address":"127.0.0.1", + "fast_open":false, + "tunnel_remote":"8.8.8.8", + "tunnel_remote_port":53, + "tunnel_port":53 +} diff --git a/shadowsocks/common.py b/shadowsocks/common.py index ee14995..1a58457 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -146,6 +146,7 @@ ADDRTYPE_MASK = 0xF def pack_addr(address): address_str = to_str(address) + address = to_bytes(address) for family in (socket.AF_INET, socket.AF_INET6): try: r = socket.inet_pton(family, address_str) @@ -160,6 +161,13 @@ def pack_addr(address): return b'\x03' + chr(len(address)) + address +# add ss header +def add_header(address, port, data=b''): + _data = b'' + _data = pack_addr(address) + struct.pack('>H', port) + data + return _data + + def parse_header(data): addrtype = ord(data[0]) dest_addr = None diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 1b65594..b46510c 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -132,6 +132,13 @@ def check_config(config, is_local): sys.exit(2) else: config['server'] = to_str(config['server']) + + if config.get('tunnel_remote', None) is None: + logging.error('tunnel_remote addr not specified') + print_local_help() + sys.exit(2) + else: + config['tunnel_remote'] = to_str(config['tunnel_remote']) else: config['server'] = to_str(config.get('server', '0.0.0.0')) try: @@ -159,6 +166,12 @@ def check_config(config, is_local): if 'server_port' in config and type(config['server_port']) != list: config['server_port'] = int(config['server_port']) + if 'tunnel_remote_port' in config: + config['tunnel_remote_port'] = \ + int(config['tunnel_remote_port']) + if 'tunnel_port' in config: + config['tunnel_port'] = int(config['tunnel_port']) + if config.get('local_address', '') in [b'0.0.0.0']: logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') if config.get('server', '') in ['127.0.0.1', 'localhost']: @@ -299,6 +312,11 @@ def get_config(is_local): config['one_time_auth'] = config.get('one_time_auth', False) config['prefer_ipv6'] = config.get('prefer_ipv6', False) config['server_port'] = config.get('server_port', 8388) + + config['tunnel_remote'] = \ + to_str(config.get('tunnel_remote', '8.8.8.8')) + config['tunnel_remote_port'] = config.get('tunnel_remote_port', 53) + config['tunnel_port'] = config.get('tunnel_port', 53) config['dns_server'] = config.get('dns_server', None) logging.getLogger('').handlers = [] diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 207407a..a61cba7 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -106,6 +106,7 @@ class NoAcceptableMethods(Exception): class TCPRelayHandler(object): + def __init__(self, server, fd_to_handlers, loop, local_sock, config, dns_resolver, is_local): self._server = server @@ -115,6 +116,10 @@ class TCPRelayHandler(object): self._remote_sock = None self._config = config self._dns_resolver = dns_resolver + self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8") + self.tunnel_remote_port = config.get('tunnel_remote_port', 53) + self.tunnel_port = config.get('tunnel_port', 53) + self._is_tunnel = server._is_tunnel # TCP Relay works as either sslocal or ssserver # if is_local, this is sslocal @@ -250,7 +255,6 @@ class TCPRelayHandler(object): else: self._data_to_write_to_remote.append(data) return - if self._ota_enable_session: data = self._ota_chunk_data_gen(data) data = self._encryptor.encrypt(data) @@ -293,29 +297,36 @@ class TCPRelayHandler(object): @shell.exception_handle(self_=True, destroy=True, conn_err=True) def _handle_stage_addr(self, data): if self._is_local: - cmd = common.ord(data[1]) - if cmd == CMD_UDP_ASSOCIATE: - logging.debug('UDP associate') - if self._local_sock.family == socket.AF_INET6: - header = b'\x05\x00\x00\x04' - else: - header = b'\x05\x00\x00\x01' - addr, port = self._local_sock.getsockname()[:2] - addr_to_send = socket.inet_pton(self._local_sock.family, - addr) - port_to_send = struct.pack('>H', port) - self._write_to_sock(header + addr_to_send + port_to_send, - self._local_sock) - self._stage = STAGE_UDP_ASSOC - # just wait for the client to disconnect - return - elif cmd == CMD_CONNECT: - # just trim VER CMD RSV - data = data[3:] + if self._is_tunnel: + # add ss header to data + tunnel_remote = self.tunnel_remote + tunnel_remote_port = self.tunnel_remote_port + data = common.add_header(tunnel_remote, + tunnel_remote_port, data) else: - logging.error('unknown command %d', cmd) - self.destroy() - return + cmd = common.ord(data[1]) + if cmd == CMD_UDP_ASSOCIATE: + logging.debug('UDP associate') + if self._local_sock.family == socket.AF_INET6: + header = b'\x05\x00\x00\x04' + else: + header = b'\x05\x00\x00\x01' + addr, port = self._local_sock.getsockname()[:2] + addr_to_send = socket.inet_pton(self._local_sock.family, + addr) + port_to_send = struct.pack('>H', port) + self._write_to_sock(header + addr_to_send + port_to_send, + self._local_sock) + self._stage = STAGE_UDP_ASSOC + # just wait for the client to disconnect + return + elif cmd == CMD_CONNECT: + # just trim VER CMD RSV + data = data[3:] + else: + logging.error('unknown command %d', cmd) + self.destroy() + return header_result = parse_header(data) if header_result is None: raise Exception('can not parse header') @@ -347,10 +358,12 @@ class TCPRelayHandler(object): self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._stage = STAGE_DNS if self._is_local: - # forward address to remote - self._write_to_sock((b'\x05\x00\x00\x01' - b'\x00\x00\x00\x00\x10\x10'), - self._local_sock) + # jump over socks5 response + if not self._is_tunnel: + # forward address to remote + self._write_to_sock((b'\x05\x00\x00\x01' + b'\x00\x00\x00\x00\x10\x10'), + self._local_sock) # spec https://shadowsocks.org/en/spec/one-time-auth.html # ATYP & 0x10 == 0x10, then OTA is enabled. if self._ota_enable_session: @@ -558,7 +571,12 @@ class TCPRelayHandler(object): self._handle_stage_stream(data) return elif is_local and self._stage == STAGE_INIT: - self._handle_stage_init(data) + # jump over socks5 init + if self._is_tunnel: + self._handle_stage_addr(data) + return + else: + self._handle_stage_init(data) elif self._stage == STAGE_CONNECTING: self._handle_stage_connecting(data) elif (is_local and self._stage == STAGE_ADDR) or \ @@ -689,6 +707,7 @@ class TCPRelayHandler(object): class TCPRelay(object): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._config = config self._is_local = is_local @@ -696,6 +715,7 @@ class TCPRelay(object): self._closed = False self._eventloop = None self._fd_to_handlers = {} + self._is_tunnel = False self._timeout = config['timeout'] self._timeouts = [] # a list for all the handlers diff --git a/shadowsocks/tunnel.py b/shadowsocks/tunnel.py new file mode 100644 index 0000000..dbfb438 --- /dev/null +++ b/shadowsocks/tunnel.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 2012-2015 clowwindy +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import os +import logging +import signal + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) +from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns + + +@shell.exception_handle(self_=False, exit_code=1) +def main(): + shell.check_python() + + # fix py2exe + if hasattr(sys, "frozen") and sys.frozen in \ + ("windows_exe", "console_exe"): + p = os.path.dirname(os.path.abspath(sys.executable)) + os.chdir(p) + + config = shell.get_config(True) + daemon.daemon_exec(config) + dns_resolver = asyncdns.DNSResolver() + loop = eventloop.EventLoop() + dns_resolver.add_to_loop(loop) + _config = config.copy() + _config["local_port"] = _config["tunnel_port"] + logging.info("starting tcp tunnel at %s:%d forward to %s:%d" % + (_config['local_address'], _config['local_port'], + _config['tunnel_remote'], _config['tunnel_remote_port'])) + tunnel_tcp_server = tcprelay.TCPRelay(_config, dns_resolver, True) + tunnel_tcp_server._is_tunnel = True + tunnel_tcp_server.add_to_loop(loop) + logging.info("starting udp tunnel at %s:%d forward to %s:%d" % + (_config['local_address'], _config['local_port'], + _config['tunnel_remote'], _config['tunnel_remote_port'])) + tunnel_udp_server = udprelay.UDPRelay(_config, dns_resolver, True) + tunnel_udp_server._is_tunnel = True + tunnel_udp_server.add_to_loop(loop) + + def handler(signum, _): + logging.warn('received SIGQUIT, doing graceful shutting down..') + tunnel_tcp_server.close(next_tick=True) + tunnel_udp_server.close(next_tick=True) + signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) + + def int_handler(signum, _): + sys.exit(1) + signal.signal(signal.SIGINT, int_handler) + + daemon.set_user(config.get('user', None)) + loop.run() + +if __name__ == '__main__': + main() diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index ba8299b..68e7c55 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -82,6 +82,7 @@ def client_key(source_addr, server_af): class UDPRelay(object): + def __init__(self, config, dns_resolver, is_local, stat_callback=None): self._config = config if is_local: @@ -94,6 +95,10 @@ class UDPRelay(object): self._listen_port = config['server_port'] self._remote_addr = None self._remote_port = None + self.tunnel_remote = config.get('tunnel_remote', "8.8.8.8") + self.tunnel_remote_port = config.get('tunnel_remote_port', 53) + self.tunnel_port = config.get('tunnel_port', 53) + self._is_tunnel = False self._dns_resolver = dns_resolver self._password = common.to_bytes(config['password']) self._method = config['method'] @@ -151,12 +156,19 @@ class UDPRelay(object): if self._stat_callback: self._stat_callback(self._listen_port, len(data)) if self._is_local: - frag = common.ord(data[2]) - if frag != 0: - logging.warn('UDP drop a message since frag is not 0') - return + if self._is_tunnel: + # add ss header to data + tunnel_remote = self.tunnel_remote + tunnel_remote_port = self.tunnel_remote_port + data = common.add_header(tunnel_remote, + tunnel_remote_port, data) else: - data = data[3:] + frag = common.ord(data[2]) + if frag != 0: + logging.warn('UDP drop a message since frag is not 0') + return + else: + data = data[3:] else: data, key, iv = encrypt.decrypt_all(self._password, self._method, @@ -171,7 +183,8 @@ class UDPRelay(object): if header_result is None: return addrtype, dest_addr, dest_port, header_length = header_result - + logging.info("udp data to %s:%d from %s:%d" + % (dest_addr, dest_port, r_addr[0], r_addr[1])) if self._is_local: server_addr, server_port = self._get_a_server() else: @@ -267,9 +280,15 @@ class UDPRelay(object): if header_result is None: return addrtype, dest_addr, dest_port, header_length = header_result - response = b'\x00\x00\x00' + data + if self._is_tunnel: + # remove ss header + response = data[header_length:] + else: + response = b'\x00\x00\x00' + data client_addr = self._client_fd_to_server_addr.get(sock.fileno()) if client_addr: + logging.debug("send udp response to %s:%d" + % (client_addr[0], client_addr[1])) self._server_socket.sendto(response, client_addr) else: # this packet is from somewhere else we know