socket的IP_TRANSPARENT选项实现代理

socket有一个IP_TRANSPARENT选项,其含义就是可以使一个服务器程序侦听所有的IP地址,哪怕不是本机的IP地址,这个特性在实现透明代理服务器时十分有用,而其使用也很简单:
int opt =1;
setsockopt(server_socket,SOL_IP, IP_TRANSPARENT,&opt,sizeof(opt));

0.导引:TCP绑定0.0.0.0的情况

TCP可以绑定0.0.0.0,这个都知道,那么到底用哪一个地址何时确定呢?答案是“根据连接源的地址反向做路由查找后确定的”。如果有一个地址A连接该服务器,那么在服务器收到syn后,就会查找目的地址为A的路由,进而确定源地址,然而如果不设置IP_TRANSPARENT选项,则这个被连接的地址必须在local路由表中被找到,否则一切都免谈。
因此如果我有一个没有设置IP_TRANSPARENT选项的TCP服务器绑定了0.0.0.0这个地址,端口绑定到80,我想这个服务器截获经过此地访问56.56.56.56:80的流量,怎么办?很简单,知道了TCP源地址选择的原理之后,我们只需要设置下面的路由即可:
ip route add local 56.56.56.56 dev lo tab local
这样一来,所有访问56.56.56.56这个地址的流量在经过本机时,都会进入local_in,因为它在local表中找到了路由。但是本机没有56.56.56.56这个地址,本机的80端口服务器回复syn-ack的时候,执行反向路由查找,在local表中找到了56.56.56.56的路由,进而成功返回,最终连接成功在A和56.56.56.56:80之间建立。
然而思考一下,以上虽然圆满完成了任务,但是如果有N多个目的地址,岂不是要设置N多地址在local路由表?有没有什么办法只设置很少的规则就能截获所有到达80端口的流量呢?有的,那就是在代理服务器的socket上设置IP_TRANSPARENT选项。

1.总体配置

如果你想操作路由查找的过程,还是要使用策略路由。针对上述的需求,有以下配置:

a).识别需要截获(代理)的流量

可选配置1:仅仅针对网卡
ip rule add iif $流量进入的网卡 tab proxy
可选配置2:针对更复杂的五元组信息
iptables -t mangle -A PREROUTING (为特定端口的流量打上mark)
ip rule add fwmark (上述mark) iif $流量进入的网卡 tab proxy

b).为策略路由表增加路由表项

ip route add local 0.0.0.0/0(或者直接写default) dev lo tab proxy
注意:增加路由表项的时候,一定要注意local这个type,这是一个路由类型,凡这个类型的路由项,一旦有流量被匹配,所有的流量统统送到本地进行处理。
值得注意的是,上述配置中并不需要将路由表项添加到local表中,这是因为我们设置了IP_TRANSPARENT选项,具体的限制请参考Linux内核代码net/ipv4/route.c中的ip_route_output_slow函数:
if (oldflp->oif == 0
    && (ipv4_is_multicast(oldflp->fl4_dst) ||
    oldflp->fl4_dst == htonl(0xFFFFFFFF))) {
    dev_out = ip_dev_find(net, oldflp->fl4_src); //强制在local表中查找
    ...
}
if (!(oldflp->flags & FLOWI_FLAG_ANYSRC)) { //如果设置了IP_TRANSPARENT选项...
...
}

2.综上

综上所述,配置完成了。我们可以看到,Linux对路由的处理是怎样进行的,如果想深入地理解它,还是需要深入了解一下iproute2工具,它提供了一个理解问题的表象。然而正如Linux其它子系统总是为发生一些让人费解的行为一样,网络子系统这类行为更多,一些是遵循了RFC或者IEEE等相关标准的建议,另一些则是Linux自身的实现技巧,对于本文的情况,有以下的主题:

a.你配置的路由nexthop并不一定会被采用

如果你的本机eth1的地址是4.4.4.1/24,你想将过路流量导入到本机应用层,一个很直观但是不可用的配置是:
ip route add 1.2.3.4 via 4.4.4.1
然而当去往1.2.3.4的流量经过本机的时候,使用route -C查看cache,发现其默认网关还是本机的默认网关,并不是你指示的4.4.4.1,如果不理解这一点,还是需要看一下代码是如何执行的。当去往1.2.3.4的流量经过时,很显然会匹配到上述配置的路由,然而内核在真正使用该路由前需要对该路由进行一些微调,最终生成的路由cache则是真正可用的路由项,起初匹配完成后会将路由cache项的rt_gateway直接初始化为目的地址,也就是1.2.3.4:
rth->rt_gateway = fl->fl4_dst;
然后在rt_set_nexthop中会判断是继续使用目的地址直接转发还是使用你在路由表中配置的那个默认网关:
if (FIB_RES_GW(*res) &&
    FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK)
    rt->rt_gateway = FIB_RES_GW(*res);
很显然,只有当FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK为真时,才会使用你配置的默认网关4.4.4.1。
接下来就是让FIB_RES_NH(*res).nh_scope == RT_SCOPE_LINK为真,于是设置下列路由表项:
ip route add 1.2.3.4/32 scope global via 4.4.4.1 dev eth1 onlink
scope为global是被迫的,因为这是规定,下一跳必须比目的地更近才可以,因此其scope一定要比下一跳的scope更小。这样还是不行,因为还有一个限制,那就是你的下一跳网关的地址必须是unicast的:
if (nh->nh_flags&RTNH_F_ONLINK) {
    struct net_device *dev;
    if (cfg->fc_scope >= RT_SCOPE_LINK)
        return -EINVAL;
    if (inet_addr_type(net, nh->nh_gw) != RTN_UNICAST)
        return -EINVAL;
    if ((dev = __dev_get_by_index(net, nh->nh_oif)) == NULL)
        return -ENODEV;
    if (!(dev->flags&IFF_UP))
        return -ENETDOWN;
    nh->nh_dev = dev;
    dev_hold(dev);
    nh->nh_scope = RT_SCOPE_LINK;
    return 0;
}
由于inet_addr_type返回unicast需要保证地址不在local表中命中:
local_table = fib_get_table(net, RT_TABLE_LOCAL);
if (local_table) {
    ret = RTN_UNICAST;
    if (!local_table->tb_lookup(local_table, &fl, &res)) {
        if (!dev || dev == res.fi->fib_dev)
            ret = res.type;
        fib_res_put(&res);
    }
}
那么下一步则将4.4.4.1这条路由从local表中删除:
ip rou del 4.4.4.1/24 tab local
OK,这下可以了,然而却在本机发送数据到1.2.3.4的时候失败了,这是因为源地址选择时出了问题,此时4.4.4.1这个地址已经不在local中了。上述失败以telnet 1.2.3.4 6666为例,在TCP进行connect的时候,在真正进入路由模块之前,首先要调用ip_route_connect确定源地址,然而在该connect流量进入路由模块的时候,源地址已经确定了,为4.4.4.1,然而该地址已经不在local路由表中存在,可是output路由查找在确定了源地址的情况下需要源地址在local表中或者源socket设置了IP_TRANSPARENT选项,我们知道标准的telnet是没有这个选项的,因此会返回:
telnet: Unable to connect to remote host: Invalid argument
这个错误。因此需要如下:
ip route add 1.2.3.4/32 scope global via 4.4.4.1 dev eth1 onlink src 7.7.7.7
其中7.7.7.7是临时添加到eth1上的另外一个地址,显然7.7.7.7是在local表中存在的。
看到这里,其实还有更简单的方式,前面的那段if (nh->nh_flags&RTNH_F_ONLINK)代码是fib_check_nh的一部分,该部分执行的是设置了onlink标志的逻辑,如果不设置呢?要知道我们要做的只是:
1.inet_addr_type(net, nh->nh_gw) == RTN_UNICAST
2.cfg->fc_scope < RT_SCOPE_LINK

于是看一下fib_check_nh的第二部分:
struct flowi fl = {
        .nl_u = {
            .ip4_u = {
                .daddr = nh->nh_gw,
                .scope = cfg->fc_scope + 1,
            },
        },
        .oif = nh->nh_oif,
    };
    if (fl.fl4_scope < RT_SCOPE_LINK)
        fl.fl4_scope = RT_SCOPE_LINK;
    //若到达这里,如果gw是本机地址,则会在local表中命中
    //因此你仍然需要将4.4.4.1从local表中删除,保证res的scope为RTN_UNICAST
    if ((err = fib_lookup(net, &fl, &res)) != 0)
        return err;
    }
    err = -EINVAL;
    if (res.type != RTN_UNICAST && res.type != RTN_LOCAL)
        goto out;
    nh->nh_scope = res.scope;
    ...
成功,不再报错,然而数据也没有到达应用层。

b.内核不会为下一跳网关再次进行路由查找

废了这么大的力气终于将流量导入4.4.4.1了,然而真的能发往本机吗?如果你试验一下,将会发现内核直接在4.4.4.1的那个网卡上arp这个4.4.4.1地址:
ARP, Request who-has 4.4.4.1 tell 4.4.4.1, length 28
内核从来不会为你的默认网关再次进行路由查找,而仅仅根据路由的scope以及下一跳网关的scope直接进行地址解析,在本例中4.4.4.1是link的,那么很显然会直接arp,因为内核相信4.4.4.1在同链路层(link)的其它地方。

c.路由工作在网络层

虽然我们都不想使用复杂的iptables,使用纯路由被看作是一种妙招,路由的blackhole/unreachable以及任意引导任意流量被看作是网络的必杀技。然而路由仅仅工作在网络层,如果你需要更高层的参数参与过滤和引导,你不得不使用其它的手段,在Linux上使用iptables工具可以完成。当然并不是说你必须使用其防火墙和NAT功能,哪怕打个mark不也很好吗?iptables是一个工具链,它可以通过mark和策略路由交互。

你可能感兴趣的:(socket)