利用nf_conntrack机制存储路由,省去每包路由查找

简介:

IP是无连接的,因此IP路由是每包一路由的,数据包通过查找路由表获取路由,这是现代操作协议协议栈IP路由的默认处理方式。但是如果协议栈具有流识别能力,是不是可以基于流来路由呢?答案无疑是肯定的。

设计思想

在Linux的实现中,nf_conntrack可以做到基于流的IP路由,大致思想就是,仅仅针对一个流的第一个正向包和第一个反向包查找标准的IP路由表,将结果保存在conntrack项中,后续的属于同一流的数据包直接取出路由项来使用。背后的思想是:这可以省去查找路由表的开销,是这样吗?也不全是!关键是,将一个数据包对应到一个数据流,这本身就需要一个查找匹配的过程,如果能将路由保存在conntrack里面,那么conntrack查找和路由查找就可以合并成一次查找。因此,查找是免不了的,只是换了地方而已,如果有了conntrack,仍然进行标准的基于包的IP路由查找过程,那就是平白多了一次查找。

实现思想

在实现上,很简单,那就是 尽量 在数据包离开协议栈的地方设置skb的路由到conntrack。之所以可以这么做是因为不管是POSTROUTING还是INPUT,都是在路由之后,如果前面进行了基于包的IP路由查找,此时skb上一定绑定了dst_entry,将其绑到conntrack里面即可。另外,在数据包刚进入协议栈的地方 试图 从conntrack项中取出路由,然后直接将其设置到skb上。整个处理过程类似skb-mark和conntrack mark的处理方式:
-A PREROUTING -m mark --mark 100 -j ACCEPT 
-A PREROUTING -j CONNMARK --restore-mark --nfmask 0xffffffff --ctmask 0xffffffff 
-A PREROUTING -m mark ! --mark 0x0 -j ACCEPT 
...... 慢速匹配过程
-A PREROUTING .....   -j MARK --set-mark 100
.....慢速匹配过程
-A POSTROUTING -m mark ! --mark 0x0 -j CONNMARK --save-mark --nfmask 0xffffffff --ctmask 0xffffffff  

有了以上的理解,代码就很简单了


#include <linux/ip.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/version.h>
#include <net/netfilter/nf_conntrack.h>
#include <net/dst.h>
#include <net/netfilter/nf_conntrack_acct.h>


MODULE_AUTHOR("xtt");
MODULE_DESCRIPTION("gll");
MODULE_LICENSE("GPL");
MODULE_ALIAS("XTT and GLL");

struct nf_conn_priv {
        struct nf_conn_counter ncc[IP_CT_DIR_MAX];
        struct dst_entry *dst[IP_CT_DIR_MAX];
};

static unsigned int ipv4_conntrack_getdst (unsigned int hooknum,
                                      struct sk_buff *skb,
                                      const struct net_device *in,
                                      const struct net_device *out,
                                      int (*okfn)(struct sk_buff *))
{
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
        struct nf_conn_counter *acct;
        struct nf_conn_priv *dst_info;
        ct = nf_ct_get(skb, &ctinfo);
        if (!ct || ct == &nf_conntrack_untracked)
                return NF_ACCEPT;
        acct = nf_conn_acct_find(ct);
        if (acct) {
                int dir = CTINFO2DIR(ctinfo);
                dst_info = (struct nf_conn_priv *)acct;
                if (dst_info->dst[dir] == NULL) {
                        dst_hold(skb_dst(skb));
                        dst_info->dst[dir] = skb_dst(skb);
                }
        }
        return NF_ACCEPT;
}

static unsigned int ipv4_conntrack_setdst (unsigned int hooknum,
                                      struct sk_buff *skb,
                                      const struct net_device *in,
                                      const struct net_device *out,
                                      int (*okfn)(struct sk_buff *))
{
        struct nf_conn *ct;
        enum ip_conntrack_info ctinfo;
        struct nf_conn_counter *acct;
        struct nf_conn_priv *dst_info;
        ct = nf_ct_get(skb, &ctinfo);
        if (!ct || ct == &nf_conntrack_untracked)
                return NF_ACCEPT;
        acct = nf_conn_acct_find(ct);
        if (acct) {
                int dir = CTINFO2DIR(ctinfo);
                dst_info = (struct nf_conn_priv *)acct;
                if (dst_info->dst[dir] != NULL) {
                       // 如果在此设置了skb的dst,那么在ip_rcv_finish中就不会再去查找路由表了
                        skb_dst_set(skb, dst_info->dst[dir]);
                }
        }
        return NF_ACCEPT;
}
static struct nf_hook_ops ipv4_conn_dst_info[] __read_mostly = {
        {
                .hook           = ipv4_conntrack_getdst,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_POST_ROUTING,
                .priority       = NF_IP_PRI_CONNTRACK + 1,
        },
        {
                .hook           = ipv4_conntrack_getdst,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_LOCAL_IN,
                .priority       = NF_IP_PRI_CONNTRACK + 1,
        },
        {
                .hook           = ipv4_conntrack_setdst,
                .owner          = THIS_MODULE,
                .pf             = NFPROTO_IPV4,
                .hooknum        = NF_INET_PRE_ROUTING,
                .priority       = NF_IP_PRI_CONNTRACK + 1,
        },
};

static int __init test_info_init(void)
{
        int err;
        err = nf_register_hooks(ipv4_conn_dst_info, ARRAY_SIZE(ipv4_conn_dst_info));
        if (err) {
                return err;
        }
        return err;
}

static void __exit test_info_exit(void)
{
        nf_unregister_hooks(ipv4_conn_dst_info, ARRAY_SIZE(ipv4_conn_dst_info));
}

module_init(test_info_init);
module_exit(test_info_exit);

在以上的实现思想的文字描述中,我使用了尽量和试图两个不那么明确的词,这就牵扯到了流路由的老化机制。老化思想
标准的路由查找是每个包都要查找,而如今引入了流路由之后,便不需要对skb进行路由查找了,取而代之的是直接从conntrack取出路由设置给skb,这个conntrack上的路由就是第一次的时候针对skb查找路由表的结果。那么就会引入一个问题,即什么时候再次针对skb查找路由表以便更新conntrack的路由。这个问题没法直接回答,对于路由一直稳定的网络,根本不需要重新查找,因为针对一个流的第一个正向包和第一个反向包的路由查找结果在该流的生命周期中将一直有效,毕竟路由没有改变,但是如果在流的生命周期内一条相关的路由发生了改变,就需要重新更新conntrack的路由结果。
      因此可以说,引入一个通知机制就能解决这个问题。每当路由发生改变的时候,在PREROUTING的hook中,不再执行:
skb_dst_set(skb, dst_info->dst[dir]);
而这个非常容易,使用内核的Notifier机制就可以了,在任何路由改变的时候,通知上述的流路由模块改变一个标志位,在PREROUTING的hook中,发现该标志位置位,就不执行skb_dst_set。如此一来,上述的代码就会变为下面的:
static unsigned int ipv4_conntrack_getdst (unsigned int hooknum,
                                      struct sk_buff *skb,
                                      const struct net_device *in,
                                      const struct net_device *out,
                                      int (*okfn)(struct sk_buff *))
{
...
        if (acct) {
                int dir = CTINFO2DIR(ctinfo);
                dst_info = (struct nf_conn_priv *)acct;
        // 无条件设置流的路由。skb的dst可能来自两个地方:
        // 1.来自ipv4_conntrack_setdst;
        // 2.来自标准的IP路由查找
                dst_hold(skb_dst(skb));
                dst_info->dst[dir] = skb_dst(skb);
        }
        return NF_ACCEPT;
}

static unsigned int ipv4_conntrack_setdst (unsigned int hooknum,
                                      struct sk_buff *skb,
                                      const struct net_device *in,
                                      const struct net_device *out,
                                      int (*okfn)(struct sk_buff *))
{
...
        if (acct) {
                int dir = CTINFO2DIR(ctinfo);
                dst_info = (struct nf_conn_priv *)acct;
        // 只有标志为1,才信任流路由,并且设置给skb
                if (flag == 1) {
                        skb_dst_set(skb, dst_info->dst[dir]);
                }
        }
        return NF_ACCEPT;
}
然而,把这件事交给用户态或许更好些。毕竟内核态发生的所有事情,用户态都有办法监控到,我认为用一个procfs的可写文件来通知flag变为1或者变为0可能更好,即flag的值由用户来设置,这样用户就可以在任意时刻启用,停用流路由机制,比如使用iproute2的monitor机制监控到了路由的改变,如果是无关路由改变了,那么就不更新flag,只有是相关的路由改变了,才更新,何其灵活。


 本文转自 dog250 51CTO博客,原文链接: http://blog.51cto.com/dog250/1398368

相关文章
|
5月前
|
算法 网络协议 网络架构
【网络层】动态路由算法、自治系统AS、IP数据报格式
【网络层】动态路由算法、自治系统AS、IP数据报格式
35 0
|
负载均衡 网络协议 算法
在隧道中使用 IPv6 流标签进行等价多路径路由和链路聚合
本文档是 Internet 工程任务组 (IETF) 的产品。它代表了 IETF 社区的共识。它已接受公众审查,并已获互联网工程指导小组 (IESG) 批准出版。有关 Internet 标准的更多信息,请参见 RFC 5741 的第 2 节。
160 0
在隧道中使用 IPv6 流标签进行等价多路径路由和链路聚合
|
负载均衡 监控 网络协议
边界网关协议 - 段路由的链路状态 (BGP-LS) 扩展
段路由 (Segment Routing,SR) 允许通过将路径编码为称为“段”的拓扑子路径序列来灵活定义端到端路径。这些段由路由协议通告,例如 IGP 拓扑中的链路状态路由协议(IS-IS、OSPFv2 和 OSPFv3)。
1340 0
边界网关协议 - 段路由的链路状态 (BGP-LS) 扩展
|
安全 物联网 网络虚拟化
路由与交换系列之基本IP ACL特性与配置
• 掌握基本IP ACL的原理 • 掌握ACL在企业网络中的应用 • 掌握基本IP ACL的配置方式 • 掌握基本IP ACL的验证效果
4190 8
  路由与交换系列之基本IP ACL特性与配置
|
网络协议 网络架构
路由与交换系列之VRRP协议特性与配置小实践
1. 深刻理解VRRP协议原理。 2. 掌握VRRP协议的配置方法
172 1
路由与交换系列之VRRP协议特性与配置小实践
|
网络协议 网络架构
IP路由基础、路由器静态路由配置方法、自治系统、缺省路由的配置方法、路由选路规则、缺省路由、备份路由、等价路由、三种查询路由表命令
路由器特点,网络IP地址规划网络间的特性,基本路由思想,编辑静态路由部分,查询设备整个路由表,查看特定的路由协议时使用,查询目的地址2.2.2.2的路由条目,IP路由表代码写法,IP路由表里的信息,路由表来源,路由表的信息,路由表选路规则,缺点:缺省路由,备份路由,等价路由,做实验的步骤......
IP路由基础、路由器静态路由配置方法、自治系统、缺省路由的配置方法、路由选路规则、缺省路由、备份路由、等价路由、三种查询路由表命令
|
网络协议 Linux
Linux网络管理之ip命令 – 显示与操作路由
ip命令用来显示或操纵Linux主机的路由、网络设备、策略路由和隧道,是Linux下较新的功能强大的网络配置工具。
184 0
Linux网络管理之ip命令 – 显示与操作路由