diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..36ddaa0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM stackbrew/debian:jessie +RUN apt-get update +RUN apt-get install -y python python-setuptools + +ADD . /shadowsocks + +WORKDIR /shadowsocks +RUN python setup.py install +CMD ssserver diff --git a/debian/config.json b/debian/config.json index 35cb14a..7846cb2 100644 --- a/debian/config.json +++ b/debian/config.json @@ -7,5 +7,6 @@ "timeout":300, "method":"aes-256-cfb", "fast_open": false, - "workers": 1 + "workers": 1, + "prefer_ipv6": false } \ No newline at end of file diff --git a/shadowsocks/asyncdns.py b/shadowsocks/asyncdns.py index acdac21..7e11d02 100644 --- a/shadowsocks/asyncdns.py +++ b/shadowsocks/asyncdns.py @@ -243,13 +243,13 @@ def __str__(self): return '%s: %s' % (self.hostname, str(self.answers)) -STATUS_IPV4 = 0 -STATUS_IPV6 = 1 +STATUS_FIRST = 0 +STATUS_SECOND = 1 class DNSResolver(object): - def __init__(self, server_list=None): + def __init__(self, server_list=None, prefer_ipv6=False): self._loop = None self._hosts = {} self._hostname_status = {} @@ -262,6 +262,10 @@ def __init__(self, server_list=None): self._parse_resolv() else: self._servers = server_list + if prefer_ipv6: + self._QTYPES = [QTYPE_AAAA, QTYPE_A] + else: + self._QTYPES = [QTYPE_A, QTYPE_AAAA] self._parse_hosts() # TODO monitor hosts change and reload hosts # TODO parse /etc/gai.conf and follow its rules @@ -342,17 +346,18 @@ def _handle_data(self, data): answer[2] == QCLASS_IN: ip = answer[0] break - if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \ - == STATUS_IPV4: - self._hostname_status[hostname] = STATUS_IPV6 - self._send_req(hostname, QTYPE_AAAA) + if not ip and self._hostname_status.get(hostname, STATUS_SECOND) \ + == STATUS_FIRST: + self._hostname_status[hostname] = STATUS_SECOND + self._send_req(hostname, self._QTYPES[1]) else: if ip: self._cache[hostname] = ip self._call_callback(hostname, ip) - elif self._hostname_status.get(hostname, None) == STATUS_IPV6: + elif self._hostname_status.get(hostname, None) \ + == STATUS_SECOND: for question in response.questions: - if question[1] == QTYPE_AAAA: + if question[1] == self._QTYPES[1]: self._call_callback(hostname, None) break @@ -418,14 +423,14 @@ def resolve(self, hostname, callback): return arr = self._hostname_to_cb.get(hostname, None) if not arr: - self._hostname_status[hostname] = STATUS_IPV4 - self._send_req(hostname, QTYPE_A) + self._hostname_status[hostname] = STATUS_FIRST + self._send_req(hostname, self._QTYPES[0]) self._hostname_to_cb[hostname] = [callback] self._cb_to_hostname[callback] = hostname else: arr.append(callback) # TODO send again only if waited too long - self._send_req(hostname, QTYPE_A) + self._send_req(hostname, self._QTYPES[0]) def close(self): if self._sock: diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 1be4c0f..4dc5621 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -58,9 +58,10 @@ def main(): udp_servers = [] if 'dns_server' in config: # allow override settings in resolv.conf - dns_resolver = asyncdns.DNSResolver(config['dns_server']) + dns_resolver = asyncdns.DNSResolver(config['dns_server'], + config['prefer_ipv6']) else: - dns_resolver = asyncdns.DNSResolver() + dns_resolver = asyncdns.DNSResolver(prefer_ipv6=config['prefer_ipv6']) port_password = config['port_password'] del config['port_password'] diff --git a/shadowsocks/shell.py b/shadowsocks/shell.py index 380bf25..6cca837 100644 --- a/shadowsocks/shell.py +++ b/shadowsocks/shell.py @@ -137,7 +137,8 @@ def get_config(is_local): else: shortopts = 'hd:s:p:k:m:c:t:vqa' longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', - 'forbidden-ip=', 'user=', 'manager-address=', 'version'] + 'forbidden-ip=', 'user=', 'manager-address=', 'version', + 'prefer-ipv6'] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -207,6 +208,8 @@ def get_config(is_local): elif key == '-q': v_count -= 1 config['verbose'] = v_count + elif key == '--prefer-ipv6': + config['prefer_ipv6'] = True except getopt.GetoptError as e: print(e, file=sys.stderr) print_help(is_local) @@ -229,6 +232,7 @@ def get_config(is_local): config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_port'] = config.get('local_port', 1080) config['one_time_auth'] = config.get('one_time_auth', False) + config['prefer_ipv6'] = config.get('prefer_ipv6', False) if is_local: if config.get('server', None) is None: logging.error('server addr not specified') @@ -290,6 +294,7 @@ def print_local_help(): -k PASSWORD password -m METHOD encryption method, default: aes-256-cfb -t TIMEOUT timeout in seconds, default: 300 + -a ONE_TIME_AUTH one time auth --fast-open use TCP_FASTOPEN, requires Linux 3.7+ General options: @@ -324,6 +329,7 @@ def print_server_help(): --workers WORKERS number of workers, available on Unix/Linux --forbidden-ip IPLIST comma seperated IP list forbidden to connect --manager-address ADDR optional server manager UDP address, see wiki + --prefer-ipv6 resolve ipv6 address first General options: -h, --help show this help message and exit diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 9595eff..fe17df2 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -37,6 +37,9 @@ MSG_FASTOPEN = 0x20000000 +# SOCKS METHOD definition +METHOD_NOAUTH = 0 + # SOCKS command definition CMD_CONNECT = 1 CMD_BIND = 2 @@ -53,7 +56,7 @@ # for each handler, it could be at one of several stages: # as sslocal: -# stage 0 SOCKS hello received from local, send hello to local +# stage 0 auth METHOD received from local, reply with selection message # stage 1 addr received from local, query DNS for remote # stage 2 UDP assoc # stage 3 DNS resolved, connect to remote @@ -93,6 +96,16 @@ BUF_SIZE = 32 * 1024 +# helper exceptions for TCPRelayHandler + +class BadSocksHeader(Exception): + pass + + +class NoAcceptableMethods(Exception): + pass + + class TCPRelayHandler(object): def __init__(self, server, fd_to_handlers, loop, local_sock, config, dns_resolver, is_local): @@ -320,6 +333,7 @@ def _handle_stage_addr(self, data): if self._is_local is False: # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._ota_enable or addrtype & ADDRTYPE_AUTH: + self._ota_enable = True if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('one time auth header is too short') return None @@ -330,6 +344,7 @@ def _handle_stage_addr(self, data): if onetimeauth_verify(_hash, _data, key) is False: logging.warn('one time auth fail') self.destroy() + return header_length += ONETIMEAUTH_BYTES self._remote_address = (common.to_str(remote_addr), remote_port) # pause reading @@ -486,6 +501,44 @@ def _handle_stage_stream(self, data): self._write_to_sock(data, self._remote_sock) return + def _check_auth_method(self, data): + # VER, NMETHODS, and at least 1 METHODS + if len(data) < 3: + logging.warning('method selection header too short') + raise BadSocksHeader + socks_version = common.ord(data[0]) + nmethods = common.ord(data[1]) + if socks_version != 5: + logging.warning('unsupported SOCKS protocol version ' + + str(socks_version)) + raise BadSocksHeader + if nmethods < 1 or len(data) != nmethods + 2: + logging.warning('NMETHODS and number of METHODS mismatch') + raise BadSocksHeader + noauth_exist = False + for method in data[2:]: + if common.ord(method) == METHOD_NOAUTH: + noauth_exist = True + break + if not noauth_exist: + logging.warning('none of SOCKS METHOD\'s ' + 'requested by client is supported') + raise NoAcceptableMethods + + def _handle_stage_init(self, data): + try: + self._check_auth_method(data) + except BadSocksHeader: + self.destroy() + return + except NoAcceptableMethods: + self._write_to_sock(b'\x05\xff', self._local_sock) + self.destroy() + return + + self._write_to_sock(b'\x05\00', self._local_sock) + self._stage = STAGE_ADDR + def _on_local_read(self): # handle all local read events and dispatch them to methods for # each stage @@ -516,10 +569,7 @@ def _on_local_read(self): self._handle_stage_stream(data) return elif is_local and self._stage == STAGE_INIT: - # TODO check auth method - self._write_to_sock(b'\x05\00', self._local_sock) - self._stage = STAGE_ADDR - return + self._handle_stage_init(data) elif self._stage == STAGE_CONNECTING: self._handle_stage_connecting(data) elif (is_local and self._stage == STAGE_ADDR) or \ diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index af7d8f6..85097e7 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -185,6 +185,7 @@ def _handle_server(self): server_addr, server_port = dest_addr, dest_port # spec https://shadowsocks.org/en/spec/one-time-auth.html if self._one_time_auth_enable or addrtype & ADDRTYPE_AUTH: + self._one_time_auth_enable = True if len(data) < header_length + ONETIMEAUTH_BYTES: logging.warn('UDP one time auth header is too short') return diff --git a/tests/socksify/install.sh b/tests/socksify/install.sh index 8eff72d..86c4e1d 100755 --- a/tests/socksify/install.sh +++ b/tests/socksify/install.sh @@ -1,8 +1,12 @@ #!/bin/bash -if [ ! -d dante-1.4.0 ]; then - wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 +if [ ! -d dante-1.4.0 ] || [ ! -d dante-1.4.0/configure ]; then + rm dante-1.4.0 -rf + #wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 + wget https://codeload.github.com/notpeter/dante/tar.gz/dante-1.4.0 -O dante-1.4.0.tar.gz || exit 1 tar xf dante-1.4.0.tar.gz || exit 1 + # + mv dante-dante-1.4.0 dante-1.4.0 fi pushd dante-1.4.0 ./configure && make -j4 && make install || exit 1