PPCA-Network 代理折腾浅记
在 2023 年暑假的PPCA中,我与7位ACM班同学一起参加了 Network组,收获颇丰。 特写此文,记录为期六周的学习成果。
我们的基础任务是 Socks5 协议服务端, 此后有若干选题,写6分可以得到PPCA的100%分数,写9分可以得到110%的分数。
- 透明代理 4’
- 代理客户端 1’
- 分流规则
- 按 socks 地址分流 1’
- 按域名分流
- HTTP 分流 1’
- TLS 分流 1’
- 按程序分流 1’
- 多级代理 2’
- 分流规则
- UDP 代理 1’
- TLS 劫持 2’
- HTTP 捕获/修改/重放 3’
- 反向代理
- TCP 反向代理 1’
- HTTP 反向代理 1’
由于不能使用c系列语言的要求,以及考虑到网络对于并行处理的需要,我选择了使用 go 语言。 如果能使用c++的话, 我或许会考虑使用epoll模型, 可惜不行, 我又懒得拿go去写如此贴近系统的事情(贴近底层为什么不用c呢...)。 事实上很多时候我都是先拿c++写个demo再换成go...
Socks5 : tcp 服务端
比较简单,照着 RFC1928 写就好。这个时候的主要难点在于学习 go 语言。
文档有中文翻译。
socks5 可以代理 tcp, udp 和反向tcp代理。
代理tcp只是在tcp字节流前面加了一个包头。 客户端向代理服务端发送自己想连谁, 然后服务端自己去连接, 连接好后,服务端直接交换tcp的字节流, 此后客户端相当于直接与远端通信。
比较坑的点是tcp可能有分包和粘包。
Socks5 : udp 服务端
udp中有些部分在 RFC 文档中语焉不详。
首先客户端通过tcp告诉代理需要建立udp, 同时告诉代理自己以后发送udp时会使用什么端口。
然后服务端需要开始监听两个udp端口。 这里就体现出了udp是无连接的性质。 监听之后,可以直接收数据或向某个地址发送数据。 tcp是做不到这一点的,必须有 accept 或 connect 的过程来建立连接。
这里有一个有意思的问题。 我们知道,比如tcp connect 时,内核会帮我们找到一个可用的端口,udp也是如此。 那么,如何在发送数据前知道自己发送数据所使用的端口? 我这才发现原来 connect 或者 sendto 之前,也是可以调用 bind 的。 对于udp而言,如果 bind 时指定的端口为 0,仍是由系统分配端口,但是可以起到固定发送端口的作用。
一个坑点是代理服务器收到远端的回信时,应该将该信息转发给客户端。 在转发时,需要加上一个报头。 在这个报头中,填入的地址是远端的地址。 文档上对这点没有说清楚,不过想一想这样也是很自然的, 这样客户端就可以在同一个代理通道内, 对多个远端地址发送udp信息,同时收取他们的信息, 收取时为了知道是谁发来的,自然需要代理服务端告诉自己远端的地址。
Socks5: tcp 客户端
tcp连接后发送下报头就可以了。
透明代理
我早先是研究过透明代理的,只是一直没有搞明白,也没配成功。
正好开始写网络时,我原来使用的前端 qv2ray 滚挂了, 换成了 v2raya, 正好发现 v2raya 在linux上支持透明代理(但是不支持系统代理qwq)。
言归正传,目前linux上的透明代理有两个实现方式。
- tproxy模式: 路由表 + 防火墙 + 代理软件
- tun模式: 路由表 + tun设备 + 代理软件
还有一个 redirect 方式,它和tproxy模式相似,但是不支持udp。
在PPCA中,对于两个方法都有所探索, 也都写过一些代码, 但只实现了一个鲁棒性不足的 tun设备代理。
Tproxy 模式
这一模式的需要写的代码会相对较少,但需要探索的内容比较多。
tproxy模式的基本原理是:
启用内核模块 nft_tproxy, 防火墙能将tcp/udp数据包转发给某个指定的地址,而保持数据包的来源地址和目的地址,使得可以通过一些方式查询。
代理程序监听某地址,它会直接收到tcp连接或udp报文。 通过系统调用获取原本的目标地址后, 进行代理或直连。
需要路由表配合,才能让数据包被防火墙转发以及防止回环。
具体还有一些细节问题:
- 代理程序监听时设置相关选项需要相应权限。
- iptables / nftables 学习成本较高。
- 通过判断 mark 或 gid 等方式标记代理程序发出的数据,防止回环。
参考:
Tun模式
tun/tap 是 linux 上的虚拟网络设备。
tun模式的原理是:
代理程序打开某个tun设备,读取的数据是原始ip数据包。
对于直连,可以直接通过原始套接字原样发出。
对于非直连,拆包后走代理发出。
路由表配合
这里也有一些问题:
- 对于tcp流量,需要手动管理连接!! 比如三次握手四次挥手,比如回复ACK,由程序自己完成。 要手写或者用别人的库。
当然,由于在同一台电脑上,我们可以假设不会有丢包的事情发生……