最近有点走火入魔了!本文所用技术非标准,较真儿者慎入!!
一个局域网内,两台机器拥有同样的IP,可以吗?
这不就是IP地址冲突吗?当然不行!
可是要知道,如果搞点旁门左道,还是可以做到的!
首先要明白的是,IP数据报在以太网中的收发特征:
对于发送来讲:只要你有一个目标MAC供你封装成帧,就可以发出去,而这个MAC地址是由ARP来获取的;
对于接收来讲:只要收到帧的目标MAC是接收到帧的网卡的MAC地址,就可以正确接收!
现在我们逐步的来实现一个局域网内拥有同样IP还能正常越过默认网关访问不同外网的情景。实际上,在我实现的简版SDN中,一切都是保存在conntrack中的,它甚至可以保存一个流的上一跳和下一跳neighbour,这就意味着完全架空了本机的路由逻辑和arp逻辑!使本机完全退化成了一个switch!专注于数据包的转发!neighbour的定义是次要的,完全没有必要照抄Linux内核的neighbour结构体,它的实质就是一个MAC地址而已,思路很简单:
1.流头进来在正方向的PREROUTING中将其源MAC地址保存在conntrack;
2.属于同一流的数据包在反方向的POSTROUTING中获取conntrack中的MAC地址封装为目标MAC;
3.发送并返回STOLEN。

就是这么简单!实际上如果能在Linux中实现Policy ARP就好了,也就是说,可以为同一个IP地址映射多条ARP项,每个项有不同的MAC地址。毕竟neighbour是和dst_entry即路由表项相关联的,因此只要保证同一个IP地址的多个ARP映射属于不同的路由表项即可,而这很容易通过IPMARK+Policy Routing来实现!
   不幸的是,Linux并没有实现如此让人走火入魔的Policy ARP,现有的隔离措施就是使用NET命名空间,然而它是用于虚拟化的,同一个网卡只能属于同一个命名空间,此场景不适用!那么怎么办?只能自己写Netfilter代码了啊,还好,由于这个逻辑超级清晰,因此所做的修改也是很容易的,实现了这个的话,仰天长啸,嘲笑一下地址冲突!
  保存源MAC地址在ipv4_conntrack_in的最后来做,而还原保存的MAC地址到目标MAC地址的操作在ipv4_confirm中来做。代码如下:

//继续偷梁换柱借用nat的extend
struct nf_conn_nat {
    //自动保存回复帧的目标MAC地址
    unsigned char reply_gw_mac[ETH_ALEN];
    //自动保存本机MAC地址
    unsigned char orig_gw_mac[ETH_ALEN];
    //自动保存设备
    struct net_device *dev;
};
static unsigned int ipv4_conntrack_in(unsigned int hooknum,
                                      struct sk_buff *skb,
                                      const struct net_device *in,
                                      const struct net_device *out,
                                      int (*okfn)(struct sk_buff *))
{
                unsigned int ret = nf_conntrack_in(dev_net(in), PF_INET, hooknum, skb);
                if (ret == NF_ACCEPT) {
                enum ip_conntrack_info ctinfo;
                struct nf_conn *ct;
                struct nf_conn_nat *automac;
                ct = nf_ct_get(skb, &ctinfo);
                if (!ct) {
                        goto out;
                }
                if (skb->dev->flags & IFF_LOOPBACK)
                        return ret;
                if (CTINFO2DIR(ctinfo) != IP_CT_DIR_ORIGINAL) {
                        goto out;
                }
                automac = nf_ct_ext_find(ct, NF_CT_EXT_NAT);
                if (automac == NULL) {
                        automac = nf_ct_ext_add(ct, NF_CT_EXT_NAT, GFP_ATOMIC);
                        if (automac == NULL) {
                                goto out;
                        }
                        struct ethhdr *eth = (struct ethhdr*)(skb->data - ETH_HLEN);
                        memcpy(automac->reply_gw_mac, eth->h_source, ETH_ALEN);
                        memcpy(automac->orig_gw_mac, eth->h_dest, ETH_ALEN);
                        automac->dev = skb->dev;
                }
        }
out:
        return ret;;
}
static unsigned int ipv4_confirm(unsigned int hooknum,
                                 struct sk_buff *skb,
                                 const struct net_device *in,
                                 const struct net_device *out,
                                 int (*okfn)(struct sk_buff *))
{
               .............
out:
        /* We've seen it coming out the other side: confirm it */
        ret = nf_conntrack_confirm(skb);
        if (ret == NF_ACCEPT) {
                struct nf_conn_nat *automac = NULL;
                struct net_device *dev = NULL;
                unsigned int hh_len = 0;
                if (skb->dev->flags & IFF_LOOPBACK)
                        return ret;
                if (CTINFO2DIR(ctinfo) == IP_CT_DIR_ORIGINAL)
                        return ret;
                if (hooknum != NF_INET_POST_ROUTING)
                        return ret;
                automac = nf_ct_ext_find(ct, NF_CT_EXT_NAT);
                if (automac == NULL)
                        return ret;
                dev = automac->dev;
                skb->dev = dev;
                hh_len = LL_RESERVED_SPACE(dev);
                if (unlikely(skb_headroom(skb) < hh_len && dev->header_ops)) {
                        struct sk_buff *skb2;
                        skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
                        if (skb2 == NULL) {
                                kfree_skb(skb);
                                return -ENOMEM;
                        }
                        if (skb->sk)
                                skb_set_owner_w(skb2, skb->sk);
                        kfree_skb(skb);
                        skb = skb2;
                }
                //填充以太帧头,注意偏移!
                char *header = skb->data - hh_len;
                header[0] = 0x0;
                header[1] = 0x0;
                struct ethhdr *eth = (struct ethhdr*)(header+2);
                memcpy(eth->h_dest, automac->reply_gw_mac, ETH_ALEN);
                memcpy(eth->h_source, automac->orig_gw_mac, ETH_ALEN);
                eth->h_proto = htons(ETH_P_IP);
                skb_push(skb, ETH_HLEN);
                skb->_skb_dst = NULL;
                dev_queue_xmit(skb);
                return NF_STOLEN;
        }
        return ret;
}

精妙之处在于,只要有一个包到达该BOX,其源MAC地址就能被conntrack记住,当反方向的包到来并要发回去的时候,就能取出该源MAC地址作为目标MAC地址封装到以太帧中,然后无需经由ARP层直接发送出去。现在的问题是,两个相同IP地址的主机可以同时发包过来吗?答案无疑是肯定的,因为它们俩发包前解析BOX的MAC地址,该BOX无疑全部回复,它们总有个先来后到,唯一的副作用时后到的那个ARP请求会更新BOX的ARP表,将相同的那个IP的MAC地址覆盖成自己的MAC地址,然而这无所谓了,因为既然两台主机都得到了BOX的MAC地址,数据包肯定能发出去到达BOX,返回包在寻址这两台机器的时候,并不通过ARP来寻址,而是通过这两台机器自己携带过去的源MAC地址来作为目标MAC的,故而覆盖也无所谓了!
   只要保证没有五元组的冲突即可,而这种冲突的可能性是极其小的。
   严格来讲ARP属于IP的下层,是和链路层接口的协议,它是”下一跳解析协议“的一种,适用于IPv4到以太网的适配,ARP只是动态的帮你完成了映射,如果你知道该如何封装以太帧,完全可以不用ARP,要么手工设置static的映射,要么就是从某处得到一个映射,关键点不在ARP或者映射,关键点在于你能得到一个MAC地址!只要你能得到目标MAC地址,那么封装以太帧即可,此时是不关心源MAC的!
   我真的走火入魔了,不管是用MAC桥微端口搞定了Open×××的源地址选择,还是构造假网关在配置Windows路由时指定源地址,还是本文说的IP地址冲突情景下的数据通信,都不是标准的做法,希望较真的同学注意。如果想靠这个理解网络的行为,那是再好不过了...