Compare commits

...

136 commits

Author SHA1 Message Date
Shell.Xu
e332ec93e9 use list instead of string, prevent injection attack. (#1009)
* fix issue:
https://github.com/shadowsocks/shadowsocks/issues/995
Command Execution

use list instead of string, prevent injection attack.
2018-02-19 10:42:32 +08:00
netroby
c668f44c68 Update README.md (#1042) 2018-02-19 01:22:57 +08:00
mengskysama
8b302646ed
update run_method() (#1161) 2018-02-19 01:08:52 +08:00
mengskysama
2ab8c6bf5d Update jenkins.sh
rm CI coverage
2017-09-06 16:11:14 +08:00
Andrew Lyu
f7d7aee3a6 Fix typo (#866) 2017-09-06 15:21:13 +08:00
mengskysama
0168be7c92 Update .travis.yml
new path for nginx static file
2017-09-06 15:19:45 +08:00
Anthony Wong
0b0379b830 Add new file snapcraft.yaml for building snap package (#932) 2017-09-06 15:13:39 +08:00
Anthony Wong
91e6237794 Update README.md (#734)
Add installation and usage instructions for snap package.
2017-09-06 15:13:26 +08:00
Minwei Shen
3c82f02343 Improve lru_cache. Update test for it (#897) 2017-09-06 15:12:37 +08:00
Esdeath
893f9099b3 Fix memory leak (#921)
* Fix memory leak

* Fix memory leak(2)

* refine
2017-09-06 15:07:19 +08:00
Ted George
d5026cf5ef Update README.md (#841)
thanks
2017-05-12 16:07:36 +08:00
Zou Yong
13b5d6e5ec Fix udp dns issue under python3 (#849)
str is not bytes under python3, if dns config is a list, will cause asyncdns.py failed:

asyncdns.py:
..........
    def handle_event(self, sock, fd, event):
        if sock != self._sock:
            return
        if event & eventloop.POLL_ERR:
       ...........
        else:
            data, addr = sock.recvfrom(1024)
            if addr[0] not in self._servers:
                logging.warn('received a packet other than our dns')
                return
            self._handle_data(data)
2017-05-12 16:06:38 +08:00
Zou Yong
06b028b5c0 Fix memory leak when excpetions occur (#814) 2017-04-29 14:29:53 +08:00
Zou Yong
0f4e3fa00c Add mbedtls wrapper, custom crypto lib path, test files (#803)
* add mbedtls crypto wrapper.
add tests files for new aead ciphers
add custom lib path support
fix some typo

* fix forbidden ip list

* rm crypto lib build files

* remove crypto source

* add xchacha20 test config

* convert dos new line format to unix format

* Fix help msg
2017-04-29 14:27:55 +08:00
Falseen
1222fb19a6 update find_lib's name for the new openssl-1.1 dll (just for windows) (#796)
the new openssl's dll(1.1) is rename to libcrypto-1_1-x64.dll and libcrypto-1_1.dll
2017-03-21 20:15:38 +08:00
Falseen
9e25cc6bb4 fix some error for parse dns_servre in config (#798)
and fix pep8

https://github.com/shadowsocks/shadowsocks/pull/739
2017-03-19 12:37:57 +08:00
Zou Yong
4e21f83bd6 Handle cipher exceptions #783 (#791) 2017-03-15 00:35:17 +08:00
Zou Yong
74f8f8cb85 Add sodium:aes-256-gcm and openssl:ocb AEAD mode (#784)
* Add sodium:aes-256-gcm and openssl:ocb AEAD mode
Use sodium_increment for nonce_increment if avaiable

* Fix pep8

* Fix python3 test code
2017-03-10 17:44:42 +08:00
Zou Yong
d6b40efa5d encrypt.py is renamed to cryptor.py (#780) 2017-03-05 01:09:40 +08:00
Zou Yong
445a3c9c7e Add AEAD ciphers support (#775)
* Add AEAD ciphers support, add manger api

* fix test_encrypt_all

* Add manager api requirements

* #775 fix UDP decrypt_all issue

* fix udp replay: decrypt_all return a list
remove manager api

* fix indent according pep8

* remove abc requirement

* remove unused import

* fix pep8 format

* fix test_aes_256_gcm()
2017-03-04 14:37:29 +08:00
Elvis Wang
c4731de532 Update README.md (#575)
The shadowsocks in pypi is out of date, may install from github@master
2017-03-02 21:28:16 +08:00
Falseen
e06819c124 add simple ss-tunnel to shadowsocks for dns forward (#759)
* add vscode to .gitignore

* add config.json to gitignore

* add simple ss-tunnel to shadowsocks for dns forward


1.add tunnel.py file to shadowoscks for dns forward (seem ss-tunnel of ss-libev)

2.add add_header to common.py for add socks5 request header

3.add dns_service dns_server dns_server_port dns_server_port dns_local_port to shell.py and config.json

4.update to udprelay for ss-tunnel (dns forward)

5.update to local.py for ss-tunnel

* add config.json.example to shadowsocks

* add tunnel_udp_server.close to local.py and tunnel.py

* fix error about pep8 and  pyflakes for travis

* update rename

rename

add "tunnel_" to dns_service dns_server dns_server_port dns_local_port

* fix tunnel for run tunnel alone

* fix pep8 for travis

* update the config name for tunnel

ord name:
tunnel_service tunnel_dns_server tunnel_dns_server_port tunnel_dns_local_port

new name:
both_tunnel_local tunnel_remote tunnel_remote_port tunnel_port

* update for tunnel

update to local.py shell.py tunnel.py for tunnel

* update pep8 for travis

* update config to _config for tunnel

and fix pep8

* changed "add socks5 header to data" to "add ss header to data"

and changed "remove socks5 header" to "remove ss header"

* add tcp forward to tunnel and fix a bug for pack_addr from common.py

1. update tunnel to tcprelay.py

2. add tunnel_tcp_server to tunnel.py

3. add tunnel_tcp_server to local.py

4. add `address = to_bytes(address)` to `pack_addr` from common.py (fix a error when address is a domain)

* fix pep8 for travis again

* remove ss_header from tcprelay.py and update the "header_length" to udprelay.py

1. Remove unnecessary "add ss_header" from tcprelay.py

2. update "data[7:]" to "data[header_length:]"

* remove "both_tunnel_local" and fix some error for tunnel

* update

* update add_header

* rename is_tunnel to _is_tunnel

https://github.com/shadowsocks/shadowsocks/pull/759
2017-03-02 21:20:04 +08:00
ImPerat0R_
fe13c20dc1 Correct spelling. (#755)
thx
2017-02-28 22:19:36 +08:00
hdl
8ac72b0b25 parse dns_servre in config (#739)
thx
2017-02-28 22:15:47 +08:00
mengskysama
ad39d957d7 2.9.1 2017-01-01 23:08:59 -08:00
Lution
392e54e508 update regex to support hostnames containing underscore (#694) 2017-01-02 14:56:57 +08:00
Anthony Wong
bdefeb523f Update README.md (#697)
Add missing links.
2017-01-02 14:56:25 +08:00
Shell.Xu
d31003e9bf new debian package (#659)
* * new upstream version
* change repository.
* change license.
* standards version change to 3.9.8
* fix #810688: man page references texinfo documents which don't exist

* * merge back thomas's change

* * Fix compatible issue (Closes: #845016)
2016-11-20 15:29:11 +08:00
Yuan Alvin Fu
229ec75e4d Update README.md (#643) 2016-11-20 15:22:17 +08:00
loggerhead
72f1d68a05 Fixed #675 (#676)
* fix a OTA bug

* correct a wrong comment

* ignore emacs autosave files

* keep consistence with the defensive style

* a little refractor

* fix daemon stop failed (#675)

* fix test failed
2016-11-20 14:59:32 +08:00
zz
6dae6e2c51 fix that autoban can not get ip when use ipv6 (#674) 2016-11-20 14:57:52 +08:00
Shell Chen
5a05312189 Fix method on manager's add command (#578) (#614) 2016-10-11 21:31:41 +08:00
loggerhead
4f28f7c8e4 Fix a OTA bug on _ota_chunk_data (#642)
* fix a OTA bug

* correct a wrong comment
2016-10-10 23:05:20 +08:00
ahxxm
5cd9f04948 Refactor (#615)
* make tcprelay.py less nested

* import traceback module at the top

* make loops in DNSResolver less nested

* make manager.py less nested

* introduce exception_handle decorator

make try/except block more clean

* apply exception_handle decorator to tcprelay

* quote condition judgement

* pep8 fix
2016-10-07 12:30:17 +08:00
mengskysama
5c11527e1b fix ota call write_to_sock multi 2016-09-04 01:18:13 -07:00
v3aqb
56bf81f58a fix ota (#609)(#610) (#610) 2016-09-04 00:05:15 -07:00
ficapy
f35590b2e2 Dockfile add libsodium (#601) 2016-08-23 22:00:32 +08:00
smounives
6ef14e56db Add chacha20-ietf crypto (#590)
* Add chacha20-ietf crypto

* fix chacha20-ietf

* PEP8

* Update sodium.py (#1)

pep8
2016-08-17 17:06:54 +08:00
mengskysama
8e8ee5d490 Update tcprelay.py
one time auth fail should be return
2016-07-18 23:31:51 +08:00
mengskysama
f7afcd4df0 add ota help in local 2016-07-18 23:00:00 +08:00
Max Lv
34fa8ded72 Merge pull request #543 from v3aqb/fix_ota
fix ota
2016-06-02 00:01:49 -05:00
v3aqb
677e6c4c56 fix ota 2016-05-15 01:27:21 +08:00
mengskysama
9844ba9dc7 pep8 2016-04-16 12:19:32 +08:00
mengskysama
f62a550e9f pep8 2016-04-16 12:13:59 +08:00
watermeter
aae990a2fc Add auth method check for TCP socks connections (#528)
* Add auth method check for TCP socks connections

* remove meaningless return at end of function

* remove extra blank lines
2016-04-16 11:54:12 +08:00
mengskysama
bb53b0cb90 fix build dante cache and old src corrupt and slow 2016-04-16 11:46:06 +08:00
mengskysama
1f24b31f89 Update install.sh
replace dante corrupt src
2016-04-16 11:07:22 +08:00
mengskysama
5b58069f14 Merge pull request #507 from htc550605125/master
Add support for option 'prefer-ipv6'
2016-03-14 18:06:50 +08:00
htc550605125
9d6b9fcde0 add dockerfile 2016-02-23 20:47:04 -05:00
htc
b276d52735 Add support for option 'prefer-ipv6' 2016-02-20 11:33:50 +08:00
mengskysama
b9766ce5df Update README.md 2016-01-12 14:38:10 +08:00
mengskysama
e1e17c8e23 Merge branch 'fixbug-ci' 2016-01-12 14:30:08 +08:00
mengskysama
efe39a75f9 README CI 2016-01-12 14:22:10 +08:00
mengskysama
a8d116d70a fix bug with ord in Py3 2016-01-12 14:20:46 +08:00
mengskysama
811a0e6eb8 PEP8 and undefined name 'gen_key_iv' 2016-01-12 12:17:46 +08:00
mengskysama
f163a7b094 add CI 2016-01-12 12:17:11 +08:00
mengskysama
269e3dd82d test pass ota in udp 2016-01-12 12:10:40 +08:00
mengskysama
248582c932 remove IPv6 testcase and add ota test 2016-01-12 10:34:48 +08:00
mengskysama
bcf9329e23 Merge branch 'master' into fixbug-ci 2016-01-12 02:08:33 +08:00
mengskysama
288c11c9d9 CI Try again! 2016-01-12 01:55:42 +08:00
mengskysama
efad757837 9001 not idle? 2016-01-12 01:44:18 +08:00
mengskysama
ae082f2f2a Merge remote-tracking branch 'origin/fixbug-ci' 2016-01-12 01:04:12 +08:00
mengskysama
d2f1caeb5d fixbug: when ota disable data not write 2016-01-12 01:03:25 +08:00
mengskysama
9f3641db54 url ssl timeout 2016-01-12 00:27:46 +08:00
jsy
3788df59bb Can't append to data files in parallel mode. 2016-01-12 00:04:40 +08:00
jsy
4786b9c3ad Can't append to data files in parallel mode. 2016-01-12 00:03:13 +08:00
jsy
ab42062587 Can't append to data files in parallel mode. 2016-01-11 23:57:18 +08:00
jsy
285dc40729 here? 2016-01-11 23:47:47 +08:00
jsy
614eed2bf0 PEP8 2016-01-11 23:27:21 +08:00
jsy
7c8ecb811c PEP8 2016-01-11 23:20:12 +08:00
jsy
a4a87eb127 PEP8 2016-01-11 23:18:16 +08:00
jsy
ee391c7773 udp ota bug 2016-01-11 22:58:53 +08:00
jsy
2d40a361db ota config 2016-01-11 18:42:23 +08:00
jsy
7efc3e2640 README.md 2016-01-11 18:32:39 +08:00
jsy
569ca0d0a4 reverse config 2016-01-11 18:30:45 +08:00
jsy
28c4d14b12 Merge branch 'feature-ota' 2016-01-11 15:06:59 +08:00
mengskysama
743ddf42d5 Merge pull request #481 from RockerFlower/master
Shadowsocks
2016-01-11 10:30:12 +08:00
jsy
ca99abff89 fix udp ota 2016-01-11 10:26:51 +08:00
jsy
77b2a22055 ota completed! 2016-01-11 10:11:31 +08:00
jsy
767b9217f8 one time auth server side 2015-12-06 02:02:25 +08:00
mengskysama
173323c0f8 Update common.py
https://github.com/mengskysama/shadowsocks-rm/issues/4
2015-11-13 14:29:27 +08:00
mengskysama
34851fbcb3 readme 2015-10-26 10:48:52 +08:00
mengskysama
0aba0aa40c fix memory leak!
_dns_cache bommmm
2015-10-26 09:45:02 +08:00
mengskysama
4d24b7477a fix bug!
if _last_visits[index] len > 1
2015-10-26 09:43:48 +08:00
mengskysama
6683dfd3bb fix manger bug 2015-10-09 14:49:09 +08:00
clowwindy
5b450acfaa Update README.md 2015-08-20 18:52:42 +08:00
clowwindy
61a0b2d1ac Update README.md 2015-08-17 20:36:34 +08:00
clowwindy
e52aa9db0c Merge pull request #335 from slayercat/master
allow user to override system dnsserver with config.
2015-08-17 11:05:58 +08:00
李林哲
ffed9b2cae dns_server config test 2015-08-17 10:14:23 +08:00
李林哲
0c4f79284e allow user to override system dnsserver with config.
config item:
    dns_server     list

    eg:
        dns_server:[8.8.8.8,8.8.4.4]
2015-08-17 09:55:05 +08:00
clowwindy
eb033a8f6a Update README.md 2015-08-16 19:53:31 +08:00
clowwindy
f19d0ea6fd fix #425 2015-08-15 22:36:29 +08:00
clowwindy
a2bc6e1945 fix #414 2015-08-11 17:55:16 +08:00
clowwindy
7c08101ce8 update CHANGES 2015-08-10 12:37:42 +08:00
clowwindy
a434eef096 fix json decode issue 2015-08-10 11:53:54 +08:00
clowwindy
3c1154923f fix json unicode issue in manager 2015-08-10 00:06:19 +08:00
clowwindy
c5dd081216 Update README.md 2015-08-09 01:33:27 +08:00
clowwindy
f04e268885 bump 2.8.1 2015-08-06 19:47:33 +08:00
clowwindy
c8b3f71e1b respond ok to add and remove commands 2015-08-06 19:45:37 +08:00
clowwindy
77f9979b2e Update CONTRIBUTING.md 2015-08-06 17:24:51 +08:00
clowwindy
3576b3006c support IPv6 on control socket 2015-08-06 15:13:29 +08:00
clowwindy
583b54f426 update CHANGES 2015-08-06 14:58:40 +08:00
clowwindy
5ed73c15f0 bump 2.8 2015-08-06 01:04:03 +08:00
clowwindy
249bcc0b29 refine unit test 2015-08-06 00:52:25 +08:00
clowwindy
30efc30360 fix pyflakes 2015-08-06 00:34:20 +08:00
clowwindy
a8ae8ab373 add unit test for manager 2015-08-05 23:59:59 +08:00
clowwindy
4f948b2286 fix password type in udprelay 2015-08-05 23:59:14 +08:00
clowwindy
d20a07192c allow manager to bind on unix socket 2015-08-05 23:11:56 +08:00
clowwindy
9c3af61433 add statistics 2015-08-05 22:43:21 +08:00
clowwindy
e08845d6f3 fix manager 2015-08-05 18:31:55 +08:00
clowwindy
956199efcd add control manager 2015-08-05 18:12:38 +08:00
clowwindy
58df1d82d0 close poll object after loop stopped 2015-08-03 23:54:30 +08:00
clowwindy
baad209160 bump 2015-08-03 01:41:15 +08:00
clowwindy
4211184886 remove unnecessary overwrite 2015-08-03 01:40:18 +08:00
clowwindy
999a54168e update CHANGES 2015-08-02 17:45:48 +08:00
clowwindy
d946ac8213 skip graceful restart test on Jenkins 2015-08-02 17:33:13 +08:00
clowwindy
177c639b19 fix graceful restart test 2015-08-02 15:53:00 +08:00
clowwindy
b28de8e2f1 fix pyflakes 2015-08-02 15:10:46 +08:00
clowwindy
02120e3402 optimize eventloop 2015-08-02 14:45:15 +08:00
clowwindy
111acf66c1 fix graceful restart and add unit test 2015-08-02 14:37:44 +08:00
clowwindy
e8b2946999 fix workers 2015-08-01 19:09:29 +08:00
clowwindy
d319fab5ca fix asyncdns unit test 2015-08-01 18:58:57 +08:00
clowwindy
80102f3899 fix pyflakes 2015-08-01 18:57:44 +08:00
clowwindy
4a8d0774b4 optimize performance for multiple ports
UDPRelay is broken now
2015-08-01 18:53:18 +08:00
clowwindy
d95e5ce680 Merge pull request #388 from cicku/patch-1
PEP8 indent
2015-07-27 15:03:50 +08:00
Christopher Meng
2555aa8e2b PEP8 indent
A tiny change to perfect the indent.
2015-07-27 00:38:23 -04:00
clowwindy
1bb0e51e8e refine tests 2015-07-11 13:05:37 +08:00
clowwindy
f7d69db6d1 bump 2015-07-10 18:01:34 +08:00
clowwindy
f55bd0302f update CHANGES 2015-07-10 17:59:52 +08:00
clowwindy
13a6bb007c cache DNS results in UDPRelay 2015-07-10 17:39:24 +08:00
clowwindy
99b4121fd9 fix problem when UDP client requesting both IPv4 and IPv6 2015-07-10 17:32:31 +08:00
clowwindy
c34c99450f fix UDP source port issue 2015-07-10 16:58:04 +08:00
clowwindy
1a62694a3b add udp source port test 2015-07-10 15:56:27 +08:00
clowwindy
a2edd6a46d Merge pull request #361 from jackyyf/master
Add fail2ban filter.
2015-06-21 11:24:38 +08:00
Yifu Yu
0edae7069f Add fail2ban filter.
Please put the shadowsocks.conf into your filter.d directory, and
using `filter = shadowsocks` to use the filter.
2015-06-21 00:56:55 +08:00
clowwindy
e001f1818c bump 2015-06-08 08:36:17 +08:00
88 changed files with 5849 additions and 839 deletions

14
.gitignore vendored
View file

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

View file

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

15
CHANGES
View file

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

View file

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

17
Dockerfile Normal file
View file

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

View file

@ -3,10 +3,16 @@ shadowsocks
[![PyPI version]][PyPI]
[![Build Status]][Travis CI]
[![Coverage Status]][Coverage]
A fast tunnel proxy that helps you bypass firewalls.
Features:
- TCP & UDP support
- User management API
- TCP Fast Open
- Workers and graceful restart
- Destination IP blacklist
Server
------
@ -15,16 +21,25 @@ Server
Debian / Ubuntu:
apt-get install python-pip
pip install shadowsocks
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
CentOS:
yum install python-setuptools && easy_install pip
pip install shadowsocks
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
For CentOS 7, if you need AEAD ciphers, you need install libsodium
```
dnf install libsodium python34-pip
pip3 install git+https://github.com/shadowsocks/shadowsocks.git@master
```
Linux distributions with [snap](http://snapcraft.io/):
snap install shadowsocks
Windows:
See [Install Server on Windows]
See [Install Shadowsocks Server on Windows](https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows).
### Usage
@ -45,62 +60,38 @@ To check the log:
Check all the options via `-h`. You can also use a [Configuration] file
instead.
Client
------
If you installed the [snap](http://snapcraft.io/) package, you have to prefix the commands with `shadowsocks.`,
like this:
* [Windows] / [OS X]
* [Android] / [iOS]
* [OpenWRT]
shadowsocks.ssserver -p 443 -k password -m aes-256-cfb
### Usage with Config File
[Create configuration file and run](https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File)
To start:
ssserver -c /etc/shadowsocks.json
Use GUI clients on your local PC/phones. Check the README of your client
for more information.
Documentation
-------------
You can find all the documentation in the [Wiki].
You can find all the documentation in the [Wiki](https://github.com/shadowsocks/shadowsocks/wiki).
License
-------
Copyright 2015 clowwindy
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Bugs and Issues
----------------
* [Troubleshooting]
* [Issue Tracker]
* [Mailing list]
Apache License
[Android]: https://github.com/shadowsocks/shadowsocks-android
[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat
[Configuration]: https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File
[Coverage Status]: https://jenkins.shadowvpn.org/result/shadowsocks
[Coverage]: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html
[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks
[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help
[Issue Tracker]: https://github.com/shadowsocks/shadowsocks/issues?state=open
[Install Server on Windows]: https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows
[Mailing list]: https://groups.google.com/group/shadowsocks
[OpenWRT]: https://github.com/shadowsocks/openwrt-shadowsocks
[OS X]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help
[PyPI]: https://pypi.python.org/pypi/shadowsocks
[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat
[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks
[Troubleshooting]: https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting
[Wiki]: https://github.com/shadowsocks/shadowsocks/wiki
[Windows]: https://github.com/shadowsocks/shadowsocks-csharp

View file

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

17
config.json.example Normal file
View file

@ -0,0 +1,17 @@
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1080,
"password":"password",
"timeout":600,
"method":"aes-256-cfb",
"local_address":"127.0.0.1",
"fast_open":false,
"tunnel_remote":"8.8.8.8",
"dns_server":["8.8.8.8", "8.8.4.4"],
"tunnel_remote_port":53,
"tunnel_port":53,
"libopenssl":"C:\\Program Files\\Git\\mingw64\\bin\\libeay32.dll",
"libsodium":"/usr/local/lib/libsodium.so",
"libmbedtls":"/usr/local/lib/libmbedcrypto.2.4.0.dylib"
}

23
debian/changelog vendored
View file

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

3
debian/config.json vendored
View file

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

21
debian/control vendored
View file

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

43
debian/copyright vendored
View file

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

2
debian/install vendored
View file

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

View file

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

4
debian/sslocal.1 vendored
View file

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

4
debian/ssserver.1 vendored
View file

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

View file

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

View file

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

View file

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

340
shadowsocks/crypto/aead.py Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

293
shadowsocks/manager.py Normal file
View file

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

View file

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

View file

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

View file

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

74
shadowsocks/tunnel.py Executable file
View file

@ -0,0 +1,74 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2012-2015 clowwindy
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from __future__ import absolute_import, division, print_function, \
with_statement
import sys
import os
import logging
import signal
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
@shell.exception_handle(self_=False, exit_code=1)
def main():
shell.check_python()
# fix py2exe
if hasattr(sys, "frozen") and sys.frozen in \
("windows_exe", "console_exe"):
p = os.path.dirname(os.path.abspath(sys.executable))
os.chdir(p)
config = shell.get_config(True)
daemon.daemon_exec(config)
dns_resolver = asyncdns.DNSResolver()
loop = eventloop.EventLoop()
dns_resolver.add_to_loop(loop)
_config = config.copy()
_config["local_port"] = _config["tunnel_port"]
logging.info("starting tcp tunnel at %s:%d forward to %s:%d" %
(_config['local_address'], _config['local_port'],
_config['tunnel_remote'], _config['tunnel_remote_port']))
tunnel_tcp_server = tcprelay.TCPRelay(_config, dns_resolver, True)
tunnel_tcp_server._is_tunnel = True
tunnel_tcp_server.add_to_loop(loop)
logging.info("starting udp tunnel at %s:%d forward to %s:%d" %
(_config['local_address'], _config['local_port'],
_config['tunnel_remote'], _config['tunnel_remote_port']))
tunnel_udp_server = udprelay.UDPRelay(_config, dns_resolver, True)
tunnel_udp_server._is_tunnel = True
tunnel_udp_server.add_to_loop(loop)
def handler(signum, _):
logging.warn('received SIGQUIT, doing graceful shutting down..')
tunnel_tcp_server.close(next_tick=True)
tunnel_udp_server.close(next_tick=True)
signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler)
def int_handler(signum, _):
sys.exit(1)
signal.signal(signal.SIGINT, int_handler)
daemon.set_user(config.get('user', None))
loop.run()
if __name__ == '__main__':
main()

View file

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

23
snapcraft.yaml Normal file
View file

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

View file

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

View file

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

View file

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

10
tests/aes-gcm.json Normal file
View file

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

11
tests/aes-ocb.json Normal file
View file

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

10
tests/aes-ofb.json Normal file
View file

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

View file

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

10
tests/camellia.json Normal file
View file

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

View file

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

10
tests/chacha20-ietf.json Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

10
tests/graceful.json Normal file
View file

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

17
tests/graceful_cli.py Normal file
View file

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

13
tests/graceful_server.py Normal file
View file

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

View file

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

View file

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

View file

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

12
tests/libmbedtls/install.sh Executable file
View file

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

19
tests/libopenssl/install.sh Executable file
View file

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

View file

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

View file

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

View file

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

10
tests/mbedtls-aes.json Normal file
View file

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

View file

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

11
tests/rc4-md5-ota.json Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,12 @@
#!/bin/bash
if [ ! -d dante-1.4.0 ]; then
wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1
if [ ! -d dante-1.4.0 ] || [ ! -d dante-1.4.0/configure ]; then
rm dante-1.4.0 -rf
#wget http://www.inet.no/dante/files/dante-1.4.0.tar.gz || exit 1
wget https://codeload.github.com/notpeter/dante/tar.gz/dante-1.4.0 -O dante-1.4.0.tar.gz || exit 1
tar xf dante-1.4.0.tar.gz || exit 1
#
mv dante-dante-1.4.0 dante-1.4.0
fi
pushd dante-1.4.0
./configure && make -j4 && make install || exit 1

View file

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

View file

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

View file

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

View file

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

64
tests/test_graceful_restart.sh Executable file
View file

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

View file

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

85
tests/test_udp_src.py Normal file
View file

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

23
tests/test_udp_src.sh Executable file
View file

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

View file

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

View file

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

10
tests/xchacha20.json Normal file
View file

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

View file

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

View file

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