Python编写socks5服务器
socks协议
- socks是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。SOCKS是”SOCKetS”的缩写。
- 当防火墙后的客户端要访问外部的服务器时,就跟socks代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到版本4。最新协议是版本5,与前一版本相比,增加支持UDP、验证,以及IPv6。根据OSI模型,socks是会话层的协议,位于表示层与传输层之间。
- socks协议的RFC地址:
socks协议使用场景
- socks协议的设计初衷是在保证网络隔离的情况下,提高部分人员的网络访问权限,但是国内似乎很少有组织机构这样使用。一般情况下,大家都会使用更新的网络安全技术来达到相同的目的。
- 但是由于socksCap32和PSD这类软件,人们找到了socks协议新的用途:突破网络通信限制,这和该协议的设计初衷正好相反。另外,socks协议也可以用来内网穿透。
与HTTP代理的对比
- socks支持多种用户身份验证方式和通信加密方式
- socks工作在比HTTP代理更低的层次:socks使用握手协议来通知代理软件其客户端试图进行的连接socks,然后尽可能透明地进行操作,而常规代理可能会解释和重写报头(例如,使用另一种底层协议,例如FTP;然而,HTTP代理只是将HTTP请求转发到所需的HTTP服务器)。虽然HTTP代理有不同的使用模式,CONNECT方法允许转发TCP连接;然而,socks代理还可以转发UDP流量和反向代理,而HTTP代理不能。HTTP代理通常更适合HTTP协议,执行更高层次的过滤(虽然通常只用于GET和POST方法,而不用于CONNECT方法)。socks不管应用层是什么协议,只要是传输层是TCP/UDP协议就可以代理。
socks5协议详解
协商
- 客户端首先向SOCKS服务器自己的协议版本号,以及支持的认证方法。SOCKS服务器向客户端返回协议版本号以及选定的认证方法。格式为:
- 各个字段的含义为:
- VER:socks版本(在socks5中是
X05
) - NMETHODS:在METHODS字段中出现的方法的数目
- METHODS:客户端支持的认证方式列表,每个方法占1字节
- VER:socks版本(在socks5中是
- 服务器端在客户端提供的METHODS域给定的方法中选择一个,然后返回一个方法选择消息:
- 各个字段的含义为:
- VER:socks版本(在socks5中是
X05
) - METHOD:服务端选中的方法(若返回
XFF
表示没有方法被选中,客户端需要关闭连接)
- VER:socks版本(在socks5中是
- 当前定义的表示方法的值有:
X00
表示NO AUTHENTICATION REQUIRED(没有认证需求)X01
表示GSSAPIX02
表示USERNAME/PASSWORD(用户名/密码)X03
到 X’7F’表示IANA ASSIGNED(由IANA负责分配)X80
到 X’FE’为私有方法保留XFF
表示NO ACCEPTABLE MEHTODS(没有方法被选中)
- 之后客户端和服务端根据选定的认证方式执行对应的认证。认证结束后客户端就可以发送请求信息(如果认证方法有特殊封装要求,请求必须按照方法所定义的方式进行封装)。
认证
- 客户端根据服务器端选定的方法进行认证,如果选定的方法是02,则根据RFC 1929定义的方法进行认证。RFC 1929定义的密码是明文传输,安全性较差。请求格式为:
- 各个字段的含义为:
- VER:表示当前子协议的版本,这里是
X01
- ULEN:表示UNAME字段的长度
- UNAME:表示用户名字节数据
- PLEN:表示PASSWD字段的长度
- PASSWD:表示密码的字节数据
- VER:表示当前子协议的版本,这里是
- 服务器校验用户名和密码,然后返回下面的响应包:
- 各个字段的含义为:
- VER:表示当前子协议的版本,这里是
X01
- STATUS:表示认证结果,如果是
X00
表示认证成功,其他的结果表示认证失败
- VER:表示当前子协议的版本,这里是
请求
- 针对所依赖方法的子协商一旦完成,客户端就发送请求细节。如果协商过的方法包含了针对完整性检查或保密性目的的封装,这些请求必须被包装到所依赖的方法的封装中。SOCKS请求按下述格式进行组织:
各个字段的含义为:
- VER:socks版本(在socks5中是
0x05
) - CMD:SOCK的命令码:
- CONNECT
X01
- BIND
X02
- UDP ASSOCIATE
X03
- CONNECT
- RSV:保留字段
- ATYP:地址类型:
- IP V4地址:
X01
- 域名地址:
X03
- IP V6地址:
X04
- IP V4地址:
- DST.ADDR:目的地址
- DST.PORT:目的端口
- VER:socks版本(在socks5中是
请求类型有下面几种:
- CONNECT :
0X01
, 建立代理连接。比较常见的请求,客服端请求服务器发起链接到目标主机,目标端口的代理。SOCKS 服务器将使用目标主机,目标端口, 客户端的源地址和端口号来评估 CONNECT 请求是否通过。成功之后后续流量都会被转发到目标主机的目标端口。 - BIND :
0X02
,BIND请求被用于要求客户端接受来自服务器连接的协议中。FTP是一个众所周知的例子,它针对命令和状态报告使用主要的“客户端到服务器”的连接,但是用来响应命令(如LS、GET、PUT命令)的数据传输可以使用一条“服务器到客户端”的连接。只有在完成了connnect操作之后才能进行bind操作,bind操作之后,代理服务器会有两次响应, 第一次响应是在创建socket监听完成之后,第二次是在目标机器连接到代理服务器上之后。.建立流程如下:- Client随BIND请求,发送其要绑定的地址和端口。
- Server返回其创建的监听端口的地址和端口。
- Server创建的监听端口有连接后,返回该连接的源地址和端口。
- Server端将上述连接中的流量,发送给client的监听端口。
- UDP ASSOCIATE :
0x03
,用于在UDP中继处理中建立一条关联以处理UDP数据报。
- CONNECT :
服务器按以下格式回应客户端的请求:
- 各个字段的含义为:
- VER:socks版本(在socks5中是
X05
) - REP:应答状态码:
X00
成功X01
连接不被规则集允许X02
普通的SOCKS服务器失败X03
网络不可达X04
主机不可达X05
连接被拒绝X06
TTL超时X07
命令不被支持X08
地址类型不被支持X09
到XFF
未分配
- RSV:保留字段(标记为被保留的域必须被设置为
X00
) - ATYP:地址类型:
- IPV4地址:
X01
- 域名:
X03
- IPV6地址:
X04
- IPV4地址:
- BND.ADDR:服务器绑定的地址
- BND.PORT:服务器绑定的端口
- VER:socks版本(在socks5中是
数据转发
- 到了这个阶段基本就是数据转发了,tcp就直接转发,udp还须要做点工作。
- 当一个响应(REP值不为
X00
)指示失败的时候,SOCKS服务器必须在发送这个响应后立刻断开这条TCP连接。这必须发生在检测到引起失败的原因之后的10秒内。 - 如果响应码(REP值为
X00
)指示成功,而且这次请求是BIND或者CONNECT,那么客户端可以立刻开始数据传输。如果选择的认证方法支持针对完整性、认证或私密性目的封装,要传输的数据应该包装在所依赖的认证方法的封装中。同样地,当针对客户端的响应数据到达SOCKS服务器的时候,服务器也必须根据使用的认证方法来封装数据。
- 当一个响应(REP值不为
- 一个基于UDP的客户端必须将它的数据报发送到UDP中继服务器的指定端口——该端口在针对UDP ASSOCIATE的响应中的BND.PORT域指明。如果选择的认证方法提供了针对认证、完整性或私密性目的的封装,数据报必须使用恰当的封装方式进行包装。每一个UDP数据报都随身携带了一个UDP请求头:
- 各个字段的含义为:
- RSV 保留位,值为
X0000
- FRAG 当前分片号
- ATYP 下列地址类型之一:
- IPV4地址:
X01
- 域名:
X03
- IPV6地址:
X04
- IPV4地址:
- DST.ADDR 目的地址
- DST.PORT 目的端口号
- DATA 用户数据
- RSV 保留位,值为
- 当一个UDP中继服务器决定中继一个UDP数据报的时候,它会默默的做——不会给客户端返回任何通知。同样的,如果它不能中继数据报那它就会丢弃数据报。当一个UDP中继服务器从远端主机收到一个响应数据报文的时候,它必须根据使用上述的UDP请求头对该响应报文进行封装,然后再进行所依赖的认证方法的封装处理。
Python socks5服务器代码实现
不需要认证的socks5服务器
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111import selectimport socketimport structfrom socketserver import StreamRequestHandler, ThreadingTCPServerSOCKS_VERSION = 5class SocksProxy(StreamRequestHandler):def handle(self):print('Accepting connection from {}'.format(self.client_address))# 协商# 从客户端读取并解包两个字节的数据header = self.connection.recv(2)version, nmethods = struct.unpack("!BB", header)# 设置socks5协议,METHODS字段的数目大于0assert version == SOCKS_VERSIONassert nmethods > 0# 接受支持的方法methods = self.get_available_methods(nmethods)# 无需认证if 0 not in set(methods):self.server.close_request(self.request)return# 发送协商响应数据包self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0))# 请求version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))assert version == SOCKS_VERSIONif address_type == 1: # IPv4address = socket.inet_ntoa(self.connection.recv(4))elif address_type == 3: # Domain namedomain_length = self.connection.recv(1)[0]address = self.connection.recv(domain_length)#address = socket.gethostbyname(address.decode("UTF-8")) # 将域名转化为IP,这一行可以去掉elif address_type == 4: # IPv6addr_ip = self.connection.recv(16)address = socket.inet_ntop(socket.AF_INET6, addr_ip)else:self.server.close_request(self.request)returnport = struct.unpack('!H', self.connection.recv(2))[0]# 响应,只支持CONNECT请求try:if cmd == 1: # CONNECTremote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)remote.connect((address, port))bind_address = remote.getsockname()print('Connected to {} {}'.format(address, port))else:self.server.close_request(self.request)addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]port = bind_address[1]#reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, address_type, addr, port)# 注意:按照标准协议,返回的应该是对应的address_type,但是实际测试发现,当address_type=3,也就是说是域名类型时,会出现卡死情况,但是将address_type该为1,则不管是IP类型和域名类型都能正常运行reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, 1, addr, port)except Exception as err:logging.error(err)# 响应拒绝连接的错误reply = self.generate_failed_reply(address_type, 5)self.connection.sendall(reply)# 建立连接成功,开始交换数据if reply[1] == 0 and cmd == 1:self.exchange_loop(self.connection, remote)self.server.close_request(self.request)def get_available_methods(self, n):methods = []for i in range(n):methods.append(ord(self.connection.recv(1)))return methodsdef generate_failed_reply(self, address_type, error_number):return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0)def exchange_loop(self, client, remote):while True:# 等待数据r, w, e = select.select([client, remote], [], [])if client in r:data = client.recv(4096)if remote.send(data) <= 0:breakif remote in r:data = remote.recv(4096)if client.send(data) <= 0:breakif __name__ == '__main__':# 使用socketserver库的多线程服务器ThreadingTCPServer启动代理with ThreadingTCPServer(('127.0.0.1', 9011), SocksProxy) as server:server.serve_forever()需要认证的socks5服务器
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139import loggingimport selectimport socketimport structfrom socketserver import StreamRequestHandler, ThreadingTCPServerlogging.basicConfig(level=logging.DEBUG)SOCKS_VERSION = 5class SocksProxy(StreamRequestHandler):username = 'username'password = 'password'def handle(self):logging.info('Accepting connection from %s:%s' % self.client_address)# 协商# 从客户端读取并解包两个字节的数据header = self.connection.recv(2)version, nmethods = struct.unpack("!BB", header)# 设置socks5协议,METHODS字段的数目大于0assert version == SOCKS_VERSIONassert nmethods > 0# 接受支持的方法methods = self.get_available_methods(nmethods)# 检查是否支持用户名/密码认证方式,不支持则断开连接if 2 not in set(methods):self.server.close_request(self.request)return# 发送协商响应数据包self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 2))# 校验用户名和密码if not self.verify_credentials():return# 请求version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))assert version == SOCKS_VERSIONif address_type == 1: # IPv4address = socket.inet_ntoa(self.connection.recv(4))elif address_type == 3: # 域名domain_length = self.connection.recv(1)[0]address = self.connection.recv(domain_length)elif address_type == 4: # IPv6addr_ip = self.connection.recv(16)address = socket.inet_ntop(socket.AF_INET6, addr_ip)else:self.server.close_request(self.request)returnport = struct.unpack('!H', self.connection.recv(2))[0]# 响应,只支持CONNECT请求try:if cmd == 1: # CONNECTremote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)remote.connect((address, port))bind_address = remote.getsockname()logging.info('Connected to %s %s' % (address, port))else:self.server.close_request(self.request)addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]port = bind_address[1]reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, 1, addr, port)except Exception as err:logging.error(err)# 响应拒绝连接的错误reply = self.generate_failed_reply(address_type, 5)self.connection.sendall(reply)# 建立连接成功,开始交换数据if reply[1] == 0 and cmd == 1:self.exchange_loop(self.connection, remote)self.server.close_request(self.request)def get_available_methods(self, n):methods = []for i in range(n):methods.append(ord(self.connection.recv(1)))return methodsdef verify_credentials(self):"""校验用户名和密码"""version = ord(self.connection.recv(1))assert version == 1username_len = ord(self.connection.recv(1))username = self.connection.recv(username_len).decode('utf-8')password_len = ord(self.connection.recv(1))password = self.connection.recv(password_len).decode('utf-8')if username == self.username and password == self.password:# 验证成功, status = 0response = struct.pack("!BB", version, 0)self.connection.sendall(response)return True# 验证失败, status != 0response = struct.pack("!BB", version, 0xFF)self.connection.sendall(response)self.server.close_request(self.request)return Falsedef generate_failed_reply(self, address_type, error_number):return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0)def exchange_loop(self, client, remote):while True:# 等待数据r, w, e = select.select([client, remote], [], [])if client in r:data = client.recv(4096)if remote.send(data) <= 0:breakif remote in r:data = remote.recv(4096)if client.send(data) <= 0:breakif __name__ == '__main__':# 使用socketserver库的多线程服务器ThreadingTCPServer启动代理with ThreadingTCPServer(('127.0.0.1', 9011), SocksProxy) as server:server.serve_forever()客户端代码
12345678import socketimport socksimport requestssocks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 9011, username=None, password=None)socket.socket = socks.socksocketprint(requests.get('http://www.baidu.com').text)或者可以通过curl等命令直接使用
123456使用账号密码访问代理curl -v --socks5 127.0.0.1:9011 -U username:password http://www.baidu.com无账号密码访问代理,DNS不通过代理curl -v --socks5 127.0.0.1:9011 http://www.baidu.com无账号密码访问代理,DNS通过代理curl -v --proxy socks5h://127.0.0.1:9011 http://www.baidu.com