diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a900064 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +*.py[co] + +# Packages +*.egg +*.egg-info +dist +build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +htmlcov +.coverage* +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg + +.DS_Store +.idea \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..014fa07 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: python +python: + - 2.6 + - 2.7 + - 3.3 + - 3.4 +cache: + directories: + - dante-1.4.0 +before_install: + - sudo apt-get update -qq + - sudo apt-get install -qq build-essential dnsutils iproute nginx bc + - sudo dd if=/dev/urandom of=/usr/share/nginx/www/file bs=1M count=10 + - sudo sh -c "echo '127.0.0.1 localhost' > /etc/hosts" + - sudo service nginx restart + - pip install pep8 pyflakes nose coverage PySocks cymysql + - sudo tests/socksify/install.sh + - sudo tests/libsodium/install.sh + - sudo tests/setup_tc.sh +script: + - tests/jenkins.sh diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..0cd9176 --- /dev/null +++ b/CHANGES @@ -0,0 +1,342 @@ +3.4.0 2017-07-27 +- add auth_chain_b +- add initmudbjson.sh +- allow set speed limit in runtime +- fix bugs & mem leak + +3.3.3 2017-06-03 +- add DNS cache +- add tls1.2_ticket_fastauth +- fix bugs + +3.3.2 2017-05-20 +- revert http reply +- refine tls1.2_ticket_auth error detector + +3.3.1 2017-05-18 +- fix stop script +- Async DNS query under UDP +- fix old version of OpenSSL +- http reply + +3.3.0 2017-05-11 +- connect_log include local addr & port +- fix auth_chain_a UDP bug +- add "additional_ports_only" +- add interface legendsockssr +- run with newest python version +- parse comment in hosts +- update mujson_mgr +- add cymysql setup script +- new speed tester +- fix leaks +- bugs fixed + +3.2.0 2017-04-27 +- add auth_chain_a +- remove auth_aes128, auth_sha1, auth_sha1_v2, verify_simple, auth_simple, verify_sha1 + +3.1.2 2017-04-07 +- display UID +- auto adjust TCP MSS + +3.1.1 2017-03-25 +- add "New session ticket" +- ignore bind 10.0.0.0/8 and 192.168.0.0/16 by default +- improve rand size under auth_aes128_* +- fix bugs + +3.1.0 2017-03-16 +- add "glzjinmod" interface +- rate limit +- add additional_ports in config + +3.0.4 2017-01-08 +- multi-user in single port + +3.0.1 2017-01-03 +- remove auth_aes128_*_compatible + +3.0.0 2016-12-23 +- http_simple fix bugs +- tls1.2_ticket_auth fix bug & defaule time diff set to 86400s + +2.9.7 2016-11-22 +- manage client with LRUCache +- catch bind error +- fix import error of resource on windows +- print RLIMIT_NOFILE +- always close cymysql objects +- add init script + +2.9.6 2016-10-17 +- tls1.2_ticket_auth random packet size + +2.9.5.1 2016-10-16 +- UDP bind address + +2.9.5 2016-10-13 +- add auth_aes128_md5 and auth_aes128_sha1 + +2.9.4 2016-10-11 +- sync client version + +2.6.13 2015-11-02 +- add protocol setting + +2.6.12 2015-10-27 +- IPv6 first +- Fix mem leaks +- auth_simple plugin +- remove FORCE_NEW_PROTOCOL +- optimize code + +2.6.11 2015-10-20 +- Obfs plugin +- Obfs parameters +- UDP over TCP +- TCP over UDP (experimental) +- Fix socket leaks +- Catch abnormal UDP package + +2.6.10 2015-06-08 +- Optimize LRU cache +- Refine logging + +2.6.9 2015-05-19 +- Fix a stability issue on Windows + +2.6.8 2015-02-10 +- Support multiple server ip on client side +- Support --version +- Minor fixes + +2.6.7 2015-02-02 +- Support --user +- Support CIDR format in --forbidden-ip +- Minor fixes + +2.6.6 2015-01-23 +- Fix a crash in forbidden list + +2.6.5 2015-01-18 +- Try both 32 bit and 64 bit dll on Windows + +2.6.4 2015-01-14 +- Also search lib* when searching libraries + +2.6.3 2015-01-12 +- Support --forbidden-ip to ban some IP, i.e. localhost +- Search OpenSSL and libsodium harder +- Now works on OpenWRT + +2.6.2 2015-01-03 +- Log client IP + +2.6.1 2014-12-26 +- Fix a problem with TCP Fast Open on local side +- Fix sometimes daemon_start returns wrong exit status + +2.6 2014-12-21 +- Add daemon support + +2.5 2014-12-11 +- Add salsa20 and chacha20 + +2.4.3 2014-11-10 +- Fix an issue on Python 3 +- Fix an issue with IPv6 + +2.4.2 2014-11-06 +- Fix command line arguments on Python 3 +- Support table on Python 3 +- Fix TCP Fast Open on Python 3 + +2.4.1 2014-11-01 +- Fix setup.py for non-utf8 locales on Python 3 + +2.4 2014-11-01 +- Python 3 support +- Performance improvement +- Fix LRU cache behavior + +2.3.2 2014-10-11 +- Fix OpenSSL on Windows + +2.3.1 2014-10-09 +- Does not require M2Crypto any more + +2.3 2014-09-23 +- Support CFB1, CFB8 and CTR mode of AES +- Do not require password config when using port_password +- Use SIGTERM instead of SIGQUIT on Windows + +2.2.2 2014-09-14 +- Fix when multiple DNS set, IPv6 only sites are broken + +2.2.1 2014-09-10 +- Support graceful shutdown +- Fix some bugs + +2.2.0 2014-09-09 +- Add RC4-MD5 encryption + +2.1.0 2014-08-10 +- Use only IPv4 DNS server +- Does not ship config.json +- Better error message + +2.0.12 2014-07-26 +- Support -q quiet mode +- Exit 0 when showing help with -h + +2.0.11 2014-07-12 +- Prefers IP addresses over hostnames, more friendly with socksify and openvpn + +2.0.10 2014-07-11 +- Fix UDP on local + +2.0.9 2014-07-06 +- Fix EWOULDBLOCK on Windows +- Fix Unicode config problem on some platforms + +2.0.8 2014-06-23 +- Use multiple DNS to query hostnames + +2.0.7 2014-06-21 +- Fix fastopen on local +- Fallback when fastopen is not available +- Add verbose logging mode -vv +- Verify if hostname is valid + +2.0.6 2014-06-19 +- Fix CPU 100% on POLL_HUP +- More friendly logging + +2.0.5 2014-06-18 +- Support a simple config format for multiple ports + +2.0.4 2014-06-12 +- Fix worker master + +2.0.3 2014-06-11 +- Fix table encryption with UDP + +2.0.2 2014-06-11 +- Add asynchronous DNS in TCP relay + +2.0.1 2014-06-05 +- Better logging +- Maybe fix bad file descriptor + +2.0 2014-06-05 +- Use a new event model +- Remove gevent +- Refuse to use default password +- Fix a problem when using multiple passwords with table encryption + +1.4.5 2014-05-24 +- Add timeout in TCP server +- Close sockets in master process + +1.4.4 2014-05-17 +- Support multiple workers + +1.4.3 2014-05-13 +- Fix Windows + +1.4.2 2014-05-10 +- Add salsa20-ctr cipher + +1.4.1 2014-05-03 +- Fix error log +- Fix EINPROGESS with some version of gevent + +1.4.0 2014-05-02 +- Adds UDP relay +- TCP fast open support on Linux 3.7+ + +1.3.7 2014-04-10 +- Fix a typo in help + +1.3.6 2014-04-10 +- Fix a typo in help + +1.3.5 2014-04-07 +- Add help +- Change default local binding address into 127.0.0.1 + +1.3.4 2014-02-17 +- Fix a bug when no config file exists +- Client now support multiple server ports and multiple server/port pairs +- Better error message with bad config.json format and wrong password + +1.3.3 2013-07-09 +- Fix default key length of rc2 + +1.3.2 2013-07-04 +- Server will listen at server IP specified in config +- Check config file and show some warning messages + +1.3.1 2013-06-29 +- Fix -c arg + +1.3.0 2013-06-22 +- Move to pypi + +1.2.3 2013-06-14 +- add bind address + +1.2.2 2013-05-31 +- local can listen at ::0 with -6 arg; bump 1.2.2 + +1.2.1 2013-05-23 +- Fix an OpenSSL crash + +1.2 2013-05-22 +- Use random iv, we finally have strong encryption + +1.1.1 2013-05-21 +- Add encryption, AES, blowfish, etc. + +1.1 2013-05-16 +- Support IPv6 addresses (type 4) +- Drop Python 2.5 support + +1.0 2013-04-03 +- Fix -6 IPv6 + +0.9.4 2013-03-04 +- Support Python 2.5 + +0.9.3 2013-01-14 +- Fix conn termination null data + +0.9.2 2013-01-05 +- Change default timeout + +0.9.1 2013-01-05 +- Add Travis-CI test + +0.9 2012-12-30 +- Replace send with sendall, fix FreeBSD + +0.6 2012-12-06 +- Support args + +0.5 2012-11-08 +- Fix encryption with negative md5sum + +0.4 2012-11-02 +- Move config into a JSON file +- Auto-detect config path + +0.3 2012-06-06 +- Move socks5 negotiation to local + +0.2 2012-05-11 +- Add -6 arg for IPv6 +- Fix socket.error + +0.1 2012-04-20 +- Initial version diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fbdb9c1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +How to Contribute +================= + +Pull Requests +------------- + +1. Pull requests are welcome. If you would like to add a large feature +or make a significant change, make sure to open an issue to discuss with +people first. +2. Follow PEP8. +3. Make sure to pass the unit tests. Write unit tests for new modules if +needed. + +Issues +------ + +1. Only bugs and feature requests are accepted here. +2. We'll only work on important features. If the feature you're asking only +benefits a few people, you'd better implement the feature yourself and send us +a pull request, or ask some of your friends to do so. +3. We don't answer questions of any other types here. Since very few people +are watching the issue tracker here, you'll probably get no help from here. +Read [Troubleshooting] and get help from forums or [mailing lists]. +4. Issues in languages other than English will be Google translated into English +later. + + +[Troubleshooting]: https://github.com/clowwindy/shadowsocks/wiki/Troubleshooting +[mailing lists]: https://groups.google.com/forum/#!forum/shadowsocks diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ec7b2c6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM alpine:3.6 + +ENV SERVER_ADDR 0.0.0.0 +ENV SERVER_PORT 51348 +ENV PASSWORD psw +ENV METHOD aes-128-ctr +ENV PROTOCOL auth_aes128_md5 +ENV PROTOCOLPARAM 32 +ENV OBFS tls1.2_ticket_auth_compatible +ENV TIMEOUT 300 +ENV DNS_ADDR 8.8.8.8 +ENV DNS_ADDR_2 8.8.4.4 + +ARG BRANCH=manyuser +ARG WORK=~ + + +RUN apk --no-cache add python \ + libsodium \ + wget + + +RUN mkdir -p $WORK && \ + wget -qO- --no-check-certificate https://github.com/shadowsocksr/shadowsocksr/archive/$BRANCH.tar.gz | tar -xzf - -C $WORK + + +WORKDIR $WORK/shadowsocksr-$BRANCH/shadowsocks + + +EXPOSE $SERVER_PORT +CMD python server.py -p $SERVER_PORT -k $PASSWORD -m $METHOD -O $PROTOCOL -o $OBFS -G $PROTOCOLPARAM diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1882dd7 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include shadowsocks *.py +include README.rst +include LICENSE diff --git a/README.md b/README.md new file mode 100644 index 0000000..7594389 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +SSRR 3.2.2 + +兼容SSRPanel的自改版SSR(R)后端,可兼容原版SS、SSR + +#### 1.安装 + + git clone https://github.com/ssrpanel/shadowsocksr.git + +或者通过wget下载 + + wget https://github.com/ssrpanel/shadowsocksr/archive/master.zip && unzip master && mv shadowsocksr-master shadowsocksr + + + +#### 2.安装cymysql + + sh setup_cymysql2.sh + + +#### 3.编辑节点配置(混淆、协议、限速、IPV6) + + vi user-mysql.json + + protocol 协议,带 _compatible 结尾兼容 原版,直接用原版可以改为 origin + protocol_param 协议参数,配置了的话,客户端也要一致 + obfs 混淆 tls1.2_ticket_auth 可以限制客户端数量 tls1.2_ticket_auth_compatible 兼容原版,直接用原版可以改为 plain + obfs_param 混淆参数,当obfs为 tls1.2_ticket_auth 的时候,这个值为 1 到 256 之间,表示限制客户端数量 + additional_ports 单端口配置,请看wiki + additional_ports_only 强制单端口,改为true则所有非设置的单端口都无法连接,只能用additional_ports设置的那些端口连接 + dns_ipv6 为true时,会优先走ipv6,需要节点服务器至少有一个2开头的ipv6地址(有时候会导致IPV4失效,不推荐开启,你可以自己试试) + connect_verbose_info 为1时记录用户访问网址,推荐打开,可以清楚知道连接成功与否 + redirect 请求失败时返回信息伪造成访问配置里网址 + + +#### 4.编辑数据库连接信息 + + vi usermysql.json + + host 数据库地址,如果是本机就是127.0.0.1 + port 数据库连接端口 + user 数据库连接用户,不推荐使用root + password 数据库连接密码 + db 面板所在数据库 + node_id 节点ID,对应面板里的 节点列表 最左侧的id(请先将面板搭建好,然后创建一个节点,就有节点ID了) + transfer_mul 节点流量计算比例,默认1.0,填1也可以,1表示:用了100M算100M,10表示用了100M算1000M,0.1表示用了100M算10M。 + +#### 5.运行、关闭、看日志 + + sh logrun.sh + sh stop.sh + sh tail.sh + + diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..bf2a3ec --- /dev/null +++ b/README.rst @@ -0,0 +1,113 @@ +shadowsocks +=========== + +|PyPI version| |Build Status| |Coverage Status| + +A fast tunnel proxy that helps you bypass firewalls. + +Server +------ + +Install +~~~~~~~ + +Debian / Ubuntu: + +:: + + apt-get install python-pip + pip install shadowsocks + +CentOS: + +:: + + yum install python-setuptools && easy_install pip + pip install shadowsocks + +Windows: + +See `Install Server on +Windows `__ + +Usage +~~~~~ + +:: + + ssserver -p 443 -k password -m rc4-md5 + +To run in the background: + +:: + + sudo ssserver -p 443 -k password -m rc4-md5 --user nobody -d start + +To stop: + +:: + + sudo ssserver -d stop + +To check the log: + +:: + + sudo less /var/log/shadowsocks.log + +Check all the options via ``-h``. You can also use a +`Configuration `__ +file instead. + +Client +------ + +- `Windows `__ + / `OS + X `__ +- `Android `__ + / `iOS `__ +- `OpenWRT `__ + +Use GUI clients on your local PC/phones. Check the README of your client +for more information. + +Documentation +------------- + +You can find all the documentation in the +`Wiki `__. + +License +------- + +Copyright 2015 clowwindy + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + +:: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Bugs and Issues +--------------- + +- `Troubleshooting `__ +- `Issue + Tracker `__ +- `Mailing list `__ + +.. |PyPI version| image:: https://img.shields.io/pypi/v/shadowsocks.svg?style=flat + :target: https://pypi.python.org/pypi/shadowsocks +.. |Build Status| image:: https://img.shields.io/travis/shadowsocks/shadowsocks/master.svg?style=flat + :target: https://travis-ci.org/shadowsocks/shadowsocks +.. |Coverage Status| image:: https://jenkins.shadowvpn.org/result/shadowsocks + :target: https://jenkins.shadowvpn.org/job/Shadowsocks/ws/PYENV/py34/label/linux/htmlcov/index.html diff --git a/apiconfig.py b/apiconfig.py new file mode 100644 index 0000000..58ad032 --- /dev/null +++ b/apiconfig.py @@ -0,0 +1,15 @@ +# Config +API_INTERFACE = 'ssrpanel' #mudbjson, sspanelv2, sspanelv3, sspanelv3ssr, glzjinmod, legendsockssr, muapiv2(not support), ssrpanel +UPDATE_TIME = 60 +SERVER_PUB_ADDR = '127.0.0.1' # mujson_mgr need this to generate ssr link + +#mudb +MUDB_FILE = 'mudb.json' + +# Mysql +MYSQL_CONFIG = 'usermysql.json' + +# API +MUAPI_CONFIG = 'usermuapi.json' + + diff --git a/asyncmgr.py b/asyncmgr.py new file mode 100644 index 0000000..9bf4d09 --- /dev/null +++ b/asyncmgr.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import time +import os +import socket +import struct +import re +import logging +from shadowsocks import common +from shadowsocks import lru_cache +from shadowsocks import eventloop +import server_pool +import Config + +class ServerMgr(object): + + def __init__(self): + self._loop = None + self._request_id = 1 + self._hosts = {} + self._hostname_status = {} + self._hostname_to_cb = {} + self._cb_to_hostname = {} + self._last_time = time.time() + self._sock = None + self._servers = None + + def add_to_loop(self, loop): + if self._loop: + raise Exception('already add to loop') + self._loop = loop + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.bind((Config.MANAGE_BIND_IP, Config.MANAGE_PORT)) + self._sock.setblocking(False) + loop.add(self._sock, eventloop.POLL_IN, self) + + def _handle_data(self, sock): + data, addr = sock.recvfrom(128) + #manage pwd:port:passwd:action + args = data.split(':') + if len(args) < 4: + return + if args[0] == Config.MANAGE_PASS: + if args[3] == '0': + server_pool.ServerPool.get_instance().cb_del_server(args[1]) + elif args[3] == '1': + server_pool.ServerPool.get_instance().new_server(args[1], args[2]) + + def handle_event(self, sock, fd, event): + if sock != self._sock: + return + if event & eventloop.POLL_ERR: + logging.error('mgr socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN, self) + else: + self._handle_data(sock) + + def close(self): + if self._sock: + if self._loop: + self._loop.remove(self._sock) + self._sock.close() + self._sock = None + + +def test(): + pass + +if __name__ == '__main__': + test() diff --git a/config.json b/config.json new file mode 100644 index 0000000..9f12d2c --- /dev/null +++ b/config.json @@ -0,0 +1,25 @@ +{ + "server": "0.0.0.0", + "server_ipv6": "::", + "server_port": 8388, + "local_address": "127.0.0.1", + "local_port": 1080, + + "password": "m", + "method": "aes-192-ctr", + "protocol": "auth_sha1_v4_compatible", + "protocol_param": "", + "obfs": "tls1.2_ticket_auth_compatible", + "obfs_param": "", + "speed_limit_per_con": 0, + "speed_limit_per_user": 0, + + "additional_ports" : {}, // only works under multi-user mode + "additional_ports_only" : false, // only works under multi-user mode + "timeout": 120, + "udp_timeout": 60, + "dns_ipv6": false, + "connect_verbose_info": 1, + "redirect": ["www.amazon.com", "images-na.ssl-images-amazon.com", "m.media-amazon.com", "kdp.amazon.com", "php.net"], + "fast_open": false +} diff --git a/configloader.py b/configloader.py new file mode 100644 index 0000000..1794641 --- /dev/null +++ b/configloader.py @@ -0,0 +1,15 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +import importloader + +g_config = None + +def load_config(): + global g_config + g_config = importloader.loads(['userapiconfig', 'apiconfig']) + +def get_config(): + return g_config + +load_config() + diff --git a/db_transfer.py b/db_transfer.py new file mode 100644 index 0000000..9c1b721 --- /dev/null +++ b/db_transfer.py @@ -0,0 +1,652 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +import logging +import time +import sys +from server_pool import ServerPool +import traceback +from shadowsocks import common, shell, lru_cache, obfs +from configloader import load_config, get_config +import importloader +import copy + +switchrule = None +db_instance = None + +class TransferBase(object): + def __init__(self): + import threading + self.event = threading.Event() + self.key_list = ['port', 'u', 'd', 'transfer_enable', 'passwd', 'enable'] + self.last_get_transfer = {} #上一次的实际流量 + self.last_update_transfer = {} #上一次更新到的流量(小于等于实际流量) + self.force_update_transfer = set() #强制推入数据库的ID + self.port_uid_table = {} #端口到uid的映射(仅v3以上有用) + self.onlineuser_cache = lru_cache.LRUCache(timeout=60*30) #用户在线状态记录 + self.pull_ok = False #记录是否已经拉出过数据 + self.mu_ports = {} + + def load_cfg(self): + pass + + def push_db_all_user(self): + if self.pull_ok is False: + return + #更新用户流量到数据库 + last_transfer = self.last_update_transfer + curr_transfer = ServerPool.get_instance().get_servers_transfer() + #上次和本次的增量 + dt_transfer = {} + for id in self.force_update_transfer: #此表中的用户统计上次未计入的流量 + if id in self.last_get_transfer and id in last_transfer: + dt_transfer[id] = [self.last_get_transfer[id][0] - last_transfer[id][0], self.last_get_transfer[id][1] - last_transfer[id][1]] + + for id in curr_transfer.keys(): + if id in self.force_update_transfer or id in self.mu_ports: + continue + #算出与上次记录的流量差值,保存于dt_transfer表 + if id in last_transfer: + if curr_transfer[id][0] + curr_transfer[id][1] - last_transfer[id][0] - last_transfer[id][1] <= 0: + continue + dt_transfer[id] = [curr_transfer[id][0] - last_transfer[id][0], + curr_transfer[id][1] - last_transfer[id][1]] + else: + if curr_transfer[id][0] + curr_transfer[id][1] <= 0: + continue + dt_transfer[id] = [curr_transfer[id][0], curr_transfer[id][1]] + + #有流量的,先记录在线状态 + if id in self.last_get_transfer: + if curr_transfer[id][0] + curr_transfer[id][1] > self.last_get_transfer[id][0] + self.last_get_transfer[id][1]: + self.onlineuser_cache[id] = curr_transfer[id][0] + curr_transfer[id][1] + else: + self.onlineuser_cache[id] = curr_transfer[id][0] + curr_transfer[id][1] + + self.onlineuser_cache.sweep() + + update_transfer = self.update_all_user(dt_transfer) #返回有更新的表 + for id in update_transfer.keys(): #其增量加在此表 + if id not in self.force_update_transfer: #但排除在force_update_transfer内的 + last = self.last_update_transfer.get(id, [0,0]) + self.last_update_transfer[id] = [last[0] + update_transfer[id][0], last[1] + update_transfer[id][1]] + self.last_get_transfer = curr_transfer + for id in self.force_update_transfer: + if id in self.last_update_transfer: + del self.last_update_transfer[id] + if id in self.last_get_transfer: + del self.last_get_transfer[id] + self.force_update_transfer = set() + + def del_server_out_of_bound_safe(self, last_rows, rows): + #停止超流量的服务 + #启动没超流量的服务 + keymap = {} + try: + switchrule = importloader.load('switchrule') + keymap = switchrule.getRowMap() + except Exception as e: + logging.error('load switchrule.py fail') + cur_servers = {} + new_servers = {} + allow_users = {} + mu_servers = {} + config = shell.get_config(False) + for row in rows: + try: + allow = switchrule.isTurnOn(row) and row['enable'] == 1 and row['u'] + row['d'] < row['transfer_enable'] + except Exception as e: + allow = False + + port = row['port'] + passwd = common.to_bytes(row['passwd']) + if hasattr(passwd, 'encode'): + passwd = passwd.encode('utf-8') + cfg = {'password': passwd} + if 'id' in row: + self.port_uid_table[row['port']] = row['id'] + + read_config_keys = ['method', 'obfs', 'obfs_param', 'protocol', 'protocol_param', 'forbidden_ip', 'forbidden_port', 'speed_limit_per_con', 'speed_limit_per_user'] + for name in read_config_keys: + if name in row and row[name]: + if name in keymap: + cfg[keymap[name]] = row[name] + else: + cfg[name] = row[name] + + merge_config_keys = ['password'] + read_config_keys + for name in cfg.keys(): + if hasattr(cfg[name], 'encode'): + try: + cfg[name] = cfg[name].encode('utf-8') + except Exception as e: + logging.warning('encode cfg key "%s" fail, val "%s"' % (name, cfg[name])) + + if port not in cur_servers: + cur_servers[port] = passwd + else: + logging.error('more than one user use the same port [%s]' % (port,)) + continue + + if 'protocol' in cfg and 'protocol_param' in cfg and common.to_str(cfg['protocol']) in obfs.mu_protocol(): + if '#' in common.to_str(cfg['protocol_param']): + mu_servers[port] = passwd + allow = True + + if allow: + if port not in mu_servers: + allow_users[port] = cfg + + cfgchange = False + if port in ServerPool.get_instance().tcp_servers_pool: + relay = ServerPool.get_instance().tcp_servers_pool[port] + for name in merge_config_keys: + if name in cfg and not self.cmp(cfg[name], relay._config[name]): + cfgchange = True + break + if not cfgchange and port in ServerPool.get_instance().tcp_ipv6_servers_pool: + relay = ServerPool.get_instance().tcp_ipv6_servers_pool[port] + for name in merge_config_keys: + if (name in cfg) and ((name not in relay._config) or not self.cmp(cfg[name], relay._config[name])): + cfgchange = True + break + + if port in mu_servers: + if ServerPool.get_instance().server_is_run(port) > 0: + if cfgchange: + logging.info('db stop server at port [%s] reason: config changed: %s' % (port, cfg)) + ServerPool.get_instance().cb_del_server(port) + self.force_update_transfer.add(port) + new_servers[port] = (passwd, cfg) + else: + self.new_server(port, passwd, cfg) + else: + if ServerPool.get_instance().server_is_run(port) > 0: + if config['additional_ports_only'] or not allow: + logging.info('db stop server at port [%s]' % (port,)) + ServerPool.get_instance().cb_del_server(port) + self.force_update_transfer.add(port) + else: + if cfgchange: + logging.info('db stop server at port [%s] reason: config changed: %s' % (port, cfg)) + ServerPool.get_instance().cb_del_server(port) + self.force_update_transfer.add(port) + new_servers[port] = (passwd, cfg) + + elif not config['additional_ports_only'] and allow and port > 0 and port < 65536 and ServerPool.get_instance().server_run_status(port) is False: + self.new_server(port, passwd, cfg) + + for row in last_rows: + if row['port'] in cur_servers: + pass + else: + logging.info('db stop server at port [%s] reason: port not exist' % (row['port'])) + ServerPool.get_instance().cb_del_server(row['port']) + self.clear_cache(row['port']) + if row['port'] in self.port_uid_table: + del self.port_uid_table[row['port']] + + if len(new_servers) > 0: + from shadowsocks import eventloop + self.event.wait(eventloop.TIMEOUT_PRECISION + eventloop.TIMEOUT_PRECISION / 2) + for port in new_servers.keys(): + passwd, cfg = new_servers[port] + self.new_server(port, passwd, cfg) + + logging.debug('db allow users %s \nmu_servers %s' % (allow_users, mu_servers)) + for port in mu_servers: + ServerPool.get_instance().update_mu_users(port, allow_users) + + self.mu_ports = mu_servers + + def clear_cache(self, port): + if port in self.force_update_transfer: del self.force_update_transfer[port] + if port in self.last_get_transfer: del self.last_get_transfer[port] + if port in self.last_update_transfer: del self.last_update_transfer[port] + + def new_server(self, port, passwd, cfg): + protocol = cfg.get('protocol', ServerPool.get_instance().config.get('protocol', 'origin')) + method = cfg.get('method', ServerPool.get_instance().config.get('method', 'None')) + obfs = cfg.get('obfs', ServerPool.get_instance().config.get('obfs', 'plain')) + logging.info('db start server at port [%s] pass [%s] protocol [%s] method [%s] obfs [%s]' % (port, passwd, protocol, method, obfs)) + ServerPool.get_instance().new_server(port, cfg) + + def cmp(self, val1, val2): + if type(val1) is bytes: + val1 = common.to_str(val1) + if type(val2) is bytes: + val2 = common.to_str(val2) + return val1 == val2 + + @staticmethod + def del_servers(): + for port in [v for v in ServerPool.get_instance().tcp_servers_pool.keys()]: + if ServerPool.get_instance().server_is_run(port) > 0: + ServerPool.get_instance().cb_del_server(port) + for port in [v for v in ServerPool.get_instance().tcp_ipv6_servers_pool.keys()]: + if ServerPool.get_instance().server_is_run(port) > 0: + ServerPool.get_instance().cb_del_server(port) + + @staticmethod + def thread_db(obj): + import socket + import time + global db_instance + timeout = 60 + socket.setdefaulttimeout(timeout) + last_rows = [] + db_instance = obj() + ServerPool.get_instance() + shell.log_shadowsocks_version() + + try: + import resource + logging.info('current process RLIMIT_NOFILE resource: soft %d hard %d' % resource.getrlimit(resource.RLIMIT_NOFILE)) + except: + pass + + try: + while True: + load_config() + db_instance.load_cfg() + try: + db_instance.push_db_all_user() + rows = db_instance.pull_db_all_user() + if rows: + db_instance.pull_ok = True + config = shell.get_config(False) + for port in config['additional_ports']: + val = config['additional_ports'][port] + val['port'] = int(port) + val['enable'] = 1 + val['transfer_enable'] = 1024 ** 7 + val['u'] = 0 + val['d'] = 0 + if "password" in val: + val["passwd"] = val["password"] + rows.append(val) + db_instance.del_server_out_of_bound_safe(last_rows, rows) + last_rows = rows + except Exception as e: + trace = traceback.format_exc() + logging.error(trace) + #logging.warn('db thread except:%s' % e) + if db_instance.event.wait(get_config().UPDATE_TIME) or not ServerPool.get_instance().thread.is_alive(): + break + except KeyboardInterrupt as e: + pass + db_instance.del_servers() + ServerPool.get_instance().stop() + db_instance = None + + @staticmethod + def thread_db_stop(): + global db_instance + db_instance.event.set() + +class DbTransfer(TransferBase): + def __init__(self): + super(DbTransfer, self).__init__() + self.user_pass = {} #记录更新此用户流量时被跳过多少次 + self.cfg = { + "host": "127.0.0.1", + "port": 3306, + "user": "ss", + "password": "pass", + "db": "shadowsocks", + "node_id": 0, + "transfer_mul": 1.0, + "ssl_enable": 0, + "ssl_ca": "", + "ssl_cert": "", + "ssl_key": ""} + self.load_cfg() + + def load_cfg(self): + import json + config_path = get_config().MYSQL_CONFIG + cfg = None + with open(config_path, 'rb+') as f: + cfg = json.loads(f.read().decode('utf8')) + + if cfg: + self.cfg.update(cfg) + + def update_all_user(self, dt_transfer): + import cymysql + update_transfer = {} + + query_head = 'UPDATE user' + query_sub_when = '' + query_sub_when2 = '' + query_sub_in = None + last_time = time.time() + + for id in dt_transfer.keys(): + transfer = dt_transfer[id] + #小于最低更新流量的先不更新 + update_trs = 1024 * (2048 - self.user_pass.get(id, 0) * 64) + if transfer[0] + transfer[1] < update_trs and id not in self.force_update_transfer: + self.user_pass[id] = self.user_pass.get(id, 0) + 1 + continue + if id in self.user_pass: + del self.user_pass[id] + + query_sub_when += ' WHEN %s THEN u+%s' % (id, int(transfer[0] * self.cfg["transfer_mul"])) + query_sub_when2 += ' WHEN %s THEN d+%s' % (id, int(transfer[1] * self.cfg["transfer_mul"])) + update_transfer[id] = transfer + + if query_sub_in is not None: + query_sub_in += ',%s' % id + else: + query_sub_in = '%s' % id + + if query_sub_when == '': + return update_transfer + query_sql = query_head + ' SET u = CASE port' + query_sub_when + \ + ' END, d = CASE port' + query_sub_when2 + \ + ' END, t = ' + str(int(last_time)) + \ + ' WHERE port IN (%s)' % query_sub_in + if self.cfg["ssl_enable"] == 1: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8', + ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]}) + else: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8') + + try: + cur = conn.cursor() + try: + cur.execute(query_sql) + except Exception as e: + logging.error(e) + update_transfer = {} + + cur.close() + conn.commit() + except Exception as e: + logging.error(e) + update_transfer = {} + finally: + conn.close() + + return update_transfer + + def pull_db_all_user(self): + import cymysql + #数据库所有用户信息 + if self.cfg["ssl_enable"] == 1: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8', + ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]}) + else: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8') + + try: + rows = self.pull_db_users(conn) + finally: + conn.close() + + if not rows: + logging.warn('no user in db') + return rows + + def pull_db_users(self, conn): + keys = copy.copy(self.key_list) + try: + switchrule = importloader.load('switchrule') + keymap = switchrule.getRowMap() + for key in keymap: + if keymap[key] in keys: + keys.remove(keymap[key]) + keys.append(key) + keys = switchrule.getKeys(keys) + except Exception as e: + logging.error('load switchrule.py fail') + + cur = conn.cursor() + cur.execute("SELECT " + ','.join(keys) + " FROM user WHERE port > 0") + rows = [] + for r in cur.fetchall(): + d = {} + for column in range(len(keys)): + d[keys[column]] = r[column] + rows.append(d) + cur.close() + return rows + +class Dbv3Transfer(DbTransfer): + def __init__(self): + super(Dbv3Transfer, self).__init__() + self.update_node_state = True if get_config().API_INTERFACE != 'legendsockssr' else False + if self.update_node_state: + self.key_list += ['id'] + self.key_list += ['method'] + if self.update_node_state: + self.ss_node_info_name = 'ss_node_info_log' + if get_config().API_INTERFACE == 'sspanelv3ssr': + self.key_list += ['obfs', 'protocol'] + if get_config().API_INTERFACE == 'glzjinmod': + self.key_list += ['obfs', 'protocol'] + self.ss_node_info_name = 'ss_node_info' + if get_config().API_INTERFACE == 'ssrpanel': + self.key_list += ['obfs', 'protocol'] + self.ss_node_info_name = 'ss_node_info' + else: + self.key_list += ['obfs', 'protocol'] + self.start_time = time.time() + + def update_all_user(self, dt_transfer): + import cymysql + update_transfer = {} + + query_head = 'UPDATE user' + query_sub_when = '' + query_sub_when2 = '' + query_sub_in = None + last_time = time.time() + + alive_user_count = len(self.onlineuser_cache) + bandwidth_thistime = 0 + + if self.cfg["ssl_enable"] == 1: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8', + ssl={'ca':self.cfg["ssl_ca"],'cert':self.cfg["ssl_cert"],'key':self.cfg["ssl_key"]}) + else: + conn = cymysql.connect(host=self.cfg["host"], port=self.cfg["port"], + user=self.cfg["user"], passwd=self.cfg["password"], + db=self.cfg["db"], charset='utf8') + conn.autocommit(True) + + for id in dt_transfer.keys(): + transfer = dt_transfer[id] + bandwidth_thistime = bandwidth_thistime + transfer[0] + transfer[1] + + update_trs = 1024 * (2048 - self.user_pass.get(id, 0) * 64) + if transfer[0] + transfer[1] < update_trs: + self.user_pass[id] = self.user_pass.get(id, 0) + 1 + continue + if id in self.user_pass: + del self.user_pass[id] + + query_sub_when += ' WHEN %s THEN u+%s' % (id, int(transfer[0] * self.cfg["transfer_mul"])) + query_sub_when2 += ' WHEN %s THEN d+%s' % (id, int(transfer[1] * self.cfg["transfer_mul"])) + update_transfer[id] = transfer + + if self.update_node_state: + cur = conn.cursor() + try: + if id in self.port_uid_table: + cur.execute("INSERT INTO `user_traffic_log` (`id`, `user_id`, `u`, `d`, `node_id`, `rate`, `traffic`, `log_time`) VALUES (NULL, '" + \ + str(self.port_uid_table[id]) + "', '" + str(transfer[0]) + "', '" + str(transfer[1]) + "', '" + \ + str(self.cfg["node_id"]) + "', '" + str(self.cfg["transfer_mul"]) + "', '" + \ + self.traffic_format((transfer[0] + transfer[1]) * self.cfg["transfer_mul"]) + "', unix_timestamp()); ") + except: + logging.warn('no `user_traffic_log` in db') + cur.close() + + if query_sub_in is not None: + query_sub_in += ',%s' % id + else: + query_sub_in = '%s' % id + + if query_sub_when != '': + query_sql = query_head + ' SET u = CASE port' + query_sub_when + \ + ' END, d = CASE port' + query_sub_when2 + \ + ' END, t = ' + str(int(last_time)) + \ + ' WHERE port IN (%s)' % query_sub_in + cur = conn.cursor() + try: + cur.execute(query_sql) + except Exception as e: + logging.error(e) + cur.close() + + if self.update_node_state: + try: + cur = conn.cursor() + try: + cur.execute("INSERT INTO `ss_node_online_log` (`id`, `node_id`, `online_user`, `log_time`) VALUES (NULL, '" + \ + str(self.cfg["node_id"]) + "', '" + str(alive_user_count) + "', unix_timestamp()); ") + except Exception as e: + logging.error(e) + cur.close() + + cur = conn.cursor() + try: + cur.execute("INSERT INTO `" + self.ss_node_info_name + "` (`id`, `node_id`, `uptime`, `load`, `log_time`) VALUES (NULL, '" + \ + str(self.cfg["node_id"]) + "', '" + str(self.uptime()) + "', '" + \ + str(self.load()) + "', unix_timestamp()); ") + except Exception as e: + logging.error(e) + cur.close() + except: + logging.warn('no `ss_node_online_log` or `" + self.ss_node_info_name + "` in db') + + conn.close() + return update_transfer + + def pull_db_users(self, conn): + keys = copy.copy(self.key_list) + try: + switchrule = importloader.load('switchrule') + keymap = switchrule.getRowMap() + for key in keymap: + if keymap[key] in keys: + keys.remove(keymap[key]) + keys.append(key) + keys = switchrule.getKeys(keys) + except Exception as e: + logging.error('load switchrule.py fail') + + cur = conn.cursor() + + if self.update_node_state: + node_info_keys = ['traffic_rate'] + try: + cur.execute("SELECT " + ','.join(node_info_keys) +" FROM ss_node where `id`='" + str(self.cfg["node_id"]) + "'") + nodeinfo = cur.fetchone() + except Exception as e: + logging.error(e) + nodeinfo = None + + if nodeinfo == None: + rows = [] + cur.close() + conn.commit() + logging.warn('None result when select node info from ss_node in db, maybe you set the incorrect node id') + return rows + cur.close() + + node_info_dict = {} + for column in range(len(nodeinfo)): + node_info_dict[node_info_keys[column]] = nodeinfo[column] + self.cfg['transfer_mul'] = float(node_info_dict['traffic_rate']) + + cur = conn.cursor() + try: + rows = [] + cur.execute("SELECT " + ','.join(keys) + " FROM user WHERE port > 0") + for r in cur.fetchall(): + d = {} + for column in range(len(keys)): + d[keys[column]] = r[column] + rows.append(d) + except Exception as e: + logging.error(e) + cur.close() + return rows + + def load(self): + import os + return os.popen("cat /proc/loadavg | awk '{ print $1\" \"$2\" \"$3 }'").readlines()[0] + + def uptime(self): + return time.time() - self.start_time + + def traffic_format(self, traffic): + if traffic < 1024 * 8: + return str(int(traffic)) + "B"; + + if traffic < 1024 * 1024 * 2: + return str(round((traffic / 1024.0), 2)) + "KB"; + + return str(round((traffic / 1048576.0), 2)) + "MB"; + +class MuJsonTransfer(TransferBase): + def __init__(self): + super(MuJsonTransfer, self).__init__() + + def update_all_user(self, dt_transfer): + import json + rows = None + + config_path = get_config().MUDB_FILE + with open(config_path, 'rb+') as f: + rows = json.loads(f.read().decode('utf8')) + for row in rows: + if "port" in row: + port = row["port"] + if port in dt_transfer: + row["u"] += dt_transfer[port][0] + row["d"] += dt_transfer[port][1] + + if rows: + output = json.dumps(rows, sort_keys=True, indent=4, separators=(',', ': ')) + with open(config_path, 'r+') as f: + f.write(output) + f.truncate() + + return dt_transfer + + def pull_db_all_user(self): + import json + rows = None + + config_path = get_config().MUDB_FILE + with open(config_path, 'rb+') as f: + rows = json.loads(f.read().decode('utf8')) + for row in rows: + try: + if 'forbidden_ip' in row: + row['forbidden_ip'] = common.IPNetwork(row['forbidden_ip']) + except Exception as e: + logging.error(e) + try: + if 'forbidden_port' in row: + row['forbidden_port'] = common.PortRange(row['forbidden_port']) + except Exception as e: + logging.error(e) + + if not rows: + logging.warn('no user in json file') + return rows + diff --git a/importloader.py b/importloader.py new file mode 100644 index 0000000..cedc526 --- /dev/null +++ b/importloader.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +def load(name): + try: + obj = __import__(name) + reload(obj) + return obj + except: + pass + + try: + import importlib + obj = importlib.__import__(name) + importlib.reload(obj) + return obj + except: + pass + +def loads(namelist): + for name in namelist: + obj = load(name) + if obj is not None: + return obj diff --git a/initcfg.bat b/initcfg.bat new file mode 100644 index 0000000..6d0bce6 --- /dev/null +++ b/initcfg.bat @@ -0,0 +1,4 @@ +@echo off +If Not Exist "userapiconfig.py" Copy "apiconfig.py" "userapiconfig.py" +If Not Exist "user-config.json" Copy "config.json" "user-config.json" +If Not Exist "usermysql.json" Copy "mysql.json" "usermysql.json" diff --git a/initcfg.sh b/initcfg.sh new file mode 100644 index 0000000..862d1ab --- /dev/null +++ b/initcfg.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +chmod +x *.sh +chmod +x shadowsocks/*.sh +cp -n apiconfig.py userapiconfig.py +cp -n config.json user-config.json +cp -n mysql.json usermysql.json + diff --git a/initmudbjson.sh b/initmudbjson.sh new file mode 100644 index 0000000..09b07f3 --- /dev/null +++ b/initmudbjson.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +bash initcfg.sh +sed -i "s/API_INTERFACE = .\+\?\#/API_INTERFACE = \'mudbjson\' \#/g" userapiconfig.py +ip_addr=`ifconfig -a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'|tr -d "addr:"` +ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c` + +if [[ $ip_count == 1 ]]; then + ip_addr=`ip a|grep inet|grep -v inet6|grep -v "127.0.0."|grep -v -e "192\.168\..[0-9]\+\.[0-9]\+"|grep -v -e "10\.[0-9]\+\.[0-9]\+\.[0-9]\+"|awk '{print $2}'` + ip_addr=${ip_addr%/*} + ip_count=`echo $ip_addr|grep -e "^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$" -c` +fi +if [[ $ip_count == 1 ]]; then + echo "server IP is "${ip_addr} + sed -i "s/SERVER_PUB_ADDR = .\+/SERVER_PUB_ADDR = \'"${ip_addr}"\'/g" userapiconfig.py + user_count=`python mujson_mgr.py -l|grep -c -e "[0-9]"` + if [[ $user_count == 0 ]]; then + port=`python -c 'import random;print(random.randint(10000, 65536))'` + python mujson_mgr.py -a -p ${port} + fi +else + echo "unable to detect server IP" +fi + diff --git a/logrun.sh b/logrun.sh new file mode 100644 index 0000000..94153fe --- /dev/null +++ b/logrun.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd `dirname $0` +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') +ulimit -n 512000 +nohup ${python_ver} server.py m>> ssserver.log 2>&1 & + diff --git a/mudb.json b/mudb.json new file mode 100644 index 0000000..0d4f101 --- /dev/null +++ b/mudb.json @@ -0,0 +1,2 @@ +[ +] diff --git a/mujson_mgr.py b/mujson_mgr.py new file mode 100644 index 0000000..2eb05d5 --- /dev/null +++ b/mujson_mgr.py @@ -0,0 +1,358 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import traceback +from shadowsocks import shell, common +from configloader import load_config, get_config +import random +import getopt +import sys +import json +import base64 + + +class MuJsonLoader(object): + def __init__(self): + self.json = None + + def load(self, path): + l = "[]" + try: + with open(path, 'rb+') as f: + l = f.read().decode('utf8') + except: + pass + self.json = json.loads(l) + + def save(self, path): + if self.json is not None: + output = json.dumps(self.json, sort_keys=True, indent=4, separators=(',', ': ')) + with open(path, 'a'): + pass + with open(path, 'rb+') as f: + f.write(output.encode('utf8')) + f.truncate() + + +class MuMgr(object): + def __init__(self): + self.config_path = get_config().MUDB_FILE + try: + self.server_addr = get_config().SERVER_PUB_ADDR + except: + self.server_addr = '127.0.0.1' + self.data = MuJsonLoader() + + if self.server_addr == '127.0.0.1': + self.server_addr = self.getipaddr() + + def getipaddr(self, ifname='eth0'): + import socket + import struct + ret = '127.0.0.1' + try: + ret = socket.gethostbyname(socket.getfqdn(socket.gethostname())) + except: + pass + if ret == '127.0.0.1': + try: + import fcntl + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ret = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24]) + except: + pass + return ret + + def ssrlink(self, user, encode, muid): + protocol = user.get('protocol', '') + obfs = user.get('obfs', '') + protocol = protocol.replace("_compatible", "") + obfs = obfs.replace("_compatible", "") + protocol_param = '' + if muid is not None: + protocol_param_ = user.get('protocol_param', '') + param = protocol_param_.split('#') + if len(param) == 2: + for row in self.data.json: + if int(row['port']) == muid: + param = str(muid) + ':' + row['passwd'] + protocol_param = '/?protoparam=' + common.to_str(base64.urlsafe_b64encode(common.to_bytes(param))).replace("=", "") + break + link = ("%s:%s:%s:%s:%s:%s" % (self.server_addr, user['port'], protocol, user['method'], obfs, common.to_str(base64.urlsafe_b64encode(common.to_bytes(user['passwd']))).replace("=", ""))) + protocol_param + return "ssr://" + (encode and common.to_str(base64.urlsafe_b64encode(common.to_bytes(link))).replace("=", "") or link) + + def userinfo(self, user, muid = None): + ret = "" + key_list = ['user', 'port', 'method', 'passwd', 'protocol', 'protocol_param', 'obfs', 'obfs_param', 'transfer_enable', 'u', 'd'] + for key in sorted(user): + if key not in key_list: + key_list.append(key) + for key in key_list: + if key in ['enable'] or key not in user: + continue + ret += '\n' + if (muid is not None) and (key in ['protocol_param']): + for row in self.data.json: + if int(row['port']) == muid: + ret += " %s : %s" % (key, str(muid) + ':' + row['passwd']) + break + elif key in ['transfer_enable', 'u', 'd']: + if muid is not None: + for row in self.data.json: + if int(row['port']) == muid: + val = row[key] + break + else: + val = user[key] + if val / 1024 < 4: + ret += " %s : %s" % (key, val) + elif val / 1024 ** 2 < 4: + val /= float(1024) + ret += " %s : %s K Bytes" % (key, val) + elif val / 1024 ** 3 < 4: + val /= float(1024 ** 2) + ret += " %s : %s M Bytes" % (key, val) + else: + val /= float(1024 ** 3) + ret += " %s : %s G Bytes" % (key, val) + else: + ret += " %s : %s" % (key, user[key]) + ret += "\n " + self.ssrlink(user, False, muid) + ret += "\n " + self.ssrlink(user, True, muid) + return ret + + def rand_pass(self): + return ''.join([random.choice('''ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~-_=+(){}[]^&%$@''') for i in range(8)]) + + def add(self, user): + up = {'enable': 1, 'u': 0, 'd': 0, 'method': "aes-128-ctr", + 'protocol': "auth_aes128_md5", + 'obfs': "tls1.2_ticket_auth_compatible", + 'transfer_enable': 9007199254740992} + up['passwd'] = self.rand_pass() + up.update(user) + + self.data.load(self.config_path) + for row in self.data.json: + match = False + if 'user' in user and row['user'] == user['user']: + match = True + if 'port' in user and row['port'] == user['port']: + match = True + if match: + print("user [%s] port [%s] already exist" % (row['user'], row['port'])) + return + self.data.json.append(up) + print("### add user info %s" % self.userinfo(up)) + self.data.save(self.config_path) + + def edit(self, user): + self.data.load(self.config_path) + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + print("edit user [%s]" % (row['user'],)) + row.update(user) + print("### new user info %s" % self.userinfo(row)) + break + self.data.save(self.config_path) + + def delete(self, user): + self.data.load(self.config_path) + index = 0 + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + print("delete user [%s]" % row['user']) + del self.data.json[index] + break + index += 1 + self.data.save(self.config_path) + + def clear_ud(self, user): + up = {'u': 0, 'd': 0} + self.data.load(self.config_path) + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + row.update(up) + print("clear user [%s]" % row['user']) + self.data.save(self.config_path) + + def list_user(self, user): + self.data.load(self.config_path) + if not user: + for row in self.data.json: + print("user [%s] port %s" % (row['user'], row['port'])) + return + for row in self.data.json: + match = True + if 'user' in user and row['user'] != user['user']: + match = False + if 'port' in user and row['port'] != user['port']: + match = False + if match: + muid = None + if 'muid' in user: + muid = user['muid'] + print("### user [%s] info %s" % (row['user'], self.userinfo(row, muid))) + + +def print_server_help(): + print('''usage: python mujson_manage.py -a|-d|-e|-c|-l [OPTION]... + +Actions: + -a add/edit a user + -d delete a user + -e edit a user + -c set u&d to zero + -l display a user infomation or all users infomation + +Options: + -u USER the user name + -p PORT server port (only this option must be set if add a user) + -k PASSWORD password + -m METHOD encryption method, default: aes-128-ctr + -O PROTOCOL protocol plugin, default: auth_aes128_md5 + -o OBFS obfs plugin, default: tls1.2_ticket_auth_compatible + -G PROTOCOL_PARAM protocol plugin param + -g OBFS_PARAM obfs plugin param + -t TRANSFER max transfer for G bytes, default: 8388608 (8 PB or 8192 TB) + -f FORBID set forbidden ports. Example (ban 1~79 and 81~100): -f "1-79,81-100" + -i MUID set sub id to display (only work with -l) + -s SPEED set speed_limit_per_con + -S SPEED set speed_limit_per_user + +General options: + -h, --help show this help message and exit +''') + + +def main(): + shortopts = 'adeclu:i:p:k:O:o:G:g:m:t:f:hs:S:' + longopts = ['help'] + action = None + user = {} + fast_set_obfs = {'0': 'plain', + '+1': 'http_simple_compatible', + '1': 'http_simple', + '+2': 'tls1.2_ticket_auth_compatible', + '2': 'tls1.2_ticket_auth'} + fast_set_protocol = {'0': 'origin', + 's4': 'auth_sha1_v4', + '+s4': 'auth_sha1_v4_compatible', + 'am': 'auth_aes128_md5', + 'as': 'auth_aes128_sha1', + 'ca': 'auth_chain_a', + } + fast_set_method = {'0': 'none', + 'a1c': 'aes-128-cfb', + 'a2c': 'aes-192-cfb', + 'a3c': 'aes-256-cfb', + 'r': 'rc4-md5', + 'r6': 'rc4-md5-6', + 'c': 'chacha20', + 'ci': 'chacha20-ietf', + 's': 'salsa20', + 'a1': 'aes-128-ctr', + 'a2': 'aes-192-ctr', + 'a3': 'aes-256-ctr'} + try: + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + for key, value in optlist: + if key == '-a': + action = 1 + elif key == '-d': + action = 2 + elif key == '-e': + action = 3 + elif key == '-l': + action = 4 + elif key == '-c': + action = 0 + elif key == '-u': + user['user'] = value + elif key == '-i': + user['muid'] = int(value) + elif key == '-p': + user['port'] = int(value) + elif key == '-k': + user['passwd'] = value + elif key == '-o': + if value in fast_set_obfs: + user['obfs'] = fast_set_obfs[value] + else: + user['obfs'] = value + elif key == '-O': + if value in fast_set_protocol: + user['protocol'] = fast_set_protocol[value] + else: + user['protocol'] = value + elif key == '-g': + user['obfs_param'] = value + elif key == '-G': + user['protocol_param'] = value + elif key == '-s': + user['speed_limit_per_con'] = int(value) + elif key == '-S': + user['speed_limit_per_user'] = int(value) + elif key == '-m': + if value in fast_set_method: + user['method'] = fast_set_method[value] + else: + user['method'] = value + elif key == '-f': + user['forbidden_port'] = value + elif key == '-t': + val = float(value) + try: + val = int(value) + except: + pass + user['transfer_enable'] = int(val * 1024) * (1024 ** 2) + elif key in ('-h', '--help'): + print_server_help() + sys.exit(0) + except getopt.GetoptError as e: + print(e) + sys.exit(2) + + manage = MuMgr() + if action == 0: + manage.clear_ud(user) + elif action == 1: + if 'user' not in user and 'port' in user: + user['user'] = str(user['port']) + if 'user' in user and 'port' in user: + manage.add(user) + else: + print("You have to set the port with -p") + elif action == 2: + if 'user' in user or 'port' in user: + manage.delete(user) + else: + print("You have to set the user name or port with -u/-p") + elif action == 3: + if 'user' in user or 'port' in user: + manage.edit(user) + else: + print("You have to set the user name or port with -u/-p") + elif action == 4: + manage.list_user(user) + elif action is None: + print_server_help() + +if __name__ == '__main__': + main() diff --git a/mysql.json b/mysql.json new file mode 100644 index 0000000..1849e9e --- /dev/null +++ b/mysql.json @@ -0,0 +1,13 @@ +{ + "host": "127.0.0.1", + "port": 3306, + "user": "ss", + "password": "pass", + "db": "sspanel", + "node_id": 0, + "transfer_mul": 1.0, + "ssl_enable": 0, + "ssl_ca": "", + "ssl_cert": "", + "ssl_key": "" +} diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..0de8d20 --- /dev/null +++ b/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash +cd `dirname $0` +python_ver=$(ls /usr/bin|grep -e "^python[23]\.[1-9]\+$"|tail -1) +eval $(ps -ef | grep "[0-9] ${python_ver} server\\.py m" | awk '{print "kill "$2}') +ulimit -n 512000 +nohup ${python_ver} server.py m>> /dev/null 2>&1 & + diff --git a/server.py b/server.py new file mode 100644 index 0000000..ba863b6 --- /dev/null +++ b/server.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2015 breakwall +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import time +import sys +import threading +import os + +if __name__ == '__main__': + import inspect + os.chdir(os.path.dirname(os.path.realpath(inspect.getfile(inspect.currentframe())))) + +import server_pool +import db_transfer +from shadowsocks import shell +from configloader import load_config, get_config + +class MainThread(threading.Thread): + def __init__(self, obj): + super(MainThread, self).__init__() + self.daemon = True + self.obj = obj + + def run(self): + self.obj.thread_db(self.obj) + + def stop(self): + self.obj.thread_db_stop() + +def main(): + shell.check_python() + if False: + db_transfer.DbTransfer.thread_db() + else: + if get_config().API_INTERFACE == 'mudbjson': + thread = MainThread(db_transfer.MuJsonTransfer) + elif get_config().API_INTERFACE == 'sspanelv2': + thread = MainThread(db_transfer.DbTransfer) + else: + thread = MainThread(db_transfer.Dbv3Transfer) + thread.start() + try: + while thread.is_alive(): + thread.join(10.0) + except (KeyboardInterrupt, IOError, OSError) as e: + import traceback + traceback.print_exc() + thread.stop() + +if __name__ == '__main__': + main() + diff --git a/server_pool.py b/server_pool.py new file mode 100644 index 0000000..fcf6caf --- /dev/null +++ b/server_pool.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import logging +import struct +import time +from shadowsocks import shell, eventloop, tcprelay, udprelay, asyncdns, common +import threading +import sys +import traceback +from socket import * +from configloader import load_config, get_config + +class MainThread(threading.Thread): + def __init__(self, params): + super(MainThread, self).__init__() + self.params = params + + def run(self): + ServerPool._loop(*self.params) + +class ServerPool(object): + + instance = None + + def __init__(self): + shell.check_python() + self.config = shell.get_config(False) + self.dns_resolver = asyncdns.DNSResolver() + if not self.config.get('dns_ipv6', False): + asyncdns.IPV6_CONNECTION_SUPPORT = False + + self.mgr = None #asyncmgr.ServerMgr() + + self.tcp_servers_pool = {} + self.tcp_ipv6_servers_pool = {} + self.udp_servers_pool = {} + self.udp_ipv6_servers_pool = {} + self.stat_counter = {} + + self.loop = eventloop.EventLoop() + self.thread = MainThread( (self.loop, self.dns_resolver, self.mgr) ) + self.thread.start() + + @staticmethod + def get_instance(): + if ServerPool.instance is None: + ServerPool.instance = ServerPool() + return ServerPool.instance + + def stop(self): + self.loop.stop() + + @staticmethod + def _loop(loop, dns_resolver, mgr): + try: + if mgr is not None: + mgr.add_to_loop(loop) + dns_resolver.add_to_loop(loop) + loop.run() + except (KeyboardInterrupt, IOError, OSError) as e: + logging.error(e) + traceback.print_exc() + os.exit(0) + except Exception as e: + logging.error(e) + traceback.print_exc() + + def server_is_run(self, port): + port = int(port) + ret = 0 + if port in self.tcp_servers_pool: + ret = 1 + if port in self.tcp_ipv6_servers_pool: + ret |= 2 + return ret + + def server_run_status(self, port): + if 'server' in self.config: + if port not in self.tcp_servers_pool: + return False + if 'server_ipv6' in self.config: + if port not in self.tcp_ipv6_servers_pool: + return False + return True + + def new_server(self, port, user_config): + ret = True + port = int(port) + ipv6_ok = False + + if 'server_ipv6' in self.config: + if port in self.tcp_ipv6_servers_pool: + logging.info("server already at %s:%d" % (self.config['server_ipv6'], port)) + return 'this port server is already running' + else: + a_config = self.config.copy() + a_config.update(user_config) + if len(a_config['server_ipv6']) > 2 and a_config['server_ipv6'][0] == b"[" and a_config['server_ipv6'][-1] == b"]": + a_config['server_ipv6'] = a_config['server_ipv6'][1:-1] + a_config['server'] = common.to_str(a_config['server_ipv6']) + a_config['server_port'] = port + a_config['max_connect'] = 128 + a_config['method'] = common.to_str(a_config['method']) + try: + logging.info("starting server at [%s]:%d" % (a_config['server'], port)) + + tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter) + tcp_server.add_to_loop(self.loop) + self.tcp_ipv6_servers_pool.update({port: tcp_server}) + + udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False, stat_counter=self.stat_counter) + udp_server.add_to_loop(self.loop) + self.udp_ipv6_servers_pool.update({port: udp_server}) + + if a_config['server_ipv6'] == "::": + ipv6_ok = True + except Exception as e: + logging.warn("IPV6 %s " % (e,)) + + if 'server' in self.config: + if port in self.tcp_servers_pool: + logging.info("server already at %s:%d" % (common.to_str(self.config['server']), port)) + return 'this port server is already running' + else: + a_config = self.config.copy() + a_config.update(user_config) + a_config['server_port'] = port + a_config['max_connect'] = 128 + a_config['method'] = common.to_str(a_config['method']) + try: + logging.info("starting server at %s:%d" % (a_config['server'], port)) + + tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False) + tcp_server.add_to_loop(self.loop) + self.tcp_servers_pool.update({port: tcp_server}) + + udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False) + udp_server.add_to_loop(self.loop) + self.udp_servers_pool.update({port: udp_server}) + + except Exception as e: + if not ipv6_ok: + logging.warn("IPV4 %s " % (e,)) + + return True + + def del_server(self, port): + port = int(port) + logging.info("del server at %d" % port) + try: + udpsock = socket(AF_INET, SOCK_DGRAM) + udpsock.sendto('%s:%s:0:0' % (get_config().MANAGE_PASS, port), (get_config().MANAGE_BIND_IP, get_config().MANAGE_PORT)) + udpsock.close() + except Exception as e: + logging.warn(e) + return True + + def cb_del_server(self, port): + port = int(port) + + if port not in self.tcp_servers_pool: + logging.info("stopped server at %s:%d already stop" % (self.config['server'], port)) + else: + logging.info("stopped server at %s:%d" % (self.config['server'], port)) + try: + self.tcp_servers_pool[port].close(True) + del self.tcp_servers_pool[port] + except Exception as e: + logging.warn(e) + try: + self.udp_servers_pool[port].close(True) + del self.udp_servers_pool[port] + except Exception as e: + logging.warn(e) + + if 'server_ipv6' in self.config: + if port not in self.tcp_ipv6_servers_pool: + logging.info("stopped server at [%s]:%d already stop" % (self.config['server_ipv6'], port)) + else: + logging.info("stopped server at [%s]:%d" % (self.config['server_ipv6'], port)) + try: + self.tcp_ipv6_servers_pool[port].close(True) + del self.tcp_ipv6_servers_pool[port] + except Exception as e: + logging.warn(e) + try: + self.udp_ipv6_servers_pool[port].close(True) + del self.udp_ipv6_servers_pool[port] + except Exception as e: + logging.warn(e) + + return True + + def update_mu_users(self, port, users): + port = int(port) + if port in self.tcp_servers_pool: + try: + self.tcp_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + try: + self.udp_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + if port in self.tcp_ipv6_servers_pool: + try: + self.tcp_ipv6_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + try: + self.udp_ipv6_servers_pool[port].update_users(users) + except Exception as e: + logging.warn(e) + + def get_server_transfer(self, port): + port = int(port) + uid = struct.pack('