什么是透明代理以及如何实现一个透明代理
前言
透明代理是一个既实用又有趣的概念。
客户端无需任何配置这一点让它有非常大的发挥空间,成为多设备管理,访问控制甚至安全防护,负载均衡等场景的利器。
然而,多数人只知其名,却不解其运作机制。本文会讲解这个概念,然后使用Linux的tproxy模块功能,实现一个最简单的透明代理。
什么是透明代理
首先,透明代理是网络代理技术的一种。 之所以称它“透明”,是因为客户端设备不知道自己的流量被代理了。
我们知道在计算机网络中,数据的传递是分层的。客户端发出数据之后并不关心数据包如何到达目标服务器。
实际上,在客户端发出数据之后,在数据包离开我们的路由器之前,中间还有许多环节我们可以控制和干预。只要我们中途拦截需要的数据包,并且把它转发到代理服务器,我们就实现“透明代理”。
Linux的透明代理支持
了解了透明代理的概念之后,可以想象,实现透明代理的方式是很多的。
网上能找到如何利用第三方软件在Windows、安卓系统上实现透明代理的教程,可以自行了解。本文重点讨论Linux下的透明代理内核模块。是的,Linux下甚至有一个内核模块叫做TProxy,专门用来支持透明代理。
不过在进入TProxy模块之前我们先来了解了一下传统的用iptables的REDIRECT语句来做到透明代理的方式。就是说哪怕没有TProxy这个模块,linux下也是可以实现透明代理的。
我们的工具是iptables。它是一个命令行工具,能让你对Linux内核处理数据包的方式进行一些定制。我们使用这个工具做以下操作:
- 找出需要代理的数据包(需要了解Netfileter,才能在正确的地方找到需要数据包,不过本文暂不涉及);
- 把这些数据包转发到代理服务器即可。
这里举个例子,把去53端口的udp流量转发到运行在本地的1080端口的代理服务器:
iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 1080
一条命令搞定。对命令行不熟悉的朋友可能会觉得头大,但是仔细观察这条命令,就会发现除了语法不一样,它和上面的中文描述几乎是一样的,应该非常好理解。
等等,上边的语句好像并不能正常工作!Linux内核文档中说这种实现透明代理有严重的缺陷,说这么做实际上改变了数据包的目标IP地址,导致UDP流量失去了原始目标地址,就算是TCP工作方式也不太正常。
怎么回事呢?
原来UDP数据包就是非常简单地在ip数据包的基础上加上了端口信息就发出去了。想要让本来去目标服务器的数据包去往代理服务器,就得改掉ip数据包中的目标地址。但是把目标地址都改成代理服务器的地址以后,代理服务器以为这个数据包是发给自己的,如果它本意是要转发这个包,现在就不知道该怎么处理了。TCP数据包因为存在握手机制,哪怕修改了TCP数据包,目标IP地址依然可以通过SO_ORIGINAL_DST获取到,从而避免了这个问题。(v2ray官网上,白话文文档中说到:“由于对 iptables 不熟,我总感觉上面对 UDP 流量的透明代理的设置使用上有点问题,知道为什么的朋友请反馈一下。如果你只是简单的上上网看看视频等,可以只代理 TCP 流量,不设 UDP 透明代理”,这就是原因。)
要解决上述问题,就终于要用到我们要讲的TProxy模块了,接下来我们就直接实践,从实践中学习它的工作原理。
TPROXY方式
要使用这种方式,需要多个组件的支持:
- 需要Linux内核的支持。比如我使用的是openwrt,需要安装kmod-nf-tproxy。
- 需要工具的支持。新版的openwrt使用nftables 而不是iptables。不懂也不要害怕,我也是从零开始学习了nftables的基本用法。再说网上iptables的资料已经够多了,我们就用点不一样的吧。想要nftables支持tproxy语句还需要安装kmod-nft-tproxy。
- 需要配合路由表使用,后面会配置。
- 需要你的代理服务器支持Tproxy(完全可以自己实现一个,但是没有必要,对吧?)。
准备好这些,接下来我们尝试使用Tpoxy的方式实现前面例子。
写一个nftables脚本:
#!/usr/sbin/nft -f
table inet proxy{
chain output {
type route hook output priority filter; policy accept;
udp dport 53 meta set mark 1
}
}
虽然上面这么大一坨,看着很复杂,最关键的就这一句udp dport 53 meta set mark 1
,它只做了一件非常简单的事儿,翻译成人话就是“把去往53端口的UDP流量做上标记‘1’。” 目的是跟其他的流量加以区分。(其他部分是关于Netfilter和Nftables的语法,这些可以自行了解)
运行这个脚本 chmod a+x script && ./script
。
然后我们需要路由表的配合,让这些明显不是去往本地的流量也能进入本地(很关键), 运行下面的命令:
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
先补充一个重要的背景知识:Linux中有0-255,共256张路由表。其中0,253,254,255号路由表是特殊路由表,默认已经被配置好了别名。如果不指定使用哪一张表就会默认使用254号表,也叫main。可以用cat cat /etc/iproute2/rt_tables
命令查看表的别名, 也可以修改这个文件,给自定义的表添加一个别名。
上面的语句首先增加一个策略把所有被标记过的的数据包都送到100号路由表。(为什么网上所有的教程都说把标记过的流量放到100号表,你应该能猜到100号并没有啥特别的,就是简单好记,Linux内核文档说明中也使用100号路由表作为例子。)
然后我们给100号路由表添加一条路由规则。观察第二条命令 local 0.0.0.0/0 dev lo
, 这条命令告诉内核这些所有的地址都属于本地地址,通过lo接口处理这些数据。
这样做,我们就让原本原本要离开的数据包重新进入本地了。
接下来我们把标记过的的数据挑出来,然后再通过TProxy模块导入到代理服务器,脚本如下:
table inet proxy{
chain prerouting{
type filter hook output priority mangle; policy accept;
#注意下面这句话
udp dport 53 meta mark 1 tproxy to :1080
}
}
上面语句的意思是:“把去往53端口的udp流量,并且有‘1’标记的,转发到1080端口”。
这样,我的们目的就达成了!
以上是本地发出的数据我们需要两个链配合才能完成。如果是进入本地数据, 只需要一条语句,把标记和导入到代理同时完成:
udp dprot 53 mark set 1 tproxy to :1080
结束语
上面是一个非常简单的例子。如果你已经看懂了,那么相信更复杂一点的操作也难不倒你。在实际使用的时候可能并不是只拦截去往某个端口或者某个IP地址的数据包,而是大范围拦截,这时候一定要记得放行透明代理发出的数据包,否则数据包刚离开代理服务器又被拦截回来了,只能在本地回环,永远也发不出去了。关于放行的方法,实际上有多种选择,常见的思路是放行目标IP、给透明代理发出的数据包做上不同的标记或者把代理运行在不同的用户下面加以区分。灵活运用Nftables即可。
参考链接:
https://docs.kernel.org/networking/tproxy.html
https://powerdns.org/tproxydoc/tproxy.md.html
https://xtls.github.io/document/level-2/transparent_proxy/transparent_proxy.html
https://xtls.github.io/document/level-2/tproxy.html
https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation
https://man7.org/linux/man-pages/man8/ip-route.8.html
http://git.netfilter.org/nftables/commit/?id=2be1d52644cf77bb2634fb504a265da480c5e901