diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02649d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 2017 Falseen + + 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/Limit_Clients/socket.py b/Limit_Clients/socket.py index b527311..378fdd3 100644 --- a/Limit_Clients/socket.py +++ b/Limit_Clients/socket.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- # -# Copyright 2015 Falseen +# Copyright 2017 Falseen # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,16 +18,11 @@ # # 功能:限制客户端数量(基于ip判断) # -# 使用说明:1.将此文件放在ss根目录中即可生效,不用做其他设置(需要重新运行ss)。 -# 2.修改53、54行的 clean_time 和 ip_numbers 为你想要的数值。 -# 3.如果你的服务器有ipv6,并且你想让ip4和ip6分开限制,则可以设置 only_port 为 False 。 +# 使用说明:1.将此文件放在程序根目录中即可生效,不用做其他设置(需要重新运行程序)。 +# 2.修改 recv_timeout 和 Limit_Clients_Num 为你想要的数值。 +# 3.如果你的服务器有ipv6或有多个ip,并且你想让这些ip分开限制,则可以设置 only_port 为 False 。 # # -# 原理:默认情况下,ss在运行的时候会引用系统中的socket文件,但是把这个socket文件放到ss目录之后,ss就会引用这个我们自定义的socket文件。 -# 然后在这个文件中再引用真正的socket包,并在原socket的基础上加以修改,最终ss调用的就是经过我们修改的socket文件了。 -# -# 所以理论上任何引用了socket包的python程序都可以用这个文件来达到限制连接ip数量的目的。 -# from __future__ import absolute_import, division, print_function, \ @@ -41,169 +36,210 @@ import time import logging import types +import struct path = sys.path[0] sys.path.pop(0) -import socket # 导入真正的socket包 +# import real socket +import socket sys.path.insert(0, path) -clean_time = 60 # 设置清理ip的时间间隔,在此时间内无连接的ip会被清理 -ip_numbers = 2 # 设置每个端口的允许通过的ip数量,即设置客户端ip数量 -only_port = True # 设置是否只根据端口判断。如果为 True ,则只根据端口判断。如果为 False ,则会严格的根据 服务端ip+端口进行判断 +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +set_close_timeout = True # 是否清理指定时间内无数据收发的连接,仅对TCP有效,socket会自动关闭正常或异常断开的连接, 所以一般不用设置。 + # 如果为True则根据下面的 recv_timeout 进行清理,如果为 False 则根据socket的默认超时时间来清理(5分钟左右)。 + +recv_timeout = 120 # 配合上面的选项设置 tcp 清理连接的超时时间,单位秒。在此时间内无连接或无数据 收发 的连接会被清理。 + # 只针对 tcp。一般客户端在关闭的时候会主动关闭连接,此选项主要是应对客户端的非正常关闭。 + # 比如说突然断网之类的。建议不要设置为0或120以下,因为 tcp keepalive 的默认时间一般是120秒。 + +recvfrom_timeout = 30 # 设置 udp 清理连接的超时时间,单位秒。在此时间内无连接或无数据 接收 的连接会被清理。只针对 udp。 + +Limit_Clients_Num = 1 # 设置每个端口的默认允许通过的ip数量,即客户端的ip数量。 + # 此为默认值,如果黑名单中没有定义,则按此选项的值来设置。 + +only_port = True # 设置是否只根据端口判断。如果为 True ,则只根据端口判断。如果为 False ,则会严格的根据服务端ip+端口进行判断。 + # 此功能主要适用于服务端有多个ip的情况,比如同时拥有ipv4和ipv6的ip。 + +# 白名单,在此名单内的端口不受限制,可以留空。格式 white_list = [80, 443] 端口间用半角逗号分隔,注意一定要是数字,不能加引号。 +white_list = [] + +# 黑名单,在此名单内的端口会受到限制, 可以留空。 +# 格式 端口:ip数量, black_list = {80:1, 443:2} 如果白名单和黑名单都留空,则默认限制所有端口。 +black_list = {} + +#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +PYTHON_VERSION = sys.version_info[0] +send_timeout = recv_timeout +limit_all_clients = False -# 动态path类方法 -def re_class_method(_class, method_name, re_method): +if not black_list and not white_list: + limit_all_clients = True + +# 动态patch类方法 +def new_class_method(_class, method_name, new_method): method = getattr(_class, method_name) info = sys.version_info if info[0]>= 3: setattr(_class, method_name, - types.MethodType(lambda *args, **kwds: re_method(method, *args, **kwds), _class)) + types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), _class)) else: setattr(_class, method_name, - types.MethodType(lambda *args, **kwds: re_method(method, *args, **kwds), None, _class)) + types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), None, _class)) + -# 动态path实例方法 -def re_self_method(self, method_name, re_method): +# 动态patch实例方法 +def new_self_method(self, method_name, new_method): method = getattr(self, method_name) - setattr(self, method_name, types.MethodType(lambda *args, **kwds: re_method(method, *args, **kwds), self, self)) + info = sys.version_info + if info[0]>= 3: + setattr(self, method_name, types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), self)) + else: + setattr(self, method_name, types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), self, self)) # 处理Tcp连接 -def re_accept(old_method, self, *args, **kwds): +def new_accept(orgin_method, self, *args, **kwds): while True: - - return_value = old_method(self, *args, **kwds) + return_value = orgin_method(*args, **kwds) self_socket = return_value[0] - - if only_port: - server_ip_port = '%s' % self.getsockname()[1] - else: - server_ip_port = '%s_%s' % (self.getsockname()[0], self.getsockname()[1]) - - client_ip = return_value[1][0] - - client_ip_list = [x[0].split('#')[0] for x in self._list_client_ip[server_ip_port]] - - if len(self._list_client_ip[server_ip_port]) == 0: - logging.debug("[re_socket] first add %s" % client_ip) - self._list_client_ip[server_ip_port].append(['%s#%s' % (client_ip, time.time()), self_socket]) - return return_value - - if client_ip in client_ip_list: - logging.debug("[re_socket] update socket in %s" % client_ip) - _ip_index = client_ip_list.index(client_ip) - self._list_client_ip[server_ip_port][_ip_index][0] = '%s#%s' % (client_ip, time.time()) - self._list_client_ip[server_ip_port][_ip_index].append(self_socket) + client_ip, client_port = return_value[1][:2] + server_addrs = self._server_addrs + client_list = self._all_client_list.get(server_addrs, {}) + if len(client_list) < self._limit_clients_num or client_ip in client_list: + self_socket._server_addrs = self._server_addrs + self_socket.close = self_socket.new_close + logging.debug("[socket] add client %s:%d" %(client_ip, client_port)) + if client_list.get(client_ip, None) == None: + client_list.update({client_ip : {"client_num":0, "last_up_time":0}}) + client_list[client_ip]["client_num"] += 1 + self._all_client_list[server_addrs].update(client_list) + if set_close_timeout: + # set recv_timeout and send_timeout , struct.pack("II", some_num_secs, some_num_microsecs) + self_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, struct.pack("LL", recv_timeout, 0)) + self_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, struct.pack("LL", send_timeout, 0)) return return_value - else: - if len(self._list_client_ip[server_ip_port]) < ip_numbers: - logging.debug("[re_socket] add %s" % client_ip) - self._list_client_ip[server_ip_port].append(['%s#%s' % (client_ip, time.time()), self_socket]) - return return_value + for k,v in self._all_client_list[server_addrs].copy().items(): + last_up_time = v["last_up_time"] + if time.time() - last_up_time> recvfrom_timeout and v["client_num"] < 1: + if set_close_timeout: + self_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, struct.pack("LL", recv_timeout, 0)) + self_socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDTIMEO, struct.pack("LL", send_timeout, 0)) + logging.info("[socket] remove the client %s" % (k)) + del client_list[k] + if client_list.get(client_ip, None) == None: + client_list.update({client_ip : {"client_num":0, "last_up_time":0}}) + client_list[client_ip]["client_num"] += 1 + self._all_client_list[server_addrs].update(client_list) + self_socket._server_addrs = self._server_addrs + self_socket.close = self_socket.new_close + return return_value + if time.time() - self.last_log_time[0]> 10: + logging.error("[socket] the server_addrs %s client more than %d" % (server_addrs, self._limit_clients_num)) + self.last_log_time[0] = time.time() + self_socket.close() - for x in [x for x in self._list_client_ip[server_ip_port]]: - is_closed = True - if time.time() - float(x[0].split('#')[1])> clean_time: - - for y in x[1:]: - try: - y.getpeername() # 判断连接是否关闭 - is_closed = False - break - except: # 如果抛出异常,则说明连接已经关闭,这时可以关闭套接字 - logging.debug("[re_socket] close and remove the time out socket 1/%s" % (len(x[1:]))) - x.remove(y) - - if not is_closed: - logging.debug('[re_socket] the %s still exists and update last_time' % str(x[1].getpeername()[0])) - _ip_index = client_ip_list.index(x[0].split('#')[0]) - self._list_client_ip[server_ip_port][_ip_index][0] = '%s#%s' % (x[0].split('#')[0], time.time()) - - else: - logging.info("[re_socket] remove time out ip and add new ip %s" % client_ip ) - self._list_client_ip[server_ip_port].remove(x) - self._list_client_ip[server_ip_port].append(['%s#%s' % (client_ip, time.time()), self_socket]) - return return_value - - if int(time.time()) % 5 == 0: - logging.debug("[re_socket] the port %s client more then the %s" % (server_ip_port, ip_numbers)) # 处理Udp连接 -def re_recvfrom(old_method, self, *args, **kwds): - - while True: - return_value = old_method(*args, **kwds) - self_socket = '' - if only_port: - server_ip_port = '%s' % self.getsockname()[1] - else: - server_ip_port = '%s_%s' % (self.getsockname()[0], self.getsockname()[1]) - client_ip = return_value[1][0] - client_ip_list = [x[0].split('#')[0] for x in self._list_client_ip[server_ip_port]] - - if len(self._list_client_ip[server_ip_port]) == 0: - logging.debug("[re_socket] first add %s" % client_ip) - self._list_client_ip[server_ip_port].append(['%s#%s' % (client_ip, time.time()), self_socket]) - return return_value - - if client_ip in client_ip_list: - logging.debug("[re_socket] update socket in %s" % client_ip) - _ip_index = client_ip_list.index(client_ip) - self._list_client_ip[server_ip_port][_ip_index][0] = '%s#%s' % (client_ip, time.time()) - return return_value - else: - if len(self._list_client_ip[server_ip_port]) < ip_numbers: - logging.debug("[re_socket] add %s" % client_ip) - self._list_client_ip[server_ip_port].append(['%s#%s' % (client_ip, time.time()), self_socket]) - return return_value - - for x in [x for x in self._list_client_ip[server_ip_port]]: - is_closed = True - if time.time() - float(x[0].split('#')[1])> clean_time: - - for y in x[1:]: - try: - y.getpeername() # 判断连接是否关闭 - is_closed = False - break - except: # 如果抛出异常,则说明连接已经关闭,这时可以关闭套接字 - logging.debug("[re_socket] close and remove the time out socket 1/%s" % (len(x[1:]))) - x.remove(y) - - if not is_closed: - logging.debug('[re_socket] the %s still exists and update last_time' % str(x[1].getpeername()[0])) - _ip_index = client_ip_list.index(x[0].split('#')[0]) - self._list_client_ip[server_ip_port][_ip_index][0] = '%s#%s' % (x[0].split('#')[0], time.time()) - - else: - logging.info("[re_socket] remove time out ip and add new ip %s" % client_ip ) - self._list_client_ip[server_ip_port].remove(x) - self._list_client_ip[server_ip_port].append(['%s#%s' % (client_ip, time.time()), self_socket]) - return return_value - - if int(time.time()) % 5 == 0: - logging.debug("[re_socket] the port %s client more then the %s" % (server_ip_port, ip_numbers)) - new_tuple = [b'', return_value[1]] - return_value = tuple(new_tuple) +def new_recvfrom(orgin_method, self, *args, **kwds): + return_value = orgin_method(*args, **kwds) + server_addrs = self._server_addrs + client_ip, client_port = return_value[1][:2] + client_list = self._all_client_list.get(server_addrs, {}) + + if len(client_list) < self._limit_clients_num or client_ip in client_list: + if client_list.get(client_ip, None) == None: + client_list.update({client_ip : {"client_num":0, "last_up_time":0}}) + client_list[client_ip]["last_up_time"] = time.time() + self._all_client_list[server_addrs].update(client_list) + logging.debug("[socket] update last_up_time for %s" % client_ip) return return_value + else: + for k,v in self._all_client_list[server_addrs].copy().items(): + last_up_time = v["last_up_time"] + if time.time() - last_up_time> recvfrom_timeout and v["client_num"] < 1: + logging.info("[socket] remove the client %s" % (k)) + del client_list[k] + logging.debug("[socket] add client %s:%d" %(client_ip, client_port)) + client_list.update({client_ip : {"client_num":0, "last_up_time":time.time()}}) + self._all_client_list[server_addrs].update(client_list) + return return_value - -def re_bind(old_method, self, *args, **kwds): - if only_port: - port = '%s' % args[0][1] + if time.time() - self.last_log_time[0]> 10: + logging.error("[socket] the server_addrs %s client more than %d" % (server_addrs, self._limit_clients_num)) + self.last_log_time[0] = time.time() + new_tuple = [b'', return_value[1]] + return_value = tuple(new_tuple) + return return_value + + +def new_bind(orgin_method, self, *args, **kwds): + + server_addres, server_port = args[0][:2] + # 如果绑定地址是0,那这个 socket 就一定不是和客户端通信的。 + # 此处主要是考虑到shadowsocks服务端在做流量转发的时候会对本地的socket进行绑定。 + if args[0][1] != 0: + if server_port in black_list or server_port not in white_list or limit_all_clients: + if only_port: + server_addrs = '*:%s' % server_port + else: + server_addrs = '%s:%s' % (server_addres, server_port) + self._server_addrs = server_addrs + self._all_client_list.update({server_addrs:{}}) + self._limit_clients_num = black_list.get(server_port, Limit_Clients_Num) + logging.debug("[socket] bind the new new_accept new_recvfrom") + new_self_method(self, 'accept', new_accept) + if self.type == socket.SOCK_DGRAM: + new_self_method(self, 'recvfrom', new_recvfrom) + if PYTHON_VERSION>= 3: + orgin_method(*args, **kwds) else: - port = '%s_%s' % (args[0][0], args[0][1]) - self._list_client_ip[port] = [] - re_self_method(self, 'recvfrom', re_recvfrom) - old_method(self, *args, **kwds) + orgin_method(self, *args, **kwds) + -setattr(socket.socket, '_list_client_ip', {}) -re_class_method(socket.socket, 'bind', re_bind) -re_class_method(socket.socket, 'accept', re_accept) +# 自定义 socket 类,可以自定义实例方法或属性。 +# 为 accept 生成的socket对象创建一个单独的类,目的是给其动态地绑定 close 方法。 +class new_socket(socket.socket): + def __init__(self, *args, **kwds): + super(new_socket, self).__init__(*args, **kwds) + if PYTHON_VERSION>= 3: + new_class_method(self, 'bind', new_bind) + + def close(self ,*args, **kwds): + super(new_socket, self).__init__(*args, **kwds) + + # 自定义 close 方法,让其在关闭的时候从列表中清理掉自身的 socket 或 ip。 + def new_close(self, *args, **kwds): + addr, port = self.getpeername()[0:2] + server_addrs = self._server_addrs + client_list = self._all_client_list[server_addrs] + if client_list.get(addr, None) != None: + last_up_time = client_list[addr]["last_up_time"] + if client_list[addr]["client_num"] <= 1 and time.time() - last_up_time> recvfrom_timeout: + del client_list[addr] + logging.info("[socket] remove the client %s" % (addr)) + else: + client_list[addr]["client_num"] -= 1 + logging.debug("[socket] close the client socket %s:%d" % (addr, port)) + self._all_client_list[server_addrs].update(client_list) + return super(new_socket, self).close(*args, **kwds) + + + +# 添加类属性, 此属性是全局的,所有socket对象都共享此属性。 +setattr(socket.socket, '_all_client_list', {}) +setattr(socket.socket, 'last_log_time', [0]) + +# python2 +if not PYTHON_VERSION>= 3: + new_class_method(socket.socket, 'bind', new_bind) + socket._socketobject = new_socket +socket.socket = new_socket diff --git a/README.md b/README.md index 61000ce..076b569 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ PySocket ,一个通过猴子补丁(monkey patch)动态修改 socket 的项 通过猴子补丁的方式给 socket 动态地添加一些增强功能,比如限制客户端数量、前置代理什么的。 **使用的时候只要把对应文件夹中的 socket.py 文件放到程序的目录即可生效,不用修改任何源码。** +**注意:** +* 对于通过pip安装的程序,需要放到执行文件所在的文件夹,但不建议这么做,可能会影响其他程序。建议不要用pip安装。 +* 对于shadowsocks来说,需要把socket.py文件放到根目录(即shadowsocks目录),而不能放到 shadowsocks/shadowsocks 目录。具体原因不明,有时间再好好查一下。 + ## 说明: 项目中每个文件夹代表不同的功能。 @@ -12,9 +16,9 @@ PySocket ,一个通过猴子补丁(monkey patch)动态修改 socket 的项 * **shadowsocks**: 给服务端添加前置代理的功能(原则上也适用于客户端),支持 http、socks4、socks5 代理。并且通过hook的方式去掉了ss的dns查询,ss在接收到数据之后会直接把域名和请求一起发给代理。 -* **proxy**:基本跟shadowscks一样,只是去掉了hook的代码。 +* **proxy**:基本跟shadowsocks一样,只是去掉了hook的代码。 -* **Limit_Clients**:限制客户端数量(基于ip),这是以前的代码,很久没更新了。随后可能会更新。 +* **Limit_Clients**:限制客户端数量(基于ip),支持tcp和udp,修改了close方法,在关闭连接的时候清理client list,基本上完美了。(目前python3下还有一点小问题,暂时还没想好怎么去解决) * **orgin_socket**:原始的socket,放在这里只是为了方便查看代码。毕竟 _socket.pyd 是加密的。 @@ -27,3 +31,8 @@ PySocket ,一个通过猴子补丁(monkey patch)动态修改 socket 的项 默认情况下,python程序在运行的时候会导入系统中的 socket 文件,但是把这个socket文件放到程序的目录之后, 它就会引用这个我们自定义的socket文件。 然后我们再在这个文件中再导入真正的socket包,并在原 socket 的基础上加以修改,最终程序调用的就是经过我们修改的socket文件了。 **最后我想说的是,python 真的是世界上最好的语言,太自由了!!!** + +## TODO +* 用recv方法替换掉现有的close方法,根据上次接收到的时间来清理不活动的连接。 + +* 用hook的方式修改socket,示例:`pysocket python test.py` \ No newline at end of file diff --git a/proxy/socket.py b/proxy/socket.py index 5a5c090..d19831c 100644 --- a/proxy/socket.py +++ b/proxy/socket.py @@ -395,6 +395,8 @@ def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, *arg self.proxy_peername = None self._timeout = None + self._is_client = True + self.proxy_udp_host = ("0.0.0.0",0) def _readall(self, file, count): """ @@ -456,9 +458,24 @@ def bind(self, *pos, **kw): Implements proxy connection for UDP sockets, which happens during the bind() phase. """ + # socket client + bind_host, bind_port = pos[0] + proxy = self._proxy_addr() + if bind_port == 0: + self._is_client = True + else: + self._is_client = False proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy - if not proxy_type or self.type != socket.SOCK_DGRAM: + if not proxy_type or not self._is_client: return _orig_socket.bind(self, *pos, **kw) + elif self.type != socket.SOCK_DGRAM and self._is_client: + if "127.0.0.1" in proxy or u"127.0.0.1" in proxy: + _pos_list = list(pos) + _pos_list[0] = ("127.0.0.1", bind_port) + _pos = tuple(_pos_list) + else: + _pos = pos + return _orig_socket.bind(self, *_pos, **kw) elif self.proxy[0] == HTTP: return _orig_socket.bind(self, *pos, **kw) @@ -490,12 +507,13 @@ def bind(self, *pos, **kw): # but some proxies return a private IP address (10.x.y.z) host, _ = proxy _, port = relay - super(socksocket, self).connect((host, port)) + #super(socksocket, self).connect((host, port)) super(socksocket, self).settimeout(self._timeout) self.proxy_sockname = ("0.0.0.0", 0) # Unknown + self.proxy_udp_host = (host, port) def sendto(self, bytes, *args, **kwargs): - if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP: + if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP or not self._is_client: return super(socksocket, self).sendto(bytes, *args, **kwargs) if not self._proxyconn: self.bind(("", 0)) @@ -509,19 +527,19 @@ def sendto(self, bytes, *args, **kwargs): STANDALONE = b"\x00" header.write(STANDALONE) self._write_SOCKS5_address(address, header) - + super(socksocket, self).connect(self.proxy_udp_host) sent = super(socksocket, self).send( header.getvalue() + bytes, *flags, **kwargs) return sent - header.tell() def send(self, bytes, flags=0, **kwargs): - if self.type == socket.SOCK_DGRAM and self.proxy[0] != HTTP: - return self.sendto(bytes, flags, self.proxy_peername, **kwargs) + if self.type == socket.SOCK_DGRAM and self.proxy[0] != HTTP and self._is_client: + return self.sendto(bytes, flags, self.proxy_udp_host, **kwargs) else: return super(socksocket, self).send(bytes, flags, **kwargs) def recvfrom(self, bufsize, flags=0): - if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP: + if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP or not self._is_client: return super(socksocket, self).recvfrom(bufsize, flags) if not self._proxyconn: self.bind(("", 0)) diff --git a/shadowsocks/README.md b/shadowsocks/README.md index bb244bd..b168a5b 100644 --- a/shadowsocks/README.md +++ b/shadowsocks/README.md @@ -2,14 +2,13 @@ ## 功能: -通过猴子补丁的方式给 socket 动态地添加一些增强功能,比如限制客户端数量、前置代理什么的。 -**使用的时候只要把对应文件夹中的 socket.py 文件放到程序的目录即可生效,不用修改任何源码。** 注意:这是python代码,因此只支持python版的shadowsocks。 -* **shadowosocks**: 给服务端添加前置代理(原则上也适用于客户端),支持 http、socks4、socks5 代理。并且通过hook的方式去掉了ss的dns查询,ss在接收到数据之后会直接把域名和请求一起发给代理。 +给服务端添加前置代理(原则上也适用于客户端),支持 http、socks4、socks5 代理。并且通过hook的方式去掉了ss的dns查询,ss在接收到数据之后会直接把域名和请求一起发给代理。 + +**使用的时候修改 socket.py 文件中 PROXY_TYPE、PROXY_ADDR、PROXY_PORT 等字段为你的代理地址,然后放到 shadowsocks 根目录即可生效。不用修改任何源码。** -使用的时候修改 socket.py 文件中 PROXY_TYPE、PROXY_ADDR、PROXY_PORT 等字段为你的代理地址,然后放到 shadowsocks 根目录即可生效。 如果不想 hook shadowsocks的代码的话,把文件中末尾的代码删除即可,如下: ```python @@ -26,5 +25,8 @@ shadowsocks.asyncdns.DNSResolver.resolve = new_resolve ``` + +* **dns_forward 文件夹** : 简单的udp端口转发,可以把服务端访问(从客户端接收到的请求)某个ip的流量强制转到另外一个ip。比如当手机端开启udp转发的时候,可以把手机端访问 8.8.8.8:53的流量转移到 114.114.114.114:53,或是内网的dns。 + ## TODO 增加类似acl的功能,过滤一些本地私有地址或其他地址。 diff --git a/shadowsocks/dns_forward/socket.py b/shadowsocks/dns_forward/socket.py new file mode 100644 index 0000000..7399619 --- /dev/null +++ b/shadowsocks/dns_forward/socket.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright 2017 Falseen +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +from __future__ import absolute_import, division, print_function, \ + with_statement, nested_scopes + +import sys + +del sys.modules['socket'] + +import sys +import time +import logging +import types +import functools + +path = sys.path[0] +sys.path.pop(0) + +# import real socket +import socket +import struct +import binascii + +sys.path.insert(0, path) + + + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +# 原始地址和端口 +orgin_addr = "114.114.114.114" +orgin_port = 53 + +# 修改后的地址和端口 +new_dst_addr = "114.114.115.115" +new_dst_port = 53 + +#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +# 动态patch类方法 +def new_class_method(_class, method_name, new_method): + method = getattr(_class, method_name) + info = sys.version_info + if info[0]>= 3: + setattr(_class, method_name, + types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), _class)) + else: + setattr(_class, method_name, + types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), None, _class)) + + +# 动态patch实例方法 +def new_self_method(self, method_name, new_method): + method = getattr(self, method_name) + info = sys.version_info + if info[0]>= 3: + setattr(self, method_name, types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), self)) + else: + setattr(self, method_name, types.MethodType(lambda *args, **kwds: new_method(method, *args, **kwds), self, self)) + + +def new_recvfrom(real_method, self, *args, **kwds): + data, src_addrs = real_method(*args, **kwds) + src_addr, src_port = src_addrs + if src_port == new_dst_port and src_addr == new_dst_addr: + # logging.info("fix %s:%d to %s:%d" % (src_addr, src_port, orgin_addr, orgin_port)) + return data, (orgin_addr, orgin_port) + return data, src_addrs + + +def new_sendto(orgin_method ,self, *args, **kwds): + data, dst_addrs = args + dst_addr, dst_port = dst_addrs + if dst_port == orgin_port and dst_addr == orgin_addr : + # logging.info("forward %s:%d to %s:%d" % (dst_addr, dst_port, new_dst_addr, new_dst_port)) + new_self_method(self, 'recvfrom', new_recvfrom) + args = (data, (new_dst_addr, new_dst_port)) + return_value = orgin_method(*args, **kwds) + return return_value + + +# make a new socket class +class new_socket(socket.socket): + + def __init__(self, *args, **kwds): + super(new_socket, self).__init__(*args, **kwds) + new_self_method(self, 'sendto', new_sendto) + + + +# replace socket class to new_socket +socket.socket = new_socket diff --git a/shadowsocks/socket.py b/shadowsocks/socket.py index a462976..74d1a57 100644 --- a/shadowsocks/socket.py +++ b/shadowsocks/socket.py @@ -395,6 +395,8 @@ def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, *arg self.proxy_peername = None self._timeout = None + self._is_client = True + self.proxy_udp_host = ("0.0.0.0",0) def _readall(self, file, count): """ @@ -456,9 +458,24 @@ def bind(self, *pos, **kw): Implements proxy connection for UDP sockets, which happens during the bind() phase. """ + # socket client + bind_host, bind_port = pos[0] + proxy = self._proxy_addr() + if bind_port == 0: + self._is_client = True + else: + self._is_client = False proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy - if not proxy_type or self.type != socket.SOCK_DGRAM: + if not proxy_type or not self._is_client: return _orig_socket.bind(self, *pos, **kw) + elif self.type != socket.SOCK_DGRAM and self._is_client: + if "127.0.0.1" in proxy or u"127.0.0.1" in proxy: + _pos_list = list(pos) + _pos_list[0] = ("127.0.0.1", bind_port) + _pos = tuple(_pos_list) + else: + _pos = pos + return _orig_socket.bind(self, *_pos, **kw) elif self.proxy[0] == HTTP: return _orig_socket.bind(self, *pos, **kw) @@ -490,12 +507,13 @@ def bind(self, *pos, **kw): # but some proxies return a private IP address (10.x.y.z) host, _ = proxy _, port = relay - super(socksocket, self).connect((host, port)) + #super(socksocket, self).connect((host, port)) super(socksocket, self).settimeout(self._timeout) self.proxy_sockname = ("0.0.0.0", 0) # Unknown + self.proxy_udp_host = (host, port) def sendto(self, bytes, *args, **kwargs): - if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP: + if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP or not self._is_client: return super(socksocket, self).sendto(bytes, *args, **kwargs) if not self._proxyconn: self.bind(("", 0)) @@ -509,19 +527,19 @@ def sendto(self, bytes, *args, **kwargs): STANDALONE = b"\x00" header.write(STANDALONE) self._write_SOCKS5_address(address, header) - + super(socksocket, self).connect(self.proxy_udp_host) sent = super(socksocket, self).send( header.getvalue() + bytes, *flags, **kwargs) return sent - header.tell() def send(self, bytes, flags=0, **kwargs): - if self.type == socket.SOCK_DGRAM and self.proxy[0] != HTTP: - return self.sendto(bytes, flags, self.proxy_peername, **kwargs) + if self.type == socket.SOCK_DGRAM and self.proxy[0] != HTTP and self._is_client: + return self.sendto(bytes, flags, self.proxy_udp_host, **kwargs) else: return super(socksocket, self).send(bytes, flags, **kwargs) def recvfrom(self, bufsize, flags=0): - if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP: + if self.type != socket.SOCK_DGRAM or self.proxy[0] == HTTP or not self._is_client: return super(socksocket, self).recvfrom(bufsize, flags) if not self._proxyconn: self.bind(("", 0))