由於【智能路由器】系列系列部落格中好幾篇文章都用到了netfilter來實現路由器中的部分功能,所以寫這篇部落格來闡述一下我在使用netfilter架構編程時的看法。
我盡量以簡潔的語言描述netfilter編程要點。 5個鉤子點
分別為:PREROUTING、POSTOUTING、INPUT、FORWARD、OUTPUT,下面給了一個很簡潔的架構圖,想必大家很熟悉
注意到:我在netfilter的5個鉤子架構上用紅色的字型標記了一下DNAT和SNAT的位置,為什麼要標出。因為他們很重要,也很特別,它們影響我們在鉤子點上截取的TCP/IP報文的內容。後面再說。
至於5個鉤子點的作用我也不必再說(在此節省篇幅),因為一般介紹netfilter的的文章都會給出解釋,搜搜好了。 原始碼 註冊/登出鉤子
nerfilter的鉤子註冊/登出需先包含netfilter.h標頭檔
int nf_register_hook(strcut nf_hook_ops *reg); void nf_unregister_hook(struct nf_hook_ops *reg);
一般Linux核心模組的註冊和登出代碼都是這個形式 結構nf_hook_ops
上面的註冊和登出函數裡都有這個結構體,內容如下:
struct nf_hook_ops{ struct list_head list; //鉤子鏈表 nf_hookfn *hook; //鉤子處理函數 struct module *owner; //模組所有者 int pf; //鉤子協議族 int hooknum; //鉤子的位置值 int priority; //鉤子的的優先順序}
仔細看一下這個nf_hookfn類型定義的形式:
typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));
處理結果
鉤子函數是一定要返回的,處理結果代表了網路資料包的命運,有一下幾種:
/* Responses from hook functions. */#define NF_DROP 0 //丟棄該資料包,釋放為他分配的資源#define NF_ACCEPT 1 //保留該資料包,交由下一個hook函數處理#define NF_STOLEN 2 //忘掉該資料包,hook函數處理該資料包,不再經過netfilter處理#define NF_QUEUE 3 //將該資料包插入到使用者空間#define NF_REPEAT 4 //再次調用該hook函數#define NF_STOP 5 //強於NF_ACCEPT,完全接受該資料包,並且後面的所有hook函數都不需處理該資料包#define NF_MAX_VERDICT NF_STOP
鉤子函數的優先順序
在同一個鉤子點下掛載的鉤子函數用優先順序來決定掛載函數的執行先後順序了,鉤子優先順序在Linux核心中定義如下(原始碼在netfilter_ipv4.h中):
enum nf_ip_hook_priorities { NF_IP_PRI_FIRST = INT_MIN, NF_IP_PRI_CONNTRACK_DEFRAG = -400, NF_IP_PRI_RAW = -300, NF_IP_PRI_SELINUX_FIRST = -225, NF_IP_PRI_CONNTRACK = -200, NF_IP_PRI_MANGLE = -150, NF_IP_PRI_NAT_DST = -100, NF_IP_PRI_FILTER = 0, NF_IP_PRI_SECURITY = 50, NF_IP_PRI_NAT_SRC = 100, NF_IP_PRI_SELINUX_LAST = 225, NF_IP_PRI_CONNTRACK_CONFIRM = INT_MAX, NF_IP_PRI_LAST = INT_MAX,};
編寫核心模組
有了以上基礎代碼就可以寫核心模組了,最簡單的例子如下:
#include <linux/moudle>#include <linux/kernel>#include <skbuff.h>//此處省略一些標頭檔static unsigned int my_hook(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *));{ struct iphdr *ip; struct udphdr *udp; if(!skb) return NF_ACCEPT; if(skb->protocol != htons(0x0800)) //抓ip包(排除arp包) return NF_ACCEPT ip = ip_hdr(skb); if(ip->protocol != 17)// 抓取udp資料包 return NFACCEPT; //udp = udp_hdr(skb); //這個函數雖然有,卻不一定能得到正確的結果,這取決於鉤子函數的掛載點和優先順序 udp = (struct udphdr *)(ip+1); if(ntohs(udp->dest) != 53) //過濾udp目的連接埠為53連接埠的資料 return NFACCEPT; return NF_DROP;}struct nf_hook_ops my_ops = { .list = {NULL, NULL}, .hook = my_hook, .pf = PF_INET, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IPPRI_FIRST+1} static int __init m_init(void){ nf_register_hook(&my_ops);}static void __exit m_exit(void){ nf_unregister_hook(&my_ops);}module_init(m_exit);module_exit(m_exit);
好了,全手打,一氣呵成。
該程式的作用是將網域名稱解析的請求資料丟掉。模組將流經netfilter的網路資料包中udp資料包目的連接埠為53的連接埠資料丟棄,而udp目的連接埠為53連接埠的資料就是即將送往DNS伺服器的DNS解析請求資料包。
建議還沒搞懂網路資料包層次的童鞋看看我的【乙太網路資料包】系列文章。 sk_buff結構體
sk_buff是網路資料包結構。恩,要詳解這個估計好幾篇文章都講不完,在此只簡單描述及解釋
struct sk_buff { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; ktime_t tstamp; struct sock *sk; struct net_device *dev; char cb[48] __aligned(8); unsigned long _skb_refdst;#ifdef CONFIG_XFRM struct sec_path *sp;#endif unsigned int len, data_len; __u16 mac_len, hdr_len; union { __wsum csum; struct { __u16 csum_start; __u16 csum_offset; }; }; __u32 priority; kmemcheck_bitfield_begin(flags1); __u8 local_df:1, cloned:1, ip_summed:2, nohdr:1, nfctinfo:3; __u8 pkt_type:3, fclone:2, ipvs_property:1, peeked:1, nf_trace:1; kmemcheck_bitfield_end(flags1); __be16 protocol; void (*destructor)(struct sk_buff *skb);#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE) struct nf_conntrack *nfct;#endif#ifdef NET_SKBUFF_NF_DEFRAG_NEEDED struct sk_buff *nfct_reasm;#endif#ifdef CONFIG_BRIDGE_NETFILTER struct nf_bridge_info *nf_bridge;#endif int skb_iif;#ifdef CONFIG_NET_SCHED __u16 tc_index; /* traffic control index */#ifdef CONFIG_NET_CLS_ACT __u16 tc_verd; /* traffic control verdict */#endif#endif __u32 rxhash; kmemcheck_bitfield_begin(flags2); __u16 queue_mapping:16;#ifdef CONFIG_IPV6_NDISC_NODETYPE __u8 ndisc_nodetype:2, deliver_no_wcard:1;#else __u8 deliver_no_wcard:1;#endif __u8 ooo_okay:1; kmemcheck_bitfield_end(flags2);#ifdef CONFIG_NET_DMA dma_cookie_t dma_cookie;#endif#ifdef CONFIG_NETWORK_SECMARK __u32 secmark;#endif union { __u32 mark; __u32 dropcount; }; __u16 vlan_tci; sk_buff_data_t transport_header; sk_buff_data_t network_header; sk_buff_data_t mac_header; /* These elements must be at the end, see alloc_skb() for details. */ sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; unsigned int truesize; atomic_t users;};
| sk_buff主要成員 |
含義 |
| next |
sk_buff鏈表中的下一個緩衝區 |
| prev |
sk_buff鏈表中的前一個緩衝區,很明顯,sk_buff是在雙向鏈表上的 |
| sk |
本報文所屬的sock結構,此值僅在本機發出的報文中有效,從網路收到的報文此值為空白 |
| tstamp |
報文收到的時間戳記 |
| dev |
收到此報文的網路裝置 |
| transport_header |
傳輸層頭部 |
| network_header |
網路層頭部 |
| mac_header |
連結層頭部 |
| cb |
用於控制緩衝區。每個層都可以使用此指標,將私人資料放置於此。 |
| len |
有效資料長度 |
| data_len |
資料長度 |
| mac_len |
串連層頭部長度,對於乙太網路,指MAC地址所用的長度,為6 |
| hdr_len |
skb的可寫頭部長度 |
| csum |
校正和(包含開始和位移) |
| csum_start |
當開始計算校正和時從skb->head的位移 |
| csum_offset |
從csum_stat開始的位移 |
| local_df |
允許本地分區 |
| pkt_type |
包的類別 |
| priority |
包隊列的優先順序 |
| truesize |
報文緩衝區的大小 |
| head |
報文緩衝區的頭 |
| data |
資料的頭指標 |
| tail |
資料的尾指標 |
| end |
報文緩衝區的尾部 |
鑒於時間和篇幅關係,sk_buff的資料結構圖以及相關操作函數本文就不做介紹了,網上應該能找到。 struct net_device結構體
這是一個巨型結構體,不便在此羅列,網上有文章對此講解,詳細瞭解可前往地址http://blog.chinaunix.net/uid-21807675-id-1814837.html NAT
網路資料包在通過netfilter的時候SNAT和DNAT對網路資料包做了更改,主要做了ip和連接埠映射,當然,輸入裝置net_device也變了,這些都要注意,在編寫程式時,在適當的鉤子點攔截資料包非常有必要,因為netfilter上除了你掛接的一些鉤子函數外還有其他模組在上面掛接了鉤子,這些鉤子都會對資料包產生影響,往往掛錯鉤子點就攔截不到想要的資料。
好了,本篇文章到此結束。