├── debian ├── compat ├── source │ └── format ├── docs ├── install ├── shadowsocks.manpages ├── rules ├── changelog ├── config.json ├── shadowsocks.default ├── control ├── sslocal.1 ├── ssserver.1 ├── copyright └── init.d ├── mudb.json ├── shadowsocks ├── tail.sh ├── stop.sh ├── run.sh ├── logrun.sh ├── version.py ├── __init__.py ├── crypto │ ├── __init__.py │ ├── rc4_md5.py │ ├── util.py │ ├── ctypes_libsodium.py │ ├── sodium.py │ ├── ctypes_openssl.py │ ├── openssl.py │ └── table.py ├── obfsplugin │ ├── __init__.py │ ├── plain.py │ └── verify.py ├── encrypt_test.py ├── local.py ├── obfs.py ├── lru_cache.py ├── daemon.py ├── encrypt.py ├── eventloop.py ├── ordereddict.py ├── server.py └── manager.py ├── tail.sh ├── MANIFEST.in ├── tests ├── socksify │ ├── socks.conf │ └── install.sh ├── server-multi-passwd-client-side.json ├── server-multi-ports.json ├── ipv6.json ├── table.json ├── aes-ctr.json ├── aes.json ├── rc4-md5.json ├── salsa20.json ├── workers.json ├── aes-cfb1.json ├── aes-cfb8.json ├── chacha20.json ├── fastopen.json ├── ipv6-client-side.json ├── salsa20-ctr.json ├── client-multi-server-ip.json ├── libsodium │ └── install.sh ├── test_udp_src.sh ├── server-multi-passwd.json ├── server-multi-passwd-table.json ├── test_large_file.sh ├── setup_tc.sh ├── nose_plugin.py ├── test_daemon.sh ├── coverage_server.py ├── test_command.sh ├── test_udp_src.py ├── jenkins.sh ├── assert.sh └── test.py ├── setup_cymysql.sh ├── utils ├── fail2ban │ └── shadowsocks.conf ├── README.md └── autoban.py ├── stop.sh ├── initcfg.sh ├── switchrule.py ├── initcfg.bat ├── run.sh ├── logrun.sh ├── mysql.json ├── configloader.py ├── apiconfig.py ├── importloader.py ├── .gitignore ├── .travis.yml ├── config.json ├── Dockerfile ├── initmudbjson.sh ├── CONTRIBUTING.md ├── setup.py ├── server.py ├── README.md ├── README.rst ├── asyncmgr.py ├── CHANGES ├── server_pool.py └── mujson_mgr.py /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /mudb.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.md 2 | README.rst 3 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | debian/config.json etc/shadowsocks/ -------------------------------------------------------------------------------- /debian/shadowsocks.manpages: -------------------------------------------------------------------------------- 1 | debian/sslocal.1 2 | debian/ssserver.1 -------------------------------------------------------------------------------- /shadowsocks/tail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tail -f ssserver.log 4 | -------------------------------------------------------------------------------- /tail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | tail -f ssserver.log 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include shadowsocks *.py 2 | include README.rst 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | 4 | %: 5 | dh $@ --with python2 --buildsystem=python_distutils 6 | -------------------------------------------------------------------------------- /tests/socksify/socks.conf: -------------------------------------------------------------------------------- 1 | route { 2 | from: 0.0.0.0/0 to: 0.0.0.0/0 via: 127.0.0.1 port = 1081 3 | proxyprotocol: socks_v5 4 | method: none 5 | } -------------------------------------------------------------------------------- /setup_cymysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rm -rf CyMySQL 3 | rm -rf cymysql 4 | git clone https://github.com/nakagami/CyMySQL.git 5 | mv CyMySQL/cymysql ./ 6 | rm -rf CyMySQL 7 | -------------------------------------------------------------------------------- /utils/fail2ban/shadowsocks.conf: -------------------------------------------------------------------------------- 1 | [Definition] 2 | 3 | _daemon = shadowsocks 4 | 5 | failregex = ^\s+ERROR\s+can not parse header when handling connection from :\d+$ 6 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | shadowsocks (2.1.0-1) unstable; urgency=low 2 | 3 | * Initial release (Closes: #758900) 4 | 5 | -- Shell.Xu Sat, 23 Aug 2014 00:56:04 +0800 6 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) 4 | eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') 5 | 6 | -------------------------------------------------------------------------------- /initcfg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | chmod +x *.sh 4 | chmod +x shadowsocks/*.sh 5 | cp -n apiconfig.py userapiconfig.py 6 | cp -n config.json user-config.json 7 | cp -n mysql.json usermysql.json 8 | 9 | -------------------------------------------------------------------------------- /shadowsocks/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) 4 | eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py a" | awk '{print "kill "$2}') 5 | 6 | -------------------------------------------------------------------------------- /switchrule.py: -------------------------------------------------------------------------------- 1 | def getKeys(key_list): 2 | return key_list 3 | #return key_list + ['plan'] # append the column name 'plan' 4 | 5 | def isTurnOn(row): 6 | return True 7 | #return row['plan'] == 'B' # then judge here 8 | 9 | -------------------------------------------------------------------------------- /tests/server-multi-passwd-client-side.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": "8385", 4 | "local_port": 1081, 5 | "password": "foobar5", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /tests/server-multi-ports.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": [8384, 8345, 8346, 8347], 4 | "local_port": 1081, 5 | "password": "foobar4", 6 | "timeout": 60, 7 | "method": "aes-256-cfb" 8 | } 9 | -------------------------------------------------------------------------------- /initcfg.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | If Not Exist "userapiconfig.py" Copy "apiconfig.py" "userapiconfig.py" 3 | If Not Exist "user-config.json" Copy "config.json" "user-config.json" 4 | If Not Exist "usermysql.json" Copy "mysql.json" "usermysql.json" 5 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | Useful Tools 2 | =========== 3 | 4 | autoban.py 5 | ---------- 6 | 7 | Automatically ban IPs that try to brute force crack the server. 8 | 9 | See https://github.com/shadowsocks/shadowsocks/wiki/Ban-Brute-Force-Crackers 10 | -------------------------------------------------------------------------------- /tests/ipv6.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"::", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) 4 | eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') 5 | ulimit -n 512000 6 | nohup ${python_ver} server.py m>> /dev/null 2>&1 & 7 | 8 | -------------------------------------------------------------------------------- /tests/table.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"table_password", 6 | "timeout":60, 7 | "method":"table", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /logrun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) 4 | eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') 5 | ulimit -n 512000 6 | nohup ${python_ver} server.py m>> ssserver.log 2>&1 & 7 | 8 | -------------------------------------------------------------------------------- /tests/aes-ctr.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-ctr", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/aes.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/rc4-md5.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"rc4-md5", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/salsa20.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"salsa20_password", 6 | "timeout":60, 7 | "method":"salsa20", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/workers.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"workers_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "workers": 4 10 | } 11 | -------------------------------------------------------------------------------- /shadowsocks/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) 4 | eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py a" | awk '{print "kill "$2}') 5 | ulimit -n 512000 6 | nohup ${python_ver} server.py a>> /dev/null 2>&1 & 7 | 8 | -------------------------------------------------------------------------------- /tests/aes-cfb1.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb1", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/aes-cfb8.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb8", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/chacha20.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"salsa20_password", 6 | "timeout":60, 7 | "method":"chacha20", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/fastopen.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"fastopen_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":true 10 | } 11 | -------------------------------------------------------------------------------- /tests/ipv6-client-side.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"::1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /tests/salsa20-ctr.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"127.0.0.1", 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"salsa20_password", 6 | "timeout":60, 7 | "method":"salsa20-ctr", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /debian/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":"my_server_ip", 3 | "server_port":8388, 4 | "local_address": "127.0.0.1", 5 | "local_port":1080, 6 | "password":"mypassword", 7 | "timeout":300, 8 | "method":"aes-256-cfb", 9 | "fast_open": false, 10 | "workers": 1 11 | } -------------------------------------------------------------------------------- /mysql.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "127.0.0.1", 3 | "port": 3306, 4 | "user": "ss", 5 | "password": "pass", 6 | "db": "sspanel", 7 | "node_id": 0, 8 | "transfer_mul": 1.0, 9 | "ssl_enable": 0, 10 | "ssl_ca": "", 11 | "ssl_cert": "", 12 | "ssl_key": "" 13 | } 14 | -------------------------------------------------------------------------------- /shadowsocks/logrun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) 4 | eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py a" | awk '{print "kill "$2}') 5 | ulimit -n 512000 6 | nohup ${python_ver} server.py a>> ssserver.log 2>&1 & 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/client-multi-server-ip.json: -------------------------------------------------------------------------------- 1 | { 2 | "server":["127.0.0.1", "127.0.0.1"], 3 | "server_port":8388, 4 | "local_port":1081, 5 | "password":"aes_password", 6 | "timeout":60, 7 | "method":"aes-256-cfb", 8 | "local_address":"127.0.0.1", 9 | "fast_open":false 10 | } 11 | -------------------------------------------------------------------------------- /configloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | import importloader 4 | 5 | g_config = None 6 | 7 | def load_config(): 8 | global g_config 9 | g_config = importloader.loads(['userapiconfig', 'apiconfig']) 10 | 11 | def get_config(): 12 | return g_config 13 | 14 | load_config() 15 | 16 | -------------------------------------------------------------------------------- /tests/socksify/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d dante-1.4.0 ]; then 4 | wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1 5 | tar xf dante-1.4.0.tar.gz || exit 1 6 | fi 7 | pushd dante-1.4.0 8 | ./configure && make -j4 && make install || exit 1 9 | popd 10 | cp tests/socksify/socks.conf /etc/ || exit 1 11 | -------------------------------------------------------------------------------- /tests/libsodium/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d libsodium-1.0.1 ]; then 4 | wget https://github.com/jedisct1/libsodium/releases/download/1.0.1/libsodium-1.0.1.tar.gz || exit 1 5 | tar xf libsodium-1.0.1.tar.gz || exit 1 6 | fi 7 | pushd libsodium-1.0.1 8 | ./configure && make -j2 && make install || exit 1 9 | sudo ldconfig 10 | popd 11 | -------------------------------------------------------------------------------- /debian/shadowsocks.default: -------------------------------------------------------------------------------- 1 | # Defaults for shadowsocks initscript 2 | # sourced by /etc/init.d/shadowsocks 3 | # installed at /etc/default/shadowsocks by the maintainer scripts 4 | 5 | USERID="nobody" 6 | 7 | # 8 | # This is a POSIX shell fragment 9 | # 10 | 11 | # Additional options that are passed to the Daemon. 12 | DAEMON_OPTS="-q -c /etc/shadowsocks/config.json" 13 | -------------------------------------------------------------------------------- /apiconfig.py: -------------------------------------------------------------------------------- 1 | # Config 2 | API_INTERFACE = 'sspanelv2' #mudbjson, sspanelv2, sspanelv3, sspanelv3ssr, glzjinmod, legendsockssr, muapiv2(not support) 3 | UPDATE_TIME = 60 4 | SERVER_PUB_ADDR = '127.0.0.1' # mujson_mgr need this to generate ssr link 5 | 6 | #mudb 7 | MUDB_FILE = 'mudb.json' 8 | 9 | # Mysql 10 | MYSQL_CONFIG = 'usermysql.json' 11 | 12 | # API 13 | MUAPI_CONFIG = 'usermuapi.json' 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/test_udp_src.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON="coverage run -p" 4 | 5 | mkdir -p tmp 6 | 7 | $PYTHON shadowsocks/local.py -c tests/aes.json -v & 8 | LOCAL=$! 9 | 10 | $PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" -v & 11 | SERVER=$! 12 | 13 | sleep 3 14 | 15 | python tests/test_udp_src.py 16 | r=$? 17 | 18 | kill -s SIGINT $LOCAL 19 | kill -s SIGINT $SERVER 20 | 21 | sleep 2 22 | 23 | exit $r 24 | -------------------------------------------------------------------------------- /importloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | def load(name): 5 | try: 6 | obj = __import__(name) 7 | reload(obj) 8 | return obj 9 | except: 10 | pass 11 | 12 | try: 13 | import importlib 14 | obj = importlib.__import__(name) 15 | importlib.reload(obj) 16 | return obj 17 | except: 18 | pass 19 | 20 | def loads(namelist): 21 | for name in namelist: 22 | obj = load(name) 23 | if obj is not None: 24 | return obj 25 | -------------------------------------------------------------------------------- /tests/server-multi-passwd.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "local_port": 1081, 4 | "port_password": { 5 | "8381": "foobar1", 6 | "8382": "foobar2", 7 | "8383": "foobar3", 8 | "8384": "foobar4", 9 | "8385": "foobar5", 10 | "8386": "foobar6", 11 | "8387": "foobar7", 12 | "8388": "foobar8", 13 | "8389": "foobar9" 14 | }, 15 | "timeout": 60, 16 | "method": "aes-256-cfb" 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | 16 | # Installer logs 17 | pip-log.txt 18 | 19 | # Unit test / coverage reports 20 | htmlcov 21 | .coverage* 22 | .tox 23 | 24 | #Translations 25 | *.mo 26 | 27 | #Mr Developer 28 | .mr.developer.cfg 29 | 30 | .DS_Store 31 | .idea 32 | 33 | # Ignore user config 34 | user-config.json 35 | userapiconfig.py 36 | usermysql.json 37 | -------------------------------------------------------------------------------- /tests/server-multi-passwd-table.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "127.0.0.1", 3 | "server_port": 8384, 4 | "local_port": 1081, 5 | "password": "foobar4", 6 | "port_password": { 7 | "8381": "foobar1", 8 | "8382": "foobar2", 9 | "8383": "foobar3", 10 | "8384": "foobar4", 11 | "8385": "foobar5", 12 | "8386": "foobar6", 13 | "8387": "foobar7", 14 | "8388": "foobar8", 15 | "8389": "foobar9" 16 | }, 17 | "timeout": 60, 18 | "method": "table" 19 | } 20 | -------------------------------------------------------------------------------- /tests/test_large_file.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PYTHON="coverage run -p" 4 | URL=http://127.0.0.1/file 5 | 6 | mkdir -p tmp 7 | 8 | $PYTHON shadowsocks/local.py -c tests/aes.json & 9 | LOCAL=$! 10 | 11 | $PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" & 12 | SERVER=$! 13 | 14 | sleep 3 15 | 16 | time curl -o tmp/expected $URL 17 | time curl -o tmp/result --socks5-hostname 127.0.0.1:1081 $URL 18 | 19 | kill -s SIGINT $LOCAL 20 | kill -s SIGINT $SERVER 21 | 22 | sleep 2 23 | 24 | diff tmp/expected tmp/result || exit 1 25 | -------------------------------------------------------------------------------- /tests/setup_tc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEV=lo 4 | PORT=8388 5 | DELAY=100ms 6 | 7 | type tc 2> /dev/null && ( 8 | tc qdisc add dev $DEV root handle 1: htb 9 | tc class add dev $DEV parent 1: classid 1:1 htb rate 2mbps 10 | tc class add dev $DEV parent 1:1 classid 1:6 htb rate 2mbps ceil 1mbps prio 0 11 | tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 6 fw flowid 1:6 12 | 13 | tc filter add dev $DEV parent 1:0 protocol ip u32 match ip dport $PORT 0xffff flowid 1:6 14 | tc filter add dev $DEV parent 1:0 protocol ip u32 match ip sport $PORT 0xffff flowid 1:6 15 | 16 | tc qdisc show dev lo 17 | ) 18 | 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - 3.3 6 | - 3.4 7 | cache: 8 | directories: 9 | - dante-1.4.0 10 | before_install: 11 | - sudo apt-get update -qq 12 | - sudo apt-get install -qq build-essential dnsutils iproute nginx bc 13 | - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 14 | - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" 15 | - sudo service nginx restart 16 | - pip install pep8 pyflakes nose coverage PySocks cymysql 17 | - sudo tests/socksify/install.sh 18 | - sudo tests/libsodium/install.sh 19 | - sudo tests/setup_tc.sh 20 | script: 21 | - tests/jenkins.sh 22 | -------------------------------------------------------------------------------- /shadowsocks/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2017 breakwa11 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | def version(): 19 | return '3.4.0 2017-07-27' 20 | 21 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "0.0.0.0", 3 | "server_ipv6": "::", 4 | "server_port": 8388, 5 | "local_address": "127.0.0.1", 6 | "local_port": 1080, 7 | 8 | "password": "m", 9 | "method": "aes-128-ctr", 10 | "protocol": "auth_aes128_md5", 11 | "protocol_param": "", 12 | "obfs": "tls1.2_ticket_auth_compatible", 13 | "obfs_param": "", 14 | "speed_limit_per_con": 0, 15 | "speed_limit_per_user": 0, 16 | 17 | "additional_ports" : {}, // only works under multi-user mode 18 | "additional_ports_only" : false, // only works under multi-user mode 19 | "timeout": 120, 20 | "udp_timeout": 60, 21 | "dns_ipv6": false, 22 | "connect_verbose_info": 0, 23 | "redirect": "", 24 | "fast_open": false 25 | } 26 | -------------------------------------------------------------------------------- /shadowsocks/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2012-2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /shadowsocks/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: shadowsocks 2 | Section: python 3 | Priority: extra 4 | Maintainer: Shell.Xu 5 | Build-Depends: debhelper (>= 8), python-all (>= 2.6.6-3~), python-setuptools 6 | Standards-Version: 3.9.5 7 | Homepage: https://github.com/clowwindy/shadowsocks 8 | Vcs-Git: git://github.com/shell909090/shadowsocks.git 9 | Vcs-Browser: http://github.com/shell909090/shadowsocks 10 | 11 | Package: shadowsocks 12 | Architecture: all 13 | Pre-Depends: dpkg (>= 1.15.6~) 14 | Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-m2crypto 15 | Description: Fast tunnel proxy that helps you bypass firewalls 16 | A secure socks5 proxy, designed to protect your Internet traffic. 17 | . 18 | This package contain local and server part of shadowsocks, a fast, 19 | powerful tunnel proxy to bypass firewalls. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.6 2 | 3 | ENV SERVER_ADDR 0.0.0.0 4 | ENV SERVER_PORT 51348 5 | ENV PASSWORD psw 6 | ENV METHOD aes-128-ctr 7 | ENV PROTOCOL auth_aes128_md5 8 | ENV PROTOCOLPARAM 32 9 | ENV OBFS tls1.2_ticket_auth_compatible 10 | ENV TIMEOUT 300 11 | ENV DNS_ADDR 8.8.8.8 12 | ENV DNS_ADDR_2 8.8.4.4 13 | 14 | ARG BRANCH=manyuser 15 | ARG WORK=~ 16 | 17 | 18 | RUN apk --no-cache add python \ 19 | libsodium \ 20 | wget 21 | 22 | 23 | RUN mkdir -p $WORK && \ 24 | wget -qO- --no-check-certificate https://github.com/shadowsocksr/shadowsocksr/archive/$BRANCH.tar.gz | tar -xzf - -C $WORK 25 | 26 | 27 | WORKDIR $WORK/shadowsocksr-$BRANCH/shadowsocks 28 | 29 | 30 | EXPOSE $SERVER_PORT 31 | CMD python server.py -p $SERVER_PORT -k $PASSWORD -m $METHOD -O $PROTOCOL -o $OBFS -G $PROTOCOLPARAM 32 | -------------------------------------------------------------------------------- /initmudbjson.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | bash initcfg.sh 4 | sed -i "s/API_INTERFACE = .\+\?\#/API_INTERFACE = \'mudbjson\' \#/g" userapiconfig.py 5 | ip_addr=`ifconfig -a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'|tr -d "addr:"` 6 | ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c` 7 | 8 | if [[ $ip_count == 1 ]]; then 9 | ip_addr=`ip a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'` 10 | ip_addr=${ip_addr%/*} 11 | ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c` 12 | fi 13 | if [[ $ip_count == 1 ]]; then 14 | echo "server IP is "${ip_addr} 15 | sed -i "s/SERVER_PUB_ADDR = .\+/SERVER_PUB_ADDR = \'"${ip_addr}"\'/g" userapiconfig.py 16 | user_count=`python mujson_mgr.py -l|grep -c -e "[0-9]"` 17 | if [[ $user_count == 0 ]]; then 18 | port=`python -c 'import random;print(random.randint(10000, 65536))'` 19 | python mujson_mgr.py -a -p ${port} 20 | fi 21 | else 22 | echo "unable to detect server IP" 23 | fi 24 | 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | Pull Requests 5 | ------------- 6 | 7 | 1. Pull requests are welcome. If you would like to add a large feature 8 | or make a significant change, make sure to open an issue to discuss with 9 | people first. 10 | 2. Follow PEP8. 11 | 3. Make sure to pass the unit tests. Write unit tests for new modules if 12 | needed. 13 | 14 | Issues 15 | ------ 16 | 17 | 1. Only bugs and feature requests are accepted here. 18 | 2. We'll only work on important features. If the feature you're asking only 19 | benefits a few people, you'd better implement the feature yourself and send us 20 | a pull request, or ask some of your friends to do so. 21 | 3. We don't answer questions of any other types here. Since very few people 22 | are watching the issue tracker here, you'll probably get no help from here. 23 | Read [Troubleshooting] and get help from forums or [mailing lists]. 24 | 4. Issues in languages other than English will be Google translated into English 25 | later. 26 | 27 | 28 | [Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting 29 | [mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks 30 | -------------------------------------------------------------------------------- /shadowsocks/encrypt_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, \ 2 | with_statement 3 | 4 | import sys 5 | import os 6 | 7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) 8 | 9 | 10 | from shadowsocks.crypto import rc4_md5 11 | from shadowsocks.crypto import openssl 12 | from shadowsocks.crypto import sodium 13 | from shadowsocks.crypto import table 14 | 15 | def run(func): 16 | try: 17 | func() 18 | except: 19 | pass 20 | 21 | def run_n(func, name): 22 | try: 23 | func(name) 24 | except: 25 | pass 26 | 27 | def main(): 28 | print("\n""rc4_md5") 29 | rc4_md5.test() 30 | print("\n""aes-256-cfb") 31 | openssl.test_aes_256_cfb() 32 | print("\n""aes-128-cfb") 33 | openssl.test_aes_128_cfb() 34 | print("\n""bf-cfb") 35 | run(openssl.test_bf_cfb) 36 | print("\n""camellia-128-cfb") 37 | run_n(openssl.run_method, "camellia-128-cfb") 38 | print("\n""cast5-cfb") 39 | run_n(openssl.run_method, "cast5-cfb") 40 | print("\n""idea-cfb") 41 | run_n(openssl.run_method, "idea-cfb") 42 | print("\n""seed-cfb") 43 | run_n(openssl.run_method, "seed-cfb") 44 | print("\n""salsa20") 45 | run(sodium.test_salsa20) 46 | print("\n""chacha20") 47 | run(sodium.test_chacha20) 48 | 49 | if __name__ == '__main__': 50 | main() 51 | 52 | -------------------------------------------------------------------------------- /tests/nose_plugin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | import nose 18 | from nose.plugins.base import Plugin 19 | 20 | 21 | class ExtensionPlugin(Plugin): 22 | 23 | name = "ExtensionPlugin" 24 | 25 | def options(self, parser, env): 26 | Plugin.options(self, parser, env) 27 | 28 | def configure(self, options, config): 29 | Plugin.configure(self, options, config) 30 | self.enabled = True 31 | 32 | def wantFile(self, file): 33 | return file.endswith('.py') 34 | 35 | def wantDirectory(self, directory): 36 | return True 37 | 38 | def wantModule(self, file): 39 | return True 40 | 41 | 42 | if __name__ == '__main__': 43 | nose.main(addplugins=[ExtensionPlugin()]) 44 | -------------------------------------------------------------------------------- /debian/sslocal.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" (C) Copyright 2014 Shell.Xu , 3 | .\" 4 | .TH SHADOWSOCKS 1 "August 23, 2014" 5 | .SH NAME 6 | shadowsocks \- Fast tunnel proxy that helps you bypass firewalls 7 | .SH SYNOPSIS 8 | .B ssserver 9 | .RI [ options ] 10 | .br 11 | .B sslocal 12 | .RI [ options ] 13 | .SH DESCRIPTION 14 | shadowsocks is a tunnel proxy helps you bypass firewall. 15 | .B ssserver 16 | is the server part, and 17 | .B sslocal 18 | is the local part. 19 | .SH OPTIONS 20 | .TP 21 | .B \-h, \-\-help 22 | Show this help message and exit. 23 | .TP 24 | .B \-s SERVER_ADDR 25 | Server address, default: 0.0.0.0. 26 | .TP 27 | .B \-p SERVER_PORT 28 | Server port, default: 8388. 29 | .TP 30 | .B \-k PASSWORD 31 | Password. 32 | .TP 33 | .B \-m METHOD 34 | Encryption method, default: aes-256-cfb. 35 | .TP 36 | .B \-t TIMEOUT 37 | Timeout in seconds, default: 300. 38 | .TP 39 | .B \-c CONFIG 40 | Path to config file. 41 | .TP 42 | .B \-\-fast-open 43 | Use TCP_FASTOPEN, requires Linux 3.7+. 44 | .TP 45 | .B \-\-workers WORKERS 46 | Number of workers, available on Unix/Linux. 47 | .TP 48 | .B \-v, \-vv 49 | Verbose mode. 50 | .TP 51 | .B \-q, \-qq 52 | Quiet mode, only show warnings/errors. 53 | .SH SEE ALSO 54 | .br 55 | The programs are documented fully by 56 | .IR "Shell Xu " 57 | and 58 | .IR "Clowwindy ", 59 | available via the Info system. 60 | -------------------------------------------------------------------------------- /debian/ssserver.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" (C) Copyright 2014 Shell.Xu , 3 | .\" 4 | .TH SHADOWSOCKS 1 "August 23, 2014" 5 | .SH NAME 6 | shadowsocks \- Fast tunnel proxy that helps you bypass firewalls 7 | .SH SYNOPSIS 8 | .B ssserver 9 | .RI [ options ] 10 | .br 11 | .B sslocal 12 | .RI [ options ] 13 | .SH DESCRIPTION 14 | shadowsocks is a tunnel proxy helps you bypass firewall. 15 | .B ssserver 16 | is the server part, and 17 | .B sslocal 18 | is the local part. 19 | .SH OPTIONS 20 | .TP 21 | .B \-h, \-\-help 22 | Show this help message and exit. 23 | .TP 24 | .B \-s SERVER_ADDR 25 | Server address, default: 0.0.0.0. 26 | .TP 27 | .B \-p SERVER_PORT 28 | Server port, default: 8388. 29 | .TP 30 | .B \-k PASSWORD 31 | Password. 32 | .TP 33 | .B \-m METHOD 34 | Encryption method, default: aes-256-cfb. 35 | .TP 36 | .B \-t TIMEOUT 37 | Timeout in seconds, default: 300. 38 | .TP 39 | .B \-c CONFIG 40 | Path to config file. 41 | .TP 42 | .B \-\-fast-open 43 | Use TCP_FASTOPEN, requires Linux 3.7+. 44 | .TP 45 | .B \-\-workers WORKERS 46 | Number of workers, available on Unix/Linux. 47 | .TP 48 | .B \-v, \-vv 49 | Verbose mode. 50 | .TP 51 | .B \-q, \-qq 52 | Quiet mode, only show warnings/errors. 53 | .SH SEE ALSO 54 | .br 55 | The programs are documented fully by 56 | .IR "Shell Xu " 57 | and 58 | .IR "Clowwindy ", 59 | available via the Info system. 60 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: shadowsocks 3 | Source: https://github.com/clowwindy/shadowsocks 4 | 5 | Files: debian/* 6 | Copyright: 2014 Shell.Xu 7 | License: Expat 8 | 9 | Files: * 10 | Copyright: 2014 clowwindy 11 | License: Expat 12 | 13 | License: Expat 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | . 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | . 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | from setuptools import setup 3 | 4 | 5 | with codecs.open('README.rst', encoding='utf-8') as f: 6 | long_description = f.read() 7 | 8 | setup( 9 | name="shadowsocks", 10 | version="2.6.12", 11 | license='http://www.apache.org/licenses/LICENSE-2.0', 12 | description="A fast tunnel proxy that help you get through firewalls", 13 | author='clowwindy', 14 | author_email='clowwindy42@gmail.com', 15 | url='https://github.com/shadowsocks/shadowsocks', 16 | packages=['shadowsocks', 'shadowsocks.crypto', 'shadowsocks.obfsplugin'], 17 | package_data={ 18 | 'shadowsocks': ['README.rst', 'LICENSE'] 19 | }, 20 | install_requires=[], 21 | entry_points=""" 22 | [console_scripts] 23 | sslocal = shadowsocks.local:main 24 | ssserver = shadowsocks.server:main 25 | """, 26 | classifiers=[ 27 | 'License :: OSI Approved :: Apache Software License', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.6', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Programming Language :: Python :: Implementation :: CPython', 35 | 'Programming Language :: Python :: Implementation :: PyPy', 36 | 'Topic :: Internet :: Proxy Servers', 37 | ], 38 | long_description=long_description, 39 | ) 40 | -------------------------------------------------------------------------------- /shadowsocks/crypto/rc4_md5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import hashlib 21 | 22 | from shadowsocks.crypto import openssl 23 | 24 | __all__ = ['ciphers'] 25 | 26 | 27 | def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, 28 | i=1, padding=1): 29 | md5 = hashlib.md5() 30 | md5.update(key) 31 | md5.update(iv) 32 | rc4_key = md5.digest() 33 | return openssl.OpenSSLCrypto(b'rc4', rc4_key, b'', op) 34 | 35 | 36 | ciphers = { 37 | 'rc4-md5': (16, 16, create_cipher), 38 | 'rc4-md5-6': (16, 6, create_cipher), 39 | } 40 | 41 | 42 | def test(): 43 | from shadowsocks.crypto import util 44 | 45 | cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1) 46 | decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0) 47 | 48 | util.run_cipher(cipher, decipher) 49 | 50 | 51 | if __name__ == '__main__': 52 | test() 53 | -------------------------------------------------------------------------------- /tests/test_daemon.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function run_test { 4 | expected=$1 5 | shift 6 | echo "running test: $command $@" 7 | $command $@ 8 | status=$? 9 | if [ $status -ne $expected ]; then 10 | echo "exit $status != $expected" 11 | exit 1 12 | fi 13 | echo "exit status $status == $expected" 14 | echo OK 15 | return 16 | } 17 | 18 | for module in local server 19 | do 20 | 21 | command="coverage run -p shadowsocks/$module.py" 22 | 23 | mkdir -p tmp 24 | 25 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 26 | 27 | run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 28 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 29 | 30 | run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 31 | run_test 1 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 32 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 33 | 34 | run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 35 | run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 36 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 37 | 38 | run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 39 | run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log 40 | 41 | run_test 1 -c tests/aes.json -d start --pid-file tmp/not_exist/shadowsocks.pid --log-file tmp/shadowsocks.log 42 | 43 | done 44 | -------------------------------------------------------------------------------- /tests/coverage_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | if __name__ == '__main__': 18 | import tornado.ioloop 19 | import tornado.web 20 | import urllib 21 | 22 | class MainHandler(tornado.web.RequestHandler): 23 | def get(self, project): 24 | try: 25 | with open('/tmp/%s-coverage' % project, 'rb') as f: 26 | coverage = f.read().strip() 27 | n = int(coverage.strip('%')) 28 | if n >= 80: 29 | color = 'brightgreen' 30 | else: 31 | color = 'yellow' 32 | self.redirect(('https://img.shields.io/badge/' 33 | 'coverage-%s-%s.svg' 34 | '?style=flat') % 35 | (urllib.quote(coverage), color)) 36 | except IOError: 37 | raise tornado.web.HTTPError(404) 38 | 39 | application = tornado.web.Application([ 40 | (r"/([a-zA-Z0-9\-_]+)", MainHandler), 41 | ]) 42 | 43 | if __name__ == "__main__": 44 | application.listen(8888, address='127.0.0.1') 45 | tornado.ioloop.IOLoop.instance().start() 46 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 breakwall 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import time 19 | import sys 20 | import threading 21 | import os 22 | 23 | if __name__ == '__main__': 24 | import inspect 25 | os.chdir(os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))) 26 | 27 | import server_pool 28 | import db_transfer 29 | from shadowsocks import shell 30 | from configloader import load_config, get_config 31 | 32 | class MainThread(threading.Thread): 33 | def __init__(self, obj): 34 | super(MainThread, self).__init__() 35 | self.daemon = True 36 | self.obj = obj 37 | 38 | def run(self): 39 | self.obj.thread_db(self.obj) 40 | 41 | def stop(self): 42 | self.obj.thread_db_stop() 43 | 44 | def main(): 45 | shell.check_python() 46 | if False: 47 | db_transfer.DbTransfer.thread_db() 48 | else: 49 | if get_config().API_INTERFACE == 'mudbjson': 50 | thread = MainThread(db_transfer.MuJsonTransfer) 51 | elif get_config().API_INTERFACE == 'sspanelv2': 52 | thread = MainThread(db_transfer.DbTransfer) 53 | else: 54 | thread = MainThread(db_transfer.Dbv3Transfer) 55 | thread.start() 56 | try: 57 | while thread.is_alive(): 58 | thread.join(10.0) 59 | except (KeyboardInterrupt, IOError, OSError) as e: 60 | import traceback 61 | traceback.print_exc() 62 | thread.stop() 63 | 64 | if __name__ == '__main__': 65 | main() 66 | 67 | -------------------------------------------------------------------------------- /tests/test_command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . tests/assert.sh 4 | 5 | PYTHON="coverage run -p" 6 | LOCAL="$PYTHON shadowsocks/local.py" 7 | SERVER="$PYTHON shadowsocks/server.py" 8 | 9 | assert "$LOCAL --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" 10 | assert "$SERVER --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks" 11 | 12 | 13 | assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: server set to listen on 127.0.0.1:8388, are you sure?" 14 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 15 | 16 | assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 0.0.0.0 -p 8388 -t10 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: your timeout 10 seems too short" 17 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 18 | 19 | assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 0.0.0.0 -p 8388 -t1000 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: your timeout 1000 seems too long" 20 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 21 | 22 | assert "$LOCAL 2>&1 -m rc4 -k testrc4 -s 0.0.0.0 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: RC4 is not safe; please use a safer cipher, like AES-256-CFB" 23 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 24 | 25 | 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!" 26 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 27 | 28 | 29 | 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" 30 | $LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop 31 | 32 | assert_end command 33 | -------------------------------------------------------------------------------- /utils/autoban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2015 clowwindy 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | from __future__ import absolute_import, division, print_function, \ 25 | with_statement 26 | 27 | import os 28 | import sys 29 | import argparse 30 | 31 | if __name__ == '__main__': 32 | parser = argparse.ArgumentParser(description='See README') 33 | parser.add_argument('-c', '--count', default=3, type=int, 34 | help='with how many failure times it should be ' 35 | 'considered as an attack') 36 | config = parser.parse_args() 37 | ips = {} 38 | banned = set() 39 | for line in sys.stdin: 40 | if 'can not parse header when' in line: 41 | ip = line.split()[-1].split(':')[0] 42 | if ip not in ips: 43 | ips[ip] = 1 44 | print(ip) 45 | sys.stdout.flush() 46 | else: 47 | ips[ip] += 1 48 | if ip not in banned and ips[ip] >= config.count: 49 | banned.add(ip) 50 | cmd = 'iptables -A INPUT -s %s -j DROP' % ip 51 | print(cmd, file=sys.stderr) 52 | sys.stderr.flush() 53 | os.system(cmd) 54 | -------------------------------------------------------------------------------- /tests/test_udp_src.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import socket 4 | import socks 5 | 6 | 7 | SERVER_IP = '127.0.0.1' 8 | SERVER_PORT = 1081 9 | 10 | 11 | if __name__ == '__main__': 12 | # Test 1: same source port IPv4 13 | sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, 14 | socket.SOL_UDP) 15 | sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) 16 | sock_out.bind(('127.0.0.1', 9000)) 17 | 18 | sock_in1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 19 | socket.SOL_UDP) 20 | sock_in2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 21 | socket.SOL_UDP) 22 | 23 | sock_in1.bind(('127.0.0.1', 9001)) 24 | sock_in2.bind(('127.0.0.1', 9002)) 25 | 26 | sock_out.sendto(b'data', ('127.0.0.1', 9001)) 27 | result1 = sock_in1.recvfrom(8) 28 | 29 | sock_out.sendto(b'data', ('127.0.0.1', 9002)) 30 | result2 = sock_in2.recvfrom(8) 31 | 32 | sock_out.close() 33 | sock_in1.close() 34 | sock_in2.close() 35 | 36 | # make sure they're from the same source port 37 | assert result1 == result2 38 | 39 | # Test 2: same source port IPv6 40 | # try again from the same port but IPv6 41 | sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, 42 | socket.SOL_UDP) 43 | sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) 44 | sock_out.bind(('127.0.0.1', 9000)) 45 | 46 | sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 47 | socket.SOL_UDP) 48 | sock_in2 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 49 | socket.SOL_UDP) 50 | 51 | sock_in1.bind(('::1', 9001)) 52 | sock_in2.bind(('::1', 9002)) 53 | 54 | sock_out.sendto(b'data', ('::1', 9001)) 55 | result1 = sock_in1.recvfrom(8) 56 | 57 | sock_out.sendto(b'data', ('::1', 9002)) 58 | result2 = sock_in2.recvfrom(8) 59 | 60 | sock_out.close() 61 | sock_in1.close() 62 | sock_in2.close() 63 | 64 | # make sure they're from the same source port 65 | assert result1 == result2 66 | 67 | # Test 3: different source ports IPv6 68 | sock_out = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM, 69 | socket.SOL_UDP) 70 | sock_out.set_proxy(socks.SOCKS5, SERVER_IP, SERVER_PORT) 71 | sock_out.bind(('127.0.0.1', 9003)) 72 | 73 | sock_in1 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, 74 | socket.SOL_UDP) 75 | sock_in1.bind(('::1', 9001)) 76 | sock_out.sendto(b'data', ('::1', 9001)) 77 | result3 = sock_in1.recvfrom(8) 78 | 79 | # make sure they're from different source ports 80 | assert result1 != result3 81 | 82 | sock_out.close() 83 | sock_in1.close() 84 | -------------------------------------------------------------------------------- /shadowsocks/local.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2012-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import sys 22 | import os 23 | import logging 24 | import signal 25 | 26 | if __name__ == '__main__': 27 | import inspect 28 | file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe()))) 29 | sys.path.insert(0, os.path.join(file_path, '../')) 30 | 31 | from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns 32 | 33 | 34 | def main(): 35 | shell.check_python() 36 | 37 | # fix py2exe 38 | if hasattr(sys, "frozen") and sys.frozen in \ 39 | ("windows_exe", "console_exe"): 40 | p = os.path.dirname(os.path.abspath(sys.executable)) 41 | os.chdir(p) 42 | 43 | config = shell.get_config(True) 44 | 45 | if not config.get('dns_ipv6', False): 46 | asyncdns.IPV6_CONNECTION_SUPPORT = False 47 | 48 | daemon.daemon_exec(config) 49 | logging.info("local start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" % 50 | (config['protocol'], config['password'], config['method'], config['obfs'], config['obfs_param'])) 51 | 52 | try: 53 | logging.info("starting local at %s:%d" % 54 | (config['local_address'], config['local_port'])) 55 | 56 | dns_resolver = asyncdns.DNSResolver() 57 | tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) 58 | udp_server = udprelay.UDPRelay(config, dns_resolver, True) 59 | loop = eventloop.EventLoop() 60 | dns_resolver.add_to_loop(loop) 61 | tcp_server.add_to_loop(loop) 62 | udp_server.add_to_loop(loop) 63 | 64 | def handler(signum, _): 65 | logging.warn('received SIGQUIT, doing graceful shutting down..') 66 | tcp_server.close(next_tick=True) 67 | udp_server.close(next_tick=True) 68 | signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) 69 | 70 | def int_handler(signum, _): 71 | sys.exit(1) 72 | signal.signal(signal.SIGINT, int_handler) 73 | 74 | daemon.set_user(config.get('user', None)) 75 | loop.run() 76 | except Exception as e: 77 | shell.print_exception(e) 78 | sys.exit(1) 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/plain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | 25 | from shadowsocks.common import ord 26 | 27 | def create_obfs(method): 28 | return plain(method) 29 | 30 | obfs_map = { 31 | 'plain': (create_obfs,), 32 | 'origin': (create_obfs,), 33 | } 34 | 35 | class plain(object): 36 | def __init__(self, method): 37 | self.method = method 38 | self.server_info = None 39 | 40 | def init_data(self): 41 | return b'' 42 | 43 | def get_overhead(self, direction): # direction: true for c->s false for s->c 44 | return 0 45 | 46 | def get_server_info(self): 47 | return self.server_info 48 | 49 | def set_server_info(self, server_info): 50 | self.server_info = server_info 51 | 52 | def client_pre_encrypt(self, buf): 53 | return buf 54 | 55 | def client_encode(self, buf): 56 | return buf 57 | 58 | def client_decode(self, buf): 59 | # (buffer_to_recv, is_need_to_encode_and_send_back) 60 | return (buf, False) 61 | 62 | def client_post_decrypt(self, buf): 63 | return buf 64 | 65 | def server_pre_encrypt(self, buf): 66 | return buf 67 | 68 | def server_encode(self, buf): 69 | return buf 70 | 71 | def server_decode(self, buf): 72 | # (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back) 73 | return (buf, True, False) 74 | 75 | def server_post_decrypt(self, buf): 76 | return (buf, False) 77 | 78 | def client_udp_pre_encrypt(self, buf): 79 | return buf 80 | 81 | def client_udp_post_decrypt(self, buf): 82 | return buf 83 | 84 | def server_udp_pre_encrypt(self, buf, uid): 85 | return buf 86 | 87 | def server_udp_post_decrypt(self, buf): 88 | return (buf, None) 89 | 90 | def dispose(self): 91 | pass 92 | 93 | def get_head_size(self, buf, def_value): 94 | if len(buf) < 2: 95 | return def_value 96 | head_type = ord(buf[0]) & 0x7 97 | if head_type == 1: 98 | return 7 99 | if head_type == 4: 100 | return 19 101 | if head_type == 3: 102 | return 4 + ord(buf[1]) 103 | return def_value 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ShadowsocksR 2 | =========== 3 | 4 | [![Build Status]][Travis CI] 5 | 6 | A fast tunnel proxy that helps you bypass firewalls. 7 | 8 | Server 9 | ------ 10 | 11 | ### Install 12 | 13 | Debian / Ubuntu: 14 | 15 | apt-get install git 16 | git clone https://github.com/shadowsocksr/shadowsocksr.git 17 | 18 | CentOS: 19 | 20 | yum install git 21 | git clone https://github.com/shadowsocksr/shadowsocksr.git 22 | 23 | Windows: 24 | 25 | git clone https://github.com/shadowsocksr/shadowsocksr.git 26 | 27 | ### Usage for single user on linux platform 28 | 29 | If you clone it into "~/shadowsocksr" 30 | move to "~/shadowsocksr", then run: 31 | 32 | bash initcfg.sh 33 | 34 | move to "~/shadowsocksr/shadowsocks", then run: 35 | 36 | python server.py -p 443 -k password -m aes-128-cfb -O auth_aes128_md5 -o tls1.2_ticket_auth_compatible 37 | 38 | Check all the options via `-h`. 39 | 40 | You can also use a configuration file instead (recommend), move to "~/shadowsocksr" and edit the file "user-config.json", then move to "~/shadowsocksr/shadowsocks" again, just run: 41 | 42 | python server.py 43 | 44 | To run in the background: 45 | 46 | ./logrun.sh 47 | 48 | To stop: 49 | 50 | ./stop.sh 51 | 52 | To monitor the log: 53 | 54 | ./tail.sh 55 | 56 | 57 | Client 58 | ------ 59 | 60 | * [Windows] / [macOS] 61 | * [Android] / [iOS] 62 | * [OpenWRT] 63 | 64 | Use GUI clients on your local PC/phones. Check the README of your client 65 | for more information. 66 | 67 | Documentation 68 | ------------- 69 | 70 | You can find all the documentation in the [Wiki]. 71 | 72 | License 73 | ------- 74 | 75 | Copyright 2015 clowwindy 76 | 77 | Licensed under the Apache License, Version 2.0 (the "License"); you may 78 | not use this file except in compliance with the License. You may obtain 79 | a copy of the License at 80 | 81 | http://www.apache.org/licenses/LICENSE-2.0 82 | 83 | Unless required by applicable law or agreed to in writing, software 84 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 85 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 86 | License for the specific language governing permissions and limitations 87 | under the License. 88 | 89 | Bugs and Issues 90 | ---------------- 91 | 92 | * [Issue Tracker] 93 | 94 | 95 | 96 | [Android]: https://github.com/shadowsocksr/shadowsocksr-android 97 | [Build Status]: https://travis-ci.org/shadowsocksr/shadowsocksr.svg?branch=manyuser 98 | [Debian sid]: https://packages.debian.org/unstable/python/shadowsocks 99 | [iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help 100 | [Issue Tracker]: https://github.com/shadowsocksr/shadowsocksr/issues?state=open 101 | [OpenWRT]: https://github.com/shadowsocks/openwrt-shadowsocks 102 | [macOS]: https://github.com/shadowsocksr/ShadowsocksX-NG 103 | [Travis CI]: https://travis-ci.org/shadowsocksr/shadowsocksr 104 | [Windows]: https://github.com/shadowsocksr/shadowsocksr-csharp 105 | [Wiki]: https://github.com/breakwa11/shadowsocks-rss/wiki 106 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | shadowsocks 2 | =========== 3 | 4 | |PyPI version| |Build Status| |Coverage Status| 5 | 6 | A fast tunnel proxy that helps you bypass firewalls. 7 | 8 | Server 9 | ------ 10 | 11 | Install 12 | ~~~~~~~ 13 | 14 | Debian / Ubuntu: 15 | 16 | :: 17 | 18 | apt-get install python-pip 19 | pip install shadowsocks 20 | 21 | CentOS: 22 | 23 | :: 24 | 25 | yum install python-setuptools && easy_install pip 26 | pip install shadowsocks 27 | 28 | Windows: 29 | 30 | See `Install Server on 31 | Windows `__ 32 | 33 | Usage 34 | ~~~~~ 35 | 36 | :: 37 | 38 | ssserver -p 443 -k password -m rc4-md5 39 | 40 | To run in the background: 41 | 42 | :: 43 | 44 | sudo ssserver -p 443 -k password -m rc4-md5 --user nobody -d start 45 | 46 | To stop: 47 | 48 | :: 49 | 50 | sudo ssserver -d stop 51 | 52 | To check the log: 53 | 54 | :: 55 | 56 | sudo less /var/log/shadowsocks.log 57 | 58 | Check all the options via ``-h``. You can also use a 59 | `Configuration `__ 60 | file instead. 61 | 62 | Client 63 | ------ 64 | 65 | - `Windows `__ 66 | / `OS 67 | X `__ 68 | - `Android `__ 69 | / `iOS `__ 70 | - `OpenWRT `__ 71 | 72 | Use GUI clients on your local PC/phones. Check the README of your client 73 | for more information. 74 | 75 | Documentation 76 | ------------- 77 | 78 | You can find all the documentation in the 79 | `Wiki `__. 80 | 81 | License 82 | ------- 83 | 84 | Copyright 2015 clowwindy 85 | 86 | Licensed under the Apache License, Version 2.0 (the "License"); you may 87 | not use this file except in compliance with the License. You may obtain 88 | a copy of the License at 89 | 90 | :: 91 | 92 | http://www.apache.org/licenses/LICENSE-2.0 93 | 94 | Unless required by applicable law or agreed to in writing, software 95 | distributed under the License is distributed on an "AS IS" BASIS, 96 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 97 | See the License for the specific language governing permissions and 98 | limitations under the License. 99 | 100 | Bugs and Issues 101 | --------------- 102 | 103 | - `Troubleshooting `__ 104 | - `Issue 105 | Tracker `__ 106 | - `Mailing list `__ 107 | 108 | .. |PyPI version| image:: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat 109 | :target: https://pypi.python.org/pypi/shadowsocks 110 | .. |Build Status| image:: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat 111 | :target: https://travis-ci.org/shadowsocks/shadowsocks 112 | .. |Coverage Status| image:: https://jenkins.shadowvpn.org/result/shadowsocks 113 | :target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html 114 | -------------------------------------------------------------------------------- /asyncmgr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014 clowwindy 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | import time 25 | import os 26 | import socket 27 | import struct 28 | import re 29 | import logging 30 | from shadowsocks import common 31 | from shadowsocks import lru_cache 32 | from shadowsocks import eventloop 33 | import server_pool 34 | import Config 35 | 36 | class ServerMgr(object): 37 | 38 | def __init__(self): 39 | self._loop = None 40 | self._request_id = 1 41 | self._hosts = {} 42 | self._hostname_status = {} 43 | self._hostname_to_cb = {} 44 | self._cb_to_hostname = {} 45 | self._last_time = time.time() 46 | self._sock = None 47 | self._servers = None 48 | 49 | def add_to_loop(self, loop): 50 | if self._loop: 51 | raise Exception('already add to loop') 52 | self._loop = loop 53 | # TODO when dns server is IPv6 54 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 55 | socket.SOL_UDP) 56 | self._sock.bind((Config.MANAGE_BIND_IP, Config.MANAGE_PORT)) 57 | self._sock.setblocking(False) 58 | loop.add(self._sock, eventloop.POLL_IN, self) 59 | 60 | def _handle_data(self, sock): 61 | data, addr = sock.recvfrom(128) 62 | #manage pwd:port:passwd:action 63 | args = data.split(':') 64 | if len(args) < 4: 65 | return 66 | if args[0] == Config.MANAGE_PASS: 67 | if args[3] == '0': 68 | server_pool.ServerPool.get_instance().cb_del_server(args[1]) 69 | elif args[3] == '1': 70 | server_pool.ServerPool.get_instance().new_server(args[1], args[2]) 71 | 72 | def handle_event(self, sock, fd, event): 73 | if sock != self._sock: 74 | return 75 | if event & eventloop.POLL_ERR: 76 | logging.error('mgr socket err') 77 | self._loop.remove(self._sock) 78 | self._sock.close() 79 | # TODO when dns server is IPv6 80 | self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 81 | socket.SOL_UDP) 82 | self._sock.setblocking(False) 83 | self._loop.add(self._sock, eventloop.POLL_IN, self) 84 | else: 85 | self._handle_data(sock) 86 | 87 | def close(self): 88 | if self._sock: 89 | if self._loop: 90 | self._loop.remove(self._sock) 91 | self._sock.close() 92 | self._sock = None 93 | 94 | 95 | def test(): 96 | pass 97 | 98 | if __name__ == '__main__': 99 | test() 100 | -------------------------------------------------------------------------------- /shadowsocks/obfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | 25 | from shadowsocks import common 26 | from shadowsocks.obfsplugin import plain, http_simple, obfs_tls, verify, auth, auth_chain 27 | 28 | 29 | method_supported = {} 30 | method_supported.update(plain.obfs_map) 31 | method_supported.update(http_simple.obfs_map) 32 | method_supported.update(obfs_tls.obfs_map) 33 | method_supported.update(verify.obfs_map) 34 | method_supported.update(auth.obfs_map) 35 | method_supported.update(auth_chain.obfs_map) 36 | 37 | def mu_protocol(): 38 | return ["auth_aes128_md5", "auth_aes128_sha1", "auth_chain_a"] 39 | 40 | class server_info(object): 41 | def __init__(self, data): 42 | self.data = data 43 | 44 | class obfs(object): 45 | def __init__(self, method): 46 | method = common.to_str(method) 47 | self.method = method 48 | self._method_info = self.get_method_info(method) 49 | if self._method_info: 50 | self.obfs = self.get_obfs(method) 51 | else: 52 | raise Exception('obfs plugin [%s] not supported' % method) 53 | 54 | def init_data(self): 55 | return self.obfs.init_data() 56 | 57 | def set_server_info(self, server_info): 58 | return self.obfs.set_server_info(server_info) 59 | 60 | def get_server_info(self): 61 | return self.obfs.get_server_info() 62 | 63 | def get_method_info(self, method): 64 | method = method.lower() 65 | m = method_supported.get(method) 66 | return m 67 | 68 | def get_obfs(self, method): 69 | m = self._method_info 70 | return m[0](method) 71 | 72 | def get_overhead(self, direction): 73 | return self.obfs.get_overhead(direction) 74 | 75 | def client_pre_encrypt(self, buf): 76 | return self.obfs.client_pre_encrypt(buf) 77 | 78 | def client_encode(self, buf): 79 | return self.obfs.client_encode(buf) 80 | 81 | def client_decode(self, buf): 82 | return self.obfs.client_decode(buf) 83 | 84 | def client_post_decrypt(self, buf): 85 | return self.obfs.client_post_decrypt(buf) 86 | 87 | def server_pre_encrypt(self, buf): 88 | return self.obfs.server_pre_encrypt(buf) 89 | 90 | def server_encode(self, buf): 91 | return self.obfs.server_encode(buf) 92 | 93 | def server_decode(self, buf): 94 | return self.obfs.server_decode(buf) 95 | 96 | def server_post_decrypt(self, buf): 97 | return self.obfs.server_post_decrypt(buf) 98 | 99 | def client_udp_pre_encrypt(self, buf): 100 | return self.obfs.client_udp_pre_encrypt(buf) 101 | 102 | def client_udp_post_decrypt(self, buf): 103 | return self.obfs.client_udp_post_decrypt(buf) 104 | 105 | def server_udp_pre_encrypt(self, buf, uid): 106 | return self.obfs.server_udp_pre_encrypt(buf, uid) 107 | 108 | def server_udp_post_decrypt(self, buf): 109 | return self.obfs.server_udp_post_decrypt(buf) 110 | 111 | def dispose(self): 112 | self.obfs.dispose() 113 | del self.obfs 114 | 115 | -------------------------------------------------------------------------------- /tests/jenkins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | result=0 4 | 5 | function run_test { 6 | printf '\e[0;36m' 7 | echo "running test: $command $@" 8 | printf '\e[0m' 9 | 10 | $command "$@" 11 | status=$? 12 | if [ $status -ne 0 ]; then 13 | printf '\e[0;31m' 14 | echo "test failed: $command $@" 15 | printf '\e[0m' 16 | echo 17 | result=1 18 | else 19 | printf '\e[0;32m' 20 | echo OK 21 | printf '\e[0m' 22 | echo 23 | fi 24 | return 0 25 | } 26 | 27 | python --version 28 | coverage erase 29 | mkdir tmp 30 | run_test pep8 --ignore=E402 . 31 | run_test pyflakes . 32 | run_test coverage run tests/nose_plugin.py -v 33 | run_test python setup.py sdist 34 | run_test tests/test_daemon.sh 35 | run_test python tests/test.py --with-coverage -c tests/aes.json 36 | run_test python tests/test.py --with-coverage -c tests/aes-ctr.json 37 | run_test python tests/test.py --with-coverage -c tests/aes-cfb1.json 38 | run_test python tests/test.py --with-coverage -c tests/aes-cfb8.json 39 | run_test python tests/test.py --with-coverage -c tests/rc4-md5.json 40 | run_test python tests/test.py --with-coverage -c tests/salsa20.json 41 | run_test python tests/test.py --with-coverage -c tests/chacha20.json 42 | run_test python tests/test.py --with-coverage -c tests/table.json 43 | run_test python tests/test.py --with-coverage -c tests/server-multi-ports.json 44 | run_test python tests/test.py --with-coverage -s tests/aes.json -c tests/client-multi-server-ip.json 45 | run_test python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json 46 | run_test python tests/test.py --with-coverage -c tests/workers.json 47 | run_test python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json 48 | 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" 49 | 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" 50 | 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" 51 | 52 | # test if DNS works 53 | run_test python tests/test.py --with-coverage -c tests/aes.json --url="https://clients1.google.com/generate_204" 54 | 55 | # test localhost is in the forbidden list by default 56 | run_test python tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" 57 | 58 | # test localhost is available when forbidden list is empty 59 | run_test python tests/test.py --with-coverage --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1" 60 | 61 | if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then 62 | if [ 3 -eq `cat /proc/sys/net/ipv4/tcp_fastopen` ] ; then 63 | # we have to run it twice: 64 | # the first time there's no syn cookie 65 | # the second time there is syn cookie 66 | run_test python tests/test.py --with-coverage -c tests/fastopen.json 67 | run_test python tests/test.py --with-coverage -c tests/fastopen.json 68 | fi 69 | fi 70 | 71 | run_test tests/test_large_file.sh 72 | run_test tests/test_udp_src.sh 73 | run_test tests/test_command.sh 74 | 75 | coverage combine && coverage report --include=shadowsocks/* 76 | rm -rf htmlcov 77 | rm -rf tmp 78 | coverage html --include=shadowsocks/* 79 | 80 | coverage report --include=shadowsocks/* | tail -n1 | rev | cut -d' ' -f 1 | rev > /tmp/shadowsocks-coverage 81 | 82 | exit $result 83 | -------------------------------------------------------------------------------- /debian/init.d: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: shadowsocks 4 | # Required-Start: $network $local_fs $remote_fs 5 | # Required-Stop: $network $local_fs $remote_fs 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: Fast tunnel proxy that helps you bypass firewalls 9 | # Description: A secure socks5 proxy, designed to protect your Internet traffic. 10 | # This package contain local and server part of shadowsocks, a fast, 11 | # powerful tunnel proxy to bypass firewalls. 12 | ### END INIT INFO 13 | 14 | # Author: Shell.Xu 15 | 16 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 17 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 18 | DESC=shadowsocks # Introduce a short description here 19 | NAME=shadowsocks # Introduce the short server's name here 20 | DAEMON=/usr/bin/ssserver # Introduce the server's location here 21 | DAEMON_ARGS="" # Arguments to run the daemon with 22 | PIDFILE=/var/run/$NAME.pid 23 | SCRIPTNAME=/etc/init.d/$NAME 24 | LOGFILE=/var/log/$NAME.log 25 | 26 | # Exit if the package is not installed 27 | [ -x $DAEMON ] || exit 0 28 | 29 | # Read configuration variable file if it is present 30 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 31 | 32 | # Load the VERBOSE setting and other rcS variables 33 | . /lib/init/vars.sh 34 | 35 | # Define LSB log_* functions. 36 | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. 37 | . /lib/lsb/init-functions 38 | 39 | # 40 | # Function that starts the daemon/service 41 | # 42 | do_start() 43 | { 44 | # Return 45 | # 0 if daemon has been started 46 | # 1 if daemon was already running 47 | # 2 if daemon could not be started 48 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ 49 | --background --make-pidfile --chdir / --chuid $USERID --no-close --test > /dev/null \ 50 | || return 1 51 | start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON \ 52 | --background --make-pidfile --chdir / --chuid $USERID --no-close -- \ 53 | $DAEMON_ARGS $DAEMON_OPTS >> $LOGFILE 2>&1 \ 54 | || return 2 55 | # Add code here, if necessary, that waits for the process to be ready 56 | # to handle requests from services started subsequently which depend 57 | # on this one. As a last resort, sleep for some time. 58 | } 59 | 60 | # 61 | # Function that stops the daemon/service 62 | # 63 | do_stop() 64 | { 65 | # Return 66 | # 0 if daemon has been stopped 67 | # 1 if daemon was already stopped 68 | # 2 if daemon could not be stopped 69 | # other if a failure occurred 70 | start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE 71 | RETVAL="$?" 72 | [ "$RETVAL" = 2 ] && return 2 73 | # Many daemons don't delete their pidfiles when they exit. 74 | rm -f $PIDFILE 75 | return "$RETVAL" 76 | } 77 | 78 | # 79 | # Function that sends a SIGHUP to the daemon/service 80 | # 81 | do_reload() { 82 | # 83 | # If the daemon can reload its configuration without 84 | # restarting (for example, when it is sent a SIGHUP), 85 | # then implement that here. 86 | # 87 | start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME 88 | return 0 89 | } 90 | 91 | case "$1" in 92 | start) 93 | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC " "$NAME" 94 | do_start 95 | case "$?" in 96 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 97 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 98 | esac 99 | ;; 100 | stop) 101 | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" 102 | do_stop 103 | case "$?" in 104 | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 105 | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; 106 | esac 107 | ;; 108 | status) 109 | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? 110 | ;; 111 | #reload|force-reload) 112 | # 113 | # If do_reload() is not implemented then leave this commented out 114 | # and leave 'force-reload' as an alias for 'restart'. 115 | # 116 | #log_daemon_msg "Reloading $DESC" "$NAME" 117 | #do_reload 118 | #log_end_msg $? 119 | #;; 120 | restart|force-reload) 121 | # 122 | # If the "reload" option is implemented then remove the 123 | # 'force-reload' alias 124 | # 125 | log_daemon_msg "Restarting $DESC" "$NAME" 126 | do_stop 127 | case "$?" in 128 | 0|1) 129 | do_start 130 | case "$?" in 131 | 0) log_end_msg 0 ;; 132 | 1) log_end_msg 1 ;; # Old process is still running 133 | *) log_end_msg 1 ;; # Failed to start 134 | esac 135 | ;; 136 | *) 137 | # Failed to stop 138 | log_end_msg 1 139 | ;; 140 | esac 141 | ;; 142 | *) 143 | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 144 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 145 | exit 3 146 | ;; 147 | esac 148 | 149 | : 150 | -------------------------------------------------------------------------------- /shadowsocks/crypto/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import logging 22 | 23 | 24 | def find_library_nt(name): 25 | # modified from ctypes.util 26 | # ctypes.util.find_library just returns first result he found 27 | # but we want to try them all 28 | # because on Windows, users may have both 32bit and 64bit version installed 29 | results = [] 30 | for directory in os.environ['PATH'].split(os.pathsep): 31 | fname = os.path.join(directory, name) 32 | if os.path.isfile(fname): 33 | results.append(fname) 34 | if fname.lower().endswith(".dll"): 35 | continue 36 | fname = fname + ".dll" 37 | if os.path.isfile(fname): 38 | results.append(fname) 39 | return results 40 | 41 | 42 | def find_library(possible_lib_names, search_symbol, library_name): 43 | import ctypes.util 44 | from ctypes import CDLL 45 | 46 | paths = [] 47 | 48 | if type(possible_lib_names) not in (list, tuple): 49 | possible_lib_names = [possible_lib_names] 50 | 51 | lib_names = [] 52 | for lib_name in possible_lib_names: 53 | lib_names.append(lib_name) 54 | lib_names.append('lib' + lib_name) 55 | 56 | for name in lib_names: 57 | if os.name == "nt": 58 | paths.extend(find_library_nt(name)) 59 | else: 60 | path = ctypes.util.find_library(name) 61 | if path: 62 | paths.append(path) 63 | 64 | if not paths: 65 | # We may get here when find_library fails because, for example, 66 | # the user does not have sufficient privileges to access those 67 | # tools underlying find_library on linux. 68 | import glob 69 | 70 | for name in lib_names: 71 | patterns = [ 72 | '/usr/local/lib*/lib%s.*' % name, 73 | '/usr/lib*/lib%s.*' % name, 74 | 'lib%s.*' % name, 75 | '%s.dll' % name] 76 | 77 | for pat in patterns: 78 | files = glob.glob(pat) 79 | if files: 80 | paths.extend(files) 81 | for path in paths: 82 | try: 83 | lib = CDLL(path) 84 | if hasattr(lib, search_symbol): 85 | logging.info('loading %s from %s', library_name, path) 86 | return lib 87 | else: 88 | logging.warn('can\'t find symbol %s in %s', search_symbol, 89 | path) 90 | except Exception: 91 | if path == paths[-1]: 92 | raise 93 | return None 94 | 95 | 96 | def run_cipher(cipher, decipher): 97 | from os import urandom 98 | import random 99 | import time 100 | 101 | BLOCK_SIZE = 16384 102 | rounds = 1 * 1024 103 | plain = urandom(BLOCK_SIZE * rounds) 104 | 105 | results = [] 106 | pos = 0 107 | print('test start') 108 | start = time.time() 109 | while pos < len(plain): 110 | l = random.randint(100, 32768) 111 | c = cipher.update(plain[pos:pos + l]) 112 | results.append(c) 113 | pos += l 114 | pos = 0 115 | c = b''.join(results) 116 | results = [] 117 | while pos < len(plain): 118 | l = random.randint(100, 32768) 119 | results.append(decipher.update(c[pos:pos + l])) 120 | pos += l 121 | end = time.time() 122 | print('speed: %d bytes/s' % (BLOCK_SIZE * rounds / (end - start))) 123 | assert b''.join(results) == plain 124 | 125 | 126 | def test_find_library(): 127 | assert find_library('c', 'strcpy', 'libc') is not None 128 | assert find_library(['c'], 'strcpy', 'libc') is not None 129 | assert find_library(('c',), 'strcpy', 'libc') is not None 130 | assert find_library(('crypto', 'eay32'), 'EVP_CipherUpdate', 131 | 'libcrypto') is not None 132 | assert find_library('notexist', 'strcpy', 'libnotexist') is None 133 | assert find_library('c', 'symbol_not_exist', 'c') is None 134 | assert find_library(('notexist', 'c', 'crypto', 'eay32'), 135 | 'EVP_CipherUpdate', 'libc') is not None 136 | 137 | 138 | if __name__ == '__main__': 139 | test_find_library() 140 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/verify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | import binascii 25 | import base64 26 | import time 27 | import datetime 28 | import random 29 | import struct 30 | import zlib 31 | import hmac 32 | import hashlib 33 | 34 | import shadowsocks 35 | from shadowsocks import common 36 | from shadowsocks.obfsplugin import plain 37 | from shadowsocks.common import to_bytes, to_str, ord, chr 38 | 39 | def create_verify_deflate(method): 40 | return verify_deflate(method) 41 | 42 | obfs_map = { 43 | 'verify_deflate': (create_verify_deflate,), 44 | } 45 | 46 | def match_begin(str1, str2): 47 | if len(str1) >= len(str2): 48 | if str1[:len(str2)] == str2: 49 | return True 50 | return False 51 | 52 | class obfs_verify_data(object): 53 | def __init__(self): 54 | pass 55 | 56 | class verify_base(plain.plain): 57 | def __init__(self, method): 58 | super(verify_base, self).__init__(method) 59 | self.method = method 60 | 61 | def init_data(self): 62 | return obfs_verify_data() 63 | 64 | def set_server_info(self, server_info): 65 | self.server_info = server_info 66 | 67 | def client_encode(self, buf): 68 | return buf 69 | 70 | def client_decode(self, buf): 71 | return (buf, False) 72 | 73 | def server_encode(self, buf): 74 | return buf 75 | 76 | def server_decode(self, buf): 77 | return (buf, True, False) 78 | 79 | class verify_deflate(verify_base): 80 | def __init__(self, method): 81 | super(verify_deflate, self).__init__(method) 82 | self.recv_buf = b'' 83 | self.unit_len = 32700 84 | self.decrypt_packet_num = 0 85 | self.raw_trans = False 86 | 87 | def pack_data(self, buf): 88 | if len(buf) == 0: 89 | return b'' 90 | data = zlib.compress(buf) 91 | data = struct.pack('>H', len(data)) + data[2:] 92 | return data 93 | 94 | def client_pre_encrypt(self, buf): 95 | ret = b'' 96 | while len(buf) > self.unit_len: 97 | ret += self.pack_data(buf[:self.unit_len]) 98 | buf = buf[self.unit_len:] 99 | ret += self.pack_data(buf) 100 | return ret 101 | 102 | def client_post_decrypt(self, buf): 103 | if self.raw_trans: 104 | return buf 105 | self.recv_buf += buf 106 | out_buf = b'' 107 | while len(self.recv_buf) > 2: 108 | length = struct.unpack('>H', self.recv_buf[:2])[0] 109 | if length >= 32768 or length < 6: 110 | self.raw_trans = True 111 | self.recv_buf = b'' 112 | raise Exception('client_post_decrypt data error') 113 | if length > len(self.recv_buf): 114 | break 115 | 116 | out_buf += zlib.decompress(b'x\x9c' + self.recv_buf[2:length]) 117 | self.recv_buf = self.recv_buf[length:] 118 | 119 | if out_buf: 120 | self.decrypt_packet_num += 1 121 | return out_buf 122 | 123 | def server_pre_encrypt(self, buf): 124 | ret = b'' 125 | while len(buf) > self.unit_len: 126 | ret += self.pack_data(buf[:self.unit_len]) 127 | buf = buf[self.unit_len:] 128 | ret += self.pack_data(buf) 129 | return ret 130 | 131 | def server_post_decrypt(self, buf): 132 | if self.raw_trans: 133 | return (buf, False) 134 | self.recv_buf += buf 135 | out_buf = b'' 136 | while len(self.recv_buf) > 2: 137 | length = struct.unpack('>H', self.recv_buf[:2])[0] 138 | if length >= 32768 or length < 6: 139 | self.raw_trans = True 140 | self.recv_buf = b'' 141 | if self.decrypt_packet_num == 0: 142 | return (b'E'*2048, False) 143 | else: 144 | raise Exception('server_post_decrype data error') 145 | if length > len(self.recv_buf): 146 | break 147 | 148 | out_buf += zlib.decompress(b'\x78\x9c' + self.recv_buf[2:length]) 149 | self.recv_buf = self.recv_buf[length:] 150 | 151 | if out_buf: 152 | self.decrypt_packet_num += 1 153 | return (out_buf, False) 154 | 155 | -------------------------------------------------------------------------------- /shadowsocks/crypto/ctypes_libsodium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014 clowwindy 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import absolute_import, division, print_function, \ 24 | with_statement 25 | 26 | import logging 27 | from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \ 28 | create_string_buffer, c_void_p 29 | 30 | __all__ = ['ciphers'] 31 | 32 | libsodium = None 33 | loaded = False 34 | 35 | buf_size = 2048 36 | 37 | # for salsa20 and chacha20 38 | BLOCK_SIZE = 64 39 | 40 | 41 | def load_libsodium(): 42 | global loaded, libsodium, buf 43 | 44 | from ctypes.util import find_library 45 | for p in ('sodium',): 46 | libsodium_path = find_library(p) 47 | if libsodium_path: 48 | break 49 | else: 50 | raise Exception('libsodium not found') 51 | logging.info('loading libsodium from %s', libsodium_path) 52 | libsodium = CDLL(libsodium_path) 53 | libsodium.sodium_init.restype = c_int 54 | libsodium.crypto_stream_salsa20_xor_ic.restype = c_int 55 | libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, 56 | c_ulonglong, 57 | c_char_p, c_ulonglong, 58 | c_char_p) 59 | libsodium.crypto_stream_chacha20_xor_ic.restype = c_int 60 | libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, 61 | c_ulonglong, 62 | c_char_p, c_ulonglong, 63 | c_char_p) 64 | 65 | libsodium.sodium_init() 66 | 67 | buf = create_string_buffer(buf_size) 68 | loaded = True 69 | 70 | 71 | class Salsa20Crypto(object): 72 | def __init__(self, cipher_name, key, iv, op): 73 | if not loaded: 74 | load_libsodium() 75 | self.key = key 76 | self.iv = iv 77 | self.key_ptr = c_char_p(key) 78 | self.iv_ptr = c_char_p(iv) 79 | if cipher_name == b'salsa20': 80 | self.cipher = libsodium.crypto_stream_salsa20_xor_ic 81 | elif cipher_name == b'chacha20': 82 | self.cipher = libsodium.crypto_stream_chacha20_xor_ic 83 | else: 84 | raise Exception('Unknown cipher') 85 | # byte counter, not block counter 86 | self.counter = 0 87 | 88 | def update(self, data): 89 | global buf_size, buf 90 | l = len(data) 91 | 92 | # we can only prepend some padding to make the encryption align to 93 | # blocks 94 | padding = self.counter % BLOCK_SIZE 95 | if buf_size < padding + l: 96 | buf_size = (padding + l) * 2 97 | buf = create_string_buffer(buf_size) 98 | 99 | if padding: 100 | data = (b'\0' * padding) + data 101 | self.cipher(byref(buf), c_char_p(data), padding + l, 102 | self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) 103 | self.counter += l 104 | # buf is copied to a str object when we access buf.raw 105 | # strip off the padding 106 | return buf.raw[padding:padding + l] 107 | 108 | 109 | ciphers = { 110 | b'salsa20': (32, 8, Salsa20Crypto), 111 | b'chacha20': (32, 8, Salsa20Crypto), 112 | } 113 | 114 | 115 | def test_salsa20(): 116 | from shadowsocks.crypto import util 117 | 118 | cipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 1) 119 | decipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 0) 120 | 121 | util.run_cipher(cipher, decipher) 122 | 123 | 124 | def test_chacha20(): 125 | from shadowsocks.crypto import util 126 | 127 | cipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 1) 128 | decipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 0) 129 | 130 | util.run_cipher(cipher, decipher) 131 | 132 | 133 | if __name__ == '__main__': 134 | test_chacha20() 135 | test_salsa20() 136 | -------------------------------------------------------------------------------- /shadowsocks/crypto/sodium.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | from ctypes import c_char_p, c_int, c_ulong, c_ulonglong, byref, \ 21 | create_string_buffer, c_void_p 22 | 23 | from shadowsocks.crypto import util 24 | 25 | __all__ = ['ciphers'] 26 | 27 | libsodium = None 28 | loaded = False 29 | 30 | buf_size = 2048 31 | 32 | # for salsa20 and chacha20 and chacha20-ietf 33 | BLOCK_SIZE = 64 34 | 35 | 36 | def load_libsodium(): 37 | global loaded, libsodium, buf 38 | 39 | libsodium = util.find_library('sodium', 'crypto_stream_salsa20_xor_ic', 40 | 'libsodium') 41 | if libsodium is None: 42 | raise Exception('libsodium not found') 43 | 44 | libsodium.crypto_stream_salsa20_xor_ic.restype = c_int 45 | libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, 46 | c_ulonglong, 47 | c_char_p, c_ulonglong, 48 | c_char_p) 49 | libsodium.crypto_stream_chacha20_xor_ic.restype = c_int 50 | libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, 51 | c_ulonglong, 52 | c_char_p, c_ulonglong, 53 | c_char_p) 54 | 55 | try: 56 | libsodium.crypto_stream_chacha20_ietf_xor_ic.restype = c_int 57 | libsodium.crypto_stream_chacha20_ietf_xor_ic.argtypes = (c_void_p, c_char_p, 58 | c_ulonglong, 59 | c_char_p, c_ulong, 60 | c_char_p) 61 | except: 62 | pass 63 | 64 | buf = create_string_buffer(buf_size) 65 | loaded = True 66 | 67 | 68 | class SodiumCrypto(object): 69 | def __init__(self, cipher_name, key, iv, op): 70 | if not loaded: 71 | load_libsodium() 72 | self.key = key 73 | self.iv = iv 74 | self.key_ptr = c_char_p(key) 75 | self.iv_ptr = c_char_p(iv) 76 | if cipher_name == 'salsa20': 77 | self.cipher = libsodium.crypto_stream_salsa20_xor_ic 78 | elif cipher_name == 'chacha20': 79 | self.cipher = libsodium.crypto_stream_chacha20_xor_ic 80 | elif cipher_name == 'chacha20-ietf': 81 | self.cipher = libsodium.crypto_stream_chacha20_ietf_xor_ic 82 | else: 83 | raise Exception('Unknown cipher') 84 | # byte counter, not block counter 85 | self.counter = 0 86 | 87 | def update(self, data): 88 | global buf_size, buf 89 | l = len(data) 90 | 91 | # we can only prepend some padding to make the encryption align to 92 | # blocks 93 | padding = self.counter % BLOCK_SIZE 94 | if buf_size < padding + l: 95 | buf_size = (padding + l) * 2 96 | buf = create_string_buffer(buf_size) 97 | 98 | if padding: 99 | data = (b'\0' * padding) + data 100 | self.cipher(byref(buf), c_char_p(data), padding + l, 101 | self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) 102 | self.counter += l 103 | # buf is copied to a str object when we access buf.raw 104 | # strip off the padding 105 | return buf.raw[padding:padding + l] 106 | 107 | 108 | ciphers = { 109 | 'salsa20': (32, 8, SodiumCrypto), 110 | 'chacha20': (32, 8, SodiumCrypto), 111 | 'chacha20-ietf': (32, 12, SodiumCrypto), 112 | } 113 | 114 | 115 | def test_salsa20(): 116 | cipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 1) 117 | decipher = SodiumCrypto('salsa20', b'k' * 32, b'i' * 16, 0) 118 | 119 | util.run_cipher(cipher, decipher) 120 | 121 | 122 | def test_chacha20(): 123 | 124 | cipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 1) 125 | decipher = SodiumCrypto('chacha20', b'k' * 32, b'i' * 16, 0) 126 | 127 | util.run_cipher(cipher, decipher) 128 | 129 | 130 | def test_chacha20_ietf(): 131 | 132 | cipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 1) 133 | decipher = SodiumCrypto('chacha20-ietf', b'k' * 32, b'i' * 16, 0) 134 | 135 | util.run_cipher(cipher, decipher) 136 | 137 | if __name__ == '__main__': 138 | test_chacha20_ietf() 139 | test_chacha20() 140 | test_salsa20() 141 | -------------------------------------------------------------------------------- /tests/assert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # assert.sh 1.0 - bash unit testing framework 3 | # Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann 4 | # 5 | # http://github.com/lehmannro/assert.sh 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Lesser General Public License as published 9 | # by the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with this program. If not, see . 19 | 20 | export DISCOVERONLY=${DISCOVERONLY:-} 21 | export DEBUG=${DEBUG:-} 22 | export STOP=${STOP:-} 23 | export INVARIANT=${INVARIANT:-} 24 | export CONTINUE=${CONTINUE:-} 25 | 26 | args="$(getopt -n "$0" -l \ 27 | verbose,help,stop,discover,invariant,continue vhxdic $*)" \ 28 | || exit -1 29 | for arg in $args; do 30 | case "$arg" in 31 | -h) 32 | echo "$0 [-vxidc]" \ 33 | "[--verbose] [--stop] [--invariant] [--discover] [--continue]" 34 | echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]" 35 | exit 0;; 36 | --help) 37 | cat < [stdin] 98 | (( tests_ran++ )) || : 99 | [[ -n "$DISCOVERONLY" ]] && return || true 100 | # printf required for formatting 101 | printf -v expected "x${2:-}" # x required to overwrite older results 102 | result="$(eval 2>/dev/null $1 <<< ${3:-})" || true 103 | # Note: $expected is already decorated 104 | if [[ "x$result" == "$expected" ]]; then 105 | [[ -n "$DEBUG" ]] && echo -n . || true 106 | return 107 | fi 108 | result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")" 109 | [[ -z "$result" ]] && result="nothing" || result="\"$result\"" 110 | [[ -z "$2" ]] && expected="nothing" || expected="\"$2\"" 111 | _assert_fail "expected $expected${_indent}got $result" "$1" "$3" 112 | } 113 | 114 | assert_raises() { 115 | # assert_raises [stdin] 116 | (( tests_ran++ )) || : 117 | [[ -n "$DISCOVERONLY" ]] && return || true 118 | status=0 119 | (eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$? 120 | expected=${2:-0} 121 | if [[ "$status" -eq "$expected" ]]; then 122 | [[ -n "$DEBUG" ]] && echo -n . || true 123 | return 124 | fi 125 | _assert_fail "program terminated with code $status instead of $expected" "$1" "$3" 126 | } 127 | 128 | _assert_fail() { 129 | # _assert_fail 130 | [[ -n "$DEBUG" ]] && echo -n X 131 | report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1" 132 | if [[ -n "$STOP" ]]; then 133 | [[ -n "$DEBUG" ]] && echo 134 | echo "$report" 135 | exit 1 136 | fi 137 | tests_errors[$tests_failed]="$report" 138 | (( tests_failed++ )) || : 139 | } 140 | 141 | _assert_reset 142 | : ${tests_suite_status:=0} # remember if any of the tests failed so far 143 | _assert_cleanup() { 144 | local status=$? 145 | # modify exit code if it's not already non-zero 146 | [[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status 147 | } 148 | trap _assert_cleanup EXIT 149 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import sys 22 | import os 23 | import signal 24 | import select 25 | import time 26 | import argparse 27 | from subprocess import Popen, PIPE 28 | 29 | python = ['python'] 30 | 31 | default_url = 'http://localhost/' 32 | 33 | parser = argparse.ArgumentParser(description='test Shadowsocks') 34 | parser.add_argument('-c', '--client-conf', type=str, default=None) 35 | parser.add_argument('-s', '--server-conf', type=str, default=None) 36 | parser.add_argument('-a', '--client-args', type=str, default=None) 37 | parser.add_argument('-b', '--server-args', type=str, default=None) 38 | parser.add_argument('--with-coverage', action='store_true', default=None) 39 | parser.add_argument('--should-fail', action='store_true', default=None) 40 | parser.add_argument('--tcp-only', action='store_true', default=None) 41 | parser.add_argument('--url', type=str, default=default_url) 42 | parser.add_argument('--dns', type=str, default='8.8.8.8') 43 | 44 | config = parser.parse_args() 45 | 46 | if config.with_coverage: 47 | python = ['coverage', 'run', '-p'] 48 | 49 | client_args = python + ['shadowsocks/local.py', '-v'] 50 | server_args = python + ['shadowsocks/server.py', '-v'] 51 | 52 | if config.client_conf: 53 | client_args.extend(['-c', config.client_conf]) 54 | if config.server_conf: 55 | server_args.extend(['-c', config.server_conf]) 56 | else: 57 | server_args.extend(['-c', config.client_conf]) 58 | if config.client_args: 59 | client_args.extend(config.client_args.split()) 60 | if config.server_args: 61 | server_args.extend(config.server_args.split()) 62 | else: 63 | server_args.extend(config.client_args.split()) 64 | if config.url == default_url: 65 | server_args.extend(['--forbidden-ip', '']) 66 | 67 | p1 = Popen(server_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 68 | p2 = Popen(client_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 69 | p3 = None 70 | p4 = None 71 | p3_fin = False 72 | p4_fin = False 73 | 74 | # 1 shadowsocks started 75 | # 2 curl started 76 | # 3 curl finished 77 | # 4 dig started 78 | # 5 dig finished 79 | stage = 1 80 | 81 | try: 82 | local_ready = False 83 | server_ready = False 84 | fdset = [p1.stdout, p2.stdout, p1.stderr, p2.stderr] 85 | while True: 86 | r, w, e = select.select(fdset, [], fdset) 87 | if e: 88 | break 89 | 90 | for fd in r: 91 | line = fd.readline() 92 | if not line: 93 | if stage == 2 and fd == p3.stdout: 94 | stage = 3 95 | if stage == 4 and fd == p4.stdout: 96 | stage = 5 97 | if bytes != str: 98 | line = str(line, 'utf8') 99 | sys.stderr.write(line) 100 | if line.find('starting local') >= 0: 101 | local_ready = True 102 | if line.find('starting server') >= 0: 103 | server_ready = True 104 | 105 | if stage == 1: 106 | time.sleep(2) 107 | 108 | p3 = Popen(['curl', config.url, '-v', '-L', 109 | '--socks5-hostname', '127.0.0.1:1081', 110 | '-m', '15', '--connect-timeout', '10'], 111 | stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 112 | if p3 is not None: 113 | fdset.append(p3.stdout) 114 | fdset.append(p3.stderr) 115 | stage = 2 116 | else: 117 | sys.exit(1) 118 | 119 | if stage == 3 and p3 is not None: 120 | fdset.remove(p3.stdout) 121 | fdset.remove(p3.stderr) 122 | r = p3.wait() 123 | if config.should_fail: 124 | if r == 0: 125 | sys.exit(1) 126 | else: 127 | if r != 0: 128 | sys.exit(1) 129 | if config.tcp_only: 130 | break 131 | p4 = Popen(['socksify', 'dig', '@%s' % config.dns, 132 | 'www.google.com'], 133 | stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 134 | if p4 is not None: 135 | fdset.append(p4.stdout) 136 | fdset.append(p4.stderr) 137 | stage = 4 138 | else: 139 | sys.exit(1) 140 | 141 | if stage == 5: 142 | r = p4.wait() 143 | if config.should_fail: 144 | if r == 0: 145 | sys.exit(1) 146 | print('test passed (expecting failure)') 147 | else: 148 | if r != 0: 149 | sys.exit(1) 150 | print('test passed') 151 | break 152 | finally: 153 | for p in [p1, p2]: 154 | try: 155 | os.kill(p.pid, signal.SIGINT) 156 | os.waitpid(p.pid, 0) 157 | except OSError: 158 | pass 159 | -------------------------------------------------------------------------------- /shadowsocks/lru_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import collections 22 | import logging 23 | import time 24 | 25 | if __name__ == '__main__': 26 | import os, sys, inspect 27 | file_path = os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe()))) 28 | sys.path.insert(0, os.path.join(file_path, '../')) 29 | 30 | try: 31 | from collections import OrderedDict 32 | except: 33 | from shadowsocks.ordereddict import OrderedDict 34 | 35 | # this LRUCache is optimized for concurrency, not QPS 36 | # n: concurrency, keys stored in the cache 37 | # m: visits not timed out, proportional to QPS * timeout 38 | # get & set is O(1), not O(n). thus we can support very large n 39 | # sweep is O((n - m)) or O(1024) at most, 40 | # no metter how large the cache or timeout value is 41 | 42 | SWEEP_MAX_ITEMS = 1024 43 | 44 | class LRUCache(collections.MutableMapping): 45 | """This class is not thread safe""" 46 | 47 | def __init__(self, timeout=60, close_callback=None, *args, **kwargs): 48 | self.timeout = timeout 49 | self.close_callback = close_callback 50 | self._store = {} 51 | self._keys_to_last_time = OrderedDict() 52 | self.update(dict(*args, **kwargs)) # use the free update to set keys 53 | 54 | def __getitem__(self, key): 55 | # O(1) 56 | t = time.time() 57 | last_t = self._keys_to_last_time[key] 58 | del self._keys_to_last_time[key] 59 | self._keys_to_last_time[key] = t 60 | return self._store[key] 61 | 62 | def __setitem__(self, key, value): 63 | # O(1) 64 | t = time.time() 65 | if key in self._keys_to_last_time: 66 | del self._keys_to_last_time[key] 67 | self._keys_to_last_time[key] = t 68 | self._store[key] = value 69 | 70 | def __delitem__(self, key): 71 | # O(1) 72 | last_t = self._keys_to_last_time[key] 73 | del self._store[key] 74 | del self._keys_to_last_time[key] 75 | 76 | def __contains__(self, key): 77 | return key in self._store 78 | 79 | def __iter__(self): 80 | return iter(self._store) 81 | 82 | def __len__(self): 83 | return len(self._store) 84 | 85 | def first(self): 86 | if len(self._keys_to_last_time) > 0: 87 | for key in self._keys_to_last_time: 88 | return key 89 | 90 | def sweep(self, sweep_item_cnt = SWEEP_MAX_ITEMS): 91 | # O(n - m) 92 | now = time.time() 93 | c = 0 94 | while c < sweep_item_cnt: 95 | if len(self._keys_to_last_time) == 0: 96 | break 97 | for key in self._keys_to_last_time: 98 | break 99 | last_t = self._keys_to_last_time[key] 100 | if now - last_t <= self.timeout: 101 | break 102 | value = self._store[key] 103 | del self._store[key] 104 | del self._keys_to_last_time[key] 105 | if self.close_callback is not None: 106 | self.close_callback(value) 107 | c += 1 108 | if c: 109 | logging.debug('%d keys swept' % c) 110 | return c < SWEEP_MAX_ITEMS 111 | 112 | def clear(self, keep): 113 | now = time.time() 114 | c = 0 115 | while len(self._keys_to_last_time) > keep: 116 | if len(self._keys_to_last_time) == 0: 117 | break 118 | for key in self._keys_to_last_time: 119 | break 120 | last_t = self._keys_to_last_time[key] 121 | value = self._store[key] 122 | if self.close_callback is not None: 123 | self.close_callback(value) 124 | del self._store[key] 125 | del self._keys_to_last_time[key] 126 | c += 1 127 | if c: 128 | logging.debug('%d keys swept' % c) 129 | return c < SWEEP_MAX_ITEMS 130 | 131 | def test(): 132 | c = LRUCache(timeout=0.3) 133 | 134 | c['a'] = 1 135 | assert c['a'] == 1 136 | c['a'] = 1 137 | 138 | time.sleep(0.5) 139 | c.sweep() 140 | assert 'a' not in c 141 | 142 | c['a'] = 2 143 | c['b'] = 3 144 | time.sleep(0.2) 145 | c.sweep() 146 | assert c['a'] == 2 147 | assert c['b'] == 3 148 | 149 | time.sleep(0.2) 150 | c.sweep() 151 | c['b'] 152 | time.sleep(0.2) 153 | c.sweep() 154 | assert 'a' not in c 155 | assert c['b'] == 3 156 | 157 | time.sleep(0.5) 158 | c.sweep() 159 | assert 'a' not in c 160 | assert 'b' not in c 161 | 162 | global close_cb_called 163 | close_cb_called = False 164 | 165 | def close_cb(t): 166 | global close_cb_called 167 | assert not close_cb_called 168 | close_cb_called = True 169 | 170 | c = LRUCache(timeout=0.1, close_callback=close_cb) 171 | c['s'] = 1 172 | c['s'] 173 | time.sleep(0.1) 174 | c['s'] 175 | time.sleep(0.3) 176 | c.sweep() 177 | 178 | if __name__ == '__main__': 179 | test() 180 | -------------------------------------------------------------------------------- /shadowsocks/daemon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2014-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, \ 19 | with_statement 20 | 21 | import os 22 | import sys 23 | import logging 24 | import signal 25 | import time 26 | from shadowsocks import common, shell 27 | 28 | # this module is ported from ShadowVPN daemon.c 29 | 30 | 31 | def daemon_exec(config): 32 | if 'daemon' in config: 33 | if os.name != 'posix': 34 | raise Exception('daemon mode is only supported on Unix') 35 | command = config['daemon'] 36 | if not command: 37 | command = 'start' 38 | pid_file = config['pid-file'] 39 | log_file = config['log-file'] 40 | if command == 'start': 41 | daemon_start(pid_file, log_file) 42 | elif command == 'stop': 43 | daemon_stop(pid_file) 44 | # always exit after daemon_stop 45 | sys.exit(0) 46 | elif command == 'restart': 47 | daemon_stop(pid_file) 48 | daemon_start(pid_file, log_file) 49 | else: 50 | raise Exception('unsupported daemon command %s' % command) 51 | 52 | 53 | def write_pid_file(pid_file, pid): 54 | import fcntl 55 | import stat 56 | 57 | try: 58 | fd = os.open(pid_file, os.O_RDWR | os.O_CREAT, 59 | stat.S_IRUSR | stat.S_IWUSR) 60 | except OSError as e: 61 | shell.print_exception(e) 62 | return -1 63 | flags = fcntl.fcntl(fd, fcntl.F_GETFD) 64 | assert flags != -1 65 | flags |= fcntl.FD_CLOEXEC 66 | r = fcntl.fcntl(fd, fcntl.F_SETFD, flags) 67 | assert r != -1 68 | # There is no platform independent way to implement fcntl(fd, F_SETLK, &fl) 69 | # via fcntl.fcntl. So use lockf instead 70 | try: 71 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET) 72 | except IOError: 73 | r = os.read(fd, 32) 74 | if r: 75 | logging.error('already started at pid %s' % common.to_str(r)) 76 | else: 77 | logging.error('already started') 78 | os.close(fd) 79 | return -1 80 | os.ftruncate(fd, 0) 81 | os.write(fd, common.to_bytes(str(pid))) 82 | return 0 83 | 84 | 85 | def freopen(f, mode, stream): 86 | oldf = open(f, mode) 87 | oldfd = oldf.fileno() 88 | newfd = stream.fileno() 89 | os.close(newfd) 90 | os.dup2(oldfd, newfd) 91 | 92 | 93 | def daemon_start(pid_file, log_file): 94 | 95 | def handle_exit(signum, _): 96 | if signum == signal.SIGTERM: 97 | sys.exit(0) 98 | sys.exit(1) 99 | 100 | signal.signal(signal.SIGINT, handle_exit) 101 | signal.signal(signal.SIGTERM, handle_exit) 102 | 103 | # fork only once because we are sure parent will exit 104 | pid = os.fork() 105 | assert pid != -1 106 | 107 | if pid > 0: 108 | # parent waits for its child 109 | time.sleep(5) 110 | sys.exit(0) 111 | 112 | # child signals its parent to exit 113 | ppid = os.getppid() 114 | pid = os.getpid() 115 | if write_pid_file(pid_file, pid) != 0: 116 | os.kill(ppid, signal.SIGINT) 117 | sys.exit(1) 118 | 119 | os.setsid() 120 | signal.signal(signal.SIG_IGN, signal.SIGHUP) 121 | 122 | print('started') 123 | os.kill(ppid, signal.SIGTERM) 124 | 125 | sys.stdin.close() 126 | try: 127 | freopen(log_file, 'a', sys.stdout) 128 | freopen(log_file, 'a', sys.stderr) 129 | except IOError as e: 130 | shell.print_exception(e) 131 | sys.exit(1) 132 | 133 | 134 | def daemon_stop(pid_file): 135 | import errno 136 | try: 137 | with open(pid_file) as f: 138 | buf = f.read() 139 | pid = common.to_str(buf) 140 | if not buf: 141 | logging.error('not running') 142 | except IOError as e: 143 | shell.print_exception(e) 144 | if e.errno == errno.ENOENT: 145 | # always exit 0 if we are sure daemon is not running 146 | logging.error('not running') 147 | return 148 | sys.exit(1) 149 | pid = int(pid) 150 | if pid > 0: 151 | try: 152 | os.kill(pid, signal.SIGTERM) 153 | except OSError as e: 154 | if e.errno == errno.ESRCH: 155 | logging.error('not running') 156 | # always exit 0 if we are sure daemon is not running 157 | return 158 | shell.print_exception(e) 159 | sys.exit(1) 160 | else: 161 | logging.error('pid is not positive: %d', pid) 162 | 163 | # sleep for maximum 10s 164 | for i in range(0, 200): 165 | try: 166 | # query for the pid 167 | os.kill(pid, 0) 168 | except OSError as e: 169 | if e.errno == errno.ESRCH: 170 | break 171 | time.sleep(0.05) 172 | else: 173 | logging.error('timed out when stopping pid %d', pid) 174 | sys.exit(1) 175 | print('stopped') 176 | os.unlink(pid_file) 177 | 178 | 179 | def set_user(username): 180 | if username is None: 181 | return 182 | 183 | import pwd 184 | import grp 185 | 186 | try: 187 | pwrec = pwd.getpwnam(username) 188 | except KeyError: 189 | logging.error('user not found: %s' % username) 190 | raise 191 | user = pwrec[0] 192 | uid = pwrec[2] 193 | gid = pwrec[3] 194 | 195 | cur_uid = os.getuid() 196 | if uid == cur_uid: 197 | return 198 | if cur_uid != 0: 199 | logging.error('can not set user as nonroot user') 200 | # will raise later 201 | 202 | # inspired by supervisor 203 | if hasattr(os, 'setgroups'): 204 | groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]] 205 | groups.insert(0, gid) 206 | os.setgroups(groups) 207 | os.setgid(gid) 208 | os.setuid(uid) 209 | -------------------------------------------------------------------------------- /shadowsocks/crypto/ctypes_openssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014 clowwindy 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in 13 | # all copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from __future__ import absolute_import, division, print_function, \ 24 | with_statement 25 | 26 | import logging 27 | from ctypes import CDLL, c_char_p, c_int, c_long, byref,\ 28 | create_string_buffer, c_void_p 29 | 30 | __all__ = ['ciphers'] 31 | 32 | libcrypto = None 33 | loaded = False 34 | 35 | buf_size = 2048 36 | 37 | 38 | def load_openssl(): 39 | global loaded, libcrypto, buf 40 | 41 | from ctypes.util import find_library 42 | for p in ('crypto', 'eay32', 'libeay32'): 43 | libcrypto_path = find_library(p) 44 | if libcrypto_path: 45 | break 46 | else: 47 | raise Exception('libcrypto(OpenSSL) not found') 48 | logging.info('loading libcrypto from %s', libcrypto_path) 49 | libcrypto = CDLL(libcrypto_path) 50 | libcrypto.EVP_get_cipherbyname.restype = c_void_p 51 | libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p 52 | 53 | libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, 54 | c_char_p, c_char_p, c_int) 55 | 56 | libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, 57 | c_char_p, c_int) 58 | 59 | libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) 60 | libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) 61 | if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): 62 | libcrypto.OpenSSL_add_all_ciphers() 63 | 64 | buf = create_string_buffer(buf_size) 65 | loaded = True 66 | 67 | 68 | def load_cipher(cipher_name): 69 | func_name = b'EVP_' + cipher_name.replace(b'-', b'_') 70 | if bytes != str: 71 | func_name = str(func_name, 'utf-8') 72 | cipher = getattr(libcrypto, func_name, None) 73 | if cipher: 74 | cipher.restype = c_void_p 75 | return cipher() 76 | return None 77 | 78 | 79 | class CtypesCrypto(object): 80 | def __init__(self, cipher_name, key, iv, op): 81 | if not loaded: 82 | load_openssl() 83 | self._ctx = None 84 | cipher = libcrypto.EVP_get_cipherbyname(cipher_name) 85 | if not cipher: 86 | cipher = load_cipher(cipher_name) 87 | if not cipher: 88 | raise Exception('cipher %s not found in libcrypto' % cipher_name) 89 | key_ptr = c_char_p(key) 90 | iv_ptr = c_char_p(iv) 91 | self._ctx = libcrypto.EVP_CIPHER_CTX_new() 92 | if not self._ctx: 93 | raise Exception('can not create cipher context') 94 | r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, 95 | key_ptr, iv_ptr, c_int(op)) 96 | if not r: 97 | self.clean() 98 | raise Exception('can not initialize cipher context') 99 | 100 | def update(self, data): 101 | global buf_size, buf 102 | cipher_out_len = c_long(0) 103 | l = len(data) 104 | if buf_size < l: 105 | buf_size = l * 2 106 | buf = create_string_buffer(buf_size) 107 | libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), 108 | byref(cipher_out_len), c_char_p(data), l) 109 | # buf is copied to a str object when we access buf.raw 110 | return buf.raw[:cipher_out_len.value] 111 | 112 | def __del__(self): 113 | self.clean() 114 | 115 | def clean(self): 116 | if self._ctx: 117 | libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) 118 | libcrypto.EVP_CIPHER_CTX_free(self._ctx) 119 | 120 | 121 | ciphers = { 122 | b'aes-128-cfb': (16, 16, CtypesCrypto), 123 | b'aes-192-cfb': (24, 16, CtypesCrypto), 124 | b'aes-256-cfb': (32, 16, CtypesCrypto), 125 | b'aes-128-ofb': (16, 16, CtypesCrypto), 126 | b'aes-192-ofb': (24, 16, CtypesCrypto), 127 | b'aes-256-ofb': (32, 16, CtypesCrypto), 128 | b'aes-128-ctr': (16, 16, CtypesCrypto), 129 | b'aes-192-ctr': (24, 16, CtypesCrypto), 130 | b'aes-256-ctr': (32, 16, CtypesCrypto), 131 | b'aes-128-cfb8': (16, 16, CtypesCrypto), 132 | b'aes-192-cfb8': (24, 16, CtypesCrypto), 133 | b'aes-256-cfb8': (32, 16, CtypesCrypto), 134 | b'aes-128-cfb1': (16, 16, CtypesCrypto), 135 | b'aes-192-cfb1': (24, 16, CtypesCrypto), 136 | b'aes-256-cfb1': (32, 16, CtypesCrypto), 137 | b'bf-cfb': (16, 8, CtypesCrypto), 138 | b'camellia-128-cfb': (16, 16, CtypesCrypto), 139 | b'camellia-192-cfb': (24, 16, CtypesCrypto), 140 | b'camellia-256-cfb': (32, 16, CtypesCrypto), 141 | b'cast5-cfb': (16, 8, CtypesCrypto), 142 | b'des-cfb': (8, 8, CtypesCrypto), 143 | b'idea-cfb': (16, 8, CtypesCrypto), 144 | b'rc2-cfb': (16, 8, CtypesCrypto), 145 | b'rc4': (16, 0, CtypesCrypto), 146 | b'seed-cfb': (16, 16, CtypesCrypto), 147 | } 148 | 149 | 150 | def run_method(method): 151 | from shadowsocks.crypto import util 152 | 153 | cipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 1) 154 | decipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 0) 155 | 156 | util.run_cipher(cipher, decipher) 157 | 158 | 159 | def test_aes_128_cfb(): 160 | run_method(b'aes-128-cfb') 161 | 162 | 163 | def test_aes_256_cfb(): 164 | run_method(b'aes-256-cfb') 165 | 166 | 167 | def test_aes_128_cfb8(): 168 | run_method(b'aes-128-cfb8') 169 | 170 | 171 | def test_aes_256_ofb(): 172 | run_method(b'aes-256-ofb') 173 | 174 | 175 | def test_aes_256_ctr(): 176 | run_method(b'aes-256-ctr') 177 | 178 | 179 | def test_bf_cfb(): 180 | run_method(b'bf-cfb') 181 | 182 | 183 | def test_rc4(): 184 | run_method(b'rc4') 185 | 186 | 187 | if __name__ == '__main__': 188 | test_aes_128_cfb() 189 | -------------------------------------------------------------------------------- /shadowsocks/crypto/openssl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | from ctypes import c_char_p, c_int, c_long, byref,\ 21 | create_string_buffer, c_void_p 22 | 23 | from shadowsocks import common 24 | from shadowsocks.crypto import util 25 | 26 | __all__ = ['ciphers'] 27 | 28 | libcrypto = None 29 | loaded = False 30 | 31 | buf_size = 2048 32 | 33 | 34 | def load_openssl(): 35 | global loaded, libcrypto, buf 36 | 37 | libcrypto = util.find_library(('crypto', 'eay32'), 38 | 'EVP_get_cipherbyname', 39 | 'libcrypto') 40 | if libcrypto is None: 41 | raise Exception('libcrypto(OpenSSL) not found') 42 | 43 | libcrypto.EVP_get_cipherbyname.restype = c_void_p 44 | libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p 45 | 46 | libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, 47 | c_char_p, c_char_p, c_int) 48 | 49 | libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, 50 | c_char_p, c_int) 51 | 52 | if hasattr(libcrypto, "EVP_CIPHER_CTX_cleanup"): 53 | libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) 54 | else: 55 | libcrypto.EVP_CIPHER_CTX_reset.argtypes = (c_void_p,) 56 | libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) 57 | 58 | libcrypto.RAND_bytes.restype = c_int 59 | libcrypto.RAND_bytes.argtypes = (c_void_p, c_int) 60 | 61 | if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): 62 | libcrypto.OpenSSL_add_all_ciphers() 63 | 64 | buf = create_string_buffer(buf_size) 65 | loaded = True 66 | 67 | 68 | def load_cipher(cipher_name): 69 | func_name = 'EVP_' + cipher_name.replace('-', '_') 70 | cipher = getattr(libcrypto, func_name, None) 71 | if cipher: 72 | cipher.restype = c_void_p 73 | return cipher() 74 | return None 75 | 76 | def rand_bytes(length): 77 | if not loaded: 78 | load_openssl() 79 | buf = create_string_buffer(length) 80 | r = libcrypto.RAND_bytes(buf, length) 81 | if r <= 0: 82 | raise Exception('RAND_bytes return error') 83 | return buf.raw 84 | 85 | class OpenSSLCrypto(object): 86 | def __init__(self, cipher_name, key, iv, op): 87 | self._ctx = None 88 | if not loaded: 89 | load_openssl() 90 | cipher = libcrypto.EVP_get_cipherbyname(common.to_bytes(cipher_name)) 91 | if not cipher: 92 | cipher = load_cipher(cipher_name) 93 | if not cipher: 94 | raise Exception('cipher %s not found in libcrypto' % cipher_name) 95 | key_ptr = c_char_p(key) 96 | iv_ptr = c_char_p(iv) 97 | self._ctx = libcrypto.EVP_CIPHER_CTX_new() 98 | if not self._ctx: 99 | raise Exception('can not create cipher context') 100 | r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, 101 | key_ptr, iv_ptr, c_int(op)) 102 | if not r: 103 | self.clean() 104 | raise Exception('can not initialize cipher context') 105 | 106 | def update(self, data): 107 | global buf_size, buf 108 | cipher_out_len = c_long(0) 109 | l = len(data) 110 | if buf_size < l: 111 | buf_size = l * 2 112 | buf = create_string_buffer(buf_size) 113 | libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), 114 | byref(cipher_out_len), c_char_p(data), l) 115 | # buf is copied to a str object when we access buf.raw 116 | return buf.raw[:cipher_out_len.value] 117 | 118 | def __del__(self): 119 | self.clean() 120 | 121 | def clean(self): 122 | if self._ctx: 123 | if hasattr(libcrypto, "EVP_CIPHER_CTX_cleanup"): 124 | libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) 125 | else: 126 | libcrypto.EVP_CIPHER_CTX_reset(self._ctx) 127 | libcrypto.EVP_CIPHER_CTX_free(self._ctx) 128 | 129 | 130 | ciphers = { 131 | 'aes-128-cbc': (16, 16, OpenSSLCrypto), 132 | 'aes-192-cbc': (24, 16, OpenSSLCrypto), 133 | 'aes-256-cbc': (32, 16, OpenSSLCrypto), 134 | 'aes-128-cfb': (16, 16, OpenSSLCrypto), 135 | 'aes-192-cfb': (24, 16, OpenSSLCrypto), 136 | 'aes-256-cfb': (32, 16, OpenSSLCrypto), 137 | 'aes-128-ofb': (16, 16, OpenSSLCrypto), 138 | 'aes-192-ofb': (24, 16, OpenSSLCrypto), 139 | 'aes-256-ofb': (32, 16, OpenSSLCrypto), 140 | 'aes-128-ctr': (16, 16, OpenSSLCrypto), 141 | 'aes-192-ctr': (24, 16, OpenSSLCrypto), 142 | 'aes-256-ctr': (32, 16, OpenSSLCrypto), 143 | 'aes-128-cfb8': (16, 16, OpenSSLCrypto), 144 | 'aes-192-cfb8': (24, 16, OpenSSLCrypto), 145 | 'aes-256-cfb8': (32, 16, OpenSSLCrypto), 146 | 'aes-128-cfb1': (16, 16, OpenSSLCrypto), 147 | 'aes-192-cfb1': (24, 16, OpenSSLCrypto), 148 | 'aes-256-cfb1': (32, 16, OpenSSLCrypto), 149 | 'bf-cfb': (16, 8, OpenSSLCrypto), 150 | 'camellia-128-cfb': (16, 16, OpenSSLCrypto), 151 | 'camellia-192-cfb': (24, 16, OpenSSLCrypto), 152 | 'camellia-256-cfb': (32, 16, OpenSSLCrypto), 153 | 'cast5-cfb': (16, 8, OpenSSLCrypto), 154 | 'des-cfb': (8, 8, OpenSSLCrypto), 155 | 'idea-cfb': (16, 8, OpenSSLCrypto), 156 | 'rc2-cfb': (16, 8, OpenSSLCrypto), 157 | 'rc4': (16, 0, OpenSSLCrypto), 158 | 'seed-cfb': (16, 16, OpenSSLCrypto), 159 | } 160 | 161 | 162 | def run_method(method): 163 | 164 | cipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 1) 165 | decipher = OpenSSLCrypto(method, b'k' * 32, b'i' * 16, 0) 166 | 167 | util.run_cipher(cipher, decipher) 168 | 169 | 170 | def test_aes_128_cfb(): 171 | run_method('aes-128-cfb') 172 | 173 | 174 | def test_aes_256_cfb(): 175 | run_method('aes-256-cfb') 176 | 177 | 178 | def test_aes_128_cfb8(): 179 | run_method('aes-128-cfb8') 180 | 181 | 182 | def test_aes_256_ofb(): 183 | run_method('aes-256-ofb') 184 | 185 | 186 | def test_aes_256_ctr(): 187 | run_method('aes-256-ctr') 188 | 189 | 190 | def test_bf_cfb(): 191 | run_method('bf-cfb') 192 | 193 | 194 | def test_rc4(): 195 | run_method('rc4') 196 | 197 | 198 | if __name__ == '__main__': 199 | test_aes_128_cfb() 200 | -------------------------------------------------------------------------------- /shadowsocks/encrypt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2012-2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | 25 | from shadowsocks import common 26 | from shadowsocks.crypto import rc4_md5, openssl, sodium, table 27 | 28 | 29 | method_supported = {} 30 | method_supported.update(rc4_md5.ciphers) 31 | method_supported.update(openssl.ciphers) 32 | method_supported.update(sodium.ciphers) 33 | method_supported.update(table.ciphers) 34 | 35 | 36 | def random_string(length): 37 | try: 38 | return os.urandom(length) 39 | except NotImplementedError as e: 40 | return openssl.rand_bytes(length) 41 | 42 | cached_keys = {} 43 | 44 | 45 | def try_cipher(key, method=None): 46 | Encryptor(key, method) 47 | 48 | 49 | def EVP_BytesToKey(password, key_len, iv_len): 50 | # equivalent to OpenSSL's EVP_BytesToKey() with count 1 51 | # so that we make the same key and iv as nodejs version 52 | if hasattr(password, 'encode'): 53 | password = password.encode('utf-8') 54 | cached_key = '%s-%d-%d' % (password, key_len, iv_len) 55 | r = cached_keys.get(cached_key, None) 56 | if r: 57 | return r 58 | m = [] 59 | i = 0 60 | while len(b''.join(m)) < (key_len + iv_len): 61 | md5 = hashlib.md5() 62 | data = password 63 | if i > 0: 64 | data = m[i - 1] + password 65 | md5.update(data) 66 | m.append(md5.digest()) 67 | i += 1 68 | ms = b''.join(m) 69 | key = ms[:key_len] 70 | iv = ms[key_len:key_len + iv_len] 71 | cached_keys[cached_key] = (key, iv) 72 | return key, iv 73 | 74 | 75 | class Encryptor(object): 76 | def __init__(self, key, method, iv = None): 77 | self.key = key 78 | self.method = method 79 | self.iv = None 80 | self.iv_sent = False 81 | self.cipher_iv = b'' 82 | self.iv_buf = b'' 83 | self.cipher_key = b'' 84 | self.decipher = None 85 | method = method.lower() 86 | self._method_info = self.get_method_info(method) 87 | if self._method_info: 88 | if iv is None or len(iv) != self._method_info[1]: 89 | self.cipher = self.get_cipher(key, method, 1, 90 | random_string(self._method_info[1])) 91 | else: 92 | self.cipher = self.get_cipher(key, method, 1, iv) 93 | else: 94 | logging.error('method %s not supported' % method) 95 | sys.exit(1) 96 | 97 | def get_method_info(self, method): 98 | method = method.lower() 99 | m = method_supported.get(method) 100 | return m 101 | 102 | def iv_len(self): 103 | return len(self.cipher_iv) 104 | 105 | def get_cipher(self, password, method, op, iv): 106 | password = common.to_bytes(password) 107 | m = self._method_info 108 | if m[0] > 0: 109 | key, iv_ = EVP_BytesToKey(password, m[0], m[1]) 110 | else: 111 | # key_length == 0 indicates we should use the key directly 112 | key, iv = password, b'' 113 | 114 | iv = iv[:m[1]] 115 | if op == 1: 116 | # this iv is for cipher not decipher 117 | self.cipher_iv = iv[:m[1]] 118 | self.cipher_key = key 119 | return m[2](method, key, iv, op) 120 | 121 | def encrypt(self, buf): 122 | if len(buf) == 0: 123 | return buf 124 | if self.iv_sent: 125 | return self.cipher.update(buf) 126 | else: 127 | self.iv_sent = True 128 | return self.cipher_iv + self.cipher.update(buf) 129 | 130 | def decrypt(self, buf): 131 | if len(buf) == 0: 132 | return buf 133 | if self.decipher is not None: #optimize 134 | return self.decipher.update(buf) 135 | 136 | decipher_iv_len = self._method_info[1] 137 | if len(self.iv_buf) <= decipher_iv_len: 138 | self.iv_buf += buf 139 | if len(self.iv_buf) > decipher_iv_len: 140 | decipher_iv = self.iv_buf[:decipher_iv_len] 141 | self.decipher = self.get_cipher(self.key, self.method, 0, 142 | iv=decipher_iv) 143 | buf = self.iv_buf[decipher_iv_len:] 144 | del self.iv_buf 145 | return self.decipher.update(buf) 146 | else: 147 | return b'' 148 | 149 | def encrypt_all(password, method, op, data): 150 | result = [] 151 | method = method.lower() 152 | (key_len, iv_len, m) = method_supported[method] 153 | if key_len > 0: 154 | key, _ = EVP_BytesToKey(password, key_len, iv_len) 155 | else: 156 | key = password 157 | if op: 158 | iv = random_string(iv_len) 159 | result.append(iv) 160 | else: 161 | iv = data[:iv_len] 162 | data = data[iv_len:] 163 | cipher = m(method, key, iv, op) 164 | result.append(cipher.update(data)) 165 | return b''.join(result) 166 | 167 | def encrypt_key(password, method): 168 | method = method.lower() 169 | (key_len, iv_len, m) = method_supported[method] 170 | if key_len > 0: 171 | key, _ = EVP_BytesToKey(password, key_len, iv_len) 172 | else: 173 | key = password 174 | return key 175 | 176 | def encrypt_iv_len(method): 177 | method = method.lower() 178 | (key_len, iv_len, m) = method_supported[method] 179 | return iv_len 180 | 181 | def encrypt_new_iv(method): 182 | method = method.lower() 183 | (key_len, iv_len, m) = method_supported[method] 184 | return random_string(iv_len) 185 | 186 | def encrypt_all_iv(key, method, op, data, ref_iv): 187 | result = [] 188 | method = method.lower() 189 | (key_len, iv_len, m) = method_supported[method] 190 | if op: 191 | iv = ref_iv[0] 192 | result.append(iv) 193 | else: 194 | iv = data[:iv_len] 195 | data = data[iv_len:] 196 | ref_iv[0] = iv 197 | cipher = m(method, key, iv, op) 198 | result.append(cipher.update(data)) 199 | return b''.join(result) 200 | 201 | 202 | CIPHERS_TO_TEST = [ 203 | 'aes-128-cfb', 204 | 'aes-256-cfb', 205 | 'rc4-md5', 206 | 'salsa20', 207 | 'chacha20', 208 | 'table', 209 | ] 210 | 211 | 212 | def test_encryptor(): 213 | from os import urandom 214 | plain = urandom(10240) 215 | for method in CIPHERS_TO_TEST: 216 | logging.warn(method) 217 | encryptor = Encryptor(b'key', method) 218 | decryptor = Encryptor(b'key', method) 219 | cipher = encryptor.encrypt(plain) 220 | plain2 = decryptor.decrypt(cipher) 221 | assert plain == plain2 222 | 223 | 224 | def test_encrypt_all(): 225 | from os import urandom 226 | plain = urandom(10240) 227 | for method in CIPHERS_TO_TEST: 228 | logging.warn(method) 229 | cipher = encrypt_all(b'key', method, 1, plain) 230 | plain2 = encrypt_all(b'key', method, 0, cipher) 231 | assert plain == plain2 232 | 233 | 234 | if __name__ == '__main__': 235 | test_encrypt_all() 236 | test_encryptor() 237 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 3.4.0 2017-07-27 2 | - add auth_chain_b 3 | - add initmudbjson.sh 4 | - allow set speed limit in runtime 5 | - fix bugs & mem leak 6 | 7 | 3.3.3 2017-06-03 8 | - add DNS cache 9 | - add tls1.2_ticket_fastauth 10 | - fix bugs 11 | 12 | 3.3.2 2017-05-20 13 | - revert http reply 14 | - refine tls1.2_ticket_auth error detector 15 | 16 | 3.3.1 2017-05-18 17 | - fix stop script 18 | - Async DNS query under UDP 19 | - fix old version of OpenSSL 20 | - http reply 21 | 22 | 3.3.0 2017-05-11 23 | - connect_log include local addr & port 24 | - fix auth_chain_a UDP bug 25 | - add "additional_ports_only" 26 | - add interface legendsockssr 27 | - run with newest python version 28 | - parse comment in hosts 29 | - update mujson_mgr 30 | - add cymysql setup script 31 | - new speed tester 32 | - fix leaks 33 | - bugs fixed 34 | 35 | 3.2.0 2017-04-27 36 | - add auth_chain_a 37 | - remove auth_aes128, auth_sha1, auth_sha1_v2, verify_simple, auth_simple, verify_sha1 38 | 39 | 3.1.2 2017-04-07 40 | - display UID 41 | - auto adjust TCP MSS 42 | 43 | 3.1.1 2017-03-25 44 | - add "New session ticket" 45 | - ignore bind 10.0.0.0/8 and 192.168.0.0/16 by default 46 | - improve rand size under auth_aes128_* 47 | - fix bugs 48 | 49 | 3.1.0 2017-03-16 50 | - add "glzjinmod" interface 51 | - rate limit 52 | - add additional_ports in config 53 | 54 | 3.0.4 2017-01-08 55 | - multi-user in single port 56 | 57 | 3.0.1 2017-01-03 58 | - remove auth_aes128_*_compatible 59 | 60 | 3.0.0 2016-12-23 61 | - http_simple fix bugs 62 | - tls1.2_ticket_auth fix bug & defaule time diff set to 86400s 63 | 64 | 2.9.7 2016-11-22 65 | - manage client with LRUCache 66 | - catch bind error 67 | - fix import error of resource on windows 68 | - print RLIMIT_NOFILE 69 | - always close cymysql objects 70 | - add init script 71 | 72 | 2.9.6 2016-10-17 73 | - tls1.2_ticket_auth random packet size 74 | 75 | 2.9.5.1 2016-10-16 76 | - UDP bind address 77 | 78 | 2.9.5 2016-10-13 79 | - add auth_aes128_md5 and auth_aes128_sha1 80 | 81 | 2.9.4 2016-10-11 82 | - sync client version 83 | 84 | 2.6.13 2015-11-02 85 | - add protocol setting 86 | 87 | 2.6.12 2015-10-27 88 | - IPv6 first 89 | - Fix mem leaks 90 | - auth_simple plugin 91 | - remove FORCE_NEW_PROTOCOL 92 | - optimize code 93 | 94 | 2.6.11 2015-10-20 95 | - Obfs plugin 96 | - Obfs parameters 97 | - UDP over TCP 98 | - TCP over UDP (experimental) 99 | - Fix socket leaks 100 | - Catch abnormal UDP package 101 | 102 | 2.6.10 2015-06-08 103 | - Optimize LRU cache 104 | - Refine logging 105 | 106 | 2.6.9 2015-05-19 107 | - Fix a stability issue on Windows 108 | 109 | 2.6.8 2015-02-10 110 | - Support multiple server ip on client side 111 | - Support --version 112 | - Minor fixes 113 | 114 | 2.6.7 2015-02-02 115 | - Support --user 116 | - Support CIDR format in --forbidden-ip 117 | - Minor fixes 118 | 119 | 2.6.6 2015-01-23 120 | - Fix a crash in forbidden list 121 | 122 | 2.6.5 2015-01-18 123 | - Try both 32 bit and 64 bit dll on Windows 124 | 125 | 2.6.4 2015-01-14 126 | - Also search lib* when searching libraries 127 | 128 | 2.6.3 2015-01-12 129 | - Support --forbidden-ip to ban some IP, i.e. localhost 130 | - Search OpenSSL and libsodium harder 131 | - Now works on OpenWRT 132 | 133 | 2.6.2 2015-01-03 134 | - Log client IP 135 | 136 | 2.6.1 2014-12-26 137 | - Fix a problem with TCP Fast Open on local side 138 | - Fix sometimes daemon_start returns wrong exit status 139 | 140 | 2.6 2014-12-21 141 | - Add daemon support 142 | 143 | 2.5 2014-12-11 144 | - Add salsa20 and chacha20 145 | 146 | 2.4.3 2014-11-10 147 | - Fix an issue on Python 3 148 | - Fix an issue with IPv6 149 | 150 | 2.4.2 2014-11-06 151 | - Fix command line arguments on Python 3 152 | - Support table on Python 3 153 | - Fix TCP Fast Open on Python 3 154 | 155 | 2.4.1 2014-11-01 156 | - Fix setup.py for non-utf8 locales on Python 3 157 | 158 | 2.4 2014-11-01 159 | - Python 3 support 160 | - Performance improvement 161 | - Fix LRU cache behavior 162 | 163 | 2.3.2 2014-10-11 164 | - Fix OpenSSL on Windows 165 | 166 | 2.3.1 2014-10-09 167 | - Does not require M2Crypto any more 168 | 169 | 2.3 2014-09-23 170 | - Support CFB1, CFB8 and CTR mode of AES 171 | - Do not require password config when using port_password 172 | - Use SIGTERM instead of SIGQUIT on Windows 173 | 174 | 2.2.2 2014-09-14 175 | - Fix when multiple DNS set, IPv6 only sites are broken 176 | 177 | 2.2.1 2014-09-10 178 | - Support graceful shutdown 179 | - Fix some bugs 180 | 181 | 2.2.0 2014-09-09 182 | - Add RC4-MD5 encryption 183 | 184 | 2.1.0 2014-08-10 185 | - Use only IPv4 DNS server 186 | - Does not ship config.json 187 | - Better error message 188 | 189 | 2.0.12 2014-07-26 190 | - Support -q quiet mode 191 | - Exit 0 when showing help with -h 192 | 193 | 2.0.11 2014-07-12 194 | - Prefers IP addresses over hostnames, more friendly with socksify and openvpn 195 | 196 | 2.0.10 2014-07-11 197 | - Fix UDP on local 198 | 199 | 2.0.9 2014-07-06 200 | - Fix EWOULDBLOCK on Windows 201 | - Fix Unicode config problem on some platforms 202 | 203 | 2.0.8 2014-06-23 204 | - Use multiple DNS to query hostnames 205 | 206 | 2.0.7 2014-06-21 207 | - Fix fastopen on local 208 | - Fallback when fastopen is not available 209 | - Add verbose logging mode -vv 210 | - Verify if hostname is valid 211 | 212 | 2.0.6 2014-06-19 213 | - Fix CPU 100% on POLL_HUP 214 | - More friendly logging 215 | 216 | 2.0.5 2014-06-18 217 | - Support a simple config format for multiple ports 218 | 219 | 2.0.4 2014-06-12 220 | - Fix worker master 221 | 222 | 2.0.3 2014-06-11 223 | - Fix table encryption with UDP 224 | 225 | 2.0.2 2014-06-11 226 | - Add asynchronous DNS in TCP relay 227 | 228 | 2.0.1 2014-06-05 229 | - Better logging 230 | - Maybe fix bad file descriptor 231 | 232 | 2.0 2014-06-05 233 | - Use a new event model 234 | - Remove gevent 235 | - Refuse to use default password 236 | - Fix a problem when using multiple passwords with table encryption 237 | 238 | 1.4.5 2014-05-24 239 | - Add timeout in TCP server 240 | - Close sockets in master process 241 | 242 | 1.4.4 2014-05-17 243 | - Support multiple workers 244 | 245 | 1.4.3 2014-05-13 246 | - Fix Windows 247 | 248 | 1.4.2 2014-05-10 249 | - Add salsa20-ctr cipher 250 | 251 | 1.4.1 2014-05-03 252 | - Fix error log 253 | - Fix EINPROGESS with some version of gevent 254 | 255 | 1.4.0 2014-05-02 256 | - Adds UDP relay 257 | - TCP fast open support on Linux 3.7+ 258 | 259 | 1.3.7 2014-04-10 260 | - Fix a typo in help 261 | 262 | 1.3.6 2014-04-10 263 | - Fix a typo in help 264 | 265 | 1.3.5 2014-04-07 266 | - Add help 267 | - Change default local binding address into 127.0.0.1 268 | 269 | 1.3.4 2014-02-17 270 | - Fix a bug when no config file exists 271 | - Client now support multiple server ports and multiple server/port pairs 272 | - Better error message with bad config.json format and wrong password 273 | 274 | 1.3.3 2013-07-09 275 | - Fix default key length of rc2 276 | 277 | 1.3.2 2013-07-04 278 | - Server will listen at server IP specified in config 279 | - Check config file and show some warning messages 280 | 281 | 1.3.1 2013-06-29 282 | - Fix -c arg 283 | 284 | 1.3.0 2013-06-22 285 | - Move to pypi 286 | 287 | 1.2.3 2013-06-14 288 | - add bind address 289 | 290 | 1.2.2 2013-05-31 291 | - local can listen at ::0 with -6 arg; bump 1.2.2 292 | 293 | 1.2.1 2013-05-23 294 | - Fix an OpenSSL crash 295 | 296 | 1.2 2013-05-22 297 | - Use random iv, we finally have strong encryption 298 | 299 | 1.1.1 2013-05-21 300 | - Add encryption, AES, blowfish, etc. 301 | 302 | 1.1 2013-05-16 303 | - Support IPv6 addresses (type 4) 304 | - Drop Python 2.5 support 305 | 306 | 1.0 2013-04-03 307 | - Fix -6 IPv6 308 | 309 | 0.9.4 2013-03-04 310 | - Support Python 2.5 311 | 312 | 0.9.3 2013-01-14 313 | - Fix conn termination null data 314 | 315 | 0.9.2 2013-01-05 316 | - Change default timeout 317 | 318 | 0.9.1 2013-01-05 319 | - Add Travis-CI test 320 | 321 | 0.9 2012-12-30 322 | - Replace send with sendall, fix FreeBSD 323 | 324 | 0.6 2012-12-06 325 | - Support args 326 | 327 | 0.5 2012-11-08 328 | - Fix encryption with negative md5sum 329 | 330 | 0.4 2012-11-02 331 | - Move config into a JSON file 332 | - Auto-detect config path 333 | 334 | 0.3 2012-06-06 335 | - Move socks5 negotiation to local 336 | 337 | 0.2 2012-05-11 338 | - Add -6 arg for IPv6 339 | - Fix socket.error 340 | 341 | 0.1 2012-04-20 342 | - Initial version 343 | -------------------------------------------------------------------------------- /shadowsocks/eventloop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2013-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # from ssloop 19 | # https://github.com/clowwindy/ssloop 20 | 21 | from __future__ import absolute_import, division, print_function, \ 22 | with_statement 23 | 24 | import os 25 | import time 26 | import socket 27 | import select 28 | import errno 29 | import logging 30 | from collections import defaultdict 31 | 32 | from shadowsocks import shell 33 | 34 | 35 | __all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR', 36 | 'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES'] 37 | 38 | POLL_NULL = 0x00 39 | POLL_IN = 0x01 40 | POLL_OUT = 0x04 41 | POLL_ERR = 0x08 42 | POLL_HUP = 0x10 43 | POLL_NVAL = 0x20 44 | 45 | 46 | EVENT_NAMES = { 47 | POLL_NULL: 'POLL_NULL', 48 | POLL_IN: 'POLL_IN', 49 | POLL_OUT: 'POLL_OUT', 50 | POLL_ERR: 'POLL_ERR', 51 | POLL_HUP: 'POLL_HUP', 52 | POLL_NVAL: 'POLL_NVAL', 53 | } 54 | 55 | # we check timeouts every TIMEOUT_PRECISION seconds 56 | TIMEOUT_PRECISION = 2 57 | 58 | 59 | class KqueueLoop(object): 60 | 61 | MAX_EVENTS = 1024 62 | 63 | def __init__(self): 64 | self._kqueue = select.kqueue() 65 | self._fds = {} 66 | 67 | def _control(self, fd, mode, flags): 68 | events = [] 69 | if mode & POLL_IN: 70 | events.append(select.kevent(fd, select.KQ_FILTER_READ, flags)) 71 | if mode & POLL_OUT: 72 | events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags)) 73 | for e in events: 74 | self._kqueue.control([e], 0) 75 | 76 | def poll(self, timeout): 77 | if timeout < 0: 78 | timeout = None # kqueue behaviour 79 | events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout) 80 | results = defaultdict(lambda: POLL_NULL) 81 | for e in events: 82 | fd = e.ident 83 | if e.filter == select.KQ_FILTER_READ: 84 | results[fd] |= POLL_IN 85 | elif e.filter == select.KQ_FILTER_WRITE: 86 | results[fd] |= POLL_OUT 87 | return results.items() 88 | 89 | def register(self, fd, mode): 90 | self._fds[fd] = mode 91 | self._control(fd, mode, select.KQ_EV_ADD) 92 | 93 | def unregister(self, fd): 94 | self._control(fd, self._fds[fd], select.KQ_EV_DELETE) 95 | del self._fds[fd] 96 | 97 | def modify(self, fd, mode): 98 | self.unregister(fd) 99 | self.register(fd, mode) 100 | 101 | def close(self): 102 | self._kqueue.close() 103 | 104 | 105 | class SelectLoop(object): 106 | 107 | def __init__(self): 108 | self._r_list = set() 109 | self._w_list = set() 110 | self._x_list = set() 111 | 112 | def poll(self, timeout): 113 | r, w, x = select.select(self._r_list, self._w_list, self._x_list, 114 | timeout) 115 | results = defaultdict(lambda: POLL_NULL) 116 | for p in [(r, POLL_IN), (w, POLL_OUT), (x, POLL_ERR)]: 117 | for fd in p[0]: 118 | results[fd] |= p[1] 119 | return results.items() 120 | 121 | def register(self, fd, mode): 122 | if mode & POLL_IN: 123 | self._r_list.add(fd) 124 | if mode & POLL_OUT: 125 | self._w_list.add(fd) 126 | if mode & POLL_ERR: 127 | self._x_list.add(fd) 128 | 129 | def unregister(self, fd): 130 | if fd in self._r_list: 131 | self._r_list.remove(fd) 132 | if fd in self._w_list: 133 | self._w_list.remove(fd) 134 | if fd in self._x_list: 135 | self._x_list.remove(fd) 136 | 137 | def modify(self, fd, mode): 138 | self.unregister(fd) 139 | self.register(fd, mode) 140 | 141 | def close(self): 142 | pass 143 | 144 | 145 | class EventLoop(object): 146 | def __init__(self): 147 | if hasattr(select, 'epoll'): 148 | self._impl = select.epoll() 149 | model = 'epoll' 150 | elif hasattr(select, 'kqueue'): 151 | self._impl = KqueueLoop() 152 | model = 'kqueue' 153 | elif hasattr(select, 'select'): 154 | self._impl = SelectLoop() 155 | model = 'select' 156 | else: 157 | raise Exception('can not find any available functions in select ' 158 | 'package') 159 | self._fdmap = {} # (f, handler) 160 | self._last_time = time.time() 161 | self._periodic_callbacks = [] 162 | self._stopping = False 163 | logging.debug('using event model: %s', model) 164 | 165 | def poll(self, timeout=None): 166 | events = self._impl.poll(timeout) 167 | return [(self._fdmap[fd][0], fd, event) for fd, event in events] 168 | 169 | def add(self, f, mode, handler): 170 | fd = f.fileno() 171 | self._fdmap[fd] = (f, handler) 172 | self._impl.register(fd, mode) 173 | 174 | def remove(self, f): 175 | fd = f.fileno() 176 | del self._fdmap[fd] 177 | self._impl.unregister(fd) 178 | 179 | def removefd(self, fd): 180 | del self._fdmap[fd] 181 | self._impl.unregister(fd) 182 | 183 | def add_periodic(self, callback): 184 | self._periodic_callbacks.append(callback) 185 | 186 | def remove_periodic(self, callback): 187 | self._periodic_callbacks.remove(callback) 188 | 189 | def modify(self, f, mode): 190 | fd = f.fileno() 191 | self._impl.modify(fd, mode) 192 | 193 | def stop(self): 194 | self._stopping = True 195 | 196 | def run(self): 197 | events = [] 198 | while not self._stopping: 199 | asap = False 200 | try: 201 | events = self.poll(TIMEOUT_PRECISION) 202 | except (OSError, IOError) as e: 203 | if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): 204 | # EPIPE: Happens when the client closes the connection 205 | # EINTR: Happens when received a signal 206 | # handles them as soon as possible 207 | asap = True 208 | logging.debug('poll:%s', e) 209 | else: 210 | logging.error('poll:%s', e) 211 | import traceback 212 | traceback.print_exc() 213 | continue 214 | 215 | handle = False 216 | for sock, fd, event in events: 217 | handler = self._fdmap.get(fd, None) 218 | if handler is not None: 219 | handler = handler[1] 220 | try: 221 | handle = handler.handle_event(sock, fd, event) or handle 222 | except (OSError, IOError) as e: 223 | shell.print_exception(e) 224 | now = time.time() 225 | if asap or now - self._last_time >= TIMEOUT_PRECISION: 226 | for callback in self._periodic_callbacks: 227 | callback() 228 | self._last_time = now 229 | if events and not handle: 230 | time.sleep(0.001) 231 | 232 | def __del__(self): 233 | self._impl.close() 234 | 235 | 236 | # from tornado 237 | def errno_from_exception(e): 238 | """Provides the errno from an Exception object. 239 | 240 | There are cases that the errno attribute was not set so we pull 241 | the errno out of the args but if someone instatiates an Exception 242 | without any args you will get a tuple error. So this function 243 | abstracts all that behavior to give you a safe way to get the 244 | errno. 245 | """ 246 | 247 | if hasattr(e, 'errno'): 248 | return e.errno 249 | elif e.args: 250 | return e.args[0] 251 | else: 252 | return None 253 | 254 | 255 | # from tornado 256 | def get_sock_error(sock): 257 | error_number = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 258 | return socket.error(error_number, os.strerror(error_number)) 259 | -------------------------------------------------------------------------------- /shadowsocks/ordereddict.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | ################################################################################ 4 | ### OrderedDict 5 | ################################################################################ 6 | 7 | class OrderedDict(dict): 8 | 'Dictionary that remembers insertion order' 9 | # An inherited dict maps keys to values. 10 | # The inherited dict provides __getitem__, __len__, __contains__, and get. 11 | # The remaining methods are order-aware. 12 | # Big-O running times for all methods are the same as regular dictionaries. 13 | 14 | # The internal self.__map dict maps keys to links in a doubly linked list. 15 | # The circular doubly linked list starts and ends with a sentinel element. 16 | # The sentinel element never gets deleted (this simplifies the algorithm). 17 | # Each link is stored as a list of length three: [PREV, NEXT, KEY]. 18 | 19 | def __init__(*args, **kwds): 20 | '''Initialize an ordered dictionary. The signature is the same as 21 | regular dictionaries, but keyword arguments are not recommended because 22 | their insertion order is arbitrary. 23 | 24 | ''' 25 | if not args: 26 | raise TypeError("descriptor '__init__' of 'OrderedDict' object " 27 | "needs an argument") 28 | self = args[0] 29 | args = args[1:] 30 | if len(args) > 1: 31 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 32 | try: 33 | self.__root 34 | except AttributeError: 35 | self.__root = root = [] # sentinel node 36 | root[:] = [root, root, None] 37 | self.__map = {} 38 | self.__update(*args, **kwds) 39 | 40 | def __setitem__(self, key, value, dict_setitem=dict.__setitem__): 41 | 'od.__setitem__(i, y) <==> od[i]=y' 42 | # Setting a new item creates a new link at the end of the linked list, 43 | # and the inherited dictionary is updated with the new key/value pair. 44 | if key not in self: 45 | root = self.__root 46 | last = root[0] 47 | last[1] = root[0] = self.__map[key] = [last, root, key] 48 | return dict_setitem(self, key, value) 49 | 50 | def __delitem__(self, key, dict_delitem=dict.__delitem__): 51 | 'od.__delitem__(y) <==> del od[y]' 52 | # Deleting an existing item uses self.__map to find the link which gets 53 | # removed by updating the links in the predecessor and successor nodes. 54 | dict_delitem(self, key) 55 | link_prev, link_next, _ = self.__map.pop(key) 56 | link_prev[1] = link_next # update link_prev[NEXT] 57 | link_next[0] = link_prev # update link_next[PREV] 58 | 59 | def __iter__(self): 60 | 'od.__iter__() <==> iter(od)' 61 | # Traverse the linked list in order. 62 | root = self.__root 63 | curr = root[1] # start at the first node 64 | while curr is not root: 65 | yield curr[2] # yield the curr[KEY] 66 | curr = curr[1] # move to next node 67 | 68 | def __reversed__(self): 69 | 'od.__reversed__() <==> reversed(od)' 70 | # Traverse the linked list in reverse order. 71 | root = self.__root 72 | curr = root[0] # start at the last node 73 | while curr is not root: 74 | yield curr[2] # yield the curr[KEY] 75 | curr = curr[0] # move to previous node 76 | 77 | def clear(self): 78 | 'od.clear() -> None. Remove all items from od.' 79 | root = self.__root 80 | root[:] = [root, root, None] 81 | self.__map.clear() 82 | dict.clear(self) 83 | 84 | # -- the following methods do not depend on the internal structure -- 85 | 86 | def keys(self): 87 | 'od.keys() -> list of keys in od' 88 | return list(self) 89 | 90 | def values(self): 91 | 'od.values() -> list of values in od' 92 | return [self[key] for key in self] 93 | 94 | def items(self): 95 | 'od.items() -> list of (key, value) pairs in od' 96 | return [(key, self[key]) for key in self] 97 | 98 | def iterkeys(self): 99 | 'od.iterkeys() -> an iterator over the keys in od' 100 | return iter(self) 101 | 102 | def itervalues(self): 103 | 'od.itervalues -> an iterator over the values in od' 104 | for k in self: 105 | yield self[k] 106 | 107 | def iteritems(self): 108 | 'od.iteritems -> an iterator over the (key, value) pairs in od' 109 | for k in self: 110 | yield (k, self[k]) 111 | 112 | update = collections.MutableMapping.update 113 | 114 | __update = update # let subclasses override update without breaking __init__ 115 | 116 | __marker = object() 117 | 118 | def pop(self, key, default=__marker): 119 | '''od.pop(k[,d]) -> v, remove specified key and return the corresponding 120 | value. If key is not found, d is returned if given, otherwise KeyError 121 | is raised. 122 | 123 | ''' 124 | if key in self: 125 | result = self[key] 126 | del self[key] 127 | return result 128 | if default is self.__marker: 129 | raise KeyError(key) 130 | return default 131 | 132 | def setdefault(self, key, default=None): 133 | 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' 134 | if key in self: 135 | return self[key] 136 | self[key] = default 137 | return default 138 | 139 | def popitem(self, last=True): 140 | '''od.popitem() -> (k, v), return and remove a (key, value) pair. 141 | Pairs are returned in LIFO order if last is true or FIFO order if false. 142 | 143 | ''' 144 | if not self: 145 | raise KeyError('dictionary is empty') 146 | key = next(reversed(self) if last else iter(self)) 147 | value = self.pop(key) 148 | return key, value 149 | 150 | def __repr__(self, _repr_running={}): 151 | 'od.__repr__() <==> repr(od)' 152 | call_key = id(self), _get_ident() 153 | if call_key in _repr_running: 154 | return '...' 155 | _repr_running[call_key] = 1 156 | try: 157 | if not self: 158 | return '%s()' % (self.__class__.__name__,) 159 | return '%s(%r)' % (self.__class__.__name__, self.items()) 160 | finally: 161 | del _repr_running[call_key] 162 | 163 | def __reduce__(self): 164 | 'Return state information for pickling' 165 | items = [[k, self[k]] for k in self] 166 | inst_dict = vars(self).copy() 167 | for k in vars(OrderedDict()): 168 | inst_dict.pop(k, None) 169 | if inst_dict: 170 | return (self.__class__, (items,), inst_dict) 171 | return self.__class__, (items,) 172 | 173 | def copy(self): 174 | 'od.copy() -> a shallow copy of od' 175 | return self.__class__(self) 176 | 177 | @classmethod 178 | def fromkeys(cls, iterable, value=None): 179 | '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. 180 | If not specified, the value defaults to None. 181 | 182 | ''' 183 | self = cls() 184 | for key in iterable: 185 | self[key] = value 186 | return self 187 | 188 | def __eq__(self, other): 189 | '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive 190 | while comparison to a regular mapping is order-insensitive. 191 | 192 | ''' 193 | if isinstance(other, OrderedDict): 194 | return dict.__eq__(self, other) and all(_imap(_eq, self, other)) 195 | return dict.__eq__(self, other) 196 | 197 | def __ne__(self, other): 198 | 'od.__ne__(y) <==> od!=y' 199 | return not self == other 200 | 201 | # -- the following methods support python 3.x style dictionary views -- 202 | 203 | def viewkeys(self): 204 | "od.viewkeys() -> a set-like object providing a view on od's keys" 205 | return KeysView(self) 206 | 207 | def viewvalues(self): 208 | "od.viewvalues() -> an object providing a view on od's values" 209 | return ValuesView(self) 210 | 211 | def viewitems(self): 212 | "od.viewitems() -> a set-like object providing a view on od's items" 213 | return ItemsView(self) 214 | 215 | -------------------------------------------------------------------------------- /shadowsocks/crypto/table.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import string 21 | import struct 22 | import hashlib 23 | 24 | 25 | __all__ = ['ciphers'] 26 | 27 | cached_tables = {} 28 | 29 | if hasattr(string, 'maketrans'): 30 | maketrans = string.maketrans 31 | translate = string.translate 32 | else: 33 | maketrans = bytes.maketrans 34 | translate = bytes.translate 35 | 36 | 37 | def get_table(key): 38 | m = hashlib.md5() 39 | m.update(key) 40 | s = m.digest() 41 | a, b = struct.unpack(' 1: 73 | stat_counter_dict = None 74 | else: 75 | stat_counter_dict = {} 76 | port_password = config['port_password'] 77 | config_password = config.get('password', 'm') 78 | del config['port_password'] 79 | for port, password_obfs in port_password.items(): 80 | method = config["method"] 81 | protocol = config.get("protocol", 'origin') 82 | protocol_param = config.get("protocol_param", '') 83 | obfs = config.get("obfs", 'plain') 84 | obfs_param = config.get("obfs_param", '') 85 | bind = config.get("out_bind", '') 86 | bindv6 = config.get("out_bindv6", '') 87 | if type(password_obfs) == list: 88 | password = password_obfs[0] 89 | obfs = common.to_str(password_obfs[1]) 90 | if len(password_obfs) > 2: 91 | protocol = common.to_str(password_obfs[2]) 92 | elif type(password_obfs) == dict: 93 | password = password_obfs.get('password', config_password) 94 | method = common.to_str(password_obfs.get('method', method)) 95 | protocol = common.to_str(password_obfs.get('protocol', protocol)) 96 | protocol_param = common.to_str(password_obfs.get('protocol_param', protocol_param)) 97 | obfs = common.to_str(password_obfs.get('obfs', obfs)) 98 | obfs_param = common.to_str(password_obfs.get('obfs_param', obfs_param)) 99 | bind = password_obfs.get('out_bind', bind) 100 | bindv6 = password_obfs.get('out_bindv6', bindv6) 101 | else: 102 | password = password_obfs 103 | a_config = config.copy() 104 | ipv6_ok = False 105 | logging.info("server start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" % 106 | (protocol, password, method, obfs, obfs_param)) 107 | if 'server_ipv6' in a_config: 108 | try: 109 | if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == "[" and a_config['server_ipv6'][-1] == "]": 110 | a_config['server_ipv6'] = a_config['server_ipv6'][1:-1] 111 | a_config['server_port'] = int(port) 112 | a_config['password'] = password 113 | a_config['method'] = method 114 | a_config['protocol'] = protocol 115 | a_config['protocol_param'] = protocol_param 116 | a_config['obfs'] = obfs 117 | a_config['obfs_param'] = obfs_param 118 | a_config['out_bind'] = bind 119 | a_config['out_bindv6'] = bindv6 120 | a_config['server'] = a_config['server_ipv6'] 121 | logging.info("starting server at [%s]:%d" % 122 | (a_config['server'], int(port))) 123 | tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) 124 | udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) 125 | if a_config['server_ipv6'] == b"::": 126 | ipv6_ok = True 127 | except Exception as e: 128 | shell.print_exception(e) 129 | 130 | try: 131 | a_config = config.copy() 132 | a_config['server_port'] = int(port) 133 | a_config['password'] = password 134 | a_config['method'] = method 135 | a_config['protocol'] = protocol 136 | a_config['protocol_param'] = protocol_param 137 | a_config['obfs'] = obfs 138 | a_config['obfs_param'] = obfs_param 139 | a_config['out_bind'] = bind 140 | a_config['out_bindv6'] = bindv6 141 | logging.info("starting server at %s:%d" % 142 | (a_config['server'], int(port))) 143 | tcp_servers.append(tcprelay.TCPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) 144 | udp_servers.append(udprelay.UDPRelay(a_config, dns_resolver, False, stat_counter=stat_counter_dict)) 145 | except Exception as e: 146 | if not ipv6_ok: 147 | shell.print_exception(e) 148 | 149 | def run_server(): 150 | def child_handler(signum, _): 151 | logging.warn('received SIGQUIT, doing graceful shutting down..') 152 | list(map(lambda s: s.close(next_tick=True), 153 | tcp_servers + udp_servers)) 154 | signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), 155 | child_handler) 156 | 157 | def int_handler(signum, _): 158 | sys.exit(1) 159 | signal.signal(signal.SIGINT, int_handler) 160 | 161 | try: 162 | loop = eventloop.EventLoop() 163 | dns_resolver.add_to_loop(loop) 164 | list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers)) 165 | 166 | daemon.set_user(config.get('user', None)) 167 | loop.run() 168 | except Exception as e: 169 | shell.print_exception(e) 170 | sys.exit(1) 171 | 172 | if int(config['workers']) > 1: 173 | if os.name == 'posix': 174 | children = [] 175 | is_child = False 176 | for i in range(0, int(config['workers'])): 177 | r = os.fork() 178 | if r == 0: 179 | logging.info('worker started') 180 | is_child = True 181 | run_server() 182 | break 183 | else: 184 | children.append(r) 185 | if not is_child: 186 | def handler(signum, _): 187 | for pid in children: 188 | try: 189 | os.kill(pid, signum) 190 | os.waitpid(pid, 0) 191 | except OSError: # child may already exited 192 | pass 193 | sys.exit() 194 | signal.signal(signal.SIGTERM, handler) 195 | signal.signal(signal.SIGQUIT, handler) 196 | signal.signal(signal.SIGINT, handler) 197 | 198 | # master 199 | for a_tcp_server in tcp_servers: 200 | a_tcp_server.close() 201 | for a_udp_server in udp_servers: 202 | a_udp_server.close() 203 | dns_resolver.close() 204 | 205 | for child in children: 206 | os.waitpid(child, 0) 207 | else: 208 | logging.warn('worker is only available on Unix/Linux') 209 | run_server() 210 | else: 211 | run_server() 212 | 213 | 214 | if __name__ == '__main__': 215 | main() 216 | -------------------------------------------------------------------------------- /server_pool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014 clowwindy 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | import os 25 | import logging 26 | import struct 27 | import time 28 | from shadowsocks import shell, eventloop, tcprelay, udprelay, asyncdns, common 29 | import threading 30 | import sys 31 | import traceback 32 | from socket import * 33 | from configloader import load_config, get_config 34 | 35 | class MainThread(threading.Thread): 36 | def __init__(self, params): 37 | super(MainThread, self).__init__() 38 | self.params = params 39 | 40 | def run(self): 41 | ServerPool._loop(*self.params) 42 | 43 | class ServerPool(object): 44 | 45 | instance = None 46 | 47 | def __init__(self): 48 | shell.check_python() 49 | self.config = shell.get_config(False) 50 | self.dns_resolver = asyncdns.DNSResolver() 51 | if not self.config.get('dns_ipv6', False): 52 | asyncdns.IPV6_CONNECTION_SUPPORT = False 53 | 54 | self.mgr = None #asyncmgr.ServerMgr() 55 | 56 | self.tcp_servers_pool = {} 57 | self.tcp_ipv6_servers_pool = {} 58 | self.udp_servers_pool = {} 59 | self.udp_ipv6_servers_pool = {} 60 | self.stat_counter = {} 61 | 62 | self.loop = eventloop.EventLoop() 63 | self.thread = MainThread( (self.loop, self.dns_resolver, self.mgr) ) 64 | self.thread.start() 65 | 66 | @staticmethod 67 | def get_instance(): 68 | if ServerPool.instance is None: 69 | ServerPool.instance = ServerPool() 70 | return ServerPool.instance 71 | 72 | def stop(self): 73 | self.loop.stop() 74 | 75 | @staticmethod 76 | def _loop(loop, dns_resolver, mgr): 77 | try: 78 | if mgr is not None: 79 | mgr.add_to_loop(loop) 80 | dns_resolver.add_to_loop(loop) 81 | loop.run() 82 | except (KeyboardInterrupt, IOError, OSError) as e: 83 | logging.error(e) 84 | traceback.print_exc() 85 | os.exit(0) 86 | except Exception as e: 87 | logging.error(e) 88 | traceback.print_exc() 89 | 90 | def server_is_run(self, port): 91 | port = int(port) 92 | ret = 0 93 | if port in self.tcp_servers_pool: 94 | ret = 1 95 | if port in self.tcp_ipv6_servers_pool: 96 | ret |= 2 97 | return ret 98 | 99 | def server_run_status(self, port): 100 | if 'server' in self.config: 101 | if port not in self.tcp_servers_pool: 102 | return False 103 | if 'server_ipv6' in self.config: 104 | if port not in self.tcp_ipv6_servers_pool: 105 | return False 106 | return True 107 | 108 | def new_server(self, port, user_config): 109 | ret = True 110 | port = int(port) 111 | ipv6_ok = False 112 | 113 | if 'server_ipv6' in self.config: 114 | if port in self.tcp_ipv6_servers_pool: 115 | logging.info("server already at %s:%d" % (self.config['server_ipv6'], port)) 116 | return 'this port server is already running' 117 | else: 118 | a_config = self.config.copy() 119 | a_config.update(user_config) 120 | if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == "[" and a_config['server_ipv6'][-1] == "]": 121 | a_config['server_ipv6'] = a_config['server_ipv6'][1:-1] 122 | a_config['server'] = a_config['server_ipv6'] 123 | a_config['server_port'] = port 124 | a_config['max_connect'] = 128 125 | a_config['method'] = common.to_str(a_config['method']) 126 | try: 127 | logging.info("starting server at [%s]:%d" % (common.to_str(a_config['server']), port)) 128 | 129 | tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter) 130 | tcp_server.add_to_loop(self.loop) 131 | self.tcp_ipv6_servers_pool.update({port: tcp_server}) 132 | 133 | udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter) 134 | udp_server.add_to_loop(self.loop) 135 | self.udp_ipv6_servers_pool.update({port: udp_server}) 136 | 137 | if common.to_str(a_config['server_ipv6']) == "::": 138 | ipv6_ok = True 139 | except Exception as e: 140 | logging.warn("IPV6 %s " % (e,)) 141 | 142 | if 'server' in self.config: 143 | if port in self.tcp_servers_pool: 144 | logging.info("server already at %s:%d" % (common.to_str(self.config['server']), port)) 145 | return 'this port server is already running' 146 | else: 147 | a_config = self.config.copy() 148 | a_config.update(user_config) 149 | a_config['server_port'] = port 150 | a_config['max_connect'] = 128 151 | a_config['method'] = common.to_str(a_config['method']) 152 | try: 153 | logging.info("starting server at %s:%d" % (common.to_str(a_config['server']), port)) 154 | 155 | tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False) 156 | tcp_server.add_to_loop(self.loop) 157 | self.tcp_servers_pool.update({port: tcp_server}) 158 | 159 | udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False) 160 | udp_server.add_to_loop(self.loop) 161 | self.udp_servers_pool.update({port: udp_server}) 162 | 163 | except Exception as e: 164 | if not ipv6_ok: 165 | logging.warn("IPV4 %s " % (e,)) 166 | 167 | return True 168 | 169 | def del_server(self, port): 170 | port = int(port) 171 | logging.info("del server at %d" % port) 172 | try: 173 | udpsock = socket(AF_INET, SOCK_DGRAM) 174 | udpsock.sendto('%s:%s:0:0' % (get_config().MANAGE_PASS, port), (get_config().MANAGE_BIND_IP, get_config().MANAGE_PORT)) 175 | udpsock.close() 176 | except Exception as e: 177 | logging.warn(e) 178 | return True 179 | 180 | def cb_del_server(self, port): 181 | port = int(port) 182 | 183 | if port not in self.tcp_servers_pool: 184 | logging.info("stopped server at %s:%d already stop" % (self.config['server'], port)) 185 | else: 186 | logging.info("stopped server at %s:%d" % (self.config['server'], port)) 187 | try: 188 | self.tcp_servers_pool[port].close(True) 189 | del self.tcp_servers_pool[port] 190 | except Exception as e: 191 | logging.warn(e) 192 | try: 193 | self.udp_servers_pool[port].close(True) 194 | del self.udp_servers_pool[port] 195 | except Exception as e: 196 | logging.warn(e) 197 | 198 | if 'server_ipv6' in self.config: 199 | if port not in self.tcp_ipv6_servers_pool: 200 | logging.info("stopped server at [%s]:%d already stop" % (self.config['server_ipv6'], port)) 201 | else: 202 | logging.info("stopped server at [%s]:%d" % (self.config['server_ipv6'], port)) 203 | try: 204 | self.tcp_ipv6_servers_pool[port].close(True) 205 | del self.tcp_ipv6_servers_pool[port] 206 | except Exception as e: 207 | logging.warn(e) 208 | try: 209 | self.udp_ipv6_servers_pool[port].close(True) 210 | del self.udp_ipv6_servers_pool[port] 211 | except Exception as e: 212 | logging.warn(e) 213 | 214 | return True 215 | 216 | def update_mu_users(self, port, users): 217 | port = int(port) 218 | if port in self.tcp_servers_pool: 219 | try: 220 | self.tcp_servers_pool[port].update_users(users) 221 | except Exception as e: 222 | logging.warn(e) 223 | try: 224 | self.udp_servers_pool[port].update_users(users) 225 | except Exception as e: 226 | logging.warn(e) 227 | if port in self.tcp_ipv6_servers_pool: 228 | try: 229 | self.tcp_ipv6_servers_pool[port].update_users(users) 230 | except Exception as e: 231 | logging.warn(e) 232 | try: 233 | self.udp_ipv6_servers_pool[port].update_users(users) 234 | except Exception as e: 235 | logging.warn(e) 236 | 237 | def get_server_transfer(self, port): 238 | port = int(port) 239 | uid = struct.pack('= STAT_SEND_LIMIT: 168 | send_data(r) 169 | r.clear() 170 | i = 0 171 | if len(r) > 0 : 172 | send_data(r) 173 | self._statistics.clear() 174 | 175 | def _send_control_data(self, data): 176 | if self._control_client_addr: 177 | try: 178 | self._control_socket.sendto(data, self._control_client_addr) 179 | except (socket.error, OSError, IOError) as e: 180 | error_no = eventloop.errno_from_exception(e) 181 | if error_no in (errno.EAGAIN, errno.EINPROGRESS, 182 | errno.EWOULDBLOCK): 183 | return 184 | else: 185 | shell.print_exception(e) 186 | if self._config['verbose']: 187 | traceback.print_exc() 188 | 189 | def run(self): 190 | self._loop.run() 191 | 192 | 193 | def run(config): 194 | Manager(config).run() 195 | 196 | 197 | def test(): 198 | import time 199 | import threading 200 | import struct 201 | from shadowsocks import encrypt 202 | 203 | logging.basicConfig(level=5, 204 | format='%(asctime)s %(levelname)-8s %(message)s', 205 | datefmt='%Y-%m-%d %H:%M:%S') 206 | enc = [] 207 | eventloop.TIMEOUT_PRECISION = 1 208 | 209 | def run_server(): 210 | config = shell.get_config(True) 211 | config = config.copy() 212 | a_config = { 213 | 'server': '127.0.0.1', 214 | 'local_port': 1081, 215 | 'port_password': { 216 | '8381': 'foobar1', 217 | '8382': 'foobar2' 218 | }, 219 | 'method': 'aes-256-cfb', 220 | 'manager_address': '127.0.0.1:6001', 221 | 'timeout': 60, 222 | 'fast_open': False, 223 | 'verbose': 2 224 | } 225 | config.update(a_config) 226 | manager = Manager(config) 227 | enc.append(manager) 228 | manager.run() 229 | 230 | t = threading.Thread(target=run_server) 231 | t.start() 232 | time.sleep(1) 233 | manager = enc[0] 234 | cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 235 | cli.connect(('127.0.0.1', 6001)) 236 | 237 | # test add and remove 238 | time.sleep(1) 239 | cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}') 240 | time.sleep(1) 241 | assert 7001 in manager._relays 242 | data, addr = cli.recvfrom(1506) 243 | assert b'ok' in data 244 | 245 | cli.send(b'remove: {"server_port":8381}') 246 | time.sleep(1) 247 | assert 8381 not in manager._relays 248 | data, addr = cli.recvfrom(1506) 249 | assert b'ok' in data 250 | logging.info('add and remove test passed') 251 | 252 | # test statistics for TCP 253 | header = common.pack_addr(b'google.com') + struct.pack('>H', 80) 254 | data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, 255 | header + b'GET /\r\n\r\n') 256 | tcp_cli = socket.socket() 257 | tcp_cli.connect(('127.0.0.1', 7001)) 258 | tcp_cli.send(data) 259 | tcp_cli.recv(4096) 260 | tcp_cli.close() 261 | 262 | data, addr = cli.recvfrom(1506) 263 | data = common.to_str(data) 264 | assert data.startswith('stat: ') 265 | data = data.split('stat:')[1] 266 | stats = shell.parse_json_in_str(data) 267 | assert '7001' in stats 268 | logging.info('TCP statistics test passed') 269 | 270 | # test statistics for UDP 271 | header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) 272 | data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, 273 | header + b'test') 274 | udp_cli = socket.socket(type=socket.SOCK_DGRAM) 275 | udp_cli.sendto(data, ('127.0.0.1', 8382)) 276 | tcp_cli.close() 277 | 278 | data, addr = cli.recvfrom(1506) 279 | data = common.to_str(data) 280 | assert data.startswith('stat: ') 281 | data = data.split('stat:')[1] 282 | stats = json.loads(data) 283 | assert '8382' in stats 284 | logging.info('UDP statistics test passed') 285 | 286 | manager._loop.stop() 287 | t.join() 288 | 289 | 290 | if __name__ == '__main__': 291 | test() 292 | -------------------------------------------------------------------------------- /mujson_mgr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import traceback 5 | from shadowsocks import shell, common 6 | from configloader import load_config, get_config 7 | import random 8 | import getopt 9 | import sys 10 | import json 11 | import base64 12 | 13 | 14 | class MuJsonLoader(object): 15 | def __init__(self): 16 | self.json = None 17 | 18 | def load(self, path): 19 | l = "[]" 20 | try: 21 | with open(path, 'rb+') as f: 22 | l = f.read().decode('utf8') 23 | except: 24 | pass 25 | self.json = json.loads(l) 26 | 27 | def save(self, path): 28 | if self.json is not None: 29 | output = json.dumps(self.json, sort_keys=True, indent=4, separators=(',', ': ')) 30 | with open(path, 'a'): 31 | pass 32 | with open(path, 'rb+') as f: 33 | f.write(output.encode('utf8')) 34 | f.truncate() 35 | 36 | 37 | class MuMgr(object): 38 | def __init__(self): 39 | self.config_path = get_config().MUDB_FILE 40 | try: 41 | self.server_addr = get_config().SERVER_PUB_ADDR 42 | except: 43 | self.server_addr = '127.0.0.1' 44 | self.data = MuJsonLoader() 45 | 46 | if self.server_addr == '127.0.0.1': 47 | self.server_addr = self.getipaddr() 48 | 49 | def getipaddr(self, ifname='eth0'): 50 | import socket 51 | import struct 52 | ret = '127.0.0.1' 53 | try: 54 | ret = socket.gethostbyname(socket.getfqdn(socket.gethostname())) 55 | except: 56 | pass 57 | if ret == '127.0.0.1': 58 | try: 59 | import fcntl 60 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 61 | ret = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24]) 62 | except: 63 | pass 64 | return ret 65 | 66 | def ssrlink(self, user, encode, muid): 67 | protocol = user.get('protocol', '') 68 | obfs = user.get('obfs', '') 69 | protocol = protocol.replace("_compatible", "") 70 | obfs = obfs.replace("_compatible", "") 71 | protocol_param = '' 72 | if muid is not None: 73 | protocol_param_ = user.get('protocol_param', '') 74 | param = protocol_param_.split('#') 75 | if len(param) == 2: 76 | for row in self.data.json: 77 | if int(row['port']) == muid: 78 | param = str(muid) + ':' + row['passwd'] 79 | protocol_param = '/?protoparam=' + common.to_str(base64.urlsafe_b64encode(common.to_bytes(param))).replace("=", "") 80 | break 81 | link = ("%s:%s:%s:%s:%s:%s" % (self.server_addr, user['port'], protocol, user['method'], obfs, common.to_str(base64.urlsafe_b64encode(common.to_bytes(user['passwd']))).replace("=", ""))) + protocol_param 82 | return "ssr://" + (encode and common.to_str(base64.urlsafe_b64encode(common.to_bytes(link))).replace("=", "") or link) 83 | 84 | def userinfo(self, user, muid = None): 85 | ret = "" 86 | key_list = ['user', 'port', 'method', 'passwd', 'protocol', 'protocol_param', 'obfs', 'obfs_param', 'transfer_enable', 'u', 'd'] 87 | for key in sorted(user): 88 | if key not in key_list: 89 | key_list.append(key) 90 | for key in key_list: 91 | if key in ['enable'] or key not in user: 92 | continue 93 | ret += '\n' 94 | if (muid is not None) and (key in ['protocol_param']): 95 | for row in self.data.json: 96 | if int(row['port']) == muid: 97 | ret += " %s : %s" % (key, str(muid) + ':' + row['passwd']) 98 | break 99 | elif key in ['transfer_enable', 'u', 'd']: 100 | if muid is not None: 101 | for row in self.data.json: 102 | if int(row['port']) == muid: 103 | val = row[key] 104 | break 105 | else: 106 | val = user[key] 107 | if val / 1024 < 4: 108 | ret += " %s : %s" % (key, val) 109 | elif val / 1024 ** 2 < 4: 110 | val /= float(1024) 111 | ret += " %s : %s K Bytes" % (key, val) 112 | elif val / 1024 ** 3 < 4: 113 | val /= float(1024 ** 2) 114 | ret += " %s : %s M Bytes" % (key, val) 115 | else: 116 | val /= float(1024 ** 3) 117 | ret += " %s : %s G Bytes" % (key, val) 118 | else: 119 | ret += " %s : %s" % (key, user[key]) 120 | ret += "\n " + self.ssrlink(user, False, muid) 121 | ret += "\n " + self.ssrlink(user, True, muid) 122 | return ret 123 | 124 | def rand_pass(self): 125 | return ''.join([random.choice('''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~-_=+(){}[]^&%$@''') for i in range(8)]) 126 | 127 | def add(self, user): 128 | up = {'enable': 1, 'u': 0, 'd': 0, 'method': "aes-128-ctr", 129 | 'protocol': "auth_aes128_md5", 130 | 'obfs': "tls1.2_ticket_auth_compatible", 131 | 'transfer_enable': 9007199254740992} 132 | up['passwd'] = self.rand_pass() 133 | up.update(user) 134 | 135 | self.data.load(self.config_path) 136 | for row in self.data.json: 137 | match = False 138 | if 'user' in user and row['user'] == user['user']: 139 | match = True 140 | if 'port' in user and row['port'] == user['port']: 141 | match = True 142 | if match: 143 | print("user [%s] port [%s] already exist" % (row['user'], row['port'])) 144 | return 145 | self.data.json.append(up) 146 | print("### add user info %s" % self.userinfo(up)) 147 | self.data.save(self.config_path) 148 | 149 | def edit(self, user): 150 | self.data.load(self.config_path) 151 | for row in self.data.json: 152 | match = True 153 | if 'user' in user and row['user'] != user['user']: 154 | match = False 155 | if 'port' in user and row['port'] != user['port']: 156 | match = False 157 | if match: 158 | print("edit user [%s]" % (row['user'],)) 159 | row.update(user) 160 | print("### new user info %s" % self.userinfo(row)) 161 | break 162 | self.data.save(self.config_path) 163 | 164 | def delete(self, user): 165 | self.data.load(self.config_path) 166 | index = 0 167 | for row in self.data.json: 168 | match = True 169 | if 'user' in user and row['user'] != user['user']: 170 | match = False 171 | if 'port' in user and row['port'] != user['port']: 172 | match = False 173 | if match: 174 | print("delete user [%s]" % row['user']) 175 | del self.data.json[index] 176 | break 177 | index += 1 178 | self.data.save(self.config_path) 179 | 180 | def clear_ud(self, user): 181 | up = {'u': 0, 'd': 0} 182 | self.data.load(self.config_path) 183 | for row in self.data.json: 184 | match = True 185 | if 'user' in user and row['user'] != user['user']: 186 | match = False 187 | if 'port' in user and row['port'] != user['port']: 188 | match = False 189 | if match: 190 | row.update(up) 191 | print("clear user [%s]" % row['user']) 192 | self.data.save(self.config_path) 193 | 194 | def list_user(self, user): 195 | self.data.load(self.config_path) 196 | if not user: 197 | for row in self.data.json: 198 | print("user [%s] port %s" % (row['user'], row['port'])) 199 | return 200 | for row in self.data.json: 201 | match = True 202 | if 'user' in user and row['user'] != user['user']: 203 | match = False 204 | if 'port' in user and row['port'] != user['port']: 205 | match = False 206 | if match: 207 | muid = None 208 | if 'muid' in user: 209 | muid = user['muid'] 210 | print("### user [%s] info %s" % (row['user'], self.userinfo(row, muid))) 211 | 212 | 213 | def print_server_help(): 214 | print('''usage: python mujson_manage.py -a|-d|-e|-c|-l [OPTION]... 215 | 216 | Actions: 217 | -a add/edit a user 218 | -d delete a user 219 | -e edit a user 220 | -c set u&d to zero 221 | -l display a user infomation or all users infomation 222 | 223 | Options: 224 | -u USER the user name 225 | -p PORT server port (only this option must be set if add a user) 226 | -k PASSWORD password 227 | -m METHOD encryption method, default: aes-128-ctr 228 | -O PROTOCOL protocol plugin, default: auth_aes128_md5 229 | -o OBFS obfs plugin, default: tls1.2_ticket_auth_compatible 230 | -G PROTOCOL_PARAM protocol plugin param 231 | -g OBFS_PARAM obfs plugin param 232 | -t TRANSFER max transfer for G bytes, default: 8388608 (8 PB or 8192 TB) 233 | -f FORBID set forbidden ports. Example (ban 1~79 and 81~100): -f "1-79,81-100" 234 | -i MUID set sub id to display (only work with -l) 235 | -s SPEED set speed_limit_per_con 236 | -S SPEED set speed_limit_per_user 237 | 238 | General options: 239 | -h, --help show this help message and exit 240 | ''') 241 | 242 | 243 | def main(): 244 | shortopts = 'adeclu:i:p:k:O:o:G:g:m:t:f:hs:S:' 245 | longopts = ['help'] 246 | action = None 247 | user = {} 248 | fast_set_obfs = {'0': 'plain', 249 | '+1': 'http_simple_compatible', 250 | '1': 'http_simple', 251 | '+2': 'tls1.2_ticket_auth_compatible', 252 | '2': 'tls1.2_ticket_auth'} 253 | fast_set_protocol = {'0': 'origin', 254 | 's4': 'auth_sha1_v4', 255 | '+s4': 'auth_sha1_v4_compatible', 256 | 'am': 'auth_aes128_md5', 257 | 'as': 'auth_aes128_sha1', 258 | 'ca': 'auth_chain_a', 259 | } 260 | fast_set_method = {'0': 'none', 261 | 'a1c': 'aes-128-cfb', 262 | 'a2c': 'aes-192-cfb', 263 | 'a3c': 'aes-256-cfb', 264 | 'r': 'rc4-md5', 265 | 'r6': 'rc4-md5-6', 266 | 'c': 'chacha20', 267 | 'ci': 'chacha20-ietf', 268 | 's': 'salsa20', 269 | 'a1': 'aes-128-ctr', 270 | 'a2': 'aes-192-ctr', 271 | 'a3': 'aes-256-ctr'} 272 | try: 273 | optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) 274 | for key, value in optlist: 275 | if key == '-a': 276 | action = 1 277 | elif key == '-d': 278 | action = 2 279 | elif key == '-e': 280 | action = 3 281 | elif key == '-l': 282 | action = 4 283 | elif key == '-c': 284 | action = 0 285 | elif key == '-u': 286 | user['user'] = value 287 | elif key == '-i': 288 | user['muid'] = int(value) 289 | elif key == '-p': 290 | user['port'] = int(value) 291 | elif key == '-k': 292 | user['passwd'] = value 293 | elif key == '-o': 294 | if value in fast_set_obfs: 295 | user['obfs'] = fast_set_obfs[value] 296 | else: 297 | user['obfs'] = value 298 | elif key == '-O': 299 | if value in fast_set_protocol: 300 | user['protocol'] = fast_set_protocol[value] 301 | else: 302 | user['protocol'] = value 303 | elif key == '-g': 304 | user['obfs_param'] = value 305 | elif key == '-G': 306 | user['protocol_param'] = value 307 | elif key == '-s': 308 | user['speed_limit_per_con'] = int(value) 309 | elif key == '-S': 310 | user['speed_limit_per_user'] = int(value) 311 | elif key == '-m': 312 | if value in fast_set_method: 313 | user['method'] = fast_set_method[value] 314 | else: 315 | user['method'] = value 316 | elif key == '-f': 317 | user['forbidden_port'] = value 318 | elif key == '-t': 319 | val = float(value) 320 | try: 321 | val = int(value) 322 | except: 323 | pass 324 | user['transfer_enable'] = int(val * 1024) * (1024 ** 2) 325 | elif key in ('-h', '--help'): 326 | print_server_help() 327 | sys.exit(0) 328 | except getopt.GetoptError as e: 329 | print(e) 330 | sys.exit(2) 331 | 332 | manage = MuMgr() 333 | if action == 0: 334 | manage.clear_ud(user) 335 | elif action == 1: 336 | if 'user' not in user and 'port' in user: 337 | user['user'] = str(user['port']) 338 | if 'user' in user and 'port' in user: 339 | manage.add(user) 340 | else: 341 | print("You have to set the port with -p") 342 | elif action == 2: 343 | if 'user' in user or 'port' in user: 344 | manage.delete(user) 345 | else: 346 | print("You have to set the user name or port with -u/-p") 347 | elif action == 3: 348 | if 'user' in user or 'port' in user: 349 | manage.edit(user) 350 | else: 351 | print("You have to set the user name or port with -u/-p") 352 | elif action == 4: 353 | manage.list_user(user) 354 | elif action is None: 355 | print_server_help() 356 | 357 | if __name__ == '__main__': 358 | main() 359 | --------------------------------------------------------------------------------