代理服务器原理(https, socks5)

Posted by Leo on 2023-11-25
Estimated Reading Time 10 Minutes
Words 2.5k In Total

HTTP代理原理

HTTP存在两种代理方式:

  1. 普通代理
  2. 隧道代理

普通代理

HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端

对于连接到它的客户端来说,它是服务端;对于要连接的服务端来说,它是客户端

这里也有两种代理服务器:

  1. 正向代理:客户端通过修改配置,显式指定浏览器代理地址,代理服务器可以通过请求头的一些字段(X-Forwarded-For)决定是否隐藏客户端的真实IP,抑或是伪造IP,如X-Forwarded-For伪造客户端ip漏洞

    详见X-Forwarded-for漏洞解析 - 浅易深 - 博客园 (cnblogs.com)

  2. 反向代理:客户端正常访问,但实际访问的是代理,由代理向真实的服务器发起请求,用来隐藏服务器 IP 及端口,反向代理是 Web 系统最为常见的一种部署方式,如Nginx中间件

这种代理的本质是 “中间人”,而HTTPS的机制可以抵抗中间人攻击(这部分参考另一篇博客TLS协商过程 - Leo’s Blog),代理服务器无法与客户端建立TLS连接,所以不能用作HTTPS代理

隧道代理

HTTP 客户端通过 CONNECT 方法请求隧道代理创建一条到达任意目的服务器和端口的 TCP 连接,并对客户端和服务器之间的后继数据进行盲转发

假设通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接,只需要提供服务器域名及端口即可,并不需要具体的资源路径;TCP 连接建好后,代理服务器向客户端发送Connection Established报文,浏览器收到响应报文后直接向TCP连接写入数据,就好像建立了一个看不到内部的隧道

这种代理,理论上适用于任意基于 TCP 的应用层协议,比如HTTPS 网站使用的 TLS 协议

对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的,使用wireshake抓包会发现是交换的是加密后的数据,代理服务器没有密钥,无法解密

中间人代理

这里还存在另外一种情况,如果代理服务器是客户端受信任的,比如我们自己的代理服务器,可以采用代理服务器代替客户端进行TLS认证的过程,当然,如果代理服务器不是受信任的,就会有数据泄露的风险,具体流程如下图

image-20231125151019879

代理服务器的证书可以采用自己签发证书的办法,即本地使用openssl工具生成证书,然后手动添加到本地受信任根证书列表中

Socks代理原理

Socks协议

什么是Socks协议

Socks协议是一种代理 (Proxy) 协议,主要用于客户端与外网服务器之间通讯的中间传递,Socks是“SOCKetS”的缩写,最新的版本为5,即Socks5

Socks协议的作用

通常组织内部的专用网络与Internet是隔离的,代理服务器创建一个从内网到外网的通道,用于组织内的主机与组织外的主机进行通信,这样代理服务器实际上相当于一个应用层网关,控制客户端访问外网的资格。

在 Socks 协议之前没有一套通用的标准,需要根据实际需求实现多种应用层协议的网关,如 HTTP 代理、FTP 代理等,Socks 5 协议工作在传输层与应用层中间,提供了一种对应用层协议透明的代理服务,例如地址分别为A、B的两个进程进行 HTTP 通信, 其中 C 为代理服务器,实际的数据链路为 A → C → B, 但在引入 Socks 协议之后, 从应用层的视角来看,整个通信过程仍然是 A → B 的模式

Bill希望通过互联网与Jane沟通,但他们的网络之间存在一个防火墙,Bill不能直接与Jane沟通。所以,Bill连接到他的网络上的SOCKS代理,告知它他想要与Jane创建连接;SOCKS代理打开一个能穿过防火墙的连接,并促进Bill和Jane之间的通信

Bill希望从Jane的Web服务器下载一个网页。Bill不能直接连接到Jane的服务器,因为在他的网络上设置了防火墙。为了与该服务器通信,Bill连接到其网络的HTTP代理,网页浏览器会发送一个标准的HTTP请求头。HTTP代理连接到Jane的服务器,然后将Jane的服务器返回的任何数据传回Bill

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
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

一、客户端认证请求
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1~255 |
+----+----------+----------+
二、服务端回应认证
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 |
+----+--------+
三、客户端连接请求(连接目的网络)
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 1 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
四、服务端回应连接
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 1 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+

*数字代表字节数
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

具体实现代码如下:

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import sys
import select
import socket
import struct
from socketserver import StreamRequestHandler, ThreadingTCPServer

from sokt_control.oplist import Opblklist

SOCKS_VERSION = 5 # socks版本
HOST = "127.0.0.1"
PORT = 8081

class SocksProxy(StreamRequestHandler):
def handle(self):

"""
一、客户端认证请求
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1~255 |
+----+----------+----------+
"""
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, 'SOCKS版本错误'
assert Nmethods > 0
# 接受支持的方法
# 无需认证:0x00 用户名密码认证:0x02
methods = self.get_available_methods(Nmethods)
# 检查是否支持该方式,不支持则断开连接
if 0 not in set(methods):
self.server.close_request(self.request)
return


"""
二、服务端回应认证
+----+--------+
|VER | METHOD |
+----+--------+
| 1 | 1 |
+----+--------+
"""
# 发送协商响应数据包
self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0))
# 校验用户名和密码
# if not self.VerifyAuth():
# return


"""
三、客户端连接请求(连接目的网络)
+----+-----+-------+------+----------+----------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 1 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
"""
version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))
assert version == SOCKS_VERSION, 'socks版本错误'
if address_type == 1: # IPv4
# 转换IPV4地址字符串(xxx.xxx.xxx.xxx)成为32位打包的二进制格式(长度为4个字节的二进制字符串)
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)
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]

# 获取IP
print("src_host: ", self.client_address[0])
dsthost = address.decode("UTF-8")
if(port!=80):
dsthost = dsthost+":"+str(port)
# 去掉协议头
split_url = dsthost.split("/", 2) # 将URL按照斜杠分割成最多2个部分
if len(split_url) > 2:
dsthost = split_url[2]
print("dst_host: ", dsthost)
# 获取黑名单
opblklist = Opblklist('./sokt_control/blklist.txt')
blklist = opblklist.show()

if dsthost in blklist:
print("dst_host:{} is in blklist, conn is not supported!".format(dsthost))
else:

"""
四、服务端回应连接
+----+-----+-------+------+----------+----------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1 | 1 | 1 | 1 | Variable | 2 |
+----+-----+-------+------+----------+----------+
"""
# 响应,只支持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:
# 响应拒绝连接的错误
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)

return

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 VerifyAuth(self):
"""
校验用户名和密码
"""
version = ord(self.connection.recv(1))
# assert version == 1
assert version == SOCKS_VERSION, 'SOCKS版本错误'
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')
self.username = "leo"
self.password = "1234"
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 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
# cache.append(data) # 将响应数据添加到缓存中
if remote in r:
data = remote.recv(4096)
if client.send(data) <= 0:
break
# if client in r:
# data = client.recv(4096)
# if not cache: # 如果缓存为空,则向远程服务器请求数据
# if remote.send(data) <= 0:
# break
# else: # 如果缓存不为空,则直接从缓存中获取响应数据
# response = cache.pop(0) # 获取缓存中的第一个响应数据
# if client.send(response) <= 0:
# break


def my_socks5_proxy_server():
# 使用socketserver库的多线程服务器ThreadingTCPServer启动代理
with ThreadingTCPServer((HOST, PORT), SocksProxy) as server:
print("SOCKS5 proxy server is running on {} : {} ...".format(HOST, PORT))
server.serve_forever()


if __name__ == '__main__':
if sys.argv[1:]:
PORT = int(sys.argv[1])
my_socks5_proxy_server()

本着互联网开源的性质,欢迎分享这篇文章,以帮助到更多的人,谢谢!