上文介紹了netfilter機制下,如何重造並發送一個skb,涉及到核心協議棧編程,而不是我們平時所說的使用者層socket網路編程。
我們先來介紹下上面skb重構程式涉及到的幾個函數:
首先,有必要說下,也是後面每段程式中都有說道的,就是開發源碼樹版本是3.13的,這個版本的skb_buff和我們常見的2.4、2.6有很大的不同。
一、主要看一下四個欄位:
//typedef unsigned int sk_buff_data_t; sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data;
其表示含義如下圖所示《深入理解linux網路技術內幕》
head和end指向緩衝區的頭部和尾部,而data和tail指向實際資料的頭部和尾部,每一層會在head和data之間填充協議頭,或者在tail和end之間添加新的協議資料。
所以根據上面的分析,head和data之間是協議頭和承載,比如傳輸層、網路層、乙太網路幀頭。
重點:sk_buff 中的資料部分包含協議層頭部和承載。
下面就是sk_buff中各個頭部的長度欄位:
//局部 __u16 transport_header; __u16 network_header; __u16 mac_header;//sk_buff取消了在該資料結構內放置聯合體union欄位,//所以編程的時候不能直接調用sk_buff中的聯合體成員定位到對應的傳輸層頭部、網路層頭部等。//但是卻提供給我們更為便捷的方式://舉網路層為例static inline struct iphdr *ip_hdr(const struct sk_buff *skb){ return (struct iphdr *)skb_network_header(skb);}static inline unsigned char *skb_network_header(const struct sk_buff *skb){ return skb->head + skb->network_header;}//和我們在linux網路協議棧分析的如出一轍,就是通過先定位整個換緩衝區的頭端,//再根據協議層的位移量定位該協議層,至於協議頭的順序..都知道
二、alloc_skb 和 skb_reserve
還有,對於alloc_skb(),我們只需要指定其資料部分的大小即可(應用程式層資料+協議層頭部),該函數內部會額外把skb_buff所佔的那部分空間也給分配出來,這無需我們操心。
skb_reserve(),是一個定位函數,就是前面alloc一個skb之後,用這個函數調整下data和tail資料指標
static inline void skb_reserve(struct sk_buff *skb, int len){ skb->data += len; skb->tail += len;}//len取多大,取決於你後面怎麼填充資料了
skb_reserve() 這個函數就是在緩衝區中預留skb協議頭部以及有效資料部分的空間,空間可大可小。
經過前面的alloc_skb和skb_reserve,現在那兩個指標(data和tail)發生了變化:
alloc_skb():
//tail以及tail之前的欄位初始化為0 memset(skb, 0, offsetof(struct sk_buff, tail)); /* Account for allocated memory : skb + skb->head */... skb->head = data; skb->data = data;... skb->end = skb->tail + size;//head和data均指向分配的skb資料部分(協議頭+承載)的首地址位置,//end則指向尾端位置,tail值就是0
skb_reserve(skb, len)之後,data和tail的指標發生了變化,換句話說在head與data之間預留了len大小的空間用於填充協議頭和承載。
上面預留了整體空間,下面我們還要細分空間,上面的整體空間包括了各個協議層頭部和承載部分,我們還得按照各層協議頭部的防止順序合理劃分空間:
三、skb_push 和 skb_put
怎麼分,我們前面用的是 skb_push()
unsigned char *skb_push(struct sk_buff *skb, unsigned int len){ skb->data -= len; skb->len += len; if (unlikely(skb->data<skb->head)) skb_under_panic(skb, len, __builtin_return_address(0)); return skb->data;}//看到起實現,我們就知道,調用這個函數來細分空間,必須嚴格按照資料幀的格式來劃分//乙太網路幀頭 | 網路層首部 | 傳輸層首部 | 應用程式層資料//所以先得細分應用程式層資料,最後是乙太網路幀頭//eg, pdata = skb_push(skb, pkt_len); udph = (struct udphdr*)skb_push(skb, sizeof(struct udphdr)); iph = (struct iphdr*)skb_push(skb, sizeof(struct iphdr)); ethdr = (struct ethhdr*)skb_push(skb, sizeof(struct ethhdr));
如果前面你看懂了,知道sk_buff 的資料空間布局的話,這個函數一看便知。額,sk_buff 中有好幾個len,分別表徵承載長度,各個協議頭部長度,以及整個長度。
再來看看 skb_put()
unsigned char *skb_put(struct sk_buff *skb, unsigned int len){ unsigned char *tmp = skb_tail_pointer(skb); SKB_LINEAR_ASSERT(skb); skb->tail += len; skb->len += len; if (unlikely(skb->tail > skb->end)) skb_over_panic(skb, len, __builtin_return_address(0)); return tmp;}//看代碼,這裡改動的是tail(資料的尾部指標),所以這裡就是在資料區的尾部追加資料,//和skb_push()恰恰是反過來的
所以調用skb_put() 的話,前面的skb_reserve()就只需要預留乙太網路幀頭的空間,後面則調用skb_put(),依次往後追加細分網路層頭部、協議層頭部和承載部分了。
兩個函數都是用於細分空間,返回細分空間後的首部地址指標,便於後續的協議頭部以及承載資料填充。
空間細分之後就是單純的協議層欄位填充和承載填充了。
可以看出來建立skb幾個重要的函數介面就是:
alloc_skb、skb_reserver、skb_push、skb_put
後面三個函數其實都是簡單的修改指標操作。
上篇及本篇介紹的都是自己建立一個skb,實際上我們還可以拷貝(skb_copy())接收到的skb,然後針對性的修改發送出去。或者直接修改接收到的skb。由於接收到的skb,空間已經細分好,我們則不需要以上後面三個函數,直接調用以下幾個函數定位到各個協議層頭部:
static inline struct ethhdr *eth_hdr(const struct sk_buff *skb);static inline struct iphdr *ip_hdr(const struct sk_buff *skb);static inline struct tcphdr *tcp_hdr(const struct sk_buff *skb);static inline struct udphdr *udp_hdr(const struct sk_buff *skb);...
然後針對性的修改即可,其餘原理是差不多的,這裡就不額外介紹了。
參考資料:
《深入理解Linux網路技術內幕》
http://www.2cto.com/os/201502/376226.html