原文連結:http://blog.chinaunix.net/uid-10167808-id-25974.html
本文歡迎自由轉載,但請標明出處,並保證本文的完整性。
作者:Godbach
日期:2009/09/01
一、構造資料包簡析
這裡並不詳細介紹如何在核心中構造資料包,下文如有需要會在適當的位置進行分析。這裡簡單的分析講一下核心態基於Netfilter架構構造資料包的方式。
核心中可以用到的構造資料包的方式,個人認為可以分為兩種。
其一,我們直接用alloc_skb申請一個skb結構體,然後根據實際的應用填充不同的成員,或者基於當前資料包的skb,調用skb_copy_expand()函數等新申請一個nskb,並且拷貝skb的內容。
其二,也是個人比較常用的,就是直接在先前接收到的資料包skb上作修改,主要有源IP、目IP,如果是TCP/UDP協議的話,還有源連接埠目的連接埠號碼。總之,就是根據自己的需求去調整資料包的相關成員即可。
通常,這兩種方式最終可能都要涉及到重新計算各個部分的校正和,這也是必須的。
二、如何發送構造的資料包
承接上文,資料包已經構造完畢,下一步關鍵就是如何發送資料包了。個人這裡總結的有兩種方法。
方法一,就是讓資料包接著按照Netfilter的流程進行傳輸。因為資料包的一些內容已經被更改,尤其是當源IP和目的IP被更改,主要是交換的情況下,是需要確保有路由可查的。
NF架構中查路由的位置一是在PREROUTING之後,而是在LOCALOUT之後。又由於這裡是需要將資料包從本地發送出去。因此,可以考慮讓修改後的資料包從LOCALOUT點發出。
核心代碼中有這種方式的典型體現。本文涉及的相關核心代碼的版本都是2.6.18.3。源檔案為ipt_REJECT.c,函數send_reset用於往當前接收到資料包的源IP上發送RST包,整個函數涉及了資料包的構造和發送,這裡一起做個簡單分析。
/* Send RST reply */
static void send_reset(struct sk_buff *oldskb, int hook)
{
struct sk_buff *nskb;
struct iphdr *iph = oldskb->nh.iph;
struct tcphdr _otcph, *oth, *tcph;
struct rtable *rt;
u_int16_t tmp_port;
u_int32_t tmp_addr;
int needs_ack;
int hh_len;
/* 判斷是否是分區包*/
if (oldskb->nh.iph->frag_off & htons(IP_OFFSET))
return;
/*得到TCP頭部指標*/
oth = skb_header_pointer(oldskb, oldskb->nh.iph->ihl * 4,
sizeof(_otcph), &_otcph);
if (oth == NULL)
return;
/* 當期收到的包就是RST包,就不用再發送RST包了*/
if (oth->rst)
return;
/*檢查資料包的校正和是否正確*/
if (nf_ip_checksum(oldskb, hook, iph->ihl * 4, IPPROTO_TCP))
return;
/*這一步比較關鍵,做的就是更新路由的工作。該函數的主要工作就是將當前資料包的源IP當做路由的目的IP,同時考慮資料包的目的IP,得到去往該源IP的路由*/
if ((rt = route_reverse(oldskb, oth, hook)) == NULL)
return;
hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);
/* 拷貝當前的oldskb,包括skb結構體和資料部分。這就是我們上面提到的構造資料包的第一種方式*/
nskb = skb_copy_expand(oldskb, hh_len, skb_tailroom(oldskb),
GFP_ATOMIC);
if (!nskb) {
dst_release(&rt->u.dst);
return;
}
/*因為是拷貝的oldskb,這裡不需要再引用了,因此釋放對該路由項的引用*/
dst_release(nskb->dst);
/*將新構造資料包引用的路由指向上面由route_reverse函數返回的新的路由項 */
nskb->dst = &rt->u.dst;
/* 清除nskb中拷貝過來的oldskb中連結跟蹤相關的內容*/
nf_reset(nskb);
nskb->nfmark = 0;
skb_init_secmark(nskb);
/*以下就是構造資料包的實際資料部分。如果我們將這裡不為nskb新申請緩衝區,而直接指向oldskb的緩衝區,就使我們上面提到的第二種構造資料包的方法。*/
/*擷取nskb的tcp header*/
tcph = (struct tcphdr *)((u_int32_t*)nskb->nh.iph + nskb->nh.iph->ihl);
/*交換源和目的IP */
tmp_addr = nskb->nh.iph->saddr;
nskb->nh.iph->saddr = nskb->nh.iph->daddr;
nskb->nh.iph->daddr = tmp_addr;
/*交換源和目的連接埠 */
tmp_port = tcph->source;
tcph->source = tcph->dest;
tcph->dest = tmp_port;
/*重設TCP頭部的長度,並修改IP頭部中記錄的資料包的總長度。因為這裡是發送RST報文,只需要有TCP的頭部,不需要TCP的資料部分*/
tcph->doff = sizeof(struct tcphdr)/4;
skb_trim(nskb, nskb->nh.iph->ihl*4 + sizeof(struct tcphdr));
nskb->nh.iph->tot_len = htons(nskb->len);
/*重新設定 seq, ack_seq,分兩種情況(TCP/IP詳解有描述)*/
if (tcph->ack) { /*未經處理資料包中ACK標記位置位的情況*/
needs_ack = 0;
tcph->seq = oth->ack_seq; /*未經處理資料包的ack_seq作為nskb的seq*/
tcph->ack_seq = 0;
} else { /*未經處理資料包中ACK標記位沒有置位的情況,初始串連SYN或者結束串連FIN等*/
needs_ack = 1;
/*這種情況應該是SYN或者FIN包,由於SYN和FIN包都佔用1個位元組的長度。因此ack_seq應該等於舊包的seq+1即可。這裡之所以這樣表示,可能是還存在其他情況的資料包。*/
tcph->ack_seq = htonl(ntohl(oth->seq) + oth->syn + oth->fin
+ oldskb->len - oldskb->nh.iph->ihl*4
- (oth->doff<<2));
tcph->seq = 0;
}
/* RST標記位置1*/
((u_int8_t *)tcph)[13] = 0;
tcph->rst = 1;
tcph->ack = needs_ack;
tcph->window = 0;
tcph->urg_ptr = 0;
/*重新計算TCP校正和*/
tcph->check = 0;
tcph->check = tcp_v4_check(tcph, sizeof(struct tcphdr),
nskb->nh.iph->saddr,
nskb->nh.iph->daddr,
csum_partial((char *)tcph,
sizeof(struct tcphdr), 0));
/* 修改IP包的TTL,並且設定禁止分區*/
nskb->nh.iph->ttl = dst_metric(nskb->dst, RTAX_HOPLIMIT);
/* Set DF, id = 0 */
nskb->nh.iph->frag_off = htons(IP_DF);
nskb->nh.iph->id = 0;
/*重新計算IP資料包頭部校正和*/
nskb->nh.iph->check = 0;
nskb->nh.iph->check = ip_fast_csum((unsigned char *)nskb->nh.iph,
nskb->nh.iph->ihl);
/* "Never happens" */
if (nskb->len > dst_mtu(nskb->dst))
goto free_nskb;
/*使nskb和oldskb的連結記錄關聯*/
nf_ct_attach(nskb, oldskb);
/*這裡就是最終發送資料包的方式,具體方法就是讓新資料包經過LOACLOUT的hook點,然後查路由,最後經由PREROUTING點,將資料包發送出去。
其實這裡我還是有1個疑問:(1)為什麼不可以直接尋找路由,而必須先經過LOCALOUT點;*/
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, nskb, NULL, nskb->dst->dev,
dst_output);
return;
free_nskb:
kfree_skb(nskb);
}
源碼中提到的那個疑問,有網友給出瞭解釋,這裡引用過來:
--------------------------------------------------------------------------------------------------
其實,這不是丟到了高層,而是和ip_queue_xmit()發送過程意義一樣。
對這包進行重新路由後,封裝了頭部,之後,放到了NF_IP_LOCAL_IN之前而已。
其實,這裡面只要修改了中途修改了ip地址,肯定是需要手動重新路由的。
這就涉及到一些比較複雜的route cache的尋找,如果沒有就去尋找route tables;之後,進行路由結構和neighbour結構的關聯,就涉及到鄰居子系統的相關操作;接著就涉及到arp cache的尋找,如果沒有,進行一些操作,arp的過程等等,才找到了相關的ip對應的mac資訊。
息。
--------------------------------------------------------------------------------------------------
通過以上對send_reset函數的分析,應該明白了利用NF架構將構造資料包發送出去的方法。
方法二,就是直接調用dev_queue_xmit函數,將構造完畢的資料包直接發送到網卡驅動。從NF架構來看,該函數的調用是在POSTROUTING點之後了,也可以理解為直接通過調用二層的發送函數,將三層構造的資料包發送出去。該函數實際上會調用skb->dev->hard_start_xmit,即對應網卡的驅動函數,將資料包直接發送的出去。
很顯然,這個工作在二層的函數,發送資料包(資料包在二層的時候準確叫法應該是幀,我們這裡是在三層直接調用的,權且還稱作資料包)的方式是不需要再查路由了。
但是,二層發送的時候是需要根據目的MAC來進行的。在第一種方法構造的資料包中,僅僅交換了IP地址,而沒有對MAC做任何修改。這樣直接調用dev_queue_xmit是會產生問題的,並且該函數發送的內容應該是從二層頭部開始,到資料包的結束。因此,如果三層構造的資料包,想調用該函數直接發送資料包的話,則需要修改資料包的源和目的MAC,並將skb->data指標指向MAC頭部,以及skb->len的值也要加上頭部的長度方法。以下是可參考的範例程式碼:
unsigned char mac_temp[ETH_ALEN] = {0};
struct ethhdr *mach = NULL;
……
/*code…… 構造資料包的IP即上層協議及資料*/
……
/*交換源和目的MAC*/
mach = (struct ethhdr *)skb->mac.raw;
memcpy(mac_temp, (unsigned char *)mach->h_dest, ETH_ALEN);
memcpy(mach->h_dest, (unsigned char *)mach->h_source, ETH_ALEN);
memcpy(mach->h_source, mac_temp, ETH_ALEN);
/*修改skb->data指標,使其指向MAC頭部,並且增加skb->len*/
skb_push(skb , ETH_HLEN);
/*直接調用該函數,將資料包從網卡上發送出去*/
ret = dev_queue_xmit(skb);
這裡還要順便說一下構造的資料包發送完畢之後,對於hook函數的傳回值問題。
(1)第一種發送資料包的實現,對於send_reset函數的實現中,由於單獨申請了nskb的記憶體,並構造的新的資料包。新資料包接著走NF的流程了。而對於原始的skb,就通過模組的傳回值return NF_DROP做出了處理。
(2)第二種發送資料包的實現,若是基於已有資料包的基礎上重新構造的資料包,那麼實際上未經處理資料包的內容已經不複存在,而且調用完畢 dev_queue_xmit已將同一塊緩衝區,只是填充了新資料的資料包發送出去,因此,這裡已經沒有未經處理資料包的存在了,需要返回 NF_STOLEN,告訴協議棧不用關心原始的包即可。否則,若是新資料包是單獨申請的記憶體,那麼對於原資料包還應該是返回NF_DROP.
三、總結
以上就是個人分享和總結和核心中構造的資料包發送出去的兩種方式。實際中常用的就是構造完資料包之後,調用dev_queue_xmit函數發送報文,也測試過調用send_reset發送RST方式。但並未採用send_reset中通過調用NF_HOOK發送過其他資料包。如果諸位朋友有相關的實踐經驗,歡迎分享。
本文在分析send_reset代碼的過程中,參考了百度中搜到的muddoghole的文章,因為只能從百度快照看到這篇文章,並且連結過長,這裡就不列出串連,對於原文的作者表示感謝。
由於對核心中的一些地方理解不夠深入,因此文章中肯定存在很多問題。歡迎各位朋友指正,多多交流。