1 什么是代理服务器
代理服务器就是在请求资源的客户端和提供该资源的服务器之间的中介, 向服务器转发请求并将响应返回给客户端。 它可以提高流程中的隐私性、安全性和性能。
1.1 正向代理
位于客户端和目标服务器之间。客户端向代理发送请求,代理服务器接收请求后,代表客户端向目标服务器发起请求,然后将获得的响应返回给客户端。正向代理主要是服务于客户端,用于帮助客户端访问无法直接到达的资源,或者为了提高安全性和隐私性,隐藏客户端的身份,它对客户端是透明的,即服务器不知道背后有代理存在,只认为请求直接来自客户端。如下图所示:
应用场景:
- 绕过访问限制或内容过滤(例如,突破地理限制访问国外网站);
- 提高访问速度,通过代理服务器缓存常用资源;
- 网络监控和访问控制;
- 用户隐私保护,隐藏用户真实IP地址。
1.2 反向代理
对外表现为一个(或一组)服务器的接口,客户端向反向代理发起请求,反向代理决定将请求转发至内部网络中的哪个服务器,然后将获得的响应返回给客户端,对客户端是透明的,客户端认为所有请求都直接响应自一个服务器。反向代理主要服务于服务器端,通过接收来自互联网的请求并将其转发到内部网络的服务器上,反向代理提高了内部服务器的安全性、负载均衡和高可用性。
因为反向代理更靠近 Web 服务器,并且只为一组有限的网站提供服务。安装反向代理服务有几个原因:
- 加密/SSL 加速:创建安全网站时,安全套接字层(SSL) 加密通常不是由 Web 服务器本身完成的,而是由配备 SSL 加速硬件的反向代理完成的。此外,主机可以提供单个“SSL 代理”,为任意数量的主机提供 SSL 加密,从而无需为每个主机提供单独的 SSL 服务器证书,但缺点是 SSL 代理后面的所有主机都必须共享用于 SSL 连接的公共 DNS 名称或 IP 地址。使用X.509证书的SubjectAltName功能或TLS的SNI 扩展可以部分解决此问题。https://en.wikipedia.org/wiki/X.509https://en.wikipedia.org/wiki/Server_Name_Indicationhttps://en.wikipedia.org/wiki/Transport_Layer_Security
- 负载平衡:反向代理可以将负载分配到多个 Web 服务器,每个服务器服务于自己的应用领域。在这种情况下,反向代理可能需要重写每个网页中的URL (从外部已知的 URL 转换为内部位置)。
- 提供/缓存静态内容:反向代理可以通过缓存图片和其他静态图形内容等静态内容来减轻 Web 服务器的负担。
- 压缩:代理服务器可以优化和压缩内容以加快加载时间。
- 填鸭式:通过缓存 Web 服务器发送的内容并慢慢地“填鸭式”地将其提供给客户端,减少 Web 服务器上客户端速度慢造成的资源占用。这对动态生成的页面尤其有益。
- 安全性:代理服务器是额外的防御层,可以防御某些针对操作系统和 Web 服务器的攻击。但是,它无法防御针对 Web 应用程序或服务本身的攻击,而这通常被认为是更大的威胁。
- 外联网发布:面向互联网的反向代理服务器可用于与组织内部的防火墙服务器进行通信,从而提供对某些功能的外联网访问,同时将服务器保留在防火墙后面。如果以这种方式使用,则应考虑采取安全措施来保护其余基础设施,以防此服务器受到威胁,因为其 Web 应用程序会受到来自互联网的攻击。
1.3 总结
反向代理主要保护服务器,而正向代理保护客户端。
2 用来监控或者转发数据包三种方式
2.1 系统代理
大多数操作系统都允许用户设置全局或特定协议的代理服务器(例如HTTP、HTTPS、SOCKS代理)。系统代理通常工作在应用层,可对特定类型的流量进行拦截和转发。它适用于简单的网络请求代理和内容过滤场景。但是,系统代理通常无法处理非代理协议,例如复杂的应用层协议或直接使用TCP/UDP的通讯。
2.2 虚拟网卡
虚拟网卡涉及到网络层和数据链路层。虚拟网卡是模拟的网络接口,可以处理通过它的所有IP网络层数据包,因此它能够在更低层次上监控和控制网络流量。
2.3 Socket Hook
Socket Hook可以作用于不同层次,但主要是在传输层(尤指对TCP/UDP协议的处理)。通过钩子函数篡改Socket的正常工作流程,Socket Hook能够拦截、修改或监视通过操作系统Sockets API的网络流量。
2.4 VPN
VPN的隧道协议主要有三种,PPTP,L2TP和IPSec,其中PPTP和L2TP协议工作在OSI模型的第二层,又称为二层隧道协议;IPSec是第三层隧道协议,也是最常见的协议。L2TP和IPSec配合使用是目前性能最好,应用最广泛的一种。
3 系统代理的具体实现
3.1 HTTP代理
- 客户端(浏览器或应用程序)发起HTTP请求:在这一步,客户端不直接对请求的域名进行DNS解析。相反,流量被强制由操作系统转发到配置的代理服务器(Proxy)。因为我们要经过proxy进行流量的转发,所以此时我们只需要解析对Proxy进行DNS解析即可。代理服务器可能位于本地(例如127.0.0.1回环地址)或者外网。客户端需要知道代理服务器的IP地址来建立一个socket通道。这需要客户端对代理服务器的域名进行DNS解析,除非代理服务器地址已经是直接提供的IP地址。
- 建立Socket通道:基于TCP协议的三次握手机制,客户端和代理服务器之间建立起一个虚拟的Socket通道。为了支持数据的发送和接收,各自生成了Socket套接字。
- 代理服务器处理数据:代理接收到所有客户端原本想要发送给目标服务器的数据包。此时,代理服务器可以对这些数据包进行各种内部处理。例如,数据包处理:代理服务器可以读取和修改经过的HTTP请求和响应。因为HTTP协议是基于明文的,代理服务器能够检查请求的内容、URL、头部等信息;过滤和分流:代理可以根据请求的URL或内容实施不同的策略。比如,它可以拦截和阻止特定的广告域名请求,对这些请求直接返回错误响应(如404);对于白名单内的域名,则可以进行正常的请求转发;智能路由:基于请求的目标和代理服务器的网络位置,代理可以决定最佳的请求路径。例如,对于无法直接访问的外网资源,代理可以执行二次转发或通过特定路线访问资源。
- 代理服务器与客户端间的数据交换:代理服务器处理完成后,会向其Socket(socket_proxy)写入处理后的数据。客户端的Socket(socket_client)接着读取这些数据,完成了从客户端发起请求到获取代理处理后的响应的整个过程。
HTTP代理的核心功能是在客户端和目标服务器之间充当中间人,对客户端的请求进行拦截、处理和转发,同时也可以对返回的数据进行处理后再发送给客户端。这不仅可以用于缓存、过滤内容和增强隐私,也可以用于监控和审计网络流量。
下面是由GPT生成的简易代码:
import socket
import threading
def handle_client(client_socket):
# 接收客户端请求数据
request_data = client_socket.recv(1024)
# 解析请求行,获取请求方法、URL 和 HTTP 版本号
request_line = request_data.split(b'\r\n')[0]
method, url, http_version = request_line.split(b' ')
# 构造代理服务器请求地址
proxy_url = b'http://' + url
# 发送代理服务器请求
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
# 解析代理服务器地址
proxy_host = proxy_url.split(b'//')[1].split(b'/')[0]
proxy_port = 80
# 建立连接并发送请求数据
proxy_socket.connect((proxy_host, proxy_port))
proxy_socket.sendall(request_data)
# 接收代理服务器响应并发送给客户端
while True:
response_data = proxy_socket.recv(1024)
if not response_data:
break
client_socket.sendall(response_data)
def run_server():
# 创建套接字并绑定到本地地址和端口
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8888))
server_socket.listen(5)
print('Proxy server is running on port 8888...')
while True:
# 接受客户端连接并创建线程处理请求
client_socket, address = server_socket.accept()
print(f'Request from {address}')
t = threading.Thread(target=handle_client, args=(client_socket,))
t.start()
if __name__ == '__main__':
run_server()
3.2 HTTPS代理
我们在上面3.1HTTP代理中了解到了,我们需要知道目标域名才能够做上面提到的转发过滤等工作。但是HTTPS是加密的,我们无法得知要client和服务器直接的数据内容。可是不知道数据包内容,我们就不知道目标服务器的域名。所以我们做HTTPS代理的难点就在于如何得知目标服务器的域名。所以我们需要有个HTTP CONNECT的过程来获取域名。
- HTTP CONNECT:当客户端需要通过HTTPS与远程服务器建立安全连接时,它会向代理服务器发送一个CONNECT请求。这个请求包含了目的服务器的域名和端口,例如CONNECT www.example.com:443 HTTP/1.1。这一步是明文的,使得代理服务器可以知道客户端尝试连接的目标域名,而不必解密通信内容。
- 域名的使用:代理收到CONNECT请求后,会根据提供的域名进行必要的处理。这些处理可能包括基于域名的过滤、流量分流或是其他的规则执行,例如根据访问策略拦截某些请求或者进行路由优化。
- 隧道建立:一旦代理服务器根据域名完成了必要的处理并决定允许连接,它会在自己和目标服务器之间建立一个TCP连接。然后,代理服务器对客户端回应,表明隧道已经建立(例如发送HTTP/1.1 200 Connection Established响应)。此时,客户端与目标服务器之间的所有通信都将通过这个加密隧道进行,代理服务器将不再介入数据的加密或解密过程,只是简单地转发加密的数据包。
HTTPS 代理整体流程和 HTTP 没有变化,HTTPS代理的关键在于利用CONNECT方法建立一个加密隧道,通过这个隧道安全地转发客户端与服务器之间的加密通信。其中,代理服务器的角色主要是隧道的建立和维护,以及数据的转发,而不参与加密解密的过程。这种方式既保证了数据的安全传输,又允许代理服务器在不知道具体内容的情况下实施基于域名的访问控制和流量管理。
下面是由GPT生成的简易代码:
import socket
import ssl
import threading
def handle_client(client_socket):
# 接收客户端请求数据
request_data = client_socket.recv(1024)
# 解析请求行,获取请求方法、URL 和 HTTP 版本号
request_line = request_data.split(b'\r\n')[0]
method, url, http_version = request_line.split(b' ')
if method == b'CONNECT':
# 解析请求行,获取请求方法、目标主机和端口号
_, target_host, target_port, _ = url.split(b':') + [b'']
target_port = int(target_port)
# 建立与目标主机的加密连接
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
proxy_socket.connect((target_host, target_port))
proxy_socket = ssl.wrap_socket(proxy_socket, server_side=False)
# 响应客户端 CONNECT 请求
response_data = b'HTTP/1.1 200 Connection Established\r\n\r\n'
client_socket.sendall(response_data)
# 交换数据
while True:
data = client_socket.recv(1024)
if not data:
break
proxy_socket.sendall(data)
response_data = proxy_socket.recv(1024)
client_socket.sendall(response_data)
else:
# 构造代理服务器请求地址
proxy_url = b'http://' + url
# 发送代理服务器请求
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
# 解析代理服务器地址
proxy_host = proxy_url.split(b'//')[1].split(b'/')[0]
proxy_port = 80
# 建立连接并发送请求数据
proxy_socket.connect((proxy_host, proxy_port))
proxy_socket.sendall(request_data)
# 接收代理服务器响应并发送给客户端
while True:
response_data = proxy_socket.recv(1024)
if not response_data:
break
client_socket.sendall(response_data)
def run_server():
# 创建套接字并绑定到本地地址和端口
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8888))
server_socket.listen(5)
print('Proxy server is running on port 8888...')
while True:
# 接受客户端连接并创建线程处理请求
client_socket, address = server_socket.accept()
print(f'Request from {address}')
t = threading.Thread(target=handle_client, args=(client_socket,))
t.start()
if __name__ == '__main__':
run_server()
3.3 SOCKS5
SOCKS5本身是一套协议,非应用层协议,而是一种系统代理传输协议,即操作系统如何将 socket 数据包给到 proxy 的协议。SOCKS5不仅仅局限于HTTP或HTTPS流量,它可以代理任何基于TCP的协议的网络连接,甚至支持UDP协议,使其适用于多种网络请求和服务。
- 认证:客户端连接至SOCKS5代理服务器,根据配置进行认证。可能包括无认证、用户名密码认证等方式。
- 请求:认证通过后,客户端发送一个连接请求,其中包括目标服务器的地址和端口,请求类型(TCP连接、TCP绑定、UDP)等信息。
- 代理连接:SOCKS5代理服务器根据请求建立到目标服务器的连接。如果是域名,由代理服务器进行DNS解析。
- 数据转发:一旦代理服务器与目标服务器的连接建立,客户端与目标服务器间的所有数据都将通过代理服务器进行转发。
- 断开连接:请求完成后,任一方面结束连接,代理服务器相应地关闭客户端和服务器之间的连接。
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为网络字节序表示的服务器绑定的端口
我们肯定还是要拿到目标服务器的域名,在HTTPS中是通过HTTP CONNECT获得,而SOCKS5中我们需要通过字节偏移获得(DST.ADDR)。
下面是由GPT生成的简易代码:
import socket
import threading
def handle_client(client_socket):
# 接收客户端连接请求
data = client_socket.recv(1024)
# 发送协商响应,仅支持无认证方式
response = b"\x05\x00"
client_socket.sendall(response)
# 接收客户端连接请求,解析目标主机和端口号
data = client_socket.recv(1024)
mode = data[1]
if mode == 1: # CONNECT
addrtype = data[3]
if addrtype == 1: # IPv4
addr = socket.inet_ntoa(data[4:8])
port = int.from_bytes(data[8:], byteorder='big')
elif addrtype == 3: # 域名
addrlen = data[4]
addr = data[5:5+addrlen].decode()
port = int.from_bytes(data[5+addrlen:], byteorder='big')
else:
client_socket.close()
return
# 建立与目标主机的连接,并响应客户端连接请求
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as proxy_socket:
proxy_socket.connect((addr, port))
response = b"\x05\x00\x00\x01"
response += socket.inet_aton('0.0.0.0') + (0).to_bytes(2, byteorder='big')
client_socket.sendall(response)
# 交换数据
while True:
data = client_socket.recv(1024)
if not data:
break
proxy_socket.sendall(data)
response_data = proxy_socket.recv(1024)
client_socket.sendall(response_data)
else:
client_socket.close()
def run_server():
# 创建套接字并绑定到本地地址和端口
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 8888))
server_socket.listen(5)
print('SOCKS5 proxy server is running on port 8888...')
while True:
# 接受客户端连接并创建线程处理请求
client_socket, address = server_socket.accept()
print(f'Request from {address}')
t = threading.Thread(target=handle_client, args=(client_socket,))
t.start()
if __name__ == '__main__':
run_server()
4 根据匿名程度分
4.1 透明代理
透明代理,顾名思义,对于客户端和服务器来说是“透明”的,即用户和服务端都能知道对方的真实IP地址。透明代理不会修改或隐藏用户的IP地址。当请求通过透明代理时,它会在HTTP头部中添加或保留原始IP信息(比如,通过X-Forwarded-For字段)。这种代理常用于公司或教育机构网络中,用于内容缓存、网站过滤、带宽管理等,而不是为了保护用户的隐私。
特点
- 不修改用户的真实IP地址。
- 服务器能够获取到用户的真实IP地址。
- 主要用于内部网络管理和优化网络性能。
4.2 匿名代理
匿名代理则不同,它的主要目的是隐藏用户的真实IP地址,增强用户在互联网上的匿名性。通过匿名代理服务器,用户的请求会首先发到代理服务器,然后由代理服务器转发给目标服务器。在这个过程中,代理服务器不会将用户的真实IP地址透露给目标服务器。在某些匿名代理服务中,甚至可以删除或修改HTTP头部中关于用户信息的字段,如用户代理信息,进一步提高匿名性。
特点
- 隐藏用户的真实IP地址,对外显示为代理服务器的IP地址。
- 提供更高程度的隐私保护。
- 用于绕过地理位置限制、网络审查,或保护用户在线活动不被跟踪。
5 虚拟网卡
虚拟网卡主要是通过接管 IP 网络层数据包实现转发。因为虚拟网卡工作在 IP 网络层,且DNS域名解析发生在他的上一层,所以我们如果像系统代理那样通过获取域名是走不通的,此时我们只能获取到ip地址,但是为了要获取到域名,我们就需要主动对域名进行捕获。Surge 的做法是捕获到 DNS 解析的数据包后,强制返回 198.18 网段的内网 ip,并将内网 ip 和域名做映射(这就是文章开头说到的,dig 本网站 ip 是 198 的困惑)。这样 client 以为 google 的 ip 是 198.18.x,发送的数据包就会携带这个 ip。在 IP 网络层收到这个 ip 的数据包之后,就知道访问的是 google 了。这样就完成了域名捕获的工作。
6 参考资料
科学上网原理 · Issue #28 · Pines-Cheng/blog · GitHub
https://en.wikipedia.org/wiki/Proxy_server