Python编写socks5服务器

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协议详解

1.png

协商

  • 客户端首先向SOCKS服务器自己的协议版本号,以及支持的认证方法。SOCKS服务器向客户端返回协议版本号以及选定的认证方法。格式为:
    2.png
  • 各个字段的含义为:
    • VER:socks版本(在socks5中是X05
    • NMETHODS:在METHODS字段中出现的方法的数目
    • METHODS:客户端支持的认证方式列表,每个方法占1字节
  • 服务器端在客户端提供的METHODS域给定的方法中选择一个,然后返回一个方法选择消息:
    3.png
  • 各个字段的含义为:
    • VER:socks版本(在socks5中是X05
    • METHOD:服务端选中的方法(若返回XFF表示没有方法被选中,客户端需要关闭连接)
  • 当前定义的表示方法的值有:
    • X00 表示NO AUTHENTICATION REQUIRED(没有认证需求)
    • X01 表示GSSAPI
    • X02 表示USERNAME/PASSWORD(用户名/密码)
    • X03 到 X’7F’表示IANA ASSIGNED(由IANA负责分配)
    • X80 到 X’FE’为私有方法保留
    • XFF 表示NO ACCEPTABLE MEHTODS(没有方法被选中)
  • 之后客户端和服务端根据选定的认证方式执行对应的认证。认证结束后客户端就可以发送请求信息(如果认证方法有特殊封装要求,请求必须按照方法所定义的方式进行封装)。

认证

  • 客户端根据服务器端选定的方法进行认证,如果选定的方法是02,则根据RFC 1929定义的方法进行认证。RFC 1929定义的密码是明文传输,安全性较差。请求格式为:
    4.png
  • 各个字段的含义为:
    • VER:表示当前子协议的版本,这里是X01
    • ULEN:表示UNAME字段的长度
    • UNAME:表示用户名字节数据
    • PLEN:表示PASSWD字段的长度
    • PASSWD:表示密码的字节数据
  • 服务器校验用户名和密码,然后返回下面的响应包:
    5.png
  • 各个字段的含义为:
    • VER:表示当前子协议的版本,这里是X01
    • STATUS:表示认证结果,如果是X00表示认证成功,其他的结果表示认证失败

请求

  • 针对所依赖方法的子协商一旦完成,客户端就发送请求细节。如果协商过的方法包含了针对完整性检查或保密性目的的封装,这些请求必须被包装到所依赖的方法的封装中。SOCKS请求按下述格式进行组织:
    6.png
  • 各个字段的含义为:

    • VER:socks版本(在socks5中是0x05
    • CMD:SOCK的命令码:
      • CONNECT X01
      • BIND X02
      • UDP ASSOCIATE X03
    • RSV:保留字段
    • ATYP:地址类型:
      • IP V4地址:X01
      • 域名地址:X03
      • IP V6地址:X04
    • DST.ADDR:目的地址
    • DST.PORT:目的端口
  • 请求类型有下面几种:

    • 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数据报。
  • 服务器按以下格式回应客户端的请求:
    7.png

  • 各个字段的含义为:
    • VER:socks版本(在socks5中是X05
    • REP:应答状态码:
      • X00 成功
      • X01 连接不被规则集允许
      • X02 普通的SOCKS服务器失败
      • X03 网络不可达
      • X04 主机不可达
      • X05 连接被拒绝
      • X06 TTL超时
      • X07 命令不被支持
      • X08 地址类型不被支持
      • X09XFF 未分配
    • RSV:保留字段(标记为被保留的域必须被设置为X00
    • ATYP:地址类型:
      • IPV4地址:X01
      • 域名:X03
      • IPV6地址:X04
    • BND.ADDR:服务器绑定的地址
    • BND.PORT:服务器绑定的端口

数据转发

  • 到了这个阶段基本就是数据转发了,tcp就直接转发,udp还须要做点工作。
    • 当一个响应(REP值不为X00)指示失败的时候,SOCKS服务器必须在发送这个响应后立刻断开这条TCP连接。这必须发生在检测到引起失败的原因之后的10秒内。
    • 如果响应码(REP值为X00)指示成功,而且这次请求是BIND或者CONNECT,那么客户端可以立刻开始数据传输。如果选择的认证方法支持针对完整性、认证或私密性目的封装,要传输的数据应该包装在所依赖的认证方法的封装中。同样地,当针对客户端的响应数据到达SOCKS服务器的时候,服务器也必须根据使用的认证方法来封装数据。
  • 一个基于UDP的客户端必须将它的数据报发送到UDP中继服务器的指定端口——该端口在针对UDP ASSOCIATE的响应中的BND.PORT域指明。如果选择的认证方法提供了针对认证、完整性或私密性目的的封装,数据报必须使用恰当的封装方式进行包装。每一个UDP数据报都随身携带了一个UDP请求头:
    8.png
  • 各个字段的含义为:
    • RSV 保留位,值为X0000
    • FRAG 当前分片号
    • ATYP 下列地址类型之一:
      • IPV4地址:X01
      • 域名:X03
      • IPV6地址:X04
    • DST.ADDR 目的地址
    • DST.PORT 目的端口号
    • DATA 用户数据
  • 当一个UDP中继服务器决定中继一个UDP数据报的时候,它会默默的做——不会给客户端返回任何通知。同样的,如果它不能中继数据报那它就会丢弃数据报。当一个UDP中继服务器从远端主机收到一个响应数据报文的时候,它必须根据使用上述的UDP请求头对该响应报文进行封装,然后再进行所依赖的认证方法的封装处理。

Python socks5服务器代码实现

  • 不需要认证的socks5服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    import select
    import socket
    import struct
    from socketserver import StreamRequestHandler, ThreadingTCPServer
    SOCKS_VERSION = 5
    class 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字段的数目大于0
    assert version == SOCKS_VERSION
    assert 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_VERSION
    if address_type == 1: # IPv4
    address = socket.inet_ntoa(self.connection.recv(4))
    elif address_type == 3: # Domain name
    domain_length = self.connection.recv(1)[0]
    address = self.connection.recv(domain_length)
    #address = socket.gethostbyname(address.decode("UTF-8")) # 将域名转化为IP,这一行可以去掉
    elif address_type == 4: # IPv6
    addr_ip = self.connection.recv(16)
    address = socket.inet_ntop(socket.AF_INET6, addr_ip)
    else:
    self.server.close_request(self.request)
    return
    port = struct.unpack('!H', self.connection.recv(2))[0]
    # 响应,只支持CONNECT请求
    try:
    if cmd == 1: # CONNECT
    remote = 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 methods
    def 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:
    break
    if remote in r:
    data = remote.recv(4096)
    if client.send(data) <= 0:
    break
    if __name__ == '__main__':
    # 使用socketserver库的多线程服务器ThreadingTCPServer启动代理
    with ThreadingTCPServer(('127.0.0.1', 9011), SocksProxy) as server:
    server.serve_forever()
  • 需要认证的socks5服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    import logging
    import select
    import socket
    import struct
    from socketserver import StreamRequestHandler, ThreadingTCPServer
    logging.basicConfig(level=logging.DEBUG)
    SOCKS_VERSION = 5
    class 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字段的数目大于0
    assert version == SOCKS_VERSION
    assert 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_VERSION
    if address_type == 1: # IPv4
    address = 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: # IPv6
    addr_ip = self.connection.recv(16)
    address = socket.inet_ntop(socket.AF_INET6, addr_ip)
    else:
    self.server.close_request(self.request)
    return
    port = struct.unpack('!H', self.connection.recv(2))[0]
    # 响应,只支持CONNECT请求
    try:
    if cmd == 1: # CONNECT
    remote = 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 methods
    def verify_credentials(self):
    """校验用户名和密码"""
    version = ord(self.connection.recv(1))
    assert version == 1
    username_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 = 0
    response = struct.pack("!BB", version, 0)
    self.connection.sendall(response)
    return True
    # 验证失败, status != 0
    response = struct.pack("!BB", version, 0xFF)
    self.connection.sendall(response)
    self.server.close_request(self.request)
    return False
    def 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:
    break
    if remote in r:
    data = remote.recv(4096)
    if client.send(data) <= 0:
    break
    if __name__ == '__main__':
    # 使用socketserver库的多线程服务器ThreadingTCPServer启动代理
    with ThreadingTCPServer(('127.0.0.1', 9011), SocksProxy) as server:
    server.serve_forever()
  • 客户端代码

    1
    2
    3
    4
    5
    6
    7
    8
    import socket
    import socks
    import requests
    socks.set_default_proxy(socks.SOCKS5, "127.0.0.1", 9011, username=None, password=None)
    socket.socket = socks.socksocket
    print(requests.get('http://www.baidu.com').text)
  • 或者可以通过curl等命令直接使用

    1
    2
    3
    4
    5
    6
    # 使用账号密码访问代理
    $ 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

参考资料

文章目录
  1. 1. socks协议
  2. 2. socks协议使用场景
  3. 3. 与HTTP代理的对比
  4. 4. socks5协议详解
    1. 4.1. 协商
    2. 4.2. 认证
    3. 4.3. 请求
    4. 4.4. 数据转发
  5. 5. Python socks5服务器代码实现
    1. 5.1. 参考资料
|