Skip to content

Instantly share code, notes, and snippets.

@falseen
Last active February 21, 2017 06:16
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save falseen/61861b9fe7fa643479bf to your computer and use it in GitHub Desktop.
Save falseen/61861b9fe7fa643479bf to your computer and use it in GitHub Desktop.
限制ss客户端数量(基于ip判断),理论上可以用于其他使用了socket模块的python程序。为方便管理,已迁移至github : https://github.com/falseen/socket_path
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright 2015 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.
#
#
# 功能:限制客户端数量(基于ip判断)
#
# 使用说明:1.将此文件放在ss根目录中即可生效,不用做其他设置(需要重新运行ss)。
# 2.修改53、54行的 clean_time 和 ip_numbers 为你想要的数值。
# 3.如果你的服务器有ipv6,并且你想让ip4和ip6分开限制,则可以设置 only_port 为 False 。
#
#
# 原理:默认情况下,ss在运行的时候会引用系统中的socket文件,但是把这个socket文件放到ss目录之后,ss就会引用这个我们自定义的socket文件。
# 然后在这个文件中再引用真正的socket包,并在原socket的基础上加以修改,最终ss调用的就是经过我们修改的socket文件了。
#
# 所以理论上任何引用了socket包的python程序都可以用这个文件来达到限制连接ip数量的目的。
#
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
path = sys.path[0]
sys.path.pop(0)
import socket # 导入真正的socket包
sys.path.insert(0, path)
clean_time = 60 # 设置清理ip的时间间隔,在此时间内无连接的ip会被清理
ip_numbers = 2 # 设置每个端口的允许通过的ip数量,即设置客户端ip数量
only_port = True # 设置是否只根据端口判断。如果为 True ,则只根据端口判断。如果为 False ,则会严格的根据 服务端ip+端口进行判断
# 动态path类方法
def re_class_method(_class, method_name, re_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))
else:
setattr(_class, method_name,
types.MethodType(lambda *args, **kwds: re_method(method, *args, **kwds), None, _class))
# 动态path实例方法
def re_self_method(self, method_name, re_method):
method = getattr(self, method_name)
setattr(self, method_name, types.MethodType(lambda *args, **kwds: re_method(method, *args, **kwds), self, self))
# 处理Tcp连接
def re_accept(old_method, self, *args, **kwds):
while True:
return_value = old_method(self, *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)
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))
# 处理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)
return return_value
def re_bind(old_method, self, *args, **kwds):
if only_port:
port = '%s' % args[0][1]
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)
setattr(socket.socket, '_list_client_ip', {})
re_class_method(socket.socket, 'bind', re_bind)
re_class_method(socket.socket, 'accept', re_accept)
@falseen
Copy link
Author

falseen commented Nov 17, 2015

说明:

把这个文件放入ss根目录之后,等于是劫持了ss所引用的socket模块。因为python的机制是优先从当前目录引用模块,所以ss自然会引用这个我们自定义的文件。然后我们再在这个文件中引入真正的socket模块,并对原来的 accept 方法加以修改(实际上是加了一层壳),最终ss调用 accept 方法的时候会先调用我们自定义的方法,然后这个方法再调用真正的accept方法,之后根据accept返回的结果判断是否返回结果给 ss。大概就是这样。

有些地方的外网ip在短时间内可能会变动比较频繁,所以ip数量最好设置多一些,以免误伤。

2015年11月25日更新:
增加了对udp的限制(之前的版本只限制了tcp),目前还不是很完美,可能会有点小问题(我只是简单的测试了一下)。

@wangyandong
Copy link

你好,遇到一点问题需要麻烦你:
我把这个socks.py直接放到shadowsocks的根目录,好像没有起作用。看了根目录下的common.py中下面的代码

from __future__ import absolute_import, division, print_function, \
    with_statement

我把common.py里面的absolute_import去掉后,ss能正常运行,但是通过代理不能访问页面,通过IP可以。这个有可能是什么原因呢?谢谢

@falseen
Copy link
Author

falseen commented Dec 13, 2016

@wangyandong
没有起作用的话,可能是socket.py文件没有被引用,先看下日志里面有没有 [re_socket] 开头的字样(需要用debug模式才能看到),如果有的话则说明引用成功。如果没有,则说明引用失败。如果引用失败的话,可以试一下把它放到shadowsocks/shadowsocks目录中,因为我这个代码是针对之前的ss版本的,可能后来ss的代码做了一些更改。

“from future import absolute_import, division, print_function,
with_statement”
这行语句是为了兼容旧版本的Python而写的,所以去掉“absolute_import”并没有太大的作用。

另外你说的 ”通过代理不能访问页面,通过ip可以“ 是什么意思 ?我没太听懂。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment