- 論壇徽章:
- 36
|
本文歡迎自由轉(zhuǎn)載,但請標(biāo)明出處,并保證本文的完整性。
作者:Godbach
日期:2009/09/01
一、構(gòu)造數(shù)據(jù)包簡析
這里并不詳細(xì)介紹如何在內(nèi)核中構(gòu)造數(shù)據(jù)包,下文如有需要會(huì)在適當(dāng)?shù)奈恢眠M(jìn)行分析。這里簡單的分析講一下內(nèi)核態(tài)基于Netfilter框架構(gòu)造數(shù)據(jù)包的方式。
內(nèi)核中可以用到的構(gòu)造數(shù)據(jù)包的方式,個(gè)人認(rèn)為可以分為兩種。
其一,我們直接用alloc_skb申請一個(gè)skb結(jié)構(gòu)體,然后根據(jù)實(shí)際的應(yīng)用填充不同的成員,或者基于當(dāng)前數(shù)據(jù)包的skb,調(diào)用skb_copy_expand()函數(shù)等新申請一個(gè)nskb,并且拷貝skb的內(nèi)容。
其二,也是個(gè)人比較常用的,就是直接在先前接收到的數(shù)據(jù)包skb上作修改,主要有源IP、目IP,如果是TCP/UDP協(xié)議的話,還有源端口目的端口號(hào)?傊,就是根據(jù)自己的需求去調(diào)整數(shù)據(jù)包的相關(guān)成員即可。
通常,這兩種方式最終可能都要涉及到重新計(jì)算各個(gè)部分的校驗(yàn)和,這也是必須的。
二、如何發(fā)送構(gòu)造的數(shù)據(jù)包
承接上文,數(shù)據(jù)包已經(jīng)構(gòu)造完畢,下一步關(guān)鍵就是如何發(fā)送數(shù)據(jù)包了。個(gè)人這里總結(jié)的有兩種方法。
方法一,就是讓數(shù)據(jù)包接著按照Netfilter的流程進(jìn)行傳輸。因?yàn)閿?shù)據(jù)包的一些內(nèi)容已經(jīng)被更改,尤其是當(dāng)源IP和目的IP被更改,主要是交換的情況下,是需要確保有路由可查的。
NF框架中查路由的位置一是在PREROUTING之后,而是在LOCALOUT之后。又由于這里是需要將數(shù)據(jù)包從本地發(fā)送出去。因此,可以考慮讓修改后的數(shù)據(jù)包從LOCALOUT點(diǎn)發(fā)出。
內(nèi)核代碼中有這種方式的典型體現(xiàn)。本文涉及的相關(guān)內(nèi)核代碼的版本都是2.6.18.3。源文件為ipt_REJECT.c,函數(shù)send_reset用于往當(dāng)前接收到數(shù)據(jù)包的源IP上發(fā)送RST包,整個(gè)函數(shù)涉及了數(shù)據(jù)包的構(gòu)造和發(fā)送,這里一起做個(gè)簡單分析。
- /* 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;
- /* 當(dāng)期收到的包就是RST包,就不用再發(fā)送RST包了*/
- if (oth->rst)
- return;
- /*檢查數(shù)據(jù)包的校驗(yàn)和是否正確*/
- if (nf_ip_checksum(oldskb, hook, iph->ihl * 4, IPPROTO_TCP))
- return;
- /*這一步比較關(guān)鍵,做的就是更新路由的工作。該函數(shù)的主要工作就是將當(dāng)前數(shù)據(jù)包的源IP當(dāng)做路由的目的IP,同時(shí)考慮數(shù)據(jù)包的目的IP,得到去往該源IP的路由*/
- if ((rt = route_reverse(oldskb, oth, hook)) == NULL)
- return;
- hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);
- /* 拷貝當(dāng)前的oldskb,包括skb結(jié)構(gòu)體和數(shù)據(jù)部分。這就是我們上面提到的構(gòu)造數(shù)據(jù)包的第一種方式*/
- nskb = skb_copy_expand(oldskb, hh_len, skb_tailroom(oldskb),
- GFP_ATOMIC);
- if (!nskb) {
- dst_release(&rt->u.dst);
- return;
- }
- /*因?yàn)槭强截惖膐ldskb,這里不需要再引用了,因此釋放對該路由項(xiàng)的引用*/
- dst_release(nskb->dst);
- /*將新構(gòu)造數(shù)據(jù)包引用的路由指向上面由route_reverse函數(shù)返回的新的路由項(xiàng) */
- nskb->dst = &rt->u.dst;
- /* 清除nskb中拷貝過來的oldskb中鏈接跟蹤相關(guān)的內(nèi)容*/
- nf_reset(nskb);
- nskb->nfmark = 0;
- skb_init_secmark(nskb);
- /*以下就是構(gòu)造數(shù)據(jù)包的實(shí)際數(shù)據(jù)部分。如果我們將這里不為nskb新申請緩沖區(qū),而直接指向oldskb的緩沖區(qū),就使我們上面提到的第二種構(gòu)造數(shù)據(jù)包的方法。*/
- /*獲取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頭部中記錄的數(shù)據(jù)包的總長度。因?yàn)檫@里是發(fā)送RST報(bào)文,只需要有TCP的頭部,不需要TCP的數(shù)據(jù)部分*/
- 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);
- /*重新設(shè)置 seq, ack_seq,分兩種情況(TCP/IP詳解有描述)*/
- if (tcph->ack) { /*原始數(shù)據(jù)包中ACK標(biāo)記位置位的情況*/
- needs_ack = 0;
- tcph->seq = oth->ack_seq; /*原始數(shù)據(jù)包的ack_seq作為nskb的seq*/
- tcph->ack_seq = 0;
- } else { /*原始數(shù)據(jù)包中ACK標(biāo)記位沒有置位的情況,初始連接SYN或者結(jié)束連接FIN等*/
- needs_ack = 1;
- /*這種情況應(yīng)該是SYN或者FIN包,由于SYN和FIN包都占用1個(gè)字節(jié)的長度。因此ack_seq應(yīng)該等于舊包的seq+1即可。這里之所以這樣表示,可能是還存在其他情況的數(shù)據(jù)包。*/
- 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標(biāo)記位置1*/
- ((u_int8_t *)tcph)[13] = 0;
- tcph->rst = 1;
- tcph->ack = needs_ack;
- tcph->window = 0;
- tcph->urg_ptr = 0;
- /*重新計(jì)算TCP校驗(yàn)和*/
- 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,并且設(shè)置禁止分片*/
- 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;
- /*重新計(jì)算IP數(shù)據(jù)包頭部校驗(yàn)和*/
- 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的鏈接記錄關(guān)聯(lián)*/
- nf_ct_attach(nskb, oldskb);
-
- /*這里就是最終發(fā)送數(shù)據(jù)包的方式,具體方法就是讓新數(shù)據(jù)包經(jīng)過LOACLOUT的hook點(diǎn),然后查路由,最后經(jīng)由POSTROUTING點(diǎn),將數(shù)據(jù)包發(fā)送出去。
- 其實(shí)這里我還是有1個(gè)疑問:(1)為什么不可以直接查找路由,而必須先經(jīng)過LOCALOUT點(diǎn)*/
- NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, nskb, NULL, nskb->dst->dev,
- dst_output);
- return;
- free_nskb:
- kfree_skb(nskb);
- }
復(fù)制代碼
通過以上對send_reset函數(shù)的分析,應(yīng)該明白了利用NF框架將構(gòu)造數(shù)據(jù)包發(fā)送出去的方法。
源碼分析中提到的1個(gè)疑問,35樓給出了解釋,這里引用過來:
其實(shí),這不是丟到了高層,而是和ip_queue_xmit()發(fā)送過程意義一樣。
對這包進(jìn)行重新路由后,封裝了頭部,之后,放到了NF_IP_LOCAL_IN之前而已。
其實(shí),這里面只要修改了中途修改了ip地址,肯定是需要手動(dòng)重新路由的。
這就涉及到一些比較復(fù)雜的route cache的查找,如果沒有就去查找route tables;之后,進(jìn)行路由結(jié)構(gòu)和neighbour結(jié)構(gòu)的關(guān)聯(lián),就涉及到鄰居子系統(tǒng)的相關(guān)操作;接著就涉及到arp cache的查找,如果沒有,進(jìn)行一些操作,arp的過程等等,才找到了相關(guān)的ip對應(yīng)的mac信息。
息。
方法二,就是直接調(diào)用dev_queue_xmit函數(shù),將構(gòu)造完畢的數(shù)據(jù)包直接發(fā)送到網(wǎng)卡驅(qū)動(dòng)。從NF框架來看,該函數(shù)的調(diào)用是在POSTROUTING點(diǎn)之后了,也可以理解為直接通過調(diào)用二層的發(fā)送函數(shù),將三層構(gòu)造的數(shù)據(jù)包發(fā)送出去。該函數(shù)實(shí)際上會(huì)調(diào)用skb->dev->hard_start_xmit,即對應(yīng)網(wǎng)卡的驅(qū)動(dòng)函數(shù),將數(shù)據(jù)包直接發(fā)送的出去。
很顯然,這個(gè)工作在二層的函數(shù),發(fā)送數(shù)據(jù)包(數(shù)據(jù)包在二層的時(shí)候準(zhǔn)確叫法應(yīng)該是幀,我們這里是在三層直接調(diào)用的,權(quán)且還稱作數(shù)據(jù)包)的方式是不需要再查路由了。
但是,二層發(fā)送的時(shí)候是需要根據(jù)目的MAC來進(jìn)行的。在第一種方法構(gòu)造的數(shù)據(jù)包中,僅僅交換了IP地址,而沒有對MAC做任何修改。這樣直接調(diào)用dev_queue_xmit是會(huì)產(chǎn)生問題的,并且該函數(shù)發(fā)送的內(nèi)容應(yīng)該是從二層頭部開始,到數(shù)據(jù)包的結(jié)束。因此,如果三層構(gòu)造的數(shù)據(jù)包,想調(diào)用該函數(shù)直接發(fā)送數(shù)據(jù)包的話,則需要修改數(shù)據(jù)包的源和目的MAC,并將skb->data指針指向MAC頭部,以及skb->len的值也要加上頭部的長度方法。以下是可參考的示例代碼:
- unsigned char mac_temp[ETH_ALEN] = {0};
- struct ethhdr *mach = NULL;
- ……
- /*code…… 構(gòu)造數(shù)據(jù)包的IP即上層協(xié)議及數(shù)據(jù)*/
- ……
- /*交換源和目的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);
- /*直接調(diào)用該函數(shù),將數(shù)據(jù)包從網(wǎng)卡上發(fā)送出去*/
- ret = dev_queue_xmit(skb);
復(fù)制代碼
這里還要順便說一下構(gòu)造的數(shù)據(jù)包發(fā)送完畢之后,對于hook函數(shù)的返回值問題。
(1)第一種發(fā)送數(shù)據(jù)包的實(shí)現(xiàn),對于send_reset函數(shù)的實(shí)現(xiàn)中,由于單獨(dú)申請了nskb的內(nèi)存,并構(gòu)造的新的數(shù)據(jù)包。新數(shù)據(jù)包接著走NF的流程了。而對于原始的skb,就通過模塊的返回值return NF_DROP做出了處理。
(2)第二種發(fā)送數(shù)據(jù)包的實(shí)現(xiàn),若是基于已有數(shù)據(jù)包的基礎(chǔ)上重新構(gòu)造的數(shù)據(jù)包,那么實(shí)際上原始數(shù)據(jù)包的內(nèi)容已經(jīng)不復(fù)存在,而且調(diào)用完畢dev_queue_xmit已將同一塊緩沖區(qū),只是填充了新數(shù)據(jù)的數(shù)據(jù)包發(fā)送出去,因此,這里已經(jīng)沒有原始數(shù)據(jù)包的存在了,需要返回NF_STOLEN,告訴協(xié)議棧不用關(guān)心原始的包即可。否則,若是新數(shù)據(jù)包是單獨(dú)申請的內(nèi)存,那么對于原數(shù)據(jù)包還應(yīng)該是返回NF_DROP.
三、總結(jié)
以上就是個(gè)人分享和總結(jié)和內(nèi)核中構(gòu)造的數(shù)據(jù)包發(fā)送出去的兩種方式。實(shí)際中常用的就是構(gòu)造完數(shù)據(jù)包之后,調(diào)用dev_queue_xmit函數(shù)發(fā)送報(bào)文,也測試過調(diào)用send_reset發(fā)送RST方式。但并未采用send_reset中通過調(diào)用NF_HOOK發(fā)送過其他數(shù)據(jù)包。如果諸位朋友有相關(guān)的實(shí)踐經(jīng)驗(yàn),歡迎分享。
本文在分析send_reset代碼的過程中,參考了百度中搜到的muddoghole的文章,因?yàn)橹荒軓陌俣瓤煺湛吹竭@篇文章,并且鏈接過長,這里就不列出連接,對于原文的作者表示感謝。
由于對內(nèi)核中的一些地方理解不夠深入,因此文章中肯定存在很多問題。歡迎各位朋友指正,多多交流。
本文原文鏈接:http://blog.chinaunix.net/u/33048/showart_2043789.html
[ 本帖最后由 Godbach 于 2009-9-5 11:18 編輯 ] |
評分
-
查看全部評分
|