利用nf_conntrack机制存储路由,省去每包路由查找
IP是無連接的,因此IP路由是每包一路由的,數(shù)據(jù)包通過查找路由表獲取路由,這是現(xiàn)代操作協(xié)議協(xié)議棧IP路由的默認(rèn)處理方式。但是如果協(xié)議棧具有流識別能力,是不是可以基于流來路由呢?答案無疑是肯定的。
設(shè)計思想
在Linux的實現(xiàn)中,nf_conntrack可以做到基于流的IP路由,大致思想就是,僅僅針對一個流的第一個正向包和第一個反向包查找標(biāo)準(zhǔn)的IP路由表,將結(jié)果保存在conntrack項中,后續(xù)的屬于同一流的數(shù)據(jù)包直接取出路由項來使用。背后的思想是:這可以省去查找路由表的開銷,是這樣嗎?也不全是!關(guān)鍵是,將一個數(shù)據(jù)包對應(yīng)到一個數(shù)據(jù)流,這本身就需要一個查找匹配的過程,如果能將路由保存在conntrack里面,那么conntrack查找和路由查找就可以合并成一次查找。因此,查找是免不了的,只是換了地方而已,如果有了conntrack,仍然進(jìn)行標(biāo)準(zhǔn)的基于包的IP路由查找過程,那就是平白多了一次查找。
實現(xiàn)思想
在實現(xiàn)上,很簡單,那就是 盡量在數(shù)據(jù)包離開協(xié)議棧的地方設(shè)置skb的路由到conntrack。之所以可以這么做是因為不管是POSTROUTING還是INPUT,都是在路由之后,如果前面進(jìn)行了基于包的IP路由查找,此時skb上一定綁定了dst_entry,將其綁到conntrack里面即可。另外,在數(shù)據(jù)包剛進(jìn)入?yún)f(xié)議棧的地方 試圖從conntrack項中取出路由,然后直接將其設(shè)置到skb上。整個處理過程類似skb-mark和conntrack mark的處理方式:
有了以上的理解,代碼就很簡單了
#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_release, 需要到注冊ACCT 調(diào)用destroy方法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) {// 如果在此設(shè)置了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);在以上的實現(xiàn)思想的文字描述中,我使用了盡量和試圖兩個不那么明確的詞,這就牽扯到了流路由的老化機制。
老化思想
標(biāo)準(zhǔn)的路由查找是每個包都要查找,而如今引入了流路由之后,便不需要對skb進(jìn)行路由查找了,取而代之的是直接從conntrack取出路由設(shè)置給skb,這個conntrack上的路由就是第一次的時候針對skb查找路由表的結(jié)果。那么就會引入一個問題,即什么時候再次針對skb查找路由表以便更新conntrack的路由。這個問題沒法直接回答,對于路由一直穩(wěn)定的網(wǎng)絡(luò),根本不需要重新查找,因為針對一個流的第一個正向包和第一個反向包的路由查找結(jié)果在該流的生命周期中將一直有效,畢竟路由沒有改變,但是如果在流的生命周期內(nèi)一條相關(guān)的路由發(fā)生了改變,就需要重新更新conntrack的路由結(jié)果。
? ? ? ?因此可以說,引入一個通知機制就能解決這個問題。每當(dāng)路由發(fā)生改變的時候,在PREROUTING的hook中,不再執(zhí)行:
而這個非常容易,使用內(nèi)核的Notifier機制就可以了,在任何路由改變的時候,通知上述的流路由模塊改變一個標(biāo)志位,在PREROUTING的hook中,發(fā)現(xiàn)該標(biāo)志位置位,就不執(zhí)行skb_dst_set。如此一來,上述的代碼就會變?yōu)橄旅娴?#xff1a;
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;// 無條件設(shè)置流的路由。skb的dst可能來自兩個地方:// 1.來自ipv4_conntrack_setdst;// 2.來自標(biāo)準(zhǔn)的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;// 只有標(biāo)志為1,才信任流路由,并且設(shè)置給skbif (flag == 1) {skb_dst_set(skb, dst_info->dst[dir]);}}return NF_ACCEPT; }然而,把這件事交給用戶態(tài)或許更好些。畢竟內(nèi)核態(tài)發(fā)生的所有事情,用戶態(tài)都有辦法監(jiān)控到,我認(rèn)為用一個procfs的可寫文件來通知flag變?yōu)?或者變?yōu)?可能更好,即flag的值由用戶來設(shè)置,這樣用戶就可以在任意時刻啟用,停用流路由機制,比如使用iproute2的monitor機制監(jiān)控到了路由的改變,如果是無關(guān)路由改變了,那么就不更新flag,只有是相關(guān)的路由改變了,才更新,何其靈活。
?
總結(jié)
以上是生活随笔為你收集整理的利用nf_conntrack机制存储路由,省去每包路由查找的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 拆解 Linux 网络包发送过程
- 下一篇: 反病毒引擎设计全解(一)