FreeBSD的netgraph真是太帅了,它到底是个什么玩艺呢?知道Linux的Netfilter的不少,那么就用Netfilter来类比吧。netgraph是一个基于图的钩子系统,正如其名称所展示的那样,什么样的图呢?很简单,就是通过边连接的节点,和数据结构里面学到的一样。netgraph系统挂接在内核协议栈的特定点上,哪些点呢?这个和Netfilter很类似,但是却不是Netfilter精心设计的那5个点,而是更简单的每一层处理的输入点和输出点,如下图所示:
netgraph到底长什么样子呢?到目前为止,我们只是知道了一张图挂上去了,这仅仅是个接口,一个开始,既然挂上去了,数据包就从此处进入这张图了,把它叫做地图更加适合,因此从此以后,数据包就要在游历于这张地图了,最终的结果有两个:
1.数据包从地图的某处出来,重新进入系统标准的协议栈的当初被拦截的那个地方;
2.数据包再也没有出来回到原点,要么被地图吃掉了(进入了某一房间?),要么就是从某处出去,进入协议栈的别的地方。
以上两点很类似于Netfilter的ACCEPT,STOLEN这样的结果,仔细想想不是么?netgraph和标准协议栈的衔接如下图所示:
既然知道了netgraph的位置,那么下面就看看它的样子吧。还是先给出一幅图
该图中有两种元素,一种是节点,另一种是连接到节点的边的两端的顶点。在netgraph的术语中,节点就是Node,而顶点叫做hook,一条边连接两个hook,hook通过CONNECT/MKPEER构成一条边。从上图中可以看出,一条边的两端必然有两个hook,从命名上可以看出这些“边的端点”其实就是真正处理数据的地方,而Node其实就是一个“数据+操作”的封装,一个Node可以有多个hook,通过这些hook连接到其它的Node。
我们可以用OO的思想来理解这些个netgraph的概念,Node就是一个对象,每一个Node都有它所属的Type,可以将Type理解成类。而hook其实就是一个Node对象的私有数据,整个graph通过“各个hook的对接”来完成,FreeBSD提供了丰富的命令来完成netgraph的构建,说白了其实就是以下几步骤:
1.生成一系列的Node对象;
2.为每一个Node定义一个或多个hook;
3.将特定的Node通过hook连接在一起。
如此一来整个graph就构建好了,FreeBSD提供了struct ng_type,它便是代表了一个类,然后你每生成一个特定ng_type的实例就相当于生成了一个对象,通过对该结构体里面的一些字段的理解,我们就可以完整理解数据包在这个graph中的游历过成了。struct ng_type定义如下:
注释很清楚了,自不必说,如果我们看看其中一些回调函数的定义,就更能理解了。“构造函数”和“析构函数”都有,每一个“成员函数”的参数列表的第一个参数类型都是node_p,这难道不是this么?这里唯一要注意的就是rcvdata回调函数,该函数接收从另一个Node发送过来的数据,接收者是hook,而不是Node,再次强调,Node之间通过hook相连接,而不是通过node本身,然而每一个hook都要唯一绑定一个Node对象,因此我们可以从hook解析出唯一的Node对象,却不能从Node中直接得到hook(一个Node对象拥有N多hook呢),要分清一对一和一对多的关系。因此rcvdata的第一个参数是hook_p就是合理的了。
Node和Node之间通过hook传递控制信息,而网络数据包则是通过一个hook向其peer hook发送消息的方式完成的,当然所谓的发送消息大多数情况下就是函数直接调用。既然一条边两端有两个hook,那么每一个hook就有一个peer,每当我们将数据包发送到一个hook的时候,实际的效果就是数据包被发送到了该hook的peer,这是netgraph的核心逻辑实现的,我们可以从下面的这个核心宏中看到这一点:
其中ng_address_hook完成了peer的定位,这个peer可以通过ngctl命令来设置。
就这样,一个数据包在整个netgraph中通过“离开一个Node的某个hook,进入另一个Node的某个hook的rcvdata”的方式游历,Node在这里的作用就是封装私有数据和统一的操作,当然,你可以重载掉一个Node内统一的rcvdata回调函数,而是为每一个hook都设置一个私有的rcvdata回调函数,再次强调,是hook在rcvdata,而不是Node在rcvdata,Node的rcvdata是一个该Node所有hook通用的回调函数,如果没有hook私有的rcvdata,该通用函数将被调用,ng_snd_item最终将进入下面的逻辑:
由此看出,Node有一个默认的对所有hook都适用的rcvdata回调函数,然而各个hook可以重载掉这个默认的rcvdata回调函数。
接下来我们看一下netgraph如何和协议栈对接,不要把操作系统想得太神奇,实际上完成这种工作只需要一个回调函数即可。以以太网接收为例,以太网接收处理函数中会调用ng_ether_input_p回调函数,你只需要将其定义一下即可,对于很多场合都使用的ng_ether,它将此函数定义为:
最后通过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的话,就会调用下面的函数:
如果本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处理,主要你能根据协议处理逻辑构建好一张图,将这张图挂接在协议栈,甚至挂接在驱动上,你就能很方便的实现网络的任意扩展...
最后看一下netgraph的依赖关系,在netgraph中,每张图都是相对独立的,数据包从某处进入一张图A,然后从某处出来,在另一处再进入图B,此时它将不能再使用图A。这和Netfilter不同,Netfilter基于HOOK设计,使用一些match来进行filter,比如NAT就需要ip_conntrack,ctdir需要ip_conntrack等等,ip_conntrack一直都面临table full的问题,因此你要用raw表的NOTRACK这个target来免除追踪不感兴趣流量来缓解这个问题。有下面的需求:
从网段M发出到网段N的流量(两个方向)打上tag待策略路由来处理,从网段N发出到网段M的流量(两个方向)不打tag。
分析:
很显然要使用ctdir这个match,否则将会过滤掉返回流量,于是有以下target为NOTRACK的match:
!dst N/interface $内网口
然而意味着从网段N发出到达M的返回流量也将被conntrack,这是因为ctdir和conntrack相互依赖才导致了这样的问题,在raw表中,你甚至都不知道数据包到底是走INPUT还是FORWARD,所以你很难让所有这一切关联起来,虽然conntrack可以保持一个流信息在内存中,但是却可能存在大量不相关的流也被保存。如果使用netgraph呢?很简单,我们可以写在两个命令中:
No.x check-status
No.z skip No.y from N to M #对于返回流量,只检测conntrack
No.y netgraph tag from M to N keep-status
如此即可。FreeBSD不需要conntrack,它内建了一个动态ruleset,凡是keep-status的流量都将自动将返回流量加入动态ruleset中,实际上也就是“保持了一个流信息在内存中”,FreeBSD的conntrack和单独的rule相关联而不是和整个协议栈关联,这实际上也是netgraph的思想,我们看一下rule相关的conntrack和协议栈香瓜的conntrack的区别:
IPFW:没有全局的conntrack信息,然而需要查询动态ruleset,以匹配返回流量;
Netfilter:需要查询全局的conntrack表,可以取出一切头包经过时流量的匹配结果,不需要也没有动态ruleset
我们看一下全局的conntrack和全局的ruleset所针对的对象有何不同。很简单,全局的conntrack针对除了NOTRACK的所有的数据包,然而如果NOTRACK需要指明方向,就会需要循环依赖,问题将无解。全局的动态ruleset仅仅针对匹配到的数据包,对其它的没有匹配到的数据包除了一个查询性能影响之外没有其他影响,事到如今,我想查询性能应该不是问题吧,再说动态ruleset一般都比全局conntrackset小得多,查询conntrackset都不怕,查询动态ruleset就怕了么?换句话说,Netfilter的ip_conntrack是宁可枉杀一千,不能使一人漏网,而ipfw则是精确的匹配。效率啊,BSD不愧是网络领头军!
netgraph到底长什么样子呢?到目前为止,我们只是知道了一张图挂上去了,这仅仅是个接口,一个开始,既然挂上去了,数据包就从此处进入这张图了,把它叫做地图更加适合,因此从此以后,数据包就要在游历于这张地图了,最终的结果有两个:
1.数据包从地图的某处出来,重新进入系统标准的协议栈的当初被拦截的那个地方;
2.数据包再也没有出来回到原点,要么被地图吃掉了(进入了某一房间?),要么就是从某处出去,进入协议栈的别的地方。
以上两点很类似于Netfilter的ACCEPT,STOLEN这样的结果,仔细想想不是么?netgraph和标准协议栈的衔接如下图所示:
既然知道了netgraph的位置,那么下面就看看它的样子吧。还是先给出一幅图
该图中有两种元素,一种是节点,另一种是连接到节点的边的两端的顶点。在netgraph的术语中,节点就是Node,而顶点叫做hook,一条边连接两个hook,hook通过CONNECT/MKPEER构成一条边。从上图中可以看出,一条边的两端必然有两个hook,从命名上可以看出这些“边的端点”其实就是真正处理数据的地方,而Node其实就是一个“数据+操作”的封装,一个Node可以有多个hook,通过这些hook连接到其它的Node。
我们可以用OO的思想来理解这些个netgraph的概念,Node就是一个对象,每一个Node都有它所属的Type,可以将Type理解成类。而hook其实就是一个Node对象的私有数据,整个graph通过“各个hook的对接”来完成,FreeBSD提供了丰富的命令来完成netgraph的构建,说白了其实就是以下几步骤:
1.生成一系列的Node对象;
2.为每一个Node定义一个或多个hook;
3.将特定的Node通过hook连接在一起。
如此一来整个graph就构建好了,FreeBSD提供了struct ng_type,它便是代表了一个类,然后你每生成一个特定ng_type的实例就相当于生成了一个对象,通过对该结构体里面的一些字段的理解,我们就可以完整理解数据包在这个graph中的游历过成了。struct ng_type定义如下:
struct ng_type { u_int32_t version; /* must equal NG_API_VERSION */ const char *name; /* Unique type name */ modeventhand_t mod_event; /* Module event handler (optional) */ ng_constructor_t *constructor; /* Node constructor */ ng_rcvmsg_t *rcvmsg; /* control messages come here */ ng_close_t *close; /* warn about forthcoming shutdown */ ng_shutdown_t *shutdown; /* reset, and free resources */ ng_newhook_t *newhook; /* first notification of new hook */ ng_findhook_t *findhook; /* only if you have lots of hooks */ ng_connect_t *connect; /* final notification of new hook */ ng_rcvdata_t *rcvdata; /* data comes here */ ng_disconnect_t *disconnect; /* notify on disconnect */ const struct ng_cmdlist *cmdlist; /* commands we can convert */ LIST_ENTRY(ng_type) types; /* linked list of all types */ int refs; /* number of instances */ };
注释很清楚了,自不必说,如果我们看看其中一些回调函数的定义,就更能理解了。“构造函数”和“析构函数”都有,每一个“成员函数”的参数列表的第一个参数类型都是node_p,这难道不是this么?这里唯一要注意的就是rcvdata回调函数,该函数接收从另一个Node发送过来的数据,接收者是hook,而不是Node,再次强调,Node之间通过hook相连接,而不是通过node本身,然而每一个hook都要唯一绑定一个Node对象,因此我们可以从hook解析出唯一的Node对象,却不能从Node中直接得到hook(一个Node对象拥有N多hook呢),要分清一对一和一对多的关系。因此rcvdata的第一个参数是hook_p就是合理的了。
Node和Node之间通过hook传递控制信息,而网络数据包则是通过一个hook向其peer hook发送消息的方式完成的,当然所谓的发送消息大多数情况下就是函数直接调用。既然一条边两端有两个hook,那么每一个hook就有一个peer,每当我们将数据包发送到一个hook的时候,实际的效果就是数据包被发送到了该hook的peer,这是netgraph的核心逻辑实现的,我们可以从下面的这个核心宏中看到这一点:
#define NG_FWD_ITEM_HOOK_FLAGS(error, item, hook, flags) \ do { \ (error) = \ ng_address_hook(NULL, (item), (hook), NG_NOFLAGS); \ if (error == 0) { \ SAVE_LINE(item); \ (error) = ng_snd_item((item), (flags)); \ } \ (item) = NULL; \ } while (0)
其中ng_address_hook完成了peer的定位,这个peer可以通过ngctl命令来设置。
就这样,一个数据包在整个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处理,主要你能根据协议处理逻辑构建好一张图,将这张图挂接在协议栈,甚至挂接在驱动上,你就能很方便的实现网络的任意扩展...
最后看一下netgraph的依赖关系,在netgraph中,每张图都是相对独立的,数据包从某处进入一张图A,然后从某处出来,在另一处再进入图B,此时它将不能再使用图A。这和Netfilter不同,Netfilter基于HOOK设计,使用一些match来进行filter,比如NAT就需要ip_conntrack,ctdir需要ip_conntrack等等,ip_conntrack一直都面临table full的问题,因此你要用raw表的NOTRACK这个target来免除追踪不感兴趣流量来缓解这个问题。有下面的需求:
从网段M发出到网段N的流量(两个方向)打上tag待策略路由来处理,从网段N发出到网段M的流量(两个方向)不打tag。
分析:
很显然要使用ctdir这个match,否则将会过滤掉返回流量,于是有以下target为NOTRACK的match:
!dst N/interface $内网口
然而意味着从网段N发出到达M的返回流量也将被conntrack,这是因为ctdir和conntrack相互依赖才导致了这样的问题,在raw表中,你甚至都不知道数据包到底是走INPUT还是FORWARD,所以你很难让所有这一切关联起来,虽然conntrack可以保持一个流信息在内存中,但是却可能存在大量不相关的流也被保存。如果使用netgraph呢?很简单,我们可以写在两个命令中:
No.x check-status
No.z skip No.y from N to M #对于返回流量,只检测conntrack
No.y netgraph tag from M to N keep-status
如此即可。FreeBSD不需要conntrack,它内建了一个动态ruleset,凡是keep-status的流量都将自动将返回流量加入动态ruleset中,实际上也就是“保持了一个流信息在内存中”,FreeBSD的conntrack和单独的rule相关联而不是和整个协议栈关联,这实际上也是netgraph的思想,我们看一下rule相关的conntrack和协议栈香瓜的conntrack的区别:
IPFW:没有全局的conntrack信息,然而需要查询动态ruleset,以匹配返回流量;
Netfilter:需要查询全局的conntrack表,可以取出一切头包经过时流量的匹配结果,不需要也没有动态ruleset
我们看一下全局的conntrack和全局的ruleset所针对的对象有何不同。很简单,全局的conntrack针对除了NOTRACK的所有的数据包,然而如果NOTRACK需要指明方向,就会需要循环依赖,问题将无解。全局的动态ruleset仅仅针对匹配到的数据包,对其它的没有匹配到的数据包除了一个查询性能影响之外没有其他影响,事到如今,我想查询性能应该不是问题吧,再说动态ruleset一般都比全局conntrackset小得多,查询conntrackset都不怕,查询动态ruleset就怕了么?换句话说,Netfilter的ip_conntrack是宁可枉杀一千,不能使一人漏网,而ipfw则是精确的匹配。效率啊,BSD不愧是网络领头军!