Compare commits
291 commits
Author | SHA1 | Date | |
---|---|---|---|
|
e332ec93e9 | ||
|
c668f44c68 | ||
|
8b302646ed | ||
|
2ab8c6bf5d | ||
|
f7d7aee3a6 | ||
|
0168be7c92 | ||
|
0b0379b830 | ||
|
91e6237794 | ||
|
3c82f02343 | ||
|
893f9099b3 | ||
|
d5026cf5ef | ||
|
13b5d6e5ec | ||
|
06b028b5c0 | ||
|
0f4e3fa00c | ||
|
1222fb19a6 | ||
|
9e25cc6bb4 | ||
|
4e21f83bd6 | ||
|
74f8f8cb85 | ||
|
d6b40efa5d | ||
|
445a3c9c7e | ||
|
c4731de532 | ||
|
e06819c124 | ||
|
fe13c20dc1 | ||
|
8ac72b0b25 | ||
|
ad39d957d7 | ||
|
392e54e508 | ||
|
bdefeb523f | ||
|
d31003e9bf | ||
|
229ec75e4d | ||
|
72f1d68a05 | ||
|
6dae6e2c51 | ||
|
5a05312189 | ||
|
4f28f7c8e4 | ||
|
5cd9f04948 | ||
|
5c11527e1b | ||
|
56bf81f58a | ||
|
f35590b2e2 | ||
|
6ef14e56db | ||
|
8e8ee5d490 | ||
|
f7afcd4df0 | ||
|
34fa8ded72 | ||
|
677e6c4c56 | ||
|
9844ba9dc7 | ||
|
f62a550e9f | ||
|
aae990a2fc | ||
|
bb53b0cb90 | ||
|
1f24b31f89 | ||
|
5b58069f14 | ||
|
9d6b9fcde0 | ||
|
b276d52735 | ||
|
b9766ce5df | ||
|
e1e17c8e23 | ||
|
efe39a75f9 | ||
|
a8d116d70a | ||
|
811a0e6eb8 | ||
|
f163a7b094 | ||
|
269e3dd82d | ||
|
248582c932 | ||
|
bcf9329e23 | ||
|
288c11c9d9 | ||
|
efad757837 | ||
|
ae082f2f2a | ||
|
d2f1caeb5d | ||
|
9f3641db54 | ||
|
3788df59bb | ||
|
4786b9c3ad | ||
|
ab42062587 | ||
|
285dc40729 | ||
|
614eed2bf0 | ||
|
7c8ecb811c | ||
|
a4a87eb127 | ||
|
ee391c7773 | ||
|
2d40a361db | ||
|
7efc3e2640 | ||
|
569ca0d0a4 | ||
|
28c4d14b12 | ||
|
743ddf42d5 | ||
|
ca99abff89 | ||
|
77b2a22055 | ||
|
767b9217f8 | ||
|
173323c0f8 | ||
|
34851fbcb3 | ||
|
0aba0aa40c | ||
|
4d24b7477a | ||
|
6683dfd3bb | ||
|
5b450acfaa | ||
|
61a0b2d1ac | ||
|
e52aa9db0c | ||
|
ffed9b2cae | ||
|
0c4f79284e | ||
|
eb033a8f6a | ||
|
f19d0ea6fd | ||
|
a2bc6e1945 | ||
|
7c08101ce8 | ||
|
a434eef096 | ||
|
3c1154923f | ||
|
c5dd081216 | ||
|
f04e268885 | ||
|
c8b3f71e1b | ||
|
77f9979b2e | ||
|
3576b3006c | ||
|
583b54f426 | ||
|
5ed73c15f0 | ||
|
249bcc0b29 | ||
|
30efc30360 | ||
|
a8ae8ab373 | ||
|
4f948b2286 | ||
|
d20a07192c | ||
|
9c3af61433 | ||
|
e08845d6f3 | ||
|
956199efcd | ||
|
58df1d82d0 | ||
|
baad209160 | ||
|
4211184886 | ||
|
999a54168e | ||
|
d946ac8213 | ||
|
177c639b19 | ||
|
b28de8e2f1 | ||
|
02120e3402 | ||
|
111acf66c1 | ||
|
e8b2946999 | ||
|
d319fab5ca | ||
|
80102f3899 | ||
|
4a8d0774b4 | ||
|
d95e5ce680 | ||
|
2555aa8e2b | ||
|
1bb0e51e8e | ||
|
f7d69db6d1 | ||
|
f55bd0302f | ||
|
13a6bb007c | ||
|
99b4121fd9 | ||
|
c34c99450f | ||
|
1a62694a3b | ||
|
a2edd6a46d | ||
|
0edae7069f | ||
|
e001f1818c | ||
|
56c289ba21 | ||
|
e74ae193d0 | ||
|
16db66675b | ||
|
c46234af41 | ||
|
405120c59f | ||
|
082c8a80f4 | ||
|
893d21da76 | ||
|
ea7a3e1b58 | ||
|
e898f92191 | ||
|
5c0391d146 | ||
|
e17279e5bf | ||
|
f17da943b3 | ||
|
d3831bef8c | ||
|
b6e6e14b8a | ||
|
4172639d48 | ||
|
e8488895f0 | ||
|
edb7822a7b | ||
|
294556f8bc | ||
|
1b7ab23f78 | ||
|
0e14f3bbef | ||
|
42ce2569c4 | ||
|
581d6e687f | ||
|
d774286dc0 | ||
|
cb7062e1c1 | ||
|
48ddc1714b | ||
|
dfd81af844 | ||
|
783a6ef7f2 | ||
|
e71ce6c758 | ||
|
e564f17505 | ||
|
6d09cd21ca | ||
|
318d88ec89 | ||
|
ce805f0aea | ||
|
96a86c028d | ||
|
2c59ffb952 | ||
|
27a0c7754d | ||
|
0cd261dd10 | ||
|
fbf15cb942 | ||
|
1c81465436 | ||
|
73f21ffbf6 | ||
|
7aa37cad8e | ||
|
ae99698b4e | ||
|
da65d0a2ee | ||
|
3b7755bd5e | ||
|
5a7225c54b | ||
|
4aba904d6e | ||
|
b77f419336 | ||
|
c39bbbe237 | ||
|
453a9c61a6 | ||
|
54181ef821 | ||
|
17624d0b99 | ||
|
a0aa9173a8 | ||
|
79b9b53dbe | ||
|
070108f78b | ||
|
b11d848986 | ||
|
aa28796524 | ||
|
8af359ae05 | ||
|
100ebcf064 | ||
|
8783e0e9ae | ||
|
ada97ab6d9 | ||
|
5316b3bf11 | ||
|
4a2d98b280 | ||
|
51f47ccb91 | ||
|
70ebd2ef28 | ||
|
1f8819f790 | ||
|
0e6a4cd8ff | ||
|
f4052fbc84 | ||
|
5e5d25efd9 | ||
|
2e9ce11ea1 | ||
|
6efb3d00e4 | ||
|
13413267dc | ||
|
bd22e3ef75 | ||
|
5179e018e2 | ||
|
af6c6f3f23 | ||
|
5c05a74727 | ||
|
53a7e4d0e4 | ||
|
5e476843ec | ||
|
32a6b8fd7a | ||
|
9fe2f4ef16 | ||
|
f7316c0047 | ||
|
cc36de5a2f | ||
|
4312eb9e58 | ||
|
eb94bd1cc3 | ||
|
f29bfb0cc7 | ||
|
80b8bd7014 | ||
|
a4b0ea5b8f | ||
|
e582b2b929 | ||
|
3d03dbf716 | ||
|
1becc9362d | ||
|
ebfd1486d8 | ||
|
6eadfca78e | ||
|
3294a92a61 | ||
|
2711002142 | ||
|
18da3554ff | ||
|
a5fc1c7e5f | ||
|
8b2deb01d8 | ||
|
176e97bb45 | ||
|
b6efc0efd9 | ||
|
4598e09b78 | ||
|
363965c364 | ||
|
a90c354f42 | ||
|
69d1d876bc | ||
|
ae57ebde68 | ||
|
09f8f1ccc8 | ||
|
b12ba5383c | ||
|
0bc0117cd7 | ||
|
85cbac127d | ||
|
f7a63a0ad7 | ||
|
a3faf2c5fb | ||
|
d01ef02b5a | ||
|
1db1e4f4e9 | ||
|
510dc64bf2 | ||
|
8be120518b | ||
|
e7984930d1 | ||
|
b8f1825776 | ||
|
dde25f89e1 | ||
|
98f73e44d0 | ||
|
e0189ab1de | ||
|
e68b3e430b | ||
|
bac675d7f6 | ||
|
291adf8b85 | ||
|
1423fe1921 | ||
|
5a5c8b9c7e | ||
|
70559a1030 | ||
|
45ee594d58 | ||
|
707b5c1def | ||
|
d91f7d85d4 | ||
|
5a5158b33f | ||
|
7f19c31640 | ||
|
937693519a | ||
|
9e6e884b12 | ||
|
e8501da271 | ||
|
99c8bbafe7 | ||
|
30f4f78557 | ||
|
dd140814a8 | ||
|
d228ffbe84 | ||
|
ab975d1753 | ||
|
c6bc912c11 | ||
|
b785d95f66 | ||
|
1d0c8b1800 | ||
|
9cfffa360e | ||
|
cd07001471 | ||
|
2cc7ee5053 | ||
|
9b3944c954 | ||
|
f1b084be06 | ||
|
5ea8403e56 | ||
|
072afd68f2 | ||
|
be1d1d5032 | ||
|
536b7d1ee6 | ||
|
c7b5a5a011 | ||
|
be2ab378ff | ||
|
2b4c3619d6 | ||
|
dae2624b30 | ||
|
fb789d8e9f | ||
|
2733e6a4ba | ||
|
35c3b0b41c |
103 changed files with 7850 additions and 2393 deletions
17
.gitignore
vendored
17
.gitignore
vendored
|
@ -17,7 +17,8 @@ develop-eggs
|
|||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
htmlcov
|
||||
.coverage*
|
||||
.tox
|
||||
|
||||
#Translations
|
||||
|
@ -28,3 +29,17 @@ pip-log.txt
|
|||
|
||||
.DS_Store
|
||||
.idea
|
||||
tags
|
||||
|
||||
#Emacs
|
||||
.#*
|
||||
venv/
|
||||
|
||||
# VS-code
|
||||
.vscode/
|
||||
|
||||
# Pycharm
|
||||
.idea
|
||||
|
||||
#ss
|
||||
config.json
|
||||
|
|
32
.travis.yml
32
.travis.yml
|
@ -9,29 +9,15 @@ cache:
|
|||
- dante-1.4.0
|
||||
before_install:
|
||||
- sudo apt-get update -qq
|
||||
- sudo apt-get install -qq build-essential libssl-dev swig python-m2crypto python-numpy dnsutils
|
||||
- pip install m2crypto salsa20 pep8 pyflakes nose coverage
|
||||
- sudo apt-get install -qq build-essential dnsutils iproute nginx bc
|
||||
- sudo dd if=/dev/urandom of=/usr/share/nginx/html/file bs=1M count=10
|
||||
- sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts"
|
||||
- sudo service nginx restart
|
||||
- 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:
|
||||
- pep8 .
|
||||
- pyflakes .
|
||||
- coverage run tests/nose_plugin.py -v
|
||||
- python setup.py sdist
|
||||
- tests/test_daemon.sh
|
||||
- python tests/test.py --with-coverage -c tests/aes.json
|
||||
- python tests/test.py --with-coverage -c tests/aes-ctr.json
|
||||
- python tests/test.py --with-coverage -c tests/aes-cfb1.json
|
||||
- python tests/test.py --with-coverage -c tests/aes-cfb8.json
|
||||
- python tests/test.py --with-coverage -c tests/rc4-md5.json
|
||||
- python tests/test.py --with-coverage -c tests/salsa20.json
|
||||
- python tests/test.py --with-coverage -c tests/chacha20.json
|
||||
- python tests/test.py --with-coverage -c tests/salsa20-ctr.json
|
||||
- python tests/test.py --with-coverage -c tests/table.json
|
||||
- python tests/test.py --with-coverage -c tests/server-multi-ports.json
|
||||
- python tests/test.py --with-coverage -s tests/server-multi-passwd.json -c tests/server-multi-passwd-client-side.json
|
||||
- python tests/test.py --with-coverage -c tests/workers.json
|
||||
- python tests/test.py --with-coverage -s tests/ipv6.json -c tests/ipv6-client-side.json
|
||||
- python tests/test.py --with-coverage -b "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388" -a "-m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -l 1081"
|
||||
- python tests/test.py --with-coverage -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081"
|
||||
- coverage combine && coverage report --include=shadowsocks/*
|
||||
- tests/jenkins.sh
|
||||
|
|
53
CHANGES
53
CHANGES
|
@ -1,3 +1,56 @@
|
|||
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
|
||||
|
||||
2.6.9 2015-05-19
|
||||
- Fix a stability issue on Windows
|
||||
|
||||
2.6.8 2015-02-10
|
||||
- Support multiple server ip on client side
|
||||
- Support --version
|
||||
- Minor fixes
|
||||
|
||||
2.6.7 2015-02-02
|
||||
- Support --user
|
||||
- Support CIDR format in --forbidden-ip
|
||||
- Minor fixes
|
||||
|
||||
2.6.6 2015-01-23
|
||||
- Fix a crash in forbidden list
|
||||
|
||||
2.6.5 2015-01-18
|
||||
- Try both 32 bit and 64 bit dll on Windows
|
||||
|
||||
2.6.4 2015-01-14
|
||||
- Also search lib* when searching libraries
|
||||
|
||||
2.6.3 2015-01-12
|
||||
- Support --forbidden-ip to ban some IP, i.e. localhost
|
||||
- Search OpenSSL and libsodium harder
|
||||
- Now works on OpenWRT
|
||||
|
||||
2.6.2 2015-01-03
|
||||
- Log client IP
|
||||
|
||||
2.6.1 2014-12-26
|
||||
- Fix a problem with TCP Fast Open on local side
|
||||
- Fix sometimes daemon_start returns wrong exit status
|
||||
|
||||
2.6 2014-12-21
|
||||
- Add daemon support
|
||||
|
||||
|
|
|
@ -1,38 +1,31 @@
|
|||
How to contribute
|
||||
How to Contribute
|
||||
=================
|
||||
|
||||
在你提交问题前,请先[自行诊断]一下。提交时附上诊断过程中的问题和下列结果,
|
||||
否则如果我们无法重现你的问题,也就不能帮助你。
|
||||
Notice this is the repository for Shadowsocks Python version. If you have problems with Android / iOS / Windows etc clients, please post your questions in their issue trackers.
|
||||
|
||||
Before you submit issues, please read [Troubleshooting] and take a few minutes
|
||||
to read this guide.
|
||||
Pull Requests
|
||||
-------------
|
||||
|
||||
问题反馈
|
||||
-------
|
||||
|
||||
请提交下面的信息:
|
||||
|
||||
1. 你是如何搭建环境的?(操作系统,Shadowsocks 版本)
|
||||
2. 操作步骤是什么?
|
||||
3. 浏览器里的现象是什么?一直转菊花,还是有提示错误?
|
||||
4. 发生错误时,客户端和服务端最后一部分日志。
|
||||
5. 其它你认为可能和问题有关的信息。
|
||||
|
||||
如果你不清楚其中某条的含义, 可以直接跳过那一条。
|
||||
1. Pull requests are welcome. If you would like to add a large feature
|
||||
or make a significant change, make sure to open an issue to discuss with
|
||||
people first.
|
||||
2. Follow PEP8.
|
||||
3. Make sure to pass the unit tests. Write unit tests for new modules if
|
||||
needed.
|
||||
|
||||
Issues
|
||||
------
|
||||
|
||||
Please include the following information in your submission:
|
||||
1. Only bugs and feature requests are accepted here.
|
||||
2. We'll only work on important features. If the feature you're asking only
|
||||
benefits a few people, you'd better implement the feature yourself and send us
|
||||
a pull request, or ask some of your friends to do so.
|
||||
3. We don't answer questions of any other types here. Since very few people
|
||||
are watching the issue tracker here, you'll probably get no help from here.
|
||||
Read [Troubleshooting] and get help from forums or [mailing lists].
|
||||
4. Issues in languages other than English will be Google translated into English
|
||||
later.
|
||||
|
||||
1. How did you set up your environment? (OS, version of Shadowsocks)
|
||||
2. Steps to reproduce the problem.
|
||||
3. What happened in your browser? Just no response, or any error message?
|
||||
4. 10 lines of log on the local side of shadowsocks when the error happened.
|
||||
5. 10 lines of log on the server side of shadowsocks when the error happened.
|
||||
6. Any other useful information.
|
||||
|
||||
Skip any of them if you don't know its meaning.
|
||||
|
||||
[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting
|
||||
[自行诊断]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting
|
||||
[mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks
|
||||
|
|
17
Dockerfile
Normal file
17
Dockerfile
Normal 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
|
215
LICENSE
215
LICENSE
|
@ -1,21 +1,202 @@
|
|||
Shadowsocks
|
||||
|
||||
Copyright (c) 2014 clowwindy
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
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:
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
1. Definitions.
|
||||
|
||||
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.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
recursive-include *.py
|
||||
recursive-include shadowsocks *.py
|
||||
include README.rst
|
||||
include LICENSE
|
||||
|
|
190
README.md
190
README.md
|
@ -1,147 +1,97 @@
|
|||
shadowsocks
|
||||
===========
|
||||
|
||||
[![PyPI version]][PyPI] [![Build Status]][Travis CI]
|
||||
[![PyPI version]][PyPI]
|
||||
[![Build Status]][Travis CI]
|
||||
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
[中文说明][Chinese Readme]
|
||||
Features:
|
||||
- TCP & UDP support
|
||||
- User management API
|
||||
- TCP Fast Open
|
||||
- Workers and graceful restart
|
||||
- Destination IP blacklist
|
||||
|
||||
Install
|
||||
-------
|
||||
Server
|
||||
------
|
||||
|
||||
You'll have a client on your local side, and setup a server on a
|
||||
remote server.
|
||||
### Install
|
||||
|
||||
### Client
|
||||
|
||||
* [Windows] / [OS X]
|
||||
* [Android] / [iOS]
|
||||
* [OpenWRT]
|
||||
|
||||
### Server
|
||||
|
||||
#### Debian / Ubuntu:
|
||||
Debian / Ubuntu:
|
||||
|
||||
apt-get install python-pip
|
||||
pip install shadowsocks
|
||||
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
|
||||
|
||||
Or simply `apt-get install shadowsocks` if you have [Debian sid] in your
|
||||
source list.
|
||||
CentOS:
|
||||
|
||||
#### CentOS:
|
||||
yum install python-setuptools && easy_install pip
|
||||
pip install git+https://github.com/shadowsocks/shadowsocks.git@master
|
||||
|
||||
yum install python-setuptools
|
||||
easy_install pip
|
||||
pip install shadowsocks
|
||||
For CentOS 7, if you need AEAD ciphers, you need install libsodium
|
||||
```
|
||||
dnf install libsodium python34-pip
|
||||
pip3 install git+https://github.com/shadowsocks/shadowsocks.git@master
|
||||
```
|
||||
Linux distributions with [snap](http://snapcraft.io/):
|
||||
|
||||
#### Windows:
|
||||
snap install shadowsocks
|
||||
|
||||
Download [OpenSSL for Windows] and install. Then install shadowsocks via
|
||||
easy_install and pip as Linux. If you don't know how to use them, you can
|
||||
directly download [the package], and use `python shadowsocks/server.py`
|
||||
instead of `ssserver` command below.
|
||||
Windows:
|
||||
|
||||
Configuration
|
||||
See [Install Shadowsocks Server on Windows](https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows).
|
||||
|
||||
### Usage
|
||||
|
||||
ssserver -p 443 -k password -m aes-256-cfb
|
||||
|
||||
To run in the background:
|
||||
|
||||
sudo ssserver -p 443 -k password -m aes-256-cfb --user nobody -d start
|
||||
|
||||
To stop:
|
||||
|
||||
sudo ssserver -d stop
|
||||
|
||||
To check the log:
|
||||
|
||||
sudo less /var/log/shadowsocks.log
|
||||
|
||||
Check all the options via `-h`. You can also use a [Configuration] file
|
||||
instead.
|
||||
|
||||
If you installed the [snap](http://snapcraft.io/) package, you have to prefix the commands with `shadowsocks.`,
|
||||
like this:
|
||||
|
||||
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
|
||||
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
On your server create a config file `/etc/shadowsocks.json`.
|
||||
Example:
|
||||
|
||||
{
|
||||
"server":"my_server_ip",
|
||||
"server_port":8388,
|
||||
"local_address": "127.0.0.1",
|
||||
"local_port":1080,
|
||||
"password":"mypassword",
|
||||
"timeout":300,
|
||||
"method":"aes-256-cfb",
|
||||
"fast_open": false,
|
||||
"workers": 1
|
||||
}
|
||||
|
||||
Explanation of the fields:
|
||||
|
||||
| Name | Explanation |
|
||||
| ------------- | ----------------------------------------------- |
|
||||
| server | the address your server listens |
|
||||
| server_port | server port |
|
||||
| local_address | the address your local listens |
|
||||
| local_port | local port |
|
||||
| password | password used for encryption |
|
||||
| timeout | in seconds |
|
||||
| method | default: "aes-256-cfb", see [Encryption] |
|
||||
| fast_open | use [TCP_FASTOPEN], true / false |
|
||||
| workers | number of workers, available on Unix/Linux |
|
||||
|
||||
Run `ssserver -c /etc/shadowsocks.json` on your server. To run it in the
|
||||
background, use [Supervisor].
|
||||
|
||||
On your client machine, use the same configuration as your server, and
|
||||
start your client.
|
||||
|
||||
If you use Chrome, it's recommended to use [SwitchySharp]. Change the proxy
|
||||
settings to
|
||||
|
||||
protocol: socks5
|
||||
hostname: 127.0.0.1
|
||||
port: your local_port
|
||||
|
||||
If you can't install [SwitchySharp], you can launch Chrome with the following
|
||||
arguments to force Chrome to use the proxy:
|
||||
|
||||
Chrome.exe --proxy-server="socks5://127.0.0.1:1080" --host-resolver-rules="MAP * 0.0.0.0 , EXCLUDE localhost"
|
||||
|
||||
If you can't even download Chrome, find a friend to download a
|
||||
[Chrome Standalone] installer for you.
|
||||
|
||||
Command line args
|
||||
------------------
|
||||
|
||||
You can use args to override settings from `config.json`.
|
||||
|
||||
sslocal -s server_name -p server_port -l local_port -k password -m bf-cfb
|
||||
ssserver -p server_port -k password -m bf-cfb --workers 2
|
||||
ssserver -c /etc/shadowsocks/config.json
|
||||
|
||||
List all available args with `-h`.
|
||||
|
||||
Wiki
|
||||
----
|
||||
|
||||
You can find all the documentation in the wiki:
|
||||
https://github.com/clowwindy/shadowsocks/wiki
|
||||
You can find all the documentation in the [Wiki](https://github.com/shadowsocks/shadowsocks/wiki).
|
||||
|
||||
License
|
||||
-------
|
||||
MIT
|
||||
|
||||
Bugs and Issues
|
||||
----------------
|
||||
|
||||
* [Troubleshooting]
|
||||
* [Issue Tracker]
|
||||
* [Mailing list]
|
||||
Apache License
|
||||
|
||||
|
||||
[Android]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#android
|
||||
[Build Status]: https://img.shields.io/travis/clowwindy/shadowsocks/master.svg?style=flat
|
||||
[Chinese Readme]: https://github.com/clowwindy/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
|
||||
[Chrome Standalone]: https://support.google.com/installer/answer/126299
|
||||
[Debian sid]: https://packages.debian.org/unstable/python/shadowsocks
|
||||
[the package]: https://pypi.python.org/pypi/shadowsocks
|
||||
[Encryption]: https://github.com/clowwindy/shadowsocks/wiki/Encryption
|
||||
[iOS]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help
|
||||
[Issue Tracker]: https://github.com/clowwindy/shadowsocks/issues?state=open
|
||||
[Mailing list]: http://groups.google.com/group/shadowsocks
|
||||
[OpenSSL for Windows]: http://slproweb.com/products/Win32OpenSSL.html
|
||||
[OpenWRT]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#openwrt
|
||||
[OS X]: https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Build Status]: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat
|
||||
[PyPI]: https://pypi.python.org/pypi/shadowsocks
|
||||
[PyPI version]: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat
|
||||
[Supervisor]: https://github.com/clowwindy/shadowsocks/wiki/Configure-Shadowsocks-with-Supervisor
|
||||
[TCP_FASTOPEN]: https://github.com/clowwindy/shadowsocks/wiki/TCP-Fast-Open
|
||||
[Travis CI]: https://travis-ci.org/clowwindy/shadowsocks
|
||||
[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting
|
||||
[SwitchySharp]: https://chrome.google.com/webstore/detail/proxy-switchysharp/dpplabbmogkhghncfbfdeeokoefdjegm
|
||||
[Windows]: https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#windows
|
||||
[Travis CI]: https://travis-ci.org/shadowsocks/shadowsocks
|
||||
|
||||
|
|
207
README.rst
207
README.rst
|
@ -1,167 +1,118 @@
|
|||
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
|
||||
===========
|
||||
|
||||
|PyPI version| |Build Status|
|
||||
|PyPI version| |Build Status| |Coverage Status|
|
||||
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
`中文说明 <https://github.com/clowwindy/shadowsocks/wiki/Shadowsocks-%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E>`__
|
||||
Server
|
||||
------
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
You'll have a client on your local side, and setup a server on a remote
|
||||
server.
|
||||
|
||||
Client
|
||||
~~~~~~
|
||||
|
||||
- `Windows <https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#windows>`__
|
||||
/ `OS
|
||||
X <https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help>`__
|
||||
- `Android <https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#android>`__
|
||||
/ `iOS <https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help>`__
|
||||
- `OpenWRT <https://github.com/clowwindy/shadowsocks/wiki/Ports-and-Clients#openwrt>`__
|
||||
|
||||
Server
|
||||
~~~~~~
|
||||
~~~~~~~
|
||||
|
||||
Debian / Ubuntu:
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
apt-get install python-pip
|
||||
pip install shadowsocks
|
||||
|
||||
Or simply ``apt-get install shadowsocks`` if you have `Debian
|
||||
sid <https://packages.debian.org/unstable/python/shadowsocks>`__ in your
|
||||
source list.
|
||||
|
||||
CentOS:
|
||||
^^^^^^^
|
||||
|
||||
::
|
||||
|
||||
yum install python-setuptools
|
||||
easy_install pip
|
||||
yum install python-setuptools && easy_install pip
|
||||
pip install shadowsocks
|
||||
|
||||
Windows:
|
||||
^^^^^^^^
|
||||
|
||||
Download OpenSSL for Windows and install. Then install shadowsocks via
|
||||
easy\_install and pip as Linux. If you don't know how to use them, you
|
||||
can directly download `the
|
||||
package <https://pypi.python.org/pypi/shadowsocks>`__, and use
|
||||
``python shadowsocks/server.py`` instead of ``ssserver`` command below.
|
||||
See `Install Server on
|
||||
Windows <https://github.com/shadowsocks/shadowsocks/wiki/Install-Shadowsocks-Server-on-Windows>`__
|
||||
|
||||
Configuration
|
||||
Usage
|
||||
~~~~~
|
||||
|
||||
::
|
||||
|
||||
ssserver -p 443 -k password -m rc4-md5
|
||||
|
||||
To run in the background:
|
||||
|
||||
::
|
||||
|
||||
sudo ssserver -p 443 -k password -m rc4-md5 --user nobody -d start
|
||||
|
||||
To stop:
|
||||
|
||||
::
|
||||
|
||||
sudo ssserver -d stop
|
||||
|
||||
To check the log:
|
||||
|
||||
::
|
||||
|
||||
sudo less /var/log/shadowsocks.log
|
||||
|
||||
Check all the options via ``-h``. You can also use a
|
||||
`Configuration <https://github.com/shadowsocks/shadowsocks/wiki/Configuration-via-Config-File>`__
|
||||
file instead.
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
- `Windows <https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#windows>`__
|
||||
/ `OS
|
||||
X <https://github.com/shadowsocks/shadowsocks-iOS/wiki/Shadowsocks-for-OSX-Help>`__
|
||||
- `Android <https://github.com/shadowsocks/shadowsocks/wiki/Ports-and-Clients#android>`__
|
||||
/ `iOS <https://github.com/shadowsocks/shadowsocks-iOS/wiki/Help>`__
|
||||
- `OpenWRT <https://github.com/shadowsocks/openwrt-shadowsocks>`__
|
||||
|
||||
Use GUI clients on your local PC/phones. Check the README of your client
|
||||
for more information.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
On your server create a config file ``/etc/shadowsocks.json``. Example:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"server":"my_server_ip",
|
||||
"server_port":8388,
|
||||
"local_address": "127.0.0.1",
|
||||
"local_port":1080,
|
||||
"password":"mypassword",
|
||||
"timeout":300,
|
||||
"method":"aes-256-cfb",
|
||||
"fast_open": false,
|
||||
"workers": 1
|
||||
}
|
||||
|
||||
Explanation of the fields:
|
||||
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| Name | Explanation |
|
||||
+==================+=========================================================================================================+
|
||||
| server | the address your server listens |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| server\_port | server port |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| local\_address | the address your local listens |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| local\_port | local port |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| password | password used for encryption |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| timeout | in seconds |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| method | default: "aes-256-cfb", see `Encryption <https://github.com/clowwindy/shadowsocks/wiki/Encryption>`__ |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| fast\_open | use `TCP\_FASTOPEN <https://github.com/clowwindy/shadowsocks/wiki/TCP-Fast-Open>`__, true / false |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
| workers | number of workers, available on Unix/Linux |
|
||||
+------------------+---------------------------------------------------------------------------------------------------------+
|
||||
|
||||
Run ``ssserver -c /etc/shadowsocks.json`` on your server. To run it in
|
||||
the background, use
|
||||
`Supervisor <https://github.com/clowwindy/shadowsocks/wiki/Configure-Shadowsocks-with-Supervisor>`__.
|
||||
|
||||
On your client machine, use the same configuration as your server, and
|
||||
start your client.
|
||||
|
||||
If you use Chrome, it's recommended to use
|
||||
`SwitchySharp <https://chrome.google.com/webstore/detail/proxy-switchysharp/dpplabbmogkhghncfbfdeeokoefdjegm>`__.
|
||||
Change the proxy settings to
|
||||
|
||||
::
|
||||
|
||||
protocol: socks5
|
||||
hostname: 127.0.0.1
|
||||
port: your local_port
|
||||
|
||||
If you can't install
|
||||
`SwitchySharp <https://chrome.google.com/webstore/detail/proxy-switchysharp/dpplabbmogkhghncfbfdeeokoefdjegm>`__,
|
||||
you can launch Chrome with the following arguments to force Chrome to
|
||||
use the proxy:
|
||||
|
||||
::
|
||||
|
||||
Chrome.exe --proxy-server="socks5://127.0.0.1:1080" --host-resolver-rules="MAP * 0.0.0.0 , EXCLUDE localhost"
|
||||
|
||||
If you can't even download Chrome, find a friend to download a `Chrome
|
||||
Standalone <https://support.google.com/installer/answer/126299>`__
|
||||
installer for you.
|
||||
|
||||
Command line args
|
||||
-----------------
|
||||
|
||||
You can use args to override settings from ``config.json``.
|
||||
|
||||
::
|
||||
|
||||
sslocal -s server_name -p server_port -l local_port -k password -m bf-cfb
|
||||
ssserver -p server_port -k password -m bf-cfb --workers 2
|
||||
ssserver -c /etc/shadowsocks/config.json
|
||||
|
||||
List all available args with ``-h``.
|
||||
|
||||
Wiki
|
||||
----
|
||||
|
||||
You can find all the documentation in the wiki:
|
||||
https://github.com/clowwindy/shadowsocks/wiki
|
||||
You can find all the documentation in the
|
||||
`Wiki <https://github.com/shadowsocks/shadowsocks/wiki>`__.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
MIT
|
||||
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 <https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting>`__
|
||||
- `Troubleshooting <https://github.com/shadowsocks/shadowsocks/wiki/Troubleshooting>`__
|
||||
- `Issue
|
||||
Tracker <https://github.com/clowwindy/shadowsocks/issues?state=open>`__
|
||||
- `Mailing list <http://groups.google.com/group/shadowsocks>`__
|
||||
Tracker <https://github.com/shadowsocks/shadowsocks/issues?state=open>`__
|
||||
- `Mailing list <https://groups.google.com/group/shadowsocks>`__
|
||||
|
||||
.. |PyPI version| image:: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat
|
||||
:target: https://pypi.python.org/pypi/shadowsocks
|
||||
.. |Build Status| image:: https://img.shields.io/travis/clowwindy/shadowsocks/master.svg?style=flat
|
||||
:target: https://travis-ci.org/clowwindy/shadowsocks
|
||||
.. |Build Status| image:: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat
|
||||
:target: https://travis-ci.org/shadowsocks/shadowsocks
|
||||
.. |Coverage Status| image:: https://jenkins.shadowvpn.org/result/shadowsocks
|
||||
:target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html
|
||||
|
|
17
config.json.example
Normal file
17
config.json.example
Normal 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
23
debian/changelog
vendored
|
@ -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
3
debian/config.json
vendored
|
@ -7,5 +7,6 @@
|
|||
"timeout":300,
|
||||
"method":"aes-256-cfb",
|
||||
"fast_open": false,
|
||||
"workers": 1
|
||||
"workers": 1,
|
||||
"prefer_ipv6": false
|
||||
}
|
21
debian/control
vendored
21
debian/control
vendored
|
@ -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
43
debian/copyright
vendored
|
@ -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
2
debian/install
vendored
|
@ -1 +1 @@
|
|||
debian/config.json etc/shadowsocks/
|
||||
debian/config.json etc/shadowsocks/
|
||||
|
|
2
debian/shadowsocks.manpages
vendored
2
debian/shadowsocks.manpages
vendored
|
@ -1,2 +1,2 @@
|
|||
debian/sslocal.1
|
||||
debian/ssserver.1
|
||||
debian/ssserver.1
|
||||
|
|
4
debian/sslocal.1
vendored
4
debian/sslocal.1
vendored
|
@ -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
4
debian/ssserver.1
vendored
|
@ -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>"
|
||||
.
|
||||
|
|
8
setup.py
8
setup.py
|
@ -7,12 +7,12 @@ with codecs.open('README.rst', encoding='utf-8') as f:
|
|||
|
||||
setup(
|
||||
name="shadowsocks",
|
||||
version="2.6",
|
||||
license='MIT',
|
||||
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',
|
||||
author_email='clowwindy42@gmail.com',
|
||||
url='https://github.com/clowwindy/shadowsocks',
|
||||
url='https://github.com/shadowsocks/shadowsocks',
|
||||
packages=['shadowsocks', 'shadowsocks.crypto'],
|
||||
package_data={
|
||||
'shadowsocks': ['README.rst', 'LICENSE']
|
||||
|
@ -24,7 +24,7 @@ setup(
|
|||
ssserver = shadowsocks.server:main
|
||||
""",
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2012-2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
|
|
@ -1,42 +1,35 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2014-2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import time
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import re
|
||||
import logging
|
||||
|
||||
from shadowsocks import common, lru_cache, eventloop
|
||||
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()
|
||||
|
||||
|
@ -93,11 +86,12 @@ def build_address(address):
|
|||
return b''.join(results)
|
||||
|
||||
|
||||
def build_request(address, qtype, request_id):
|
||||
header = struct.pack('!HBBHHHH', request_id, 1, 0, 1, 0, 0, 0)
|
||||
def build_request(address, qtype):
|
||||
request_id = os.urandom(2)
|
||||
header = struct.pack('!BBHHHH', 1, 0, 1, 0, 0, 0)
|
||||
addr = build_address(address)
|
||||
qtype_qclass = struct.pack('!HH', qtype, QCLASS_IN)
|
||||
return header + addr + qtype_qclass
|
||||
return request_id + header + addr + qtype_qclass
|
||||
|
||||
|
||||
def parse_ip(addrtype, data, length, offset):
|
||||
|
@ -226,24 +220,10 @@ def parse_response(data):
|
|||
response.answers.append((an[1], an[2], an[3]))
|
||||
return response
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
return None
|
||||
|
||||
|
||||
def is_ip(address):
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
try:
|
||||
if type(address) != str:
|
||||
address = address.decode('utf8')
|
||||
socket.inet_pton(family, address)
|
||||
return family
|
||||
except (TypeError, ValueError, OSError, IOError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_hostname(hostname):
|
||||
if len(hostname) > 255:
|
||||
return False
|
||||
|
@ -262,24 +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._request_id = 1
|
||||
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
|
||||
|
@ -291,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 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:
|
||||
|
@ -314,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 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
|
||||
|
@ -332,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, [])
|
||||
|
@ -360,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)
|
||||
|
@ -412,10 +402,7 @@ class DNSResolver(object):
|
|||
del self._hostname_status[hostname]
|
||||
|
||||
def _send_req(self, hostname, qtype):
|
||||
self._request_id += 1
|
||||
if self._request_id > 32768:
|
||||
self._request_id = 1
|
||||
req = build_request(hostname, qtype, self._request_id)
|
||||
req = build_request(hostname, qtype)
|
||||
for server in self._servers:
|
||||
logging.debug('resolving %s with type %d using server %s',
|
||||
hostname, qtype, server)
|
||||
|
@ -426,7 +413,7 @@ class DNSResolver(object):
|
|||
hostname = hostname.encode('utf8')
|
||||
if not hostname:
|
||||
callback(None, Exception('empty hostname'))
|
||||
elif is_ip(hostname):
|
||||
elif common.is_ip(hostname):
|
||||
callback((hostname, hostname), None)
|
||||
elif hostname in self._hosts:
|
||||
logging.debug('hit hosts: %s', hostname)
|
||||
|
@ -442,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
|
||||
|
||||
|
@ -460,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
|
||||
|
@ -474,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
|
||||
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2013-2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -27,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):
|
||||
|
@ -101,6 +114,18 @@ def inet_pton(family, addr):
|
|||
raise RuntimeError("What family?")
|
||||
|
||||
|
||||
def is_ip(address):
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
try:
|
||||
if type(address) != str:
|
||||
address = address.decode('utf8')
|
||||
inet_pton(family, address)
|
||||
return family
|
||||
except (TypeError, ValueError, OSError, IOError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def patch_socket():
|
||||
if not hasattr(socket, 'inet_pton'):
|
||||
socket.inet_pton = inet_pton
|
||||
|
@ -112,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)
|
||||
|
@ -133,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]
|
||||
|
@ -165,13 +200,68 @@ def parse_header(data):
|
|||
else:
|
||||
logging.warn('header is too short')
|
||||
else:
|
||||
logging.warn('unsupported addrtype %d, maybe wrong password' %
|
||||
addrtype)
|
||||
logging.warn('unsupported addrtype %d, maybe wrong password or '
|
||||
'encryption method' % addrtype)
|
||||
if dest_addr is None:
|
||||
return None
|
||||
return addrtype, to_bytes(dest_addr), dest_port, header_length
|
||||
|
||||
|
||||
class IPNetwork(object):
|
||||
ADDRLENGTH = {socket.AF_INET: 32, socket.AF_INET6: 128, False: 0}
|
||||
|
||||
def __init__(self, addrs):
|
||||
self._network_list_v4 = []
|
||||
self._network_list_v6 = []
|
||||
if type(addrs) == str:
|
||||
addrs = addrs.split(',')
|
||||
list(map(self.add_network, addrs))
|
||||
|
||||
def add_network(self, addr):
|
||||
if addr is "":
|
||||
return
|
||||
block = addr.split('/')
|
||||
addr_family = is_ip(block[0])
|
||||
addr_len = IPNetwork.ADDRLENGTH[addr_family]
|
||||
if addr_family is socket.AF_INET:
|
||||
ip, = struct.unpack("!I", socket.inet_aton(block[0]))
|
||||
elif addr_family is socket.AF_INET6:
|
||||
hi, lo = struct.unpack("!QQ", inet_pton(addr_family, block[0]))
|
||||
ip = (hi << 64) | lo
|
||||
else:
|
||||
raise Exception("Not a valid CIDR notation: %s" % addr)
|
||||
if len(block) is 1:
|
||||
prefix_size = 0
|
||||
while (ip & 1) == 0 and ip is not 0:
|
||||
ip >>= 1
|
||||
prefix_size += 1
|
||||
logging.warn("You did't specify CIDR routing prefix size for %s, "
|
||||
"implicit treated as %s/%d" % (addr, addr, addr_len))
|
||||
elif block[1].isdigit() and int(block[1]) <= addr_len:
|
||||
prefix_size = addr_len - int(block[1])
|
||||
ip >>= prefix_size
|
||||
else:
|
||||
raise Exception("Not a valid CIDR notation: %s" % addr)
|
||||
if addr_family is socket.AF_INET:
|
||||
self._network_list_v4.append((ip, prefix_size))
|
||||
else:
|
||||
self._network_list_v6.append((ip, prefix_size))
|
||||
|
||||
def __contains__(self, addr):
|
||||
addr_family = is_ip(addr)
|
||||
if addr_family is socket.AF_INET:
|
||||
ip, = struct.unpack("!I", socket.inet_aton(addr))
|
||||
return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1],
|
||||
self._network_list_v4))
|
||||
elif addr_family is socket.AF_INET6:
|
||||
hi, lo = struct.unpack("!QQ", inet_pton(addr_family, addr))
|
||||
ip = (hi << 64) | lo
|
||||
return any(map(lambda n_ps: n_ps[0] == ip >> n_ps[1],
|
||||
self._network_list_v6))
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def test_inet_conv():
|
||||
ipv4 = b'8.8.4.4'
|
||||
b = inet_pton(socket.AF_INET, ipv4)
|
||||
|
@ -198,7 +288,23 @@ def test_pack_header():
|
|||
assert pack_addr(b'www.google.com') == b'\x03\x0ewww.google.com'
|
||||
|
||||
|
||||
def test_ip_network():
|
||||
ip_network = IPNetwork('127.0.0.0/24,::ff:1/112,::1,192.168.1.1,192.0.2.0')
|
||||
assert '127.0.0.1' in ip_network
|
||||
assert '127.0.1.1' not in ip_network
|
||||
assert ':ff:ffff' in ip_network
|
||||
assert '::ffff:1' not in ip_network
|
||||
assert '::1' in ip_network
|
||||
assert '::2' not in ip_network
|
||||
assert '192.168.1.1' in ip_network
|
||||
assert '192.168.1.2' not in ip_network
|
||||
assert '192.0.2.1' in ip_network
|
||||
assert '192.0.3.1' in ip_network # 192.0.2.0 is treated as 192.0.2.0/23
|
||||
assert 'www.google.com' not in ip_network
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_inet_conv()
|
||||
test_parse_header()
|
||||
test_pack_header()
|
||||
test_ip_network()
|
||||
|
|
|
@ -1,24 +1,18 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
|
340
shadowsocks/crypto/aead.py
Normal file
340
shadowsocks/crypto/aead.py
Normal 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()
|
|
@ -1,135 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import logging
|
||||
from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
libsodium = None
|
||||
loaded = False
|
||||
|
||||
buf_size = 2048
|
||||
|
||||
# for salsa20 and chacha20
|
||||
BLOCK_SIZE = 64
|
||||
|
||||
|
||||
def load_libsodium():
|
||||
global loaded, libsodium, buf
|
||||
|
||||
from ctypes.util import find_library
|
||||
for p in ('sodium',):
|
||||
libsodium_path = find_library(p)
|
||||
if libsodium_path:
|
||||
break
|
||||
else:
|
||||
raise Exception('libsodium not found')
|
||||
logging.info('loading libsodium from %s', libsodium_path)
|
||||
libsodium = CDLL(libsodium_path)
|
||||
libsodium.sodium_init.restype = c_int
|
||||
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_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.sodium_init()
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
class Salsa20Crypto(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
if not loaded:
|
||||
load_libsodium()
|
||||
self.key = key
|
||||
self.iv = iv
|
||||
self.key_ptr = c_char_p(key)
|
||||
self.iv_ptr = c_char_p(iv)
|
||||
if cipher_name == b'salsa20':
|
||||
self.cipher = libsodium.crypto_stream_salsa20_xor_ic
|
||||
elif cipher_name == b'chacha20':
|
||||
self.cipher = libsodium.crypto_stream_chacha20_xor_ic
|
||||
else:
|
||||
raise Exception('Unknown cipher')
|
||||
# byte counter, not block counter
|
||||
self.counter = 0
|
||||
|
||||
def update(self, data):
|
||||
global buf_size, buf
|
||||
l = len(data)
|
||||
|
||||
# we can only prepend some padding to make the encryption align to
|
||||
# blocks
|
||||
padding = self.counter % BLOCK_SIZE
|
||||
if buf_size < padding + l:
|
||||
buf_size = (padding + l) * 2
|
||||
buf = create_string_buffer(buf_size)
|
||||
|
||||
if padding:
|
||||
data = (b'\0' * padding) + data
|
||||
self.cipher(byref(buf), c_char_p(data), padding + l,
|
||||
self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr)
|
||||
self.counter += l
|
||||
# buf is copied to a str object when we access buf.raw
|
||||
# strip off the padding
|
||||
return buf.raw[padding:padding + l]
|
||||
|
||||
|
||||
ciphers = {
|
||||
b'salsa20': (32, 8, Salsa20Crypto),
|
||||
b'chacha20': (32, 8, Salsa20Crypto),
|
||||
}
|
||||
|
||||
|
||||
def test_salsa20():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
def test_chacha20():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_chacha20()
|
||||
test_salsa20()
|
|
@ -1,188 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import logging
|
||||
from ctypes import CDLL, c_char_p, c_int, c_long, byref,\
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
libcrypto = None
|
||||
loaded = False
|
||||
|
||||
buf_size = 2048
|
||||
|
||||
|
||||
def load_openssl():
|
||||
global loaded, libcrypto, buf
|
||||
|
||||
from ctypes.util import find_library
|
||||
for p in ('crypto', 'eay32', 'libeay32'):
|
||||
libcrypto_path = find_library(p)
|
||||
if libcrypto_path:
|
||||
break
|
||||
else:
|
||||
raise Exception('libcrypto(OpenSSL) not found')
|
||||
logging.info('loading libcrypto from %s', libcrypto_path)
|
||||
libcrypto = CDLL(libcrypto_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_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_CIPHER_CTX_free.argtypes = (c_void_p,)
|
||||
if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'):
|
||||
libcrypto.OpenSSL_add_all_ciphers()
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
def load_cipher(cipher_name):
|
||||
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)
|
||||
if cipher:
|
||||
cipher.restype = c_void_p
|
||||
return cipher()
|
||||
return None
|
||||
|
||||
|
||||
class CtypesCrypto(object):
|
||||
def __init__(self, cipher_name, key, iv, op):
|
||||
if not loaded:
|
||||
load_openssl()
|
||||
self._ctx = None
|
||||
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()
|
||||
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 update(self, data):
|
||||
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)
|
||||
# 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:
|
||||
libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx)
|
||||
libcrypto.EVP_CIPHER_CTX_free(self._ctx)
|
||||
|
||||
|
||||
ciphers = {
|
||||
b'aes-128-cfb': (16, 16, CtypesCrypto),
|
||||
b'aes-192-cfb': (24, 16, CtypesCrypto),
|
||||
b'aes-256-cfb': (32, 16, CtypesCrypto),
|
||||
b'aes-128-ofb': (16, 16, CtypesCrypto),
|
||||
b'aes-192-ofb': (24, 16, CtypesCrypto),
|
||||
b'aes-256-ofb': (32, 16, CtypesCrypto),
|
||||
b'aes-128-ctr': (16, 16, CtypesCrypto),
|
||||
b'aes-192-ctr': (24, 16, CtypesCrypto),
|
||||
b'aes-256-ctr': (32, 16, CtypesCrypto),
|
||||
b'aes-128-cfb8': (16, 16, CtypesCrypto),
|
||||
b'aes-192-cfb8': (24, 16, CtypesCrypto),
|
||||
b'aes-256-cfb8': (32, 16, CtypesCrypto),
|
||||
b'aes-128-cfb1': (16, 16, CtypesCrypto),
|
||||
b'aes-192-cfb1': (24, 16, CtypesCrypto),
|
||||
b'aes-256-cfb1': (32, 16, CtypesCrypto),
|
||||
b'bf-cfb': (16, 8, CtypesCrypto),
|
||||
b'camellia-128-cfb': (16, 16, CtypesCrypto),
|
||||
b'camellia-192-cfb': (24, 16, CtypesCrypto),
|
||||
b'camellia-256-cfb': (32, 16, CtypesCrypto),
|
||||
b'cast5-cfb': (16, 8, CtypesCrypto),
|
||||
b'des-cfb': (8, 8, CtypesCrypto),
|
||||
b'idea-cfb': (16, 8, CtypesCrypto),
|
||||
b'rc2-cfb': (16, 8, CtypesCrypto),
|
||||
b'rc4': (16, 0, CtypesCrypto),
|
||||
b'seed-cfb': (16, 16, CtypesCrypto),
|
||||
}
|
||||
|
||||
|
||||
def run_method(method):
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 1)
|
||||
decipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
def test_aes_128_cfb():
|
||||
run_method(b'aes-128-cfb')
|
||||
|
||||
|
||||
def test_aes_256_cfb():
|
||||
run_method(b'aes-256-cfb')
|
||||
|
||||
|
||||
def test_aes_128_cfb8():
|
||||
run_method(b'aes-128-cfb8')
|
||||
|
||||
|
||||
def test_aes_256_ofb():
|
||||
run_method(b'aes-256-ofb')
|
||||
|
||||
|
||||
def test_aes_256_ctr():
|
||||
run_method(b'aes-256-ctr')
|
||||
|
||||
|
||||
def test_bf_cfb():
|
||||
run_method(b'bf-cfb')
|
||||
|
||||
|
||||
def test_rc4():
|
||||
run_method(b'rc4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_aes_128_cfb()
|
98
shadowsocks/crypto/hkdf.py
Normal file
98
shadowsocks/crypto/hkdf.py
Normal 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)
|
|
@ -1,119 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
__all__ = ['ciphers']
|
||||
|
||||
has_m2 = True
|
||||
try:
|
||||
__import__('M2Crypto')
|
||||
except ImportError:
|
||||
has_m2 = False
|
||||
if bytes != str:
|
||||
has_m2 = False
|
||||
|
||||
|
||||
def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1,
|
||||
padding=1):
|
||||
|
||||
import M2Crypto.EVP
|
||||
return M2Crypto.EVP.Cipher(alg.replace('-', '_'), key, iv, op,
|
||||
key_as_bytes=0, d='md5', salt=None, i=1,
|
||||
padding=1)
|
||||
|
||||
|
||||
def err(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, padding=1):
|
||||
logging.error(('M2Crypto is required to use %s, please run'
|
||||
' `apt-get install python-m2crypto`') % alg)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if has_m2:
|
||||
ciphers = {
|
||||
b'aes-128-cfb': (16, 16, create_cipher),
|
||||
b'aes-192-cfb': (24, 16, create_cipher),
|
||||
b'aes-256-cfb': (32, 16, create_cipher),
|
||||
b'bf-cfb': (16, 8, create_cipher),
|
||||
b'camellia-128-cfb': (16, 16, create_cipher),
|
||||
b'camellia-192-cfb': (24, 16, create_cipher),
|
||||
b'camellia-256-cfb': (32, 16, create_cipher),
|
||||
b'cast5-cfb': (16, 8, create_cipher),
|
||||
b'des-cfb': (8, 8, create_cipher),
|
||||
b'idea-cfb': (16, 8, create_cipher),
|
||||
b'rc2-cfb': (16, 8, create_cipher),
|
||||
b'rc4': (16, 0, create_cipher),
|
||||
b'seed-cfb': (16, 16, create_cipher),
|
||||
}
|
||||
else:
|
||||
ciphers = {}
|
||||
|
||||
|
||||
def run_method(method):
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = create_cipher(method, b'k' * 32, b'i' * 16, 1)
|
||||
decipher = create_cipher(method, b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
def check_env():
|
||||
# skip this test on pypy and Python 3
|
||||
try:
|
||||
import __pypy__
|
||||
del __pypy__
|
||||
from nose.plugins.skip import SkipTest
|
||||
raise SkipTest
|
||||
except ImportError:
|
||||
pass
|
||||
if bytes != str:
|
||||
from nose.plugins.skip import SkipTest
|
||||
raise SkipTest
|
||||
|
||||
|
||||
def test_aes_128_cfb():
|
||||
check_env()
|
||||
run_method(b'aes-128-cfb')
|
||||
|
||||
|
||||
def test_aes_256_cfb():
|
||||
check_env()
|
||||
run_method(b'aes-256-cfb')
|
||||
|
||||
|
||||
def test_bf_cfb():
|
||||
check_env()
|
||||
run_method(b'bf-cfb')
|
||||
|
||||
|
||||
def test_rc4():
|
||||
check_env()
|
||||
run_method(b'rc4')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_aes_128_cfb()
|
478
shadowsocks/crypto/mbedtls.py
Normal file
478
shadowsocks/crypto/mbedtls.py
Normal 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)
|
448
shadowsocks/crypto/openssl.py
Normal file
448
shadowsocks/crypto/openssl.py
Normal file
|
@ -0,0 +1,448 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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
|
||||
|
||||
from ctypes import c_char_p, c_int, c_long, byref,\
|
||||
create_string_buffer, c_void_p
|
||||
|
||||
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
|
||||
|
||||
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', path)
|
||||
if libcrypto is None:
|
||||
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_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()
|
||||
|
||||
buf = create_string_buffer(buf_size)
|
||||
loaded = True
|
||||
|
||||
|
||||
def load_cipher(cipher_name):
|
||||
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)
|
||||
if cipher:
|
||||
cipher.restype = c_void_p
|
||||
return cipher()
|
||||
return None
|
||||
|
||||
|
||||
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(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)
|
||||
self._ctx = libcrypto.EVP_CIPHER_CTX_new()
|
||||
self._cipher = cipher
|
||||
if not self._ctx:
|
||||
raise Exception('can not create 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
|
||||
)
|
||||
# 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:
|
||||
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, 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):
|
||||
|
||||
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')
|
||||
|
||||
|
||||
def test_aes_256_cfb():
|
||||
run_method('aes-256-cfb')
|
||||
|
||||
|
||||
def test_aes_128_cfb8():
|
||||
run_method('aes-128-cfb8')
|
||||
|
||||
|
||||
def test_aes_256_ofb():
|
||||
run_method('aes-256-ofb')
|
||||
|
||||
|
||||
def test_aes_256_ctr():
|
||||
run_method('aes-256-ctr')
|
||||
|
||||
|
||||
def test_bf_cfb():
|
||||
run_method('bf-cfb')
|
||||
|
||||
|
||||
def test_rc4():
|
||||
run_method('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)
|
|
@ -1,61 +1,48 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
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()
|
||||
|
||||
try:
|
||||
from shadowsocks.crypto import ctypes_openssl
|
||||
return ctypes_openssl.CtypesCrypto(b'rc4', rc4_key, b'', op)
|
||||
except:
|
||||
import M2Crypto.EVP
|
||||
return M2Crypto.EVP.Cipher(b'rc4', rc4_key, b'', op,
|
||||
key_as_bytes=0, d='md5', salt=None, i=1,
|
||||
padding=1)
|
||||
return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op, crypto_path)
|
||||
|
||||
|
||||
ciphers = {
|
||||
b'rc4-md5': (16, 16, create_cipher),
|
||||
'rc4-md5': (16, 16, create_cipher),
|
||||
}
|
||||
|
||||
|
||||
def test():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = create_cipher(b'rc4-md5', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = create_cipher(b'rc4-md5', b'k' * 32, b'i' * 16, 0)
|
||||
cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1)
|
||||
decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import struct
|
||||
import logging
|
||||
import sys
|
||||
|
||||
slow_xor = False
|
||||
imported = False
|
||||
|
||||
salsa20 = None
|
||||
numpy = None
|
||||
|
||||
BLOCK_SIZE = 16384
|
||||
|
||||
|
||||
def run_imports():
|
||||
global imported, slow_xor, salsa20, numpy
|
||||
if not imported:
|
||||
imported = True
|
||||
try:
|
||||
numpy = __import__('numpy')
|
||||
except ImportError:
|
||||
logging.error('can not import numpy, using SLOW XOR')
|
||||
logging.error('please install numpy if you use salsa20')
|
||||
slow_xor = True
|
||||
try:
|
||||
salsa20 = __import__('salsa20')
|
||||
except ImportError:
|
||||
logging.error('you have to install salsa20 before you use salsa20')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def numpy_xor(a, b):
|
||||
if slow_xor:
|
||||
return py_xor_str(a, b)
|
||||
dtype = numpy.byte
|
||||
if len(a) % 4 == 0:
|
||||
dtype = numpy.uint32
|
||||
elif len(a) % 2 == 0:
|
||||
dtype = numpy.uint16
|
||||
|
||||
ab = numpy.frombuffer(a, dtype=dtype)
|
||||
bb = numpy.frombuffer(b, dtype=dtype)
|
||||
c = numpy.bitwise_xor(ab, bb)
|
||||
r = c.tostring()
|
||||
return r
|
||||
|
||||
|
||||
def py_xor_str(a, b):
|
||||
c = []
|
||||
if bytes == str:
|
||||
for i in range(0, len(a)):
|
||||
c.append(chr(ord(a[i]) ^ ord(b[i])))
|
||||
return ''.join(c)
|
||||
else:
|
||||
for i in range(0, len(a)):
|
||||
c.append(a[i] ^ b[i])
|
||||
return bytes(c)
|
||||
|
||||
|
||||
class Salsa20Cipher(object):
|
||||
"""a salsa20 CTR implemetation, provides m2crypto like cipher API"""
|
||||
|
||||
def __init__(self, alg, key, iv, op, key_as_bytes=0, d=None, salt=None,
|
||||
i=1, padding=1):
|
||||
run_imports()
|
||||
if alg != b'salsa20-ctr':
|
||||
raise Exception('unknown algorithm')
|
||||
self._key = key
|
||||
self._nonce = struct.unpack('<Q', iv)[0]
|
||||
self._pos = 0
|
||||
self._next_stream()
|
||||
|
||||
def _next_stream(self):
|
||||
self._nonce &= 0xFFFFFFFFFFFFFFFF
|
||||
self._stream = salsa20.Salsa20_keystream(BLOCK_SIZE,
|
||||
struct.pack('<Q',
|
||||
self._nonce),
|
||||
self._key)
|
||||
self._nonce += 1
|
||||
|
||||
def update(self, data):
|
||||
results = []
|
||||
while True:
|
||||
remain = BLOCK_SIZE - self._pos
|
||||
cur_data = data[:remain]
|
||||
cur_data_len = len(cur_data)
|
||||
cur_stream = self._stream[self._pos:self._pos + cur_data_len]
|
||||
self._pos = self._pos + cur_data_len
|
||||
data = data[remain:]
|
||||
|
||||
results.append(numpy_xor(cur_data, cur_stream))
|
||||
|
||||
if self._pos >= BLOCK_SIZE:
|
||||
self._next_stream()
|
||||
self._pos = 0
|
||||
if not data:
|
||||
break
|
||||
return b''.join(results)
|
||||
|
||||
|
||||
ciphers = {
|
||||
b'salsa20-ctr': (32, 8, Salsa20Cipher),
|
||||
}
|
||||
|
||||
|
||||
def test():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = Salsa20Cipher(b'salsa20-ctr', b'k' * 32, b'i' * 8, 1)
|
||||
decipher = Salsa20Cipher(b'salsa20-ctr', b'k' * 32, b'i' * 8, 1)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
442
shadowsocks/crypto/sodium.py
Normal file
442
shadowsocks/crypto/sodium.py
Normal file
|
@ -0,0 +1,442 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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
|
||||
|
||||
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 and chacha20-ietf
|
||||
BLOCK_SIZE = 64
|
||||
|
||||
|
||||
def load_libsodium(crypto_path=None):
|
||||
global loaded, libsodium, buf
|
||||
|
||||
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, # 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
|
||||
)
|
||||
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, crypto_path=None):
|
||||
if not loaded:
|
||||
load_libsodium(crypto_path)
|
||||
self.key = key
|
||||
self.iv = iv
|
||||
self.key_ptr = c_char_p(key)
|
||||
self.iv_ptr = c_char_p(iv)
|
||||
if cipher_name == 'salsa20':
|
||||
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)
|
||||
|
||||
# we can only prepend some padding to make the encryption align to
|
||||
# blocks
|
||||
padding = self.counter % BLOCK_SIZE
|
||||
if buf_size < padding + l:
|
||||
buf_size = (padding + l) * 2
|
||||
buf = create_string_buffer(buf_size)
|
||||
|
||||
if padding:
|
||||
data = (b'\0' * padding) + data
|
||||
self.cipher(byref(buf), c_char_p(data), padding + l,
|
||||
self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr)
|
||||
self.counter += l
|
||||
# buf is copied to a str object when we access buf.raw
|
||||
# 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_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()
|
|
@ -1,24 +1,18 @@
|
|||
# !/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -61,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:
|
||||
|
@ -73,7 +71,7 @@ class TableCipher(object):
|
|||
|
||||
|
||||
ciphers = {
|
||||
b'table': (0, 0, TableCipher)
|
||||
'table': (0, 0, TableCipher)
|
||||
}
|
||||
|
||||
|
||||
|
@ -169,8 +167,8 @@ def test_table_result():
|
|||
def test_encryption():
|
||||
from shadowsocks.crypto import util
|
||||
|
||||
cipher = TableCipher(b'table', b'test', b'', 1)
|
||||
decipher = TableCipher(b'table', b'test', b'', 0)
|
||||
cipher = TableCipher('table', b'test', b'', 1)
|
||||
decipher = TableCipher('table', b'test', b'', 0)
|
||||
|
||||
util.run_cipher(cipher, decipher)
|
||||
|
||||
|
|
|
@ -1,24 +1,121 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
def find_library_nt(name):
|
||||
# modified from ctypes.util
|
||||
# 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)
|
||||
if os.path.isfile(fname):
|
||||
results.append(fname)
|
||||
if fname.lower().endswith(".dll"):
|
||||
continue
|
||||
fname += "*.dll"
|
||||
files = glob.glob(fname)
|
||||
if files:
|
||||
results.extend(files)
|
||||
return results
|
||||
|
||||
|
||||
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 = []
|
||||
|
||||
if type(possible_lib_names) not in (list, tuple):
|
||||
possible_lib_names = [possible_lib_names]
|
||||
|
||||
lib_names = []
|
||||
for lib_name in possible_lib_names:
|
||||
lib_names.append(lib_name)
|
||||
lib_names.append('lib' + lib_name)
|
||||
|
||||
for name in lib_names:
|
||||
if os.name == "nt":
|
||||
paths.extend(find_library_nt(name))
|
||||
else:
|
||||
path = ctypes.util.find_library(name)
|
||||
if path:
|
||||
paths.append(path)
|
||||
|
||||
if not paths:
|
||||
# We may get here when find_library fails because, for example,
|
||||
# the user does not have sufficient privileges to access those
|
||||
# tools underlying find_library on linux.
|
||||
import glob
|
||||
|
||||
for name in lib_names:
|
||||
patterns = [
|
||||
'/usr/local/lib*/lib%s.*' % name,
|
||||
'/usr/lib*/lib%s.*' % name,
|
||||
'lib%s.*' % name,
|
||||
'%s.dll' % name]
|
||||
|
||||
for pat in patterns:
|
||||
files = glob.glob(pat)
|
||||
if files:
|
||||
paths.extend(files)
|
||||
for path in paths:
|
||||
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
|
||||
|
||||
|
||||
def run_cipher(cipher, decipher):
|
||||
|
@ -26,26 +123,44 @@ 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():
|
||||
assert find_library('c', 'strcpy', 'libc') is not None
|
||||
assert find_library(['c'], 'strcpy', 'libc') is not None
|
||||
assert find_library(('c',), 'strcpy', 'libc') is not None
|
||||
assert find_library(('crypto', 'eay32'), 'EVP_CipherUpdate',
|
||||
'libcrypto') is not None
|
||||
assert find_library('notexist', 'strcpy', 'libnotexist') is None
|
||||
assert find_library('c', 'symbol_not_exist', 'c') is None
|
||||
assert find_library(('notexist', 'c', 'crypto', 'eay32'),
|
||||
'EVP_CipherUpdate', 'libc') is not None
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_find_library()
|
||||
|
|
245
shadowsocks/cryptor.py
Normal file
245
shadowsocks/cryptor.py
Normal file
|
@ -0,0 +1,245 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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 os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from shadowsocks import common
|
||||
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)
|
||||
|
||||
|
||||
def random_string(length):
|
||||
return os.urandom(length)
|
||||
|
||||
cached_keys = {}
|
||||
|
||||
|
||||
def try_cipher(key, method=None, crypto_path=None):
|
||||
Cryptor(key, method, crypto_path)
|
||||
|
||||
|
||||
def EVP_BytesToKey(password, key_len, iv_len):
|
||||
# equivalent to OpenSSL's EVP_BytesToKey() with count 1
|
||||
# so that we make the same key and iv as nodejs version
|
||||
cached_key = '%s-%d-%d' % (password, key_len, iv_len)
|
||||
r = cached_keys.get(cached_key, None)
|
||||
if r:
|
||||
return r
|
||||
m = []
|
||||
i = 0
|
||||
while len(b''.join(m)) < (key_len + iv_len):
|
||||
md5 = hashlib.md5()
|
||||
data = password
|
||||
if i > 0:
|
||||
data = m[i - 1] + password
|
||||
md5.update(data)
|
||||
m.append(md5.digest())
|
||||
i += 1
|
||||
ms = b''.join(m)
|
||||
key = ms[:key_len]
|
||||
iv = ms[key_len:key_len + iv_len]
|
||||
cached_keys[cached_key] = (key, iv)
|
||||
return key, iv
|
||||
|
||||
|
||||
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_sent = False
|
||||
self.cipher_iv = b''
|
||||
self.decipher = None
|
||||
self.decipher_iv = None
|
||||
self.crypto_path = crypto_path
|
||||
method = method.lower()
|
||||
self._method_info = Cryptor.get_method_info(method)
|
||||
if self._method_info:
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def get_method_info(method):
|
||||
method = method.lower()
|
||||
m = method_supported.get(method)
|
||||
return m
|
||||
|
||||
def iv_len(self):
|
||||
return len(self.cipher_iv)
|
||||
|
||||
def get_cipher(self, password, method, op, iv):
|
||||
password = common.to_bytes(password)
|
||||
m = self._method_info
|
||||
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''
|
||||
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
|
||||
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.encrypt(buf)
|
||||
else:
|
||||
self.iv_sent = True
|
||||
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[METHOD_INFO_IV_LEN]
|
||||
decipher_iv = buf[:decipher_iv_len]
|
||||
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.decrypt(buf)
|
||||
|
||||
|
||||
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
|
||||
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',
|
||||
'table',
|
||||
]
|
||||
|
||||
|
||||
def test_encryptor():
|
||||
from os import urandom
|
||||
plain = urandom(10240)
|
||||
for method in CIPHERS_TO_TEST:
|
||||
logging.warn(method)
|
||||
encryptor = Cryptor(b'key', method)
|
||||
decryptor = Cryptor(b'key', method)
|
||||
cipher = encryptor.encrypt(plain)
|
||||
plain2 = decryptor.decrypt(cipher)
|
||||
assert plain == plain2
|
||||
|
||||
|
||||
def test_encrypt_all():
|
||||
from os import urandom
|
||||
plain = urandom(10240)
|
||||
for method in CIPHERS_TO_TEST:
|
||||
logging.warn(method)
|
||||
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()
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2014-2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -29,7 +23,7 @@ import sys
|
|||
import logging
|
||||
import signal
|
||||
import time
|
||||
from shadowsocks import common
|
||||
from shadowsocks import common, shell
|
||||
|
||||
# this module is ported from ShadowVPN daemon.c
|
||||
|
||||
|
@ -37,15 +31,12 @@ from shadowsocks import common
|
|||
def daemon_exec(config):
|
||||
if 'daemon' in config:
|
||||
if os.name != 'posix':
|
||||
raise Exception('daemon mode is only supported in unix')
|
||||
raise Exception('daemon mode is only supported on Unix')
|
||||
command = config['daemon']
|
||||
if not command:
|
||||
command = 'start'
|
||||
pid_file = config['pid-file']
|
||||
log_file = config['log-file']
|
||||
command = common.to_str(command)
|
||||
pid_file = common.to_str(pid_file)
|
||||
log_file = common.to_str(log_file)
|
||||
if command == 'start':
|
||||
daemon_start(pid_file, log_file)
|
||||
elif command == 'stop':
|
||||
|
@ -67,7 +58,7 @@ def write_pid_file(pid_file, pid):
|
|||
fd = os.open(pid_file, os.O_RDWR | os.O_CREAT,
|
||||
stat.S_IRUSR | stat.S_IWUSR)
|
||||
except OSError as e:
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
return -1
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
assert flags != -1
|
||||
|
@ -100,19 +91,21 @@ def freopen(f, mode, stream):
|
|||
|
||||
|
||||
def daemon_start(pid_file, log_file):
|
||||
# fork only once because we are sure parent will exit
|
||||
pid = os.fork()
|
||||
assert pid != -1
|
||||
|
||||
def handle_exit(signum, _):
|
||||
if signum == signal.SIGTERM:
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
|
||||
signal.signal(signal.SIGINT, handle_exit)
|
||||
signal.signal(signal.SIGTERM, handle_exit)
|
||||
|
||||
# fork only once because we are sure parent will exit
|
||||
pid = os.fork()
|
||||
assert pid != -1
|
||||
|
||||
if pid > 0:
|
||||
# parent waits for its child
|
||||
signal.signal(signal.SIGINT, handle_exit)
|
||||
signal.signal(signal.SIGTERM, handle_exit)
|
||||
time.sleep(5)
|
||||
sys.exit(0)
|
||||
|
||||
|
@ -123,6 +116,9 @@ def daemon_start(pid_file, log_file):
|
|||
os.kill(ppid, signal.SIGINT)
|
||||
sys.exit(1)
|
||||
|
||||
os.setsid()
|
||||
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
||||
|
||||
print('started')
|
||||
os.kill(ppid, signal.SIGTERM)
|
||||
|
||||
|
@ -131,8 +127,7 @@ def daemon_start(pid_file, log_file):
|
|||
freopen(log_file, 'a', sys.stdout)
|
||||
freopen(log_file, 'a', sys.stderr)
|
||||
except IOError as e:
|
||||
logging.error(e)
|
||||
os.kill(ppid, signal.SIGINT)
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
@ -145,7 +140,7 @@ def daemon_stop(pid_file):
|
|||
if not buf:
|
||||
logging.error('not running')
|
||||
except IOError as e:
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
if e.errno == errno.ENOENT:
|
||||
# always exit 0 if we are sure daemon is not running
|
||||
logging.error('not running')
|
||||
|
@ -160,7 +155,7 @@ def daemon_stop(pid_file):
|
|||
logging.error('not running')
|
||||
# always exit 0 if we are sure daemon is not running
|
||||
return
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.error('pid is not positive: %d', pid)
|
||||
|
@ -179,3 +174,35 @@ def daemon_stop(pid_file):
|
|||
sys.exit(1)
|
||||
print('stopped')
|
||||
os.unlink(pid_file)
|
||||
|
||||
|
||||
def set_user(username):
|
||||
if username is None:
|
||||
return
|
||||
|
||||
import pwd
|
||||
import grp
|
||||
|
||||
try:
|
||||
pwrec = pwd.getpwnam(username)
|
||||
except KeyError:
|
||||
logging.error('user not found: %s' % username)
|
||||
raise
|
||||
user = pwrec[0]
|
||||
uid = pwrec[2]
|
||||
gid = pwrec[3]
|
||||
|
||||
cur_uid = os.getuid()
|
||||
if uid == cur_uid:
|
||||
return
|
||||
if cur_uid != 0:
|
||||
logging.error('can not set user as nonroot user')
|
||||
# will raise later
|
||||
|
||||
# inspired by supervisor
|
||||
if hasattr(os, 'setgroups'):
|
||||
groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]]
|
||||
groups.insert(0, gid)
|
||||
os.setgroups(groups)
|
||||
os.setgid(gid)
|
||||
os.setuid(uid)
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from shadowsocks.crypto import m2, rc4_md5, salsa20_ctr,\
|
||||
ctypes_openssl, ctypes_libsodium, table
|
||||
|
||||
|
||||
method_supported = {}
|
||||
method_supported.update(rc4_md5.ciphers)
|
||||
method_supported.update(salsa20_ctr.ciphers)
|
||||
method_supported.update(ctypes_openssl.ciphers)
|
||||
method_supported.update(ctypes_libsodium.ciphers)
|
||||
# let M2Crypto override ctypes_openssl
|
||||
method_supported.update(m2.ciphers)
|
||||
method_supported.update(table.ciphers)
|
||||
|
||||
|
||||
def random_string(length):
|
||||
try:
|
||||
import M2Crypto.Rand
|
||||
return M2Crypto.Rand.rand_bytes(length)
|
||||
except ImportError:
|
||||
return os.urandom(length)
|
||||
|
||||
|
||||
cached_keys = {}
|
||||
|
||||
|
||||
def try_cipher(key, method=None):
|
||||
Encryptor(key, method)
|
||||
|
||||
|
||||
def EVP_BytesToKey(password, key_len, iv_len):
|
||||
# equivalent to OpenSSL's EVP_BytesToKey() with count 1
|
||||
# so that we make the same key and iv as nodejs version
|
||||
if hasattr(password, 'encode'):
|
||||
password = password.encode('utf-8')
|
||||
cached_key = '%s-%d-%d' % (password, key_len, iv_len)
|
||||
r = cached_keys.get(cached_key, None)
|
||||
if r:
|
||||
return r
|
||||
m = []
|
||||
i = 0
|
||||
while len(b''.join(m)) < (key_len + iv_len):
|
||||
md5 = hashlib.md5()
|
||||
data = password
|
||||
if i > 0:
|
||||
data = m[i - 1] + password
|
||||
md5.update(data)
|
||||
m.append(md5.digest())
|
||||
i += 1
|
||||
ms = b''.join(m)
|
||||
key = ms[:key_len]
|
||||
iv = ms[key_len:key_len + iv_len]
|
||||
cached_keys[cached_key] = (key, iv)
|
||||
return key, iv
|
||||
|
||||
|
||||
class Encryptor(object):
|
||||
def __init__(self, key, method):
|
||||
self.key = key
|
||||
self.method = method
|
||||
self.iv = None
|
||||
self.iv_sent = False
|
||||
self.cipher_iv = b''
|
||||
self.decipher = None
|
||||
method = method.lower()
|
||||
self._method_info = self.get_method_info(method)
|
||||
if self._method_info:
|
||||
self.cipher = self.get_cipher(key, method, 1,
|
||||
random_string(self._method_info[1]))
|
||||
else:
|
||||
logging.error('method %s not supported' % method)
|
||||
sys.exit(1)
|
||||
|
||||
def get_method_info(self, method):
|
||||
method = method.lower()
|
||||
m = method_supported.get(method)
|
||||
return m
|
||||
|
||||
def iv_len(self):
|
||||
return len(self.cipher_iv)
|
||||
|
||||
def get_cipher(self, password, method, op, iv):
|
||||
if hasattr(password, 'encode'):
|
||||
password = password.encode('utf-8')
|
||||
m = self._method_info
|
||||
if m[0] > 0:
|
||||
key, iv_ = EVP_BytesToKey(password, m[0], m[1])
|
||||
else:
|
||||
# key_length == 0 indicates we should use the key directly
|
||||
key, iv = password, b''
|
||||
|
||||
iv = iv[:m[1]]
|
||||
if op == 1:
|
||||
# this iv is for cipher not decipher
|
||||
self.cipher_iv = iv[:m[1]]
|
||||
return m[2](method, key, iv, op)
|
||||
|
||||
def encrypt(self, buf):
|
||||
if len(buf) == 0:
|
||||
return buf
|
||||
if self.iv_sent:
|
||||
return self.cipher.update(buf)
|
||||
else:
|
||||
self.iv_sent = True
|
||||
return self.cipher_iv + self.cipher.update(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 = buf[:decipher_iv_len]
|
||||
self.decipher = self.get_cipher(self.key, self.method, 0,
|
||||
iv=decipher_iv)
|
||||
buf = buf[decipher_iv_len:]
|
||||
if len(buf) == 0:
|
||||
return buf
|
||||
return self.decipher.update(buf)
|
||||
|
||||
|
||||
def encrypt_all(password, method, op, data):
|
||||
result = []
|
||||
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))
|
||||
return b''.join(result)
|
||||
|
||||
|
||||
CIPHERS_TO_TEST = [
|
||||
b'aes-128-cfb',
|
||||
b'aes-256-cfb',
|
||||
b'rc4-md5',
|
||||
b'salsa20',
|
||||
b'chacha20',
|
||||
b'table',
|
||||
]
|
||||
|
||||
|
||||
def test_encryptor():
|
||||
from os import urandom
|
||||
plain = urandom(10240)
|
||||
for method in CIPHERS_TO_TEST:
|
||||
logging.warn(method)
|
||||
encryptor = Encryptor(b'key', method)
|
||||
decryptor = Encryptor(b'key', method)
|
||||
cipher = encryptor.encrypt(plain)
|
||||
plain2 = decryptor.decrypt(cipher)
|
||||
assert plain == plain2
|
||||
|
||||
|
||||
def test_encrypt_all():
|
||||
from os import urandom
|
||||
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)
|
||||
assert plain == plain2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_encrypt_all()
|
||||
test_encryptor()
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2013-2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
# from ssloop
|
||||
# https://github.com/clowwindy/ssloop
|
||||
|
@ -28,12 +22,16 @@ 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
|
||||
|
||||
from shadowsocks import shell
|
||||
|
||||
|
||||
__all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR',
|
||||
'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES']
|
||||
|
@ -55,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):
|
||||
|
@ -104,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):
|
||||
|
@ -133,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:
|
||||
|
@ -141,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:
|
||||
|
@ -149,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()
|
||||
|
@ -169,73 +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:
|
||||
logging.error(e)
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
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
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2012-2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -30,12 +24,12 @@ import logging
|
|||
import signal
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
|
||||
from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\
|
||||
asyncdns
|
||||
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns
|
||||
|
||||
|
||||
@shell.exception_handle(self_=False, exit_code=1)
|
||||
def main():
|
||||
utils.check_python()
|
||||
shell.check_python()
|
||||
|
||||
# fix py2exe
|
||||
if hasattr(sys, "frozen") and sys.frozen in \
|
||||
|
@ -43,38 +37,32 @@ def main():
|
|||
p = os.path.dirname(os.path.abspath(sys.executable))
|
||||
os.chdir(p)
|
||||
|
||||
config = utils.get_config(True)
|
||||
|
||||
config = shell.get_config(True)
|
||||
daemon.daemon_exec(config)
|
||||
|
||||
utils.print_shadowsocks()
|
||||
logging.info("starting local at %s:%d" %
|
||||
(config['local_address'], config['local_port']))
|
||||
|
||||
encrypt.try_cipher(config['password'], config['method'])
|
||||
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)
|
||||
|
||||
try:
|
||||
logging.info("starting local at %s:%d" %
|
||||
(config['local_address'], config['local_port']))
|
||||
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)
|
||||
|
||||
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 int_handler(signum, _):
|
||||
sys.exit(1)
|
||||
signal.signal(signal.SIGINT, int_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)
|
||||
loop.run()
|
||||
except (KeyboardInterrupt, IOError, OSError) as e:
|
||||
logging.error(e)
|
||||
if config['verbose']:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
os._exit(1)
|
||||
daemon.set_user(config.get('user', None))
|
||||
loop.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -47,6 +41,7 @@ class LRUCache(collections.MutableMapping):
|
|||
self._time_to_keys = collections.defaultdict(list)
|
||||
self._keys_to_last_time = {}
|
||||
self._last_visits = collections.deque()
|
||||
self._closed_values = set()
|
||||
self.update(dict(*args, **kwargs)) # use the free update to set keys
|
||||
|
||||
def __getitem__(self, key):
|
||||
|
@ -84,21 +79,21 @@ 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:
|
||||
value = self._store[key]
|
||||
self.close_callback(value)
|
||||
self._last_visits.popleft()
|
||||
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:
|
||||
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)
|
||||
del self._store[key]
|
||||
del self._keys_to_last_time[key]
|
||||
c += 1
|
||||
del self._time_to_keys[least]
|
||||
if c:
|
||||
self._closed_values.clear()
|
||||
logging.debug('%d keys swept' % c)
|
||||
|
||||
|
||||
|
@ -132,5 +127,22 @@ def test():
|
|||
assert 'a' not in c
|
||||
assert 'b' not in c
|
||||
|
||||
global close_cb_called
|
||||
close_cb_called = False
|
||||
|
||||
def close_cb(t):
|
||||
global close_cb_called
|
||||
assert not close_cb_called
|
||||
close_cb_called = True
|
||||
|
||||
c = LRUCache(timeout=0.1, close_callback=close_cb)
|
||||
c['s'] = 1
|
||||
c['t'] = 1
|
||||
c['s']
|
||||
time.sleep(0.1)
|
||||
c['s']
|
||||
time.sleep(0.3)
|
||||
c.sweep()
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
|
293
shadowsocks/manager.py
Normal file
293
shadowsocks/manager.py
Normal 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()
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -30,19 +24,17 @@ import logging
|
|||
import signal
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
|
||||
from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\
|
||||
asyncdns
|
||||
from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \
|
||||
asyncdns, manager
|
||||
|
||||
|
||||
def main():
|
||||
utils.check_python()
|
||||
shell.check_python()
|
||||
|
||||
config = utils.get_config(False)
|
||||
config = shell.get_config(False)
|
||||
|
||||
daemon.daemon_exec(config)
|
||||
|
||||
utils.print_shadowsocks()
|
||||
|
||||
if config['port_password']:
|
||||
if config['password']:
|
||||
logging.warn('warning: port_password should not be used with '
|
||||
|
@ -57,11 +49,23 @@ def main():
|
|||
else:
|
||||
config['port_password'][str(server_port)] = config['password']
|
||||
|
||||
encrypt.try_cipher(config['password'], config['method'])
|
||||
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
|
||||
|
@ -77,17 +81,21 @@ def main():
|
|||
tcp_servers + udp_servers))
|
||||
signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM),
|
||||
child_handler)
|
||||
|
||||
def int_handler(signum, _):
|
||||
sys.exit(1)
|
||||
signal.signal(signal.SIGINT, int_handler)
|
||||
|
||||
try:
|
||||
loop = eventloop.EventLoop()
|
||||
dns_resolver.add_to_loop(loop)
|
||||
list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers))
|
||||
|
||||
daemon.set_user(config.get('user', None))
|
||||
loop.run()
|
||||
except (KeyboardInterrupt, IOError, OSError) as e:
|
||||
logging.error(e)
|
||||
if config['verbose']:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
os._exit(1)
|
||||
except Exception as e:
|
||||
shell.print_exception(e)
|
||||
sys.exit(1)
|
||||
|
||||
if int(config['workers']) > 1:
|
||||
if os.name == 'posix':
|
||||
|
@ -113,6 +121,7 @@ def main():
|
|||
sys.exit()
|
||||
signal.signal(signal.SIGTERM, handler)
|
||||
signal.signal(signal.SIGQUIT, handler)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
# master
|
||||
for a_tcp_server in tcp_servers:
|
||||
|
|
509
shadowsocks/shell.py
Normal file
509
shadowsocks/shell.py
Normal file
|
@ -0,0 +1,509 @@
|
|||
#!/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 os
|
||||
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 cryptor
|
||||
|
||||
|
||||
VERBOSE_LEVEL = 5
|
||||
|
||||
verbose = 0
|
||||
|
||||
|
||||
def check_python():
|
||||
info = sys.version_info
|
||||
if info[0] == 2 and not info[1] >= 6:
|
||||
print('Python 2.6+ required')
|
||||
sys.exit(1)
|
||||
elif info[0] == 3 and not info[1] >= 3:
|
||||
print('Python 3.3+ required')
|
||||
sys.exit(1)
|
||||
elif info[0] not in [2, 3]:
|
||||
print('Python version not supported')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def print_exception(e):
|
||||
global verbose
|
||||
logging.error(e)
|
||||
if verbose > 0:
|
||||
import traceback
|
||||
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:
|
||||
import pkg_resources
|
||||
version = pkg_resources.get_distribution('shadowsocks').version
|
||||
except Exception:
|
||||
pass
|
||||
print('Shadowsocks %s' % version)
|
||||
|
||||
|
||||
def find_config():
|
||||
config_path = 'config.json'
|
||||
if os.path.exists(config_path):
|
||||
return config_path
|
||||
config_path = os.path.join(os.path.dirname(__file__), '../', 'config.json')
|
||||
if os.path.exists(config_path):
|
||||
return config_path
|
||||
return None
|
||||
|
||||
|
||||
def check_config(config, is_local):
|
||||
if config.get('daemon', None) == 'stop':
|
||||
# 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('manager_address'):
|
||||
logging.error('password or port_password not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
if 'local_port' in config:
|
||||
config['local_port'] = int(config['local_port'])
|
||||
|
||||
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']:
|
||||
logging.warn('warning: server set to listen on %s:%s, are you sure?' %
|
||||
(to_str(config['server']), config['server_port']))
|
||||
if (config.get('method', '') or '').lower() == 'table':
|
||||
logging.warn('warning: table is not safe; please use a safer cipher, '
|
||||
'like AES-256-CFB')
|
||||
if (config.get('method', '') or '').lower() == 'rc4':
|
||||
logging.warn('warning: RC4 is not safe; please use a safer cipher, '
|
||||
'like AES-256-CFB')
|
||||
if config.get('timeout', 300) < 100:
|
||||
logging.warn('warning: your timeout %d seems too short' %
|
||||
int(config.get('timeout')))
|
||||
if config.get('timeout', 300) > 600:
|
||||
logging.warn('warning: your timeout %d seems too long' %
|
||||
int(config.get('timeout')))
|
||||
if config.get('password') in [b'mypassword']:
|
||||
logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your '
|
||||
'config.json!')
|
||||
sys.exit(1)
|
||||
if config.get('user', None) is not None:
|
||||
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'])
|
||||
|
||||
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):
|
||||
global verbose
|
||||
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(levelname)-s: %(message)s')
|
||||
if is_local:
|
||||
shortopts = 'hd:s:b:p:k:l:m:c:t:vqa'
|
||||
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'user=',
|
||||
'libopenssl=', 'libmbedtls=', 'libsodium=', 'version']
|
||||
else:
|
||||
shortopts = 'hd:s:p:k:m:c:t:vqa'
|
||||
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=',
|
||||
'forbidden-ip=', 'user=', 'manager-address=', 'version',
|
||||
'libopenssl=', 'libmbedtls=', 'libsodium=', 'prefer-ipv6']
|
||||
try:
|
||||
config_path = find_config()
|
||||
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
|
||||
for key, value in optlist:
|
||||
if key == '-c':
|
||||
config_path = value
|
||||
|
||||
if config_path:
|
||||
logging.info('loading config from %s' % config_path)
|
||||
with open(config_path, 'rb') as f:
|
||||
try:
|
||||
config = parse_json_in_str(f.read().decode('utf8'))
|
||||
except ValueError as e:
|
||||
logging.error('found an error in config.json: %s',
|
||||
e.message)
|
||||
sys.exit(1)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
v_count = 0
|
||||
for key, value in optlist:
|
||||
if key == '-p':
|
||||
config['server_port'] = int(value)
|
||||
elif key == '-k':
|
||||
config['password'] = to_bytes(value)
|
||||
elif key == '-l':
|
||||
config['local_port'] = int(value)
|
||||
elif key == '-s':
|
||||
config['server'] = to_str(value)
|
||||
elif key == '-m':
|
||||
config['method'] = to_str(value)
|
||||
elif key == '-b':
|
||||
config['local_address'] = to_str(value)
|
||||
elif key == '-v':
|
||||
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':
|
||||
config['forbidden_ip'] = to_str(value).split(',')
|
||||
elif key in ('-h', '--help'):
|
||||
if is_local:
|
||||
print_local_help()
|
||||
else:
|
||||
print_server_help()
|
||||
sys.exit(0)
|
||||
elif key == '--version':
|
||||
print_shadowsocks()
|
||||
sys.exit(0)
|
||||
elif key == '-d':
|
||||
config['daemon'] = to_str(value)
|
||||
elif key == '--pid-file':
|
||||
config['pid-file'] = to_str(value)
|
||||
elif key == '--log-file':
|
||||
config['log-file'] = to_str(value)
|
||||
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)
|
||||
sys.exit(2)
|
||||
|
||||
if not config:
|
||||
logging.error('config not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
config['password'] = to_bytes(config.get('password', b''))
|
||||
config['method'] = to_str(config.get('method', 'aes-256-cfb'))
|
||||
config['port_password'] = config.get('port_password', None)
|
||||
config['timeout'] = int(config.get('timeout', 300))
|
||||
config['fast_open'] = config.get('fast_open', False)
|
||||
config['workers'] = config.get('workers', 1)
|
||||
config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid')
|
||||
config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log')
|
||||
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)
|
||||
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')
|
||||
if config['verbose'] >= 2:
|
||||
level = VERBOSE_LEVEL
|
||||
elif config['verbose'] == 1:
|
||||
level = logging.DEBUG
|
||||
elif config['verbose'] == -1:
|
||||
level = logging.WARN
|
||||
elif config['verbose'] <= -2:
|
||||
level = logging.ERROR
|
||||
else:
|
||||
level = logging.INFO
|
||||
verbose = config['verbose']
|
||||
logging.basicConfig(level=level,
|
||||
format='%(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
check_config(config, is_local)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def print_help(is_local):
|
||||
if is_local:
|
||||
print_local_help()
|
||||
else:
|
||||
print_server_help()
|
||||
|
||||
|
||||
def print_local_help():
|
||||
print('''usage: sslocal [OPTION]...
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
You can supply configurations via either config file or command line arguments.
|
||||
|
||||
Proxy options:
|
||||
-c CONFIG path to config file
|
||||
-s SERVER_ADDR server address
|
||||
-p SERVER_PORT server port, default: 8388
|
||||
-b LOCAL_ADDR local binding address, default: 127.0.0.1
|
||||
-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
|
||||
-v, -vv verbose mode
|
||||
-q, -qq quiet mode, only show warnings/errors
|
||||
--version show version information
|
||||
|
||||
Online help: <https://github.com/shadowsocks/shadowsocks>
|
||||
''')
|
||||
|
||||
|
||||
def print_server_help():
|
||||
print('''usage: ssserver [OPTION]...
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
You can supply configurations via either config file or command line arguments.
|
||||
|
||||
Proxy options:
|
||||
-c CONFIG path to config file
|
||||
-s SERVER_ADDR server address, default: 0.0.0.0
|
||||
-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
|
||||
--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
|
||||
-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
|
||||
-v, -vv verbose mode
|
||||
-q, -qq quiet mode, only show warnings/errors
|
||||
--version show version information
|
||||
|
||||
Online help: <https://github.com/shadowsocks/shadowsocks>
|
||||
''')
|
||||
|
||||
|
||||
def _decode_list(data):
|
||||
rv = []
|
||||
for item in data:
|
||||
if hasattr(item, 'encode'):
|
||||
item = item.encode('utf-8')
|
||||
elif isinstance(item, list):
|
||||
item = _decode_list(item)
|
||||
elif isinstance(item, dict):
|
||||
item = _decode_dict(item)
|
||||
rv.append(item)
|
||||
return rv
|
||||
|
||||
|
||||
def _decode_dict(data):
|
||||
rv = {}
|
||||
for key, value in data.items():
|
||||
if hasattr(value, 'encode'):
|
||||
value = value.encode('utf-8')
|
||||
elif isinstance(value, list):
|
||||
value = _decode_list(value)
|
||||
elif isinstance(value, dict):
|
||||
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)
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -32,49 +26,43 @@ import logging
|
|||
import traceback
|
||||
import random
|
||||
|
||||
from shadowsocks import encrypt, eventloop, utils, 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 CMD defination
|
||||
# SOCKS METHOD definition
|
||||
METHOD_NOAUTH = 0
|
||||
|
||||
# SOCKS command definition
|
||||
CMD_CONNECT = 1
|
||||
CMD_BIND = 2
|
||||
CMD_UDP_ASSOCIATE = 3
|
||||
|
||||
# TCP Relay can be either sslocal or ssserver
|
||||
# for sslocal it is called is_local=True
|
||||
|
||||
# for each opening port, we have a TCP Relay
|
||||
|
||||
# for each connection, we have a TCP Relay Handler to handle the connection
|
||||
|
||||
# for each handler, we have 2 sockets:
|
||||
# local: connected to the client
|
||||
# remote: connected to remote server
|
||||
|
||||
# for each handler, we have 2 streams:
|
||||
# upstream: from client to server direction
|
||||
# read local and write to remote
|
||||
# downstream: from server to client direction
|
||||
# read remote and write to local
|
||||
|
||||
# for each handler, it could be at one of several stages:
|
||||
|
||||
# sslocal:
|
||||
# stage 0 SOCKS hello received from local, send hello to local
|
||||
# as sslocal:
|
||||
# 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
|
||||
# stage 4 still connecting, more data from local received
|
||||
# stage 5 remote connected, piping local and remote
|
||||
|
||||
# ssserver:
|
||||
# as ssserver:
|
||||
# stage 0 just jump to stage 1
|
||||
# stage 1 addr received from local, query DNS for remote
|
||||
# stage 3 DNS resolved, connect to remote
|
||||
|
@ -89,20 +77,38 @@ STAGE_CONNECTING = 4
|
|||
STAGE_STREAM = 5
|
||||
STAGE_DESTROYED = -1
|
||||
|
||||
# stream direction
|
||||
# for each handler, we have 2 stream directions:
|
||||
# upstream: from client to server direction
|
||||
# read local and write to remote
|
||||
# downstream: from server to client direction
|
||||
# read remote and write to local
|
||||
|
||||
STREAM_UP = 0
|
||||
STREAM_DOWN = 1
|
||||
|
||||
# stream wait status, indicating it's waiting for reading, etc
|
||||
# for each stream, it's waiting for reading, or writing, or both
|
||||
WAIT_STATUS_INIT = 0
|
||||
WAIT_STATUS_READING = 1
|
||||
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
|
||||
|
@ -112,22 +118,39 @@ 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 = []
|
||||
self._upstream_status = WAIT_STATUS_READING
|
||||
self._downstream_status = WAIT_STATUS_INIT
|
||||
self._client_address = local_sock.getpeername()[:2]
|
||||
self._remote_address = 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()
|
||||
|
||||
|
@ -145,14 +168,15 @@ class TCPRelayHandler(object):
|
|||
server_port = self._config['server_port']
|
||||
if type(server_port) == list:
|
||||
server_port = random.choice(server_port)
|
||||
if type(server) == list:
|
||||
server = random.choice(server)
|
||||
logging.debug('chosen server: %s:%d', server, server_port)
|
||||
# TODO support multiple server IP
|
||||
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
|
||||
|
@ -203,9 +229,7 @@ class TCPRelayHandler(object):
|
|||
errno.EWOULDBLOCK):
|
||||
uncomplete = True
|
||||
else:
|
||||
logging.error(e)
|
||||
if self._config['verbose']:
|
||||
traceback.print_exc()
|
||||
shell.print_exception(e)
|
||||
self.destroy()
|
||||
return False
|
||||
if uncomplete:
|
||||
|
@ -227,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:
|
||||
|
@ -240,34 +272,41 @@ 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)
|
||||
data = b''.join(self._data_to_write_to_local)
|
||||
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_local = [data]
|
||||
self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING)
|
||||
self._data_to_write_to_remote = [data]
|
||||
else:
|
||||
self._data_to_write_to_local = []
|
||||
self._update_stream(STREAM_UP, WAIT_STATUS_READING)
|
||||
self._stage = STAGE_STREAM
|
||||
self._data_to_write_to_remote = []
|
||||
self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING)
|
||||
except (OSError, IOError) as e:
|
||||
if eventloop.errno_from_exception(e) == errno.EINPROGRESS:
|
||||
# in this case data is not sent at all
|
||||
self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING)
|
||||
elif eventloop.errno_from_exception(e) == errno.ENOTCONN:
|
||||
logging.error('fast open not supported on this OS')
|
||||
self._config['fast_open'] = False
|
||||
self.destroy()
|
||||
else:
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
if self._config['verbose']:
|
||||
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')
|
||||
|
@ -291,45 +330,77 @@ 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' % (common.to_str(remote_addr),
|
||||
remote_port))
|
||||
self._remote_address = (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:
|
||||
logging.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,
|
||||
socket.SOL_TCP)
|
||||
if len(addrs) == 0:
|
||||
raise Exception("getaddrinfo failed for %s:%d" % (ip, port))
|
||||
raise Exception("getaddrinfo failed for %s:%d" % (ip, port))
|
||||
af, socktype, proto, canonname, sa = addrs[0]
|
||||
if self._forbidden_iplist:
|
||||
if common.to_str(sa[0]) in self._forbidden_iplist:
|
||||
raise Exception('IP %s is in forbidden list, reject' %
|
||||
common.to_str(sa[0]))
|
||||
remote_sock = socket.socket(af, socktype, proto)
|
||||
self._remote_sock = remote_sock
|
||||
self._fd_to_handlers[remote_sock.fileno()] = self
|
||||
|
@ -337,62 +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:
|
||||
logging.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]
|
||||
|
||||
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)
|
||||
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 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 (OSError, IOError) as e:
|
||||
logging.error(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):
|
||||
|
@ -400,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 \
|
||||
|
@ -422,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):
|
||||
|
@ -433,14 +607,15 @@ 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:
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
if self._config['verbose']:
|
||||
traceback.print_exc()
|
||||
# TODO use logging when debug completed
|
||||
|
@ -477,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:
|
||||
|
@ -543,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
|
||||
|
@ -584,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:
|
||||
|
@ -591,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)
|
||||
|
@ -603,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
|
||||
|
@ -623,7 +803,7 @@ class TCPRelay(object):
|
|||
# we just need a sorted last_activity queue and it's faster than heapq
|
||||
# in fact we can do O(1) insertion/remove so we invent our own
|
||||
if self._timeouts:
|
||||
logging.log(utils.VERBOSE_LEVEL, 'sweeping timeouts')
|
||||
logging.log(shell.VERBOSE_LEVEL, 'sweeping timeouts')
|
||||
now = time.time()
|
||||
length = len(self._timeouts)
|
||||
pos = self._timeout_offset
|
||||
|
@ -652,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(utils.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:
|
||||
logging.error(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
74
shadowsocks/tunnel.py
Executable 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()
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
# SOCKS5 UDP Request
|
||||
# +----+------+------+----------+----------+----------+
|
||||
|
@ -68,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
|
||||
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']
|
||||
|
@ -99,38 +95,48 @@ 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()
|
||||
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']
|
||||
server_port = self._config['server_port']
|
||||
if type(server_port) == list:
|
||||
server_port = random.choice(server_port)
|
||||
if type(server) == list:
|
||||
server = random.choice(server)
|
||||
logging.debug('chosen server: %s:%d', server, server_port)
|
||||
# TODO support multiple server IP
|
||||
return server, server_port
|
||||
|
||||
def _close_client(self, client):
|
||||
|
@ -145,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
|
||||
|
@ -164,32 +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]
|
||||
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:
|
||||
|
@ -203,75 +261,105 @@ class UDPRelay(object):
|
|||
if err in (errno.EINPROGRESS, errno.EAGAIN):
|
||||
pass
|
||||
else:
|
||||
logging.error(e)
|
||||
shell.print_exception(e)
|
||||
|
||||
def _handle_client(self, sock):
|
||||
data, r_addr = sock.recvfrom(BUF_SIZE)
|
||||
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()
|
||||
|
|
|
@ -1,324 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import getopt
|
||||
import logging
|
||||
from shadowsocks.common import to_bytes
|
||||
|
||||
|
||||
VERBOSE_LEVEL = 5
|
||||
|
||||
|
||||
def check_python():
|
||||
info = sys.version_info
|
||||
if info[0] == 2 and not info[1] >= 6:
|
||||
print('Python 2.6+ required')
|
||||
sys.exit(1)
|
||||
elif info[0] == 3 and not info[1] >= 3:
|
||||
print('Python 3.3+ required')
|
||||
sys.exit(1)
|
||||
elif info[0] not in [2, 3]:
|
||||
print('Python version not supported')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def print_shadowsocks():
|
||||
version = ''
|
||||
try:
|
||||
import pkg_resources
|
||||
version = pkg_resources.get_distribution('shadowsocks').version
|
||||
except Exception:
|
||||
pass
|
||||
print('shadowsocks %s' % version)
|
||||
|
||||
|
||||
def find_config():
|
||||
config_path = 'config.json'
|
||||
if os.path.exists(config_path):
|
||||
return config_path
|
||||
config_path = os.path.join(os.path.dirname(__file__), '../', 'config.json')
|
||||
if os.path.exists(config_path):
|
||||
return config_path
|
||||
return None
|
||||
|
||||
|
||||
def check_config(config):
|
||||
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 [b'127.0.0.1', b'localhost']:
|
||||
logging.warn('warning: server set to listen on %s:%s, are you sure?' %
|
||||
(config['server'], config['server_port']))
|
||||
if (config.get('method', '') or '').lower() == b'table':
|
||||
logging.warn('warning: table is not safe; please use a safer cipher, '
|
||||
'like AES-256-CFB')
|
||||
if (config.get('method', '') or '').lower() == b'rc4':
|
||||
logging.warn('warning: RC4 is not safe; please use a safer cipher, '
|
||||
'like AES-256-CFB')
|
||||
if config.get('timeout', 300) < 100:
|
||||
logging.warn('warning: your timeout %d seems too short' %
|
||||
int(config.get('timeout')))
|
||||
if config.get('timeout', 300) > 600:
|
||||
logging.warn('warning: your timeout %d seems too long' %
|
||||
int(config.get('timeout')))
|
||||
if config.get('password') in [b'mypassword']:
|
||||
logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your '
|
||||
'config.json!')
|
||||
exit(1)
|
||||
|
||||
|
||||
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'
|
||||
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=']
|
||||
else:
|
||||
shortopts = 'hd:s:p:k:m:c:t:vq'
|
||||
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=']
|
||||
try:
|
||||
config_path = find_config()
|
||||
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
|
||||
for key, value in optlist:
|
||||
if key == '-c':
|
||||
config_path = value
|
||||
|
||||
if config_path:
|
||||
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)
|
||||
except ValueError as e:
|
||||
logging.error('found an error in config.json: %s',
|
||||
e.message)
|
||||
sys.exit(1)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
|
||||
v_count = 0
|
||||
for key, value in optlist:
|
||||
if key == '-p':
|
||||
config['server_port'] = int(value)
|
||||
elif key == '-k':
|
||||
config['password'] = to_bytes(value)
|
||||
elif key == '-l':
|
||||
config['local_port'] = int(value)
|
||||
elif key == '-s':
|
||||
config['server'] = to_bytes(value)
|
||||
elif key == '-m':
|
||||
config['method'] = to_bytes(value)
|
||||
elif key == '-b':
|
||||
config['local_address'] = to_bytes(value)
|
||||
elif key == '-v':
|
||||
v_count += 1
|
||||
# '-vv' turns on more verbose mode
|
||||
config['verbose'] = v_count
|
||||
elif key == '-t':
|
||||
config['timeout'] = int(value)
|
||||
elif key == '--fast-open':
|
||||
config['fast_open'] = True
|
||||
elif key == '--workers':
|
||||
config['workers'] = int(value)
|
||||
elif key in ('-h', '--help'):
|
||||
if is_local:
|
||||
print_local_help()
|
||||
else:
|
||||
print_server_help()
|
||||
sys.exit(0)
|
||||
elif key == '-d':
|
||||
config['daemon'] = value
|
||||
elif key == '--pid-file':
|
||||
config['pid-file'] = value
|
||||
elif key == '--log-file':
|
||||
config['log-file'] = value
|
||||
elif key == '-q':
|
||||
v_count -= 1
|
||||
config['verbose'] = v_count
|
||||
except getopt.GetoptError as e:
|
||||
print(e, file=sys.stderr)
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
if not config:
|
||||
logging.error('config not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
config['password'] = config.get('password', '')
|
||||
config['method'] = config.get('method', 'aes-256-cfb')
|
||||
config['port_password'] = config.get('port_password', None)
|
||||
config['timeout'] = int(config.get('timeout', 300))
|
||||
config['fast_open'] = config.get('fast_open', False)
|
||||
config['workers'] = config.get('workers', 1)
|
||||
config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid')
|
||||
config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log')
|
||||
config['workers'] = config.get('workers', 1)
|
||||
config['verbose'] = config.get('verbose', False)
|
||||
config['local_address'] = 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'] = config.get('server', '0.0.0.0')
|
||||
config['server_port'] = config.get('server_port', 8388)
|
||||
|
||||
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):
|
||||
logging.error('password or port_password not specified')
|
||||
print_help(is_local)
|
||||
sys.exit(2)
|
||||
|
||||
if 'local_port' in config:
|
||||
config['local_port'] = int(config['local_port'])
|
||||
|
||||
if 'server_port' in config and type(config['server_port']) != list:
|
||||
config['server_port'] = int(config['server_port'])
|
||||
|
||||
logging.getLogger('').handlers = []
|
||||
logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE')
|
||||
if config['verbose'] >= 2:
|
||||
level = VERBOSE_LEVEL
|
||||
elif config['verbose'] == 1:
|
||||
level = logging.DEBUG
|
||||
elif config['verbose'] == -1:
|
||||
level = logging.WARN
|
||||
elif config['verbose'] <= -2:
|
||||
level = logging.ERROR
|
||||
else:
|
||||
level = logging.INFO
|
||||
logging.basicConfig(level=level,
|
||||
format='%(asctime)s %(levelname)-8s %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
check_config(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def print_help(is_local):
|
||||
if is_local:
|
||||
print_local_help()
|
||||
else:
|
||||
print_server_help()
|
||||
|
||||
|
||||
def print_local_help():
|
||||
print('''usage: sslocal [-h] -s SERVER_ADDR [-p SERVER_PORT]
|
||||
[-b LOCAL_ADDR] [-l LOCAL_PORT] -k PASSWORD [-m METHOD]
|
||||
[-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] -[d] [-q]
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
You can supply configurations via either config file or command line arguments.
|
||||
|
||||
Proxy options:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG path to config file
|
||||
-s SERVER_ADDR server address
|
||||
-p SERVER_PORT server port, default: 8388
|
||||
-b LOCAL_ADDR local binding address, default: 127.0.0.1
|
||||
-l LOCAL_PORT local port, default: 1080
|
||||
-k PASSWORD password
|
||||
-m METHOD encryption method, default: aes-256-cfb
|
||||
-t TIMEOUT timeout in seconds, default: 300
|
||||
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
||||
|
||||
General options:
|
||||
-d start/stop/restart daemon mode
|
||||
--pid-file PID_FILE pid file for daemon mode
|
||||
--log-file LOG_FILE log file for daemon mode
|
||||
-v, -vv verbose mode
|
||||
-q, -qq quiet mode, only show warnings/errors
|
||||
|
||||
Online help: <https://github.com/clowwindy/shadowsocks>
|
||||
''')
|
||||
|
||||
|
||||
def print_server_help():
|
||||
print('''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD
|
||||
-m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open]
|
||||
[--workers WORKERS] [-v] [-d start] [-q]
|
||||
A fast tunnel proxy that helps you bypass firewalls.
|
||||
|
||||
You can supply configurations via either config file or command line arguments.
|
||||
|
||||
Proxy options:
|
||||
-h, --help show this help message and exit
|
||||
-c CONFIG path to config file
|
||||
-s SERVER_ADDR server address, default: 0.0.0.0
|
||||
-p SERVER_PORT server port, default: 8388
|
||||
-k PASSWORD password
|
||||
-m METHOD encryption method, default: aes-256-cfb
|
||||
-t TIMEOUT timeout in seconds, default: 300
|
||||
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
|
||||
--workers WORKERS number of workers, available on Unix/Linux
|
||||
|
||||
General options:
|
||||
-d start/stop/restart daemon mode
|
||||
--pid-file PID_FILE pid file for daemon mode
|
||||
--log-file LOG_FILE log file for daemon mode
|
||||
-v, -vv verbose mode
|
||||
-q, -qq quiet mode, only show warnings/errors
|
||||
|
||||
Online help: <https://github.com/clowwindy/shadowsocks>
|
||||
''')
|
||||
|
||||
|
||||
def _decode_list(data):
|
||||
rv = []
|
||||
for item in data:
|
||||
if hasattr(item, 'encode'):
|
||||
item = item.encode('utf-8')
|
||||
elif isinstance(item, list):
|
||||
item = _decode_list(item)
|
||||
elif isinstance(item, dict):
|
||||
item = _decode_dict(item)
|
||||
rv.append(item)
|
||||
return rv
|
||||
|
||||
|
||||
def _decode_dict(data):
|
||||
rv = {}
|
||||
for key, value in data.items():
|
||||
if hasattr(value, 'encode'):
|
||||
value = value.encode('utf-8')
|
||||
elif isinstance(value, list):
|
||||
value = _decode_list(value)
|
||||
elif isinstance(value, dict):
|
||||
value = _decode_dict(value)
|
||||
rv[key] = value
|
||||
return rv
|
23
snapcraft.yaml
Normal file
23
snapcraft.yaml
Normal 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
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
10
tests/aes-gcm.json
Normal 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
11
tests/aes-ocb.json
Normal 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
10
tests/aes-ofb.json
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
148
tests/assert.sh
Normal file
148
tests/assert.sh
Normal file
|
@ -0,0 +1,148 @@
|
|||
#!/bin/bash
|
||||
# assert.sh 1.0 - bash unit testing framework
|
||||
# Copyright (C) 2009, 2010, 2011, 2012 Robert Lehmann
|
||||
#
|
||||
# http://github.com/lehmannro/assert.sh
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
export DISCOVERONLY=${DISCOVERONLY:-}
|
||||
export DEBUG=${DEBUG:-}
|
||||
export STOP=${STOP:-}
|
||||
export INVARIANT=${INVARIANT:-}
|
||||
export CONTINUE=${CONTINUE:-}
|
||||
|
||||
args="$(getopt -n "$0" -l \
|
||||
verbose,help,stop,discover,invariant,continue vhxdic $*)" \
|
||||
|| exit -1
|
||||
for arg in $args; do
|
||||
case "$arg" in
|
||||
-h)
|
||||
echo "$0 [-vxidc]" \
|
||||
"[--verbose] [--stop] [--invariant] [--discover] [--continue]"
|
||||
echo "`sed 's/./ /g' <<< "$0"` [-h] [--help]"
|
||||
exit 0;;
|
||||
--help)
|
||||
cat <<EOF
|
||||
Usage: $0 [options]
|
||||
Language-agnostic unit tests for subprocesses.
|
||||
|
||||
Options:
|
||||
-v, --verbose generate output for every individual test case
|
||||
-x, --stop stop running tests after the first failure
|
||||
-i, --invariant do not measure timings to remain invariant between runs
|
||||
-d, --discover collect test suites only, do not run any tests
|
||||
-c, --continue do not modify exit code to test suite status
|
||||
-h show brief usage information and exit
|
||||
--help show this help message and exit
|
||||
EOF
|
||||
exit 0;;
|
||||
-v|--verbose)
|
||||
DEBUG=1;;
|
||||
-x|--stop)
|
||||
STOP=1;;
|
||||
-i|--invariant)
|
||||
INVARIANT=1;;
|
||||
-d|--discover)
|
||||
DISCOVERONLY=1;;
|
||||
-c|--continue)
|
||||
CONTINUE=1;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf -v _indent "\n\t" # local format helper
|
||||
|
||||
_assert_reset() {
|
||||
tests_ran=0
|
||||
tests_failed=0
|
||||
tests_errors=()
|
||||
tests_starttime="$(date +%s.%N)" # seconds_since_epoch.nanoseconds
|
||||
}
|
||||
|
||||
assert_end() {
|
||||
# assert_end [suite ..]
|
||||
tests_endtime="$(date +%s.%N)"
|
||||
tests="$tests_ran ${*:+$* }tests"
|
||||
[[ -n "$DISCOVERONLY" ]] && echo "collected $tests." && _assert_reset && return
|
||||
[[ -n "$DEBUG" ]] && echo
|
||||
[[ -z "$INVARIANT" ]] && report_time=" in $(bc \
|
||||
<<< "${tests_endtime%.N} - ${tests_starttime%.N}" \
|
||||
| sed -e 's/\.\([0-9]\{0,3\}\)[0-9]*/.\1/' -e 's/^\./0./')s" \
|
||||
|| report_time=
|
||||
|
||||
if [[ "$tests_failed" -eq 0 ]]; then
|
||||
echo "all $tests passed$report_time."
|
||||
else
|
||||
for error in "${tests_errors[@]}"; do echo "$error"; done
|
||||
echo "$tests_failed of $tests failed$report_time."
|
||||
fi
|
||||
tests_failed_previous=$tests_failed
|
||||
[[ $tests_failed -gt 0 ]] && tests_suite_status=1
|
||||
_assert_reset
|
||||
return $tests_failed_previous
|
||||
}
|
||||
|
||||
assert() {
|
||||
# assert <command> <expected stdout> [stdin]
|
||||
(( tests_ran++ )) || :
|
||||
[[ -n "$DISCOVERONLY" ]] && return || true
|
||||
# printf required for formatting
|
||||
printf -v expected "x${2:-}" # x required to overwrite older results
|
||||
result="$(eval 2>/dev/null $1 <<< ${3:-})" || true
|
||||
# Note: $expected is already decorated
|
||||
if [[ "x$result" == "$expected" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo -n . || true
|
||||
return
|
||||
fi
|
||||
result="$(sed -e :a -e '$!N;s/\n/\\n/;ta' <<< "$result")"
|
||||
[[ -z "$result" ]] && result="nothing" || result="\"$result\""
|
||||
[[ -z "$2" ]] && expected="nothing" || expected="\"$2\""
|
||||
_assert_fail "expected $expected${_indent}got $result" "$1" "$3"
|
||||
}
|
||||
|
||||
assert_raises() {
|
||||
# assert_raises <command> <expected code> [stdin]
|
||||
(( tests_ran++ )) || :
|
||||
[[ -n "$DISCOVERONLY" ]] && return || true
|
||||
status=0
|
||||
(eval $1 <<< ${3:-}) > /dev/null 2>&1 || status=$?
|
||||
expected=${2:-0}
|
||||
if [[ "$status" -eq "$expected" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo -n . || true
|
||||
return
|
||||
fi
|
||||
_assert_fail "program terminated with code $status instead of $expected" "$1" "$3"
|
||||
}
|
||||
|
||||
_assert_fail() {
|
||||
# _assert_fail <failure> <command> <stdin>
|
||||
[[ -n "$DEBUG" ]] && echo -n X
|
||||
report="test #$tests_ran \"$2${3:+ <<< $3}\" failed:${_indent}$1"
|
||||
if [[ -n "$STOP" ]]; then
|
||||
[[ -n "$DEBUG" ]] && echo
|
||||
echo "$report"
|
||||
exit 1
|
||||
fi
|
||||
tests_errors[$tests_failed]="$report"
|
||||
(( tests_failed++ )) || :
|
||||
}
|
||||
|
||||
_assert_reset
|
||||
: ${tests_suite_status:=0} # remember if any of the tests failed so far
|
||||
_assert_cleanup() {
|
||||
local status=$?
|
||||
# modify exit code if it's not already non-zero
|
||||
[[ $status -eq 0 && -z $CONTINUE ]] && exit $tests_suite_status
|
||||
}
|
||||
trap _assert_cleanup EXIT
|
10
tests/camellia.json
Normal file
10
tests/camellia.json
Normal 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
|
||||
}
|
10
tests/chacha20-ietf-poly1305.json
Normal file
10
tests/chacha20-ietf-poly1305.json
Normal 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
10
tests/chacha20-ietf.json
Normal 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
|
||||
}
|
10
tests/chacha20-poly1305.json
Normal file
10
tests/chacha20-poly1305.json
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
10
tests/client-multi-server-ip.json
Normal file
10
tests/client-multi-server-ip.json
Normal file
|
@ -0,0 +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
|
||||
}
|
45
tests/coverage_server.py
Normal file
45
tests/coverage_server.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
if __name__ == '__main__':
|
||||
import tornado.ioloop
|
||||
import tornado.web
|
||||
import urllib
|
||||
|
||||
class MainHandler(tornado.web.RequestHandler):
|
||||
def get(self, project):
|
||||
try:
|
||||
with open('/tmp/%s-coverage' % project, 'rb') as f:
|
||||
coverage = f.read().strip()
|
||||
n = int(coverage.strip('%'))
|
||||
if n >= 80:
|
||||
color = 'brightgreen'
|
||||
else:
|
||||
color = 'yellow'
|
||||
self.redirect(('https://img.shields.io/badge/'
|
||||
'coverage-%s-%s.svg'
|
||||
'?style=flat') %
|
||||
(urllib.quote(coverage), color))
|
||||
except IOError:
|
||||
raise tornado.web.HTTPError(404)
|
||||
|
||||
application = tornado.web.Application([
|
||||
(r"/([a-zA-Z0-9\-_]+)", MainHandler),
|
||||
])
|
||||
|
||||
if __name__ == "__main__":
|
||||
application.listen(8888, address='127.0.0.1')
|
||||
tornado.ioloop.IOLoop.instance().start()
|
|
@ -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
|
||||
}
|
||||
|
|
18
tests/gen_multiple_passwd.py
Normal file
18
tests/gen_multiple_passwd.py
Normal 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
10
tests/graceful.json
Normal 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
17
tests/graceful_cli.py
Normal 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
13
tests/graceful_server.py
Normal 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()
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
111
tests/jenkins.sh
Executable file
111
tests/jenkins.sh
Executable file
|
@ -0,0 +1,111 @@
|
|||
#!/bin/bash
|
||||
|
||||
result=0
|
||||
|
||||
function run_test {
|
||||
printf '\e[0;36m'
|
||||
echo "running test: $command $@"
|
||||
printf '\e[0m'
|
||||
|
||||
$command "$@"
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
printf '\e[0;31m'
|
||||
echo "test failed: $command $@"
|
||||
printf '\e[0m'
|
||||
echo
|
||||
result=1
|
||||
else
|
||||
printf '\e[0;32m'
|
||||
echo OK
|
||||
printf '\e[0m'
|
||||
echo
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
python --version
|
||||
coverage erase
|
||||
mkdir tmp
|
||||
run_test pep8 --ignore=E402 .
|
||||
run_test pyflakes .
|
||||
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 -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"
|
||||
|
||||
# test localhost is in the forbidden list by default
|
||||
run_test python tests/test.py --with-coverage --should-fail --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1"
|
||||
|
||||
# test localhost is available when forbidden list is empty
|
||||
run_test python tests/test.py --with-coverage --tcp-only --url="http://127.0.0.1/" -b "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 --forbidden-ip=" -a "-m aes-256-cfb -k testrc4 -s 127.0.0.1 -p 8388 -l 1081 -t 30 -b 127.0.0.1"
|
||||
|
||||
if [ -f /proc/sys/net/ipv4/tcp_fastopen ] ; then
|
||||
if [ 3 -eq `cat /proc/sys/net/ipv4/tcp_fastopen` ] ; then
|
||||
# we have to run it twice:
|
||||
# the first time there's no syn cookie
|
||||
# the second time there is syn cookie
|
||||
run_test python tests/test.py --with-coverage -c tests/fastopen.json
|
||||
run_test python tests/test.py --with-coverage -c tests/fastopen.json
|
||||
fi
|
||||
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
|
||||
|
||||
exit $result
|
12
tests/libmbedtls/install.sh
Executable file
12
tests/libmbedtls/install.sh
Executable 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
19
tests/libopenssl/install.sh
Executable 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
|
|
@ -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
|
||||
|
|
10
tests/mbedtls-aes-ctr.json
Normal file
10
tests/mbedtls-aes-ctr.json
Normal 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
|
||||
}
|
10
tests/mbedtls-aes-gcm.json
Normal file
10
tests/mbedtls-aes-gcm.json
Normal 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
10
tests/mbedtls-aes.json
Normal 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
|
||||
}
|
10
tests/mbedtls-camellia.json
Normal file
10
tests/mbedtls-camellia.json
Normal 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
|
||||
}
|
|
@ -1,3 +1,19 @@
|
|||
#!/usr/bin/env python
|
||||
#
|
||||
# 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.
|
||||
|
||||
import nose
|
||||
from nose.plugins.base import Plugin
|
||||
|
||||
|
|
11
tests/rc4-md5-ota.json
Normal file
11
tests/rc4-md5-ota.json
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
11
tests/server-dnsserver.json
Normal file
11
tests/server-dnsserver.json
Normal 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"]
|
||||
}
|
8
tests/server-multi-passwd-empty.json
Normal file
8
tests/server-multi-passwd-empty.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"server": "127.0.0.1",
|
||||
"local_port": 1081,
|
||||
"port_password": {
|
||||
},
|
||||
"timeout": 60,
|
||||
"method": "aes-256-cfb"
|
||||
}
|
2008
tests/server-multi-passwd-performance.json
Normal file
2008
tests/server-multi-passwd-performance.json
Normal file
File diff suppressed because it is too large
Load diff
18
tests/setup_tc.sh
Executable file
18
tests/setup_tc.sh
Executable file
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
|
||||
DEV=lo
|
||||
PORT=8388
|
||||
DELAY=100ms
|
||||
|
||||
type tc 2> /dev/null && (
|
||||
tc qdisc add dev $DEV root handle 1: htb
|
||||
tc class add dev $DEV parent 1: classid 1:1 htb rate 2mbps
|
||||
tc class add dev $DEV parent 1:1 classid 1:6 htb rate 2mbps ceil 1mbps prio 0
|
||||
tc filter add dev $DEV parent 1:0 prio 0 protocol ip handle 6 fw flowid 1:6
|
||||
|
||||
tc filter add dev $DEV parent 1:0 protocol ip u32 match ip dport $PORT 0xffff flowid 1:6
|
||||
tc filter add dev $DEV parent 1:0 protocol ip u32 match ip sport $PORT 0xffff flowid 1:6
|
||||
|
||||
tc qdisc show dev lo
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2014 clowwindy
|
||||
#
|
||||
# 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:
|
||||
# Copyright 2015 clowwindy
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
# 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.
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
with_statement
|
||||
|
@ -34,17 +28,23 @@ from subprocess import Popen, PIPE
|
|||
|
||||
python = ['python']
|
||||
|
||||
default_url = 'http://localhost/'
|
||||
|
||||
parser = argparse.ArgumentParser(description='test Shadowsocks')
|
||||
parser.add_argument('-c', '--client-conf', type=str, default=None)
|
||||
parser.add_argument('-s', '--server-conf', type=str, default=None)
|
||||
parser.add_argument('-a', '--client-args', type=str, default=None)
|
||||
parser.add_argument('-b', '--server-args', type=str, default=None)
|
||||
parser.add_argument('--with-coverage', action='store_true', default=None)
|
||||
parser.add_argument('--should-fail', action='store_true', default=None)
|
||||
parser.add_argument('--tcp-only', action='store_true', default=None)
|
||||
parser.add_argument('--url', type=str, default=default_url)
|
||||
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']
|
||||
|
@ -61,6 +61,8 @@ if config.client_args:
|
|||
server_args.extend(config.server_args.split())
|
||||
else:
|
||||
server_args.extend(config.client_args.split())
|
||||
if config.url == default_url:
|
||||
server_args.extend(['--forbidden-ip', ''])
|
||||
|
||||
p1 = Popen(server_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
|
||||
p2 = Popen(client_args, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
|
||||
|
@ -94,7 +96,7 @@ try:
|
|||
stage = 5
|
||||
if bytes != str:
|
||||
line = str(line, 'utf8')
|
||||
sys.stdout.write(line)
|
||||
sys.stderr.write(line)
|
||||
if line.find('starting local') >= 0:
|
||||
local_ready = True
|
||||
if line.find('starting server') >= 0:
|
||||
|
@ -103,7 +105,7 @@ try:
|
|||
if stage == 1:
|
||||
time.sleep(2)
|
||||
|
||||
p3 = Popen(['curl', 'http://www.example.com/', '-v', '-L',
|
||||
p3 = Popen(['curl', config.url, '-v', '-L',
|
||||
'--socks5-hostname', '127.0.0.1:1081',
|
||||
'-m', '15', '--connect-timeout', '10'],
|
||||
stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
|
||||
|
@ -118,9 +120,16 @@ try:
|
|||
fdset.remove(p3.stdout)
|
||||
fdset.remove(p3.stderr)
|
||||
r = p3.wait()
|
||||
if r != 0:
|
||||
sys.exit(1)
|
||||
p4 = Popen(['socksify', 'dig', '@8.8.8.8', 'www.google.com'],
|
||||
if config.should_fail:
|
||||
if r == 0:
|
||||
sys.exit(1)
|
||||
else:
|
||||
if r != 0:
|
||||
sys.exit(1)
|
||||
if config.tcp_only:
|
||||
break
|
||||
p4 = Popen(['socksify', 'dig', '@%s' % config.dns,
|
||||
'www.google.com'],
|
||||
stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
|
||||
if p4 is not None:
|
||||
fdset.append(p4.stdout)
|
||||
|
@ -131,14 +140,19 @@ try:
|
|||
|
||||
if stage == 5:
|
||||
r = p4.wait()
|
||||
if r != 0:
|
||||
sys.exit(1)
|
||||
print('test passed')
|
||||
if config.should_fail:
|
||||
if r == 0:
|
||||
sys.exit(1)
|
||||
print('test passed (expecting failure)')
|
||||
else:
|
||||
if r != 0:
|
||||
sys.exit(1)
|
||||
print('test passed')
|
||||
break
|
||||
finally:
|
||||
for p in [p1, p2]:
|
||||
try:
|
||||
os.kill(p.pid, signal.SIGQUIT)
|
||||
os.kill(p.pid, signal.SIGINT)
|
||||
os.waitpid(p.pid, 0)
|
||||
except OSError:
|
||||
pass
|
||||
|
|
45
tests/test_command.sh
Executable file
45
tests/test_command.sh
Executable file
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
|
||||
. tests/assert.sh
|
||||
|
||||
PYTHON="coverage run -a"
|
||||
LOCAL="$PYTHON shadowsocks/local.py"
|
||||
SERVER="$PYTHON shadowsocks/server.py"
|
||||
|
||||
assert "$LOCAL --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks"
|
||||
assert "$SERVER --version 2>&1 | grep Shadowsocks | awk -F\" \" '{print \$1}'" "Shadowsocks"
|
||||
|
||||
assert "$LOCAL 2>&1 | grep ERROR" "ERROR: config not specified"
|
||||
assert "$LOCAL 2>&1 | grep usage | cut -d: -f1" "usage"
|
||||
|
||||
assert "$SERVER 2>&1 | grep ERROR" "ERROR: config not specified"
|
||||
assert "$SERVER 2>&1 | grep usage | cut -d: -f1" "usage"
|
||||
|
||||
assert "$LOCAL 2>&1 -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: server set to listen on 127.0.0.1:8388, are you sure?"
|
||||
$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 -k testrc4 -s 0.0.0.0 -p 8388 -t10 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: your timeout 10 seems too short"
|
||||
$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 -k testrc4 -s 0.0.0.0 -p 8388 -t1000 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: your timeout 1000 seems too long"
|
||||
$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 -k testrc4 -s 0.0.0.0 -p 8388 -d start | grep WARNING | awk -F\"WARNING\" '{print \$2}'" " warning: RC4 is not safe; please use a safer cipher, like AES-256-CFB"
|
||||
$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 -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"
|
||||
$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"
|
||||
$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 -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"
|
||||
$LOCAL 2>/dev/null 1>/dev/null -m rc4-md5 -k testrc4 -s 127.0.0.1 -p 8388 -d stop
|
||||
|
||||
assert_end command
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
function test {
|
||||
function run_test {
|
||||
expected=$1
|
||||
shift
|
||||
echo "running test: $command $@"
|
||||
|
@ -18,25 +18,26 @@ function test {
|
|||
for module in local server
|
||||
do
|
||||
|
||||
command="coverage run -p -a shadowsocks/$module.py"
|
||||
command="coverage run -a shadowsocks/$module.py"
|
||||
|
||||
test 0 -c tests/aes.json -d stop --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
mkdir -p tmp
|
||||
|
||||
test 0 -c tests/aes.json -d start --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 0 -c tests/aes.json -d stop --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
|
||||
test 0 -c tests/aes.json -d start --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 1 -c tests/aes.json -d start --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 0 -c tests/aes.json -d stop --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
|
||||
test 0 -c tests/aes.json -d start --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 0 -c tests/aes.json -d restart --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 0 -c tests/aes.json -d stop --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
run_test 1 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
|
||||
test 0 -c tests/aes.json -d restart --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 0 -c tests/aes.json -d stop --pid-file /tmp/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d start --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
|
||||
test 1 -c tests/aes.json -d start --pid-file /tmp/not_exist/shadowsocks.pid --log-file /tmp/shadowsocks.log
|
||||
test 1 -c tests/aes.json -d start --pid-file /tmp/shadowsocks.pid --log-file /tmp/not_exist/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d restart --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
run_test 0 -c tests/aes.json -d stop --pid-file tmp/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
|
||||
run_test 1 -c tests/aes.json -d start --pid-file tmp/not_exist/shadowsocks.pid --log-file tmp/shadowsocks.log
|
||||
|
||||
done
|
||||
|
|
64
tests/test_graceful_restart.sh
Executable file
64
tests/test_graceful_restart.sh
Executable 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
|
24
tests/test_large_file.sh
Executable file
24
tests/test_large_file.sh
Executable file
|
@ -0,0 +1,24 @@
|
|||
#!/bin/bash
|
||||
|
||||
PYTHON="coverage run -a"
|
||||
URL=http://127.0.0.1/file
|
||||
|
||||
mkdir -p tmp
|
||||
|
||||
$PYTHON shadowsocks/local.py -c tests/aes.json &
|
||||
LOCAL=$!
|
||||
|
||||
$PYTHON shadowsocks/server.py -c tests/aes.json --forbidden-ip "" &
|
||||
SERVER=$!
|
||||
|
||||
sleep 3
|
||||
|
||||
time curl -o tmp/expected $URL
|
||||
time curl -o tmp/result --socks5-hostname 127.0.0.1:1081 $URL
|
||||
|
||||
kill -s SIGINT $LOCAL
|
||||
kill -s SIGINT $SERVER
|
||||
|
||||
sleep 2
|
||||
|
||||
diff tmp/expected tmp/result || exit 1
|
85
tests/test_udp_src.py
Normal file
85
tests/test_udp_src.py
Normal 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
23
tests/test_udp_src.sh
Executable 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
|
|
@ -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
|
||||
}
|
||||
|
|
10
tests/xchacha20-ietf-poly1305.json
Normal file
10
tests/xchacha20-ietf-poly1305.json
Normal 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
10
tests/xchacha20.json
Normal 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
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue