由浅入深写代理(4)-socks5-代理.md

接下来我们用 python 写下 socks5 服务端的实现

0x03 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 logging
import socket
import struct
import select
import threading
def send_data(sock, data):
print(data)
bytes_sent = 0
while True:
r = sock.send(data[bytes_sent:])
if r < 0:
return r
bytes_sent += r
if bytes_sent == len(data):
return bytes_sent
def handle_tcp(sock, remote):
# 处理 client socket 和 remote socket 的数据流
try:
fdset = [sock, remote]
while True:
# 用 IO 多路复用 select 监听套接字是否有数据流
r, w, e = select.select(fdset, [], [])
if sock in r:
data = sock.recv(4096)
if len(data) <= 0:
break
result = send_data(remote, data)
if result < len(data):
raise Exception('failed to send all data')
if remote in r:
data = remote.recv(4096)
if len(data) <= 0:
break
result = send_data(sock, data)
if result < len(data):
raise Exception('failed to send all data')
except Exception as e:
raise(e)
finally:
sock.close()
remote.close()
def handle_con(sock, addr):
# 接受客户端来的请求,socks5 的 认证和连接过程
sock.recv(256)
# 无需进一步认证信息
sock.send(b"\x05\x00")
data = sock.recv(4) or '\x00' * 4
# CMD 为 0x01 也就是 CONNECT 继续
mode = data[1]
if mode != 1:
return
# DST.ADDR 有三种形式,分别做判断
addr_type = data[3]
if addr_type == 1:
addr_ip = sock.recv(4)
remote_addr = socket.inet_ntoa(addr_ip)
elif addr_type == 3:
addr_len = int.from_bytes(sock.recv(1), byteorder='big')
remote_addr = sock.recv(addr_len)
elif addr_type == 4:
addr_ip = sock.recv(16)
remote_addr = socket.inet_ntop(socket.AF_INET6, addr_ip)
else:
return
# DST.PORT
remote_addr_port = struct.unpack('>H', sock.recv(2))
# 返回给客户端 success
reply = b"\x05\x00\x00\x01"
reply += socket.inet_aton('0.0.0.0') + struct.pack(">H", 8888)
sock.send(reply)
# 拿到 remote address 的信息后,建立连接
try:
remote = socket.create_connection((remote_addr, remote_addr_port[0]))
logging.info('connecting %s:%d' % (remote_addr, remote_addr_port[0]))
except socket.error as e:
logging.error(e)
return
handle_tcp(sock, remote)
def main():
socketServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socketServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
socketServer.bind(('', 8888))
socketServer.listen(5)
try:
while True:
sock, addr = socketServer.accept()
t = threading.Thread(target=handle_con, args=(sock, addr))
t.start()
except socket.error as e:
logging.error(e)
except KeyboardInterrupt:
socketServer.close()
if __name__ == '__main__':
main()

首先,上面只是个 socks5 服务器的简单实现,处理 TCP 的转发,无需密码认证。

接下来我们分开来看,主要是三个部分 main(), handle_con(), handle_tcp()

  • main() 函数大家其实挺熟悉的,就是前面教程介绍的 socket 编程的线程方法,绑定套接字监听,然后有客户端请求后,调用 handle_con()
  • handle_con 实现了 socks5 的 认证和连接过程,大家对照下注释和前面的 socks5 协议,应该挺容易看懂。
  • 认证完成后,就开始转发请求了, handle_tcp() 实现了这个功能,这里面用到了 select ,IO 多路复用模式。其实一开始怕大家不理解,也想用线程来实现,不过发现 IO 多路复用在一个线程中就能监听多个套接字,代码比多线程方式更加简洁,索性就这么用了。这里面没有根据平台去判断用 select, poll 还是 epoll,感兴趣的话可以直接看 shadowsocks 的实现 eventloop

下篇我们看看 socks5 服务器如何和 client 端交互。

参考链接:

* https://github.com/felix021/ssocks5/blob/master/ssocks5.py
* https://github.com/RicterZ/reprocks/blob/master/server/reprocks_server.py

由浅入深写代理(3) -socks5 代理

本文讲的是如何写一个 socks5 代理,其实 shadowsocks 的代理也是 socks5 协议的,所以 socks5 代理也是本系列教程的一个重点。

首先放出 socks5 协议的 rfc,socks5 协议很简单,SOCKS5 协议并不负责代理服务器的数据传输环节,此协议只是在 C/S 两端真实交互之间,建立起一条从客户端到代理服务器的授信连接。

sock5 代理结构图

0x02 socks5 协议分析

认证阶段

首先客户端需要和服务端有个握手认证的过程,可以采用 用户名/密码 认证或者无需认证方式。

格式如下 (数字表示位数

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     |  1~255   |
+----+----------+----------+
  • VER 字段是当前协议的版本号,也就是 5;
  • NMETHODS 字段是 METHODS 字段占用的字节数;
  • METHODS 字段的每一个字节表示一种认证方式,表示客户端支持的全部认证方式。

    0x00: NO AUTHENTICATION REQUIRED
    0x01: GSSAPI
    0x02: USERNAME/PASSWORD
    0x03: to X’7F’ IANA ASSIGNED
    0x80: to X’FE’ RESERVED FOR PRIVATE METHODS
    0xFF: NO ACCEPTABLE METHODS
    

服务端返回格式

+----+--------+
|VER | METHOD |
+----+--------+
| 1  |   1    |
+----+--------+

一般情况下服务端返回两种情况

0x05 0x00:告诉客户端采用无认证的方式建立连接;
0x05 0xff:客户端的任意一种认证方式服务器都不支持。

举个例子, 服务器无需认证的情况如下

client -> server: 0x05 0x01 0x00
server -> client: 0x05 0x00
连接阶段

认证完成,客户端向服务端发送请求:

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  |   1   |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • CMD 字段 command 的缩写:
    • 0x01:CONNECT 建立 TCP 连接
    • 0x02: BIND 上报反向连接地址
    • 0x03:关联 UDP 请求
  • RSV 字段:保留字段,值为 0x00
  • ATYP 字段:address type 的缩写,取值为:
    • 0x01:IPv4
    • 0x03:域名
    • 0x04:IPv6
  • DST.ADDR 字段:destination address 的缩写,取值随 ATYP 变化:
    • ATYP == 0x01:4 个字节的 IPv4 地址
    • ATYP == 0x03:1 个字节表示域名长度,紧随其后的是对应的域名
    • ATYP == 0x04:16 个字节的 IPv6 地址
    • DST.PORT 字段:目的服务器的端口

服务端返回格式

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  |   1   |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • REP 字段
    • X’00’ succeeded
    • X’01’ general SOCKS server failure
    • X’02’ connection not allowed by ruleset
    • X’03’ Network unreachable
    • X’04’ Host unreachable
    • X’05’ Connection refused
    • X’06’ TTL expired
    • X’07’ Command not supported
    • X’08’ Address type not supported
    • X’09’ to X’FF’ unassigned

举个例子,客户端通过 127.0.0.1:8000 的代理发送请求

# request:        VER  CMD  RSV  ATYP DST.ADDR            DST.PORT
client -> server: 0x05 0x01 0x00 0x01 0x7f 0x00 0x00 0x01 0x1f 0x40
# response:       VER  REP  RSV  ATYP BND.ADDR            BND.PORT
server -> client: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10

传输阶段

接下来就开始传输数据,socks5 服务器只做单纯的转发功能

整个过程如下

# 认证阶段
client -> server: 0x05 0x01 0x00
server -> client: 0x05 0x00
# 连接阶段
client -> server: 0x05 0x01 0x00 0x03 0x0a b'google.com'  0x00 0x50
server -> client: 0x05 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x10 0x10
# 传输阶段
client -> server -> remote
remote -> server -> client
...    

下篇教程用代码实现下 socks5 代理

参考链接:

* https://loggerhead.me/posts/shadowsocks-yuan-ma-fen-xi-xie-yi-yu-jie-gou.html#fn:bnd.addr
* https://www.ietf.org/rfc/rfc1928.txt
* http://www.moye.me/2017/08/03/analyze-socks5-protocol/

由浅入深写代理(2)- socket 编程

说到代理,那肯定会跟网络协议有关,包括(tcp, ip, http),网络中的进程需要通过 socket 来通信,socket 可以认为是操作系统抽象出来的一类接口,供使用者能够更加方便的与底层的网络协议打交道。

0x01
我们先来看看 tcp 的 socket 编程。

服务端

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
import socket
import threading
# AF_INET: 基于 IPV4 的网络通信 SOCK_STREAM: 基于 TCP 的流式 socket 通信
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 将套接字绑定到地址
s.bind(('127.0.0.1', 8888))
# 监听TCP传入连接
s.listen(5)
def handle_tcp(sock, addr):
print("new connection from %s:%s" % addr)
sock.send(b'Welcome!')
while True:
data = sock.recv(1024)
if not data:
break
sock.send(b'Hello, %s!' % data)
sock.close()
while True:
sock, addr = s.accept()
t = threading.Thread(target=handle_tcp, args=(sock, addr))
t.start()

客户端

1
2
3
4
5
6
7
8
9
10
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8888))
print(s.recv(1024))
for data in [b'dog']:
s.send(data)
print(s.recv(1024))
s.close()

上面是个很简单的客户端和服务端的例子,服务器端用了线程,主要是为了能够同时处理多个请求。不然每次处理请求的时候整个程序就会处于阻塞状态。

通过 wireshark 捕获请求,可以看到客户端经过三次握手和服务端连接成功,接下来双方开始发送数据,发送完成后,四次挥手断开连接。

具体说下过程吧

  1. 首先服务器端初始化一个 socket 对象,将 socket 绑定到 (127.0.0.1, 8888) 这个地址上(bind),然后开始监听(listen),并阻塞在 accept 函数上,直到有连接过来。
  2. 客户端也初始化一个 socket 对象,调用 connect 和服务端建立连接。
  3. 服务端 accept 函数返回了一个新的 sock 套接字对象,传入到新线程中和客户端交互数据。
  4. 接下来就是 socket 的 recv 和 send 函数进行数据的交互。
  5. 最后 socket close 关闭套接字。

由于 tcp 传递的数据属于 stream, 也就是调用 recv 和 send 的次数都没有限制,对数据的发送和边界也没有限制。这个和下文的 udp 编程有区别,发送端每执行一次写操作,udp 模块就会将它封装成一个 udp 包发送,接收端也对每个 udp 包执行一次读操作,每次都得完整取出来,如果没有足够的应用缓冲区来读取 udp 数据包,则会被截断。

0x02

再简单看下 udp 的 socket 编程

服务端

1
2
3
4
5
6
7
8
9
10
11
import socket
# AF_INET: 基于 IPV4 的网络通信 SOCK_DGRAM: 基于 udp 的流式 socket 通信
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 将套接字绑定到地址
s.bind(('127.0.0.1', 8888))
while True:
data, addr = s.recvfrom(1024)
print('Received from %s:%s.' % addr)
s.sendto(b'Hello, %s!' % data, addr)

客户端

1
2
3
4
5
6
7
8
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for data in [b'dog']:
s.sendto(data, ('127.0.0.1', 8888))
print(s.recv(1024))
s.close()

由于 udp 不需要建立连接,只需要知道对方的 IP 地址和端口号,就可以直接发数据包。但是,能不能到达就不知道了。所以客户端直接通过 sendto() 给服务器发数据,服务端调用 recvfrom() 就能拿到数据。

由浅入深写代理(1)-介绍

代理作为现代人上网必备的一个工具,但是大家其实对其中的原理也不是特别了解,所以写了一个系列的教程,由浅入深的讲解下。

代理可以分为正向代理和反向代理。

  • 正向代理是一个位于客户端和目标服务器之间的代理服务器,用来把客户端的请求代理到目标服务器,比如科学上网。
  • 反向代理就好像是目标服务器,客户端并不知道这个请求具体会被转发到哪里,由反向代理自己判断,比如负载均衡。

这次主要教程跟正向代理有关,目前已更新如下

由浅入深写代理(2)- socket 编程
由浅入深写代理(3) -socks5 代理
由浅入深写代理(4)-socks5-代理
由浅入深写代理(5)-socks5-代理
由浅入深写代理(6)-http-代理
由浅入深写代理(7)-https-代理
由浅入深写代理(8)-ss-代理
由浅入深写代理(9)-ssh-代理
由浅入深写代理(10)-内网穿透

tips

  1. 这次教程的所有资源的 github 地址为 https://github.com/facert/socket-example
    (除了 ss-local.py 和 ss-server.py 以外,其他都用 python3 编写)
  2. 公众号 程序化思维 , 欢迎关注

写给前端的甩锅指南

1
2
3
4
5
6
7
8
9
前端小白,“老王,你的接口不能用了”
后端老王一脸不屑,“怎么可能,刚刚还能用,你看看参数传对了没有。”
前端小白吭哧吭哧检查了几遍,“参数没问题啊,你看看是不是有 username, id。。。”
后端老王顿了许久, “参数应该没问题,你再清下浏览器缓存试试”
前端小白摁着 "ctr + f5" 刷新了好多次, 一脸苦逼的说:“清完浏览器缓存了,还是有问题”
后端老王略过一丝疑虑,难道是我接口出问题了,不该啊,刚还能用,都测试好几遍了,说道:“你看看是不是数据库存的测试数据有问题,删了再试试,或者换个浏览器,重启下 server。”
前端小白照着老王的方法逐一尝试着。。。
突然老王拍了下脑瓜,一脸尴尬的说:“小白,我刚 fix 一个线上 bug, 测试环境换了分支,所以接口出问题了。”
小白一脸生无可恋的望着老王头顶的地中海发呆。。。

上面情节纯属虚构,作为一个后端开发人员,我怎么忍心这么黑自己的同行呢 😭。不过思考下这么一个问题,前端如何能够快速的定位问题呢。这里面跟很多因素有关,比如前后端协作流程是否完善,前后端人员的经验。

Read More