在所有高端型号,大多数中端型号以及部分低端型号的交换机/路由器上,都可以配置一个或者多个镜像端口,它是流量分析的利器。然而,Linux上没有现成的技术可以实现镜像端口,当然,我指的不是Linux 3.x(x是几,忘了)以上的内核,这些内核已经支持了镜像,但不够好。起码2.6.35的内核是不能支持的,那么Linux实现的软交换机属于哪个档次呢?关键是,很多高端的网络产品也是基于Linux实现的,没有镜像口怎么能行,即使在不使用Linux bridge的情况下,也希望能有一个技术实现镜像端口。
我相信,并且确信,很多产品都已经实现了这个技术,它事实上很简单,多年以前,我自己在学习华为网络技术的时候,也曾在Linux写了一个支持镜像的内核模块,虽然那是在网上找的人家实现的半片子代码改的。现如今,在我可以很不谦虚地说自己已经很精通Netfilter以及Linux IP路由的时候,决定给出一个基于Netfilter的实现,Netfilter就在那,它几乎可以扩展任何协议栈的东西,甚至重写整个协议栈...多年以来,关于这个Linux如何实现镜像端口的讨论很多很多,也催生了不少爱美之士的不断尝试和修正,对我个人来讲,第一次涉足这个话题是在2009年,虽然在学习Cisco技术的时候也搞过,但毕竟不是任务化的,只是说我对Cisco技术是学而不考-太贵,因此可以有大把本应该用于考试准备的时间用来学习Linux,特别是把Cisco的特性实现在Linux上,说句题外话,我之所以对Cisco和Linux的网络技术能同时掌握,和学而不考有很大的关系,然而对于求职,那就是另外一回事了...
在给出代码之前,我先给出一个只依靠配置就可以完成的实现,然后说一下它的缺点。事实上,仅仅依靠brctl命令或者sysfs,echo就可以实现一个镜像端口,具体做法就是:
1.确定你的镜像端口,比如eth5;
2.将实际数据通过的端口,比如eth0和镜像端口绑成一个bridge;
3.调用brctl的setageing命令将老化时间设置为0,这就模拟了一个2端口的hub;
4.所有数据端口eth0发出的包都会发往eth5
...
但是!但是每一个物理接口只能属于一个bridge,这就意味着你只能通过上述的方式捕获一个方向的数据,不得不使用另外的一个镜像口使用相同的办法捕获另外一个方向的数据,然后再把这两个镜像口接在一个switch上,在此switch上合二为一,这种方式,还是,太硬了!
那么,软件做法有没有呢?有的,我多年前实现的那个就是,大体想法就是注册一个ETH_P_ALL类型的packet_type,类似tcpdump抓包那样捕获数据包,然后在内核模块中调用dev_queue_xmit将其发送到你定义的镜像端口,具体定义方式需要通过字符设备的ioctl,procfs等方式来定义。这种方式比较常规,工作地比较好,并且可以从诸如tap等虚拟网卡将流量镜像给进程而不是线缆那头的审计设备。然而,还是太硬了,在你通过BPF语法过滤数据包之前,流量已经被ETH_P_ALL截取了...事实上,并不是所有的流量都需要被镜像!BPF虽然强大,但是依靠中间层进行解析翻译,门槛太高,我相信,一条iptables规则和一条等价的BPF规则放在那,能看懂前者的占绝大多数,看不懂后者占绝大多数,过于灵活就是不灵活,给你一本新华字典,所有字都在里面,你读十遍也不如读一遍《古文观止》...这个可以从香农的信息论中得到证明。
xt_TEE的实现
在xtables-addons中,已经有了一个xt_TEE的实现,在其manual中,有一个一目了然的配置:-t mangle -A PREROUTING -i eth0 -j TEE --gateway 2001:db8::1
即将数据包克隆一份,然后发往一个IP地址,该IP地址可以配置。我为何觉得它不好呢?第一,我认为依基于IP而不是基于端口来镜像数据包可能需要额外太多的配置,比如你事先要有一个接收端的明确IP地址;第二我觉得它的实现不是很好,它的实现阻碍了原始数据包的快速通过,而我比较倾向于用“下半部”的思想解决克隆包的发送问题,即先将其排入一个队列,然后让系统调度其发送,而不是强制在代码中调用发送代码。除了这两点,TEE的实现真的不错。看了TEE的实现之后,我在想,为何:
-j TEE --dev ethX,ethY,ethZ
这种设置就不行呢?当然,肯定不行,因为TEE target没有--dev参数,可是为何没有人实现呢?...难道仅仅是内核缺少由dev自动封装以太头的接口?也许是吧,毕竟,所有的dev_queue_xmit调用都是从路由层一路下来的...
现在该给出我最新的实现了。这个实现很简单,和TEE一样,写了一个新的iptables target,即CLONE。克隆一个数据包并且打上标签,然后如何处理该数据包呢?很显然是根据标签来查找策略路由表了,你可以在策略路由表中将所有克隆的数据包发到任何一个网卡中,这不就是镜像口的含义么?
要说明的是,虽然你可以通过reroute的方式将带有标签的克隆数据包发往一个网卡,但是由于网卡在发包前需要对目标或者对下一跳进行ARP,那么可能导致由于ARP没有回应而发包失败,幸运的是,ifconfig命令可以禁用网卡的ARP,这不正是为镜像端口准备的么??
前传
起初,写这个模块的目标并不是为了做镜像端口,而是为了将一个数据包复制两份,仅此而已,其实本意就是一个Netfilter实现的抓包模块,和使用pcap抓包相比,它的优势在于可以去除很多不相关数据包的干扰,它只能抓取确实是发往本机的数据包,虽然这也许违背的抓包的原本的意义,但是那只是一个词汇而已!我以及很多人大多数情况下抓包并不是为了嗅探别人的数据,而是为了解决和自己相关的问题,这就需要过滤掉那些不小心到来的由于交换机MAC映射到期导致的发往所有端口的数据,而这需要写一大堆tcpdump规则。使用Netfilter配合iptables来做这件事,优势在于不需要把全部的规则写在一条命令里面,你完全可以在PREROUTING的mangle表用mark过滤掉那些你不感兴趣的包,然后在FORWARD的filter上对感兴趣的包实施包克隆,然后将克隆到的数据包通过策略路由发往任何你希望它到达的地方。你可以再写一个模块,用以决定是对包进行完全的记录呢,还是对仅仅像LOG target那样只记录协议元数据-这很重要,大多数时候,我们并不关心载荷内容,除非你做深度分析。
总之,我不喜欢那种包揽一切的程序,抓包也是如此,一个ETH_P_ALL将所有数据不问青红皂白全部截取,这是不合适的,当然它更加符合抓包的原本含义,但是谁在乎呢?也许是UNIX哲学在作崇,但也只是也许而已。实证主义并不在任何地方都有效。
在我的实现中,和TEE的实现不同,我只是克隆数据包,然后为其打上一个标签,至于说接下来怎么做,后续的HOOK来决定,你甚至都可以用我的CLONE target和TEE target结合在一起,形成一个packet fork×××。
下面给出实现,注意,该实现不能做到包嗅探!
实现
本实现由4部分,其中包含一个内核模块文件,一个用户态的iptables库文件,一个结构体定义头文件,一套Makefile。代码完全按照xtables-addons的规范制作。结构体定义头文件:xt_CLONE.h
#ifndef _LINUX_NETFILTER_XT_CLONEMARK_H #define _LINUX_NETFILTER_XT_CLONEMARK_H 1 struct xt_clonemark_tginfo { __u32 mark; }; #endif /* _LINUX_NETFILTER_XT_CLONEMARK_H */
内核模块:xt_CLONE.c
/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License; either * version 2 of the License, or any later version, as published by the * Free Software Foundation. */ #include <linux/module.h> #include <linux/skbuff.h> #include <linux/netfilter/x_tables.h> #include <net/ip6_route.h> #include "xt_CLONE.h" #include <net/ip.h> #include "compat_xtables.h" struct sk_buff_head clq; static struct tasklet_struct clone_xmit_tasklet; static void clone_xmit_work(unsigned long data) { struct sk_buff_head *pclq = (struct sk_buff_head *)data; struct net_device *old_dev = NULL; struct net_device *new_dev = NULL; do { struct sk_buff * skb = skb_dequeue_tail(pclq); old_dev = skb_dst(skb)->dev; if (ip_route_me_harder(&skb, RTN_UNSPEC)) { kfree_skb(skb); } new_dev = skb_dst(skb)->dev; if (old_dev != new_dev) { ip_local_out(skb); } else { kfree_skb(skb); } } while (!skb_queue_empty(pclq)); } static unsigned int clone_tg6(struct sk_buff **poldskb, const struct xt_action_param *par) { // TODO return XT_CONTINUE;; } static unsigned int clone_tg4(struct sk_buff **poldskb, const struct xt_action_param *par) { const struct xt_clonemark_tginfo *markinfo = par->targinfo; struct sk_buff *newskb; __u32 mark; __u32 qlen; qlen = skb_queue_len (&clq); // 控制总量! if (qlen > 1000/*sysctl参数控制*/) { return XT_CONTINUE; } mark = markinfo->mark; newskb = pskb_copy(*poldskb, GFP_ATOMIC); if (newskb == NULL) return XT_CONTINUE; // 在FORWARD链上做的目的是可以放心reroute,关键在re前缀 // skb_dst_drop(newskb); // 丢弃连接跟踪,但是要为之初始化一个notrack的伪连接跟踪 #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) #include <net/netfilter/nf_conntrack.h> nf_conntrack_put(newskb->nfct); newskb->nfct = &nf_conntrack_untracked.ct_general; newskb->nfctinfo = IP_CT_NEW; nf_conntrack_get(newskb->nfct); #endif newskb->mark = mark; skb_queue_head(&clq, newskb); tasklet_schedule(&clone_xmit_tasklet); return XT_CONTINUE; } static struct xt_target clone_tg_reg[] __read_mostly = { { .name = "CLONE", .revision = 0, .family = NFPROTO_IPV6, .table = "filter", .target = clone_tg6, .targetsize = sizeof(struct xt_clonemark_tginfo), .me = THIS_MODULE, }, { .name = "CLONE", .revision = 0, .family = NFPROTO_IPV4, .table = "filter", .target = clone_tg4, .targetsize = sizeof(struct xt_clonemark_tginfo), .me = THIS_MODULE, }, }; static int __init clone_tg_init(void) { skb_queue_head_init(&clq); tasklet_init(&clone_xmit_tasklet, clone_xmit_work, (unsigned long)&clq); return xt_register_targets(clone_tg_reg, ARRAY_SIZE(clone_tg_reg)); } static void __exit clone_tg_exit(void) { tasklet_kill(&clone_xmit_tasklet); return xt_unregister_targets(clone_tg_reg, ARRAY_SIZE(clone_tg_reg)); } module_init(clone_tg_init); module_exit(clone_tg_exit); MODULE_AUTHOR("Wangran <marywangran@126.com>"); MODULE_DESCRIPTION("Xtables: CLONE packet target"); MODULE_LICENSE("GPL"); MODULE_ALIAS("ip6t_CLONE"); MODULE_ALIAS("ipt_CLONE");
iptables模块:libxt_CLONE.c
/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License; either * version 2 of the License, or any later version, as published by the * Free Software Foundation. */ #include <stdio.h> #include <getopt.h> #include <xtables.h> #include "xt_CLONE.h" #include "compat_user.h" enum { FL_MARK_USED = 1 << 0, }; static const struct option clonemark_tg_opts[] = { {.name = "mark", .has_arg = true, .val = '1'}, {NULL}, }; static void clonemark_tg_init(struct xt_entry_target *t) { struct xt_clonemark_tginfo *info = (void *)t->data; info->mark = ~0U; } static void clone_tg_help(void) { printf("CLONE --mark mark\n\n"); } static int clone_tg_parse(int c, char **argv, int invert, unsigned int *flags, const void *entry, struct xt_entry_target **target) { struct xt_clonemark_tginfo *info = (void *)(*target)->data; unsigned int n; switch (c) { case '1': xtables_param_act(XTF_ONLY_ONCE, "CLONE", "--mark", *flags & FL_MARK_USED); xtables_param_act(XTF_NO_INVERT, "CLONE", "--mark", invert); if (!xtables_strtoui(optarg, NULL, &n, 0, ~0U)) xtables_param_act(XTF_BAD_VALUE, "CLONE", "--mark", optarg); info->mark = n; *flags |= FL_MARK_USED; return true; } return false; } static void clone_tg_check(unsigned int flags) { //TODO } static void clonemark_tg_save(const void *entry, const struct xt_entry_target *target) { const struct xt_clonemark_tginfo *info = (const void *)target->data; printf(" --mark 0x%x ", (__u32)info->mark); } static struct xtables_target clone_tg_reg = { .version = XTABLES_VERSION, .name = "CLONE", .family = NFPROTO_UNSPEC, .size = XT_ALIGN(sizeof(struct xt_clonemark_tginfo)), .userspacesize = XT_ALIGN(sizeof(struct xt_clonemark_tginfo)), .init = clonemark_tg_init, .save = clonemark_tg_save, .help = clone_tg_help, .parse = clone_tg_parse, .final_check = clone_tg_check, .extra_opts = clonemark_tg_opts, }; static __attribute__((constructor)) void clone_tg_ldr(void) { xtables_register_target(&clone_tg_reg); }
编译:
建议编译时将c代码全部放入xtables-addons的extensions目录,然后修改该目录下的Kbuild文件,加入以下一行:
obj-${build_CLONE} += xt_CLONE.o
修改该目录下的Mbuild文件,加入下面一行:
obj-${build_CLONE} += libxt_CLONE.so
修改该目录上级目录的mconfig文件,加入下面一行:
build_CLONE=m
在extensions目录下执行make && make install即可,
说明
为何要在filter表做呢?因为filter表都在路由之后执行,这是为了调用reroute接口函数ip_route_me_harder的方便,该函数导出为一个内核接口,可以直接调用。在这么做之前,我尝试过直接调用ip_queue_xmit函数,然而发现只有在本机出发的包才会经过该路径,因此需要为skb绑定一个socket才可以,而这无疑是工作量加大了;后来,我想到了直接调用ip_rcv_finish函数,可以该函数并未导出,需要在加载模块前先去procfs里面查一下该函数的地址,然后传入模块,这种做法并不标准;再往后,自然而然就是调用ip_route_me_harder接口函数了,然而该函数需要skb已经有了一个dst_entry(这很正常,reroute中的re前缀表明skb已经被路由过一次了),因此必然要在路由之后调用,那么显然处理位置就落到了Netfilter的HOOK点和路由构成的马鞍面的中间位置了,只能在filter表来做,重新路由之后,直接调用ip_local_out从第三层发出即可。此时又有问题了,既然已经重路由了,为了不直接从第二层路由结果的dev中发出呢,也就是调用dev_queue_xmit函数。实际上是完全可以的,然而工作量也会加大,比如你要自行增加MAC头封装等。在一个成型的实现中,所有的封装都必须由协议栈本身来完成,即调用协议栈的函数,因为协议栈本身就是干这个的,决不要在自己的代码中实现,如果你觉得自己可以实现一个更妙的,那就直接改掉协议栈。
局限
该实现还是有一定局限的,毕竟该实现的做法太高层,它会改变数据包的MAC头,但是这对于针对应用层内容的深度解析,无所谓了。另外需要注意的是,需要在本机做三件工作,第一就是设置CLONE规则及确定mark,第二是根据mark设置策略路由,第三就是将策略路由指向的出口设备的arp禁用掉。除了本机做的工作之外,还要在接收镜像数据的机器的接收接口上开启混杂模式。毕竟这只是一个试验,并非成型的解决方案,能做到这一点我已经很满足了。