学习socks5协议,使用python实现一个socks5代理服务器。
环境: windows10
python: 3.7
SOCKS5通讯流程
client(浏览器)发送:
VER | NMETHODS | METHODS |
---|---|---|
1 | 1 | 1-255 |
VER是SOCKS版本,这里应该是0x05;
NMETHODS是METHODS部分的长度;
METHODS是客户端支持的认证方式列表,每个方法占1字节。当前的定义是:
0x00 不需要认证
0x01 GSSAPI
0x02 用户名、密码认证
0x03 - 0x7F由IANA分配(保留)
0x80 - 0xFE为私人方法保留
0xFF 无可接受的方法
服务器从客户端提供的方法中选择一个并通过以下消息通知客户端(以字节为单位):
VER | METHOD |
---|---|
1 | 1 |
VER是SOCKS版本,这里应该是0x05;
METHOD是服务端选中的方法。如果返回0xFF表示没有一个认证方法被选中,客户端需要关闭连接。
之后客户端和服务端根据选定的认证方式执行对应的认证。
认证结束后客户端就可以发送请求信息。如果认证方法有特殊封装要求,请求必须按照方法所定义的方式进行封装。
SOCKS5请求格式(以字节为单位):
VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1 | 1 | 0x00 | 1 | 动态 | 2 |
VER是SOCKS版本,这里应该是0x05;
CMD是SOCK的命令码
0x01表示CONNECT请求
0x02表示BIND请求
0x03表示UDP转发
RSV 0x00,保留
ATYP DST.ADDR类型
0x01 IPv4地址,DST.ADDR部分4字节长度
0x03 域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
0x04 IPv6地址,16个字节长度。
DST.ADDR 目的地址
DST.PORT 网络字节序表示的目的端口
服务器按以下格式回应客户端的请求(以字节为单位):
VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
---|---|---|---|---|---|
1 | 1 | 0x00 | 1 | 动态 | 2 |
VER是SOCKS版本,这里应该是0x05;
REP应答字段
0x00表示成功
0x01普通SOCKS服务器连接失败
0x02现有规则不允许连接
0x03网络不可达
0x04主机不可达
0x05连接被拒
0x06 TTL超时
0x07不支持的命令
0x08不支持的地址类型
0x09 - 0xFF未定义
RSV 0x00,保留
ATYP BND.ADDR类型
0x01 IPv4地址,DST.ADDR部分4字节长度
0x03域名,DST.ADDR部分第一个字节为域名长度,DST.ADDR剩余的内容为域名,没有\0结尾。
0x04 IPv6地址,16个字节长度。
BND.ADDR 服务器绑定的地址
BND.PORT 网络字节序表示的服务器绑定的端口
代码
import select, socket, struct
fd_map = {}
r_list = set()
class Sockv5:
def __init__(self, ip, port):
self.sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock_server.bind((ip, port))
self.sock_server.setblocking(False)
self.sock_server.listen(1024)
def handle(self, sock):
conn, addr = self.sock_server.accept()
fileno = conn.fileno()
session = ScokSession(conn)
fd_map[fileno] = (conn, session)
r_list.add(fileno)
class ScokSession:
def __init__(self, conn:socket.socket):
self.conn = conn
self.stage = 'init'
self.remote = None
def _send_data(self, data):
self.conn.send(data)
def handle(self, sock):
data = None
try:
data = sock.recv(32 * 1024)
except BaseException as e:
print('get data error {err}'.format(err = e))
if not data:
self.destory()
if self.stage == 'init' and sock == self.conn:
self._init_stage(data)
elif self.stage == 'addr' and sock == self.conn:
self._addr_stage(data)
elif self.stage == 'connect':
self._connect_stage(data, sock)
else:
self.destory()
def _init_stage(self, data):
self._send_data(b'\x05\x00')
self.stage = 'addr'
def _addr_stage(self, data):
ver, cmd, _, atyp = struct.unpack('!BBBB', data[0:4])
data = data[4:]
# 取得要访问的地址
if atyp == 1:
address = socket.inet_ntoa(data[0:4])
data = data[4:]
elif atyp == 3:
domain_length = struct.unpack('!H', data[0])
address = data[1: domain_length+1]
data = data[domain_length+1:]
elif atyp == 4:
addr_ip = data[0: 16]
address = socket.inet_ntop(socket.AF_INET6, addr_ip)
data = data[16:]
else:
self.destory()
return
port = struct.unpack('!H', data[0:2])[0]
if cmd == 1:
print('accessing: {ip}:{port}'.format(ip=address, port=port))
self.remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.remote.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
self.remote.settimeout(5)
try:
self.remote.connect((address, port))
bind_address = self.remote.getsockname()
addr = struct.unpack('!I', socket.inet_aton(bind_address[0]))[0]
port = bind_address[1]
REP = 0
except BaseException as e:
print('connecting error : {err}'.format(err=e))
REP = 5
addr = struct.unpack("!I", socket.inet_aton('127.0.0.1'))[0]
reply = struct.pack('!BBBBIH', 5, REP, 0, atyp, addr, port)
self._send_data(reply)
if REP == 5:
self.destory()
return
fileno = self.remote.fileno()
fd_map[fileno] = (self.remote, self)
r_list.add(fileno)
self.stage = 'connect'
else:
self.destory()
return
def _connect_stage(self, data, sock):
try:
if sock == self.conn:
self.remote.send(data)
else:
self.conn.send(data)
pass
except BaseException as e:
print('exchange data error : {err}'.format(err=e))
def destory(self):
self.stage = 'DESTORYED'
if self.conn.fileno() in r_list:
r_list.remove(self.conn.fileno())
if self.remote and self.remote.fileno() in r_list:
r_list.remove(self.remote.fileno())
self.remote.close()
self.conn.close()
def main():
S = Sockv5('127.0.0.1', 9990)
fileno = S.sock_server.fileno()
fd_map[fileno] = (S.sock_server, S)
r_list.add(fileno)
while True:
try:
r, w, e = select.select(r_list, [], [])
except (OSError, IOError) as e:
continue
for h in r:
handler = fd_map[h][1]
sock = fd_map[h][0]
handler.handle(sock)
if __name__ == "__main__":
main()