Redsocks/ss-redir的实现
DNAT与REDIRECT模块在iptables target extension之列。
这两个模块都能根据参数,修改数据包的header中的目的IP和端口。
既然修改,那就不是附加,毕竟网络层传输层header的内容怎么可能随随便便增删呢,这意味着,数据包的真实目的地会被完完全全地抹掉!
Redsocks/ss-redir收到这样的数据包后,不做特殊处理,何以知道这数据包原来想发给谁呢?
这些程序之所以能正常工作,得益于NAT的透明性,被NAT处理的数据包,在内核中均有所记录,NAT后的正常通讯由NAT发起者维持,Redsocks/ss-redir这类程序便是通过内核中的记录得知真实目的地的。
这个原地址的获取可以通过系统调用实现:
1 2 3 4 5 |
#include <sys/types.h> #include <sys/socket.h> int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); |
optname需要传值SO_ORIGINAL_DST。
Redsocks的调用见base.c的223行:
217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
#ifdef USE_IPTABLES static int getdestaddr_iptables(int fd, const struct sockaddr_in *client, const struct sockaddr_in *bindaddr, struct sockaddr_in *destaddr) { socklen_t socklen = sizeof(*destaddr); int error; error = getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, destaddr, &socklen); if (error) { log_errno(LOG_WARNING, "getsockopt"); return -1; } return 0; } #endif |
问题所在
假定有以下的网络架构:
正常情况下,NAT与getsockopt(…, SO_ORIGINAL_DST, …)调用程序都运行在Router上,工作正常。
现在把getsockopt(…, SO_ORIGINAL_DST, …)调用程序迁移到Server 1上,很容易想到的,就是把原来的NAT规则改成DNAT的,并指定目的地IP与端口:
1 |
iptables -t nat ... -p tcp ... -j DNAT --to 192.168.1.2:GETSOCKOPT_PROGRAM_LISTEN_PORT |
NAT的记录由内核管理,这隐含的意思是,只有在发起NAT处理的内核上才能获取到NAT前的信息。
若调用远程的getsockopt(…, SO_ORIGINAL_DST, …)调用程序,即发起NAT的与运行getsockopt(…, SO_ORIGINAL_DST, …)调用程序的不是同一个内核,很明显,这些程序对getsockopt()的调用不可能实现他们的目的。
问题已很明显,这类程序无法通过getsockopt()调用取得真实目的地。
实现getsockopt()的远程调用成本有点高,不太现实。
不过,我们把方式稍作改变,让“远程”变为“本地”。
实现
假定有以下的网络架构:
Server 1上运行着getsockopt(…, SO_ORIGINAL_DST, …)调用程序。
既然处理NAT要与调用getsockopt()的使用同一个内核,那么把需要NAT的那部分也交给Server 1处理好了。
要实现这种需求,可以利用策略路由,正常的数据包按正常处理,原本需要NAT到Server 1的数据包,使用路由规则转发到Server 1。这样就不需要网内其余联网设备作任何改变,只需要对Router与Server 1进行操作。
准备
Router 1上首先把针对getsockopt(…, SO_ORIGINAL_DST, …)调用程序的NAT规则删掉。
路由表
修改/etc/iproute2/rt_tables增加一个路由表,如12行,增加一个ID为250,名为server1的路由表:
12 |
250 server1 |
往server1路由表中增加一条默认路由规则,把所有数据包都转发到Server 1:
1 |
ip route add default via 192.168.1.2 table server1 |
数据包分类
接下来需要把数据包分类,即决定哪些数据包应使用路由表server1。
我这里选择iptables的MARK模块,把所有需要给getsockopt(…, SO_ORIGINAL_DST, …)调用程序处理的数据包,都加一个固定标记,然后让路由让有该特定标记的数据包都根据路由表server1处理。
iptables MARK模块的–set-mark操作仅能在mangle表完成。
假设MARK为150(把…改成你需要的参数):
1 |
iptables -t mangle ... -j MARK --set-mark 150 |
这里需要注意,Server 1访问国际互联网,也要通过Router,如果getsockopt(…, SO_ORIGINAL_DST, …)调用程序访问互联网会匹配到上面的规则,必须排除,否则就有环路了,最简单的,就是直接排除掉Server 1的MAC地址:
1 |
iptables -t mangle ... -m mac --mac-source SERVER1_MAC -j RETURN |
这条排除Server 1的规则需要插入在MARK之前。
最后修改路由策略,让带150 MARK的数据包都使用路由表server1:
1 |
ip rule add fwmark 150 table server1 |
Server 1的处理
数据包已在Router处分类,并不经网络层和传输层Header的修改转发到Server 1处。
Server 1要区分这些数据包是要交给getsockopt(…, SO_ORIGINAL_DST, …)调用程序,还是交给自己的程序处理,很简单,看看数据包目的IP是不是自己就行了,要交给getsockopt(…, SO_ORIGINAL_DST, …)调用程序的数据包,目的IP肯定不是Server 1的IP:
1 |
iptables -t nat ... ! -d 192.168.1.2 -p tcp -j REDIRECT --to GETSOCKOPT_PROGRAM_LISTEN_PORT |
总结
这个方法仅适用于Router与Server 1处于同一局域网的情况,因为路由转发仅能在链路层上进行,若需要跨网域调用,可能要考虑使用隧道封装,但这样开销有点大。
Router一般都在芯片上实现了NAT,NAT效率高,Server 1虽然没芯片级的NAT,但我相信,你处理被NAT的数据包的程序,开销比NAT大得多。