就这样,一个数据包在整个netgraph中通过“离开一个Node的某个hook,进入另一个Node的某个hook的rcvdata”的方式游历,Node在这里的作用就是封装私有数据和统一的操作,当然,你可以重载掉一个Node内统一的rcvdata回调函数,而是为每一个hook都设置一个私有的rcvdata回调函数,再次强调,是hook在rcvdata,而不是Node在rcvdata,Node的rcvdata是一个该Node所有hook通用的回调函数,如果没有hook私有的rcvdata,该通用函数将被调用,ng_snd_item最终将进入下面的逻辑:
if ((!(rcvdata = hook->hk_rcvdata)) && (!(rcvdata = NG_HOOK_NODE(hook)->nd_type->rcvdata))) { error = 0; NG_FREE_ITEM(item); break; }
由此看出,Node有一个默认的对所有hook都适用的rcvdata回调函数,然而各个hook可以重载掉这个默认的rcvdata回调函数。接下来我们看一下netgraph如何和协议栈对接,不要把操作系统想得太神奇,实际上完成这种工作只需要一个回调函数即可。以以太网接收为例,以太网接收处理函数中会调用ng_ether_input_p回调函数,你只需要将其定义一下即可,对于很多场合都使用的ng_ether,它将此函数定义为:
static void ng_ether_input(struct ifnet *ifp, struct mbuf **mp) { const node_p node = IFP2NG(ifp); const priv_p priv = NG_NODE_PRIVATE(node); int error; /* If "lower" hook not connected, let packet continue */ if (priv->lower == NULL) return; NG_SEND_DATA_ONLY(error, priv->lower, *mp); /* sets *mp = NULL */ }
最后通过NG_SEND_DATA_ONLY将数据包发送给priv->lower这个hook,最终数据包会进入priv->lower的peer,调用priv->lower->peer的rcvdata回调函数,在一切开始工作之前,你首先需要构建好整个graph。对于以太网发送函数,也有类似的_p回调函数。netgraph和Netfilter的区别在于它可以将graph“挂接”在特定的interface上,而Netfilter却把HOOK直接挂在协议栈本身,interface在Netfilter中只是一个match。如此一比较,效率差异就很明显了。以以太网为例,在ether_input中就会调用netgraph,如果加载了ng_ether的话,就会调用下面的函数:
static void ng_ether_input(struct ifnet *ifp, struct mbuf **mp) { const node_p node = IFP2NG(ifp); const priv_p priv = NG_NODE_PRIVATE(node); int error; /* If "lower" hook not connected, let packet continue */ if (priv->lower == NULL) //如果这块网卡上没有任何hook,将不作处理直接返回。 return; NG_SEND_DATA_ONLY(error, priv->lower, *mp); /* sets *mp = NULL */ }
如果本ifp上没有挂接任何graph,则直接返回标准协议栈处理,如果挂接了一个graph,则数据包将进入该graph,你可以将firewall rule配置在此graph里面。对于Netfilter而言,在网卡接收这一层,没有任何HOOK,只有到了IP层,才会进入PREROUTING/INPUT/FORWARD...等HOOK,哪怕你配置了一条rule,所有的包都将接受检查以确定是否匹配,在Netfilter的rule中,所谓的interface只是一个match。需要说明的是,netgraph也可以像Netfilter那样工作,你只需要将其挂在ip_in(out)put上即可。
我们给出两个例子来看看netgraph如何实现bridge和bonding,这些在Linux上都是通过虚拟net_device来实现的,其发送逻辑都是该虚拟net_device的hard_xmit实现的,而其数据接收逻辑则是硬编码在netif_receive_skb中的,bridge是通过handle_bridge这个硬编码hook进入的,而bonding是通过skb_bond来实现的。也就是说Linux是通过对既有的协议栈进行硬修改来实现的,而netgraph则不需要这样,对于FreeBSD,我们只需要构建一张graph就可以实现bridge或者bonding,首先我们先看看bridge的实现逻辑,如下图所示:
我个人以为图示已经很清晰了。需要注意的是,netgraph将本地的网卡作为了局域网上一张普通的网卡来看待,并没有刻意区分流量是本机发出的还是从其它机器发出的,因此,如果你只是想将bridge作为一个二层设备,那么可以断开Hook-ethX-low和Hook-ethX-upper之间的边即可,netgraph实现的bridge,你看不到虚拟设备,这种实现更纯粹,伟大的BSD将这种思想带给了其衍生出来的Cisco IOS。
下面是bonding的实现逻辑:
由于bonding网卡大多数负责的是本地IP层发出的数据,需要和路由转发表相配合,因此需要有一块虚拟网卡,这个是通过ng_eiface的构造函数ng_eiface_constructor实现的。依然无其它话可说。
以上两个图展示了netgraph的魅力,既然这样,也就可以依照这种方式实现VLAN,IPSec等了,要比Linux的Netfilter加设备驱动模型的实现方式更“可插拔”,有netgraph,FreeBSD可以将所有的协议处理在一张张的graph中进行,数据包在graph中游历在每一个Node被接收到的hook处理,主要你能根据协议处理逻辑构建好一张图,将这张图挂接在协议栈,甚至挂接在驱动上,你就能很方便的实现网络的任意扩展...