最近做基于 Iptables REDIRECT 的透明代理方案时遇到一个问题,解决的过程中涉及到几个重要的网络相关知识点,记录在这里以便查找。
网上有很多种透明代理的解决方案,使用最多的是通过 libev 或者其他的 ss 版本与 Iptables、TProxy 等等技术结合进行搭建,如果对速度要求高的话,还需要加上黑白名单功能进行分流。( 如果需要命令行代理的话推荐 proxychains ) 透明代理一般分为客户端和服务端,客户端安装在用户机器上,配置好后不用再对其他进程做额外配置,服务端则常运行在 VPS 上,已经有很多成熟的软件,比如 http 代理 squid,socks 代理 ss-server 等等。 项目的代理服务端已经配好了(squid),而透明代理的客户端需要自动截取用户指定端口/协议的流量,这里就遇到第一个问题,如何对指定端口的流量进行自动转发。
对数据包的自动转发可以使用 Iptables 的重定向机制,通过如下命令实现。这条命令实际上是进行一个端口映射,将 80 端口的数据包重定向到 8118 端口。
iptables -t nat -A OUTPUT -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 8118
但要注意的是这条命令中还指定了其他参数,OUTPUT 指的是 Iptables 的 OUTPUT 链,它截获并处理的是所有从这台机器上发出去的所有数据包 (客户端形式);还有一种做法是设置 PREROUTING 链,只处理进入当前主机的数据包,但这样截获不到本机进程发送的包 (网关形式) 。具体应该选择哪种做法,需要自己深入理解 Iptables 四表五链的原理后确定。
如果透明代理客户端程序和用户进程在同一台主机上,Iptables 转发的目标端口就是客户端程序所在的端口,这时会遇到第二个问题,如何获取用户进程数据包的源地址和端口。
Iptables 重定向的策略很简单,直接修改需要重定向的数据包的五元组 (协议, 本地地址, 本地端口, 远端地址, 远端端口),将远端地址和远端端口改为重定向目标的地址和端口。但这样会导致客户端找不到原本的目标地址和端口,以致无法告诉代理服务器需要代理连接哪个目标。 因此借用了 ss-redir 中的一个方法,其主要代码如下所示。getdestaddr 函数通过连接跟踪机制,查询 Iptables 修改之前的源地址和端口,并存在 sockaddr_storage 结构体中。需要注意的是此方法只支持 TCP 协议的查找,不支持 UDP,这篇文章 对原因进行了分析。
int getdestaddr(int fd, struct sockaddr_storage *destaddr)
{
socklen_t socklen = sizeof(*destaddr);
int error = 0;
error = getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, destaddr, &socklen);
if (error) {
error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen);
if (error) {
return -1;
}
}
return 0;
}
sockaddr_storage 中的 IP 地址和端口号并不能直接使用,还要经过如下的转化,之前自己试了很久,直接把这段代码贴出来以防有人碰到同样的问题。
char * original_ip;
int original_port = 0;
original_ip = inet_ntoa(((struct sockaddr_in*)((struct sockaddr*)&destaddr))->sin_addr);
original_port = ntohs(((struct sockaddr_in*)((struct sockaddr*)&destaddr))->sin_port);
解决了主要的问题之后,剩下的功能就比较常见了。代理客户端一方面作为服务器接收用户进程的数据包,一方面作为客户端向代理服务器发送数据包,反之亦然。代理客户端可能同时需要接收很多个连接请求,需要一个性能良好的事件库保证不会崩溃,这里推荐 ss-libev 用的事件库 libev,上手简单且性能不错,适合搭建类似的代理程序。
除此之外,为了功能完备性,最好加上黑白名单,以及在设置 Iptables 重定向时过滤内网地址,保证不会拦截不需要的流量等等。
ps : 虽然不能贴源码,但如果有人想看的话后续可以把实现的思路和框架讲一遍,基本模仿 ss 的 python/C++ 版本实现。
本文为博主原创,转载请注明出处