Compare commits

..

No commits in common. "master" and "2.6.10" have entirely different histories.

88 changed files with 835 additions and 5845 deletions

14
.gitignore vendored
View file

@ -29,17 +29,3 @@ htmlcov
.DS_Store .DS_Store
.idea .idea
tags
#Emacs
.#*
venv/
# VS-code
.vscode/
# Pycharm
.idea
#ss
config.json

View file

@ -10,14 +10,12 @@ cache:
before_install: before_install:
- sudo apt-get update -qq - sudo apt-get update -qq
- sudo apt-get install -qq build-essential dnsutils iproute nginx bc - sudo apt-get install -qq build-essential dnsutils iproute nginx bc
- sudo dd if=/dev/urandom of=/usr/share/nginx/html/file bs=1M count=10 - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10
- sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts"
- sudo service nginx restart - sudo service nginx restart
- pip install pep8 pyflakes nose coverage PySocks - pip install pep8 pyflakes nose coverage
- sudo tests/socksify/install.sh - sudo tests/socksify/install.sh
- sudo tests/libsodium/install.sh - sudo tests/libsodium/install.sh
- sudo tests/libmbedtls/install.sh
- sudo tests/libopenssl/install.sh
- sudo tests/setup_tc.sh - sudo tests/setup_tc.sh
script: script:
- tests/jenkins.sh - tests/jenkins.sh

15
CHANGES
View file

@ -1,18 +1,3 @@
2.8.2 2015-08-10
- Fix a encoding problem in manager
2.8.1 2015-08-06
- Respond ok to add and remove commands
2.8 2015-08-06
- Add Shadowsocks manager
2.7 2015-08-02
- Optimize speed for multiple ports
2.6.11 2015-07-10
- Fix a compatibility issue in UDP Relay
2.6.10 2015-06-08 2.6.10 2015-06-08
- Optimize LRU cache - Optimize LRU cache
- Refine logging - Refine logging

View file

@ -1,8 +1,6 @@
How to Contribute How to Contribute
================= =================
Notice this is the repository for Shadowsocks Python version. If you have problems with Android / iOS / Windows etc clients, please post your questions in their issue trackers.
Pull Requests Pull Requests
------------- -------------

View file

@ -1,17 +0,0 @@
FROM ubuntu:14.04
RUN apt-get update && apt-get install -y \
python-software-properties \
software-properties-common \
&& add-apt-repository ppa:chris-lea/libsodium \
&& echo "deb http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" >> /etc/apt/sources.list \
&& echo "deb-src http://ppa.launchpad.net/chris-lea/libsodium/ubuntu trusty main" >> /etc/apt/sources.list \
&& apt-get update \
&& apt-get install -y libsodium-dev python-pip
RUN pip install shadowsocks
ENTRYPOINT ["/usr/local/bin/ssserver"]
# usage:
# docker run -d --restart=always -p 1314:1314 ficapy/shadowsocks -s 0.0.0.0 -p 1314 -k $PD -m chacha20

View file

@ -3,16 +3,10 @@ shadowsocks
[![PyPI version]][PyPI] [![PyPI version]][PyPI]
[![Build Status]][Travis CI] [![Build Status]][Travis CI]
[![Coverage Status]][Coverage]
A fast tunnel proxy that helps you bypass firewalls. A fast tunnel proxy that helps you bypass firewalls.
Features:
- TCP & UDP support
- User management API
- TCP Fast Open
- Workers and graceful restart
- Destination IP blacklist
Server Server
------ ------
@ -21,25 +15,16 @@ Server
Debian / Ubuntu: Debian / Ubuntu:
apt-get install python-pip apt-get install python-pip
pip install git+https://github.com/shadowsocks/shadowsocks.git@master pip install shadowsocks
CentOS: CentOS:
yum install python-setuptools && easy_install pip yum install python-setuptools && easy_install pip
pip install git+https://github.com/shadowsocks/shadowsocks.git@master pip install shadowsocks
For CentOS 7, if you need AEAD ciphers, you need install libsodium
```
dnf install libsodium python34-pip
pip3 install git+https://github.com/shadowsocks/shadowsocks.git@master
```
Linux distributions with [snap](http://snapcraft.io/):
snap install shadowsocks
Windows: Windows:
See [Install Shadowsocks Server on Windows](https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows). See [Install Server on Windows]
### Usage ### Usage
@ -60,38 +45,62 @@ To check the log:
Check all the options via `-h`. You can also use a [Configuration] file Check all the options via `-h`. You can also use a [Configuration] file
instead. instead.
If you installed the [snap](http://snapcraft.io/) package, you have to prefix the commands with `shadowsocks.`, Client
like this: ------
shadowsocks.ssserver -p 443 -k password -m aes-256-cfb * [Windows] / [OS X]
* [Android] / [iOS]
### Usage with Config File * [OpenWRT]
[Create configuration file and run](https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File)
To start:
ssserver -c /etc/shadowsocks.json
Use GUI clients on your local PC/phones. Check the README of your client
for more information.
Documentation Documentation
------------- -------------
You can find all the documentation in the [Wiki](https://github.com/shadowsocks/shadowsocks/wiki). You can find all the documentation in the [Wiki].
License License
------- -------
Apache License Copyright 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.
Bugs and Issues
----------------
* [Troubleshooting]
* [Issue Tracker]
* [Mailing list]
[Android]: https://github.com/shadowsocks/shadowsocks-android
[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat [Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat
[Configuration]: https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File
[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks
[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html
[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks
[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help
[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open
[Install Server on Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows
[Mailing list]: https://groups.google.com/group/shadowsocks
[OpenWRT]: https://github.com/shadowsocks/openwrt-shadowsocks
[OS X]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help
[PyPI]: https://pypi.python.org/pypi/shadowsocks [PyPI]: https://pypi.python.org/pypi/shadowsocks
[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat [PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat
[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks [Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks
[Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting
[Wiki]: https://github.com/shadowsocks/shadowsocks/wiki
[Windows]: https://github.com/shadowsocks/shadowsocks-csharp

View file

@ -1,8 +1,3 @@
About shadowsocks-rm
---------------
This project is https://github.com/shadowsocks/shadowsocks clone. I JUST fix bug on the original code. Unless it is necessary to have additional features.
shadowsocks shadowsocks
=========== ===========

View file

@ -1,17 +0,0 @@
{
"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",
"dns_server":["8.8.8.8", "8.8.4.4"],
"tunnel_remote_port":53,
"tunnel_port":53,
"libopenssl":"C:\\Program Files\\Git\\mingw64\\bin\\libeay32.dll",
"libsodium":"/usr/local/lib/libsodium.so",
"libmbedtls":"/usr/local/lib/libmbedcrypto.2.4.0.dylib"
}

23
debian/changelog vendored
View file

@ -1,26 +1,3 @@
shadowsocks (2.9.0-2) unstable; urgency=medium
[ Shell.Xu ]
* Fix compatible issue (Closes: #845016)
-- Shell.Xu <shell909090@gmail.com> Sun, 20 Nov 2016 14:33:31 +0800
shadowsocks (2.9.0-1) unstable; urgency=medium
[ Shell Xu ]
* Upstream update (Closes: #824640)
* Remove reference not exists (Closes: #810688)
[ Thomas Goirand ]
* Added lsb-base as Depends:.
* Removed Pre-Depends: dpkg (>= 1.15.6~).
* Ran wrap-and-sort -t -a.
* Fixed VCS URLs to use https.
* Removed useless obsolete version of python-all build-depends.
* Fixed debian/copyright ordering.
-- Shell.Xu <shell909090@gmail.com> Sat, 01 Oct 2016 16:14:47 +0800
shadowsocks (2.1.0-1) unstable; urgency=low shadowsocks (2.1.0-1) unstable; urgency=low
* Initial release (Closes: #758900) * Initial release (Closes: #758900)

3
debian/config.json vendored
View file

@ -7,6 +7,5 @@
"timeout":300, "timeout":300,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"fast_open": false, "fast_open": false,
"workers": 1, "workers": 1
"prefer_ipv6": false
} }

21
debian/control vendored
View file

@ -2,23 +2,18 @@ Source: shadowsocks
Section: python Section: python
Priority: extra Priority: extra
Maintainer: Shell.Xu <shell909090@gmail.com> Maintainer: Shell.Xu <shell909090@gmail.com>
Build-Depends: debhelper (>= 8), Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools
python-all, Standards-Version: 3.9.5
python-setuptools, Homepage: https://github.com/clowwindy/shadowsocks
Standards-Version: 3.9.8 Vcs-Git: git://github.com/shell909090/shadowsocks.git
Homepage: https://github.com/shadowsocks/shadowsocks Vcs-Browser: http://github.com/shell909090/shadowsocks
Vcs-Git: https://github.com/shell909090/shadowsocks.git
Vcs-Browser: https://github.com/shell909090/shadowsocks
Package: shadowsocks Package: shadowsocks
Architecture: all Architecture: all
Depends: lsb-base (>= 3.0-6), Pre-Depends: dpkg (>= 1.15.6~)
python-m2crypto, Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto
python-pkg-resources,
${misc:Depends},
${python:Depends},
Description: Fast tunnel proxy that helps you bypass firewalls Description: Fast tunnel proxy that helps you bypass firewalls
A secure socks5 proxy, designed to protect your Internet traffic. A secure socks5 proxy, designed to protect your Internet traffic.
. .
This package contain local and server part of shadowsocks, a fast, This package contain local and server part of shadowsocks, a fast,
powerful tunnel proxy to bypass firewalls. powerful tunnel proxy to bypass firewalls.

43
debian/copyright vendored
View file

@ -1,27 +1,30 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: shadowsocks Upstream-Name: shadowsocks
Source: https://github.com/shadowsocks/shadowsocks Source: https://github.com/clowwindy/shadowsocks
Files: debian/*
Copyright: 2014 Shell.Xu <shell909090@gmail.com>
License: Expat
Files: * Files: *
Copyright: 2014 clowwindy <clowwindy42@gmail.com> Copyright: 2014 clowwindy <clowwindy42@gmail.com>
License: Apache-2.0 License: Expat
Files: debian/* License: Expat
Copyright: 2016 Shell.Xu <shell909090@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy
License: Apache-2.0 of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
License: Apache-2.0 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
Licensed under the Apache License, Version 2.0 (the "License"); copies of the Software, and to permit persons to whom the Software is
you may not use this file except in compliance with the License. furnished to do so, subject to the following conditions:
You may obtain a copy of the License at .
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
. .
http://www.apache.org/licenses/LICENSE-2.0 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
Unless required by applicable law or agreed to in writing, software FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
distributed under the License is distributed on an "AS IS" BASIS, AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
See the License for the specific language governing permissions and OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
limitations under the License. SOFTWARE.
.
On Debian systems, the complete text of the Apache License 2.0 can
be found in "/usr/share/common-licenses/Apache-2.0"

2
debian/install vendored
View file

@ -1 +1 @@
debian/config.json etc/shadowsocks/ debian/config.json etc/shadowsocks/

View file

@ -1,2 +1,2 @@
debian/sslocal.1 debian/sslocal.1
debian/ssserver.1 debian/ssserver.1

4
debian/sslocal.1 vendored
View file

@ -55,5 +55,5 @@ Quiet mode, only show warnings/errors.
The programs are documented fully by The programs are documented fully by
.IR "Shell Xu <shell909090@gmail.com>" .IR "Shell Xu <shell909090@gmail.com>"
and and
.IR "Clowwindy <clowwindy42@gmail.com>" .IR "Clowwindy <clowwindy42@gmail.com>",
. available via the Info system.

4
debian/ssserver.1 vendored
View file

@ -55,5 +55,5 @@ Quiet mode, only show warnings/errors.
The programs are documented fully by The programs are documented fully by
.IR "Shell Xu <shell909090@gmail.com>" .IR "Shell Xu <shell909090@gmail.com>"
and and
.IR "Clowwindy <clowwindy42@gmail.com>" .IR "Clowwindy <clowwindy42@gmail.com>",
. available via the Info system.

View file

@ -7,7 +7,7 @@ with codecs.open('README.rst', encoding='utf-8') as f:
setup( setup(
name="shadowsocks", name="shadowsocks",
version="3.0.0", version="2.6.10",
license='http://www.apache.org/licenses/LICENSE-2.0', license='http://www.apache.org/licenses/LICENSE-2.0',
description="A fast tunnel proxy that help you get through firewalls", description="A fast tunnel proxy that help you get through firewalls",
author='clowwindy', author='clowwindy',

View file

@ -18,6 +18,7 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
with_statement with_statement
import time
import os import os
import socket import socket
import struct import struct
@ -29,7 +30,7 @@ from shadowsocks import common, lru_cache, eventloop, shell
CACHE_SWEEP_INTERVAL = 30 CACHE_SWEEP_INTERVAL = 30
VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d\-_]{1,63}(?<!-)$", re.IGNORECASE) VALID_HOSTNAME = re.compile(br"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
common.patch_socket() common.patch_socket()
@ -242,29 +243,23 @@ class DNSResponse(object):
return '%s: %s' % (self.hostname, str(self.answers)) return '%s: %s' % (self.hostname, str(self.answers))
STATUS_FIRST = 0 STATUS_IPV4 = 0
STATUS_SECOND = 1 STATUS_IPV6 = 1
class DNSResolver(object): class DNSResolver(object):
def __init__(self, server_list=None, prefer_ipv6=False): def __init__(self):
self._loop = None self._loop = None
self._hosts = {} self._hosts = {}
self._hostname_status = {} self._hostname_status = {}
self._hostname_to_cb = {} self._hostname_to_cb = {}
self._cb_to_hostname = {} self._cb_to_hostname = {}
self._cache = lru_cache.LRUCache(timeout=300) self._cache = lru_cache.LRUCache(timeout=300)
self._last_time = time.time()
self._sock = None self._sock = None
if server_list is None: self._servers = None
self._servers = None self._parse_resolv()
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() self._parse_hosts()
# TODO monitor hosts change and reload hosts # TODO monitor hosts change and reload hosts
# TODO parse /etc/gai.conf and follow its rules # TODO parse /etc/gai.conf and follow its rules
@ -276,18 +271,15 @@ class DNSResolver(object):
content = f.readlines() content = f.readlines()
for line in content: for line in content:
line = line.strip() line = line.strip()
if not (line and line.startswith(b'nameserver')): if line:
continue if line.startswith(b'nameserver'):
parts = line.split()
parts = line.split() if len(parts) >= 2:
if len(parts) < 2: server = parts[1]
continue if common.is_ip(server) == socket.AF_INET:
if type(server) != str:
server = parts[1] server = server.decode('utf8')
if common.is_ip(server) == socket.AF_INET: self._servers.append(server)
if type(server) != str:
server = server.decode('utf8')
self._servers.append(server)
except IOError: except IOError:
pass pass
if not self._servers: if not self._servers:
@ -302,21 +294,17 @@ class DNSResolver(object):
for line in f.readlines(): for line in f.readlines():
line = line.strip() line = line.strip()
parts = line.split() parts = line.split()
if len(parts) < 2: if len(parts) >= 2:
continue ip = parts[0]
if common.is_ip(ip):
ip = parts[0] for i in range(1, len(parts)):
if not common.is_ip(ip): hostname = parts[i]
continue if hostname:
self._hosts[hostname] = ip
for i in range(1, len(parts)):
hostname = parts[i]
if hostname:
self._hosts[hostname] = ip
except IOError: except IOError:
self._hosts['localhost'] = '127.0.0.1' self._hosts['localhost'] = '127.0.0.1'
def add_to_loop(self, loop): def add_to_loop(self, loop, ref=False):
if self._loop: if self._loop:
raise Exception('already add to loop') raise Exception('already add to loop')
self._loop = loop self._loop = loop
@ -324,8 +312,8 @@ class DNSResolver(object):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
socket.SOL_UDP) socket.SOL_UDP)
self._sock.setblocking(False) self._sock.setblocking(False)
loop.add(self._sock, eventloop.POLL_IN, self) loop.add(self._sock, eventloop.POLL_IN)
loop.add_periodic(self.handle_periodic) loop.add_handler(self.handle_events, ref=ref)
def _call_callback(self, hostname, ip, error=None): def _call_callback(self, hostname, ip, error=None):
callbacks = self._hostname_to_cb.get(hostname, []) callbacks = self._hostname_to_cb.get(hostname, [])
@ -352,42 +340,44 @@ class DNSResolver(object):
answer[2] == QCLASS_IN: answer[2] == QCLASS_IN:
ip = answer[0] ip = answer[0]
break break
if not ip and self._hostname_status.get(hostname, STATUS_SECOND) \ if not ip and self._hostname_status.get(hostname, STATUS_IPV6) \
== STATUS_FIRST: == STATUS_IPV4:
self._hostname_status[hostname] = STATUS_SECOND self._hostname_status[hostname] = STATUS_IPV6
self._send_req(hostname, self._QTYPES[1]) self._send_req(hostname, QTYPE_AAAA)
else: else:
if ip: if ip:
self._cache[hostname] = ip self._cache[hostname] = ip
self._call_callback(hostname, ip) self._call_callback(hostname, ip)
elif self._hostname_status.get(hostname, None) \ elif self._hostname_status.get(hostname, None) == STATUS_IPV6:
== STATUS_SECOND:
for question in response.questions: for question in response.questions:
if question[1] == self._QTYPES[1]: if question[1] == QTYPE_AAAA:
self._call_callback(hostname, None) self._call_callback(hostname, None)
break break
def handle_event(self, sock, fd, event): def handle_events(self, events):
if sock != self._sock: for sock, fd, event in events:
return if sock != self._sock:
if event & eventloop.POLL_ERR: continue
logging.error('dns socket err') if event & eventloop.POLL_ERR:
self._loop.remove(self._sock) logging.error('dns socket err')
self._sock.close() self._loop.remove(self._sock)
# TODO when dns server is IPv6 self._sock.close()
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, # TODO when dns server is IPv6
socket.SOL_UDP) self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
self._sock.setblocking(False) socket.SOL_UDP)
self._loop.add(self._sock, eventloop.POLL_IN, self) self._sock.setblocking(False)
else: self._loop.add(self._sock, eventloop.POLL_IN)
data, addr = sock.recvfrom(1024) else:
if addr[0] not in self._servers: data, addr = sock.recvfrom(1024)
logging.warn('received a packet other than our dns') if addr[0] not in self._servers:
return logging.warn('received a packet other than our dns')
self._handle_data(data) break
self._handle_data(data)
def handle_periodic(self): break
self._cache.sweep() now = time.time()
if now - self._last_time > CACHE_SWEEP_INTERVAL:
self._cache.sweep()
self._last_time = now
def remove_callback(self, callback): def remove_callback(self, callback):
hostname = self._cb_to_hostname.get(callback) hostname = self._cb_to_hostname.get(callback)
@ -429,20 +419,17 @@ class DNSResolver(object):
return return
arr = self._hostname_to_cb.get(hostname, None) arr = self._hostname_to_cb.get(hostname, None)
if not arr: if not arr:
self._hostname_status[hostname] = STATUS_FIRST self._hostname_status[hostname] = STATUS_IPV4
self._send_req(hostname, self._QTYPES[0]) self._send_req(hostname, QTYPE_A)
self._hostname_to_cb[hostname] = [callback] self._hostname_to_cb[hostname] = [callback]
self._cb_to_hostname[callback] = hostname self._cb_to_hostname[callback] = hostname
else: else:
arr.append(callback) arr.append(callback)
# TODO send again only if waited too long # TODO send again only if waited too long
self._send_req(hostname, self._QTYPES[0]) self._send_req(hostname, QTYPE_A)
def close(self): def close(self):
if self._sock: if self._sock:
if self._loop:
self._loop.remove_periodic(self.handle_periodic)
self._loop.remove(self._sock)
self._sock.close() self._sock.close()
self._sock = None self._sock = None
@ -450,7 +437,7 @@ class DNSResolver(object):
def test(): def test():
dns_resolver = DNSResolver() dns_resolver = DNSResolver()
loop = eventloop.EventLoop() loop = eventloop.EventLoop()
dns_resolver.add_to_loop(loop) dns_resolver.add_to_loop(loop, ref=True)
global counter global counter
counter = 0 counter = 0
@ -464,8 +451,8 @@ def test():
print(result, error) print(result, error)
counter += 1 counter += 1
if counter == 9: if counter == 9:
loop.remove_handler(dns_resolver.handle_events)
dns_resolver.close() dns_resolver.close()
loop.stop()
a_callback = callback a_callback = callback
return a_callback return a_callback

View file

@ -21,25 +21,6 @@ from __future__ import absolute_import, division, print_function, \
import socket import socket
import struct import struct
import logging import logging
import hashlib
import hmac
ONETIMEAUTH_BYTES = 10
ONETIMEAUTH_CHUNK_BYTES = 12
ONETIMEAUTH_CHUNK_DATA_LEN = 2
def sha1_hmac(secret, data):
return hmac.new(secret, data, hashlib.sha1).digest()
def onetimeauth_verify(_hash, data, key):
return _hash == sha1_hmac(key, data)[:ONETIMEAUTH_BYTES]
def onetimeauth_gen(data, key):
return sha1_hmac(key, data)[:ONETIMEAUTH_BYTES]
def compat_ord(s): def compat_ord(s):
@ -137,16 +118,13 @@ def patch_socket():
patch_socket() patch_socket()
ADDRTYPE_IPV4 = 0x01 ADDRTYPE_IPV4 = 1
ADDRTYPE_IPV6 = 0x04 ADDRTYPE_IPV6 = 4
ADDRTYPE_HOST = 0x03 ADDRTYPE_HOST = 3
ADDRTYPE_AUTH = 0x10
ADDRTYPE_MASK = 0xF
def pack_addr(address): def pack_addr(address):
address_str = to_str(address) address_str = to_str(address)
address = to_bytes(address)
for family in (socket.AF_INET, socket.AF_INET6): for family in (socket.AF_INET, socket.AF_INET6):
try: try:
r = socket.inet_pton(family, address_str) r = socket.inet_pton(family, address_str)
@ -161,38 +139,31 @@ def pack_addr(address):
return b'\x03' + chr(len(address)) + 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): def parse_header(data):
addrtype = ord(data[0]) addrtype = ord(data[0])
dest_addr = None dest_addr = None
dest_port = None dest_port = None
header_length = 0 header_length = 0
if addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV4: if addrtype == ADDRTYPE_IPV4:
if len(data) >= 7: if len(data) >= 7:
dest_addr = socket.inet_ntoa(data[1:5]) dest_addr = socket.inet_ntoa(data[1:5])
dest_port = struct.unpack('>H', data[5:7])[0] dest_port = struct.unpack('>H', data[5:7])[0]
header_length = 7 header_length = 7
else: else:
logging.warn('header is too short') logging.warn('header is too short')
elif addrtype & ADDRTYPE_MASK == ADDRTYPE_HOST: elif addrtype == ADDRTYPE_HOST:
if len(data) > 2: if len(data) > 2:
addrlen = ord(data[1]) addrlen = ord(data[1])
if len(data) >= 4 + addrlen: if len(data) >= 2 + addrlen:
dest_addr = data[2:2 + addrlen] dest_addr = data[2:2 + addrlen]
dest_port = struct.unpack('>H', data[2 + addrlen:4 + dest_port = struct.unpack('>H', data[2 + addrlen:4 +
addrlen])[0] addrlen])[0]
header_length = 4 + addrlen header_length = 4 + addrlen
else: else:
logging.warn('header is too short') logging.warn('header is too short')
else: else:
logging.warn('header is too short') logging.warn('header is too short')
elif addrtype & ADDRTYPE_MASK == ADDRTYPE_IPV6: elif addrtype == ADDRTYPE_IPV6:
if len(data) >= 19: if len(data) >= 19:
dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17]) dest_addr = socket.inet_ntop(socket.AF_INET6, data[1:17])
dest_port = struct.unpack('>H', data[17:19])[0] dest_port = struct.unpack('>H', data[17:19])[0]

View file

@ -1,340 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Void Copyright NO ONE
#
# Void License
#
# The code belongs to no one. Do whatever you want.
# Forget about boring open source license.
#
# AEAD cipher for shadowsocks
#
from __future__ import absolute_import, division, print_function, \
with_statement
from ctypes import c_int, create_string_buffer, byref, c_void_p
import hashlib
from struct import pack, unpack
from shadowsocks.crypto import util
from shadowsocks.crypto import hkdf
from shadowsocks.common import ord, chr
EVP_CTRL_GCM_SET_IVLEN = 0x9
EVP_CTRL_GCM_GET_TAG = 0x10
EVP_CTRL_GCM_SET_TAG = 0x11
EVP_CTRL_CCM_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN
EVP_CTRL_CCM_GET_TAG = EVP_CTRL_GCM_GET_TAG
EVP_CTRL_CCM_SET_TAG = EVP_CTRL_GCM_SET_TAG
EVP_CTRL_AEAD_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN
EVP_CTRL_AEAD_SET_TAG = EVP_CTRL_GCM_SET_TAG
EVP_CTRL_AEAD_GET_TAG = EVP_CTRL_GCM_GET_TAG
AEAD_MSG_LEN_UNKNOWN = 0
AEAD_CHUNK_SIZE_LEN = 2
AEAD_CHUNK_SIZE_MASK = 0x3FFF
CIPHER_NONCE_LEN = {
'aes-128-gcm': 12,
'aes-192-gcm': 12,
'aes-256-gcm': 12,
'aes-128-ocb': 12, # requires openssl 1.1
'aes-192-ocb': 12,
'aes-256-ocb': 12,
'chacha20-poly1305': 12,
'chacha20-ietf-poly1305': 12,
'xchacha20-ietf-poly1305': 24,
'sodium:aes-256-gcm': 12,
}
CIPHER_TAG_LEN = {
'aes-128-gcm': 16,
'aes-192-gcm': 16,
'aes-256-gcm': 16,
'aes-128-ocb': 16, # requires openssl 1.1
'aes-192-ocb': 16,
'aes-256-ocb': 16,
'chacha20-poly1305': 16,
'chacha20-ietf-poly1305': 16,
'xchacha20-ietf-poly1305': 16,
'sodium:aes-256-gcm': 16,
}
SUBKEY_INFO = b"ss-subkey"
libsodium = None
sodium_loaded = False
def load_sodium(path=None):
"""
Load libsodium helpers for nonce increment
:return: None
"""
global libsodium, sodium_loaded
libsodium = util.find_library('sodium', 'sodium_increment',
'libsodium', path)
if libsodium is None:
print('load libsodium failed with path %s' % path)
return
if libsodium.sodium_init() < 0:
libsodium = None
print('sodium init failed')
return
libsodium.sodium_increment.restype = c_void_p
libsodium.sodium_increment.argtypes = (
c_void_p, c_int
)
sodium_loaded = True
return
def nonce_increment(nonce, nlen):
"""
Increase nonce by 1 in little endian
From libsodium sodium_increment():
for (; i < nlen; i++) {
c += (uint_fast16_t) n[i];
n[i] = (unsigned char) c;
c >>= 8;
}
:param nonce: string_buffer nonce
:param nlen: nonce length
:return: nonce plus by 1
"""
c = 1
i = 0
# n = create_string_buffer(nlen)
while i < nlen:
c += ord(nonce[i])
nonce[i] = chr(c & 0xFF)
c >>= 8
i += 1
return # n.raw
class AeadCryptoBase(object):
"""
Handles basic aead process of shadowsocks protocol
TCP Chunk (after encryption, *ciphertext*)
+--------------+---------------+--------------+------------+
| *DataLen* | DataLen_TAG | *Data* | Data_TAG |
+--------------+---------------+--------------+------------+
| 2 | Fixed | Variable | Fixed |
+--------------+---------------+--------------+------------+
UDP (after encryption, *ciphertext*)
+--------+-----------+-----------+
| NONCE | *Data* | Data_TAG |
+-------+-----------+-----------+
| Fixed | Variable | Fixed |
+--------+-----------+-----------+
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
self._op = int(op)
self._salt = iv
self._nlen = CIPHER_NONCE_LEN[cipher_name]
self._nonce = create_string_buffer(self._nlen)
self._tlen = CIPHER_TAG_LEN[cipher_name]
crypto_hkdf = hkdf.Hkdf(iv, key, algorithm=hashlib.sha1)
self._skey = crypto_hkdf.expand(info=SUBKEY_INFO, length=len(key))
# _chunk['mlen']:
# -1, waiting data len header
# n, n > 0, waiting data
self._chunk = {'mlen': AEAD_MSG_LEN_UNKNOWN, 'data': b''}
# load libsodium for nonce increment
if not sodium_loaded:
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('sodium', None)
load_sodium(path)
def nonce_increment(self):
"""
AEAD ciphers need nonce to be unique per key
TODO: cache and check unique
:return: None
"""
global libsodium, sodium_loaded
if sodium_loaded:
libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen))
else:
nonce_increment(self._nonce, self._nlen)
# print("".join("%02x" % ord(b) for b in self._nonce))
def cipher_ctx_init(self):
"""
Increase nonce to make it unique for the same key
:return: None
"""
self.nonce_increment()
def aead_encrypt(self, data):
"""
Encrypt data with authenticate tag
:param data: plain text
:return: str [payload][tag] cipher text with tag
"""
raise Exception("Must implement aead_encrypt method")
def encrypt_chunk(self, data):
"""
Encrypt a chunk for TCP chunks
:param data: str
:return: str [len][tag][payload][tag]
"""
plen = len(data)
# l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2
# network byte order
ctext = [self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))]
if len(ctext[0]) != AEAD_CHUNK_SIZE_LEN + self._tlen:
self.clean()
raise Exception("size length invalid")
ctext.append(self.aead_encrypt(data))
if len(ctext[1]) != plen + self._tlen:
self.clean()
raise Exception("data length invalid")
return b''.join(ctext)
def encrypt(self, data):
"""
Encrypt data, for TCP divided into chunks
For UDP data, call aead_encrypt instead
:param data: str data bytes
:return: str encrypted data
"""
plen = len(data)
if plen <= AEAD_CHUNK_SIZE_MASK:
ctext = self.encrypt_chunk(data)
return ctext
ctext = []
while plen > 0:
mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \
else AEAD_CHUNK_SIZE_MASK
c = self.encrypt_chunk(data[:mlen])
ctext.append(c)
data = data[mlen:]
plen -= mlen
return b''.join(ctext)
def aead_decrypt(self, data):
"""
Decrypt data and authenticate tag
:param data: str [len][tag][payload][tag] cipher text with tag
:return: str plain text
"""
raise Exception("Must implement aead_decrypt method")
def decrypt_chunk_size(self, data):
"""
Decrypt chunk size
:param data: str [size][tag] encrypted chunk payload len
:return: (int, str) msg length and remaining encrypted data
"""
if self._chunk['mlen'] > 0:
return self._chunk['mlen'], data
data = self._chunk['data'] + data
self._chunk['data'] = b""
hlen = AEAD_CHUNK_SIZE_LEN + self._tlen
if hlen > len(data):
self._chunk['data'] = data
return 0, b""
plen = self.aead_decrypt(data[:hlen])
plen, = unpack("!H", plen)
if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0:
self.clean()
raise Exception('Invalid message length')
return plen, data[hlen:]
def decrypt_chunk_payload(self, plen, data):
"""
Decrypted encrypted msg payload
:param plen: int payload length
:param data: str [payload][tag][[len][tag]....] encrypted data
:return: (str, str) plain text and remaining encrypted data
"""
data = self._chunk['data'] + data
if len(data) < plen + self._tlen:
self._chunk['mlen'] = plen
self._chunk['data'] = data
return b"", b""
self._chunk['mlen'] = AEAD_MSG_LEN_UNKNOWN
self._chunk['data'] = b""
plaintext = self.aead_decrypt(data[:plen + self._tlen])
if len(plaintext) != plen:
self.clean()
raise Exception("plaintext length invalid")
return plaintext, data[plen + self._tlen:]
def decrypt_chunk(self, data):
"""
Decrypt a TCP chunk
:param data: str [len][tag][payload][tag][[len][tag]...] encrypted msg
:return: (str, str) decrypted msg and remaining encrypted data
"""
plen, data = self.decrypt_chunk_size(data)
if plen <= 0:
return b"", b""
return self.decrypt_chunk_payload(plen, data)
def decrypt(self, data):
"""
Decrypt data for TCP data divided into chunks
For UDP data, call aead_decrypt instead
:param data: str
:return: str
"""
ptext = []
pnext, left = self.decrypt_chunk(data)
ptext.append(pnext)
while len(left) > 0:
pnext, left = self.decrypt_chunk(left)
ptext.append(pnext)
return b''.join(ptext)
def test_nonce_increment():
buf = create_string_buffer(12)
print("".join("%02x" % ord(b) for b in buf))
nonce_increment(buf, 12)
nonce_increment(buf, 12)
nonce_increment(buf, 12)
nonce_increment(buf, 12)
print("".join("%02x" % ord(b) for b in buf))
for i in range(256):
nonce_increment(buf, 12)
print("".join("%02x" % ord(b) for b in buf))
if __name__ == '__main__':
load_sodium()
test_nonce_increment()

View file

@ -1,98 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Void Copyright NO ONE
#
# Void License
#
# The code belongs to no one. Do whatever you want.
# Forget about boring open source license.
#
# HKDF for AEAD ciphers
#
from __future__ import division
import hmac
import hashlib
import sys
if sys.version_info[0] == 3:
def buffer(x):
return x
def hkdf_extract(salt, input_key_material, algorithm=hashlib.sha256):
"""
Extract a pseudorandom key suitable for use with hkdf_expand
from the input_key_material and a salt using HMAC with the
provided hash (default SHA-256).
salt should be a random, application-specific byte string. If
salt is None or the empty string, an all-zeros string of the same
length as the hash's block size will be used instead per the RFC.
See the HKDF draft RFC and paper for usage notes.
"""
hash_len = algorithm().digest_size
if salt is None or len(salt) == 0:
salt = bytearray((0,) * hash_len)
return hmac.new(bytes(salt), buffer(input_key_material), algorithm)\
.digest()
def hkdf_expand(pseudo_random_key, info=b"", length=32,
algorithm=hashlib.sha256):
"""
Expand `pseudo_random_key` and `info` into a key of length `bytes` using
HKDF's expand function based on HMAC with the provided hash (default
SHA-256). See the HKDF draft RFC and paper for usage notes.
"""
hash_len = algorithm().digest_size
length = int(length)
if length > 255 * hash_len:
raise Exception("Cannot expand to more than 255 * %d = %d "
"bytes using the specified hash function" %
(hash_len, 255 * hash_len))
blocks_needed = length // hash_len \
+ (0 if length % hash_len == 0 else 1) # ceil
okm = b""
output_block = b""
for counter in range(blocks_needed):
output_block = hmac.new(
pseudo_random_key,
buffer(output_block + info + bytearray((counter + 1,))),
algorithm
).digest()
okm += output_block
return okm[:length]
class Hkdf(object):
"""
Wrapper class for HKDF extract and expand functions
"""
def __init__(self, salt, input_key_material, algorithm=hashlib.sha256):
"""
Extract a pseudorandom key from `salt` and `input_key_material`
arguments.
See the HKDF draft RFC for guidance on setting these values.
The constructor optionally takes a `algorithm` argument defining
the hash function use, defaulting to hashlib.sha256.
"""
self._hash = algorithm
self._prk = hkdf_extract(salt, input_key_material, self._hash)
def expand(self, info, length=32):
"""
Generate output key material based on an `info` value
Arguments:
- info - context to generate the OKM
- length - length in bytes of the key to generate
See the HKDF draft RFC for guidance.
"""
return hkdf_expand(self._prk, info, length, self._hash)

View file

@ -1,478 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Void Copyright NO ONE
#
# Void License
#
# The code belongs to no one. Do whatever you want.
# Forget about boring open source license.
#
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
from __future__ import absolute_import, division, print_function, \
with_statement
from ctypes import c_char_p, c_int, c_size_t, byref,\
create_string_buffer, c_void_p
from shadowsocks import common
from shadowsocks.crypto import util
from shadowsocks.crypto.aead import AeadCryptoBase
__all__ = ['ciphers']
libmbedtls = None
loaded = False
buf = None
buf_size = 2048
CIPHER_ENC_UNCHANGED = -1
# define MAX_KEY_LENGTH 64
# define MAX_NONCE_LENGTH 32
# typedef struct {
# uint32_t init;
# uint64_t counter;
# cipher_evp_t *evp;
# cipher_t *cipher;
# buffer_t *chunk;
# uint8_t salt[MAX_KEY_LENGTH];
# uint8_t skey[MAX_KEY_LENGTH];
# uint8_t nonce[MAX_NONCE_LENGTH];
# } cipher_ctx_t;
#
# sizeof(cipher_ctx_t) = 196
CIPHER_CTX_SIZE = 256
def load_mbedtls(crypto_path=None):
global loaded, libmbedtls, buf
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('mbedtls', None)
libmbedtls = util.find_library('mbedcrypto',
'mbedtls_cipher_init',
'libmbedcrypto', path)
if libmbedtls is None:
raise Exception('libmbedcrypto(mbedtls) not found with path %s'
% path)
libmbedtls.mbedtls_cipher_init.restype = None
libmbedtls.mbedtls_cipher_free.restype = None
libmbedtls.mbedtls_cipher_info_from_string.restype = c_void_p
libmbedtls.mbedtls_cipher_info_from_string.argtypes = (c_char_p,)
libmbedtls.mbedtls_cipher_setup.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_setup.argtypes = (c_void_p, c_void_p)
libmbedtls.mbedtls_cipher_setkey.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_setkey.argtypes = (
c_void_p, # ctx
c_char_p, # key
c_int, # key_bitlen, not bytes
c_int # op: 1 enc, 0 dec, -1 none
)
libmbedtls.mbedtls_cipher_set_iv.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_set_iv.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t # iv_len
)
libmbedtls.mbedtls_cipher_reset.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_reset.argtypes = (c_void_p,) # ctx
if hasattr(libmbedtls, 'mbedtls_cipher_update_ad'):
libmbedtls.mbedtls_cipher_update_ad.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_update_ad.argtypes = (
c_void_p, # ctx
c_char_p, # ad
c_size_t # ad_len
)
libmbedtls.mbedtls_cipher_update.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_update.argtypes = (
c_void_p, # ctx
c_char_p, # input
c_size_t, # ilen, must be multiple of block size except last one
c_void_p, # *output
c_void_p # *olen
)
libmbedtls.mbedtls_cipher_finish.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_finish.argtypes = (
c_void_p, # ctx
c_void_p, # *output
c_void_p # *olen
)
if hasattr(libmbedtls, 'mbedtls_cipher_write_tag'):
libmbedtls.mbedtls_cipher_write_tag.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_write_tag.argtypes = (
c_void_p, # ctx
c_void_p, # *tag
c_size_t # tag_len
)
libmbedtls.mbedtls_cipher_check_tag.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_check_tag.argtypes = (
c_void_p, # ctx
c_char_p, # tag
c_size_t # tag_len
)
libmbedtls.mbedtls_cipher_crypt.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_crypt.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t, # iv_len, = 0 if iv = NULL
c_char_p, # input
c_size_t, # ilen
c_void_p, # *output, no less than ilen + block_size
c_void_p # *olen
)
if hasattr(libmbedtls, 'mbedtls_cipher_auth_encrypt'):
libmbedtls.mbedtls_cipher_auth_encrypt.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_auth_encrypt.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t, # iv_len
c_char_p, # ad
c_size_t, # ad_len
c_char_p, # input
c_size_t, # ilen
c_void_p, # *output, no less than ilen + block_size
c_void_p, # *olen
c_void_p, # *tag
c_size_t # tag_len
)
libmbedtls.mbedtls_cipher_auth_decrypt.restype = c_int # 0 on success
libmbedtls.mbedtls_cipher_auth_decrypt.argtypes = (
c_void_p, # ctx
c_char_p, # iv
c_size_t, # iv_len
c_char_p, # ad
c_size_t, # ad_len
c_char_p, # input
c_size_t, # ilen
c_void_p, # *output, no less than ilen + block_size
c_void_p, # *olen
c_char_p, # tag
c_size_t, # tag_len
)
buf = create_string_buffer(buf_size)
loaded = True
class MbedTLSCryptoBase(object):
"""
MbedTLS crypto base class
"""
def __init__(self, cipher_name, crypto_path=None):
global loaded
self._ctx = create_string_buffer(b'\0' * CIPHER_CTX_SIZE)
self._cipher = None
if not loaded:
load_mbedtls(crypto_path)
cipher_name = common.to_bytes(cipher_name.upper())
cipher = libmbedtls.mbedtls_cipher_info_from_string(cipher_name)
if not cipher:
raise Exception('cipher %s not found in libmbedtls' % cipher_name)
libmbedtls.mbedtls_cipher_init(byref(self._ctx))
if libmbedtls.mbedtls_cipher_setup(byref(self._ctx), cipher):
raise Exception('can not setup cipher')
self._cipher = cipher
self.encrypt_once = self.update
self.decrypt_once = self.update
def update(self, data):
"""
Encrypt/decrypt data
:param data: str
:return: str
"""
global buf_size, buf
cipher_out_len = c_size_t(0)
l = len(data)
if buf_size < l:
buf_size = l * 2
buf = create_string_buffer(buf_size)
libmbedtls.mbedtls_cipher_update(
byref(self._ctx),
c_char_p(data), c_size_t(l),
byref(buf), byref(cipher_out_len)
)
# buf is copied to a str object when we access buf.raw
return buf.raw[:cipher_out_len.value]
def __del__(self):
self.clean()
def clean(self):
if self._ctx:
libmbedtls.mbedtls_cipher_free(byref(self._ctx))
class MbedTLSAeadCrypto(MbedTLSCryptoBase, AeadCryptoBase):
"""
Implement mbedtls Aead mode: gcm
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if cipher_name[:len('mbedtls:')] == 'mbedtls:':
cipher_name = cipher_name[len('mbedtls:'):]
MbedTLSCryptoBase.__init__(self, cipher_name, crypto_path)
AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path)
key_ptr = c_char_p(self._skey)
r = libmbedtls.mbedtls_cipher_setkey(
byref(self._ctx),
key_ptr, c_int(len(key) * 8),
c_int(op)
)
if r:
self.clean()
raise Exception('can not initialize cipher context')
r = libmbedtls.mbedtls_cipher_reset(byref(self._ctx))
if r:
self.clean()
raise Exception('can not finish preparation of mbed TLS '
'cipher context')
def cipher_ctx_init(self):
"""
Nonce + 1
:return: None
"""
AeadCryptoBase.nonce_increment(self)
def set_tag(self, tag):
"""
Set tag before decrypt any data (update)
:param tag: authenticated tag
:return: None
"""
tag_len = self._tlen
r = libmbedtls.mbedtls_cipher_check_tag(
byref(self._ctx),
c_char_p(tag), c_size_t(tag_len)
)
if not r:
raise Exception('Set tag failed')
def get_tag(self):
"""
Get authenticated tag, called after EVP_CipherFinal_ex
:return: str
"""
tag_len = self._tlen
tag_buf = create_string_buffer(tag_len)
r = libmbedtls.mbedtls_cipher_write_tag(
byref(self._ctx),
byref(tag_buf), c_size_t(tag_len)
)
if not r:
raise Exception('Get tag failed')
return tag_buf.raw[:tag_len]
def final(self):
"""
Finish encrypt/decrypt a chunk (<= 0x3FFF)
:return: str
"""
global buf_size, buf
cipher_out_len = c_size_t(0)
r = libmbedtls.mbedtls_cipher_finish(
byref(self._ctx),
byref(buf), byref(cipher_out_len)
)
if not r:
# print(self._nonce.raw, r, cipher_out_len)
raise Exception('Finalize cipher failed')
return buf.raw[:cipher_out_len.value]
def aead_encrypt(self, data):
"""
Encrypt data with authenticate tag
:param data: plain text
:return: cipher text with tag
"""
global buf_size, buf
plen = len(data)
if buf_size < plen + self._tlen:
buf_size = (plen + self._tlen) * 2
buf = create_string_buffer(buf_size)
cipher_out_len = c_size_t(0)
tag_buf = create_string_buffer(self._tlen)
r = libmbedtls.mbedtls_cipher_auth_encrypt(
byref(self._ctx),
c_char_p(self._nonce.raw), c_size_t(self._nlen),
None, c_size_t(0),
c_char_p(data), c_size_t(plen),
byref(buf), byref(cipher_out_len),
byref(tag_buf), c_size_t(self._tlen)
)
assert cipher_out_len.value == plen
if r:
raise Exception('AEAD encrypt failed {0:#x}'.format(r))
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value] + tag_buf.raw[:self._tlen]
def aead_decrypt(self, data):
"""
Decrypt data and authenticate tag
:param data: cipher text with tag
:return: plain text
"""
global buf_size, buf
cipher_out_len = c_size_t(0)
plen = len(data) - self._tlen
if buf_size < plen:
buf_size = plen * 2
buf = create_string_buffer(buf_size)
tag = data[plen:]
r = libmbedtls.mbedtls_cipher_auth_decrypt(
byref(self._ctx),
c_char_p(self._nonce.raw), c_size_t(self._nlen),
None, c_size_t(0),
c_char_p(data), c_size_t(plen),
byref(buf), byref(cipher_out_len),
c_char_p(tag), c_size_t(self._tlen)
)
if r:
raise Exception('AEAD encrypt failed {0:#x}'.format(r))
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value]
class MbedTLSStreamCrypto(MbedTLSCryptoBase):
"""
Crypto for stream modes: cfb, ofb, ctr
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if cipher_name[:len('mbedtls:')] == 'mbedtls:':
cipher_name = cipher_name[len('mbedtls:'):]
MbedTLSCryptoBase.__init__(self, cipher_name, crypto_path)
key_ptr = c_char_p(key)
iv_ptr = c_char_p(iv)
r = libmbedtls.mbedtls_cipher_setkey(
byref(self._ctx),
key_ptr, c_int(len(key) * 8),
c_int(op)
)
if r:
self.clean()
raise Exception('can not set cipher key')
r = libmbedtls.mbedtls_cipher_set_iv(
byref(self._ctx),
iv_ptr, c_size_t(len(iv))
)
if r:
self.clean()
raise Exception('can not set cipher iv')
r = libmbedtls.mbedtls_cipher_reset(byref(self._ctx))
if r:
self.clean()
raise Exception('can not reset cipher')
self.encrypt = self.update
self.decrypt = self.update
ciphers = {
'mbedtls:aes-128-cfb128': (16, 16, MbedTLSStreamCrypto),
'mbedtls:aes-192-cfb128': (24, 16, MbedTLSStreamCrypto),
'mbedtls:aes-256-cfb128': (32, 16, MbedTLSStreamCrypto),
'mbedtls:aes-128-ctr': (16, 16, MbedTLSStreamCrypto),
'mbedtls:aes-192-ctr': (24, 16, MbedTLSStreamCrypto),
'mbedtls:aes-256-ctr': (32, 16, MbedTLSStreamCrypto),
'mbedtls:camellia-128-cfb128': (16, 16, MbedTLSStreamCrypto),
'mbedtls:camellia-192-cfb128': (24, 16, MbedTLSStreamCrypto),
'mbedtls:camellia-256-cfb128': (32, 16, MbedTLSStreamCrypto),
# AEAD: iv_len = salt_len = key_len
'mbedtls:aes-128-gcm': (16, 16, MbedTLSAeadCrypto),
'mbedtls:aes-192-gcm': (24, 24, MbedTLSAeadCrypto),
'mbedtls:aes-256-gcm': (32, 32, MbedTLSAeadCrypto),
}
def run_method(method):
print(method, ': [stream]', 32)
cipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 1)
decipher = MbedTLSStreamCrypto(method, b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def run_aead_method(method, key_len=16):
print(method, ': [payload][tag]', key_len)
key_len = int(key_len)
cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = MbedTLSAeadCrypto(
method,
b'k' * key_len, b'i' * key_len, 0
)
util.run_cipher(cipher, decipher)
def run_aead_method_chunk(method, key_len=16):
print(method, ': chunk([size][tag][payload][tag]', key_len)
key_len = int(key_len)
cipher = MbedTLSAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = MbedTLSAeadCrypto(
method,
b'k' * key_len, b'i' * key_len, 0
)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_camellia_256_cfb():
run_method('camellia-256-cfb128')
def test_aes_gcm(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method(method, bits / 8)
def test_aes_gcm_chunk(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method_chunk(method, bits / 8)
def test_aes_256_cfb():
run_method('aes-256-cfb128')
def test_aes_256_ctr():
run_method('aes-256-ctr')
if __name__ == '__main__':
test_aes_256_cfb()
test_camellia_256_cfb()
test_aes_256_ctr()
test_aes_gcm(128)
test_aes_gcm(192)
test_aes_gcm(256)
test_aes_gcm_chunk(128)
test_aes_gcm_chunk(192)
test_aes_gcm_chunk(256)

View file

@ -22,52 +22,34 @@ from ctypes import c_char_p, c_int, c_long, byref,\
from shadowsocks import common from shadowsocks import common
from shadowsocks.crypto import util from shadowsocks.crypto import util
from shadowsocks.crypto.aead import AeadCryptoBase, EVP_CTRL_AEAD_SET_IVLEN, \
EVP_CTRL_AEAD_GET_TAG, EVP_CTRL_AEAD_SET_TAG
__all__ = ['ciphers'] __all__ = ['ciphers']
libcrypto = None libcrypto = None
loaded = False loaded = False
libsodium = None
buf = None
buf_size = 2048 buf_size = 2048
ctx_cleanup = None
CIPHER_ENC_UNCHANGED = -1 def load_openssl():
global loaded, libcrypto, buf
def load_openssl(crypto_path=None):
global loaded, libcrypto, libsodium, buf, ctx_cleanup
crypto_path = dict(crypto_path) if crypto_path else dict()
path = crypto_path.get('openssl', None)
libcrypto = util.find_library(('crypto', 'eay32'), libcrypto = util.find_library(('crypto', 'eay32'),
'EVP_get_cipherbyname', 'EVP_get_cipherbyname',
'libcrypto', path) 'libcrypto')
if libcrypto is None: if libcrypto is None:
raise Exception('libcrypto(OpenSSL) not found with path %s' % path) raise Exception('libcrypto(OpenSSL) not found')
libcrypto.EVP_get_cipherbyname.restype = c_void_p libcrypto.EVP_get_cipherbyname.restype = c_void_p
libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p
libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p,
c_char_p, c_char_p, c_int) c_char_p, c_char_p, c_int)
libcrypto.EVP_CIPHER_CTX_ctrl.argtypes = (c_void_p, c_int, c_int, c_void_p)
libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p,
c_char_p, c_int) c_char_p, c_int)
libcrypto.EVP_CipherFinal_ex.argtypes = (c_void_p, c_void_p, c_void_p) libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
try:
libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,)
ctx_cleanup = libcrypto.EVP_CIPHER_CTX_cleanup
except AttributeError:
libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,)
ctx_cleanup = libcrypto.EVP_CIPHER_CTX_reset
libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,)
if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'):
libcrypto.OpenSSL_add_all_ciphers() libcrypto.OpenSSL_add_all_ciphers()
@ -77,7 +59,7 @@ def load_openssl(crypto_path=None):
def load_cipher(cipher_name): def load_cipher(cipher_name):
func_name = b'EVP_' + cipher_name.replace(b'-', b'_') func_name = 'EVP_' + cipher_name.replace('-', '_')
if bytes != str: if bytes != str:
func_name = str(func_name, 'utf-8') func_name = str(func_name, 'utf-8')
cipher = getattr(libcrypto, func_name, None) cipher = getattr(libcrypto, func_name, None)
@ -87,48 +69,37 @@ def load_cipher(cipher_name):
return None return None
class OpenSSLCryptoBase(object): class OpenSSLCrypto(object):
""" def __init__(self, cipher_name, key, iv, op):
OpenSSL crypto base class
"""
def __init__(self, cipher_name, crypto_path=None):
self._ctx = None self._ctx = None
self._cipher = None
if not loaded: if not loaded:
load_openssl(crypto_path) load_openssl()
cipher_name = common.to_bytes(cipher_name) cipher_name = common.to_bytes(cipher_name)
cipher = libcrypto.EVP_get_cipherbyname(cipher_name) cipher = libcrypto.EVP_get_cipherbyname(cipher_name)
if not cipher: if not cipher:
cipher = load_cipher(cipher_name) cipher = load_cipher(cipher_name)
if not cipher: if not cipher:
raise Exception('cipher %s not found in libcrypto' % cipher_name) raise Exception('cipher %s not found in libcrypto' % cipher_name)
key_ptr = c_char_p(key)
iv_ptr = c_char_p(iv)
self._ctx = libcrypto.EVP_CIPHER_CTX_new() self._ctx = libcrypto.EVP_CIPHER_CTX_new()
self._cipher = cipher
if not self._ctx: if not self._ctx:
raise Exception('can not create cipher context') raise Exception('can not create cipher context')
r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None,
def encrypt_once(self, data): key_ptr, iv_ptr, c_int(op))
return self.update(data) if not r:
self.clean()
def decrypt_once(self, data): raise Exception('can not initialize cipher context')
return self.update(data)
def update(self, data): def update(self, data):
"""
Encrypt/decrypt data
:param data: str
:return: str
"""
global buf_size, buf global buf_size, buf
cipher_out_len = c_long(0) cipher_out_len = c_long(0)
l = len(data) l = len(data)
if buf_size < l: if buf_size < l:
buf_size = l * 2 buf_size = l * 2
buf = create_string_buffer(buf_size) buf = create_string_buffer(buf_size)
libcrypto.EVP_CipherUpdate( libcrypto.EVP_CipherUpdate(self._ctx, byref(buf),
self._ctx, byref(buf), byref(cipher_out_len), c_char_p(data), l)
byref(cipher_out_len), c_char_p(data), l
)
# buf is copied to a str object when we access buf.raw # buf is copied to a str object when we access buf.raw
return buf.raw[:cipher_out_len.value] return buf.raw[:cipher_out_len.value]
@ -137,271 +108,47 @@ class OpenSSLCryptoBase(object):
def clean(self): def clean(self):
if self._ctx: if self._ctx:
ctx_cleanup(self._ctx) libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
libcrypto.EVP_CIPHER_CTX_free(self._ctx) libcrypto.EVP_CIPHER_CTX_free(self._ctx)
self._ctx = None
class OpenSSLAeadCrypto(OpenSSLCryptoBase, AeadCryptoBase):
"""
Implement OpenSSL Aead mode: gcm, ocb
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
OpenSSLCryptoBase.__init__(self, cipher_name, crypto_path)
AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path)
key_ptr = c_char_p(self._skey)
r = libcrypto.EVP_CipherInit_ex(
self._ctx,
self._cipher,
None,
key_ptr, None,
c_int(op)
)
if not r:
self.clean()
raise Exception('can not initialize cipher context')
r = libcrypto.EVP_CIPHER_CTX_ctrl(
self._ctx,
c_int(EVP_CTRL_AEAD_SET_IVLEN),
c_int(self._nlen),
None
)
if not r:
self.clean()
raise Exception('Set ivlen failed')
self.cipher_ctx_init()
def cipher_ctx_init(self):
"""
Need init cipher context after EVP_CipherFinal_ex to reuse context
:return: None
"""
iv_ptr = c_char_p(self._nonce.raw)
r = libcrypto.EVP_CipherInit_ex(
self._ctx,
None,
None,
None, iv_ptr,
c_int(CIPHER_ENC_UNCHANGED)
)
if not r:
self.clean()
raise Exception('can not initialize cipher context')
AeadCryptoBase.nonce_increment(self)
def set_tag(self, tag):
"""
Set tag before decrypt any data (update)
:param tag: authenticated tag
:return: None
"""
tag_len = self._tlen
r = libcrypto.EVP_CIPHER_CTX_ctrl(
self._ctx,
c_int(EVP_CTRL_AEAD_SET_TAG),
c_int(tag_len), c_char_p(tag)
)
if not r:
self.clean()
raise Exception('Set tag failed')
def get_tag(self):
"""
Get authenticated tag, called after EVP_CipherFinal_ex
:return: str
"""
tag_len = self._tlen
tag_buf = create_string_buffer(tag_len)
r = libcrypto.EVP_CIPHER_CTX_ctrl(
self._ctx,
c_int(EVP_CTRL_AEAD_GET_TAG),
c_int(tag_len), byref(tag_buf)
)
if not r:
self.clean()
raise Exception('Get tag failed')
return tag_buf.raw[:tag_len]
def final(self):
"""
Finish encrypt/decrypt a chunk (<= 0x3FFF)
:return: str
"""
global buf_size, buf
cipher_out_len = c_long(0)
r = libcrypto.EVP_CipherFinal_ex(
self._ctx,
byref(buf), byref(cipher_out_len)
)
if not r:
self.clean()
# print(self._nonce.raw, r, cipher_out_len)
raise Exception('Finalize cipher failed')
return buf.raw[:cipher_out_len.value]
def aead_encrypt(self, data):
"""
Encrypt data with authenticate tag
:param data: plain text
:return: cipher text with tag
"""
ctext = self.update(data) + self.final() + self.get_tag()
self.cipher_ctx_init()
return ctext
def aead_decrypt(self, data):
"""
Decrypt data and authenticate tag
:param data: cipher text with tag
:return: plain text
"""
clen = len(data)
if clen < self._tlen:
self.clean()
raise Exception('Data too short')
self.set_tag(data[clen - self._tlen:])
plaintext = self.update(data[:clen - self._tlen]) + self.final()
self.cipher_ctx_init()
return plaintext
def encrypt_once(self, data):
return self.aead_encrypt(data)
def decrypt_once(self, data):
return self.aead_decrypt(data)
class OpenSSLStreamCrypto(OpenSSLCryptoBase):
"""
Crypto for stream modes: cfb, ofb, ctr
"""
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
OpenSSLCryptoBase.__init__(self, cipher_name, crypto_path)
key_ptr = c_char_p(key)
iv_ptr = c_char_p(iv)
r = libcrypto.EVP_CipherInit_ex(self._ctx, self._cipher, None,
key_ptr, iv_ptr, c_int(op))
if not r:
self.clean()
raise Exception('can not initialize cipher context')
def encrypt(self, data):
return self.update(data)
def decrypt(self, data):
return self.update(data)
ciphers = { ciphers = {
'aes-128-cfb': (16, 16, OpenSSLStreamCrypto), 'aes-128-cfb': (16, 16, OpenSSLCrypto),
'aes-192-cfb': (24, 16, OpenSSLStreamCrypto), 'aes-192-cfb': (24, 16, OpenSSLCrypto),
'aes-256-cfb': (32, 16, OpenSSLStreamCrypto), 'aes-256-cfb': (32, 16, OpenSSLCrypto),
'aes-128-ofb': (16, 16, OpenSSLStreamCrypto), 'aes-128-ofb': (16, 16, OpenSSLCrypto),
'aes-192-ofb': (24, 16, OpenSSLStreamCrypto), 'aes-192-ofb': (24, 16, OpenSSLCrypto),
'aes-256-ofb': (32, 16, OpenSSLStreamCrypto), 'aes-256-ofb': (32, 16, OpenSSLCrypto),
'aes-128-ctr': (16, 16, OpenSSLStreamCrypto), 'aes-128-ctr': (16, 16, OpenSSLCrypto),
'aes-192-ctr': (24, 16, OpenSSLStreamCrypto), 'aes-192-ctr': (24, 16, OpenSSLCrypto),
'aes-256-ctr': (32, 16, OpenSSLStreamCrypto), 'aes-256-ctr': (32, 16, OpenSSLCrypto),
'aes-128-cfb8': (16, 16, OpenSSLStreamCrypto), 'aes-128-cfb8': (16, 16, OpenSSLCrypto),
'aes-192-cfb8': (24, 16, OpenSSLStreamCrypto), 'aes-192-cfb8': (24, 16, OpenSSLCrypto),
'aes-256-cfb8': (32, 16, OpenSSLStreamCrypto), 'aes-256-cfb8': (32, 16, OpenSSLCrypto),
'aes-128-cfb1': (16, 16, OpenSSLStreamCrypto), 'aes-128-cfb1': (16, 16, OpenSSLCrypto),
'aes-192-cfb1': (24, 16, OpenSSLStreamCrypto), 'aes-192-cfb1': (24, 16, OpenSSLCrypto),
'aes-256-cfb1': (32, 16, OpenSSLStreamCrypto), 'aes-256-cfb1': (32, 16, OpenSSLCrypto),
'bf-cfb': (16, 8, OpenSSLStreamCrypto), 'bf-cfb': (16, 8, OpenSSLCrypto),
'camellia-128-cfb': (16, 16, OpenSSLStreamCrypto), 'camellia-128-cfb': (16, 16, OpenSSLCrypto),
'camellia-192-cfb': (24, 16, OpenSSLStreamCrypto), 'camellia-192-cfb': (24, 16, OpenSSLCrypto),
'camellia-256-cfb': (32, 16, OpenSSLStreamCrypto), 'camellia-256-cfb': (32, 16, OpenSSLCrypto),
'cast5-cfb': (16, 8, OpenSSLStreamCrypto), 'cast5-cfb': (16, 8, OpenSSLCrypto),
'des-cfb': (8, 8, OpenSSLStreamCrypto), 'des-cfb': (8, 8, OpenSSLCrypto),
'idea-cfb': (16, 8, OpenSSLStreamCrypto), 'idea-cfb': (16, 8, OpenSSLCrypto),
'rc2-cfb': (16, 8, OpenSSLStreamCrypto), 'rc2-cfb': (16, 8, OpenSSLCrypto),
'rc4': (16, 0, OpenSSLStreamCrypto), 'rc4': (16, 0, OpenSSLCrypto),
'seed-cfb': (16, 16, OpenSSLStreamCrypto), 'seed-cfb': (16, 16, OpenSSLCrypto),
# AEAD: iv_len = salt_len = key_len
'aes-128-gcm': (16, 16, OpenSSLAeadCrypto),
'aes-192-gcm': (24, 24, OpenSSLAeadCrypto),
'aes-256-gcm': (32, 32, OpenSSLAeadCrypto),
'aes-128-ocb': (16, 16, OpenSSLAeadCrypto),
'aes-192-ocb': (24, 24, OpenSSLAeadCrypto),
'aes-256-ocb': (32, 32, OpenSSLAeadCrypto),
} }
def run_method(method): def run_method(method):
print(method, ': [stream]', 32) cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1)
cipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 1) decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0)
decipher = OpenSSLStreamCrypto(method, b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher) util.run_cipher(cipher, decipher)
def run_aead_method(method, key_len=16):
if not loaded:
load_openssl(None)
print(method, ': [payload][tag]', key_len)
cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method))
if not cipher:
cipher = load_cipher(common.to_bytes(method))
if not cipher:
print('cipher not avaiable, please upgrade openssl')
return
key_len = int(key_len)
cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0)
util.run_cipher(cipher, decipher)
def run_aead_method_chunk(method, key_len=16):
if not loaded:
load_openssl(None)
print(method, ': chunk([size][tag][payload][tag]', key_len)
cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(method))
if not cipher:
cipher = load_cipher(common.to_bytes(method))
if not cipher:
print('cipher not avaiable, please upgrade openssl')
return
key_len = int(key_len)
cipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 1)
decipher = OpenSSLAeadCrypto(method, b'k' * key_len, b'i' * key_len, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_aes_gcm(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method(method, bits / 8)
def test_aes_ocb(bits=128):
method = "aes-{0}-ocb".format(bits)
run_aead_method(method, bits / 8)
def test_aes_gcm_chunk(bits=128):
method = "aes-{0}-gcm".format(bits)
run_aead_method_chunk(method, bits / 8)
def test_aes_ocb_chunk(bits=128):
method = "aes-{0}-ocb".format(bits)
run_aead_method_chunk(method, bits / 8)
def test_aes_128_cfb(): def test_aes_128_cfb():
run_method('aes-128-cfb') run_method('aes-128-cfb')
@ -432,17 +179,3 @@ def test_rc4():
if __name__ == '__main__': if __name__ == '__main__':
test_aes_128_cfb() test_aes_128_cfb()
test_aes_256_cfb()
test_aes_256_ofb()
test_aes_gcm(128)
test_aes_gcm(192)
test_aes_gcm(256)
test_aes_gcm_chunk(128)
test_aes_gcm_chunk(192)
test_aes_gcm_chunk(256)
test_aes_ocb(128)
test_aes_ocb(192)
test_aes_ocb(256)
test_aes_ocb_chunk(128)
test_aes_ocb_chunk(192)
test_aes_ocb_chunk(256)

View file

@ -18,19 +18,19 @@ from __future__ import absolute_import, division, print_function, \
with_statement with_statement
import hashlib import hashlib
from shadowsocks.crypto import openssl from shadowsocks.crypto import openssl
__all__ = ['ciphers'] __all__ = ['ciphers']
def create_cipher(alg, key, iv, op, crypto_path=None, def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None,
key_as_bytes=0, d=None, salt=None,
i=1, padding=1): i=1, padding=1):
md5 = hashlib.md5() md5 = hashlib.md5()
md5.update(key) md5.update(key)
md5.update(iv) md5.update(iv)
rc4_key = md5.digest() rc4_key = md5.digest()
return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op, crypto_path) return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op)
ciphers = { ciphers = {

View file

@ -17,162 +17,49 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
with_statement with_statement
from ctypes import c_char_p, c_int, c_uint, c_ulonglong, byref, \ from ctypes import c_char_p, c_int, c_ulonglong, byref, \
create_string_buffer, c_void_p create_string_buffer, c_void_p
from shadowsocks.crypto import util from shadowsocks.crypto import util
from shadowsocks.crypto import aead
from shadowsocks.crypto.aead import AeadCryptoBase
__all__ = ['ciphers'] __all__ = ['ciphers']
libsodium = None libsodium = None
loaded = False loaded = False
buf = None
buf_size = 2048 buf_size = 2048
# for salsa20 and chacha20 and chacha20-ietf # for salsa20 and chacha20
BLOCK_SIZE = 64 BLOCK_SIZE = 64
def load_libsodium(crypto_path=None): def load_libsodium():
global loaded, libsodium, buf global loaded, libsodium, buf
crypto_path = dict(crypto_path) if crypto_path else dict() libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic',
path = crypto_path.get('sodium', None) 'libsodium')
if libsodium is None:
if not aead.sodium_loaded: raise Exception('libsodium not found')
aead.load_sodium(path)
if aead.sodium_loaded:
libsodium = aead.libsodium
else:
print('load libsodium again with path %s' % path)
libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic',
'libsodium', path)
if libsodium is None:
raise Exception('libsodium not found')
if libsodium.sodium_init() < 0:
raise Exception('libsodium init failed')
libsodium.crypto_stream_salsa20_xor_ic.restype = c_int libsodium.crypto_stream_salsa20_xor_ic.restype = c_int
libsodium.crypto_stream_salsa20_xor_ic.argtypes = ( libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p,
c_void_p, c_char_p, # cipher output, msg c_ulonglong,
c_ulonglong, # msg len c_char_p, c_ulonglong,
c_char_p, c_ulonglong, # nonce, uint64_t initial block counter c_char_p)
c_char_p # key
)
libsodium.crypto_stream_chacha20_xor_ic.restype = c_int libsodium.crypto_stream_chacha20_xor_ic.restype = c_int
libsodium.crypto_stream_chacha20_xor_ic.argtypes = ( libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p,
c_void_p, c_char_p, c_ulonglong,
c_ulonglong, c_char_p, c_ulonglong,
c_char_p, c_ulonglong, c_char_p)
c_char_p
)
if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'):
libsodium.crypto_stream_xchacha20_xor_ic.restype = c_int
libsodium.crypto_stream_xchacha20_xor_ic.argtypes = (
c_void_p, c_char_p,
c_ulonglong,
c_char_p, c_ulonglong,
c_char_p
)
libsodium.crypto_stream_chacha20_ietf_xor_ic.restype = c_int
libsodium.crypto_stream_chacha20_ietf_xor_ic.argtypes = (
c_void_p, c_char_p,
c_ulonglong,
c_char_p,
c_uint, # uint32_t initial counter
c_char_p
)
# chacha20-poly1305
libsodium.crypto_aead_chacha20poly1305_encrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_encrypt.argtypes = (
c_void_p, c_void_p, # c, clen
c_char_p, c_ulonglong, # m, mlen
c_char_p, c_ulonglong, # ad, adlen
c_char_p, # nsec, not used
c_char_p, c_char_p # npub, k
)
libsodium.crypto_aead_chacha20poly1305_decrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_decrypt.argtypes = (
c_void_p, c_void_p, # m, mlen
c_char_p, # nsec, not used
c_char_p, c_ulonglong, # c, clen
c_char_p, c_ulonglong, # ad, adlen
c_char_p, c_char_p # npub, k
)
# chacha20-ietf-poly1305, same api structure as above
libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_ietf_encrypt.argtypes = (
c_void_p, c_void_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p,
c_char_p, c_char_p
)
libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.restype = c_int
libsodium.crypto_aead_chacha20poly1305_ietf_decrypt.argtypes = (
c_void_p, c_void_p,
c_char_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p, c_char_p
)
# xchacha20-ietf-poly1305, same api structure as above
if hasattr(libsodium, 'crypto_aead_xchacha20poly1305_ietf_encrypt'):
libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.restype = c_int
libsodium.crypto_aead_xchacha20poly1305_ietf_encrypt.argtypes = (
c_void_p, c_void_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p,
c_char_p, c_char_p
)
libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.restype = c_int
libsodium.crypto_aead_xchacha20poly1305_ietf_decrypt.argtypes = (
c_void_p, c_void_p,
c_char_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p, c_char_p
)
# aes-256-gcm, same api structure as above
libsodium.crypto_aead_aes256gcm_is_available.restype = c_int
if libsodium.crypto_aead_aes256gcm_is_available():
libsodium.crypto_aead_aes256gcm_encrypt.restype = c_int
libsodium.crypto_aead_aes256gcm_encrypt.argtypes = (
c_void_p, c_void_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p,
c_char_p, c_char_p
)
libsodium.crypto_aead_aes256gcm_decrypt.restype = c_int
libsodium.crypto_aead_aes256gcm_decrypt.argtypes = (
c_void_p, c_void_p,
c_char_p,
c_char_p, c_ulonglong,
c_char_p, c_ulonglong,
c_char_p, c_char_p
)
buf = create_string_buffer(buf_size) buf = create_string_buffer(buf_size)
loaded = True loaded = True
class SodiumCrypto(object): class SodiumCrypto(object):
def __init__(self, cipher_name, key, iv, op, crypto_path=None): def __init__(self, cipher_name, key, iv, op):
if not loaded: if not loaded:
load_libsodium(crypto_path) load_libsodium()
self.key = key self.key = key
self.iv = iv self.iv = iv
self.key_ptr = c_char_p(key) self.key_ptr = c_char_p(key)
@ -181,30 +68,11 @@ class SodiumCrypto(object):
self.cipher = libsodium.crypto_stream_salsa20_xor_ic self.cipher = libsodium.crypto_stream_salsa20_xor_ic
elif cipher_name == 'chacha20': elif cipher_name == 'chacha20':
self.cipher = libsodium.crypto_stream_chacha20_xor_ic self.cipher = libsodium.crypto_stream_chacha20_xor_ic
elif cipher_name == 'xchacha20':
if hasattr(libsodium, 'crypto_stream_xchacha20_xor_ic'):
self.cipher = libsodium.crypto_stream_xchacha20_xor_ic
else:
raise Exception('Unsupported cipher')
elif cipher_name == 'chacha20-ietf':
self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic
else: else:
raise Exception('Unknown cipher') raise Exception('Unknown cipher')
# byte counter, not block counter # byte counter, not block counter
self.counter = 0 self.counter = 0
def encrypt(self, data):
return self.update(data)
def decrypt(self, data):
return self.update(data)
def encrypt_once(self, data):
return self.update(data)
def decrypt_once(self, data):
return self.update(data)
def update(self, data): def update(self, data):
global buf_size, buf global buf_size, buf
l = len(data) l = len(data)
@ -225,218 +93,28 @@ class SodiumCrypto(object):
# strip off the padding # strip off the padding
return buf.raw[padding:padding + l] return buf.raw[padding:padding + l]
def clean(self):
pass
class SodiumAeadCrypto(AeadCryptoBase):
def __init__(self, cipher_name, key, iv, op, crypto_path=None):
if not loaded:
load_libsodium(crypto_path)
AeadCryptoBase.__init__(self, cipher_name, key, iv, op, crypto_path)
if cipher_name == 'chacha20-poly1305':
self.encryptor = libsodium.crypto_aead_chacha20poly1305_encrypt
self.decryptor = libsodium.crypto_aead_chacha20poly1305_decrypt
elif cipher_name == 'chacha20-ietf-poly1305':
self.encryptor = libsodium. \
crypto_aead_chacha20poly1305_ietf_encrypt
self.decryptor = libsodium. \
crypto_aead_chacha20poly1305_ietf_decrypt
elif cipher_name == 'xchacha20-ietf-poly1305':
if hasattr(libsodium,
'crypto_aead_xchacha20poly1305_ietf_encrypt'):
self.encryptor = libsodium. \
crypto_aead_xchacha20poly1305_ietf_encrypt
self.decryptor = libsodium. \
crypto_aead_xchacha20poly1305_ietf_decrypt
else:
raise Exception('Unsupported cipher')
elif cipher_name == 'sodium:aes-256-gcm':
if hasattr(libsodium, 'crypto_aead_aes256gcm_encrypt'):
self.encryptor = libsodium.crypto_aead_aes256gcm_encrypt
self.decryptor = libsodium.crypto_aead_aes256gcm_decrypt
else:
raise Exception('Unsupported cipher')
else:
raise Exception('Unknown cipher')
def cipher_ctx_init(self):
global libsodium
libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen))
# print("".join("%02x" % ord(b) for b in self._nonce))
def aead_encrypt(self, data):
global buf, buf_size
plen = len(data)
if buf_size < plen + self._tlen:
buf_size = (plen + self._tlen) * 2
buf = create_string_buffer(buf_size)
cipher_out_len = c_ulonglong(0)
self.encryptor(
byref(buf), byref(cipher_out_len),
c_char_p(data), c_ulonglong(plen),
None, c_ulonglong(0), None,
c_char_p(self._nonce.raw), c_char_p(self._skey)
)
if cipher_out_len.value != plen + self._tlen:
raise Exception("Encrypt failed")
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value]
def aead_decrypt(self, data):
global buf, buf_size
clen = len(data)
if buf_size < clen:
buf_size = clen * 2
buf = create_string_buffer(buf_size)
cipher_out_len = c_ulonglong(0)
r = self.decryptor(
byref(buf), byref(cipher_out_len),
None,
c_char_p(data), c_ulonglong(clen),
None, c_ulonglong(0),
c_char_p(self._nonce.raw), c_char_p(self._skey)
)
if r != 0:
raise Exception("Decrypt failed")
if cipher_out_len.value != clen - self._tlen:
raise Exception("Decrypt failed")
self.cipher_ctx_init()
return buf.raw[:cipher_out_len.value]
def encrypt_once(self, data):
return self.aead_encrypt(data)
def decrypt_once(self, data):
return self.aead_decrypt(data)
ciphers = { ciphers = {
'salsa20': (32, 8, SodiumCrypto), 'salsa20': (32, 8, SodiumCrypto),
'chacha20': (32, 8, SodiumCrypto), 'chacha20': (32, 8, SodiumCrypto),
'xchacha20': (32, 24, SodiumCrypto),
'chacha20-ietf': (32, 12, SodiumCrypto),
# AEAD: iv_len = salt_len = key_len
'chacha20-poly1305': (32, 32, SodiumAeadCrypto),
'chacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto),
'xchacha20-ietf-poly1305': (32, 32, SodiumAeadCrypto),
'sodium:aes-256-gcm': (32, 32, SodiumAeadCrypto),
} }
def test_chacha20():
print("Test chacha20")
cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher)
def test_xchacha20():
print("Test xchacha20")
cipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 1)
decipher = SodiumCrypto('xchacha20', b'k' * 32, b'i' * 24, 0)
util.run_cipher(cipher, decipher)
def test_salsa20(): def test_salsa20():
print("Test salsa20")
cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher) util.run_cipher(cipher, decipher)
def test_chacha20_ietf(): def test_chacha20():
print("Test chacha20-ietf")
cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0)
util.run_cipher(cipher, decipher) cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1)
decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0)
def test_chacha20_poly1305():
print("Test chacha20-poly1305 [payload][tag]")
cipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_poly1305_chunk():
print("Test chacha20-poly1305 chunk [size][tag][payload][tag]")
cipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-poly1305',
b'k' * 32, b'i' * 32, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_chacha20_ietf_poly1305():
print("Test chacha20-ietf-poly1305 [payload][tag]")
cipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 0)
util.run_cipher(cipher, decipher)
def test_chacha20_ietf_poly1305_chunk():
print("Test chacha20-ietf-poly1305 chunk [size][tag][payload][tag]")
cipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('chacha20-ietf-poly1305',
b'k' * 32, b'i' * 32, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher)
def test_aes_256_gcm():
print("Test sodium:aes-256-gcm [payload][tag]")
cipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 0)
util.run_cipher(cipher, decipher)
def test_aes_256_gcm_chunk():
print("Test sodium:aes-256-gcm chunk [size][tag][payload][tag]")
cipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 1)
decipher = SodiumAeadCrypto('sodium:aes-256-gcm',
b'k' * 32, b'i' * 32, 0)
cipher.encrypt_once = cipher.encrypt
decipher.decrypt_once = decipher.decrypt
util.run_cipher(cipher, decipher) util.run_cipher(cipher, decipher)
if __name__ == '__main__': if __name__ == '__main__':
test_chacha20() test_chacha20()
test_xchacha20()
test_salsa20() test_salsa20()
test_chacha20_ietf()
test_chacha20_poly1305()
test_chacha20_poly1305_chunk()
test_chacha20_ietf_poly1305()
test_chacha20_ietf_poly1305_chunk()
test_aes_256_gcm()
test_aes_256_gcm_chunk()

View file

@ -55,13 +55,9 @@ def init_table(key):
class TableCipher(object): class TableCipher(object):
def __init__(self, cipher_name, key, iv, op, crypto_path=None): def __init__(self, cipher_name, key, iv, op):
self._encrypt_table, self._decrypt_table = init_table(key) self._encrypt_table, self._decrypt_table = init_table(key)
self._op = op self._op = op
self.encrypt = self.update
self.decrypt = self.update
self.encrypt_once = self.update
self.decrypt_once = self.update
def update(self, data): def update(self, data):
if self._op: if self._op:

View file

@ -26,7 +26,6 @@ def find_library_nt(name):
# ctypes.util.find_library just returns first result he found # ctypes.util.find_library just returns first result he found
# but we want to try them all # but we want to try them all
# because on Windows, users may have both 32bit and 64bit version installed # because on Windows, users may have both 32bit and 64bit version installed
import glob
results = [] results = []
for directory in os.environ['PATH'].split(os.pathsep): for directory in os.environ['PATH'].split(os.pathsep):
fname = os.path.join(directory, name) fname = os.path.join(directory, name)
@ -34,34 +33,15 @@ def find_library_nt(name):
results.append(fname) results.append(fname)
if fname.lower().endswith(".dll"): if fname.lower().endswith(".dll"):
continue continue
fname += "*.dll" fname = fname + ".dll"
files = glob.glob(fname) if os.path.isfile(fname):
if files: results.append(fname)
results.extend(files)
return results return results
def load_library(path, search_symbol, library_name): def find_library(possible_lib_names, search_symbol, library_name):
from ctypes import CDLL
try:
lib = CDLL(path)
if hasattr(lib, search_symbol):
logging.info('loading %s from %s', library_name, path)
return lib
else:
logging.warn('can\'t find symbol %s in %s', search_symbol,
path)
except Exception:
pass
return None
def find_library(possible_lib_names, search_symbol, library_name,
custom_path=None):
import ctypes.util import ctypes.util
from ctypes import CDLL
if custom_path:
return load_library(custom_path, search_symbol, library_name)
paths = [] paths = []
@ -99,22 +79,16 @@ def find_library(possible_lib_names, search_symbol, library_name,
if files: if files:
paths.extend(files) paths.extend(files)
for path in paths: for path in paths:
lib = load_library(path, search_symbol, library_name) try:
if lib: lib = CDLL(path)
return lib if hasattr(lib, search_symbol):
return None logging.info('loading %s from %s', library_name, path)
return lib
else:
def parse_mode(cipher_nme): logging.warn('can\'t find symbol %s in %s', search_symbol,
""" path)
Parse the cipher mode from cipher name except Exception:
e.g. aes-128-gcm, the mode is gcm pass
:param cipher_nme: str cipher name, aes-128-cfb, aes-128-gcm ...
:return: str/None The mode, cfb, gcm ...
"""
hyphen = cipher_nme.rfind('-')
if hyphen > 0:
return cipher_nme[hyphen:]
return None return None
@ -123,31 +97,29 @@ def run_cipher(cipher, decipher):
import random import random
import time import time
block_size = 16384 BLOCK_SIZE = 16384
rounds = 1 * 1024 rounds = 1 * 1024
plain = urandom(block_size * rounds) plain = urandom(BLOCK_SIZE * rounds)
cipher_results = [] results = []
pos = 0 pos = 0
print('test start') print('test start')
start = time.time() start = time.time()
while pos < len(plain): while pos < len(plain):
l = random.randint(100, 32768) l = random.randint(100, 32768)
# print(pos, l) c = cipher.update(plain[pos:pos + l])
c = cipher.encrypt_once(plain[pos:pos + l]) results.append(c)
cipher_results.append(c)
pos += l pos += l
pos = 0 pos = 0
# c = b''.join(cipher_results) c = b''.join(results)
plain_results = [] results = []
for c in cipher_results: while pos < len(plain):
# l = random.randint(100, 32768) l = random.randint(100, 32768)
l = len(c) results.append(decipher.update(c[pos:pos + l]))
plain_results.append(decipher.decrypt_once(c))
pos += l pos += l
end = time.time() end = time.time()
print('speed: %d bytes/s' % (block_size * rounds / (end - start))) print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start)))
assert b''.join(plain_results) == plain assert b''.join(results) == plain
def test_find_library(): def test_find_library():

View file

@ -117,7 +117,7 @@ def daemon_start(pid_file, log_file):
sys.exit(1) sys.exit(1)
os.setsid() os.setsid()
signal.signal(signal.SIGHUP, signal.SIG_IGN) signal.signal(signal.SIG_IGN, signal.SIGHUP)
print('started') print('started')
os.kill(ppid, signal.SIGTERM) os.kill(ppid, signal.SIGTERM)

View file

@ -23,20 +23,12 @@ import hashlib
import logging import logging
from shadowsocks import common from shadowsocks import common
from shadowsocks.crypto import rc4_md5, openssl, mbedtls, sodium, table from shadowsocks.crypto import rc4_md5, openssl, sodium, table
CIPHER_ENC_ENCRYPTION = 1
CIPHER_ENC_DECRYPTION = 0
METHOD_INFO_KEY_LEN = 0
METHOD_INFO_IV_LEN = 1
METHOD_INFO_CRYPTO = 2
method_supported = {} method_supported = {}
method_supported.update(rc4_md5.ciphers) method_supported.update(rc4_md5.ciphers)
method_supported.update(openssl.ciphers) method_supported.update(openssl.ciphers)
method_supported.update(mbedtls.ciphers)
method_supported.update(sodium.ciphers) method_supported.update(sodium.ciphers)
method_supported.update(table.ciphers) method_supported.update(table.ciphers)
@ -44,11 +36,12 @@ method_supported.update(table.ciphers)
def random_string(length): def random_string(length):
return os.urandom(length) return os.urandom(length)
cached_keys = {} cached_keys = {}
def try_cipher(key, method=None, crypto_path=None): def try_cipher(key, method=None):
Cryptor(key, method, crypto_path) Encryptor(key, method)
def EVP_BytesToKey(password, key_len, iv_len): def EVP_BytesToKey(password, key_len, iv_len):
@ -75,36 +68,24 @@ def EVP_BytesToKey(password, key_len, iv_len):
return key, iv return key, iv
class Cryptor(object): class Encryptor(object):
def __init__(self, password, method, crypto_path=None): def __init__(self, key, method):
""" self.key = key
Crypto wrapper
:param password: str cipher password
:param method: str cipher
:param crypto_path: dict or none
{'openssl': path, 'sodium': path, 'mbedtls': path}
"""
self.password = password
self.key = None
self.method = method self.method = method
self.iv = None
self.iv_sent = False self.iv_sent = False
self.cipher_iv = b'' self.cipher_iv = b''
self.decipher = None self.decipher = None
self.decipher_iv = None
self.crypto_path = crypto_path
method = method.lower() method = method.lower()
self._method_info = Cryptor.get_method_info(method) self._method_info = self.get_method_info(method)
if self._method_info: if self._method_info:
self.cipher = self.get_cipher( self.cipher = self.get_cipher(key, method, 1,
password, method, CIPHER_ENC_ENCRYPTION, random_string(self._method_info[1]))
random_string(self._method_info[METHOD_INFO_IV_LEN])
)
else: else:
logging.error('method %s not supported' % method) logging.error('method %s not supported' % method)
sys.exit(1) sys.exit(1)
@staticmethod def get_method_info(self, method):
def get_method_info(method):
method = method.lower() method = method.lower()
m = method_supported.get(method) m = method_supported.get(method)
return m return m
@ -115,90 +96,63 @@ class Cryptor(object):
def get_cipher(self, password, method, op, iv): def get_cipher(self, password, method, op, iv):
password = common.to_bytes(password) password = common.to_bytes(password)
m = self._method_info m = self._method_info
if m[METHOD_INFO_KEY_LEN] > 0: if m[0] > 0:
key, _ = EVP_BytesToKey(password, key, iv_ = EVP_BytesToKey(password, m[0], m[1])
m[METHOD_INFO_KEY_LEN],
m[METHOD_INFO_IV_LEN])
else: else:
# key_length == 0 indicates we should use the key directly # key_length == 0 indicates we should use the key directly
key, iv = password, b'' key, iv = password, b''
self.key = key
iv = iv[:m[METHOD_INFO_IV_LEN]] iv = iv[:m[1]]
if op == CIPHER_ENC_ENCRYPTION: if op == 1:
# this iv is for cipher not decipher # this iv is for cipher not decipher
self.cipher_iv = iv self.cipher_iv = iv[:m[1]]
return m[METHOD_INFO_CRYPTO](method, key, iv, op, self.crypto_path) return m[2](method, key, iv, op)
def encrypt(self, buf): def encrypt(self, buf):
if len(buf) == 0: if len(buf) == 0:
return buf return buf
if self.iv_sent: if self.iv_sent:
return self.cipher.encrypt(buf) return self.cipher.update(buf)
else: else:
self.iv_sent = True self.iv_sent = True
return self.cipher_iv + self.cipher.encrypt(buf) return self.cipher_iv + self.cipher.update(buf)
def decrypt(self, buf): def decrypt(self, buf):
if len(buf) == 0: if len(buf) == 0:
return buf return buf
if self.decipher is None: if self.decipher is None:
decipher_iv_len = self._method_info[METHOD_INFO_IV_LEN] decipher_iv_len = self._method_info[1]
decipher_iv = buf[:decipher_iv_len] decipher_iv = buf[:decipher_iv_len]
self.decipher_iv = decipher_iv self.decipher = self.get_cipher(self.key, self.method, 0,
self.decipher = self.get_cipher( iv=decipher_iv)
self.password, self.method,
CIPHER_ENC_DECRYPTION,
decipher_iv
)
buf = buf[decipher_iv_len:] buf = buf[decipher_iv_len:]
if len(buf) == 0: if len(buf) == 0:
return buf return buf
return self.decipher.decrypt(buf) return self.decipher.update(buf)
def gen_key_iv(password, method): def encrypt_all(password, method, op, data):
result = []
method = method.lower() method = method.lower()
(key_len, iv_len, m) = method_supported[method] (key_len, iv_len, m) = method_supported[method]
if key_len > 0: if key_len > 0:
key, _ = EVP_BytesToKey(password, key_len, iv_len) key, _ = EVP_BytesToKey(password, key_len, iv_len)
else: else:
key = password key = password
iv = random_string(iv_len) if op:
return key, iv, m iv = random_string(iv_len)
result.append(iv)
else:
def encrypt_all_m(key, iv, m, method, data, crypto_path=None): iv = data[:iv_len]
result = [iv] data = data[iv_len:]
cipher = m(method, key, iv, 1, crypto_path) cipher = m(method, key, iv, op)
result.append(cipher.encrypt_once(data)) result.append(cipher.update(data))
return b''.join(result)
def decrypt_all(password, method, data, crypto_path=None):
result = []
method = method.lower()
(key, iv, m) = gen_key_iv(password, method)
iv = data[:len(iv)]
data = data[len(iv):]
cipher = m(method, key, iv, CIPHER_ENC_DECRYPTION, crypto_path)
result.append(cipher.decrypt_once(data))
return b''.join(result), key, iv
def encrypt_all(password, method, data, crypto_path=None):
result = []
method = method.lower()
(key, iv, m) = gen_key_iv(password, method)
result.append(iv)
cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION, crypto_path)
result.append(cipher.encrypt_once(data))
return b''.join(result) return b''.join(result)
CIPHERS_TO_TEST = [ CIPHERS_TO_TEST = [
'aes-128-cfb', 'aes-128-cfb',
'aes-256-cfb', 'aes-256-cfb',
'aes-256-gcm',
'rc4-md5', 'rc4-md5',
'salsa20', 'salsa20',
'chacha20', 'chacha20',
@ -211,8 +165,8 @@ def test_encryptor():
plain = urandom(10240) plain = urandom(10240)
for method in CIPHERS_TO_TEST: for method in CIPHERS_TO_TEST:
logging.warn(method) logging.warn(method)
encryptor = Cryptor(b'key', method) encryptor = Encryptor(b'key', method)
decryptor = Cryptor(b'key', method) decryptor = Encryptor(b'key', method)
cipher = encryptor.encrypt(plain) cipher = encryptor.encrypt(plain)
plain2 = decryptor.decrypt(cipher) plain2 = decryptor.decrypt(cipher)
assert plain == plain2 assert plain == plain2
@ -223,23 +177,11 @@ def test_encrypt_all():
plain = urandom(10240) plain = urandom(10240)
for method in CIPHERS_TO_TEST: for method in CIPHERS_TO_TEST:
logging.warn(method) logging.warn(method)
cipher = encrypt_all(b'key', method, plain) cipher = encrypt_all(b'key', method, 1, plain)
plain2, key, iv = decrypt_all(b'key', method, cipher) plain2 = encrypt_all(b'key', method, 0, cipher)
assert plain == plain2
def test_encrypt_all_m():
from os import urandom
plain = urandom(10240)
for method in CIPHERS_TO_TEST:
logging.warn(method)
key, iv, m = gen_key_iv(b'key', method)
cipher = encrypt_all_m(key, iv, m, method, plain)
plain2, key, iv = decrypt_all(b'key', method, cipher)
assert plain == plain2 assert plain == plain2
if __name__ == '__main__': if __name__ == '__main__':
test_encrypt_all() test_encrypt_all()
test_encryptor() test_encryptor()
test_encrypt_all_m()

View file

@ -22,10 +22,8 @@ from __future__ import absolute_import, division, print_function, \
with_statement with_statement
import os import os
import time
import socket import socket
import select import select
import traceback
import errno import errno
import logging import logging
from collections import defaultdict from collections import defaultdict
@ -53,8 +51,23 @@ EVENT_NAMES = {
POLL_NVAL: 'POLL_NVAL', POLL_NVAL: 'POLL_NVAL',
} }
# we check timeouts every TIMEOUT_PRECISION seconds
TIMEOUT_PRECISION = 10 class EpollLoop(object):
def __init__(self):
self._epoll = select.epoll()
def poll(self, timeout):
return self._epoll.poll(timeout)
def add_fd(self, fd, mode):
self._epoll.register(fd, mode)
def remove_fd(self, fd):
self._epoll.unregister(fd)
def modify_fd(self, fd, mode):
self._epoll.modify(fd, mode)
class KqueueLoop(object): class KqueueLoop(object):
@ -87,20 +100,17 @@ class KqueueLoop(object):
results[fd] |= POLL_OUT results[fd] |= POLL_OUT
return results.items() return results.items()
def register(self, fd, mode): def add_fd(self, fd, mode):
self._fds[fd] = mode self._fds[fd] = mode
self._control(fd, mode, select.KQ_EV_ADD) self._control(fd, mode, select.KQ_EV_ADD)
def unregister(self, fd): def remove_fd(self, fd):
self._control(fd, self._fds[fd], select.KQ_EV_DELETE) self._control(fd, self._fds[fd], select.KQ_EV_DELETE)
del self._fds[fd] del self._fds[fd]
def modify(self, fd, mode): def modify_fd(self, fd, mode):
self.unregister(fd) self.remove_fd(fd)
self.register(fd, mode) self.add_fd(fd, mode)
def close(self):
self._kqueue.close()
class SelectLoop(object): class SelectLoop(object):
@ -119,7 +129,7 @@ class SelectLoop(object):
results[fd] |= p[1] results[fd] |= p[1]
return results.items() return results.items()
def register(self, fd, mode): def add_fd(self, fd, mode):
if mode & POLL_IN: if mode & POLL_IN:
self._r_list.add(fd) self._r_list.add(fd)
if mode & POLL_OUT: if mode & POLL_OUT:
@ -127,7 +137,7 @@ class SelectLoop(object):
if mode & POLL_ERR: if mode & POLL_ERR:
self._x_list.add(fd) self._x_list.add(fd)
def unregister(self, fd): def remove_fd(self, fd):
if fd in self._r_list: if fd in self._r_list:
self._r_list.remove(fd) self._r_list.remove(fd)
if fd in self._w_list: if fd in self._w_list:
@ -135,18 +145,16 @@ class SelectLoop(object):
if fd in self._x_list: if fd in self._x_list:
self._x_list.remove(fd) self._x_list.remove(fd)
def modify(self, fd, mode): def modify_fd(self, fd, mode):
self.unregister(fd) self.remove_fd(fd)
self.register(fd, mode) self.add_fd(fd, mode)
def close(self):
pass
class EventLoop(object): class EventLoop(object):
def __init__(self): def __init__(self):
self._iterating = False
if hasattr(select, 'epoll'): if hasattr(select, 'epoll'):
self._impl = select.epoll() self._impl = EpollLoop()
model = 'epoll' model = 'epoll'
elif hasattr(select, 'kqueue'): elif hasattr(select, 'kqueue'):
self._impl = KqueueLoop() self._impl = KqueueLoop()
@ -157,73 +165,72 @@ class EventLoop(object):
else: else:
raise Exception('can not find any available functions in select ' raise Exception('can not find any available functions in select '
'package') 'package')
self._fdmap = {} # (f, handler) self._fd_to_f = {}
self._last_time = time.time() self._handlers = []
self._periodic_callbacks = [] self._ref_handlers = []
self._stopping = False self._handlers_to_remove = []
logging.debug('using event model: %s', model) logging.debug('using event model: %s', model)
def poll(self, timeout=None): def poll(self, timeout=None):
events = self._impl.poll(timeout) events = self._impl.poll(timeout)
return [(self._fdmap[fd][0], fd, event) for fd, event in events] return [(self._fd_to_f[fd], fd, event) for fd, event in events]
def add(self, f, mode, handler): def add(self, f, mode):
fd = f.fileno() fd = f.fileno()
self._fdmap[fd] = (f, handler) self._fd_to_f[fd] = f
self._impl.register(fd, mode) self._impl.add_fd(fd, mode)
def remove(self, f): def remove(self, f):
fd = f.fileno() fd = f.fileno()
del self._fdmap[fd] del self._fd_to_f[fd]
self._impl.unregister(fd) self._impl.remove_fd(fd)
def add_periodic(self, callback):
self._periodic_callbacks.append(callback)
def remove_periodic(self, callback):
self._periodic_callbacks.remove(callback)
def modify(self, f, mode): def modify(self, f, mode):
fd = f.fileno() fd = f.fileno()
self._impl.modify(fd, mode) self._impl.modify_fd(fd, mode)
def stop(self): def add_handler(self, handler, ref=True):
self._stopping = True self._handlers.append(handler)
if ref:
# when all ref handlers are removed, loop stops
self._ref_handlers.append(handler)
def remove_handler(self, handler):
if handler in self._ref_handlers:
self._ref_handlers.remove(handler)
if self._iterating:
self._handlers_to_remove.append(handler)
else:
self._handlers.remove(handler)
def run(self): def run(self):
events = [] events = []
while not self._stopping: while self._ref_handlers:
asap = False
try: try:
events = self.poll(TIMEOUT_PRECISION) events = self.poll(1)
except (OSError, IOError) as e: except (OSError, IOError) as e:
if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): if errno_from_exception(e) in (errno.EPIPE, errno.EINTR):
# EPIPE: Happens when the client closes the connection # EPIPE: Happens when the client closes the connection
# EINTR: Happens when received a signal # EINTR: Happens when received a signal
# handles them as soon as possible # handles them as soon as possible
asap = True
logging.debug('poll:%s', e) logging.debug('poll:%s', e)
else: else:
logging.error('poll:%s', e) logging.error('poll:%s', e)
import traceback
traceback.print_exc() traceback.print_exc()
continue continue
self._iterating = True
for sock, fd, event in events: for handler in self._handlers:
handler = self._fdmap.get(fd, None) # TODO when there are a lot of handlers
if handler is not None: try:
handler = handler[1] handler(events)
try: except (OSError, IOError) as e:
handler.handle_event(sock, fd, event) shell.print_exception(e)
except (OSError, IOError) as e: if self._handlers_to_remove:
shell.print_exception(e) for handler in self._handlers_to_remove:
now = time.time() self._handlers.remove(handler)
if asap or now - self._last_time >= TIMEOUT_PRECISION: self._handlers_to_remove = []
for callback in self._periodic_callbacks: self._iterating = False
callback()
self._last_time = now
def __del__(self):
self._impl.close()
# from tornado # from tornado

View file

@ -27,7 +27,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
@shell.exception_handle(self_=False, exit_code=1)
def main(): def main():
shell.check_python() shell.check_python()
@ -38,31 +37,36 @@ def main():
os.chdir(p) os.chdir(p)
config = shell.get_config(True) config = shell.get_config(True)
daemon.daemon_exec(config) daemon.daemon_exec(config)
logging.info("starting local at %s:%d" % try:
(config['local_address'], config['local_port'])) logging.info("starting local at %s:%d" %
(config['local_address'], config['local_port']))
dns_resolver = asyncdns.DNSResolver() dns_resolver = asyncdns.DNSResolver()
tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) tcp_server = tcprelay.TCPRelay(config, dns_resolver, True)
udp_server = udprelay.UDPRelay(config, dns_resolver, True) udp_server = udprelay.UDPRelay(config, dns_resolver, True)
loop = eventloop.EventLoop() loop = eventloop.EventLoop()
dns_resolver.add_to_loop(loop) dns_resolver.add_to_loop(loop)
tcp_server.add_to_loop(loop) tcp_server.add_to_loop(loop)
udp_server.add_to_loop(loop) udp_server.add_to_loop(loop)
def handler(signum, _): def handler(signum, _):
logging.warn('received SIGQUIT, doing graceful shutting down..') logging.warn('received SIGQUIT, doing graceful shutting down..')
tcp_server.close(next_tick=True) tcp_server.close(next_tick=True)
udp_server.close(next_tick=True) udp_server.close(next_tick=True)
signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler)
def int_handler(signum, _): def int_handler(signum, _):
sys.exit(1)
signal.signal(signal.SIGINT, int_handler)
daemon.set_user(config.get('user', None))
loop.run()
except Exception as e:
shell.print_exception(e)
sys.exit(1) sys.exit(1)
signal.signal(signal.SIGINT, int_handler)
daemon.set_user(config.get('user', None))
loop.run()
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -79,15 +79,18 @@ class LRUCache(collections.MutableMapping):
least = self._last_visits[0] least = self._last_visits[0]
if now - least <= self.timeout: if now - least <= self.timeout:
break break
self._last_visits.popleft() if self.close_callback is not None:
for key in self._time_to_keys[least]: for key in self._time_to_keys[least]:
if key in self._store: if key in self._store:
if now - self._keys_to_last_time[key] > self.timeout: if now - self._keys_to_last_time[key] > self.timeout:
if self.close_callback is not None:
value = self._store[key] value = self._store[key]
if value not in self._closed_values: if value not in self._closed_values:
self.close_callback(value) self.close_callback(value)
self._closed_values.add(value) self._closed_values.add(value)
for key in self._time_to_keys[least]:
self._last_visits.popleft()
if key in self._store:
if now - self._keys_to_last_time[key] > self.timeout:
del self._store[key] del self._store[key]
del self._keys_to_last_time[key] del self._keys_to_last_time[key]
c += 1 c += 1
@ -137,7 +140,6 @@ def test():
c = LRUCache(timeout=0.1, close_callback=close_cb) c = LRUCache(timeout=0.1, close_callback=close_cb)
c['s'] = 1 c['s'] = 1
c['t'] = 1
c['s'] c['s']
time.sleep(0.1) time.sleep(0.1)
c['s'] c['s']

View file

@ -1,293 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 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 errno
import traceback
import socket
import logging
import json
import collections
from shadowsocks import common, eventloop, tcprelay, udprelay, asyncdns, shell
BUF_SIZE = 1506
STAT_SEND_LIMIT = 50
class Manager(object):
def __init__(self, config):
self._config = config
self._relays = {} # (tcprelay, udprelay)
self._loop = eventloop.EventLoop()
self._dns_resolver = asyncdns.DNSResolver()
self._dns_resolver.add_to_loop(self._loop)
self._statistics = collections.defaultdict(int)
self._control_client_addr = None
try:
manager_address = config['manager_address']
if ':' in manager_address:
addr = manager_address.rsplit(':', 1)
addr = addr[0], int(addr[1])
addrs = socket.getaddrinfo(addr[0], addr[1])
if addrs:
family = addrs[0][0]
else:
logging.error('invalid address: %s', manager_address)
exit(1)
else:
addr = manager_address
family = socket.AF_UNIX
self._control_socket = socket.socket(family,
socket.SOCK_DGRAM)
self._control_socket.bind(addr)
self._control_socket.setblocking(False)
except (OSError, IOError) as e:
logging.error(e)
logging.error('can not bind to manager address')
exit(1)
self._loop.add(self._control_socket,
eventloop.POLL_IN, self)
self._loop.add_periodic(self.handle_periodic)
port_password = config['port_password']
del config['port_password']
config['crypto_path'] = config.get('crypto_path', dict())
for port, password in port_password.items():
a_config = config.copy()
a_config['server_port'] = int(port)
a_config['password'] = password
self.add_port(a_config)
def add_port(self, config):
port = int(config['server_port'])
servers = self._relays.get(port, None)
if servers:
logging.error("server already exists at %s:%d" % (config['server'],
port))
return
logging.info("adding server at %s:%d" % (config['server'], port))
t = tcprelay.TCPRelay(config, self._dns_resolver, False,
self.stat_callback)
u = udprelay.UDPRelay(config, self._dns_resolver, False,
self.stat_callback)
t.add_to_loop(self._loop)
u.add_to_loop(self._loop)
self._relays[port] = (t, u)
def remove_port(self, config):
port = int(config['server_port'])
servers = self._relays.get(port, None)
if servers:
logging.info("removing server at %s:%d" % (config['server'], port))
t, u = servers
t.close(next_tick=False)
u.close(next_tick=False)
del self._relays[port]
else:
logging.error("server not exist at %s:%d" % (config['server'],
port))
def handle_event(self, sock, fd, event):
if sock == self._control_socket and event == eventloop.POLL_IN:
data, self._control_client_addr = sock.recvfrom(BUF_SIZE)
parsed = self._parse_command(data)
if parsed:
command, config = parsed
a_config = self._config.copy()
if config:
# let the command override the configuration file
a_config.update(config)
if 'server_port' not in a_config:
logging.error('can not find server_port in config')
else:
if command == 'add':
self.add_port(a_config)
self._send_control_data(b'ok')
elif command == 'remove':
self.remove_port(a_config)
self._send_control_data(b'ok')
elif command == 'ping':
self._send_control_data(b'pong')
else:
logging.error('unknown command %s', command)
def _parse_command(self, data):
# commands:
# add: {"server_port": 8000, "password": "foobar"}
# remove: {"server_port": 8000"}
data = common.to_str(data)
parts = data.split(':', 1)
if len(parts) < 2:
return data, None
command, config_json = parts
try:
config = shell.parse_json_in_str(config_json)
if 'method' in config:
config['method'] = common.to_str(config['method'])
return command, config
except Exception as e:
logging.error(e)
return None
def stat_callback(self, port, data_len):
self._statistics[port] += data_len
def handle_periodic(self):
r = {}
i = 0
def send_data(data_dict):
if data_dict:
# use compact JSON format (without space)
data = common.to_bytes(json.dumps(data_dict,
separators=(',', ':')))
self._send_control_data(b'stat: ' + data)
for k, v in self._statistics.items():
r[k] = v
i += 1
# split the data into segments that fit in UDP packets
if i >= STAT_SEND_LIMIT:
send_data(r)
r.clear()
i = 0
if len(r) > 0:
send_data(r)
self._statistics.clear()
def _send_control_data(self, data):
if not self._control_client_addr:
return
try:
self._control_socket.sendto(data, self._control_client_addr)
except (socket.error, OSError, IOError) as e:
error_no = eventloop.errno_from_exception(e)
if error_no in (errno.EAGAIN, errno.EINPROGRESS,
errno.EWOULDBLOCK):
return
else:
shell.print_exception(e)
if self._config['verbose']:
traceback.print_exc()
def run(self):
self._loop.run()
def run(config):
Manager(config).run()
def test():
import time
import threading
import struct
from shadowsocks import cryptor
logging.basicConfig(level=5,
format='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
enc = []
eventloop.TIMEOUT_PRECISION = 1
def run_server():
config = {
'server': '127.0.0.1',
'local_port': 1081,
'port_password': {
'8381': 'foobar1',
'8382': 'foobar2'
},
'method': 'aes-256-cfb',
'manager_address': '127.0.0.1:6001',
'timeout': 60,
'fast_open': False,
'verbose': 2
}
manager = Manager(config)
enc.append(manager)
manager.run()
t = threading.Thread(target=run_server)
t.start()
time.sleep(1)
manager = enc[0]
cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
cli.connect(('127.0.0.1', 6001))
# test add and remove
time.sleep(1)
cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}')
time.sleep(1)
assert 7001 in manager._relays
data, addr = cli.recvfrom(1506)
assert b'ok' in data
cli.send(b'remove: {"server_port":8381}')
time.sleep(1)
assert 8381 not in manager._relays
data, addr = cli.recvfrom(1506)
assert b'ok' in data
logging.info('add and remove test passed')
# test statistics for TCP
header = common.pack_addr(b'google.com') + struct.pack('>H', 80)
data = cryptor.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb',
header + b'GET /\r\n\r\n')
tcp_cli = socket.socket()
tcp_cli.connect(('127.0.0.1', 7001))
tcp_cli.send(data)
tcp_cli.recv(4096)
tcp_cli.close()
data, addr = cli.recvfrom(1506)
data = common.to_str(data)
assert data.startswith('stat: ')
data = data.split('stat:')[1]
stats = shell.parse_json_in_str(data)
assert '7001' in stats
logging.info('TCP statistics test passed')
# test statistics for UDP
header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80)
data = cryptor.encrypt_all(b'foobar2', 'aes-256-cfb',
header + b'test')
udp_cli = socket.socket(type=socket.SOCK_DGRAM)
udp_cli.sendto(data, ('127.0.0.1', 8382))
tcp_cli.close()
data, addr = cli.recvfrom(1506)
data = common.to_str(data)
assert data.startswith('stat: ')
data = data.split('stat:')[1]
stats = json.loads(data)
assert '8382' in stats
logging.info('UDP statistics test passed')
manager._loop.stop()
t.join()
if __name__ == '__main__':
test()

View file

@ -24,8 +24,7 @@ import logging
import signal import signal
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
asyncdns, manager
def main(): def main():
@ -49,23 +48,10 @@ def main():
else: else:
config['port_password'][str(server_port)] = config['password'] config['port_password'][str(server_port)] = config['password']
if config.get('manager_address', 0):
logging.info('entering manager mode')
manager.run(config)
return
tcp_servers = [] tcp_servers = []
udp_servers = [] udp_servers = []
dns_resolver = asyncdns.DNSResolver()
if 'dns_server' in config: # allow override settings in resolv.conf for port, password in config['port_password'].items():
dns_resolver = asyncdns.DNSResolver(config['dns_server'],
config['prefer_ipv6'])
else:
dns_resolver = asyncdns.DNSResolver(prefer_ipv6=config['prefer_ipv6'])
port_password = config['port_password']
del config['port_password']
for port, password in port_password.items():
a_config = config.copy() a_config = config.copy()
a_config['server_port'] = int(port) a_config['server_port'] = int(port)
a_config['password'] = password a_config['password'] = password

View file

@ -23,12 +23,8 @@ import json
import sys import sys
import getopt import getopt
import logging import logging
import traceback
from functools import wraps
from shadowsocks.common import to_bytes, to_str, IPNetwork from shadowsocks.common import to_bytes, to_str, IPNetwork
from shadowsocks import cryptor from shadowsocks import encrypt
VERBOSE_LEVEL = 5 VERBOSE_LEVEL = 5
@ -57,49 +53,6 @@ def print_exception(e):
traceback.print_exc() traceback.print_exc()
def exception_handle(self_, err_msg=None, exit_code=None,
destroy=False, conn_err=False):
# self_: if function passes self as first arg
def process_exception(e, self=None):
print_exception(e)
if err_msg:
logging.error(err_msg)
if exit_code:
sys.exit(1)
if not self_:
return
if conn_err:
addr, port = self._client_address[0], self._client_address[1]
logging.error('%s when handling connection from %s:%d' %
(e, addr, port))
if self._config['verbose']:
traceback.print_exc()
if destroy:
self.destroy()
def decorator(func):
if self_:
@wraps(func)
def wrapper(self, *args, **kwargs):
try:
func(self, *args, **kwargs)
except Exception as e:
process_exception(e, self)
else:
@wraps(func)
def wrapper(*args, **kwargs):
try:
func(*args, **kwargs)
except Exception as e:
process_exception(e)
return wrapper
return decorator
def print_shadowsocks(): def print_shadowsocks():
version = '' version = ''
try: try:
@ -125,37 +78,13 @@ def check_config(config, is_local):
# no need to specify configuration for daemon stop # no need to specify configuration for daemon stop
return return
if is_local:
if config.get('server', None) is None:
logging.error('server addr not specified')
print_local_help()
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:
config['forbidden_ip'] = \
IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128'))
except Exception as e:
logging.error(e)
sys.exit(2)
if is_local and not config.get('password', None): if is_local and not config.get('password', None):
logging.error('password not specified') logging.error('password not specified')
print_help(is_local) print_help(is_local)
sys.exit(2) sys.exit(2)
if not is_local and not config.get('password', None) \ if not is_local and not config.get('password', None) \
and not config.get('port_password', None) \ and not config.get('port_password', None):
and not config.get('manager_address'):
logging.error('password or port_password not specified') logging.error('password or port_password not specified')
print_help(is_local) print_help(is_local)
sys.exit(2) sys.exit(2)
@ -166,11 +95,6 @@ def check_config(config, is_local):
if 'server_port' in config and type(config['server_port']) != list: if 'server_port' in config and type(config['server_port']) != list:
config['server_port'] = int(config['server_port']) 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']: 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') 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']: if config.get('server', '') in ['127.0.0.1', 'localhost']:
@ -196,19 +120,8 @@ def check_config(config, is_local):
if os.name != 'posix': if os.name != 'posix':
logging.error('user can be used only on Unix') logging.error('user can be used only on Unix')
sys.exit(1) sys.exit(1)
if config.get('dns_server', None) is not None:
if type(config['dns_server']) != list:
config['dns_server'] = to_str(config['dns_server'])
else:
config['dns_server'] = [to_str(ds) for ds in config['dns_server']]
logging.info('Specified DNS server: %s' % config['dns_server'])
config['crypto_path'] = {'openssl': config['libopenssl'], encrypt.try_cipher(config['password'], config['method'])
'mbedtls': config['libmbedtls'],
'sodium': config['libsodium']}
cryptor.try_cipher(config['password'], config['method'],
config['crypto_path'])
def get_config(is_local): def get_config(is_local):
@ -217,14 +130,13 @@ def get_config(is_local):
logging.basicConfig(level=logging.INFO, logging.basicConfig(level=logging.INFO,
format='%(levelname)-s: %(message)s') format='%(levelname)-s: %(message)s')
if is_local: if is_local:
shortopts = 'hd:s:b:p:k:l:m:c:t:vqa' shortopts = 'hd:s:b:p:k:l:m:c:t:vq'
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=', longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=',
'libopenssl=', 'libmbedtls=', 'libsodium=', 'version'] 'version']
else: else:
shortopts = 'hd:s:p:k:m:c:t:vqa' shortopts = 'hd:s:p:k:m:c:t:vq'
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=', longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
'forbidden-ip=', 'user=', 'manager-address=', 'version', 'forbidden-ip=', 'user=', 'version']
'libopenssl=', 'libmbedtls=', 'libsodium=', 'prefer-ipv6']
try: try:
config_path = find_config() config_path = find_config()
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
@ -236,7 +148,8 @@ def get_config(is_local):
logging.info('loading config from %s' % config_path) logging.info('loading config from %s' % config_path)
with open(config_path, 'rb') as f: with open(config_path, 'rb') as f:
try: try:
config = parse_json_in_str(f.read().decode('utf8')) config = json.loads(f.read().decode('utf8'),
object_hook=_decode_dict)
except ValueError as e: except ValueError as e:
logging.error('found an error in config.json: %s', logging.error('found an error in config.json: %s',
e.message) e.message)
@ -262,22 +175,12 @@ def get_config(is_local):
v_count += 1 v_count += 1
# '-vv' turns on more verbose mode # '-vv' turns on more verbose mode
config['verbose'] = v_count config['verbose'] = v_count
elif key == '-a':
config['one_time_auth'] = True
elif key == '-t': elif key == '-t':
config['timeout'] = int(value) config['timeout'] = int(value)
elif key == '--fast-open': elif key == '--fast-open':
config['fast_open'] = True config['fast_open'] = True
elif key == '--libopenssl':
config['libopenssl'] = to_str(value)
elif key == '--libmbedtls':
config['libmbedtls'] = to_str(value)
elif key == '--libsodium':
config['libsodium'] = to_str(value)
elif key == '--workers': elif key == '--workers':
config['workers'] = int(value) config['workers'] = int(value)
elif key == '--manager-address':
config['manager_address'] = to_str(value)
elif key == '--user': elif key == '--user':
config['user'] = to_str(value) config['user'] = to_str(value)
elif key == '--forbidden-ip': elif key == '--forbidden-ip':
@ -300,8 +203,6 @@ def get_config(is_local):
elif key == '-q': elif key == '-q':
v_count -= 1 v_count -= 1
config['verbose'] = v_count config['verbose'] = v_count
elif key == '--prefer-ipv6':
config['prefer_ipv6'] = True
except getopt.GetoptError as e: except getopt.GetoptError as e:
print(e, file=sys.stderr) print(e, file=sys.stderr)
print_help(is_local) print_help(is_local)
@ -323,17 +224,22 @@ def get_config(is_local):
config['verbose'] = config.get('verbose', False) config['verbose'] = config.get('verbose', False)
config['local_address'] = to_str(config.get('local_address', '127.0.0.1')) config['local_address'] = to_str(config.get('local_address', '127.0.0.1'))
config['local_port'] = config.get('local_port', 1080) config['local_port'] = config.get('local_port', 1080)
config['one_time_auth'] = config.get('one_time_auth', False) if is_local:
config['prefer_ipv6'] = config.get('prefer_ipv6', False) if config.get('server', None) is None:
logging.error('server addr not specified')
print_local_help()
sys.exit(2)
else:
config['server'] = to_str(config['server'])
else:
config['server'] = to_str(config.get('server', '0.0.0.0'))
try:
config['forbidden_ip'] = \
IPNetwork(config.get('forbidden_ip', '127.0.0.0/8,::1/128'))
except Exception as e:
logging.error(e)
sys.exit(2)
config['server_port'] = config.get('server_port', 8388) config['server_port'] = config.get('server_port', 8388)
config['dns_server'] = config.get('dns_server', None)
config['libopenssl'] = config.get('libopenssl', None)
config['libmbedtls'] = config.get('libmbedtls', None)
config['libsodium'] = config.get('libsodium', None)
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)
logging.getLogger('').handlers = [] logging.getLogger('').handlers = []
logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE')
@ -378,40 +284,15 @@ Proxy options:
-l LOCAL_PORT local port, default: 1080 -l LOCAL_PORT local port, default: 1080
-k PASSWORD password -k PASSWORD password
-m METHOD encryption method, default: aes-256-cfb -m METHOD encryption method, default: aes-256-cfb
Sodium:
chacha20-poly1305, chacha20-ietf-poly1305,
xchacha20-ietf-poly1305,
sodium:aes-256-gcm,
salsa20, chacha20, chacha20-ietf.
Sodium 1.0.12:
xchacha20
OpenSSL:
aes-{128|192|256}-gcm, aes-{128|192|256}-cfb,
aes-{128|192|256}-ofb, aes-{128|192|256}-ctr,
camellia-{128|192|256}-cfb,
bf-cfb, cast5-cfb, des-cfb, idea-cfb,
rc2-cfb, seed-cfb,
rc4, rc4-md5, table.
OpenSSL 1.1:
aes-{128|192|256}-ocb
mbedTLS:
mbedtls:aes-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-ctr,
mbedtls:camellia-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-gcm
-t TIMEOUT timeout in seconds, default: 300 -t TIMEOUT timeout in seconds, default: 300
-a ONE_TIME_AUTH one time auth
--fast-open use TCP_FASTOPEN, requires Linux 3.7+ --fast-open use TCP_FASTOPEN, requires Linux 3.7+
--libopenssl=PATH custom openssl crypto lib path
--libmbedtls=PATH custom mbedtls crypto lib path
--libsodium=PATH custom sodium crypto lib path
General options: General options:
-h, --help show this help message and exit -h, --help show this help message and exit
-d start/stop/restart daemon mode -d start/stop/restart daemon mode
--pid-file=PID_FILE pid file for daemon mode --pid-file PID_FILE pid file for daemon mode
--log-file=LOG_FILE log file for daemon mode --log-file LOG_FILE log file for daemon mode
--user=USER username to run as --user USER username to run as
-v, -vv verbose mode -v, -vv verbose mode
-q, -qq quiet mode, only show warnings/errors -q, -qq quiet mode, only show warnings/errors
--version show version information --version show version information
@ -432,37 +313,10 @@ Proxy options:
-p SERVER_PORT server port, default: 8388 -p SERVER_PORT server port, default: 8388
-k PASSWORD password -k PASSWORD password
-m METHOD encryption method, default: aes-256-cfb -m METHOD encryption method, default: aes-256-cfb
Sodium:
chacha20-poly1305, chacha20-ietf-poly1305,
xchacha20-ietf-poly1305,
sodium:aes-256-gcm,
salsa20, chacha20, chacha20-ietf.
Sodium 1.0.12:
xchacha20
OpenSSL:
aes-{128|192|256}-gcm, aes-{128|192|256}-cfb,
aes-{128|192|256}-ofb, aes-{128|192|256}-ctr,
camellia-{128|192|256}-cfb,
bf-cfb, cast5-cfb, des-cfb, idea-cfb,
rc2-cfb, seed-cfb,
rc4, rc4-md5, table.
OpenSSL 1.1:
aes-{128|192|256}-ocb
mbedTLS:
mbedtls:aes-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-ctr,
mbedtls:camellia-{128|192|256}-cfb128,
mbedtls:aes-{128|192|256}-gcm
-t TIMEOUT timeout in seconds, default: 300 -t TIMEOUT timeout in seconds, default: 300
-a ONE_TIME_AUTH one time auth
--fast-open use TCP_FASTOPEN, requires Linux 3.7+ --fast-open use TCP_FASTOPEN, requires Linux 3.7+
--workers=WORKERS number of workers, available on Unix/Linux --workers WORKERS number of workers, available on Unix/Linux
--forbidden-ip=IPLIST comma seperated IP list forbidden to connect --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
--libopenssl=PATH custom openssl crypto lib path
--libmbedtls=PATH custom mbedtls crypto lib path
--libsodium=PATH custom sodium crypto lib path
General options: General options:
-h, --help show this help message and exit -h, --help show this help message and exit
@ -502,8 +356,3 @@ def _decode_dict(data):
value = _decode_dict(value) value = _decode_dict(value)
rv[key] = value rv[key] = value
return rv return rv
def parse_json_in_str(data):
# parse json and convert everything from unicode to str
return json.loads(data, object_hook=_decode_dict)

View file

@ -26,18 +26,16 @@ import logging
import traceback import traceback
import random import random
from shadowsocks import cryptor, eventloop, shell, common from shadowsocks import encrypt, eventloop, shell, common
from shadowsocks.common import parse_header, onetimeauth_verify, \ from shadowsocks.common import parse_header
onetimeauth_gen, ONETIMEAUTH_BYTES, ONETIMEAUTH_CHUNK_BYTES, \
ONETIMEAUTH_CHUNK_DATA_LEN, ADDRTYPE_AUTH
# we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time # we clear at most TIMEOUTS_CLEAN_SIZE timeouts each time
TIMEOUTS_CLEAN_SIZE = 512 TIMEOUTS_CLEAN_SIZE = 512
MSG_FASTOPEN = 0x20000000 # we check timeouts every TIMEOUT_PRECISION seconds
TIMEOUT_PRECISION = 4
# SOCKS METHOD definition MSG_FASTOPEN = 0x20000000
METHOD_NOAUTH = 0
# SOCKS command definition # SOCKS command definition
CMD_CONNECT = 1 CMD_CONNECT = 1
@ -55,7 +53,7 @@ CMD_UDP_ASSOCIATE = 3
# for each handler, it could be at one of several stages: # for each handler, it could be at one of several stages:
# as sslocal: # as sslocal:
# stage 0 auth METHOD received from local, reply with selection message # stage 0 SOCKS hello received from local, send hello to local
# stage 1 addr received from local, query DNS for remote # stage 1 addr received from local, query DNS for remote
# stage 2 UDP assoc # stage 2 UDP assoc
# stage 3 DNS resolved, connect to remote # stage 3 DNS resolved, connect to remote
@ -93,22 +91,9 @@ WAIT_STATUS_WRITING = 2
WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING WAIT_STATUS_READWRITING = WAIT_STATUS_READING | WAIT_STATUS_WRITING
BUF_SIZE = 32 * 1024 BUF_SIZE = 32 * 1024
UP_STREAM_BUF_SIZE = 16 * 1024
DOWN_STREAM_BUF_SIZE = 32 * 1024
# helper exceptions for TCPRelayHandler
class BadSocksHeader(Exception):
pass
class NoAcceptableMethods(Exception):
pass
class TCPRelayHandler(object): class TCPRelayHandler(object):
def __init__(self, server, fd_to_handlers, loop, local_sock, config, def __init__(self, server, fd_to_handlers, loop, local_sock, config,
dns_resolver, is_local): dns_resolver, is_local):
self._server = server self._server = server
@ -118,24 +103,13 @@ class TCPRelayHandler(object):
self._remote_sock = None self._remote_sock = None
self._config = config self._config = config
self._dns_resolver = dns_resolver 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 # TCP Relay works as either sslocal or ssserver
# if is_local, this is sslocal # if is_local, this is sslocal
self._is_local = is_local self._is_local = is_local
self._stage = STAGE_INIT self._stage = STAGE_INIT
self._cryptor = cryptor.Cryptor(config['password'], self._encryptor = encrypt.Encryptor(config['password'],
config['method'], config['method'])
config['crypto_path'])
self._ota_enable = config.get('one_time_auth', False)
self._ota_enable_session = self._ota_enable
self._ota_buff_head = b''
self._ota_buff_data = b''
self._ota_len = 0
self._ota_chunk_idx = 0
self._fastopen_connected = False self._fastopen_connected = False
self._data_to_write_to_local = [] self._data_to_write_to_local = []
self._data_to_write_to_remote = [] self._data_to_write_to_remote = []
@ -143,14 +117,16 @@ class TCPRelayHandler(object):
self._downstream_status = WAIT_STATUS_INIT self._downstream_status = WAIT_STATUS_INIT
self._client_address = local_sock.getpeername()[:2] self._client_address = local_sock.getpeername()[:2]
self._remote_address = None self._remote_address = None
self._forbidden_iplist = config.get('forbidden_ip') if 'forbidden_ip' in config:
self._forbidden_iplist = config['forbidden_ip']
else:
self._forbidden_iplist = None
if is_local: if is_local:
self._chosen_server = self._get_a_server() self._chosen_server = self._get_a_server()
fd_to_handlers[local_sock.fileno()] = self fd_to_handlers[local_sock.fileno()] = self
local_sock.setblocking(False) local_sock.setblocking(False)
local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) local_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR, loop.add(local_sock, eventloop.POLL_IN | eventloop.POLL_ERR)
self._server)
self.last_activity = 0 self.last_activity = 0
self._update_activity() self._update_activity()
@ -173,10 +149,10 @@ class TCPRelayHandler(object):
logging.debug('chosen server: %s:%d', server, server_port) logging.debug('chosen server: %s:%d', server, server_port)
return server, server_port return server, server_port
def _update_activity(self, data_len=0): def _update_activity(self):
# tell the TCP Relay we have activities recently # tell the TCP Relay we have activities recently
# else it will think we are inactive and timed out # else it will think we are inactive and timed out
self._server.update_activity(self, data_len) self._server.update_activity(self)
def _update_stream(self, stream, status): def _update_stream(self, stream, status):
# update a stream to a new waiting status # update a stream to a new waiting status
@ -192,23 +168,21 @@ class TCPRelayHandler(object):
if self._upstream_status != status: if self._upstream_status != status:
self._upstream_status = status self._upstream_status = status
dirty = True dirty = True
if not dirty: if dirty:
return if self._local_sock:
event = eventloop.POLL_ERR
if self._local_sock: if self._downstream_status & WAIT_STATUS_WRITING:
event = eventloop.POLL_ERR event |= eventloop.POLL_OUT
if self._downstream_status & WAIT_STATUS_WRITING: if self._upstream_status & WAIT_STATUS_READING:
event |= eventloop.POLL_OUT event |= eventloop.POLL_IN
if self._upstream_status & WAIT_STATUS_READING: self._loop.modify(self._local_sock, event)
event |= eventloop.POLL_IN if self._remote_sock:
self._loop.modify(self._local_sock, event) event = eventloop.POLL_ERR
if self._remote_sock: if self._downstream_status & WAIT_STATUS_READING:
event = eventloop.POLL_ERR event |= eventloop.POLL_IN
if self._downstream_status & WAIT_STATUS_READING: if self._upstream_status & WAIT_STATUS_WRITING:
event |= eventloop.POLL_IN event |= eventloop.POLL_OUT
if self._upstream_status & WAIT_STATUS_WRITING: self._loop.modify(self._remote_sock, event)
event |= eventloop.POLL_OUT
self._loop.modify(self._remote_sock, event)
def _write_to_sock(self, data, sock): def _write_to_sock(self, data, sock):
# write data to sock # write data to sock
@ -251,19 +225,11 @@ class TCPRelayHandler(object):
return True return True
def _handle_stage_connecting(self, data): def _handle_stage_connecting(self, data):
if not self._is_local: if self._is_local:
if self._ota_enable_session: data = self._encryptor.encrypt(data)
self._ota_chunk_data(data,
self._data_to_write_to_remote.append)
else:
self._data_to_write_to_remote.append(data)
return
if self._ota_enable_session:
data = self._ota_chunk_data_gen(data)
data = self._cryptor.encrypt(data)
self._data_to_write_to_remote.append(data) self._data_to_write_to_remote.append(data)
if self._is_local and not self._fastopen_connected and \
if self._config['fast_open'] and not self._fastopen_connected: self._config['fast_open']:
# for sslocal and fastopen, we basically wait for data and use # for sslocal and fastopen, we basically wait for data and use
# sendto to connect # sendto to connect
try: try:
@ -272,11 +238,10 @@ class TCPRelayHandler(object):
remote_sock = \ remote_sock = \
self._create_remote_socket(self._chosen_server[0], self._create_remote_socket(self._chosen_server[0],
self._chosen_server[1]) self._chosen_server[1])
self._loop.add(remote_sock, eventloop.POLL_ERR, self._server) self._loop.add(remote_sock, eventloop.POLL_ERR)
data = b''.join(self._data_to_write_to_remote) data = b''.join(self._data_to_write_to_remote)
l = len(data) l = len(data)
s = remote_sock.sendto(data, MSG_FASTOPEN, s = remote_sock.sendto(data, MSG_FASTOPEN, self._chosen_server)
self._chosen_server)
if s < l: if s < l:
data = data[s:] data = data[s:]
self._data_to_write_to_remote = [data] self._data_to_write_to_remote = [data]
@ -297,16 +262,9 @@ class TCPRelayHandler(object):
traceback.print_exc() traceback.print_exc()
self.destroy() self.destroy()
@shell.exception_handle(self_=True, destroy=True, conn_err=True)
def _handle_stage_addr(self, data): def _handle_stage_addr(self, data):
if self._is_local: try:
if self._is_tunnel: if self._is_local:
# 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:
cmd = common.ord(data[1]) cmd = common.ord(data[1])
if cmd == CMD_UDP_ASSOCIATE: if cmd == CMD_UDP_ASSOCIATE:
logging.debug('UDP associate') logging.debug('UDP associate')
@ -330,66 +288,39 @@ class TCPRelayHandler(object):
logging.error('unknown command %d', cmd) logging.error('unknown command %d', cmd)
self.destroy() self.destroy()
return return
header_result = parse_header(data) header_result = parse_header(data)
if header_result is None: if header_result is None:
raise Exception('can not parse header') raise Exception('can not parse header')
addrtype, remote_addr, remote_port, header_length = header_result addrtype, remote_addr, remote_port, header_length = header_result
logging.info('connecting %s:%d from %s:%d' % logging.info('connecting %s:%d from %s:%d' %
(common.to_str(remote_addr), remote_port, (common.to_str(remote_addr), remote_port,
self._client_address[0], self._client_address[1])) self._client_address[0], self._client_address[1]))
if self._is_local is False: self._remote_address = (common.to_str(remote_addr), remote_port)
# spec https://shadowsocks.org/en/spec/one-time-auth.html # pause reading
self._ota_enable_session = addrtype & ADDRTYPE_AUTH self._update_stream(STREAM_UP, WAIT_STATUS_WRITING)
if self._ota_enable and not self._ota_enable_session: self._stage = STAGE_DNS
logging.warn('client one time auth is required') if self._is_local:
return
if self._ota_enable_session:
if len(data) < header_length + ONETIMEAUTH_BYTES:
logging.warn('one time auth header is too short')
return None
offset = header_length + ONETIMEAUTH_BYTES
_hash = data[header_length: offset]
_data = data[:header_length]
key = self._cryptor.decipher_iv + self._cryptor.key
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
self._update_stream(STREAM_UP, WAIT_STATUS_WRITING)
self._stage = STAGE_DNS
if self._is_local:
# jump over socks5 response
if not self._is_tunnel:
# forward address to remote # forward address to remote
self._write_to_sock((b'\x05\x00\x00\x01' self._write_to_sock((b'\x05\x00\x00\x01'
b'\x00\x00\x00\x00\x10\x10'), b'\x00\x00\x00\x00\x10\x10'),
self._local_sock) self._local_sock)
# spec https://shadowsocks.org/en/spec/one-time-auth.html data_to_send = self._encryptor.encrypt(data)
# ATYP & 0x10 == 0x10, then OTA is enabled. self._data_to_write_to_remote.append(data_to_send)
if self._ota_enable_session: # notice here may go into _handle_dns_resolved directly
data = common.chr(addrtype | ADDRTYPE_AUTH) + data[1:] self._dns_resolver.resolve(self._chosen_server[0],
key = self._cryptor.cipher_iv + self._cryptor.key self._handle_dns_resolved)
_header = data[:header_length] else:
sha110 = onetimeauth_gen(data, key) if len(data) > header_length:
data = _header + sha110 + data[header_length:] self._data_to_write_to_remote.append(data[header_length:])
data_to_send = self._cryptor.encrypt(data) # notice here may go into _handle_dns_resolved directly
self._data_to_write_to_remote.append(data_to_send) self._dns_resolver.resolve(remote_addr,
# notice here may go into _handle_dns_resolved directly self._handle_dns_resolved)
self._dns_resolver.resolve(self._chosen_server[0], except Exception as e:
self._handle_dns_resolved) self._log_error(e)
else: if self._config['verbose']:
if self._ota_enable_session: traceback.print_exc()
data = data[header_length:] # TODO use logging when debug completed
self._ota_chunk_data(data, self.destroy()
self._data_to_write_to_remote.append)
elif len(data) > header_length:
self._data_to_write_to_remote.append(data[header_length:])
# notice here may go into _handle_dns_resolved directly
self._dns_resolver.resolve(remote_addr,
self._handle_dns_resolved)
def _create_remote_socket(self, ip, port): def _create_remote_socket(self, ip, port):
addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM,
@ -408,160 +339,63 @@ class TCPRelayHandler(object):
remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
return remote_sock return remote_sock
@shell.exception_handle(self_=True)
def _handle_dns_resolved(self, result, error): def _handle_dns_resolved(self, result, error):
if error: if error:
addr, port = self._client_address[0], self._client_address[1] self._log_error(error)
logging.error('%s when handling connection from %s:%d' %
(error, addr, port))
self.destroy()
return
if not (result and result[1]):
self.destroy() self.destroy()
return return
if result:
ip = result[1]
if ip:
ip = result[1] try:
self._stage = STAGE_CONNECTING self._stage = STAGE_CONNECTING
remote_addr = ip remote_addr = ip
if self._is_local: if self._is_local:
remote_port = self._chosen_server[1] remote_port = self._chosen_server[1]
else: else:
remote_port = self._remote_address[1] remote_port = self._remote_address[1]
if self._is_local and self._config['fast_open']: if self._is_local and self._config['fast_open']:
# for fastopen: # for fastopen:
# wait for more data arrive and send them in one SYN # wait for more data to arrive and send them in one SYN
self._stage = STAGE_CONNECTING self._stage = STAGE_CONNECTING
# we don't have to wait for remote since it's not # we don't have to wait for remote since it's not
# created # created
self._update_stream(STREAM_UP, WAIT_STATUS_READING) self._update_stream(STREAM_UP, WAIT_STATUS_READING)
# TODO when there is already data in this packet # TODO when there is already data in this packet
else: else:
# else do connect # else do connect
remote_sock = self._create_remote_socket(remote_addr, remote_sock = self._create_remote_socket(remote_addr,
remote_port) remote_port)
try: try:
remote_sock.connect((remote_addr, remote_port)) remote_sock.connect((remote_addr, remote_port))
except (OSError, IOError) as e: except (OSError, IOError) as e:
if eventloop.errno_from_exception(e) == \ if eventloop.errno_from_exception(e) == \
errno.EINPROGRESS: errno.EINPROGRESS:
pass pass
self._loop.add(remote_sock, self._loop.add(remote_sock,
eventloop.POLL_ERR | eventloop.POLL_OUT, eventloop.POLL_ERR | eventloop.POLL_OUT)
self._server) self._stage = STAGE_CONNECTING
self._stage = STAGE_CONNECTING self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING)
self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) self._update_stream(STREAM_DOWN, WAIT_STATUS_READING)
self._update_stream(STREAM_DOWN, WAIT_STATUS_READING)
def _write_to_sock_remote(self, data):
self._write_to_sock(data, self._remote_sock)
def _ota_chunk_data(self, data, data_cb):
# spec https://shadowsocks.org/en/spec/one-time-auth.html
unchunk_data = b''
while len(data) > 0:
if self._ota_len == 0:
# get DATA.LEN + HMAC-SHA1
length = ONETIMEAUTH_CHUNK_BYTES - len(self._ota_buff_head)
self._ota_buff_head += data[:length]
data = data[length:]
if len(self._ota_buff_head) < ONETIMEAUTH_CHUNK_BYTES:
# wait more data
return return
data_len = self._ota_buff_head[:ONETIMEAUTH_CHUNK_DATA_LEN] except Exception as e:
self._ota_len = struct.unpack('>H', data_len)[0] shell.print_exception(e)
length = min(self._ota_len - len(self._ota_buff_data), len(data)) if self._config['verbose']:
self._ota_buff_data += data[:length] traceback.print_exc()
data = data[length:] self.destroy()
if len(self._ota_buff_data) == self._ota_len:
# get a chunk data
_hash = self._ota_buff_head[ONETIMEAUTH_CHUNK_DATA_LEN:]
_data = self._ota_buff_data
index = struct.pack('>I', self._ota_chunk_idx)
key = self._cryptor.decipher_iv + index
if onetimeauth_verify(_hash, _data, key) is False:
logging.warn('one time auth fail, drop chunk !')
else:
unchunk_data += _data
self._ota_chunk_idx += 1
self._ota_buff_head = b''
self._ota_buff_data = b''
self._ota_len = 0
data_cb(unchunk_data)
return
def _ota_chunk_data_gen(self, data):
data_len = struct.pack(">H", len(data))
index = struct.pack('>I', self._ota_chunk_idx)
key = self._cryptor.cipher_iv + index
sha110 = onetimeauth_gen(data, key)
self._ota_chunk_idx += 1
return data_len + sha110 + data
def _handle_stage_stream(self, data):
if self._is_local:
if self._ota_enable_session:
data = self._ota_chunk_data_gen(data)
data = self._cryptor.encrypt(data)
self._write_to_sock(data, self._remote_sock)
else:
if self._ota_enable_session:
self._ota_chunk_data(data, self._write_to_sock_remote)
else:
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): def _on_local_read(self):
# handle all local read events and dispatch them to methods for # handle all local read events and dispatch them to methods for
# each stage # each stage
self._update_activity()
if not self._local_sock: if not self._local_sock:
return return
is_local = self._is_local is_local = self._is_local
data = None data = None
if is_local:
buf_size = UP_STREAM_BUF_SIZE
else:
buf_size = DOWN_STREAM_BUF_SIZE
try: try:
data = self._local_sock.recv(buf_size) data = self._local_sock.recv(BUF_SIZE)
except (OSError, IOError) as e: except (OSError, IOError) as e:
if eventloop.errno_from_exception(e) in \ if eventloop.errno_from_exception(e) in \
(errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK):
@ -569,21 +403,20 @@ class TCPRelayHandler(object):
if not data: if not data:
self.destroy() self.destroy()
return return
self._update_activity(len(data))
if not is_local: if not is_local:
data = self._cryptor.decrypt(data) data = self._encryptor.decrypt(data)
if not data: if not data:
return return
if self._stage == STAGE_STREAM: if self._stage == STAGE_STREAM:
self._handle_stage_stream(data) if self._is_local:
data = self._encryptor.encrypt(data)
self._write_to_sock(data, self._remote_sock)
return return
elif is_local and self._stage == STAGE_INIT: elif is_local and self._stage == STAGE_INIT:
# jump over socks5 init # TODO check auth method
if self._is_tunnel: self._write_to_sock(b'\x05\00', self._local_sock)
self._handle_stage_addr(data) self._stage = STAGE_ADDR
return return
else:
self._handle_stage_init(data)
elif self._stage == STAGE_CONNECTING: elif self._stage == STAGE_CONNECTING:
self._handle_stage_connecting(data) self._handle_stage_connecting(data)
elif (is_local and self._stage == STAGE_ADDR) or \ elif (is_local and self._stage == STAGE_ADDR) or \
@ -592,14 +425,10 @@ class TCPRelayHandler(object):
def _on_remote_read(self): def _on_remote_read(self):
# handle all remote read events # handle all remote read events
self._update_activity()
data = None data = None
if self._is_local:
buf_size = UP_STREAM_BUF_SIZE
else:
buf_size = DOWN_STREAM_BUF_SIZE
try: try:
data = self._remote_sock.recv(buf_size) data = self._remote_sock.recv(BUF_SIZE)
except (OSError, IOError) as e: except (OSError, IOError) as e:
if eventloop.errno_from_exception(e) in \ if eventloop.errno_from_exception(e) in \
(errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK):
@ -607,11 +436,10 @@ class TCPRelayHandler(object):
if not data: if not data:
self.destroy() self.destroy()
return return
self._update_activity(len(data))
if self._is_local: if self._is_local:
data = self._cryptor.decrypt(data) data = self._encryptor.decrypt(data)
else: else:
data = self._cryptor.encrypt(data) data = self._encryptor.encrypt(data)
try: try:
self._write_to_sock(data, self._local_sock) self._write_to_sock(data, self._local_sock)
except Exception as e: except Exception as e:
@ -652,7 +480,6 @@ class TCPRelayHandler(object):
logging.error(eventloop.get_sock_error(self._remote_sock)) logging.error(eventloop.get_sock_error(self._remote_sock))
self.destroy() self.destroy()
@shell.exception_handle(self_=True, destroy=True)
def handle_event(self, sock, event): def handle_event(self, sock, event):
# handle all events in this handler and dispatch them to methods # handle all events in this handler and dispatch them to methods
if self._stage == STAGE_DESTROYED: if self._stage == STAGE_DESTROYED:
@ -684,6 +511,10 @@ class TCPRelayHandler(object):
else: else:
logging.warn('unknown socket') logging.warn('unknown socket')
def _log_error(self, e):
logging.error('%s when handling connection from %s:%d' %
(e, self._client_address[0], self._client_address[1]))
def destroy(self): def destroy(self):
# destroy the handler and release any resources # destroy the handler and release any resources
# promises: # promises:
@ -719,15 +550,14 @@ class TCPRelayHandler(object):
class TCPRelay(object): class TCPRelay(object):
def __init__(self, config, dns_resolver, is_local):
def __init__(self, config, dns_resolver, is_local, stat_callback=None):
self._config = config self._config = config
self._is_local = is_local self._is_local = is_local
self._dns_resolver = dns_resolver self._dns_resolver = dns_resolver
self._closed = False self._closed = False
self._eventloop = None self._eventloop = None
self._fd_to_handlers = {} self._fd_to_handlers = {}
self._is_tunnel = False self._last_time = time.time()
self._timeout = config['timeout'] self._timeout = config['timeout']
self._timeouts = [] # a list for all the handlers self._timeouts = [] # a list for all the handlers
@ -761,7 +591,6 @@ class TCPRelay(object):
self._config['fast_open'] = False self._config['fast_open'] = False
server_socket.listen(1024) server_socket.listen(1024)
self._server_socket = server_socket self._server_socket = server_socket
self._stat_callback = stat_callback
def add_to_loop(self, loop): def add_to_loop(self, loop):
if self._eventloop: if self._eventloop:
@ -769,9 +598,10 @@ class TCPRelay(object):
if self._closed: if self._closed:
raise Exception('already closed') raise Exception('already closed')
self._eventloop = loop self._eventloop = loop
loop.add_handler(self._handle_events)
self._eventloop.add(self._server_socket, self._eventloop.add(self._server_socket,
eventloop.POLL_IN | eventloop.POLL_ERR, self) eventloop.POLL_IN | eventloop.POLL_ERR)
self._eventloop.add_periodic(self.handle_periodic)
def remove_handler(self, handler): def remove_handler(self, handler):
index = self._handler_to_timeouts.get(hash(handler), -1) index = self._handler_to_timeouts.get(hash(handler), -1)
@ -780,13 +610,10 @@ class TCPRelay(object):
self._timeouts[index] = None self._timeouts[index] = None
del self._handler_to_timeouts[hash(handler)] del self._handler_to_timeouts[hash(handler)]
def update_activity(self, handler, data_len): def update_activity(self, handler):
if data_len and self._stat_callback:
self._stat_callback(self._listen_port, data_len)
# set handler to active # set handler to active
now = int(time.time()) now = int(time.time())
if now - handler.last_activity < eventloop.TIMEOUT_PRECISION: if now - handler.last_activity < TIMEOUT_PRECISION:
# thus we can lower timeout modification frequency # thus we can lower timeout modification frequency
return return
handler.last_activity = now handler.last_activity = now
@ -832,57 +659,53 @@ class TCPRelay(object):
pos = 0 pos = 0
self._timeout_offset = pos self._timeout_offset = pos
def handle_event(self, sock, fd, event): def _handle_events(self, events):
# handle events and dispatch to handlers # handle events and dispatch to handlers
if sock: for sock, fd, event in events:
logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd,
eventloop.EVENT_NAMES.get(event, event))
if sock == self._server_socket:
if event & eventloop.POLL_ERR:
# TODO
raise Exception('server_socket error')
try:
logging.debug('accept')
conn = self._server_socket.accept()
TCPRelayHandler(self, self._fd_to_handlers,
self._eventloop, conn[0], self._config,
self._dns_resolver, self._is_local)
except (OSError, IOError) as e:
error_no = eventloop.errno_from_exception(e)
if error_no in (errno.EAGAIN, errno.EINPROGRESS,
errno.EWOULDBLOCK):
return
else:
shell.print_exception(e)
if self._config['verbose']:
traceback.print_exc()
else:
if sock: if sock:
handler = self._fd_to_handlers.get(fd, None) logging.log(shell.VERBOSE_LEVEL, 'fd %d %s', fd,
if handler: eventloop.EVENT_NAMES.get(event, event))
handler.handle_event(sock, event) if sock == self._server_socket:
if event & eventloop.POLL_ERR:
# TODO
raise Exception('server_socket error')
try:
logging.debug('accept')
conn = self._server_socket.accept()
TCPRelayHandler(self, self._fd_to_handlers,
self._eventloop, conn[0], self._config,
self._dns_resolver, self._is_local)
except (OSError, IOError) as e:
error_no = eventloop.errno_from_exception(e)
if error_no in (errno.EAGAIN, errno.EINPROGRESS,
errno.EWOULDBLOCK):
continue
else:
shell.print_exception(e)
if self._config['verbose']:
traceback.print_exc()
else: else:
logging.warn('poll removed fd') if sock:
handler = self._fd_to_handlers.get(fd, None)
if handler:
handler.handle_event(sock, event)
else:
logging.warn('poll removed fd')
def handle_periodic(self): now = time.time()
if now - self._last_time > TIMEOUT_PRECISION:
self._sweep_timeout()
self._last_time = now
if self._closed: if self._closed:
if self._server_socket: if self._server_socket:
self._eventloop.remove(self._server_socket) self._eventloop.remove(self._server_socket)
self._server_socket.close() self._server_socket.close()
self._server_socket = None self._server_socket = None
logging.info('closed TCP port %d', self._listen_port) logging.info('closed listen port %d', self._listen_port)
if not self._fd_to_handlers: if not self._fd_to_handlers:
logging.info('stopping') self._eventloop.remove_handler(self._handle_events)
self._eventloop.stop()
self._sweep_timeout()
def close(self, next_tick=False): def close(self, next_tick=False):
logging.debug('TCP close')
self._closed = True self._closed = True
if not next_tick: if not next_tick:
if self._eventloop:
self._eventloop.remove_periodic(self.handle_periodic)
self._eventloop.remove(self._server_socket)
self._server_socket.close() self._server_socket.close()
for handler in list(self._fd_to_handlers.values()):
handler.destroy()

View file

@ -1,74 +0,0 @@
#!/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()

View file

@ -62,28 +62,26 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
with_statement with_statement
import time
import socket import socket
import logging import logging
import struct import struct
import errno import errno
import random import random
from shadowsocks import cryptor, eventloop, lru_cache, common, shell from shadowsocks import encrypt, eventloop, lru_cache, common, shell
from shadowsocks.common import parse_header, pack_addr, onetimeauth_verify, \ from shadowsocks.common import parse_header, pack_addr
onetimeauth_gen, ONETIMEAUTH_BYTES, ADDRTYPE_AUTH
BUF_SIZE = 65536 BUF_SIZE = 65536
def client_key(source_addr, server_af): def client_key(a, b, c, d):
# notice this is server af, not dest af return '%s:%s:%s:%s' % (a, b, c, d)
return '%s:%s:%d' % (source_addr[0], source_addr[1], server_af)
class UDPRelay(object): class UDPRelay(object):
def __init__(self, config, dns_resolver, is_local):
def __init__(self, config, dns_resolver, is_local, stat_callback=None):
self._config = config self._config = config
if is_local: if is_local:
self._listen_addr = config['local_address'] self._listen_addr = config['local_address']
@ -95,39 +93,34 @@ class UDPRelay(object):
self._listen_port = config['server_port'] self._listen_port = config['server_port']
self._remote_addr = None self._remote_addr = None
self._remote_port = 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._dns_resolver = dns_resolver
self._password = common.to_bytes(config['password']) self._password = config['password']
self._method = config['method'] self._method = config['method']
self._timeout = config['timeout'] self._timeout = config['timeout']
self._ota_enable = config.get('one_time_auth', False)
self._ota_enable_session = self._ota_enable
self._is_local = is_local self._is_local = is_local
self._cache = lru_cache.LRUCache(timeout=config['timeout'], self._cache = lru_cache.LRUCache(timeout=config['timeout'],
close_callback=self._close_client) close_callback=self._close_client)
self._client_fd_to_server_addr = \ self._client_fd_to_server_addr = \
lru_cache.LRUCache(timeout=config['timeout']) lru_cache.LRUCache(timeout=config['timeout'])
self._dns_cache = lru_cache.LRUCache(timeout=300)
self._eventloop = None self._eventloop = None
self._closed = False self._closed = False
self._last_time = time.time()
self._sockets = set() self._sockets = set()
self._forbidden_iplist = config.get('forbidden_ip') if 'forbidden_ip' in config:
self._crypto_path = config['crypto_path'] self._forbidden_iplist = config['forbidden_ip']
else:
self._forbidden_iplist = None
addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0, addrs = socket.getaddrinfo(self._listen_addr, self._listen_port, 0,
socket.SOCK_DGRAM, socket.SOL_UDP) socket.SOCK_DGRAM, socket.SOL_UDP)
if len(addrs) == 0: if len(addrs) == 0:
raise Exception("UDP can't get addrinfo for %s:%d" % raise Exception("can't get addrinfo for %s:%d" %
(self._listen_addr, self._listen_port)) (self._listen_addr, self._listen_port))
af, socktype, proto, canonname, sa = addrs[0] af, socktype, proto, canonname, sa = addrs[0]
server_socket = socket.socket(af, socktype, proto) server_socket = socket.socket(af, socktype, proto)
server_socket.bind((self._listen_addr, self._listen_port)) server_socket.bind((self._listen_addr, self._listen_port))
server_socket.setblocking(False) server_socket.setblocking(False)
self._server_socket = server_socket self._server_socket = server_socket
self._stat_callback = stat_callback
def _get_a_server(self): def _get_a_server(self):
server = self._config['server'] server = self._config['server']
@ -151,35 +144,18 @@ class UDPRelay(object):
def _handle_server(self): def _handle_server(self):
server = self._server_socket server = self._server_socket
data, r_addr = server.recvfrom(BUF_SIZE) data, r_addr = server.recvfrom(BUF_SIZE)
key = None
iv = None
if not data: if not data:
logging.debug('UDP handle_server: data is empty') logging.debug('UDP handle_server: data is empty')
if self._stat_callback:
self._stat_callback(self._listen_port, len(data))
if self._is_local: if self._is_local:
if self._is_tunnel: frag = common.ord(data[2])
# add ss header to data if frag != 0:
tunnel_remote = self.tunnel_remote logging.warn('drop a message since frag is not 0')
tunnel_remote_port = self.tunnel_remote_port
data = common.add_header(tunnel_remote,
tunnel_remote_port, data)
else:
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:
# decrypt data
try:
data, key, iv = cryptor.decrypt_all(self._password,
self._method,
data, self._crypto_path)
except Exception:
logging.debug('UDP handle_server: decrypt data failed')
return return
else:
data = data[3:]
else:
data = encrypt.encrypt_all(self._password, self._method, 0, data)
# decrypt data
if not data: if not data:
logging.debug('UDP handle_server: data is empty after decrypt') logging.debug('UDP handle_server: data is empty after decrypt')
return return
@ -187,67 +163,38 @@ class UDPRelay(object):
if header_result is None: if header_result is None:
return return
addrtype, dest_addr, dest_port, header_length = header_result 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: if self._is_local:
server_addr, server_port = self._get_a_server() server_addr, server_port = self._get_a_server()
else: else:
server_addr, server_port = dest_addr, dest_port server_addr, server_port = dest_addr, dest_port
# spec https://shadowsocks.org/en/spec/one-time-auth.html
self._ota_enable_session = addrtype & ADDRTYPE_AUTH
if self._ota_enable and not self._ota_enable_session:
logging.warn('client one time auth is required')
return
if self._ota_enable_session:
if len(data) < header_length + ONETIMEAUTH_BYTES:
logging.warn('UDP one time auth header is too short')
return
_hash = data[-ONETIMEAUTH_BYTES:]
data = data[: -ONETIMEAUTH_BYTES]
_key = iv + key
if onetimeauth_verify(_hash, data, _key) is False:
logging.warn('UDP one time auth fail')
return
addrs = self._dns_cache.get(server_addr, None)
if addrs is None:
addrs = socket.getaddrinfo(server_addr, server_port, 0,
socket.SOCK_DGRAM, socket.SOL_UDP)
if not addrs:
# drop
return
else:
self._dns_cache[server_addr] = addrs
af, socktype, proto, canonname, sa = addrs[0] key = client_key(r_addr[0], r_addr[1], dest_addr, dest_port)
key = client_key(r_addr, af)
client = self._cache.get(key, None) client = self._cache.get(key, None)
if not client: if not client:
# TODO async getaddrinfo # TODO async getaddrinfo
if self._forbidden_iplist: addrs = socket.getaddrinfo(server_addr, server_port, 0,
if common.to_str(sa[0]) in self._forbidden_iplist: socket.SOCK_DGRAM, socket.SOL_UDP)
logging.debug('IP %s is in forbidden list, drop' % if addrs:
common.to_str(sa[0])) af, socktype, proto, canonname, sa = addrs[0]
# drop if self._forbidden_iplist:
return if common.to_str(sa[0]) in self._forbidden_iplist:
client = socket.socket(af, socktype, proto) logging.debug('IP %s is in forbidden list, drop' %
client.setblocking(False) common.to_str(sa[0]))
self._cache[key] = client # drop
self._client_fd_to_server_addr[client.fileno()] = r_addr return
client = socket.socket(af, socktype, proto)
client.setblocking(False)
self._cache[key] = client
self._client_fd_to_server_addr[client.fileno()] = r_addr
else:
# drop
return
self._sockets.add(client.fileno()) self._sockets.add(client.fileno())
self._eventloop.add(client, eventloop.POLL_IN, self) self._eventloop.add(client, eventloop.POLL_IN)
if self._is_local: if self._is_local:
key, iv, m = cryptor.gen_key_iv(self._password, self._method) data = encrypt.encrypt_all(self._password, self._method, 1, data)
# spec https://shadowsocks.org/en/spec/one-time-auth.html
if self._ota_enable_session:
data = self._ota_chunk_data_gen(key, iv, data)
try:
data = cryptor.encrypt_all_m(key, iv, m, self._method, data,
self._crypto_path)
except Exception:
logging.debug("UDP handle_server: encrypt data failed")
return
if not data: if not data:
return return
else: else:
@ -268,98 +215,68 @@ class UDPRelay(object):
if not data: if not data:
logging.debug('UDP handle_client: data is empty') logging.debug('UDP handle_client: data is empty')
return return
if self._stat_callback:
self._stat_callback(self._listen_port, len(data))
if not self._is_local: if not self._is_local:
addrlen = len(r_addr[0]) addrlen = len(r_addr[0])
if addrlen > 255: if addrlen > 255:
# drop # drop
return return
data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data data = pack_addr(r_addr[0]) + struct.pack('>H', r_addr[1]) + data
try: response = encrypt.encrypt_all(self._password, self._method, 1,
response = cryptor.encrypt_all(self._password, data)
self._method, data,
self._crypto_path)
except Exception:
logging.debug("UDP handle_client: encrypt data failed")
return
if not response: if not response:
return return
else: else:
try: data = encrypt.encrypt_all(self._password, self._method, 0,
data, key, iv = cryptor.decrypt_all(self._password, data)
self._method, data,
self._crypto_path)
except Exception:
logging.debug('UDP handle_client: decrypt data failed')
return
if not data: if not data:
return return
header_result = parse_header(data) header_result = parse_header(data)
if header_result is None: if header_result is None:
return return
addrtype, dest_addr, dest_port, header_length = header_result # addrtype, dest_addr, dest_port, header_length = header_result
if self._is_tunnel: response = b'\x00\x00\x00' + data
# 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()) client_addr = self._client_fd_to_server_addr.get(sock.fileno())
if client_addr: if client_addr:
logging.debug("send udp response to %s:%d"
% (client_addr[0], client_addr[1]))
self._server_socket.sendto(response, client_addr) self._server_socket.sendto(response, client_addr)
else: else:
# this packet is from somewhere else we know # this packet is from somewhere else we know
# simply drop that packet # simply drop that packet
pass pass
def _ota_chunk_data_gen(self, key, iv, data):
data = common.chr(common.ord(data[0]) | ADDRTYPE_AUTH) + data[1:]
key = iv + key
return data + onetimeauth_gen(data, key)
def add_to_loop(self, loop): def add_to_loop(self, loop):
if self._eventloop: if self._eventloop:
raise Exception('already add to loop') raise Exception('already add to loop')
if self._closed: if self._closed:
raise Exception('already closed') raise Exception('already closed')
self._eventloop = loop self._eventloop = loop
loop.add_handler(self._handle_events)
server_socket = self._server_socket server_socket = self._server_socket
self._eventloop.add(server_socket, self._eventloop.add(server_socket,
eventloop.POLL_IN | eventloop.POLL_ERR, self) eventloop.POLL_IN | eventloop.POLL_ERR)
loop.add_periodic(self.handle_periodic)
def handle_event(self, sock, fd, event): def _handle_events(self, events):
if sock == self._server_socket: for sock, fd, event in events:
if event & eventloop.POLL_ERR: if sock == self._server_socket:
logging.error('UDP server_socket err') if event & eventloop.POLL_ERR:
self._handle_server() logging.error('UDP server_socket err')
elif sock and (fd in self._sockets): self._handle_server()
if event & eventloop.POLL_ERR: elif sock and (fd in self._sockets):
logging.error('UDP client_socket err') if event & eventloop.POLL_ERR:
self._handle_client(sock) logging.error('UDP client_socket err')
self._handle_client(sock)
def handle_periodic(self): now = time.time()
if now - self._last_time > 3:
self._cache.sweep()
self._client_fd_to_server_addr.sweep()
self._last_time = now
if self._closed: if self._closed:
if self._server_socket: self._server_socket.close()
self._server_socket.close() for sock in self._sockets:
self._server_socket = None sock.close()
for sock in self._sockets: self._eventloop.remove_handler(self._handle_events)
sock.close()
logging.info('closed UDP port %d', self._listen_port)
self._cache.sweep()
self._client_fd_to_server_addr.sweep()
self._dns_cache.sweep()
def close(self, next_tick=False): def close(self, next_tick=False):
logging.debug('UDP close')
self._closed = True self._closed = True
if not next_tick: if not next_tick:
if self._eventloop:
self._eventloop.remove_periodic(self.handle_periodic)
self._eventloop.remove(self._server_socket)
self._server_socket.close() self._server_socket.close()
for client in list(self._cache.values()):
client.close()

View file

@ -1,23 +0,0 @@
name: shadowsocks
version: 2.9.1-1
summary: A fast tunnel proxy that helps you bypass firewalls
description: A fast tunnel proxy that helps you bypass firewalls
confinement: strict
grade: stable
apps:
sslocal:
command: bin/sslocal
plugs: [network, network-bind]
aliases: [sslocal]
ssserver:
command: bin/ssserver
plugs: [network, network-bind]
aliases: [ssserver]
parts:
shadowsocks:
plugin: python
python-version: python2
source: https://github.com/shadowsocks/shadowsocks/archive/2.9.1.tar.gz

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb1", "method":"aes-256-cfb1",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb8", "method":"aes-256-cfb8",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-ctr", "method":"aes-256-ctr",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-gcm",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,11 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-ocb",
"local_address":"127.0.0.1",
"fast_open":false,
"libopenssl":"/usr/local/lib/libcrypto.so.1.1"
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-ofb",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"camellia_password",
"timeout":60,
"method":"camellia-256-cfb",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"chacha20-ietf-poly1305",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"chacha20-ietf",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"chacha20-poly1305",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"chacha20_password", "password":"salsa20_password",
"timeout":60, "timeout":60,
"method":"chacha20", "method":"chacha20",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":["127.0.0.1", "127.0.0.1"], "server":["127.0.0.1", "127.0.0.1"],
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"fastopen_password", "password":"fastopen_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":true "fast_open":true
} }

View file

@ -1,18 +0,0 @@
#!/usr/bin/python
import json
with open('server-multi-passwd-performance.json', 'wb') as f:
r = {
'server': '127.0.0.1',
'local_port': 1081,
'timeout': 60,
'method': 'aes-256-cfb'
}
ports = {}
for i in range(7000, 9000):
ports[str(i)] = 'aes_password'
r['port_password'] = ports
print(r)
f.write(json.dumps(r, indent=4).encode('utf-8'))

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":15,
"method":"aes-256-cfb",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,17 +0,0 @@
#!/usr/bin/python
import socks
import time
SERVER_IP = '127.0.0.1'
SERVER_PORT = 8001
if __name__ == '__main__':
s = socks.socksocket()
s.set_proxy(socks.SOCKS5, SERVER_IP, 1081)
s.connect((SERVER_IP, SERVER_PORT))
s.send(b'test')
time.sleep(30)
s.close()

View file

@ -1,13 +0,0 @@
#!/usr/bin/python
import socket
if __name__ == '__main__':
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 8001))
s.listen(1024)
c = None
while True:
c = s.accept()

View file

@ -1,10 +1,10 @@
{ {
"server":"::1", "server":"::1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":"::", "server":"::",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -33,47 +33,22 @@ run_test coverage run tests/nose_plugin.py -v
run_test python setup.py sdist run_test python setup.py sdist
run_test tests/test_daemon.sh run_test tests/test_daemon.sh
run_test python tests/test.py --with-coverage -c tests/aes.json run_test python tests/test.py --with-coverage -c tests/aes.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-aes.json
run_test python tests/test.py --with-coverage -c tests/aes-gcm.json
run_test python tests/test.py --with-coverage -c tests/aes-ocb.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-aes-gcm.json
run_test python tests/test.py --with-coverage -c tests/aes-ctr.json run_test python tests/test.py --with-coverage -c tests/aes-ctr.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-aes-ctr.json
run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json
run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json
run_test python tests/test.py --with-coverage -c tests/aes-ofb.json
run_test python tests/test.py --with-coverage -c tests/camellia.json
run_test python tests/test.py --with-coverage -c tests/mbedtls-camellia.json
run_test python tests/test.py --with-coverage -c tests/rc4-md5.json run_test python tests/test.py --with-coverage -c tests/rc4-md5.json
run_test python tests/test.py --with-coverage -c tests/salsa20.json run_test python tests/test.py --with-coverage -c tests/salsa20.json
run_test python tests/test.py --with-coverage -c tests/chacha20.json run_test python tests/test.py --with-coverage -c tests/chacha20.json
run_test python tests/test.py --with-coverage -c tests/xchacha20.json
run_test python tests/test.py --with-coverage -c tests/chacha20-ietf.json
run_test python tests/test.py --with-coverage -c tests/chacha20-poly1305.json
run_test python tests/test.py --with-coverage -c tests/xchacha20-ietf-poly1305.json
run_test python tests/test.py --with-coverage -c tests/chacha20-ietf-poly1305.json
run_test python tests/test.py --with-coverage -c tests/table.json run_test python tests/test.py --with-coverage -c tests/table.json
run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json
run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json
run_test python tests/test.py --with-coverage -s tests/server-dnsserver.json -c tests/client-multi-server-ip.json
run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json
run_test python tests/test.py --with-coverage -c tests/workers.json run_test python tests/test.py --with-coverage -c tests/workers.json
run_test python tests/test.py --with-coverage -c tests/rc4-md5-ota.json run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json
# travis-ci not support IPv6
# run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json
run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -q" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -vv" run_test python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -q" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -vv"
run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1" run_test python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --workers 1" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -qq -b 127.0.0.1"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=127.0.0.1,::1,8.8.8.8" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1"
# test custom lib path
run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libopenssl=/usr/local/lib/libcrypto.so" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libopenssl=/usr/local/lib/libcrypto.so"
run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m mbedtls:aes-256-cfb128 -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libmbedtls=/usr/local/lib/libmbedcrypto.so" -a "-m mbedtls:aes-256-cfb128 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libmbedtls=/usr/local/lib/libmbedcrypto.so"
run_test python tests/test.py --with-coverage --url="http://127.0.0.1/" -b "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libsodium=/usr/local/lib/libsodium.so" -a "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libsodium=/usr/local/lib/libsodium.so"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libopenssl=invalid_path" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libopenssl=invalid_path"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libsodium=invalid_path" -a "-m chacha20-ietf -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libsodium=invalid_path"
run_test python tests/test.py --with-coverage --should-fail --url="http://127.0.0.1/" -b "-m mbedtls:aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip= --libmbedtls=invalid_path" -a "-m mbedtls:aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1 --libmbedtls=invalid_path"
# test if DNS works # test if DNS works
run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204"
@ -95,17 +70,13 @@ fi
run_test tests/test_large_file.sh run_test tests/test_large_file.sh
if [ "a$JENKINS" != "a1" ] ; then
# jenkins blocked SIGQUIT with sigprocmask(), we have to skip this test on Jenkins
run_test tests/test_graceful_restart.sh
fi
run_test tests/test_udp_src.sh
run_test tests/test_command.sh run_test tests/test_command.sh
# coverage combine && coverage report --include=shadowsocks/* coverage combine && coverage report --include=shadowsocks/*
# rm -rf htmlcov rm -rf htmlcov
# rm -rf tmp rm -rf tmp
# coverage html --include=shadowsocks/* coverage html --include=shadowsocks/*
# coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage
coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage
exit $result exit $result

View file

@ -1,12 +0,0 @@
#!/bin/bash
MBEDTLS_VER=2.4.2
if [ ! -d mbedtls-$MBEDTLS_VER ]; then
wget https://tls.mbed.org/download/mbedtls-$MBEDTLS_VER-gpl.tgz || exit 1
tar xf mbedtls-$MBEDTLS_VER-gpl.tgz || exit 1
fi
pushd mbedtls-$MBEDTLS_VER
make SHARED=1 CFLAGS=-fPIC && sudo make install || exit 1
sudo ldconfig
popd
rm -rf mbedtls-$MBEDTLS_VER || exit 1

View file

@ -1,19 +0,0 @@
#!/bin/bash
OPENSSL_VER=1.1.0e
if [ ! -d openssl-$OPENSSL_VER ]; then
wget https://www.openssl.org/source/openssl-$OPENSSL_VER.tar.gz || exit 1
tar xf openssl-$OPENSSL_VER.tar.gz || exit 1
fi
pushd openssl-$OPENSSL_VER
./config && make && sudo make install || exit 1
# sudo ldconfig # test multiple libcrypto
popd
rm -rf openssl-$OPENSSL_VER || exit 1
rm /usr/bin/openssl || exit 1
rm -r /usr/include/openssl || exit 1
ln -s /usr/local/bin/openssl /usr/bin/openssl || exit 1
ln -s /usr/local/include/openssl /usr/include/openssl || exit 1
echo /usr/local/lib >> /etc/ld.so.conf || exit 1
ldconfig -v || exit 1

View file

@ -1,11 +1,10 @@
#!/bin/bash #!/bin/bash
if [ ! -d libsodium-1.0.12 ]; then if [ ! -d libsodium-1.0.1 ]; then
wget https://github.com/jedisct1/libsodium/releases/download/1.0.12/libsodium-1.0.12.tar.gz || exit 1 wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1
tar xf libsodium-1.0.12.tar.gz || exit 1 tar xf libsodium-1.0.1.tar.gz || exit 1
fi fi
pushd libsodium-1.0.12 pushd libsodium-1.0.1
./configure && make -j2 && make install || exit 1 ./configure && make -j2 && make install || exit 1
sudo ldconfig sudo ldconfig
popd popd
rm -rf libsodium-1.0.12 || exit 1

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"mbedtls:aes-256-ctr",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"mbedtls:aes-256-gcm",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"mbedtls:aes-256-cfb128",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"camellia_password",
"timeout":60,
"method":"mbedtls:camellia-256-cfb128",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,11 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"rc4-md5",
"local_address":"127.0.0.1",
"fast_open":false,
"one_time_auth":true
}

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"aes_password", "password":"aes_password",
"timeout":60, "timeout":60,
"method":"rc4-md5", "method":"rc4-md5",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"salsa20_password", "password":"salsa20_password",
"timeout":60, "timeout":60,
"method":"salsa20-ctr", "method":"salsa20-ctr",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"salsa20_password", "password":"salsa20_password",
"timeout":60, "timeout":60,
"method":"salsa20", "method":"salsa20",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -1,11 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"aes_password",
"timeout":60,
"method":"aes-256-cfb",
"local_address":"127.0.0.1",
"fast_open":false,
"dns_server": ["8.8.8.8","8.8.4.4"]
}

View file

@ -1,8 +0,0 @@
{
"server": "127.0.0.1",
"local_port": 1081,
"port_password": {
},
"timeout": 60,
"method": "aes-256-cfb"
}

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,8 @@
#!/bin/bash #!/bin/bash
if [ ! -d dante-1.4.0 ] || [ ! -d dante-1.4.0/configure ]; then if [ ! -d dante-1.4.0 ]; then
rm dante-1.4.0 -rf wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1
#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 tar xf dante-1.4.0.tar.gz || exit 1
#
mv dante-dante-1.4.0 dante-1.4.0
fi fi
pushd dante-1.4.0 pushd dante-1.4.0
./configure && make -j4 && make install || exit 1 ./configure && make -j4 && make install || exit 1

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"table_password", "password":"table_password",
"timeout":60, "timeout":60,
"method":"table", "method":"table",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"fast_open":false "fast_open":false
} }

View file

@ -44,7 +44,7 @@ parser.add_argument('--dns', type=str, default='8.8.8.8')
config = parser.parse_args() config = parser.parse_args()
if config.with_coverage: if config.with_coverage:
python = ['coverage', 'run', '-a'] python = ['coverage', 'run', '-p', '-a']
client_args = python + ['shadowsocks/local.py', '-v'] client_args = python + ['shadowsocks/local.py', '-v']
server_args = python + ['shadowsocks/server.py', '-v'] server_args = python + ['shadowsocks/server.py', '-v']

View file

@ -2,7 +2,7 @@
. tests/assert.sh . tests/assert.sh
PYTHON="coverage run -a" PYTHON="coverage run -a -p"
LOCAL="$PYTHON shadowsocks/local.py" LOCAL="$PYTHON shadowsocks/local.py"
SERVER="$PYTHON shadowsocks/server.py" SERVER="$PYTHON shadowsocks/server.py"
@ -30,7 +30,7 @@ $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d sto
assert "$LOCAL 2>&1 -m rc4-md5 -k mypassword -s 0.0.0.0 -p 8388 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " DON'T USE DEFAULT PASSWORD! Please change it in your config.json!" assert "$LOCAL 2>&1 -m rc4-md5 -k mypassword -s 0.0.0.0 -p 8388 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " DON'T USE DEFAULT PASSWORD! Please change it in your config.json!"
$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop
assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " server addr not specified" assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -k testrc4 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": server addr not specified"
$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop
assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password not specified" assert "$LOCAL 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password not specified"
@ -39,7 +39,7 @@ $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d sto
assert "$SERVER 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password or port_password not specified" assert "$SERVER 2>&1 -m rc4-md5 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " password or port_password not specified"
$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop
assert "$SERVER 2>&1 --forbidden-ip 127.0.0.1/4a -m rc4-md5 -k 12345 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" " Not a valid CIDR notation: 127.0.0.1/4a" assert "$SERVER 2>&1 --forbidden-ip 127.0.0.1/4a -m rc4-md5 -k 12345 -p 8388 -s 0.0.0.0 -d start | grep ERROR | awk -F\"ERROR\" '{print \$2}'" ": Not a valid CIDR notation: 127.0.0.1/4a"
$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop
assert_end command assert_end command

View file

@ -18,7 +18,7 @@ function run_test {
for module in local server for module in local server
do do
command="coverage run -a shadowsocks/$module.py" command="coverage run -p -a shadowsocks/$module.py"
mkdir -p tmp mkdir -p tmp

View file

@ -1,64 +0,0 @@
#!/bin/bash
PYTHON="coverage run -a"
URL=http://127.0.0.1/file
# setup processes
$PYTHON shadowsocks/local.py -c tests/graceful.json &
LOCAL=$!
$PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" &
SERVER=$!
python tests/graceful_server.py &
GSERVER=$!
sleep 1
python tests/graceful_cli.py &
GCLI=$!
sleep 1
# graceful restart server: send SIGQUIT to old process and start a new one
kill -s SIGQUIT $SERVER
sleep 0.5
$PYTHON shadowsocks/server.py -c tests/graceful.json --forbidden-ip "" &
NEWSERVER=$!
sleep 1
# check old server
ps x | grep -v grep | grep $SERVER
OLD_SERVER_RUNNING1=$?
# old server should not quit at this moment
echo old server running: $OLD_SERVER_RUNNING1
sleep 1
# close connections on old server
kill -s SIGKILL $GCLI
kill -s SIGKILL $GSERVER
kill -s SIGINT $LOCAL
sleep 11
# check old server
ps x | grep -v grep | grep $SERVER
OLD_SERVER_RUNNING2=$?
# old server should quit at this moment
echo old server running: $OLD_SERVER_RUNNING2
kill -s SIGINT $SERVER
# new server is expected running
kill -s SIGINT $NEWSERVER || exit 1
if [ $OLD_SERVER_RUNNING1 -ne 0 ]; then
exit 1
fi
if [ $OLD_SERVER_RUNNING2 -ne 1 ]; then
sleep 1
exit 1
fi

View file

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
PYTHON="coverage run -a" PYTHON="coverage run -p -a"
URL=http://127.0.0.1/file URL=http://127.0.0.1/file
mkdir -p tmp mkdir -p tmp

View file

@ -1,85 +0,0 @@
#!/usr/bin/python
import socket
import socks
SERVER_IP = '127.0.0.1'
SERVER_PORT = 1081
if __name__ == '__main__':
# Test 1: same source port IPv4
sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT)
sock_out.bind(('127.0.0.1', 9000))
sock_in1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_in2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_in1.bind(('127.0.0.1', 9001))
sock_in2.bind(('127.0.0.1', 9002))
sock_out.sendto(b'data', ('127.0.0.1', 9001))
result1 = sock_in1.recvfrom(8)
sock_out.sendto(b'data', ('127.0.0.1', 9002))
result2 = sock_in2.recvfrom(8)
sock_out.close()
sock_in1.close()
sock_in2.close()
# make sure they're from the same source port
assert result1 == result2
"""
# Test 2: same source port IPv6
# try again from the same port but IPv6
sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT)
sock_out.bind(('127.0.0.1', 9000))
sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_in1.bind(('::1', 9001))
sock_in2.bind(('::1', 9002))
sock_out.sendto(b'data', ('::1', 9001))
result1 = sock_in1.recvfrom(8)
sock_out.sendto(b'data', ('::1', 9002))
result2 = sock_in2.recvfrom(8)
sock_out.close()
sock_in1.close()
sock_in2.close()
# make sure they're from the same source port
assert result1 == result2
# Test 3: different source ports IPv6
sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT)
sock_out.bind(('127.0.0.1', 9003))
sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM,
socket.SOL_UDP)
sock_in1.bind(('::1', 9001))
sock_out.sendto(b'data', ('::1', 9001))
result3 = sock_in1.recvfrom(8)
# make sure they're from different source ports
assert result1 != result3
sock_out.close()
sock_in1.close()
"""

View file

@ -1,23 +0,0 @@
#!/bin/bash
PYTHON="coverage run -a"
mkdir -p tmp
$PYTHON shadowsocks/local.py -c tests/aes.json -v &
LOCAL=$!
$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v &
SERVER=$!
sleep 3
python tests/test_udp_src.py
r=$?
kill -s SIGINT $LOCAL
kill -s SIGINT $SERVER
sleep 2
exit $r

View file

@ -1,10 +1,10 @@
{ {
"server":"127.0.0.1", "server":"127.0.0.1",
"server_port":8388, "server_port":8388,
"local_port":1081, "local_port":1081,
"password":"workers_password", "password":"workers_password",
"timeout":60, "timeout":60,
"method":"aes-256-cfb", "method":"aes-256-cfb",
"local_address":"127.0.0.1", "local_address":"127.0.0.1",
"workers": 4 "workers": 4
} }

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"salsa20_password",
"timeout":60,
"method":"xchacha20-ietf-poly1305",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -1,10 +0,0 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1081,
"password":"xchacha20_password",
"timeout":60,
"method":"xchacha20",
"local_address":"127.0.0.1",
"fast_open":false
}

View file

@ -24,17 +24,9 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
with_statement with_statement
import os
import sys import sys
import socket
import argparse import argparse
import subprocess
def inet_pton(str_ip):
try:
return socket.inet_pton(socket.AF_INET, str_ip)
except socket.error:
return None
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='See README') parser = argparse.ArgumentParser(description='See README')
@ -45,22 +37,17 @@ if __name__ == '__main__':
ips = {} ips = {}
banned = set() banned = set()
for line in sys.stdin: for line in sys.stdin:
if 'can not parse header when' not in line: if 'can not parse header when' in line:
continue ip = line.split()[-1].split(':')[0]
ip_str = line.split()[-1].rsplit(':', 1)[0] if ip not in ips:
ip = inet_pton(ip_str) ips[ip] = 1
if ip is None: print(ip)
continue sys.stdout.flush()
if ip not in ips: else:
ips[ip] = 1 ips[ip] += 1
sys.stdout.flush() if ip not in banned and ips[ip] >= config.count:
else: banned.add(ip)
ips[ip] += 1 cmd = 'iptables -A INPUT -s %s -j DROP' % ip
if ip not in banned and ips[ip] >= config.count: print(cmd, file=sys.stderr)
banned.add(ip) sys.stderr.flush()
print('ban ip %s' % ip_str) os.system(cmd)
cmd = ['iptables', '-A', 'INPUT', '-s', ip_str, '-j', 'DROP',
'-m', 'comment', '--comment', 'autoban']
print(' '.join(cmd), file=sys.stderr)
sys.stderr.flush()
subprocess.call(cmd)

View file

@ -1,5 +0,0 @@
[Definition]
_daemon = shadowsocks
failregex = ^\s+ERROR\s+can not parse header when handling connection from <HOST>:\d+$