├── mudb.json ├── shadowsocks ├── tail.sh ├── stop.sh ├── run.sh ├── logrun.sh ├── version.py ├── __init__.py ├── crypto │ ├── __init__.py │ ├── rc4_md5.py │ ├── hkdf.py │ ├── util.py │ ├── table.py │ └── aead.py ├── obfsplugin │ ├── __init__.py │ ├── plain.py │ ├── simple_obfs_http.py │ ├── simple_obfs_tls.py │ ├── verify.py │ ├── http_simple.py │ └── obfs_tls.py ├── encrypt_test.py ├── local.py ├── obfs.py ├── lru_cache.py ├── daemon.py ├── ordereddict.py ├── eventloop.py ├── encrypt.py ├── server.py └── manager.py ├── tail.sh ├── detect.html ├── stop.sh ├── README.md ├── requirements.txt ├── package ├── rhel.repo ├── fedora.repo ├── shadowsocks-server.service └── rpm │ └── shadowsocks-server-noarch.spec ├── utils ├── fail2ban │ └── shadowsocks.conf ├── README.md └── autoban.py ├── run.sh ├── logrun.sh ├── apiconfig.py ├── switchrule.py ├── Dockerfile ├── importloader.py ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── docker-publish.yml ├── configloader.py ├── config.json ├── docker-apiconfig.py ├── CONTRIBUTING.md ├── server.py ├── webapi_utils.py ├── CHANGES ├── auto_block.py └── LICENSE /mudb.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] 3 | -------------------------------------------------------------------------------- /shadowsocks/tail.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | tail -f ssserver.log 4 | -------------------------------------------------------------------------------- /tail.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | cd `dirname $0` 3 | tail -f ssserver.log 4 | -------------------------------------------------------------------------------- /detect.html: -------------------------------------------------------------------------------- 1 | 由于碰撞到了审计规则,您的连接已经被阻断。 2 | This connection has been blocked due to matched detect rule. 3 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | eval $(ps -ef | grep "[0-9] python server\\.py m" | awk '{print "kill -9 "$2}') 4 | -------------------------------------------------------------------------------- /shadowsocks/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | eval $(ps -ef | grep "[0-9] python server\\.py a" | awk '{print "kill "$2}') 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shadowsocks-mod 2 | 3 | This program has been deprecated, please use [UIM-Server](https://github.com/SSPanel-UIM/UIM-Server) instead. 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Automatically generated by https://github.com/damnever/pigar. 2 | 3 | configloader == 1.0.1 4 | requests == 2.31.0 5 | setuptools == 65.5.1 6 | -------------------------------------------------------------------------------- /package/rhel.repo: -------------------------------------------------------------------------------- 1 | [sspanel-uim] 2 | name=SSPanel-UIM RPM Repository 3 | baseurl=https://mirror.sspanel.org/repo/rhel/$releasever/$basearch 4 | enabled=1 5 | gpgcheck=0 -------------------------------------------------------------------------------- /package/fedora.repo: -------------------------------------------------------------------------------- 1 | [sspanel-uim] 2 | name=SSPanel-UIM RPM Repository 3 | baseurl=https://mirror.sspanel.org/repo/fedora/$releasever/$basearch 4 | enabled=1 5 | gpgcheck=0 -------------------------------------------------------------------------------- /utils/fail2ban/shadowsocks.conf: -------------------------------------------------------------------------------- 1 | [Definition] 2 | 3 | _daemon = shadowsocks 4 | 5 | failregex = ^\s+ERROR\s+can not parse header when handling connection from :\d+$ 6 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | cd `dirname $0` 3 | eval $(ps -ef | grep "[0-9] python server\\.py m" | awk '{print "kill "$2}') 4 | ulimit -n 512000 5 | nohup python server.py m>> /dev/null 2>&1 & 6 | 7 | -------------------------------------------------------------------------------- /logrun.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | cd `dirname $0` 3 | eval $(ps -ef | grep "[0-9] python server\\.py m" | awk '{print "kill "$2}') 4 | ulimit -n 512000 5 | nohup python server.py m>> ssserver.log 2>&1 & 6 | 7 | -------------------------------------------------------------------------------- /shadowsocks/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | eval $(ps -ef | grep "[0-9] python server\\.py a" | awk '{print "kill "$2}') 4 | ulimit -n 4096 5 | nohup python server.py a >> /dev/null 2>&1 & 6 | 7 | -------------------------------------------------------------------------------- /shadowsocks/logrun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd `dirname $0` 3 | eval $(ps -ef | grep "[0-9] python server\\.py a" | awk '{print "kill "$2}') 4 | ulimit -n 4096 5 | nohup python server.py a >> ssserver.log 2>&1 & 6 | 7 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | Useful Tools 2 | =========== 3 | 4 | autoban.py 5 | ---------- 6 | 7 | Automatically ban IPs that try to brute force crack the server. 8 | 9 | See https://github.com/shadowsocks/shadowsocks/wiki/Ban-Brute-Force-Crackers 10 | -------------------------------------------------------------------------------- /apiconfig.py: -------------------------------------------------------------------------------- 1 | # Config 2 | NODE_ID = 0 3 | 4 | MU_SUFFIX = 'zhaoj.in' 5 | MU_REGEX = '%5m%id.%suffix' 6 | 7 | API_INTERFACE = 'modwebapi' # modwebapi 8 | WEBAPI_URL = 'https://example.com' 9 | WEBAPI_TOKEN = 'example-key' 10 | API_UPDATE_TIME = 60 11 | 12 | """ 13 | get port offset by node->name 14 | HK 1 #9900 15 | then offset is 9900 16 | """ 17 | GET_PORT_OFFSET_BY_NODE_NAME = True 18 | -------------------------------------------------------------------------------- /package/shadowsocks-server.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=shadowsocks-mod Service for SSPanel-UIM 3 | After=rc-local.service 4 | 5 | [Service] 6 | Type=simple 7 | User=root 8 | Group=root 9 | WorkingDirectory=/opt/shadowsocks-server 10 | ExecStart=/usr/bin/python3 /opt/shadowsocks-server/server.py >> /var/log/shadowsocks-server.log 11 | ExecStop=/usr/bin/bash /opt/shadowsocks-server/stop.sh 12 | Restart=always 13 | LimitNOFILE=512000 14 | [Install] 15 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /switchrule.py: -------------------------------------------------------------------------------- 1 | from configloader import get_config 2 | 3 | def getKeys(): 4 | key_list = ["id", "port", "u", "d", "transfer_enable", "passwd", "enable", "method", "obfs", "obfs_param", "protocol", "protocol_param", "id", "node_speedlimit", "forbidden_ip", "forbidden_port", "is_multi_user"] 5 | return key_list 6 | # return key_list + ['plan'] # append the column name 'plan' 7 | 8 | def isTurnOn(row): 9 | return True 10 | # return row['plan'] == 'B' # then judge here 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.2-alpine 2 | 3 | COPY . /app 4 | WORKDIR /app 5 | 6 | RUN apk add --no-cache \ 7 | gcc \ 8 | libsodium-dev \ 9 | libffi-dev \ 10 | openssl-dev && \ 11 | apk add --no-cache --virtual .build \ 12 | libc-dev \ 13 | rust \ 14 | cargo && \ 15 | pip install --no-cache-dir -r requirements.txt && \ 16 | cp docker-apiconfig.py userapiconfig.py && \ 17 | apk del --purge .build 18 | 19 | CMD ["server.py"] 20 | ENTRYPOINT ["python"] -------------------------------------------------------------------------------- /importloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | def load(name): 5 | try: 6 | obj = __import__(name) 7 | reload(obj) 8 | return obj 9 | except: 10 | pass 11 | 12 | try: 13 | import importlib 14 | 15 | obj = importlib.__import__(name) 16 | importlib.reload(obj) 17 | return obj 18 | except: 19 | pass 20 | 21 | def loads(namelist): 22 | for name in namelist: 23 | obj = load(name) 24 | if obj is not None: 25 | return obj 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | ca.pem 3 | client-cert.pem 4 | client-key.pem 5 | mysql.zip 6 | ssserver.log 7 | ssshell.asc 8 | user-config.json 9 | userapiconfig.py 10 | user-detect.html 11 | 12 | # Packages 13 | *.egg 14 | *.egg-info 15 | dist 16 | eggs 17 | parts 18 | bin 19 | var 20 | sdist 21 | develop-eggs 22 | .installed.cfg 23 | venv 24 | 25 | # Installer logs 26 | pip-log.txt 27 | 28 | # Unit test / coverage reports 29 | htmlcov 30 | .coverage* 31 | .tox 32 | 33 | #Translations 34 | *.mo 35 | 36 | #Mr Developer 37 | .mr.developer.cfg 38 | 39 | .DS_Store 40 | .idea 41 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | target-branch: "testing" 11 | schedule: 12 | interval: "daily" 13 | -------------------------------------------------------------------------------- /configloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: UTF-8 -*- 3 | import importloader 4 | 5 | g_config = None 6 | 7 | DEFAULT_CONFIG = { 8 | "GET_PORT_OFFSET_BY_NODE_NAME" : False 9 | } 10 | 11 | def set_default_config(cfg): 12 | for key in DEFAULT_CONFIG: 13 | if not hasattr(cfg, key): 14 | setattr(cfg, key, DEFAULT_CONFIG[key]) 15 | 16 | def load_config(): 17 | global g_config 18 | g_config = importloader.loads(["userapiconfig", "apiconfig"]) 19 | set_default_config(g_config) 20 | 21 | def get_config(): 22 | return g_config 23 | 24 | load_config() 25 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": "0.0.0.0", 3 | "server_ipv6": "::", 4 | "server_port": 8388, 5 | "local_address": "127.0.0.1", 6 | "local_port": 1080, 7 | 8 | "password": "m", 9 | "timeout": 120, 10 | "udp_timeout": 60, 11 | "method": "aes-256-cfb", 12 | "protocol": "auth_aes128_md5", 13 | "protocol_param": "", 14 | "obfs": "tls1.2_ticket_auth_compatible", 15 | "obfs_param": "", 16 | "speed_limit_per_con": 0, 17 | 18 | "out_bind": "", 19 | "dns_ipv6": false, 20 | "connect_verbose_info": 0, 21 | "connect_hex_data": 0, 22 | "redirect": "", 23 | "fast_open": true, 24 | "friendly_detect": 1 25 | } 26 | -------------------------------------------------------------------------------- /docker-apiconfig.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Config 4 | NODE_ID = int(os.getenv('NODE_ID'), '0') 5 | 6 | MU_SUFFIX = os.getenv('MU_SUFFIX', 'zhaoj.in') 7 | MU_REGEX = os.getenv('MU_REGEX', '%5m%id.%suffix') 8 | 9 | SERVER_PUB_ADDR = os.getenv('SERVER_PUB_ADDR', '127.0.0.1') 10 | API_INTERFACE = os.getenv('API_INTERFACE', 'modwebapi') 11 | 12 | WEBAPI_URL = os.getenv('WEBAPI_URL', 'https://demo.sspanel.host') 13 | WEBAPI_TOKEN = os.getenv('WEBAPI_TOKEN', 'sspanel') 14 | 15 | API_UPDATE_TIME = int(os.getenv('API_UPDATE_TIME', '60')) 16 | 17 | """ 18 | get port offset by node->name 19 | HK 1 #9900 20 | then offset is 9900 21 | """ 22 | GET_PORT_OFFSET_BY_NODE_NAME = os.getenv('GET_PORT_OFFSET_BY_NODE_NAME', 'true') == 'true' 23 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - manyuser 7 | 8 | jobs: 9 | push: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Build image 16 | run: docker build . --file Dockerfile --tag sspaneluim/shadowsocks 17 | 18 | - name: Login to docker hub 19 | uses: actions-hub/docker/login@master 20 | env: 21 | DOCKER_USERNAME: ${{ secrets.DOCKER_USER }} 22 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASS }} 23 | 24 | - name: Push to docker hub :latest 25 | uses: actions-hub/docker@master 26 | with: 27 | args: push sspaneluim/shadowsocks 28 | -------------------------------------------------------------------------------- /shadowsocks/version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2016 breakwa11 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | def version(): 19 | return '3.5.1' 20 | -------------------------------------------------------------------------------- /shadowsocks/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2012-2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, with_statement 18 | -------------------------------------------------------------------------------- /shadowsocks/crypto/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | -------------------------------------------------------------------------------- /package/rpm/shadowsocks-server-noarch.spec: -------------------------------------------------------------------------------- 1 | Name: shadowsocks-server 2 | Version: 0.0.1 3 | Release: 1%{?dist} 4 | Summary: Shadowsocks Server for SSPanel-UIM. 5 | Group: Unspecified 6 | License: Apache-2.0 7 | URL: https://github.com/Anankke/shadowsocks-mod 8 | Packager: SSPanel-UIM Team 9 | BuildArch: noarch 10 | BuildRequires: systemd 11 | Requires: python3, python3-pip, libsodium 12 | 13 | %description 14 | A Shadowsocks implementation from SSPanel-UIM. 15 | 16 | %install 17 | rm -rf %{buildroot} 18 | mkdir -p %{buildroot}/opt/shadowsocks-server 19 | mkdir -p %{buildroot}%{_unitdir} 20 | cp -r %{_builddir}/%{name}-%{version}/* %{buildroot}/opt/shadowsocks-server 21 | install -m 644 %{_builddir}/shadowsocks-server.service %{buildroot}%{_unitdir} 22 | 23 | %post 24 | /usr/bin/python3 -m pip install -r /opt/shadowsocks-server/requirements.txt 25 | /usr/bin/cp /opt/shadowsocks-server/apiconfig.py /opt/shadowsocks-server/userapiconfig.py 26 | 27 | %clean 28 | rm -rf %{buildroot} 29 | 30 | %files 31 | /opt/shadowsocks-server/* 32 | %{_unitdir}/shadowsocks-server.service 33 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | Pull Requests 5 | ------------- 6 | 7 | 1. Pull requests are welcome. If you would like to add a large feature 8 | or make a significant change, make sure to open an issue to discuss with 9 | people first. 10 | 2. Follow PEP8. 11 | 3. Make sure to pass the unit tests. Write unit tests for new modules if 12 | needed. 13 | 14 | Issues 15 | ------ 16 | 17 | 1. Only bugs and feature requests are accepted here. 18 | 2. We'll only work on important features. If the feature you're asking only 19 | benefits a few people, you'd better implement the feature yourself and send us 20 | a pull request, or ask some of your friends to do so. 21 | 3. We don't answer questions of any other types here. Since very few people 22 | are watching the issue tracker here, you'll probably get no help from here. 23 | Read [Troubleshooting] and get help from forums or [mailing lists]. 24 | 4. Issues in languages other than English will be Google translated into English 25 | later. 26 | 27 | 28 | [Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting 29 | [mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks 30 | -------------------------------------------------------------------------------- /shadowsocks/encrypt_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, with_statement 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) 7 | 8 | from shadowsocks.crypto import openssl, rc4_md5, sodium, table 9 | 10 | def run(func): 11 | try: 12 | func() 13 | except: 14 | pass 15 | 16 | 17 | def run_n(func, name): 18 | try: 19 | func(name) 20 | except: 21 | pass 22 | 23 | 24 | def main(): 25 | print("\n""rc4_md5") 26 | rc4_md5.test() 27 | print("\n""aes-256-cfb") 28 | openssl.test_aes_256_cfb() 29 | print("\n""aes-128-cfb") 30 | openssl.test_aes_128_cfb() 31 | print("\n""bf-cfb") 32 | run(openssl.test_bf_cfb) 33 | print("\n""camellia-128-cfb") 34 | run_n(openssl.run_method, "camellia-128-cfb") 35 | print("\n""cast5-cfb") 36 | run_n(openssl.run_method, "cast5-cfb") 37 | print("\n""idea-cfb") 38 | run_n(openssl.run_method, "idea-cfb") 39 | print("\n""seed-cfb") 40 | run_n(openssl.run_method, "seed-cfb") 41 | print("\n""salsa20") 42 | run(sodium.test_salsa20) 43 | print("\n""chacha20") 44 | run(sodium.test_chacha20) 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /shadowsocks/crypto/rc4_md5.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, with_statement 18 | 19 | import hashlib 20 | 21 | from shadowsocks.crypto import openssl 22 | 23 | __all__ = ['ciphers'] 24 | 25 | def create_cipher(alg, key, iv, op, crypto_path=None, 26 | key_as_bytes=0, d=None, salt=None, 27 | i=1, padding=1): 28 | md5 = hashlib.md5() 29 | md5.update(key) 30 | md5.update(iv) 31 | rc4_key = md5.digest() 32 | return openssl.OpenSSLStreamCrypto(b'rc4', rc4_key, b'', op, crypto_path) 33 | 34 | 35 | ciphers = { 36 | 'rc4-md5': (16, 16, create_cipher), 37 | 'rc4-md5-6': (16, 6, create_cipher), 38 | } 39 | 40 | 41 | def test(): 42 | from shadowsocks.crypto import util 43 | 44 | cipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 1) 45 | decipher = create_cipher('rc4-md5', b'k' * 32, b'i' * 16, 0) 46 | 47 | util.run_cipher(cipher, decipher) 48 | 49 | 50 | if __name__ == '__main__': 51 | test() 52 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 breakwall 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import logging 19 | import os 20 | 21 | if __name__ == "__main__": 22 | import inspect 23 | 24 | os.chdir( 25 | os.path.dirname( 26 | os.path.realpath(inspect.getfile(inspect.currentframe())) 27 | ) 28 | ) 29 | 30 | from multiprocessing import Process 31 | 32 | import web_transfer 33 | from configloader import get_config 34 | from shadowsocks import shell 35 | 36 | class MainThread(Process): 37 | def __init__(self, obj): 38 | Process.__init__(self) 39 | self.obj = obj 40 | 41 | def run(self): 42 | self.obj.thread_db(self.obj) 43 | 44 | def stop(self): 45 | self.obj.thread_db_stop() 46 | 47 | 48 | def main(): 49 | logging.basicConfig( 50 | level=logging.INFO, format="%(levelname)-s: %(message)s" 51 | ) 52 | 53 | shell.check_python() 54 | 55 | if get_config().API_INTERFACE == "modwebapi": 56 | threadMain = MainThread(web_transfer.WebTransfer) 57 | threadMain.start() 58 | 59 | try: 60 | while threadMain.is_alive(): 61 | threadMain.join(10.0) 62 | except (KeyboardInterrupt, IOError, OSError): 63 | import traceback 64 | 65 | traceback.print_exc() 66 | threadMain.stop() 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /utils/autoban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2015 clowwindy 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining a copy 7 | # of this software and associated documentation files (the "Software"), to deal 8 | # in the Software without restriction, including without limitation the rights 9 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | # copies of the Software, and to permit persons to whom the Software is 11 | # furnished to do so, subject to the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included in 14 | # all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | # SOFTWARE. 23 | 24 | from __future__ import absolute_import, division, print_function, \ 25 | with_statement 26 | 27 | import os 28 | import sys 29 | import argparse 30 | 31 | if __name__ == '__main__': 32 | parser = argparse.ArgumentParser(description='See README') 33 | parser.add_argument('-c', '--count', default=3, type=int, 34 | help='with how many failure times it should be ' 35 | 'considered as an attack') 36 | config = parser.parse_args() 37 | ips = {} 38 | banned = set() 39 | for line in sys.stdin: 40 | if 'can not parse header when' in line: 41 | ip = line.split()[-1].split(':')[0] 42 | if ip not in ips: 43 | ips[ip] = 1 44 | print(ip) 45 | sys.stdout.flush() 46 | else: 47 | ips[ip] += 1 48 | if ip not in banned and ips[ip] >= config.count: 49 | banned.add(ip) 50 | cmd = 'iptables -A INPUT -s %s -j DROP' % ip 51 | print(cmd, file=sys.stderr) 52 | sys.stderr.flush() 53 | os.system(cmd) 54 | -------------------------------------------------------------------------------- /webapi_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import logging 5 | 6 | import requests 7 | from configloader import get_config 8 | 9 | class WebApi(object): 10 | def __init__(self): 11 | self.session_pool = requests.Session() 12 | 13 | def getApi(self, uri, params={}): 14 | r""" Send a ``GET`` request to API server. Return response["data"] or [] 15 | 16 | :param uri: URI to request 17 | :param params: Optional arguments ``request`` takes 18 | :rtype: list 19 | """ 20 | params["key"] = get_config().WEBAPI_TOKEN 21 | response = self.session_pool.get( 22 | "%s/mod_mu/%s" % (get_config().WEBAPI_URL, uri), 23 | params=params, 24 | timeout=10, 25 | ) 26 | if response.status_code != 200: 27 | logging.error("Server error with status code: %i" % 28 | response.status_code) 29 | raise Exception('Server Error!') 30 | 31 | try: 32 | json_data = response.json() 33 | except: 34 | logging.error("Wrong data: %s" % response.text) 35 | raise Exception('Server Error!') 36 | 37 | if len(json_data) != 2: 38 | logging.error("Wrong data: %s" % response.text) 39 | raise Exception('Server Error!') 40 | if json_data["ret"] == 0: 41 | logging.error("Wrong data: %s" % json_data["data"]) 42 | raise Exception('Server Error!') 43 | 44 | return json_data["data"] 45 | 46 | def postApi(self, uri, params={}, json={}): 47 | r""" Send a ``POST`` request to API server. Return response["data"] or [] 48 | 49 | :param uri: URI to request 50 | :param params: Optional arguments ``request`` takes 51 | :param json: Optional arguments ``json`` that ``request`` takes 52 | :rtype: list 53 | """ 54 | params["key"] = get_config().WEBAPI_TOKEN 55 | response = self.session_pool.post( 56 | "%s/mod_mu/%s" % (get_config().WEBAPI_URL, uri), 57 | params=params, 58 | json=json, 59 | timeout=10, 60 | ) 61 | if response.status_code != 200: 62 | logging.error("Server error with status code: %i" % 63 | response.status_code) 64 | raise Exception('Server Error!') 65 | 66 | try: 67 | json_data = response.json() 68 | except: 69 | logging.error("Wrong data: %s" % response.text) 70 | raise Exception('Server Error!') 71 | 72 | if len(json_data) != 2: 73 | logging.error("Wrong data: %s" % response.text) 74 | raise Exception('Server Error!') 75 | if json_data["ret"] == 0: 76 | logging.error("Wrong data: %s" % json_data["data"]) 77 | raise Exception('Server Error!') 78 | 79 | return json_data["data"] 80 | -------------------------------------------------------------------------------- /shadowsocks/local.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2012-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, with_statement 19 | 20 | import logging 21 | import os 22 | import signal 23 | import sys 24 | 25 | if __name__ == '__main__': 26 | import inspect 27 | file_path = os.path.dirname( 28 | os.path.realpath( 29 | inspect.getfile( 30 | inspect.currentframe()))) 31 | sys.path.insert(0, os.path.join(file_path, '../')) 32 | 33 | from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, asyncdns 34 | 35 | 36 | def main(): 37 | shell.check_python() 38 | 39 | # fix py2exe 40 | if hasattr(sys, "frozen") and sys.frozen in \ 41 | ("windows_exe", "console_exe"): 42 | p = os.path.dirname(os.path.abspath(sys.executable)) 43 | os.chdir(p) 44 | 45 | config = shell.get_config(True) 46 | 47 | if not config.get('dns_ipv6', False): 48 | asyncdns.IPV6_CONNECTION_SUPPORT = False 49 | 50 | daemon.daemon_exec(config) 51 | logging.info( 52 | "local start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" % 53 | (config['protocol'], 54 | config['password'], 55 | config['method'], 56 | config['obfs'], 57 | config['obfs_param'])) 58 | 59 | try: 60 | logging.info("starting local at %s:%d" % 61 | (config['local_address'], config['local_port'])) 62 | 63 | dns_resolver = asyncdns.DNSResolver() 64 | tcp_server = tcprelay.TCPRelay(config, dns_resolver, True) 65 | udp_server = udprelay.UDPRelay(config, dns_resolver, True) 66 | loop = eventloop.EventLoop() 67 | dns_resolver.add_to_loop(loop) 68 | tcp_server.add_to_loop(loop) 69 | udp_server.add_to_loop(loop) 70 | 71 | def handler(signum, _): 72 | logging.warn('received SIGQUIT, doing graceful shutting down..') 73 | tcp_server.close(next_tick=True) 74 | udp_server.close(next_tick=True) 75 | signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), handler) 76 | 77 | def int_handler(signum, _): 78 | sys.exit(1) 79 | signal.signal(signal.SIGINT, int_handler) 80 | 81 | daemon.set_user(config.get('user', None)) 82 | loop.run() 83 | except Exception as e: 84 | shell.print_exception(e) 85 | sys.exit(1) 86 | 87 | if __name__ == '__main__': 88 | main() 89 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/plain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | 25 | from shadowsocks.common import ord 26 | 27 | def create_obfs(method): 28 | return plain(method) 29 | 30 | obfs_map = { 31 | 'plain': (create_obfs,), 32 | 'origin': (create_obfs,), 33 | } 34 | 35 | class plain(object): 36 | def __init__(self, method): 37 | self.method = method 38 | self.server_info = None 39 | 40 | def init_data(self): 41 | return b'' 42 | 43 | def get_overhead(self, direction): # direction: true for c->s false for s->c 44 | return 0 45 | 46 | def get_server_info(self): 47 | return self.server_info 48 | 49 | def set_server_info(self, server_info): 50 | self.server_info = server_info 51 | 52 | def client_pre_encrypt(self, buf): 53 | return buf 54 | 55 | def client_encode(self, buf): 56 | return buf 57 | 58 | def client_decode(self, buf): 59 | # (buffer_to_recv, is_need_to_encode_and_send_back) 60 | return (buf, False) 61 | 62 | def client_post_decrypt(self, buf): 63 | return buf 64 | 65 | def server_pre_encrypt(self, buf): 66 | return buf 67 | 68 | def server_encode(self, buf): 69 | return buf 70 | 71 | def server_decode(self, buf): 72 | # (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back) 73 | return (buf, True, False) 74 | 75 | def server_post_decrypt(self, buf): 76 | return (buf, False) 77 | 78 | def client_udp_pre_encrypt(self, buf): 79 | return buf 80 | 81 | def client_udp_post_decrypt(self, buf): 82 | return buf 83 | 84 | def server_udp_pre_encrypt(self, buf, uid): 85 | return buf 86 | 87 | def server_udp_post_decrypt(self, buf): 88 | return (buf, None) 89 | 90 | def dispose(self): 91 | pass 92 | 93 | def get_head_size(self, buf, def_value): 94 | if len(buf) < 2: 95 | return def_value 96 | head_type = ord(buf[0]) & 0x7 97 | if head_type == 1: 98 | return 7 99 | if head_type == 4: 100 | return 19 101 | if head_type == 3: 102 | return 4 + ord(buf[1]) 103 | return def_value 104 | -------------------------------------------------------------------------------- /shadowsocks/crypto/hkdf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Void Copyright NO ONE 5 | # 6 | # Void License 7 | # 8 | # The code belongs to no one. Do whatever you want. 9 | # Forget about boring open source license. 10 | # 11 | # HKDF for AEAD ciphers 12 | # 13 | 14 | from __future__ import division 15 | 16 | import hashlib 17 | import hmac 18 | import sys 19 | 20 | if sys.version_info[0] == 3: 21 | def buffer(x): 22 | return x 23 | 24 | 25 | def hkdf_extract(salt, input_key_material, algorithm=hashlib.sha256): 26 | """ 27 | Extract a pseudorandom key suitable for use with hkdf_expand 28 | from the input_key_material and a salt using HMAC with the 29 | provided hash (default SHA-256). 30 | 31 | salt should be a random, application-specific byte string. If 32 | salt is None or the empty string, an all-zeros string of the same 33 | length as the hash's block size will be used instead per the RFC. 34 | 35 | See the HKDF draft RFC and paper for usage notes. 36 | """ 37 | hash_len = algorithm().digest_size 38 | if salt is None or len(salt) == 0: 39 | salt = bytearray((0,) * hash_len) 40 | return hmac.new(bytes(salt), buffer(input_key_material), algorithm)\ 41 | .digest() 42 | 43 | 44 | def hkdf_expand(pseudo_random_key, info=b"", length=32, 45 | algorithm=hashlib.sha256): 46 | """ 47 | Expand `pseudo_random_key` and `info` into a key of length `bytes` using 48 | HKDF's expand function based on HMAC with the provided hash (default 49 | SHA-256). See the HKDF draft RFC and paper for usage notes. 50 | """ 51 | hash_len = algorithm().digest_size 52 | length = int(length) 53 | if length > 255 * hash_len: 54 | raise Exception("Cannot expand to more than 255 * %d = %d " 55 | "bytes using the specified hash function" % 56 | (hash_len, 255 * hash_len)) 57 | blocks_needed = length // hash_len \ 58 | + (0 if length % hash_len == 0 else 1) # ceil 59 | okm = b"" 60 | output_block = b"" 61 | for counter in range(blocks_needed): 62 | output_block = hmac.new( 63 | pseudo_random_key, 64 | buffer(output_block + info + bytearray((counter + 1,))), 65 | algorithm 66 | ).digest() 67 | okm += output_block 68 | return okm[:length] 69 | 70 | 71 | class Hkdf(object): 72 | """ 73 | Wrapper class for HKDF extract and expand functions 74 | """ 75 | 76 | def __init__(self, salt, input_key_material, algorithm=hashlib.sha256): 77 | """ 78 | Extract a pseudorandom key from `salt` and `input_key_material` 79 | arguments. 80 | 81 | See the HKDF draft RFC for guidance on setting these values. 82 | The constructor optionally takes a `algorithm` argument defining 83 | the hash function use, defaulting to hashlib.sha256. 84 | """ 85 | self._hash = algorithm 86 | self._prk = hkdf_extract(salt, input_key_material, self._hash) 87 | 88 | def expand(self, info, length=32): 89 | """ 90 | Generate output key material based on an `info` value 91 | 92 | Arguments: 93 | - info - context to generate the OKM 94 | - length - length in bytes of the key to generate 95 | 96 | See the HKDF draft RFC for guidance. 97 | """ 98 | return hkdf_expand(self._prk, info, length, self._hash) 99 | -------------------------------------------------------------------------------- /shadowsocks/obfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, with_statement 18 | 19 | import hashlib 20 | import logging 21 | import os 22 | import sys 23 | 24 | from shadowsocks import common 25 | from shadowsocks.obfsplugin import ( 26 | auth, 27 | auth_chain, 28 | http_simple, 29 | obfs_tls, 30 | plain, 31 | simple_obfs_http, 32 | simple_obfs_tls, 33 | verify, 34 | ) 35 | 36 | method_supported = {} 37 | method_supported.update(plain.obfs_map) 38 | method_supported.update(http_simple.obfs_map) 39 | method_supported.update(obfs_tls.obfs_map) 40 | method_supported.update(verify.obfs_map) 41 | method_supported.update(auth.obfs_map) 42 | method_supported.update(auth_chain.obfs_map) 43 | method_supported.update(simple_obfs_http.obfs_map) 44 | method_supported.update(simple_obfs_tls.obfs_map) 45 | 46 | class server_info(object): 47 | 48 | def __init__(self, data): 49 | self.data = data 50 | 51 | 52 | class obfs(object): 53 | 54 | def __init__(self, method): 55 | method = common.to_str(method) 56 | self.method = method 57 | self._method_info = self.get_method_info(method) 58 | if self._method_info: 59 | self.obfs = self.get_obfs(method) 60 | else: 61 | raise Exception('obfs plugin [%s] not supported' % method) 62 | 63 | def init_data(self): 64 | return self.obfs.init_data() 65 | 66 | def set_server_info(self, server_info): 67 | return self.obfs.set_server_info(server_info) 68 | 69 | def get_server_info(self): 70 | return self.obfs.get_server_info() 71 | 72 | def get_method_info(self, method): 73 | method = method.lower() 74 | m = method_supported.get(method) 75 | return m 76 | 77 | def get_obfs(self, method): 78 | m = self._method_info 79 | return m[0](method) 80 | 81 | def get_overhead(self, direction): 82 | return self.obfs.get_overhead(direction) 83 | 84 | def client_pre_encrypt(self, buf): 85 | return self.obfs.client_pre_encrypt(buf) 86 | 87 | def client_encode(self, buf): 88 | return self.obfs.client_encode(buf) 89 | 90 | def client_decode(self, buf): 91 | return self.obfs.client_decode(buf) 92 | 93 | def client_post_decrypt(self, buf): 94 | return self.obfs.client_post_decrypt(buf) 95 | 96 | def server_pre_encrypt(self, buf): 97 | return self.obfs.server_pre_encrypt(buf) 98 | 99 | def server_encode(self, buf): 100 | return self.obfs.server_encode(buf) 101 | 102 | def server_decode(self, buf): 103 | return self.obfs.server_decode(buf) 104 | 105 | def server_post_decrypt(self, buf): 106 | return self.obfs.server_post_decrypt(buf) 107 | 108 | def client_udp_pre_encrypt(self, buf): 109 | return self.obfs.client_udp_pre_encrypt(buf) 110 | 111 | def client_udp_post_decrypt(self, buf): 112 | return self.obfs.client_udp_post_decrypt(buf) 113 | 114 | def server_udp_pre_encrypt(self, buf, uid): 115 | return self.obfs.server_udp_pre_encrypt(buf, uid) 116 | 117 | def server_udp_post_decrypt(self, buf): 118 | return self.obfs.server_udp_post_decrypt(buf) 119 | 120 | def dispose(self): 121 | self.obfs.dispose() 122 | del self.obfs 123 | 124 | def get_hostname(self): 125 | return self.obfs.host_name 126 | -------------------------------------------------------------------------------- /shadowsocks/lru_cache.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, with_statement 19 | 20 | import collections 21 | import logging 22 | import time 23 | 24 | if __name__ == '__main__': 25 | import os 26 | import sys 27 | import inspect 28 | file_path = os.path.dirname( 29 | os.path.realpath( 30 | inspect.getfile( 31 | inspect.currentframe()))) 32 | sys.path.insert(0, os.path.join(file_path, '../')) 33 | 34 | try: 35 | from collections import OrderedDict 36 | print("loaded collections.OrderedDict") 37 | except: 38 | from shadowsocks.ordereddict import OrderedDict 39 | 40 | # this LRUCache is optimized for concurrency, not QPS 41 | # n: concurrency, keys stored in the cache 42 | # m: visits not timed out, proportional to QPS * timeout 43 | # get & set is O(1), not O(n). thus we can support very large n 44 | # sweep is O((n - m)) or O(1024) at most, 45 | # no metter how large the cache or timeout value is 46 | 47 | SWEEP_MAX_ITEMS = 1024 48 | 49 | 50 | class LRUCache(collections.abc.MutableMapping): 51 | """This class is not thread safe""" 52 | 53 | def __init__(self, timeout=60, close_callback=None, *args, **kwargs): 54 | self.timeout = timeout 55 | self.close_callback = close_callback 56 | self._store = {} 57 | self._keys_to_last_time = OrderedDict() 58 | self.update(dict(*args, **kwargs)) # use the free update to set keys 59 | 60 | def __getitem__(self, key): 61 | # O(1) 62 | t = time.time() 63 | last_t = self._keys_to_last_time[key] 64 | del self._keys_to_last_time[key] 65 | self._keys_to_last_time[key] = t 66 | return self._store[key] 67 | 68 | def __setitem__(self, key, value): 69 | # O(1) 70 | t = time.time() 71 | if key in self._keys_to_last_time: 72 | del self._keys_to_last_time[key] 73 | self._keys_to_last_time[key] = t 74 | self._store[key] = value 75 | 76 | def __delitem__(self, key): 77 | # O(1) 78 | last_t = self._keys_to_last_time[key] 79 | del self._store[key] 80 | del self._keys_to_last_time[key] 81 | 82 | def __iter__(self): 83 | return iter(self._store) 84 | 85 | def __len__(self): 86 | return len(self._store) 87 | 88 | def first(self): 89 | if len(self._keys_to_last_time) > 0: 90 | for key in self._keys_to_last_time: 91 | return key 92 | 93 | def sweep(self, sweep_item_cnt=SWEEP_MAX_ITEMS): 94 | # O(n - m) 95 | now = time.time() 96 | c = 0 97 | while c < sweep_item_cnt: 98 | if len(self._keys_to_last_time) == 0: 99 | break 100 | for key in self._keys_to_last_time: 101 | break 102 | last_t = self._keys_to_last_time[key] 103 | if now - last_t <= self.timeout: 104 | break 105 | value = self._store[key] 106 | if self.close_callback is not None: 107 | self.close_callback(value) 108 | del self._store[key] 109 | del self._keys_to_last_time[key] 110 | c += 1 111 | if c: 112 | logging.debug('%d keys swept' % c) 113 | return c < SWEEP_MAX_ITEMS 114 | 115 | def clear(self, keep): 116 | now = time.time() 117 | c = 0 118 | while len(self._keys_to_last_time) > keep: 119 | if len(self._keys_to_last_time) == 0: 120 | break 121 | for key in self._keys_to_last_time: 122 | break 123 | last_t = self._keys_to_last_time[key] 124 | value = self._store[key] 125 | if self.close_callback is not None: 126 | self.close_callback(value) 127 | del self._store[key] 128 | del self._keys_to_last_time[key] 129 | c += 1 130 | if c: 131 | logging.debug('%d keys swept' % c) 132 | return c < SWEEP_MAX_ITEMS 133 | 134 | 135 | def test(): 136 | c = LRUCache(timeout=0.3) 137 | 138 | c['a'] = 1 139 | assert c['a'] == 1 140 | c['a'] = 1 141 | 142 | time.sleep(0.5) 143 | c.sweep() 144 | assert 'a' not in c 145 | 146 | c['a'] = 2 147 | c['b'] = 3 148 | time.sleep(0.2) 149 | c.sweep() 150 | assert c['a'] == 2 151 | assert c['b'] == 3 152 | 153 | time.sleep(0.2) 154 | c.sweep() 155 | c['b'] 156 | time.sleep(0.2) 157 | c.sweep() 158 | assert 'a' not in c 159 | assert c['b'] == 3 160 | 161 | time.sleep(0.5) 162 | c.sweep() 163 | assert 'a' not in c 164 | assert 'b' not in c 165 | 166 | global close_cb_called 167 | close_cb_called = False 168 | 169 | def close_cb(t): 170 | global close_cb_called 171 | assert not close_cb_called 172 | close_cb_called = True 173 | 174 | c = LRUCache(timeout=0.1, close_callback=close_cb) 175 | c['s'] = 1 176 | c['s'] 177 | time.sleep(0.1) 178 | c['s'] 179 | time.sleep(0.3) 180 | c.sweep() 181 | 182 | if __name__ == '__main__': 183 | test() 184 | -------------------------------------------------------------------------------- /shadowsocks/crypto/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, with_statement 18 | 19 | import logging 20 | import os 21 | 22 | def find_library_nt(name): 23 | # modified from ctypes.util 24 | # ctypes.util.find_library just returns first result he found 25 | # but we want to try them all 26 | # because on Windows, users may have both 32bit and 64bit version installed 27 | import glob 28 | results = [] 29 | for directory in os.environ['PATH'].split(os.pathsep): 30 | fname = os.path.join(directory, name) 31 | if os.path.isfile(fname): 32 | results.append(fname) 33 | if fname.lower().endswith(".dll"): 34 | continue 35 | fname += "*.dll" 36 | files = glob.glob(fname) 37 | if files: 38 | results.extend(files) 39 | return results 40 | 41 | 42 | def load_library(path, search_symbol, library_name): 43 | from ctypes import CDLL 44 | try: 45 | lib = CDLL(path) 46 | if hasattr(lib, search_symbol): 47 | logging.info('loading %s from %s', library_name, path) 48 | return lib 49 | else: 50 | logging.warn('can\'t find symbol %s in %s', search_symbol, 51 | path) 52 | except Exception: 53 | pass 54 | return None 55 | 56 | 57 | def find_library(possible_lib_names, search_symbol, library_name, 58 | custom_path=None): 59 | import ctypes.util 60 | 61 | if custom_path: 62 | return load_library(custom_path, search_symbol, library_name) 63 | 64 | paths = [] 65 | 66 | if type(possible_lib_names) not in (list, tuple): 67 | possible_lib_names = [possible_lib_names] 68 | 69 | lib_names = [] 70 | for lib_name in possible_lib_names: 71 | lib_names.append(lib_name) 72 | lib_names.append('lib' + lib_name) 73 | 74 | for name in lib_names: 75 | if os.name == "nt": 76 | paths.extend(find_library_nt(name)) 77 | else: 78 | path = ctypes.util.find_library(name) 79 | if path: 80 | paths.append(path) 81 | 82 | # always find lib on extend path that to avoid ```CDLL()``` failed on some strange linux environment 83 | # in that case ```ctypes.util.find_library()``` have different find path from ```CDLL()``` 84 | if True: 85 | # We may get here when find_library fails because, for example, 86 | # the user does not have sufficient privileges to access those 87 | # tools underlying find_library on linux. 88 | import glob 89 | 90 | for name in lib_names: 91 | patterns = [ 92 | '/data/data/com.termux/files/usr/lib*/lib%s.*' % name, 93 | '/usr/local/lib*/lib%s.*' % name, 94 | '/usr/lib*/lib%s.*' % name, 95 | 'lib%s.*' % name, 96 | '%s.dll' % name] 97 | 98 | for pat in patterns: 99 | files = glob.glob(pat) 100 | if files: 101 | paths.extend(files) 102 | for path in paths: 103 | lib = load_library(path, search_symbol, library_name) 104 | if lib: 105 | return lib 106 | return None 107 | 108 | 109 | def parse_mode(cipher_nme): 110 | """ 111 | Parse the cipher mode from cipher name 112 | e.g. aes-128-gcm, the mode is gcm 113 | :param cipher_nme: str cipher name, aes-128-cfb, aes-128-gcm ... 114 | :return: str/None The mode, cfb, gcm ... 115 | """ 116 | hyphen = cipher_nme.rfind('-') 117 | if hyphen > 0: 118 | return cipher_nme[hyphen:] 119 | return None 120 | 121 | 122 | def run_cipher(cipher, decipher): 123 | from os import urandom 124 | import random 125 | import time 126 | 127 | block_size = 16384 128 | rounds = 1 * 1024 129 | plain = urandom(block_size * rounds) 130 | 131 | cipher_results = [] 132 | pos = 0 133 | print('test start') 134 | start = time.time() 135 | while pos < len(plain): 136 | l = random.randint(100, 32768) 137 | # print(pos, l) 138 | c = cipher.encrypt_once(plain[pos:pos + l]) 139 | cipher_results.append(c) 140 | pos += l 141 | pos = 0 142 | # c = b''.join(cipher_results) 143 | plain_results = [] 144 | for c in cipher_results: 145 | # l = random.randint(100, 32768) 146 | l = len(c) 147 | plain_results.append(decipher.decrypt_once(c)) 148 | pos += l 149 | end = time.time() 150 | print('speed: %d bytes/s' % (block_size * rounds / (end - start))) 151 | assert b''.join(plain_results) == plain 152 | 153 | 154 | def test_find_library(): 155 | assert find_library('c', 'strcpy', 'libc') is not None 156 | assert find_library(['c'], 'strcpy', 'libc') is not None 157 | assert find_library(('c',), 'strcpy', 'libc') is not None 158 | assert find_library(('crypto', 'eay32'), 'EVP_CipherUpdate', 159 | 'libcrypto') is not None 160 | assert find_library('notexist', 'strcpy', 'libnotexist') is None 161 | assert find_library('c', 'symbol_not_exist', 'c') is None 162 | assert find_library(('notexist', 'c', 'crypto', 'eay32'), 163 | 'EVP_CipherUpdate', 'libc') is not None 164 | 165 | 166 | if __name__ == '__main__': 167 | test_find_library() 168 | -------------------------------------------------------------------------------- /shadowsocks/daemon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2014-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, with_statement 19 | 20 | import logging 21 | import os 22 | import signal 23 | import sys 24 | import time 25 | 26 | from shadowsocks import common, shell 27 | 28 | # this module is ported from ShadowVPN daemon.c 29 | 30 | 31 | def daemon_exec(config): 32 | if 'daemon' in config: 33 | if os.name != 'posix': 34 | raise Exception('daemon mode is only supported on Unix') 35 | command = config['daemon'] 36 | if not command: 37 | command = 'start' 38 | pid_file = config['pid-file'] 39 | log_file = config['log-file'] 40 | if command == 'start': 41 | daemon_start(pid_file, log_file) 42 | elif command == 'stop': 43 | daemon_stop(pid_file) 44 | # always exit after daemon_stop 45 | sys.exit(0) 46 | elif command == 'restart': 47 | daemon_stop(pid_file) 48 | daemon_start(pid_file, log_file) 49 | else: 50 | raise Exception('unsupported daemon command %s' % command) 51 | 52 | 53 | def write_pid_file(pid_file, pid): 54 | import fcntl 55 | import stat 56 | 57 | try: 58 | fd = os.open(pid_file, os.O_RDWR | os.O_CREAT, 59 | stat.S_IRUSR | stat.S_IWUSR) 60 | except OSError as e: 61 | shell.print_exception(e) 62 | return -1 63 | flags = fcntl.fcntl(fd, fcntl.F_GETFD) 64 | assert flags != -1 65 | flags |= fcntl.FD_CLOEXEC 66 | r = fcntl.fcntl(fd, fcntl.F_SETFD, flags) 67 | assert r != -1 68 | # There is no platform independent way to implement fcntl(fd, F_SETLK, &fl) 69 | # via fcntl.fcntl. So use lockf instead 70 | try: 71 | fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET) 72 | except IOError: 73 | r = os.read(fd, 32) 74 | if r: 75 | logging.error('already started at pid %s' % common.to_str(r)) 76 | else: 77 | logging.error('already started') 78 | os.close(fd) 79 | return -1 80 | os.ftruncate(fd, 0) 81 | os.write(fd, common.to_bytes(str(pid))) 82 | return 0 83 | 84 | 85 | def freopen(f, mode, stream): 86 | oldf = open(f, mode) 87 | oldfd = oldf.fileno() 88 | newfd = stream.fileno() 89 | os.close(newfd) 90 | os.dup2(oldfd, newfd) 91 | 92 | 93 | def daemon_start(pid_file, log_file): 94 | 95 | def handle_exit(signum, _): 96 | if signum == signal.SIGTERM: 97 | sys.exit(0) 98 | sys.exit(1) 99 | 100 | signal.signal(signal.SIGINT, handle_exit) 101 | signal.signal(signal.SIGTERM, handle_exit) 102 | 103 | # fork only once because we are sure parent will exit 104 | pid = os.fork() 105 | assert pid != -1 106 | 107 | if pid > 0: 108 | # parent waits for its child 109 | time.sleep(5) 110 | sys.exit(0) 111 | 112 | # child signals its parent to exit 113 | ppid = os.getppid() 114 | pid = os.getpid() 115 | if write_pid_file(pid_file, pid) != 0: 116 | os.kill(ppid, signal.SIGINT) 117 | sys.exit(1) 118 | 119 | os.setsid() 120 | signal.signal(signal.SIG_IGN, signal.SIGHUP) 121 | 122 | print('started') 123 | os.kill(ppid, signal.SIGTERM) 124 | 125 | sys.stdin.close() 126 | try: 127 | freopen(log_file, 'a', sys.stdout) 128 | freopen(log_file, 'a', sys.stderr) 129 | except IOError as e: 130 | shell.print_exception(e) 131 | sys.exit(1) 132 | 133 | 134 | def daemon_stop(pid_file): 135 | import errno 136 | try: 137 | with open(pid_file) as f: 138 | buf = f.read() 139 | pid = common.to_str(buf) 140 | if not buf: 141 | logging.error('not running') 142 | except IOError as e: 143 | shell.print_exception(e) 144 | if e.errno == errno.ENOENT: 145 | # always exit 0 if we are sure daemon is not running 146 | logging.error('not running') 147 | return 148 | sys.exit(1) 149 | pid = int(pid) 150 | if pid > 0: 151 | try: 152 | os.kill(pid, signal.SIGTERM) 153 | except OSError as e: 154 | if e.errno == errno.ESRCH: 155 | logging.error('not running') 156 | # always exit 0 if we are sure daemon is not running 157 | return 158 | shell.print_exception(e) 159 | sys.exit(1) 160 | else: 161 | logging.error('pid is not positive: %d', pid) 162 | 163 | # sleep for maximum 10s 164 | for i in range(0, 200): 165 | try: 166 | # query for the pid 167 | os.kill(pid, 0) 168 | except OSError as e: 169 | if e.errno == errno.ESRCH: 170 | break 171 | time.sleep(0.05) 172 | else: 173 | logging.error('timed out when stopping pid %d', pid) 174 | sys.exit(1) 175 | print('stopped') 176 | os.unlink(pid_file) 177 | 178 | 179 | def set_user(username): 180 | if username is None: 181 | return 182 | 183 | import pwd 184 | import grp 185 | 186 | try: 187 | pwrec = pwd.getpwnam(username) 188 | except KeyError: 189 | logging.error('user not found: %s' % username) 190 | raise 191 | user = pwrec[0] 192 | uid = pwrec[2] 193 | gid = pwrec[3] 194 | 195 | cur_uid = os.getuid() 196 | if uid == cur_uid: 197 | return 198 | if cur_uid != 0: 199 | logging.error('can not set user as nonroot user') 200 | # will raise later 201 | 202 | # inspired by supervisor 203 | if hasattr(os, 'setgroups'): 204 | groups = [grprec[2] for grprec in grp.getgrall() if user in grprec[3]] 205 | groups.insert(0, gid) 206 | os.setgroups(groups) 207 | os.setgid(gid) 208 | os.setuid(uid) 209 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/simple_obfs_http.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | import binascii 25 | import struct 26 | import base64 27 | import datetime 28 | import random 29 | 30 | from shadowsocks import common 31 | from shadowsocks.obfsplugin import plain 32 | from shadowsocks.common import to_bytes, to_str, ord, chr 33 | 34 | 35 | def create_simple_obfs_http_obfs(method): 36 | return simple_obfs_http(method) 37 | 38 | 39 | obfs_map = { 40 | 'simple_obfs_http': (create_simple_obfs_http_obfs,), 41 | 'simple_obfs_http_compatible': (create_simple_obfs_http_obfs,), 42 | } 43 | 44 | 45 | def match_begin(str1, str2): 46 | if len(str1) >= len(str2): 47 | if str1[:len(str2)] == str2: 48 | return True 49 | return False 50 | 51 | 52 | class simple_obfs_http(plain.plain): 53 | def __init__(self, method): 54 | self.method = method 55 | self.has_sent_header = False 56 | self.has_recv_header = False 57 | self.host = None 58 | self.port = 0 59 | self.recv_buffer = b'' 60 | 61 | self.curl_version = b"7." + common.to_bytes(str(random.randint(0, 51))) + b"." + common.to_bytes( 62 | str(random.randint(0, 2))) 63 | self.nginx_version = b"1." + common.to_bytes(str(random.randint(0, 11))) + b"." + common.to_bytes( 64 | str(random.randint(0, 12))) 65 | 66 | def encode_head(self, buf): 67 | hexstr = binascii.hexlify(buf) 68 | chs = [] 69 | for i in range(0, len(hexstr), 2): 70 | chs.append(b"%" + hexstr[i:i + 2]) 71 | return b''.join(chs) 72 | 73 | def client_encode(self, buf): 74 | raise Exception('Need to finish') 75 | if self.has_sent_header: 76 | return buf 77 | port = b'' 78 | if self.server_info.port != 80: 79 | port = b':' + to_bytes(str(self.server_info.port)) 80 | hosts = (self.server_info.obfs_param or self.server_info.host) 81 | pos = hosts.find("#") 82 | if pos >= 0: 83 | body = hosts[pos + 1:].replace("\\n", "\r\n") 84 | hosts = hosts[:pos] 85 | hosts = hosts.split(',') 86 | host = random.choice(hosts) 87 | http_head = b"GET /" + b" HTTP/1.1\r\n" 88 | http_head += b"Host: " + to_bytes(host) + port + b"\r\n" 89 | http_head += b"User-Agent: curl/" + self.curl_version + b"\r\n" 90 | http_head += b"Upgrade: websocket\r\n" 91 | http_head += b"Connection: Upgrade\r\n" 92 | http_head += b"Sec-WebSocket-Key: " + common.to_bytes(common.random_base64_str(64)) + b"\r\n" 93 | http_head += b"Content-Length: " + len(buf) + b"\r\n" 94 | http_head += b"\r\n" 95 | self.has_sent_header = True 96 | return http_head + buf 97 | 98 | def client_decode(self, buf): 99 | raise Exception('Need to finish') 100 | if self.has_recv_header: 101 | return (buf, False) 102 | pos = buf.find(b'\r\n\r\n') 103 | if pos >= 0: 104 | self.has_recv_header = True 105 | return (buf[pos + 4:], False) 106 | else: 107 | return (b'', False) 108 | 109 | def server_encode(self, buf): 110 | if self.has_sent_header: 111 | return buf 112 | 113 | data = b''.join([b'HTTP/1.1 101 Switching Protocols\r\n', 114 | b'Server: nginx/', 115 | self.nginx_version, 116 | b'\r\n', 117 | b'Date: ', 118 | to_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')), 119 | b'\r\n', 120 | b'Upgrade: websocket\r\n', 121 | b'Connection: Upgrade\r\n', 122 | b'Sec-WebSocket-Accept: ', 123 | common.to_bytes(common.random_base64_str(64)), 124 | b'\r\n', 125 | b'\r\n', 126 | buf]) 127 | 128 | self.has_sent_header = True 129 | return data 130 | 131 | def get_host_from_http_header(self, buf): 132 | ret_buf = b'' 133 | lines = buf.split(b'\r\n') 134 | if lines and len(lines) > 1: 135 | for line in lines: 136 | if match_begin(line, b"Host: "): 137 | return common.to_str(line[6:]) 138 | 139 | def not_match_return(self, buf): 140 | self.has_sent_header = True 141 | self.has_recv_header = True 142 | if self.method == 'simple_obfs_http': 143 | return (b'E' * 2048, False, False) 144 | return (buf, True, False) 145 | 146 | def server_decode(self, buf): 147 | if self.has_recv_header: 148 | return (buf, True, False) 149 | 150 | self.recv_buffer += buf 151 | buf = self.recv_buffer 152 | if len(buf) > 4: 153 | if match_begin(buf, b'GET /') or match_begin(buf, b'POST /'): 154 | if len(buf) > 65536: 155 | self.recv_buffer = None 156 | logging.warn('simple_obfs_http: over size') 157 | return self.not_match_return(buf) 158 | else: # not http header, run on original protocol 159 | self.recv_buffer = None 160 | logging.debug('simple_obfs_http: not match begin') 161 | return self.not_match_return(buf) 162 | else: 163 | return (b'', True, False) 164 | 165 | if b'\r\n\r\n' in buf: 166 | if b'Upgrade: websocket' not in buf: 167 | self.recv_buffer = None 168 | logging.debug('simple_obfs_http: protocol error') 169 | return self.not_match_return(buf) 170 | datas = buf.split(b'\r\n\r\n', 1) 171 | host = self.get_host_from_http_header(buf) 172 | if host and self.server_info.obfs_param: 173 | pos = host.find(":") 174 | if pos >= 0: 175 | host = host[:pos] 176 | hosts = self.server_info.obfs_param.split(b',') 177 | if common.to_bytes(host) not in hosts: 178 | return self.not_match_return(buf) 179 | if len(datas) > 1: 180 | self.has_recv_header = True 181 | return (datas[1], True, False, host) 182 | return self.not_match_return(buf) 183 | else: 184 | return (b'', True, False) 185 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 3.5.1 2 | - fix bugs 3 | 4 | 3.5.0 5 | - Compatible for SSPanel-UIM@2021.5 version 6 | 7 | 3.4.0 8 | - add auth_chain_b 9 | - add initmudbjson.sh 10 | - fix bugs & mem leak 11 | - add ss aead and obfs support(by mod) 12 | 13 | 3.3.4 2017-06-03 14 | - add DNS cache 15 | - add tls1.2_ticket_fastauth 16 | - fix bugs 17 | 18 | 3.3.3 2017-05-28 19 | - fix overhead size 20 | - fix type 21 | 22 | 3.3.2 2017-05-20 23 | - revert http reply 24 | - refine tls1.2_ticket_auth error detector 25 | - tls1.2 0rtt 26 | 27 | 3.3.1 2017-05-18 28 | - fix stop script 29 | - Async DNS query under UDP 30 | - fix old version of OpenSSL 31 | - friendly detect block page 32 | 33 | 3.3.0 2017-05-11 34 | - connect_log include local addr & port 35 | - fix auth_chain_a UDP bug 36 | - add "additional_ports_only" 37 | - add interface legendsockssr 38 | - run with newest python version 39 | - parse comment in hosts 40 | - update mujson_mgr 41 | - add cymysql setup script 42 | - new speed tester 43 | - fix leaks 44 | - bugs fixed 45 | 46 | 3.2.0 2017-04-27 47 | - add auth_chain_a 48 | 49 | 3.1.2 2017-04-07 50 | - display UID 51 | - auto adjust TCP MSS 52 | 53 | 3.1.1 2017-03-25 54 | - add "New session ticket" 55 | - ignore bind 10.0.0.0/8 and 192.168.0.0/16 by default 56 | - improve rand size under auth_aes128_* 57 | - fix bugs 58 | 59 | 3.1.0 2017-03-16 60 | - add "glzjinmod" interface 61 | - rate limit 62 | - add additional_ports in config 63 | 64 | 3.0.4 2017-01-08 65 | - multi-user in single port 66 | 67 | 3.0.1 2017-01-03 68 | - remove auth_aes128_*_compatible 69 | 70 | 3.0.0 2016-12-23 71 | - http_simple fix bugs 72 | - tls1.2_ticket_auth fix bug & defaule time diff set to 86400s 73 | 74 | 2.9.7 2016-11-22 75 | - manage client with LRUCache 76 | - catch bind error 77 | - fix import error of resource on windows 78 | - print RLIMIT_NOFILE 79 | - always close cymysql objects 80 | - add init script 81 | 82 | 2.9.6 2016-10-17 83 | - tls1.2_ticket_auth random packet size 84 | 85 | 2.9.5.1 2016-10-16 86 | - UDP bind address 87 | 88 | 2.9.5 2016-10-13 89 | - add auth_aes128_md5 and auth_aes128_sha1 90 | 91 | 2.9.4 2016-10-11 92 | - sync client version 93 | 94 | 2.6.13 2015-11-02 95 | - add protocol setting 96 | 97 | 2.6.12 2015-10-27 98 | - IPv6 first 99 | - Fix mem leaks 100 | - auth_simple plugin 101 | - remove FORCE_NEW_PROTOCOL 102 | - optimize code 103 | 104 | 2.6.11 2015-10-20 105 | - Obfs plugin 106 | - Obfs parameters 107 | - UDP over TCP 108 | - TCP over UDP (experimental) 109 | - Fix socket leaks 110 | - Catch abnormal UDP package 111 | 112 | 2.6.10 2015-06-08 113 | - Optimize LRU cache 114 | - Refine logging 115 | 116 | 2.6.9 2015-05-19 117 | - Fix a stability issue on Windows 118 | 119 | 2.6.8 2015-02-10 120 | - Support multiple server ip on client side 121 | - Support --version 122 | - Minor fixes 123 | 124 | 2.6.7 2015-02-02 125 | - Support --user 126 | - Support CIDR format in --forbidden-ip 127 | - Minor fixes 128 | 129 | 2.6.6 2015-01-23 130 | - Fix a crash in forbidden list 131 | 132 | 2.6.5 2015-01-18 133 | - Try both 32 bit and 64 bit dll on Windows 134 | 135 | 2.6.4 2015-01-14 136 | - Also search lib* when searching libraries 137 | 138 | 2.6.3 2015-01-12 139 | - Support --forbidden-ip to ban some IP, i.e. localhost 140 | - Search OpenSSL and libsodium harder 141 | - Now works on OpenWRT 142 | 143 | 2.6.2 2015-01-03 144 | - Log client IP 145 | 146 | 2.6.1 2014-12-26 147 | - Fix a problem with TCP Fast Open on local side 148 | - Fix sometimes daemon_start returns wrong exit status 149 | 150 | 2.6 2014-12-21 151 | - Add daemon support 152 | 153 | 2.5 2014-12-11 154 | - Add salsa20 and chacha20 155 | 156 | 2.4.3 2014-11-10 157 | - Fix an issue on Python 3 158 | - Fix an issue with IPv6 159 | 160 | 2.4.2 2014-11-06 161 | - Fix command line arguments on Python 3 162 | - Support table on Python 3 163 | - Fix TCP Fast Open on Python 3 164 | 165 | 2.4.1 2014-11-01 166 | - Fix setup.py for non-utf8 locales on Python 3 167 | 168 | 2.4 2014-11-01 169 | - Python 3 support 170 | - Performance improvement 171 | - Fix LRU cache behavior 172 | 173 | 2.3.2 2014-10-11 174 | - Fix OpenSSL on Windows 175 | 176 | 2.3.1 2014-10-09 177 | - Does not require M2Crypto any more 178 | 179 | 2.3 2014-09-23 180 | - Support CFB1, CFB8 and CTR mode of AES 181 | - Do not require password config when using port_password 182 | - Use SIGTERM instead of SIGQUIT on Windows 183 | 184 | 2.2.2 2014-09-14 185 | - Fix when multiple DNS set, IPv6 only sites are broken 186 | 187 | 2.2.1 2014-09-10 188 | - Support graceful shutdown 189 | - Fix some bugs 190 | 191 | 2.2.0 2014-09-09 192 | - Add RC4-MD5 encryption 193 | 194 | 2.1.0 2014-08-10 195 | - Use only IPv4 DNS server 196 | - Does not ship config.json 197 | - Better error message 198 | 199 | 2.0.12 2014-07-26 200 | - Support -q quiet mode 201 | - Exit 0 when showing help with -h 202 | 203 | 2.0.11 2014-07-12 204 | - Prefers IP addresses over hostnames, more friendly with socksify and openvpn 205 | 206 | 2.0.10 2014-07-11 207 | - Fix UDP on local 208 | 209 | 2.0.9 2014-07-06 210 | - Fix EWOULDBLOCK on Windows 211 | - Fix Unicode config problem on some platforms 212 | 213 | 2.0.8 2014-06-23 214 | - Use multiple DNS to query hostnames 215 | 216 | 2.0.7 2014-06-21 217 | - Fix fastopen on local 218 | - Fallback when fastopen is not available 219 | - Add verbose logging mode -vv 220 | - Verify if hostname is valid 221 | 222 | 2.0.6 2014-06-19 223 | - Fix CPU 100% on POLL_HUP 224 | - More friendly logging 225 | 226 | 2.0.5 2014-06-18 227 | - Support a simple config format for multiple ports 228 | 229 | 2.0.4 2014-06-12 230 | - Fix worker master 231 | 232 | 2.0.3 2014-06-11 233 | - Fix table encryption with UDP 234 | 235 | 2.0.2 2014-06-11 236 | - Add asynchronous DNS in TCP relay 237 | 238 | 2.0.1 2014-06-05 239 | - Better logging 240 | - Maybe fix bad file descriptor 241 | 242 | 2.0 2014-06-05 243 | - Use a new event model 244 | - Remove gevent 245 | - Refuse to use default password 246 | - Fix a problem when using multiple passwords with table encryption 247 | 248 | 1.4.5 2014-05-24 249 | - Add timeout in TCP server 250 | - Close sockets in master process 251 | 252 | 1.4.4 2014-05-17 253 | - Support multiple workers 254 | 255 | 1.4.3 2014-05-13 256 | - Fix Windows 257 | 258 | 1.4.2 2014-05-10 259 | - Add salsa20-ctr cipher 260 | 261 | 1.4.1 2014-05-03 262 | - Fix error log 263 | - Fix EINPROGESS with some version of gevent 264 | 265 | 1.4.0 2014-05-02 266 | - Adds UDP relay 267 | - TCP fast open support on Linux 3.7+ 268 | 269 | 1.3.7 2014-04-10 270 | - Fix a typo in help 271 | 272 | 1.3.6 2014-04-10 273 | - Fix a typo in help 274 | 275 | 1.3.5 2014-04-07 276 | - Add help 277 | - Change default local binding address into 127.0.0.1 278 | 279 | 1.3.4 2014-02-17 280 | - Fix a bug when no config file exists 281 | - Client now support multiple server ports and multiple server/port pairs 282 | - Better error message with bad config.json format and wrong password 283 | 284 | 1.3.3 2013-07-09 285 | - Fix default key length of rc2 286 | 287 | 1.3.2 2013-07-04 288 | - Server will listen at server IP specified in config 289 | - Check config file and show some warning messages 290 | 291 | 1.3.1 2013-06-29 292 | - Fix -c arg 293 | 294 | 1.3.0 2013-06-22 295 | - Move to pypi 296 | 297 | 1.2.3 2013-06-14 298 | - add bind address 299 | 300 | 1.2.2 2013-05-31 301 | - local can listen at ::0 with -6 arg; bump 1.2.2 302 | 303 | 1.2.1 2013-05-23 304 | - Fix an OpenSSL crash 305 | 306 | 1.2 2013-05-22 307 | - Use random iv, we finally have strong encryption 308 | 309 | 1.1.1 2013-05-21 310 | - Add encryption, AES, blowfish, etc. 311 | 312 | 1.1 2013-05-16 313 | - Support IPv6 addresses (type 4) 314 | - Drop Python 2.5 support 315 | 316 | 1.0 2013-04-03 317 | - Fix -6 IPv6 318 | 319 | 0.9.4 2013-03-04 320 | - Support Python 2.5 321 | 322 | 0.9.3 2013-01-14 323 | - Fix conn termination null data 324 | 325 | 0.9.2 2013-01-05 326 | - Change default timeout 327 | 328 | 0.9.1 2013-01-05 329 | - Add Travis-CI test 330 | 331 | 0.9 2012-12-30 332 | - Replace send with sendall, fix FreeBSD 333 | 334 | 0.6 2012-12-06 335 | - Support args 336 | 337 | 0.5 2012-11-08 338 | - Fix encryption with negative md5sum 339 | 340 | 0.4 2012-11-02 341 | - Move config into a JSON file 342 | - Auto-detect config path 343 | 344 | 0.3 2012-06-06 345 | - Move socks5 negotiation to local 346 | 347 | 0.2 2012-05-11 348 | - Add -6 arg for IPv6 349 | - Fix socket.error 350 | 351 | 0.1 2012-04-20 352 | - Initial version 353 | -------------------------------------------------------------------------------- /shadowsocks/ordereddict.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | ########################################################################## 4 | # OrderedDict 5 | ########################################################################## 6 | 7 | 8 | class OrderedDict(dict): 9 | 'Dictionary that remembers insertion order' 10 | # An inherited dict maps keys to values. 11 | # The inherited dict provides __getitem__, __len__, __contains__, and get. 12 | # The remaining methods are order-aware. 13 | # Big-O running times for all methods are the same as regular dictionaries. 14 | 15 | # The internal self.__map dict maps keys to links in a doubly linked list. 16 | # The circular doubly linked list starts and ends with a sentinel element. 17 | # The sentinel element never gets deleted (this simplifies the algorithm). 18 | # Each link is stored as a list of length three: [PREV, NEXT, KEY]. 19 | 20 | def __init__(*args, **kwds): 21 | '''Initialize an ordered dictionary. The signature is the same as 22 | regular dictionaries, but keyword arguments are not recommended because 23 | their insertion order is arbitrary. 24 | 25 | ''' 26 | if not args: 27 | raise TypeError("descriptor '__init__' of 'OrderedDict' object " 28 | "needs an argument") 29 | self = args[0] 30 | args = args[1:] 31 | if len(args) > 1: 32 | raise TypeError('expected at most 1 arguments, got %d' % len(args)) 33 | try: 34 | self.__root 35 | except AttributeError: 36 | self.__root = root = [] # sentinel node 37 | root[:] = [root, root, None] 38 | self.__map = {} 39 | self.__update(*args, **kwds) 40 | 41 | def __setitem__(self, key, value, dict_setitem=dict.__setitem__): 42 | 'od.__setitem__(i, y) <==> od[i]=y' 43 | # Setting a new item creates a new link at the end of the linked list, 44 | # and the inherited dictionary is updated with the new key/value pair. 45 | if key not in self: 46 | root = self.__root 47 | last = root[0] 48 | last[1] = root[0] = self.__map[key] = [last, root, key] 49 | return dict_setitem(self, key, value) 50 | 51 | def __delitem__(self, key, dict_delitem=dict.__delitem__): 52 | 'od.__delitem__(y) <==> del od[y]' 53 | # Deleting an existing item uses self.__map to find the link which gets 54 | # removed by updating the links in the predecessor and successor nodes. 55 | dict_delitem(self, key) 56 | link_prev, link_next, _ = self.__map.pop(key) 57 | # update link_prev[NEXT] 58 | link_prev[1] = link_next 59 | # update link_next[PREV] 60 | link_next[0] = link_prev 61 | 62 | def __iter__(self): 63 | 'od.__iter__() <==> iter(od)' 64 | # Traverse the linked list in order. 65 | root = self.__root 66 | # start at the first node 67 | curr = root[1] 68 | while curr is not root: 69 | yield curr[2] # yield the curr[KEY] 70 | curr = curr[1] # move to next node 71 | 72 | def __reversed__(self): 73 | 'od.__reversed__() <==> reversed(od)' 74 | # Traverse the linked list in reverse order. 75 | root = self.__root 76 | # start at the last node 77 | curr = root[0] 78 | while curr is not root: 79 | yield curr[2] # yield the curr[KEY] 80 | curr = curr[0] # move to previous node 81 | 82 | def clear(self): 83 | 'od.clear() -> None. Remove all items from od.' 84 | root = self.__root 85 | root[:] = [root, root, None] 86 | self.__map.clear() 87 | dict.clear(self) 88 | 89 | # -- the following methods do not depend on the internal structure -- 90 | 91 | def keys(self): 92 | 'od.keys() -> list of keys in od' 93 | return list(self) 94 | 95 | def values(self): 96 | 'od.values() -> list of values in od' 97 | return [self[key] for key in self] 98 | 99 | def items(self): 100 | 'od.items() -> list of (key, value) pairs in od' 101 | return [(key, self[key]) for key in self] 102 | 103 | def iterkeys(self): 104 | 'od.iterkeys() -> an iterator over the keys in od' 105 | return iter(self) 106 | 107 | def itervalues(self): 108 | 'od.itervalues -> an iterator over the values in od' 109 | for k in self: 110 | yield self[k] 111 | 112 | def iteritems(self): 113 | 'od.iteritems -> an iterator over the (key, value) pairs in od' 114 | for k in self: 115 | yield (k, self[k]) 116 | 117 | update = collections.MutableMapping.update 118 | 119 | __update = update # let subclasses override update without breaking __init__ 120 | 121 | __marker = object() 122 | 123 | def pop(self, key, default=__marker): 124 | '''od.pop(k[,d]) -> v, remove specified key and return the corresponding 125 | value. If key is not found, d is returned if given, otherwise KeyError 126 | is raised. 127 | 128 | ''' 129 | if key in self: 130 | result = self[key] 131 | del self[key] 132 | return result 133 | if default is self.__marker: 134 | raise KeyError(key) 135 | return default 136 | 137 | def setdefault(self, key, default=None): 138 | 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' 139 | if key in self: 140 | return self[key] 141 | self[key] = default 142 | return default 143 | 144 | def popitem(self, last=True): 145 | '''od.popitem() -> (k, v), return and remove a (key, value) pair. 146 | Pairs are returned in LIFO order if last is true or FIFO order if false. 147 | 148 | ''' 149 | if not self: 150 | raise KeyError('dictionary is empty') 151 | key = next(reversed(self) if last else iter(self)) 152 | value = self.pop(key) 153 | return key, value 154 | 155 | def __repr__(self, _repr_running={}): 156 | 'od.__repr__() <==> repr(od)' 157 | call_key = id(self), _get_ident() 158 | if call_key in _repr_running: 159 | return '...' 160 | _repr_running[call_key] = 1 161 | try: 162 | if not self: 163 | return '%s()' % (self.__class__.__name__,) 164 | return '%s(%r)' % (self.__class__.__name__, self.items()) 165 | finally: 166 | del _repr_running[call_key] 167 | 168 | def __reduce__(self): 169 | 'Return state information for pickling' 170 | items = [[k, self[k]] for k in self] 171 | inst_dict = vars(self).copy() 172 | for k in vars(OrderedDict()): 173 | inst_dict.pop(k, None) 174 | if inst_dict: 175 | return (self.__class__, (items,), inst_dict) 176 | return self.__class__, (items,) 177 | 178 | def copy(self): 179 | 'od.copy() -> a shallow copy of od' 180 | return self.__class__(self) 181 | 182 | @classmethod 183 | def fromkeys(cls, iterable, value=None): 184 | '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. 185 | If not specified, the value defaults to None. 186 | 187 | ''' 188 | self = cls() 189 | for key in iterable: 190 | self[key] = value 191 | return self 192 | 193 | def __eq__(self, other): 194 | '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive 195 | while comparison to a regular mapping is order-insensitive. 196 | 197 | ''' 198 | if isinstance(other, OrderedDict): 199 | return dict.__eq__(self, other) and all(_imap(_eq, self, other)) 200 | return dict.__eq__(self, other) 201 | 202 | def __ne__(self, other): 203 | 'od.__ne__(y) <==> od!=y' 204 | return not self == other 205 | 206 | # -- the following methods support python 3.x style dictionary views -- 207 | 208 | def viewkeys(self): 209 | "od.viewkeys() -> a set-like object providing a view on od's keys" 210 | return KeysView(self) 211 | 212 | def viewvalues(self): 213 | "od.viewvalues() -> an object providing a view on od's values" 214 | return ValuesView(self) 215 | 216 | def viewitems(self): 217 | "od.viewitems() -> a set-like object providing a view on od's items" 218 | return ItemsView(self) 219 | -------------------------------------------------------------------------------- /shadowsocks/eventloop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2013-2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | # from ssloop 19 | # https://github.com/clowwindy/ssloop 20 | 21 | from __future__ import absolute_import, division, print_function, with_statement 22 | 23 | import errno 24 | import logging 25 | import os 26 | import select 27 | import socket 28 | import time 29 | from collections import defaultdict 30 | 31 | from shadowsocks import shell 32 | 33 | __all__ = ['EventLoop', 'POLL_NULL', 'POLL_IN', 'POLL_OUT', 'POLL_ERR', 34 | 'POLL_HUP', 'POLL_NVAL', 'EVENT_NAMES'] 35 | 36 | POLL_NULL = 0x00 37 | POLL_IN = 0x01 38 | POLL_OUT = 0x04 39 | POLL_ERR = 0x08 40 | POLL_HUP = 0x10 41 | POLL_NVAL = 0x20 42 | 43 | 44 | EVENT_NAMES = { 45 | POLL_NULL: 'POLL_NULL', 46 | POLL_IN: 'POLL_IN', 47 | POLL_OUT: 'POLL_OUT', 48 | POLL_ERR: 'POLL_ERR', 49 | POLL_HUP: 'POLL_HUP', 50 | POLL_NVAL: 'POLL_NVAL', 51 | } 52 | 53 | # we check timeouts every TIMEOUT_PRECISION seconds 54 | TIMEOUT_PRECISION = 2 55 | 56 | 57 | class KqueueLoop(object): 58 | 59 | MAX_EVENTS = 1024 60 | 61 | def __init__(self): 62 | self._kqueue = select.kqueue() 63 | self._fds = {} 64 | 65 | def _control(self, fd, mode, flags): 66 | events = [] 67 | if mode & POLL_IN: 68 | events.append(select.kevent(fd, select.KQ_FILTER_READ, flags)) 69 | if mode & POLL_OUT: 70 | events.append(select.kevent(fd, select.KQ_FILTER_WRITE, flags)) 71 | for e in events: 72 | self._kqueue.control([e], 0) 73 | 74 | def poll(self, timeout): 75 | if timeout < 0: 76 | timeout = None # kqueue behaviour 77 | events = self._kqueue.control(None, KqueueLoop.MAX_EVENTS, timeout) 78 | results = defaultdict(lambda: POLL_NULL) 79 | for e in events: 80 | fd = e.ident 81 | if e.filter == select.KQ_FILTER_READ: 82 | results[fd] |= POLL_IN 83 | elif e.filter == select.KQ_FILTER_WRITE: 84 | results[fd] |= POLL_OUT 85 | return results.items() 86 | 87 | def register(self, fd, mode): 88 | self._fds[fd] = mode 89 | self._control(fd, mode, select.KQ_EV_ADD) 90 | 91 | def unregister(self, fd): 92 | self._control(fd, self._fds[fd], select.KQ_EV_DELETE) 93 | del self._fds[fd] 94 | 95 | def modify(self, fd, mode): 96 | self.unregister(fd) 97 | self.register(fd, mode) 98 | 99 | def close(self): 100 | self._kqueue.close() 101 | 102 | 103 | class SelectLoop(object): 104 | 105 | def __init__(self): 106 | self._r_list = set() 107 | self._w_list = set() 108 | self._x_list = set() 109 | 110 | def poll(self, timeout): 111 | r, w, x = select.select(self._r_list, self._w_list, self._x_list, 112 | timeout) 113 | results = defaultdict(lambda: POLL_NULL) 114 | for p in [(r, POLL_IN), (w, POLL_OUT), (x, POLL_ERR)]: 115 | for fd in p[0]: 116 | results[fd] |= p[1] 117 | return results.items() 118 | 119 | def register(self, fd, mode): 120 | if mode & POLL_IN: 121 | self._r_list.add(fd) 122 | if mode & POLL_OUT: 123 | self._w_list.add(fd) 124 | if mode & POLL_ERR: 125 | self._x_list.add(fd) 126 | 127 | def unregister(self, fd): 128 | if fd in self._r_list: 129 | self._r_list.remove(fd) 130 | if fd in self._w_list: 131 | self._w_list.remove(fd) 132 | if fd in self._x_list: 133 | self._x_list.remove(fd) 134 | 135 | def modify(self, fd, mode): 136 | self.unregister(fd) 137 | self.register(fd, mode) 138 | 139 | def close(self): 140 | pass 141 | 142 | 143 | class EventLoop(object): 144 | 145 | def __init__(self): 146 | if hasattr(select, 'epoll'): 147 | self._impl = select.epoll() 148 | model = 'epoll' 149 | elif hasattr(select, 'kqueue'): 150 | self._impl = KqueueLoop() 151 | model = 'kqueue' 152 | elif hasattr(select, 'select'): 153 | self._impl = SelectLoop() 154 | model = 'select' 155 | else: 156 | raise Exception('can not find any available functions in select ' 157 | 'package') 158 | self._fdmap = {} # (f, handler) 159 | self._last_time = time.time() 160 | self._periodic_callbacks = [] 161 | self._stopping = False 162 | logging.debug('using event model: %s', model) 163 | 164 | def poll(self, timeout=None): 165 | events = self._impl.poll(timeout) 166 | return [(self._fdmap[fd][0], fd, event) for fd, event in events] 167 | 168 | def add(self, f, mode, handler): 169 | fd = f.fileno() 170 | self._fdmap[fd] = (f, handler) 171 | self._impl.register(fd, mode) 172 | 173 | def remove(self, f): 174 | fd = f.fileno() 175 | del self._fdmap[fd] 176 | self._impl.unregister(fd) 177 | 178 | def removefd(self, fd): 179 | del self._fdmap[fd] 180 | self._impl.unregister(fd) 181 | 182 | def add_periodic(self, callback): 183 | self._periodic_callbacks.append(callback) 184 | 185 | def remove_periodic(self, callback): 186 | self._periodic_callbacks.remove(callback) 187 | 188 | def modify(self, f, mode): 189 | fd = f.fileno() 190 | self._impl.modify(fd, mode) 191 | 192 | def stop(self): 193 | self._stopping = True 194 | 195 | def run(self): 196 | events = [] 197 | while not self._stopping: 198 | asap = False 199 | try: 200 | events = self.poll(TIMEOUT_PRECISION) 201 | except (OSError, IOError) as e: 202 | if errno_from_exception(e) in (errno.EPIPE, errno.EINTR): 203 | # EPIPE: Happens when the client closes the connection 204 | # EINTR: Happens when received a signal 205 | # handles them as soon as possible 206 | asap = True 207 | logging.debug('poll:%s', e) 208 | else: 209 | logging.error('poll:%s', e) 210 | import traceback 211 | traceback.print_exc() 212 | continue 213 | 214 | handle = False 215 | for sock, fd, event in events: 216 | handler = self._fdmap.get(fd, None) 217 | if handler is not None: 218 | handler = handler[1] 219 | try: 220 | handle = handler.handle_event(sock, fd, event) or handle 221 | except (OSError, IOError) as e: 222 | shell.print_exception(e) 223 | now = time.time() 224 | if asap or now - self._last_time >= TIMEOUT_PRECISION: 225 | for callback in self._periodic_callbacks: 226 | callback() 227 | self._last_time = now 228 | if events and not handle: 229 | time.sleep(0.001) 230 | 231 | def __del__(self): 232 | self._impl.close() 233 | 234 | 235 | # from tornado 236 | def errno_from_exception(e): 237 | """Provides the errno from an Exception object. 238 | 239 | There are cases that the errno attribute was not set so we pull 240 | the errno out of the args but if someone instatiates an Exception 241 | without any args you will get a tuple error. So this function 242 | abstracts all that behavior to give you a safe way to get the 243 | errno. 244 | """ 245 | 246 | if hasattr(e, 'errno'): 247 | return e.errno 248 | elif e.args: 249 | return e.args[0] 250 | else: 251 | return None 252 | 253 | 254 | # from tornado 255 | def get_sock_error(sock): 256 | error_number = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 257 | return socket.error(error_number, os.strerror(error_number)) 258 | -------------------------------------------------------------------------------- /shadowsocks/crypto/table.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/env python 2 | # 3 | # Copyright 2015 clowwindy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, with_statement 18 | 19 | import hashlib 20 | import string 21 | import struct 22 | 23 | __all__ = ['ciphers'] 24 | 25 | cached_tables = {} 26 | 27 | if hasattr(string, 'maketrans'): 28 | maketrans = string.maketrans 29 | translate = string.translate 30 | else: 31 | maketrans = bytes.maketrans 32 | translate = bytes.translate 33 | 34 | 35 | def get_table(key): 36 | m = hashlib.md5() 37 | m.update(key) 38 | s = m.digest() 39 | a, b = struct.unpack(' 0: 64 | data = m[i - 1] + password 65 | md5.update(data) 66 | m.append(md5.digest()) 67 | i += 1 68 | ms = b''.join(m) 69 | key = ms[:key_len] 70 | iv = ms[key_len:key_len + iv_len] 71 | cached_keys[cached_key] = (key, iv) 72 | return key, iv 73 | 74 | 75 | class Encryptor(object): 76 | def __init__(self, password, method, crypto_path=None, iv=None): 77 | """ 78 | Crypto wrapper 79 | :param password: str cipher password 80 | :param method: str cipher 81 | :param crypto_path: dict or none 82 | {'openssl': path, 'sodium': path, 'mbedtls': path} 83 | """ 84 | self.password = password 85 | self.key = None 86 | self.method = method 87 | self.iv_sent = False 88 | self.cipher_iv = b'' 89 | self.decipher = None 90 | self.decipher_iv = None 91 | self.crypto_path = crypto_path 92 | method = method.lower() 93 | self._method_info = Encryptor.get_method_info(method) 94 | if self._method_info: 95 | if iv is None or len(iv) != self._method_info[1]: 96 | self.cipher = self.get_cipher( 97 | password, method, CIPHER_ENC_ENCRYPTION, 98 | random_string(self._method_info[METHOD_INFO_IV_LEN]) 99 | ) 100 | else: 101 | self.cipher = self.get_cipher(key, method, 1, iv) 102 | else: 103 | logging.error('method %s not supported' % method) 104 | # sys.exit(1) 105 | raise Exception('method not supported') 106 | 107 | @staticmethod 108 | def get_method_info(method): 109 | method = method.lower() 110 | m = method_supported.get(method) 111 | return m 112 | 113 | def iv_len(self): 114 | return len(self.cipher_iv) 115 | 116 | def get_cipher(self, password, method, op, iv): 117 | password = common.to_bytes(password) 118 | m = self._method_info 119 | if m[METHOD_INFO_KEY_LEN] > 0: 120 | key, _ = EVP_BytesToKey(password, 121 | m[METHOD_INFO_KEY_LEN], 122 | m[METHOD_INFO_IV_LEN]) 123 | else: 124 | # key_length == 0 indicates we should use the key directly 125 | key, iv = password, b'' 126 | self.key = key 127 | iv = iv[:m[METHOD_INFO_IV_LEN]] 128 | if op == CIPHER_ENC_ENCRYPTION: 129 | # this iv is for cipher not decipher 130 | self.cipher_iv = iv 131 | return m[METHOD_INFO_CRYPTO](method, key, iv, op, self.crypto_path) 132 | 133 | def encrypt(self, buf): 134 | if len(buf) == 0: 135 | return buf 136 | if self.iv_sent: 137 | return self.cipher.encrypt(buf) 138 | else: 139 | self.iv_sent = True 140 | return self.cipher_iv + self.cipher.encrypt(buf) 141 | 142 | def decrypt(self, buf): 143 | if len(buf) == 0: 144 | return buf 145 | if self.decipher is None: 146 | decipher_iv_len = self._method_info[METHOD_INFO_IV_LEN] 147 | decipher_iv = buf[:decipher_iv_len] 148 | self.decipher_iv = decipher_iv 149 | self.decipher = self.get_cipher( 150 | self.password, self.method, 151 | CIPHER_ENC_DECRYPTION, 152 | decipher_iv 153 | ) 154 | buf = buf[decipher_iv_len:] 155 | if len(buf) == 0: 156 | return buf 157 | return self.decipher.decrypt(buf) 158 | 159 | 160 | def gen_key_iv(password, method): 161 | method = method.lower() 162 | if method not in method_supported: 163 | raise Exception('method not supported') 164 | (key_len, iv_len, m) = method_supported[method] 165 | if key_len > 0: 166 | key, _ = EVP_BytesToKey(password, key_len, iv_len) 167 | else: 168 | key = password 169 | iv = random_string(iv_len) 170 | 171 | return key, iv, m 172 | 173 | 174 | def encrypt_all_m(key, iv, m, method, data, crypto_path=None): 175 | result = [iv] 176 | cipher = m(method, key, iv, 1, crypto_path) 177 | result.append(cipher.encrypt_once(data)) 178 | return b''.join(result) 179 | 180 | 181 | def decrypt_all(password, method, data, crypto_path=None): 182 | result = [] 183 | method = method.lower() 184 | (key, iv, m) = gen_key_iv(password, method) 185 | iv = data[:len(iv)] 186 | data = data[len(iv):] 187 | cipher = m(method, key, iv, CIPHER_ENC_DECRYPTION, crypto_path) 188 | result.append(cipher.decrypt_once(data)) 189 | return b''.join(result), key, iv 190 | 191 | 192 | def encrypt_all(password, method, data, crypto_path=None): 193 | result = [] 194 | method = method.lower() 195 | (key, iv, m) = gen_key_iv(password, method) 196 | result.append(iv) 197 | cipher = m(method, key, iv, CIPHER_ENC_ENCRYPTION, crypto_path) 198 | result.append(cipher.encrypt_once(data)) 199 | return b''.join(result) 200 | 201 | def encrypt_key(password, method): 202 | method = method.lower() 203 | if method not in method_supported: 204 | raise Exception('method not supported') 205 | (key_len, iv_len, m) = method_supported[method] 206 | if key_len > 0: 207 | key, _ = EVP_BytesToKey(password, key_len, iv_len) 208 | else: 209 | key = password 210 | return key 211 | 212 | def encrypt_iv_len(method): 213 | method = method.lower() 214 | if method not in method_supported: 215 | raise Exception('method not supported') 216 | (key_len, iv_len, m) = method_supported[method] 217 | return iv_len 218 | 219 | def encrypt_new_iv(method): 220 | method = method.lower() 221 | if method not in method_supported: 222 | raise Exception('method not supported') 223 | (key_len, iv_len, m) = method_supported[method] 224 | return random_string(iv_len) 225 | 226 | CIPHERS_TO_TEST = [ 227 | 'aes-128-cfb', 228 | 'aes-256-cfb', 229 | 'aes-256-gcm', 230 | 'rc4-md5', 231 | 'salsa20', 232 | 'chacha20', 233 | 'table', 234 | ] 235 | 236 | 237 | def test_encryptor(): 238 | from os import urandom 239 | plain = urandom(10240) 240 | for method in CIPHERS_TO_TEST: 241 | logging.warn(method) 242 | encryptor = Encryptor(b'key', method) 243 | decryptor = Encryptor(b'key', method) 244 | cipher = encryptor.encrypt(plain) 245 | plain2 = decryptor.decrypt(cipher) 246 | assert plain == plain2 247 | 248 | 249 | def test_encrypt_all(): 250 | from os import urandom 251 | plain = urandom(10240) 252 | for method in CIPHERS_TO_TEST: 253 | logging.warn(method) 254 | cipher = encrypt_all(b'key', method, plain) 255 | plain2, key, iv = decrypt_all(b'key', method, cipher) 256 | assert plain == plain2 257 | 258 | 259 | def test_encrypt_all_m(): 260 | from os import urandom 261 | plain = urandom(10240) 262 | for method in CIPHERS_TO_TEST: 263 | logging.warn(method) 264 | key, iv, m = gen_key_iv(b'key', method) 265 | cipher = encrypt_all_m(key, iv, m, method, plain) 266 | plain2, key, iv = decrypt_all(b'key', method, cipher) 267 | assert plain == plain2 268 | 269 | 270 | if __name__ == '__main__': 271 | test_encrypt_all() 272 | test_encryptor() 273 | test_encrypt_all_m() 274 | -------------------------------------------------------------------------------- /shadowsocks/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, with_statement 19 | 20 | import logging 21 | import os 22 | import signal 23 | import sys 24 | 25 | if __name__ == '__main__': 26 | import inspect 27 | file_path = os.path.dirname( 28 | os.path.realpath( 29 | inspect.getfile( 30 | inspect.currentframe()))) 31 | sys.path.insert(0, os.path.join(file_path, '../')) 32 | 33 | from shadowsocks import shell, daemon, eventloop, tcprelay, udprelay, \ 34 | asyncdns, manager 35 | 36 | 37 | def main(): 38 | shell.check_python() 39 | 40 | config = shell.get_config(False) 41 | 42 | shell.log_shadowsocks_version() 43 | 44 | daemon.daemon_exec(config) 45 | 46 | if config['port_password']: 47 | pass 48 | else: 49 | config['port_password'] = {} 50 | server_port = config['server_port'] 51 | if isinstance(server_port, list): 52 | for a_server_port in server_port: 53 | config['port_password'][a_server_port] = config['password'] 54 | else: 55 | config['port_password'][str(server_port)] = config['password'] 56 | 57 | if not config.get('dns_ipv6', False): 58 | asyncdns.IPV6_CONNECTION_SUPPORT = False 59 | 60 | if config.get('manager_address', 0): 61 | logging.info('entering manager mode') 62 | manager.run(config) 63 | return 64 | 65 | tcp_servers = [] 66 | udp_servers = [] 67 | dns_resolver = asyncdns.DNSResolver() 68 | if int(config['workers']) > 1: 69 | stat_counter_dict = None 70 | else: 71 | stat_counter_dict = {} 72 | port_password = config['port_password'] 73 | config_password = config.get('password', 'm') 74 | del config['port_password'] 75 | for port, password_obfs in port_password.items(): 76 | method = config["method"] 77 | protocol = config.get("protocol", 'origin') 78 | protocol_param = config.get("protocol_param", '') 79 | obfs = config.get("obfs", 'plain') 80 | obfs_param = config.get("obfs_param", '') 81 | bind = config.get("out_bind", '') 82 | bindv6 = config.get("out_bindv6", '') 83 | if isinstance(password_obfs, list): 84 | password = password_obfs[0] 85 | obfs = password_obfs[1] 86 | if len(password_obfs) > 2: 87 | protocol = password_obfs[2] 88 | elif isinstance(password_obfs, dict): 89 | password = password_obfs.get('password', config_password) 90 | method = password_obfs.get('method', method) 91 | protocol = password_obfs.get('protocol', protocol) 92 | protocol_param = password_obfs.get( 93 | 'protocol_param', protocol_param) 94 | obfs = password_obfs.get('obfs', obfs) 95 | obfs_param = password_obfs.get('obfs_param', obfs_param) 96 | bind = password_obfs.get('out_bind', bind) 97 | bindv6 = password_obfs.get('out_bindv6', bindv6) 98 | else: 99 | password = password_obfs 100 | a_config = config.copy() 101 | ipv6_ok = False 102 | logging.info( 103 | "server start with protocol[%s] password [%s] method [%s] obfs [%s] obfs_param [%s]" % 104 | (protocol, password, method, obfs, obfs_param)) 105 | if 'server_ipv6' in a_config: 106 | try: 107 | if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][ 108 | 0] == "[" and a_config['server_ipv6'][-1] == "]": 109 | a_config['server_ipv6'] = a_config['server_ipv6'][1:-1] 110 | a_config['server_port'] = int(port) 111 | a_config['password'] = password 112 | a_config['method'] = method 113 | a_config['protocol'] = protocol 114 | a_config['protocol_param'] = protocol_param 115 | a_config['obfs'] = obfs 116 | a_config['obfs_param'] = obfs_param 117 | a_config['out_bind'] = bind 118 | a_config['out_bindv6'] = bindv6 119 | a_config['server'] = a_config['server_ipv6'] 120 | logging.info("starting server at [%s]:%d" % 121 | (a_config['server'], int(port))) 122 | tcp_servers.append( 123 | tcprelay.TCPRelay( 124 | a_config, 125 | dns_resolver, 126 | False, 127 | stat_counter=stat_counter_dict)) 128 | udp_servers.append( 129 | udprelay.UDPRelay( 130 | a_config, 131 | dns_resolver, 132 | False, 133 | stat_counter=stat_counter_dict)) 134 | if a_config['server_ipv6'] == b"::": 135 | ipv6_ok = True 136 | except Exception as e: 137 | shell.print_exception(e) 138 | 139 | try: 140 | a_config = config.copy() 141 | a_config['server_port'] = int(port) 142 | a_config['password'] = password 143 | a_config['method'] = method 144 | a_config['protocol'] = protocol 145 | a_config['protocol_param'] = protocol_param 146 | a_config['obfs'] = obfs 147 | a_config['obfs_param'] = obfs_param 148 | a_config['out_bind'] = bind 149 | a_config['out_bindv6'] = bindv6 150 | logging.info("starting server at %s:%d" % 151 | (a_config['server'], int(port))) 152 | tcp_servers.append( 153 | tcprelay.TCPRelay( 154 | a_config, 155 | dns_resolver, 156 | False, 157 | stat_counter=stat_counter_dict)) 158 | udp_servers.append( 159 | udprelay.UDPRelay( 160 | a_config, 161 | dns_resolver, 162 | False, 163 | stat_counter=stat_counter_dict)) 164 | except Exception as e: 165 | if not ipv6_ok: 166 | shell.print_exception(e) 167 | 168 | def run_server(): 169 | def child_handler(signum, _): 170 | logging.warn('received SIGQUIT, doing graceful shutting down..') 171 | list(map(lambda s: s.close(next_tick=True), 172 | tcp_servers + udp_servers)) 173 | signal.signal(getattr(signal, 'SIGQUIT', signal.SIGTERM), 174 | child_handler) 175 | 176 | def int_handler(signum, _): 177 | sys.exit(1) 178 | signal.signal(signal.SIGINT, int_handler) 179 | 180 | try: 181 | loop = eventloop.EventLoop() 182 | dns_resolver.add_to_loop(loop) 183 | list(map(lambda s: s.add_to_loop(loop), tcp_servers + udp_servers)) 184 | 185 | daemon.set_user(config.get('user', None)) 186 | loop.run() 187 | except Exception as e: 188 | shell.print_exception(e) 189 | sys.exit(1) 190 | 191 | if int(config['workers']) > 1: 192 | if os.name == 'posix': 193 | children = [] 194 | is_child = False 195 | for i in range(0, int(config['workers'])): 196 | r = os.fork() 197 | if r == 0: 198 | logging.info('worker started') 199 | is_child = True 200 | run_server() 201 | break 202 | else: 203 | children.append(r) 204 | if not is_child: 205 | def handler(signum, _): 206 | for pid in children: 207 | try: 208 | os.kill(pid, signum) 209 | os.waitpid(pid, 0) 210 | except OSError: # child may already exited 211 | pass 212 | sys.exit() 213 | signal.signal(signal.SIGTERM, handler) 214 | signal.signal(signal.SIGQUIT, handler) 215 | signal.signal(signal.SIGINT, handler) 216 | 217 | # master 218 | for a_tcp_server in tcp_servers: 219 | a_tcp_server.close() 220 | for a_udp_server in udp_servers: 221 | a_udp_server.close() 222 | dns_resolver.close() 223 | 224 | for child in children: 225 | os.waitpid(child, 0) 226 | else: 227 | logging.warn('worker is only available on Unix/Linux') 228 | run_server() 229 | else: 230 | run_server() 231 | 232 | 233 | if __name__ == '__main__': 234 | main() 235 | -------------------------------------------------------------------------------- /shadowsocks/manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright 2015 clowwindy 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | from __future__ import absolute_import, division, print_function, with_statement 19 | 20 | import collections 21 | import errno 22 | import json 23 | import logging 24 | import socket 25 | import traceback 26 | 27 | from shadowsocks import asyncdns, common, eventloop, shell, tcprelay, udprelay 28 | 29 | BUF_SIZE = 1506 30 | STAT_SEND_LIMIT = 50 31 | 32 | class Manager(object): 33 | 34 | def __init__(self, config): 35 | self._config = config 36 | self._relays = {} # (tcprelay, udprelay) 37 | self._loop = eventloop.EventLoop() 38 | self._dns_resolver = asyncdns.DNSResolver() 39 | self._dns_resolver.add_to_loop(self._loop) 40 | 41 | self._statistics = collections.defaultdict(int) 42 | self._control_client_addr = None 43 | try: 44 | manager_address = common.to_str(config['manager_address']) 45 | if ':' in manager_address: 46 | addr = manager_address.rsplit(':', 1) 47 | addr = addr[0], int(addr[1]) 48 | addrs = socket.getaddrinfo(addr[0], addr[1]) 49 | if addrs: 50 | family = addrs[0][0] 51 | else: 52 | logging.error('invalid address: %s', manager_address) 53 | exit(1) 54 | else: 55 | addr = manager_address 56 | family = socket.AF_UNIX 57 | self._control_socket = socket.socket(family, 58 | socket.SOCK_DGRAM) 59 | self._control_socket.bind(addr) 60 | self._control_socket.setblocking(False) 61 | except (OSError, IOError) as e: 62 | logging.error(e) 63 | logging.error('can not bind to manager address') 64 | exit(1) 65 | self._loop.add(self._control_socket, 66 | eventloop.POLL_IN, self) 67 | self._loop.add_periodic(self.handle_periodic) 68 | 69 | port_password = config['port_password'] 70 | del config['port_password'] 71 | for port, password in port_password.items(): 72 | a_config = config.copy() 73 | a_config['server_port'] = int(port) 74 | a_config['password'] = password 75 | self.add_port(a_config) 76 | 77 | def add_port(self, config): 78 | port = int(config['server_port']) 79 | servers = self._relays.get(port, None) 80 | if servers: 81 | logging.error("server already exists at %s:%d" % (config['server'], 82 | port)) 83 | return 84 | logging.info("adding server at %s:%d" % (config['server'], port)) 85 | t = tcprelay.TCPRelay(config, self._dns_resolver, False, 86 | stat_callback=self.stat_callback) 87 | u = udprelay.UDPRelay(config, self._dns_resolver, False, 88 | stat_callback=self.stat_callback) 89 | t.add_to_loop(self._loop) 90 | u.add_to_loop(self._loop) 91 | self._relays[port] = (t, u) 92 | 93 | def remove_port(self, config): 94 | port = int(config['server_port']) 95 | servers = self._relays.get(port, None) 96 | if servers: 97 | logging.info("removing server at %s:%d" % (config['server'], port)) 98 | t, u = servers 99 | t.close(next_tick=False) 100 | u.close(next_tick=False) 101 | del self._relays[port] 102 | else: 103 | logging.error("server not exist at %s:%d" % (config['server'], 104 | port)) 105 | 106 | def handle_event(self, sock, fd, event): 107 | if sock == self._control_socket and event == eventloop.POLL_IN: 108 | data, self._control_client_addr = sock.recvfrom(BUF_SIZE) 109 | parsed = self._parse_command(data) 110 | if parsed: 111 | command, config = parsed 112 | a_config = self._config.copy() 113 | if config: 114 | # let the command override the configuration file 115 | a_config.update(config) 116 | if 'server_port' not in a_config: 117 | logging.error('can not find server_port in config') 118 | else: 119 | if command == 'add': 120 | self.add_port(a_config) 121 | self._send_control_data(b'ok') 122 | elif command == 'remove': 123 | self.remove_port(a_config) 124 | self._send_control_data(b'ok') 125 | elif command == 'ping': 126 | self._send_control_data(b'pong') 127 | else: 128 | logging.error('unknown command %s', command) 129 | 130 | def _parse_command(self, data): 131 | # commands: 132 | # add: {"server_port": 8000, "password": "foobar"} 133 | # remove: {"server_port": 8000"} 134 | data = common.to_str(data) 135 | parts = data.split(':', 1) 136 | if len(parts) < 2: 137 | return data, None 138 | command, config_json = parts 139 | try: 140 | config = shell.parse_json_in_str(config_json) 141 | return command, config 142 | except Exception as e: 143 | logging.error(e) 144 | return None 145 | 146 | def stat_callback(self, port, data_len): 147 | self._statistics[port] += data_len 148 | 149 | def handle_periodic(self): 150 | r = {} 151 | i = 0 152 | 153 | def send_data(data_dict): 154 | if data_dict: 155 | # use compact JSON format (without space) 156 | data = common.to_bytes(json.dumps(data_dict, 157 | separators=(',', ':'))) 158 | self._send_control_data(b'stat: ' + data) 159 | 160 | for k, v in self._statistics.items(): 161 | r[k] = v 162 | i += 1 163 | # split the data into segments that fit in UDP packets 164 | if i >= STAT_SEND_LIMIT: 165 | send_data(r) 166 | r.clear() 167 | i = 0 168 | if len(r) > 0: 169 | send_data(r) 170 | self._statistics.clear() 171 | 172 | def _send_control_data(self, data): 173 | if self._control_client_addr: 174 | try: 175 | self._control_socket.sendto(data, self._control_client_addr) 176 | except (socket.error, OSError, IOError) as e: 177 | error_no = eventloop.errno_from_exception(e) 178 | if error_no in (errno.EAGAIN, errno.EINPROGRESS, 179 | errno.EWOULDBLOCK): 180 | return 181 | else: 182 | shell.print_exception(e) 183 | if self._config['verbose']: 184 | traceback.print_exc() 185 | 186 | def run(self): 187 | self._loop.run() 188 | 189 | 190 | def run(config): 191 | Manager(config).run() 192 | 193 | 194 | def test(): 195 | import time 196 | import threading 197 | import struct 198 | from shadowsocks import encrypt 199 | 200 | logging.basicConfig(level=5, 201 | format='%(asctime)s %(levelname)-8s %(message)s', 202 | datefmt='%Y-%m-%d %H:%M:%S') 203 | enc = [] 204 | eventloop.TIMEOUT_PRECISION = 1 205 | 206 | def run_server(): 207 | config = shell.get_config(True) 208 | config = config.copy() 209 | a_config = { 210 | 'server': '127.0.0.1', 211 | 'local_port': 1081, 212 | 'port_password': { 213 | '8381': 'foobar1', 214 | '8382': 'foobar2' 215 | }, 216 | 'method': 'aes-256-cfb', 217 | 'manager_address': '127.0.0.1:6001', 218 | 'timeout': 60, 219 | 'fast_open': False, 220 | 'verbose': 2 221 | } 222 | config.update(a_config) 223 | manager = Manager(config) 224 | enc.append(manager) 225 | manager.run() 226 | 227 | t = threading.Thread(target=run_server) 228 | t.start() 229 | time.sleep(1) 230 | manager = enc[0] 231 | cli = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 232 | cli.connect(('127.0.0.1', 6001)) 233 | 234 | # test add and remove 235 | time.sleep(1) 236 | cli.send(b'add: {"server_port":7001, "password":"asdfadsfasdf"}') 237 | time.sleep(1) 238 | assert 7001 in manager._relays 239 | data, addr = cli.recvfrom(1506) 240 | assert b'ok' in data 241 | 242 | cli.send(b'remove: {"server_port":8381}') 243 | time.sleep(1) 244 | assert 8381 not in manager._relays 245 | data, addr = cli.recvfrom(1506) 246 | assert b'ok' in data 247 | logging.info('add and remove test passed') 248 | 249 | # test statistics for TCP 250 | header = common.pack_addr(b'google.com') + struct.pack('>H', 80) 251 | data = encrypt.encrypt_all(b'asdfadsfasdf', 'aes-256-cfb', 1, 252 | header + b'GET /\r\n\r\n') 253 | tcp_cli = socket.socket() 254 | tcp_cli.connect(('127.0.0.1', 7001)) 255 | tcp_cli.send(data) 256 | tcp_cli.recv(4096) 257 | tcp_cli.close() 258 | 259 | data, addr = cli.recvfrom(1506) 260 | data = common.to_str(data) 261 | assert data.startswith('stat: ') 262 | data = data.split('stat:')[1] 263 | stats = shell.parse_json_in_str(data) 264 | assert '7001' in stats 265 | logging.info('TCP statistics test passed') 266 | 267 | # test statistics for UDP 268 | header = common.pack_addr(b'127.0.0.1') + struct.pack('>H', 80) 269 | data = encrypt.encrypt_all(b'foobar2', 'aes-256-cfb', 1, 270 | header + b'test') 271 | udp_cli = socket.socket(type=socket.SOCK_DGRAM) 272 | udp_cli.sendto(data, ('127.0.0.1', 8382)) 273 | tcp_cli.close() 274 | 275 | data, addr = cli.recvfrom(1506) 276 | data = common.to_str(data) 277 | assert data.startswith('stat: ') 278 | data = data.split('stat:')[1] 279 | stats = json.loads(data) 280 | assert '8382' in stats 281 | logging.info('UDP statistics test passed') 282 | 283 | manager._loop.stop() 284 | t.join() 285 | 286 | 287 | if __name__ == '__main__': 288 | test() 289 | -------------------------------------------------------------------------------- /shadowsocks/crypto/aead.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Void Copyright NO ONE 5 | # 6 | # Void License 7 | # 8 | # The code belongs to no one. Do whatever you want. 9 | # Forget about boring open source license. 10 | # 11 | # AEAD cipher for shadowsocks 12 | # 13 | 14 | from __future__ import absolute_import, division, print_function, with_statement 15 | 16 | import hashlib 17 | from ctypes import byref, c_int, c_void_p, create_string_buffer 18 | from struct import pack, unpack 19 | 20 | from shadowsocks.common import chr, ord 21 | from shadowsocks.crypto import hkdf, util 22 | 23 | EVP_CTRL_GCM_SET_IVLEN = 0x9 24 | EVP_CTRL_GCM_GET_TAG = 0x10 25 | EVP_CTRL_GCM_SET_TAG = 0x11 26 | EVP_CTRL_CCM_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN 27 | EVP_CTRL_CCM_GET_TAG = EVP_CTRL_GCM_GET_TAG 28 | EVP_CTRL_CCM_SET_TAG = EVP_CTRL_GCM_SET_TAG 29 | 30 | EVP_CTRL_AEAD_SET_IVLEN = EVP_CTRL_GCM_SET_IVLEN 31 | EVP_CTRL_AEAD_SET_TAG = EVP_CTRL_GCM_SET_TAG 32 | EVP_CTRL_AEAD_GET_TAG = EVP_CTRL_GCM_GET_TAG 33 | 34 | AEAD_MSG_LEN_UNKNOWN = 0 35 | AEAD_CHUNK_SIZE_LEN = 2 36 | AEAD_CHUNK_SIZE_MASK = 0x3FFF 37 | 38 | CIPHER_NONCE_LEN = { 39 | 'aes-128-gcm': 12, 40 | 'aes-192-gcm': 12, 41 | 'aes-256-gcm': 12, 42 | 'aes-128-ocb': 12, # requires openssl 1.1 43 | 'aes-192-ocb': 12, 44 | 'aes-256-ocb': 12, 45 | 'chacha20-poly1305': 12, 46 | 'chacha20-ietf-poly1305': 12, 47 | 'xchacha20-ietf-poly1305': 24, 48 | 'sodium:aes-256-gcm': 12, 49 | } 50 | 51 | CIPHER_TAG_LEN = { 52 | 'aes-128-gcm': 16, 53 | 'aes-192-gcm': 16, 54 | 'aes-256-gcm': 16, 55 | 'aes-128-ocb': 16, # requires openssl 1.1 56 | 'aes-192-ocb': 16, 57 | 'aes-256-ocb': 16, 58 | 'chacha20-poly1305': 16, 59 | 'chacha20-ietf-poly1305': 16, 60 | 'xchacha20-ietf-poly1305': 16, 61 | 'sodium:aes-256-gcm': 16, 62 | } 63 | 64 | SUBKEY_INFO = b"ss-subkey" 65 | 66 | libsodium = None 67 | sodium_loaded = False 68 | 69 | 70 | def load_sodium(path=None): 71 | """ 72 | Load libsodium helpers for nonce increment 73 | :return: None 74 | """ 75 | global libsodium, sodium_loaded 76 | 77 | libsodium = util.find_library('sodium', 'sodium_increment', 78 | 'libsodium', path) 79 | if libsodium is None: 80 | print('load libsodium failed with path %s' % path) 81 | return 82 | 83 | if libsodium.sodium_init() < 0: 84 | libsodium = None 85 | print('sodium init failed') 86 | return 87 | 88 | libsodium.sodium_increment.restype = c_void_p 89 | libsodium.sodium_increment.argtypes = ( 90 | c_void_p, c_int 91 | ) 92 | 93 | sodium_loaded = True 94 | return 95 | 96 | 97 | def nonce_increment(nonce, nlen): 98 | """ 99 | Increase nonce by 1 in little endian 100 | From libsodium sodium_increment(): 101 | for (; i < nlen; i++) { 102 | c += (uint_fast16_t) n[i]; 103 | n[i] = (unsigned char) c; 104 | c >>= 8; 105 | } 106 | :param nonce: string_buffer nonce 107 | :param nlen: nonce length 108 | :return: nonce plus by 1 109 | """ 110 | c = 1 111 | i = 0 112 | # n = create_string_buffer(nlen) 113 | while i < nlen: 114 | c += ord(nonce[i]) 115 | nonce[i] = chr(c & 0xFF) 116 | c >>= 8 117 | i += 1 118 | return # n.raw 119 | 120 | 121 | class AeadCryptoBase(object): 122 | """ 123 | Handles basic aead process of shadowsocks protocol 124 | 125 | TCP Chunk (after encryption, *ciphertext*) 126 | +--------------+---------------+--------------+------------+ 127 | | *DataLen* | DataLen_TAG | *Data* | Data_TAG | 128 | +--------------+---------------+--------------+------------+ 129 | | 2 | Fixed | Variable | Fixed | 130 | +--------------+---------------+--------------+------------+ 131 | 132 | UDP (after encryption, *ciphertext*) 133 | +--------+-----------+-----------+ 134 | | NONCE | *Data* | Data_TAG | 135 | +-------+-----------+-----------+ 136 | | Fixed | Variable | Fixed | 137 | +--------+-----------+-----------+ 138 | """ 139 | 140 | def __init__(self, cipher_name, key, iv, op, crypto_path=None): 141 | self._op = int(op) 142 | self._salt = iv 143 | self._nlen = CIPHER_NONCE_LEN[cipher_name] 144 | self._nonce = create_string_buffer(self._nlen) 145 | self._tlen = CIPHER_TAG_LEN[cipher_name] 146 | 147 | crypto_hkdf = hkdf.Hkdf(iv, key, algorithm=hashlib.sha1) 148 | self._skey = crypto_hkdf.expand(info=SUBKEY_INFO, length=len(key)) 149 | # _chunk['mlen']: 150 | # -1, waiting data len header 151 | # n, n > 0, waiting data 152 | self._chunk = {'mlen': AEAD_MSG_LEN_UNKNOWN, 'data': b''} 153 | 154 | # load libsodium for nonce increment 155 | if not sodium_loaded: 156 | crypto_path = dict(crypto_path) if crypto_path else dict() 157 | path = crypto_path.get('sodium', None) 158 | load_sodium(path) 159 | 160 | def nonce_increment(self): 161 | """ 162 | AEAD ciphers need nonce to be unique per key 163 | TODO: cache and check unique 164 | :return: None 165 | """ 166 | global libsodium, sodium_loaded 167 | if sodium_loaded: 168 | libsodium.sodium_increment(byref(self._nonce), c_int(self._nlen)) 169 | else: 170 | nonce_increment(self._nonce, self._nlen) 171 | # print("".join("%02x" % ord(b) for b in self._nonce)) 172 | 173 | def cipher_ctx_init(self): 174 | """ 175 | Increase nonce to make it unique for the same key 176 | :return: None 177 | """ 178 | self.nonce_increment() 179 | 180 | def aead_encrypt(self, data): 181 | """ 182 | Encrypt data with authenticate tag 183 | 184 | :param data: plain text 185 | :return: str [payload][tag] cipher text with tag 186 | """ 187 | raise Exception("Must implement aead_encrypt method") 188 | 189 | def encrypt_chunk(self, data): 190 | """ 191 | Encrypt a chunk for TCP chunks 192 | 193 | :param data: str 194 | :return: str [len][tag][payload][tag] 195 | """ 196 | plen = len(data) 197 | # l = AEAD_CHUNK_SIZE_LEN + plen + self._tlen * 2 198 | 199 | # network byte order 200 | ctext = [self.aead_encrypt(pack("!H", plen & AEAD_CHUNK_SIZE_MASK))] 201 | if len(ctext[0]) != AEAD_CHUNK_SIZE_LEN + self._tlen: 202 | self.clean() 203 | raise Exception("size length invalid") 204 | 205 | ctext.append(self.aead_encrypt(data)) 206 | if len(ctext[1]) != plen + self._tlen: 207 | self.clean() 208 | raise Exception("data length invalid") 209 | 210 | return b''.join(ctext) 211 | 212 | def encrypt(self, data): 213 | """ 214 | Encrypt data, for TCP divided into chunks 215 | For UDP data, call aead_encrypt instead 216 | 217 | :param data: str data bytes 218 | :return: str encrypted data 219 | """ 220 | plen = len(data) 221 | if plen <= AEAD_CHUNK_SIZE_MASK: 222 | ctext = self.encrypt_chunk(data) 223 | return ctext 224 | ctext = [] 225 | while plen > 0: 226 | mlen = plen if plen < AEAD_CHUNK_SIZE_MASK \ 227 | else AEAD_CHUNK_SIZE_MASK 228 | c = self.encrypt_chunk(data[:mlen]) 229 | ctext.append(c) 230 | data = data[mlen:] 231 | plen -= mlen 232 | 233 | return b''.join(ctext) 234 | 235 | def aead_decrypt(self, data): 236 | """ 237 | Decrypt data and authenticate tag 238 | 239 | :param data: str [len][tag][payload][tag] cipher text with tag 240 | :return: str plain text 241 | """ 242 | raise Exception("Must implement aead_decrypt method") 243 | 244 | def decrypt_chunk_size(self, data): 245 | """ 246 | Decrypt chunk size 247 | 248 | :param data: str [size][tag] encrypted chunk payload len 249 | :return: (int, str) msg length and remaining encrypted data 250 | """ 251 | if self._chunk['mlen'] > 0: 252 | return self._chunk['mlen'], data 253 | data = self._chunk['data'] + data 254 | self._chunk['data'] = b"" 255 | 256 | hlen = AEAD_CHUNK_SIZE_LEN + self._tlen 257 | if hlen > len(data): 258 | self._chunk['data'] = data 259 | return 0, b"" 260 | plen = self.aead_decrypt(data[:hlen]) 261 | plen, = unpack("!H", plen) 262 | if plen & AEAD_CHUNK_SIZE_MASK != plen or plen <= 0: 263 | self.clean() 264 | raise Exception('Invalid message length') 265 | 266 | return plen, data[hlen:] 267 | 268 | def decrypt_chunk_payload(self, plen, data): 269 | """ 270 | Decrypted encrypted msg payload 271 | 272 | :param plen: int payload length 273 | :param data: str [payload][tag][[len][tag]....] encrypted data 274 | :return: (str, str) plain text and remaining encrypted data 275 | """ 276 | data = self._chunk['data'] + data 277 | if len(data) < plen + self._tlen: 278 | self._chunk['mlen'] = plen 279 | self._chunk['data'] = data 280 | return b"", b"" 281 | self._chunk['mlen'] = AEAD_MSG_LEN_UNKNOWN 282 | self._chunk['data'] = b"" 283 | 284 | plaintext = self.aead_decrypt(data[:plen + self._tlen]) 285 | 286 | if len(plaintext) != plen: 287 | self.clean() 288 | raise Exception("plaintext length invalid") 289 | 290 | return plaintext, data[plen + self._tlen:] 291 | 292 | def decrypt_chunk(self, data): 293 | """ 294 | Decrypt a TCP chunk 295 | 296 | :param data: str [len][tag][payload][tag][[len][tag]...] encrypted msg 297 | :return: (str, str) decrypted msg and remaining encrypted data 298 | """ 299 | plen, data = self.decrypt_chunk_size(data) 300 | if plen <= 0: 301 | return b"", b"" 302 | return self.decrypt_chunk_payload(plen, data) 303 | 304 | def decrypt(self, data): 305 | """ 306 | Decrypt data for TCP data divided into chunks 307 | For UDP data, call aead_decrypt instead 308 | 309 | :param data: str 310 | :return: str 311 | """ 312 | ptext = [] 313 | pnext, left = self.decrypt_chunk(data) 314 | ptext.append(pnext) 315 | while len(left) > 0: 316 | pnext, left = self.decrypt_chunk(left) 317 | ptext.append(pnext) 318 | return b''.join(ptext) 319 | 320 | 321 | def test_nonce_increment(): 322 | buf = create_string_buffer(12) 323 | print("".join("%02x" % ord(b) for b in buf)) 324 | nonce_increment(buf, 12) 325 | nonce_increment(buf, 12) 326 | nonce_increment(buf, 12) 327 | nonce_increment(buf, 12) 328 | print("".join("%02x" % ord(b) for b in buf)) 329 | for i in range(256): 330 | nonce_increment(buf, 12) 331 | print("".join("%02x" % ord(b) for b in buf)) 332 | 333 | 334 | if __name__ == '__main__': 335 | load_sodium() 336 | test_nonce_increment() 337 | -------------------------------------------------------------------------------- /auto_block.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # -*- coding: UTF-8 -*- 3 | import fcntl 4 | import logging 5 | import os 6 | import platform 7 | import socket 8 | 9 | import configloader 10 | from shadowsocks import common 11 | 12 | 13 | class AutoBlock(object): 14 | def __init__(self): 15 | from multiprocessing import Event 16 | 17 | self.event = Event() 18 | self.start_line = self.file_len("/etc/hosts.deny") 19 | self.has_stopped = False 20 | 21 | def get_ip(self, text): 22 | if common.match_ipv4_address(text) is not None: 23 | return common.match_ipv4_address(text) 24 | else: 25 | if common.match_ipv6_address(text) is not None: 26 | return common.match_ipv6_address(text) 27 | return None 28 | 29 | def file_len(self, fname): 30 | return sum(1 for line in open(fname)) 31 | 32 | def auto_block_thread(self): 33 | global webapi 34 | server_ip = socket.gethostbyname(configloader.get_config().MYSQL_HOST) 35 | 36 | if configloader.get_config().API_INTERFACE == "modwebapi": 37 | # 读取节点IP 38 | node_ip_list = [] 39 | data = webapi.getApi("nodes") 40 | for node in data: 41 | temp_list = node["node_ip"].split(",") 42 | node_ip_list.append(temp_list[0]) 43 | 44 | deny_file = open("/etc/hosts.deny") 45 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 46 | deny_lines = deny_file.readlines() 47 | deny_file.close() 48 | 49 | logging.info("Read hosts.deny from line " + str(self.start_line)) 50 | real_deny_list = deny_lines[self.start_line:] 51 | 52 | denyed_ip_list = [] 53 | data = [] 54 | for line in real_deny_list: 55 | if self.get_ip(line) and line.find("#") != 0: 56 | ip = self.get_ip(line) 57 | 58 | if str(ip).find(str(server_ip)) != -1: 59 | i = 0 60 | 61 | for line in deny_lines: 62 | if line.find(ip) != -1: 63 | del deny_lines[i] 64 | i = i + 1 65 | 66 | deny_file = open("/etc/hosts.deny", "w+") 67 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 68 | for line in deny_lines: 69 | deny_file.write(line) 70 | deny_file.close() 71 | 72 | continue 73 | 74 | has_match_node = False 75 | for node_ip in node_ip_list: 76 | if str(ip).find(node_ip) != -1: 77 | i = 0 78 | 79 | for line in deny_lines: 80 | if line.find(ip) != -1: 81 | del deny_lines[i] 82 | i = i + 1 83 | 84 | deny_file = open("/etc/hosts.deny", "w+") 85 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 86 | for line in deny_lines: 87 | deny_file.write(line) 88 | deny_file.close() 89 | 90 | has_match_node = True 91 | continue 92 | 93 | if has_match_node: 94 | continue 95 | 96 | if configloader.get_config().API_INTERFACE == "modwebapi": 97 | data.append({"ip": ip}) 98 | logging.info("Block ip:" + str(ip)) 99 | else: 100 | cur = conn.cursor() 101 | cur.execute( 102 | "SELECT * FROM `blockip` where `ip` = '" 103 | + str(ip) 104 | + "'" 105 | ) 106 | rows = cur.fetchone() 107 | cur.close() 108 | 109 | if rows is not None: 110 | continue 111 | 112 | cur = conn.cursor() 113 | cur.execute( 114 | "INSERT INTO `blockip` (`id`, `nodeid`, `ip`, `datetime`) VALUES (NULL, '" 115 | + str(configloader.get_config().NODE_ID) 116 | + "', '" 117 | + str(ip) 118 | + "', unix_timestamp())" 119 | ) 120 | cur.close() 121 | 122 | logging.info("Block ip:" + str(ip)) 123 | 124 | denyed_ip_list.append(ip) 125 | 126 | if configloader.get_config().API_INTERFACE == "modwebapi": 127 | webapi.postApi( 128 | "func/block_ip", 129 | {"node_id": configloader.get_config().NODE_ID}, 130 | {"data": data}, 131 | ) 132 | 133 | if configloader.get_config().API_INTERFACE == "modwebapi": 134 | rows = webapi.getApi("func/block_ip") 135 | else: 136 | cur = conn.cursor() 137 | cur.execute( 138 | "SELECT * FROM `blockip` where `datetime`>unix_timestamp()-60" 139 | ) 140 | rows = cur.fetchall() 141 | cur.close() 142 | 143 | deny_str = "" 144 | deny_str_at = "" 145 | 146 | for row in rows: 147 | if configloader.get_config().API_INTERFACE == "modwebapi": 148 | node = row["nodeid"] 149 | ip = self.get_ip(row["ip"]) 150 | else: 151 | node = row[1] 152 | ip = self.get_ip(row[2]) 153 | 154 | if ip is not None: 155 | 156 | if str(node) == str(configloader.get_config().NODE_ID): 157 | if ( 158 | configloader.get_config().ANTISSATTACK == 1 159 | and configloader.get_config().CLOUDSAFE == 1 160 | and ip not in denyed_ip_list 161 | ): 162 | if common.is_ip(ip): 163 | if common.is_ip(ip) == socket.AF_INET: 164 | os.system( 165 | "route add -host %s gw 127.0.0.1" % str(ip) 166 | ) 167 | deny_str = deny_str + "\nALL: " + str(ip) 168 | else: 169 | os.system( 170 | "ip -6 route add ::1/128 via %s/128" 171 | % str(ip) 172 | ) 173 | deny_str = ( 174 | deny_str + "\nALL: [" + str(ip) + "]/128" 175 | ) 176 | 177 | logging.info("Remote Block ip:" + str(ip)) 178 | else: 179 | if common.is_ip(ip): 180 | if common.is_ip(ip) == socket.AF_INET: 181 | os.system( 182 | "route add -host %s gw 127.0.0.1" % str(ip) 183 | ) 184 | deny_str = deny_str + "\nALL: " + str(ip) 185 | else: 186 | os.system( 187 | "ip -6 route add ::1/128 via %s/128" % str(ip) 188 | ) 189 | deny_str = ( 190 | deny_str + "\nALL: [" + str(ip) + "]/128" 191 | ) 192 | logging.info("Remote Block ip:" + str(ip)) 193 | 194 | deny_file = open("/etc/hosts.deny", "a") 195 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 196 | deny_file.write(deny_str) 197 | deny_file.close() 198 | 199 | if ( 200 | configloader.get_config().ANTISSATTACK == 1 201 | and configloader.get_config().CLOUDSAFE == 1 202 | ): 203 | deny_file = open("/etc/hosts.deny", "a") 204 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 205 | deny_file.write(deny_str_at) 206 | deny_file.close() 207 | 208 | if configloader.get_config().API_INTERFACE == "modwebapi": 209 | rows = webapi.getApi("func/unblock_ip") 210 | else: 211 | cur = conn.cursor() 212 | cur.execute( 213 | "SELECT * FROM `unblockip` where `datetime`>unix_timestamp()-60" 214 | ) 215 | rows = cur.fetchall() 216 | cur.close() 217 | conn.close() 218 | 219 | deny_file = open("/etc/hosts.deny") 220 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 221 | deny_lines = deny_file.readlines() 222 | deny_file.close() 223 | 224 | i = 0 225 | 226 | for line in deny_lines: 227 | for row in rows: 228 | if configloader.get_config().API_INTERFACE == "modwebapi": 229 | ip = str(row["ip"]) 230 | else: 231 | ip = str(row[1]) 232 | if line.find(ip) != -1: 233 | del deny_lines[i] 234 | if common.is_ip(ip): 235 | if common.is_ip(ip) == socket.AF_INET: 236 | os.system( 237 | "route del -host %s gw 127.0.0.1" % str(ip) 238 | ) 239 | else: 240 | os.system( 241 | "ip -6 route del ::1/128 via %s/128" % str(ip) 242 | ) 243 | logging.info("Unblock ip:" + str(ip)) 244 | i = i + 1 245 | 246 | deny_file = open("/etc/hosts.deny", "w+") 247 | fcntl.flock(deny_file.fileno(), fcntl.LOCK_EX) 248 | for line in deny_lines: 249 | deny_file.write(line) 250 | deny_file.close() 251 | 252 | self.start_line = self.file_len("/etc/hosts.deny") 253 | 254 | @staticmethod 255 | def thread_db(obj): 256 | if ( 257 | configloader.get_config().CLOUDSAFE == 0 258 | or platform.system() != "Linux" 259 | ): 260 | return 261 | 262 | if configloader.get_config().API_INTERFACE == "modwebapi": 263 | import webapi_utils 264 | 265 | global webapi 266 | webapi = webapi_utils.WebApi() 267 | 268 | global db_instance 269 | db_instance = obj() 270 | 271 | try: 272 | while True: 273 | try: 274 | db_instance.auto_block_thread() 275 | except Exception as e: 276 | import traceback 277 | 278 | trace = traceback.format_exc() 279 | logging.error(trace) 280 | # logging.warn('db thread except:%s' % e) 281 | if db_instance.event.wait(60): 282 | break 283 | if db_instance.has_stopped: 284 | break 285 | except KeyboardInterrupt as e: 286 | pass 287 | db_instance = None 288 | 289 | @staticmethod 290 | def thread_db_stop(): 291 | global db_instance 292 | db_instance.has_stopped = True 293 | db_instance.event.set() 294 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/simple_obfs_tls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | import binascii 25 | import struct 26 | import base64 27 | import time 28 | import random 29 | import hmac 30 | import hashlib 31 | import string 32 | 33 | from shadowsocks import common 34 | from shadowsocks.obfsplugin import plain 35 | from shadowsocks.common import to_bytes, to_str, ord 36 | from shadowsocks import lru_cache 37 | 38 | def create_simple_obfs_tls(method): 39 | return simple_obfs_tls(method) 40 | 41 | obfs_map = { 42 | 'simple_obfs_tls': (create_simple_obfs_tls,), 43 | 'simple_obfs_tls_compatible': (create_simple_obfs_tls,), 44 | } 45 | 46 | def match_begin(str1, str2): 47 | if len(str1) >= len(str2): 48 | if str1[:len(str2)] == str2: 49 | return True 50 | return False 51 | 52 | class obfs_auth_data(object): 53 | def __init__(self): 54 | self.client_data = lru_cache.LRUCache(60 * 5) 55 | self.client_id = os.urandom(32) 56 | self.startup_time = int(time.time() - 60 * 30) & 0xFFFFFFFF 57 | 58 | class simple_obfs_tls(plain.plain): 59 | def __init__(self, method): 60 | self.method = method 61 | self.obfs_stage = 0 62 | self.deobfs_stage = 0 63 | self.send_buffer = b'' 64 | self.recv_buffer = b'' 65 | self.client_id = b'' 66 | self.max_time_dif = 60 * 60 * 24 # time dif (second) setting 67 | self.tls_version = b'\x03\x03' 68 | self.overhead = 5 69 | 70 | def init_data(self): 71 | return obfs_auth_data() 72 | 73 | def get_overhead(self, direction): # direction: true for c->s false for s->c 74 | return self.overhead 75 | 76 | def sni(self, url): 77 | url = common.to_bytes(url) 78 | data = b"\x00" + struct.pack('>H', len(url)) + url 79 | data = b"\x00\x00" + struct.pack('>H', len(data) + 2) + struct.pack('>H', len(data)) + data 80 | return data 81 | 82 | def pack_auth_data(self, client_id): 83 | utc_time = int(time.time()) & 0xFFFFFFFF 84 | data = struct.pack('>I', utc_time) + os.urandom(18) 85 | data += hmac.new(self.server_info.key + client_id, data, hashlib.sha1).digest()[:10] 86 | return data 87 | 88 | def client_encode(self, buf): 89 | raise Exception('Need to finish') 90 | if self.obfs_stage == -1: 91 | return buf 92 | if self.obfs_stage == 1: 93 | self.obfs_stage += 1 94 | ret = b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf 95 | return ret 96 | if self.obfs_stage == 0: 97 | self.obfs_stage += 1 98 | data = self.tls_version + self.pack_auth_data(self.server_info.data.client_id) + b"\x20" + self.server_info.data.client_id + binascii.unhexlify(b"001cc02bc02fcca9cca8cc14cc13c00ac014c009c013009c0035002f000a" + b"0100") 99 | ext = binascii.unhexlify(b"ff01000100") 100 | host = self.server_info.obfs_param or self.server_info.host 101 | if host and host[-1] in string.digits: 102 | host = '' 103 | hosts = host.split(',') 104 | host = random.choice(hosts) 105 | ext += self.sni(host) 106 | ext += b"\x00\x17\x00\x00" 107 | ext += b"\x00\x23\x00\xd0" + os.urandom(208) # ticket 108 | ext += binascii.unhexlify(b"000d001600140601060305010503040104030301030302010203") 109 | ext += binascii.unhexlify(b"000500050100000000") 110 | ext += binascii.unhexlify(b"00120000") 111 | ext += binascii.unhexlify(b"75500000") 112 | ext += binascii.unhexlify(b"000b00020100") 113 | ext += binascii.unhexlify(b"000a0006000400170018") 114 | data += struct.pack('>H', len(ext)) + ext 115 | data = b"\x01\x00" + struct.pack('>H', len(data)) + data 116 | data = b"\x16\x03\x01" + struct.pack('>H', len(data)) + data 117 | return data 118 | return buf 119 | 120 | def client_decode(self, buf): 121 | raise Exception('Need to finish') 122 | if self.deobfs_stage == -1: 123 | return (buf, False) 124 | 125 | if self.deobfs_stage == 8: 126 | ret = b'' 127 | self.recv_buffer += buf 128 | while len(self.recv_buffer) > 5: 129 | if ord(self.recv_buffer[0]) != 0x17: 130 | logging.info("data = %s" % (binascii.hexlify(self.recv_buffer))) 131 | raise Exception('server_decode appdata error') 132 | size = struct.unpack('>H', self.recv_buffer[3:5])[0] 133 | if len(self.recv_buffer) < size + 5: 134 | break 135 | buf = self.recv_buffer[5:size+5] 136 | ret += buf 137 | self.recv_buffer = self.recv_buffer[size+5:] 138 | return (ret, False) 139 | 140 | if len(buf) < 11 + 32 + 1 + 32: 141 | raise Exception('client_decode data error') 142 | verify = buf[11:33] 143 | if hmac.new(self.server_info.key + self.server_info.data.client_id, verify, hashlib.sha1).digest()[:10] != buf[33:43]: 144 | raise Exception('client_decode data error') 145 | if hmac.new(self.server_info.key + self.server_info.data.client_id, buf[:-10], hashlib.sha1).digest()[:10] != buf[-10:]: 146 | raise Exception('client_decode data error') 147 | return (b'', True) 148 | 149 | def server_encode(self, buf): 150 | if self.obfs_stage == -1: 151 | return buf 152 | if self.obfs_stage == 1: 153 | ret = b'' 154 | while len(buf) > 2048: 155 | size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) 156 | ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size] 157 | buf = buf[size:] 158 | if len(buf) > 0: 159 | ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf 160 | return ret 161 | 162 | utc_time = int(time.time()) & 0xFFFFFFFF 163 | 164 | if len(self.client_id) < 32: 165 | session_id = os.urandom(32) 166 | else: 167 | session_id = self.client_id[:32] 168 | 169 | data = struct.pack('>I', utc_time) + os.urandom(28) + b"\x20" + session_id + b"\xcc\xa8\x00\x00\x00\xff\x01\x00\x01\x00\x00\x17\x00\x00\x00\x0b\x00\x02\x01\x00" #random_unix_time, ramdom_byte, session_id_len, session_id, the reset 170 | data = b"\x02\x00" + struct.pack('>H', 87) + b"\x03\x03" + data #handshake_type, handshake_len_1, handshake_len_2, handshake_version 171 | data = b"\x16\x03\x01" + struct.pack('>H', 91) + data #content_type, version, len 172 | 173 | data += b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec 174 | 175 | size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) 176 | 177 | data += b"\x16" + self.tls_version + struct.pack('>H', size) + buf[:size] 178 | 179 | if len(buf) - size > 0: 180 | buf = buf[size:] 181 | while len(buf) > 2048: 182 | size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) 183 | data += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size] 184 | buf = buf[size:] 185 | if len(buf) > 0: 186 | data += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf 187 | 188 | self.obfs_stage += 1 189 | 190 | return data 191 | 192 | def decode_error_return(self, buf): 193 | self.deobfs_stage = -1 194 | self.obfs_stage = -1 195 | self.overhead = 0 196 | if self.method == 'simple_obfs_tls': 197 | return (b'E'*2048, False, False) 198 | return (buf, True, False) 199 | 200 | def server_decode(self, buf): 201 | if self.deobfs_stage == -1: 202 | return (buf, True, False) 203 | 204 | if self.deobfs_stage == 1: 205 | ret = b'' 206 | self.recv_buffer += buf 207 | while len(self.recv_buffer) > 5: 208 | if ord(self.recv_buffer[0]) != 0x17 or ord(self.recv_buffer[1]) != 0x3 or ord(self.recv_buffer[2]) != 0x3: 209 | logging.info("data = %s" % (binascii.hexlify(self.recv_buffer))) 210 | raise Exception('server_decode appdata error') 211 | size = struct.unpack('>H', self.recv_buffer[3:5])[0] 212 | if len(self.recv_buffer) < size + 5: 213 | break 214 | ret += self.recv_buffer[5:size+5] 215 | self.recv_buffer = self.recv_buffer[size+5:] 216 | return (ret, True, False) 217 | 218 | #raise Exception("handshake data = %s" % (binascii.hexlify(buf))) 219 | 220 | self.recv_buffer += buf 221 | buf = self.recv_buffer 222 | ogn_buf = buf 223 | if len(buf) < 5: 224 | return (b'', False, False) 225 | if not match_begin(buf, b'\x16\x03\x01'): 226 | return self.decode_error_return(ogn_buf) 227 | buf = buf[3:] 228 | header_len = struct.unpack('>H', buf[:2])[0] 229 | if header_len > len(buf) - 2: 230 | return (b'', False, False) 231 | 232 | self.recv_buffer = self.recv_buffer[header_len + 5:] 233 | self.deobfs_stage = 1 234 | buf = buf[2:header_len + 2] 235 | if not match_begin(buf, b'\x01\x00'): #client hello 236 | logging.info("tls_auth not client hello message") 237 | return self.decode_error_return(ogn_buf) 238 | buf = buf[2:] 239 | if struct.unpack('>H', buf[:2])[0] != len(buf) - 2: 240 | logging.info("tls_auth wrong message size") 241 | return self.decode_error_return(ogn_buf) 242 | buf = buf[2:] 243 | if not match_begin(buf, self.tls_version): 244 | logging.info("tls_auth wrong tls version") 245 | return self.decode_error_return(ogn_buf) 246 | buf = buf[2:] 247 | verifyid = buf[:32] 248 | buf = buf[32:] 249 | sessionid_len = ord(buf[0]) 250 | if sessionid_len < 32: 251 | logging.info("tls_auth wrong sessionid_len") 252 | return self.decode_error_return(ogn_buf) 253 | sessionid = buf[1:sessionid_len + 1] 254 | buf = buf[sessionid_len+1:] 255 | self.client_id = sessionid 256 | utc_time = struct.unpack('>I', verifyid[:4])[0] 257 | time_dif = common.int32((int(time.time()) & 0xffffffff) - utc_time) 258 | if self.server_info.obfs_param: 259 | try: 260 | self.max_time_dif = int(self.server_info.obfs_param) 261 | except: 262 | pass 263 | if self.max_time_dif > 0 and (time_dif < -self.max_time_dif or time_dif > self.max_time_dif \ 264 | or common.int32(utc_time - self.server_info.data.startup_time) < -self.max_time_dif / 2): 265 | logging.info("tls_auth wrong time") 266 | return self.decode_error_return(ogn_buf) 267 | if self.server_info.data.client_data.get(verifyid[:22]): 268 | logging.info("replay attack detect, id = %s" % (binascii.hexlify(verifyid))) 269 | return self.decode_error_return(ogn_buf) 270 | self.server_info.data.client_data.sweep() 271 | self.server_info.data.client_data[verifyid[:22]] = sessionid 272 | # (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back) 273 | 274 | buf = buf[62:] 275 | if not match_begin(buf, b'\x00\x23'): 276 | logging.info("ext header error") 277 | return self.decode_error_return(ogn_buf) 278 | 279 | buf = buf[2:] 280 | ext_length = struct.unpack('>H', buf[:2])[0] 281 | buf = buf[2:] 282 | ret = buf[:ext_length] 283 | if len(self.recv_buffer) > 0: 284 | ret += self.server_decode(b'')[0] 285 | buf = buf[ext_length:] 286 | 287 | host_name = b'' 288 | buf = buf[7:] 289 | host_name_len = struct.unpack('>H', buf[:2])[0] 290 | buf = buf[2:] 291 | hostname = buf[:host_name_len] 292 | 293 | host_name = common.to_str(hostname) 294 | 295 | return (ret, True, False, host_name) 296 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/verify.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | import binascii 25 | import base64 26 | import time 27 | import datetime 28 | import random 29 | import struct 30 | import zlib 31 | import hmac 32 | import hashlib 33 | 34 | import shadowsocks 35 | from shadowsocks import common 36 | from shadowsocks.obfsplugin import plain 37 | from shadowsocks.common import to_bytes, to_str, ord, chr 38 | 39 | def create_verify_obfs(method): 40 | return verify_simple(method) 41 | 42 | def create_verify_deflate(method): 43 | return verify_deflate(method) 44 | 45 | def create_verify_sha1(method): 46 | return verify_sha1(method) 47 | 48 | def create_auth_obfs(method): 49 | return auth_simple(method) 50 | 51 | obfs_map = { 52 | 'verify_simple': (create_verify_obfs,), 53 | 'verify_deflate': (create_verify_deflate,), 54 | 'verify_sha1': (create_verify_sha1,), 55 | 'verify_sha1_compatible': (create_verify_sha1,), 56 | } 57 | 58 | def match_begin(str1, str2): 59 | if len(str1) >= len(str2): 60 | if str1[:len(str2)] == str2: 61 | return True 62 | return False 63 | 64 | class obfs_verify_data(object): 65 | def __init__(self): 66 | pass 67 | 68 | class verify_base(plain.plain): 69 | def __init__(self, method): 70 | super(verify_base, self).__init__(method) 71 | self.method = method 72 | 73 | def init_data(self): 74 | return obfs_verify_data() 75 | 76 | def set_server_info(self, server_info): 77 | self.server_info = server_info 78 | 79 | def client_encode(self, buf): 80 | return buf 81 | 82 | def client_decode(self, buf): 83 | return (buf, False) 84 | 85 | def server_encode(self, buf): 86 | return buf 87 | 88 | def server_decode(self, buf): 89 | return (buf, True, False) 90 | 91 | class verify_simple(verify_base): 92 | def __init__(self, method): 93 | super(verify_simple, self).__init__(method) 94 | self.recv_buf = b'' 95 | self.unit_len = 8100 96 | self.decrypt_packet_num = 0 97 | self.raw_trans = False 98 | 99 | def pack_data(self, buf): 100 | if len(buf) == 0: 101 | return b'' 102 | rnd_data = os.urandom(common.ord(os.urandom(1)[0]) % 16) 103 | data = common.chr(len(rnd_data) + 1) + rnd_data + buf 104 | data = struct.pack('>H', len(data) + 6) + data 105 | crc = (0xffffffff - binascii.crc32(data)) & 0xffffffff 106 | data += struct.pack(' self.unit_len: 112 | ret += self.pack_data(buf[:self.unit_len]) 113 | buf = buf[self.unit_len:] 114 | ret += self.pack_data(buf) 115 | return ret 116 | 117 | def client_post_decrypt(self, buf): 118 | if self.raw_trans: 119 | return buf 120 | self.recv_buf += buf 121 | out_buf = b'' 122 | while len(self.recv_buf) > 2: 123 | length = struct.unpack('>H', self.recv_buf[:2])[0] 124 | if length >= 8192 or length < 7: 125 | self.raw_trans = True 126 | self.recv_buf = b'' 127 | raise Exception('client_post_decrypt data error') 128 | if length > len(self.recv_buf): 129 | break 130 | 131 | if (binascii.crc32(self.recv_buf[:length]) & 0xffffffff) != 0xffffffff: 132 | self.raw_trans = True 133 | self.recv_buf = b'' 134 | raise Exception('client_post_decrypt data uncorrect CRC32') 135 | 136 | pos = common.ord(self.recv_buf[2]) + 2 137 | out_buf += self.recv_buf[pos:length - 4] 138 | self.recv_buf = self.recv_buf[length:] 139 | 140 | if out_buf: 141 | self.decrypt_packet_num += 1 142 | return out_buf 143 | 144 | def server_pre_encrypt(self, buf): 145 | ret = b'' 146 | while len(buf) > self.unit_len: 147 | ret += self.pack_data(buf[:self.unit_len]) 148 | buf = buf[self.unit_len:] 149 | ret += self.pack_data(buf) 150 | return ret 151 | 152 | def server_post_decrypt(self, buf): 153 | if self.raw_trans: 154 | return (buf, False) 155 | self.recv_buf += buf 156 | out_buf = b'' 157 | while len(self.recv_buf) > 2: 158 | length = struct.unpack('>H', self.recv_buf[:2])[0] 159 | if length >= 8192 or length < 7: 160 | self.raw_trans = True 161 | self.recv_buf = b'' 162 | if self.decrypt_packet_num == 0: 163 | return (b'E'*2048, False) 164 | else: 165 | raise Exception('server_post_decrype data error') 166 | if length > len(self.recv_buf): 167 | break 168 | 169 | if (binascii.crc32(self.recv_buf[:length]) & 0xffffffff) != 0xffffffff: 170 | self.raw_trans = True 171 | self.recv_buf = b'' 172 | if self.decrypt_packet_num == 0: 173 | return (b'E'*2048, False) 174 | else: 175 | raise Exception('server_post_decrype data uncorrect CRC32') 176 | 177 | pos = common.ord(self.recv_buf[2]) + 2 178 | out_buf += self.recv_buf[pos:length - 4] 179 | self.recv_buf = self.recv_buf[length:] 180 | 181 | if out_buf: 182 | self.decrypt_packet_num += 1 183 | return (out_buf, False) 184 | 185 | class verify_deflate(verify_base): 186 | def __init__(self, method): 187 | super(verify_deflate, self).__init__(method) 188 | self.recv_buf = b'' 189 | self.unit_len = 32700 190 | self.decrypt_packet_num = 0 191 | self.raw_trans = False 192 | 193 | def pack_data(self, buf): 194 | if len(buf) == 0: 195 | return b'' 196 | data = zlib.compress(buf) 197 | data = struct.pack('>H', len(data)) + data[2:] 198 | return data 199 | 200 | def client_pre_encrypt(self, buf): 201 | ret = b'' 202 | while len(buf) > self.unit_len: 203 | ret += self.pack_data(buf[:self.unit_len]) 204 | buf = buf[self.unit_len:] 205 | ret += self.pack_data(buf) 206 | return ret 207 | 208 | def client_post_decrypt(self, buf): 209 | if self.raw_trans: 210 | return buf 211 | self.recv_buf += buf 212 | out_buf = b'' 213 | while len(self.recv_buf) > 2: 214 | length = struct.unpack('>H', self.recv_buf[:2])[0] 215 | if length >= 32768 or length < 6: 216 | self.raw_trans = True 217 | self.recv_buf = b'' 218 | raise Exception('client_post_decrypt data error') 219 | if length > len(self.recv_buf): 220 | break 221 | 222 | out_buf += zlib.decompress(b'x\x9c' + self.recv_buf[2:length]) 223 | self.recv_buf = self.recv_buf[length:] 224 | 225 | if out_buf: 226 | self.decrypt_packet_num += 1 227 | return out_buf 228 | 229 | def server_pre_encrypt(self, buf): 230 | ret = b'' 231 | while len(buf) > self.unit_len: 232 | ret += self.pack_data(buf[:self.unit_len]) 233 | buf = buf[self.unit_len:] 234 | ret += self.pack_data(buf) 235 | return ret 236 | 237 | def server_post_decrypt(self, buf): 238 | if self.raw_trans: 239 | return (buf, False) 240 | self.recv_buf += buf 241 | out_buf = b'' 242 | while len(self.recv_buf) > 2: 243 | length = struct.unpack('>H', self.recv_buf[:2])[0] 244 | if length >= 32768 or length < 6: 245 | self.raw_trans = True 246 | self.recv_buf = b'' 247 | if self.decrypt_packet_num == 0: 248 | return (b'E'*2048, False) 249 | else: 250 | raise Exception('server_post_decrype data error') 251 | if length > len(self.recv_buf): 252 | break 253 | 254 | out_buf += zlib.decompress(b'\x78\x9c' + self.recv_buf[2:length]) 255 | self.recv_buf = self.recv_buf[length:] 256 | 257 | if out_buf: 258 | self.decrypt_packet_num += 1 259 | return (out_buf, False) 260 | 261 | class verify_sha1(verify_base): 262 | def __init__(self, method): 263 | super(verify_sha1, self).__init__(method) 264 | self.recv_buf = b'' 265 | self.unit_len = 8100 266 | self.raw_trans = False 267 | self.pack_id = 0 268 | self.recv_id = 0 269 | self.has_sent_header = False 270 | self.has_recv_header = False 271 | 272 | def pack_data(self, buf): 273 | if len(buf) == 0: 274 | return b'' 275 | sha1data = hmac.new(self.server_info.iv + struct.pack('>I', self.pack_id), buf, hashlib.sha1).digest() 276 | data = struct.pack('>H', len(buf)) + sha1data[:10] + buf 277 | self.pack_id += 1 278 | return data 279 | 280 | def pack_auth_data(self, buf): 281 | data = chr(ord(buf[0]) | 0x10) + buf[1:] 282 | data += hmac.new(self.server_info.iv + self.server_info.key, data, hashlib.sha1).digest()[:10] 283 | return data 284 | 285 | def client_pre_encrypt(self, buf): 286 | ret = b'' 287 | if not self.has_sent_header: 288 | datalen = self.get_head_size(buf, 30) 289 | ret += self.pack_auth_data(buf[:datalen]) 290 | buf = buf[datalen:] 291 | self.has_sent_header = True 292 | while len(buf) > self.unit_len: 293 | ret += self.pack_data(buf[:self.unit_len]) 294 | buf = buf[self.unit_len:] 295 | ret += self.pack_data(buf) 296 | return ret 297 | 298 | def client_post_decrypt(self, buf): 299 | return buf 300 | 301 | def server_pre_encrypt(self, buf): 302 | return buf 303 | 304 | def not_match_return(self, buf): 305 | self.raw_trans = True 306 | if self.method == 'verify_sha1': 307 | return (b'E'*2048, False) 308 | return (buf, False) 309 | 310 | def server_post_decrypt(self, buf): 311 | if self.raw_trans: 312 | return (buf, False) 313 | self.recv_buf += buf 314 | out_buf = b'' 315 | if not self.has_recv_header: 316 | if len(self.recv_buf) < 2: 317 | return (b'', False) 318 | if (ord(self.recv_buf[0]) & 0x10) != 0x10: 319 | return self.not_match_return(self.recv_buf) 320 | head_size = self.get_head_size(self.recv_buf, 65536) 321 | if len(self.recv_buf) < head_size + 10: 322 | return self.not_match_return(self.recv_buf) 323 | sha1data = hmac.new(self.server_info.recv_iv + self.server_info.key, self.recv_buf[:head_size], hashlib.sha1).digest()[:10] 324 | if sha1data != self.recv_buf[head_size:head_size + 10]: 325 | logging.error('server_post_decrype data uncorrect auth HMAC-SHA1') 326 | return self.not_match_return(self.recv_buf) 327 | out_buf = to_bytes(chr(ord(self.recv_buf[0]) & 0xEF)) + self.recv_buf[1:head_size] 328 | self.recv_buf = self.recv_buf[head_size + 10:] 329 | self.has_recv_header = True 330 | while len(self.recv_buf) > 2: 331 | length = struct.unpack('>H', self.recv_buf[:2])[0] + 12 332 | if length > len(self.recv_buf): 333 | break 334 | 335 | data = self.recv_buf[12:length] 336 | sha1data = hmac.new(self.server_info.recv_iv + struct.pack('>I', self.recv_id), data, hashlib.sha1).digest()[:10] 337 | if sha1data != self.recv_buf[2:12]: 338 | raise Exception('server_post_decrype data uncorrect chunk HMAC-SHA1') 339 | 340 | self.recv_id = (self.recv_id + 1) & 0xFFFFFFFF 341 | out_buf += data 342 | self.recv_buf = self.recv_buf[length:] 343 | 344 | return (out_buf, False) 345 | 346 | def client_udp_pre_encrypt(self, buf): 347 | ret = self.pack_auth_data(buf) 348 | return chr(ord(buf[0]) | 0x10) + buf[1:] 349 | 350 | def server_udp_post_decrypt(self, buf): 351 | if buf and ((ord(buf[0]) & 0x10) == 0x10): 352 | if len(buf) <= 11: 353 | return (b'', None) 354 | sha1data = hmac.new(self.server_info.recv_iv + self.server_info.key, buf[:-10], hashlib.sha1).digest()[:10] 355 | if sha1data != buf[-10:]: 356 | return (b'', None) 357 | return (to_bytes(chr(ord(buf[0]) & 0xEF)) + buf[1:-10], None) 358 | else: 359 | return (buf, None) 360 | 361 | -------------------------------------------------------------------------------- /shadowsocks/obfsplugin/http_simple.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright 2015-2015 breakwa11 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | # not use this file except in compliance with the License. You may obtain 7 | # a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | # License for the specific language governing permissions and limitations 15 | # under the License. 16 | 17 | from __future__ import absolute_import, division, print_function, \ 18 | with_statement 19 | 20 | import os 21 | import sys 22 | import hashlib 23 | import logging 24 | import binascii 25 | import struct 26 | import base64 27 | import datetime 28 | import random 29 | 30 | from shadowsocks import common 31 | from shadowsocks.obfsplugin import plain 32 | from shadowsocks.common import to_bytes, to_str, ord, chr 33 | 34 | def create_http_simple_obfs(method): 35 | return http_simple(method) 36 | 37 | def create_http_post_obfs(method): 38 | return http_post(method) 39 | 40 | def create_random_head_obfs(method): 41 | return random_head(method) 42 | 43 | obfs_map = { 44 | 'http_simple': (create_http_simple_obfs,), 45 | 'http_simple_compatible': (create_http_simple_obfs,), 46 | 'http_post': (create_http_post_obfs,), 47 | 'http_post_compatible': (create_http_post_obfs,), 48 | 'random_head': (create_random_head_obfs,), 49 | 'random_head_compatible': (create_random_head_obfs,), 50 | } 51 | 52 | def match_begin(str1, str2): 53 | if len(str1) >= len(str2): 54 | if str1[:len(str2)] == str2: 55 | return True 56 | return False 57 | 58 | class http_simple(plain.plain): 59 | def __init__(self, method): 60 | self.method = method 61 | self.has_sent_header = False 62 | self.has_recv_header = False 63 | self.host = None 64 | self.port = 0 65 | self.recv_buffer = b'' 66 | self.user_agent = [b"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0", 67 | b"Mozilla/5.0 (Windows NT 6.3; WOW64; rv:40.0) Gecko/20100101 Firefox/44.0", 68 | b"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", 69 | b"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Ubuntu/11.10 Chromium/27.0.1453.93 Chrome/27.0.1453.93 Safari/537.36", 70 | b"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:35.0) Gecko/20100101 Firefox/35.0", 71 | b"Mozilla/5.0 (compatible; WOW64; MSIE 10.0; Windows NT 6.2)", 72 | b"Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27", 73 | b"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.3; Trident/7.0; .NET4.0E; .NET4.0C)", 74 | b"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko", 75 | b"Mozilla/5.0 (Linux; Android 4.4; Nexus 5 Build/BuildID) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Mobile Safari/537.36", 76 | b"Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", 77 | b"Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3"] 78 | 79 | def encode_head(self, buf): 80 | hexstr = binascii.hexlify(buf) 81 | chs = [] 82 | for i in range(0, len(hexstr), 2): 83 | chs.append(b"%" + hexstr[i:i+2]) 84 | return b''.join(chs) 85 | 86 | def client_encode(self, buf): 87 | if self.has_sent_header: 88 | return buf 89 | head_size = len(self.server_info.iv) + self.server_info.head_len 90 | if len(buf) - head_size > 64: 91 | headlen = head_size + random.randint(0, 64) 92 | else: 93 | headlen = len(buf) 94 | headdata = buf[:headlen] 95 | buf = buf[headlen:] 96 | port = b'' 97 | if self.server_info.port != 80: 98 | port = b':' + to_bytes(str(self.server_info.port)) 99 | body = None 100 | hosts = (self.server_info.obfs_param or self.server_info.host) 101 | pos = hosts.find("#") 102 | if pos >= 0: 103 | body = hosts[pos + 1:].replace("\\n", "\r\n") 104 | hosts = hosts[:pos] 105 | hosts = hosts.split(',') 106 | host = random.choice(hosts) 107 | http_head = b"GET /" + self.encode_head(headdata) + b" HTTP/1.1\r\n" 108 | http_head += b"Host: " + to_bytes(host) + port + b"\r\n" 109 | if body: 110 | http_head += body + "\r\n\r\n" 111 | else: 112 | http_head += b"User-Agent: " + random.choice(self.user_agent) + b"\r\n" 113 | http_head += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\nDNT: 1\r\nConnection: keep-alive\r\n\r\n" 114 | self.has_sent_header = True 115 | return http_head + buf 116 | 117 | def client_decode(self, buf): 118 | if self.has_recv_header: 119 | return (buf, False) 120 | pos = buf.find(b'\r\n\r\n') 121 | if pos >= 0: 122 | self.has_recv_header = True 123 | return (buf[pos + 4:], False) 124 | else: 125 | return (b'', False) 126 | 127 | def server_encode(self, buf): 128 | if self.has_sent_header: 129 | return buf 130 | 131 | header = b'HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Encoding: gzip\r\nContent-Type: text/html\r\nDate: ' 132 | header += to_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S GMT')) 133 | header += b'\r\nServer: nginx\r\nVary: Accept-Encoding\r\n\r\n' 134 | self.has_sent_header = True 135 | return header + buf 136 | 137 | def get_data_from_http_header(self, buf): 138 | ret_buf = b'' 139 | lines = buf.split(b'\r\n') 140 | if lines and len(lines) > 1: 141 | hex_items = lines[0].split(b'%') 142 | if hex_items and len(hex_items) > 1: 143 | for index in range(1, len(hex_items)): 144 | if len(hex_items[index]) < 2: 145 | ret_buf += binascii.unhexlify('0' + hex_items[index]) 146 | break 147 | elif len(hex_items[index]) > 2: 148 | ret_buf += binascii.unhexlify(hex_items[index][:2]) 149 | break 150 | else: 151 | ret_buf += binascii.unhexlify(hex_items[index]) 152 | return ret_buf 153 | return b'' 154 | 155 | def get_host_from_http_header(self, buf): 156 | ret_buf = b'' 157 | lines = buf.split(b'\r\n') 158 | if lines and len(lines) > 1: 159 | for line in lines: 160 | if match_begin(line, b"Host: "): 161 | return common.to_str(line[6:]) 162 | 163 | def not_match_return(self, buf): 164 | self.has_sent_header = True 165 | self.has_recv_header = True 166 | if self.method == 'http_simple': 167 | return (b'E'*2048, False, False) 168 | return (buf, True, False) 169 | 170 | def error_return(self, buf): 171 | self.has_sent_header = True 172 | self.has_recv_header = True 173 | return (b'E'*2048, False, False) 174 | 175 | def server_decode(self, buf): 176 | if self.has_recv_header: 177 | return (buf, True, False) 178 | 179 | self.recv_buffer += buf 180 | buf = self.recv_buffer 181 | if len(buf) > 10: 182 | if match_begin(buf, b'GET /') or match_begin(buf, b'POST /'): 183 | if len(buf) > 65536: 184 | self.recv_buffer = None 185 | logging.warn('http_simple: over size') 186 | return self.not_match_return(buf) 187 | else: #not http header, run on original protocol 188 | self.recv_buffer = None 189 | logging.debug('http_simple: not match begin') 190 | return self.not_match_return(buf) 191 | else: 192 | return (b'', True, False) 193 | 194 | if b'\r\n\r\n' in buf: 195 | datas = buf.split(b'\r\n\r\n', 1) 196 | ret_buf = self.get_data_from_http_header(buf) 197 | host = self.get_host_from_http_header(buf) 198 | if host and self.server_info.obfs_param: 199 | pos = host.find(":") 200 | if pos >= 0: 201 | host = host[:pos] 202 | hosts = self.server_info.obfs_param.split(b',') 203 | if common.to_bytes(host) not in hosts: 204 | return self.not_match_return(buf) 205 | if len(ret_buf) < 4: 206 | return self.error_return(buf) 207 | if len(datas) > 1: 208 | ret_buf += datas[1] 209 | if len(ret_buf) >= 13: 210 | self.has_recv_header = True 211 | return (ret_buf, True, False, host) 212 | return self.not_match_return(buf) 213 | else: 214 | return (b'', True, False) 215 | 216 | class http_post(http_simple): 217 | def __init__(self, method): 218 | super(http_post, self).__init__(method) 219 | 220 | def boundary(self): 221 | return to_bytes(''.join([random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") for i in range(32)])) 222 | 223 | def client_encode(self, buf): 224 | if self.has_sent_header: 225 | return buf 226 | head_size = len(self.server_info.iv) + self.server_info.head_len 227 | if len(buf) - head_size > 64: 228 | headlen = head_size + random.randint(0, 64) 229 | else: 230 | headlen = len(buf) 231 | headdata = buf[:headlen] 232 | buf = buf[headlen:] 233 | port = b'' 234 | if self.server_info.port != 80: 235 | port = b':' + to_bytes(str(self.server_info.port)) 236 | body = None 237 | hosts = (self.server_info.obfs_param or self.server_info.host) 238 | pos = hosts.find("#") 239 | if pos >= 0: 240 | body = hosts[pos + 1:].replace("\\n", "\r\n") 241 | hosts = hosts[:pos] 242 | hosts = hosts.split(',') 243 | host = random.choice(hosts) 244 | http_head = b"POST /" + self.encode_head(headdata) + b" HTTP/1.1\r\n" 245 | http_head += b"Host: " + to_bytes(host) + port + b"\r\n" 246 | if body: 247 | http_head += body + "\r\n\r\n" 248 | else: 249 | http_head += b"User-Agent: " + random.choice(self.user_agent) + b"\r\n" 250 | http_head += b"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q=0.8\r\nAccept-Encoding: gzip, deflate\r\n" 251 | http_head += b"Content-Type: multipart/form-data; boundary=" + self.boundary() + b"\r\nDNT: 1\r\n" 252 | http_head += b"Connection: keep-alive\r\n\r\n" 253 | self.has_sent_header = True 254 | return http_head + buf 255 | 256 | def not_match_return(self, buf): 257 | self.has_sent_header = True 258 | self.has_recv_header = True 259 | if self.method == 'http_post': 260 | return (b'E'*2048, False, False) 261 | return (buf, True, False) 262 | 263 | def server_decode(self, buf): 264 | if self.has_recv_header: 265 | return (buf, True, False) 266 | 267 | self.recv_buffer += buf 268 | buf = self.recv_buffer 269 | if len(buf) > 10: 270 | if match_begin(buf, b'GET ') or match_begin(buf, b'POST '): 271 | if len(buf) > 65536: 272 | self.recv_buffer = None 273 | logging.warn('http_post: over size') 274 | return self.not_match_return(buf) 275 | else: #not http header, run on original protocol 276 | self.recv_buffer = None 277 | logging.debug('http_post: not match begin') 278 | return self.not_match_return(buf) 279 | else: 280 | return (b'', True, False) 281 | 282 | if b'\r\n\r\n' in buf: 283 | datas = buf.split(b'\r\n\r\n', 1) 284 | ret_buf = self.get_data_from_http_header(buf) 285 | host = self.get_host_from_http_header(buf) 286 | if host and self.server_info.obfs_param: 287 | pos = host.find(b":") 288 | if pos >= 0: 289 | host = host[:pos] 290 | hosts = self.server_info.obfs_param.split(b',') 291 | if common.to_bytes(host) not in hosts: 292 | return self.not_match_return(buf) 293 | if len(datas) > 1: 294 | ret_buf += datas[1] 295 | if len(ret_buf) >= 7: 296 | self.has_recv_header = True 297 | return (ret_buf, True, False, host) 298 | return self.not_match_return(buf) 299 | else: 300 | return (b'', True, False) 301 | 302 | class random_head(plain.plain): 303 | def __init__(self, method): 304 | self.method = method 305 | self.has_sent_header = False 306 | self.has_recv_header = False 307 | self.raw_trans_sent = False 308 | self.raw_trans_recv = False 309 | self.send_buffer = b'' 310 | 311 | def client_encode(self, buf): 312 | if self.raw_trans_sent: 313 | return buf 314 | self.send_buffer += buf 315 | if not self.has_sent_header: 316 | self.has_sent_header = True 317 | data = os.urandom(common.ord(os.urandom(1)[0]) % 96 + 4) 318 | crc = (0xffffffff - binascii.crc32(data)) & 0xffffffff 319 | return data + struct.pack('= len(str2): 50 | if str1[:len(str2)] == str2: 51 | return True 52 | return False 53 | 54 | class obfs_auth_data(object): 55 | def __init__(self): 56 | self.client_data = lru_cache.LRUCache(60 * 5) 57 | self.client_id = os.urandom(32) 58 | self.startup_time = int(time.time() - 60 * 30) & 0xFFFFFFFF 59 | self.ticket_buf = {} 60 | 61 | class tls_ticket_auth(plain.plain): 62 | def __init__(self, method): 63 | self.method = method 64 | self.handshake_status = 0 65 | self.send_buffer = b'' 66 | self.recv_buffer = b'' 67 | self.client_id = b'' 68 | self.max_time_dif = 60 * 60 * 24 # time dif (second) setting 69 | self.tls_version = b'\x03\x03' 70 | self.overhead = 5 71 | 72 | def init_data(self): 73 | return obfs_auth_data() 74 | 75 | def get_overhead(self, direction): # direction: true for c->s false for s->c 76 | return self.overhead 77 | 78 | def sni(self, url): 79 | url = common.to_bytes(url) 80 | data = b"\x00" + struct.pack('>H', len(url)) + url 81 | data = b"\x00\x00" + struct.pack('>H', len(data) + 2) + struct.pack('>H', len(data)) + data 82 | return data 83 | 84 | def pack_auth_data(self, client_id): 85 | utc_time = int(time.time()) & 0xFFFFFFFF 86 | data = struct.pack('>I', utc_time) + os.urandom(18) 87 | data += hmac.new(self.server_info.key + client_id, data, hashlib.sha1).digest()[:10] 88 | return data 89 | 90 | def client_encode(self, buf): 91 | if self.handshake_status == -1: 92 | return buf 93 | if self.handshake_status == 8: 94 | ret = b'' 95 | while len(buf) > 2048: 96 | size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) 97 | ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size] 98 | buf = buf[size:] 99 | if len(buf) > 0: 100 | ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf 101 | return ret 102 | if len(buf) > 0: 103 | self.send_buffer += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf 104 | if self.handshake_status == 0: 105 | self.handshake_status = 1 106 | data = self.tls_version + self.pack_auth_data(self.server_info.data.client_id) + b"\x20" + self.server_info.data.client_id + binascii.unhexlify(b"001cc02bc02fcca9cca8cc14cc13c00ac014c009c013009c0035002f000a" + b"0100") 107 | ext = binascii.unhexlify(b"ff01000100") 108 | host = self.server_info.obfs_param or self.server_info.host 109 | if host and host[-1] in string.digits: 110 | host = '' 111 | hosts = host.split(',') 112 | host = random.choice(hosts) 113 | ext += self.sni(host) 114 | ext += b"\x00\x17\x00\x00" 115 | if host not in self.server_info.data.ticket_buf: 116 | self.server_info.data.ticket_buf[host] = os.urandom((struct.unpack('>H', os.urandom(2))[0] % 17 + 8) * 16) 117 | ext += b"\x00\x23" + struct.pack('>H', len(self.server_info.data.ticket_buf[host])) + self.server_info.data.ticket_buf[host] 118 | ext += b"\x00\x23\x00\xd0" + os.urandom(208) # ticket 119 | ext += binascii.unhexlify(b"000d001600140601060305010503040104030301030302010203") 120 | ext += binascii.unhexlify(b"000500050100000000") 121 | ext += binascii.unhexlify(b"00120000") 122 | ext += binascii.unhexlify(b"75500000") 123 | ext += binascii.unhexlify(b"000b00020100") 124 | ext += binascii.unhexlify(b"000a0006000400170018") 125 | data += struct.pack('>H', len(ext)) + ext 126 | data = b"\x01\x00" + struct.pack('>H', len(data)) + data 127 | data = b"\x16\x03\x01" + struct.pack('>H', len(data)) + data 128 | return data 129 | elif self.handshake_status == 1 and len(buf) == 0: 130 | data = b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec 131 | data += b"\x16" + self.tls_version + b"\x00\x20" + os.urandom(22) #Finished 132 | data += hmac.new(self.server_info.key + self.server_info.data.client_id, data, hashlib.sha1).digest()[:10] 133 | ret = data + self.send_buffer 134 | self.send_buffer = b'' 135 | self.handshake_status = 8 136 | return ret 137 | return b'' 138 | 139 | def client_decode(self, buf): 140 | if self.handshake_status == -1: 141 | return (buf, False) 142 | 143 | if self.handshake_status == 8: 144 | ret = b'' 145 | self.recv_buffer += buf 146 | while len(self.recv_buffer) > 5: 147 | if ord(self.recv_buffer[0]) != 0x17: 148 | logging.info("data = %s" % (binascii.hexlify(self.recv_buffer))) 149 | raise Exception('server_decode appdata error') 150 | size = struct.unpack('>H', self.recv_buffer[3:5])[0] 151 | if len(self.recv_buffer) < size + 5: 152 | break 153 | buf = self.recv_buffer[5:size+5] 154 | ret += buf 155 | self.recv_buffer = self.recv_buffer[size+5:] 156 | return (ret, False) 157 | 158 | if len(buf) < 11 + 32 + 1 + 32: 159 | raise Exception('client_decode data error') 160 | verify = buf[11:33] 161 | if hmac.new(self.server_info.key + self.server_info.data.client_id, verify, hashlib.sha1).digest()[:10] != buf[33:43]: 162 | raise Exception('client_decode data error') 163 | if hmac.new(self.server_info.key + self.server_info.data.client_id, buf[:-10], hashlib.sha1).digest()[:10] != buf[-10:]: 164 | raise Exception('client_decode data error') 165 | return (b'', True) 166 | 167 | def server_encode(self, buf): 168 | if self.handshake_status == -1: 169 | return buf 170 | if (self.handshake_status & 8) == 8: 171 | ret = b'' 172 | while len(buf) > 2048: 173 | size = min(struct.unpack('>H', os.urandom(2))[0] % 4096 + 100, len(buf)) 174 | ret += b"\x17" + self.tls_version + struct.pack('>H', size) + buf[:size] 175 | buf = buf[size:] 176 | if len(buf) > 0: 177 | ret += b"\x17" + self.tls_version + struct.pack('>H', len(buf)) + buf 178 | return ret 179 | self.handshake_status |= 8 180 | data = self.tls_version + self.pack_auth_data(self.client_id) + b"\x20" + self.client_id + binascii.unhexlify(b"c02f000005ff01000100") 181 | data = b"\x02\x00" + struct.pack('>H', len(data)) + data #server hello 182 | data = b"\x16" + self.tls_version + struct.pack('>H', len(data)) + data 183 | if random.randint(0, 8) < 1: 184 | ticket = os.urandom((struct.unpack('>H', os.urandom(2))[0] % 164) * 2 + 64) 185 | ticket = struct.pack('>H', len(ticket) + 4) + b"\x04\x00" + struct.pack('>H', len(ticket)) + ticket 186 | data += b"\x16" + self.tls_version + ticket #New session ticket 187 | data += b"\x14" + self.tls_version + b"\x00\x01\x01" #ChangeCipherSpec 188 | finish_len = random.choice([32, 40]) 189 | data += b"\x16" + self.tls_version + struct.pack('>H', finish_len) + os.urandom(finish_len - 10) #Finished 190 | data += hmac.new(self.server_info.key + self.client_id, data, hashlib.sha1).digest()[:10] 191 | if buf: 192 | data += self.server_encode(buf) 193 | return data 194 | 195 | def decode_error_return(self, buf): 196 | self.handshake_status = -1 197 | if self.overhead > 0: 198 | self.server_info.overhead -= self.overhead 199 | self.overhead = 0 200 | if self.method == 'tls1.2_ticket_auth' or self.method == 'tls1.2_ticket_fastauth': 201 | return (b'E'*2048, False, False) 202 | return (buf, True, False) 203 | 204 | def server_decode(self, buf): 205 | if self.handshake_status == -1: 206 | return (buf, True, False) 207 | 208 | if (self.handshake_status & 4) == 4: 209 | ret = b'' 210 | self.recv_buffer += buf 211 | while len(self.recv_buffer) > 5: 212 | if ord(self.recv_buffer[0]) != 0x17 or ord(self.recv_buffer[1]) != 0x3 or ord(self.recv_buffer[2]) != 0x3: 213 | logging.info("data = %s" % (binascii.hexlify(self.recv_buffer))) 214 | raise Exception('server_decode appdata error') 215 | size = struct.unpack('>H', self.recv_buffer[3:5])[0] 216 | if len(self.recv_buffer) < size + 5: 217 | break 218 | ret += self.recv_buffer[5:size+5] 219 | self.recv_buffer = self.recv_buffer[size+5:] 220 | return (ret, True, False) 221 | 222 | if (self.handshake_status & 1) == 1: 223 | self.recv_buffer += buf 224 | buf = self.recv_buffer 225 | verify = buf 226 | if len(buf) < 11: 227 | raise Exception('server_decode data error') 228 | if not match_begin(buf, b"\x14" + self.tls_version + b"\x00\x01\x01"): #ChangeCipherSpec 229 | raise Exception('server_decode data error') 230 | buf = buf[6:] 231 | if not match_begin(buf, b"\x16" + self.tls_version + b"\x00"): #Finished 232 | raise Exception('server_decode data error') 233 | verify_len = struct.unpack('>H', buf[3:5])[0] + 1 # 11 - 10 234 | if len(verify) < verify_len + 10: 235 | return (b'', False, False) 236 | if hmac.new(self.server_info.key + self.client_id, verify[:verify_len], hashlib.sha1).digest()[:10] != verify[verify_len:verify_len+10]: 237 | raise Exception('server_decode data error') 238 | self.recv_buffer = verify[verify_len + 10:] 239 | status = self.handshake_status 240 | self.handshake_status |= 4 241 | ret = self.server_decode(b'') 242 | return ret; 243 | 244 | #raise Exception("handshake data = %s" % (binascii.hexlify(buf))) 245 | self.recv_buffer += buf 246 | buf = self.recv_buffer 247 | ogn_buf = buf 248 | if len(buf) < 3: 249 | return (b'', False, False) 250 | if not match_begin(buf, b'\x16\x03\x01'): 251 | return self.decode_error_return(ogn_buf) 252 | buf = buf[3:] 253 | header_len = struct.unpack('>H', buf[:2])[0] 254 | if header_len > len(buf) - 2: 255 | return (b'', False, False) 256 | 257 | self.recv_buffer = self.recv_buffer[header_len + 5:] 258 | self.handshake_status = 1 259 | buf = buf[2:header_len + 2] 260 | if not match_begin(buf, b'\x01\x00'): #client hello 261 | logging.info("tls_auth not client hello message") 262 | return self.decode_error_return(ogn_buf) 263 | buf = buf[2:] 264 | if struct.unpack('>H', buf[:2])[0] != len(buf) - 2: 265 | logging.info("tls_auth wrong message size") 266 | return self.decode_error_return(ogn_buf) 267 | buf = buf[2:] 268 | if not match_begin(buf, self.tls_version): 269 | logging.info("tls_auth wrong tls version") 270 | return self.decode_error_return(ogn_buf) 271 | buf = buf[2:] 272 | verifyid = buf[:32] 273 | buf = buf[32:] 274 | sessionid_len = ord(buf[0]) 275 | if sessionid_len < 32: 276 | logging.info("tls_auth wrong sessionid_len") 277 | return self.decode_error_return(ogn_buf) 278 | sessionid = buf[1:sessionid_len + 1] 279 | buf = buf[sessionid_len+1:] 280 | self.client_id = sessionid 281 | sha1 = hmac.new(self.server_info.key + sessionid, verifyid[:22], hashlib.sha1).digest()[:10] 282 | utc_time = struct.unpack('>I', verifyid[:4])[0] 283 | time_dif = common.int32((int(time.time()) & 0xffffffff) - utc_time) 284 | if self.server_info.obfs_param: 285 | try: 286 | self.max_time_dif = int(self.server_info.obfs_param) 287 | except: 288 | pass 289 | if self.max_time_dif > 0 and (time_dif < -self.max_time_dif or time_dif > self.max_time_dif \ 290 | or common.int32(utc_time - self.server_info.data.startup_time) < -self.max_time_dif / 2): 291 | logging.info("tls_auth wrong time") 292 | return self.decode_error_return(ogn_buf) 293 | if sha1 != verifyid[22:]: 294 | logging.info("tls_auth wrong sha1") 295 | return self.decode_error_return(ogn_buf) 296 | if self.server_info.data.client_data.get(verifyid[:22]): 297 | logging.info("replay attack detect, id = %s" % (binascii.hexlify(verifyid))) 298 | return self.decode_error_return(ogn_buf) 299 | self.server_info.data.client_data.sweep() 300 | self.server_info.data.client_data[verifyid[:22]] = sessionid 301 | if len(self.recv_buffer) >= 11: 302 | ret = self.server_decode(b'') 303 | return (ret[0], True, True) 304 | # (buffer_to_recv, is_need_decrypt, is_need_to_encode_and_send_back) 305 | 306 | buf = buf[48:] 307 | 308 | host_name = b'' 309 | for index in range(len(buf)): 310 | if index + 4 < len(buf): 311 | if buf[index:index + 4] == b"\x00\x17\x00\x00": 312 | if buf[:index] != '': 313 | host_name = buf[:index] 314 | host_name = host_name.decode('utf-8') 315 | 316 | return (b'', False, True, host_name) 317 | --------------------------------------------------------------------------------