深入理解Linux网络技术内幕学习笔记第十九章:因特网协议第四版(IPv4):Linux的原理和功能
本章主要介紹Linux支持IP的數據結構和基本活動,如入口IP包如何傳遞至IP接收函數,校驗和如何驗證,以及IP選項如何處理。
主要的IPv4數據結構:
struct iphdr{
?? ?
};//ip報頭
struct ip_options{
};//此結構代表必須被傳輸或轉發的封包選項,那些選項存儲在此結構中,而不是struct iphdr結構中
struct ipcm_cookie{
?? ?
};//此結構包含了傳輸封包所需的各種信息
struct ipq{
?? ?
};//IP封包的片段集,參見第二十二章IP片段hash表的組織一節
struct inet_peer{
?? ?
};//內核會為最近連接過的每個遠程主機都保留一個這一結構的實例
struct ipstats_mib{
?? ?
};//SNMP(簡單網絡管理協議)采用一種名為MIB(Management Information Base ,管理信息庫)的對象來收集系統的相關統計數據。此結構會保存關于IP層的統計資料。
struct in_device{
?? ?
};//此結構存儲了一個網絡設備所有與IPv4相關的配置內容。net_device結構中的ip_ptr指針指向該結構
struct in_ifaddr{
?? ?
};//當在接口山配置一個IPv4地址時,內核會建立一個in_faddr結構
struct ipv4_devconf{
?? ?
};//該結構用于調整網絡設備的行為。每個設備都有一個實例。其字段通過/proc/sys/net/ipv4/conf輸出
struct ipv4_config{
?? ?
};//不同于ipv4_devconf存儲每個設備的配置,該結構存儲每個主機的配置
struct cork{
?? ?
};//該結構用于存儲套接字選項CORK選項。第二十一章會看到其字段如何應用
sk_buff和net_device結構里與校驗和有關的字段:
net_device->features字段表明設備的能力,其中和控制檢驗和計算的一些標志如下:
? ? NETIF_F_NO_SUM:此設備很可靠,不需要使用任何L4校驗和?;乩@設備就開啟了此c功能。
? ? NETIF_F_IP_CSUM:此設備可以在硬件中計算L4檢驗和,但是只針對使用IPv4的TCP,UDP。
? ? NETIF_F_HW_CSUM:此設備可以為任何協議在硬件中計算L4校驗和。
skb_buff中主要有兩個字段跟校驗和有關:skb->csum和skb->ip_summed。
當一個封包被接收時,skb->csum可能包含其L4校驗和,skb->ip_summed字段則會記錄L4校驗和的狀態,這些狀態代表設備驅動程序要告訴L4層的事,狀態有下列這些值:
? ? CHECKSUM_NONE:csum中的校驗和無效,需要L4層自己來計算。為社么csum中的校驗和無效,因為①設備不提供硬件校驗和計算。②校驗和必須重新計算并驗證。如第十八章“對L4校驗和所做的修改”一節提到的情況。
? ? CHECKSUM_HW:NIC以L4報頭和有效載荷計算了校驗和,然后把校驗和拷貝到skb->csum字段。軟件(L4接收函數)需要把偽報頭的校驗和添加到skb->csum,并驗證最后所得的校驗和。
? ? CHECKSUM_UNNECESSARY:NIC已經計算了L4報頭以及偽報頭的校驗和(偽報頭的校驗和可以由設備驅動程序在軟件中計算),所以,軟件(L4接收函數)無需再計算L4的校驗和。
當一個封包被傳輸時,skb->csum不再是校驗和本是,而是指向NIC要把它計算的校驗和即將存放的地方,也就是說,封包傳輸期間,只有當校驗和是在硬件中計算時,才會用到此字段。skb->ip_summed依然表示L4校驗和狀態:
?? ?CHECKSUM_NONE:協議已經處理了校驗和,設備不需要做任何事。
?? ?CHECKSUM_HW:協議只把偽報頭的校驗和存儲在報頭中,設備應該添加L4報頭和有效載荷的校驗和。
封包的一般性處理:
協議初始化:
ipv4協議由ip_init函數初始化,該函數完成以下主要任務:
? ? 為ip封包注冊處理函數ip_rcv。(參見第十三章)
? ? 初始化路由子系統,包括與協議無關的緩存。(參見第三十二章)
? ? 初始化用于管理ip端點的基礎架構(參見二十三章“長效ip端點信息”一節)
開機期間,ip_init會由inet_init調用。inet_init會處理所有與ipv4有關的子系統的初始化。
和Netfilter交互:
基本上,防火墻在網絡堆棧程序中的某些地方都有鉤子函數。當符合某些條件時,封包就會通過那些鉤子函數。
與路由子系統的交互:
ip層在好幾個地方必須和路由表交互。本章只簡單說明ip層用于查詢路由表的三個函數:
? ? ip_route_input:決定封包是被本地傳遞,轉發或丟棄。
? ? ip_route_output_flow:傳輸封包前使用,此函數會返回下個跳點以及要使用的出口設備。
? ? dst_pmtu:給定一個路由表緩存項目,返回相關的PMTU。
上述函數會把路由表的查詢結構存儲在skb->dst中。
處理輸入ip封包:
下面從ip_rcv函數開始分析內核網絡協議棧內ip封包的路徑。ip_rcv函數原型如下:
int ip_rcv(struct sk_buff *skb,struct net_device *dev,struct packet_type *pt);
在第十章和第十三章已經知道,NIC驅動如何設定L3協議標識符skb->protocol和封包類型skb->pkt_type。當L2的目的地址和接收接口的地址不同時,skb->pkt_type = PACKET_OTHERHOST,通常這些包會被拋棄去,若接口處于混雜模式,會把這些包傳給相關的嗅探器。但是,到了L3,ip_rcv只會將其丟棄:
? ? if(skb->pkt_type == PACKET_OTHERHOST)
?? ?? ? goto drop;
skb_share_check會檢查封包的引用計數是否大于1,若處理程序看見引用計數大于1,就會自己建立一份緩沖區副本,使其可以修改封包。
? ? if((skb = skb_share_check(skb,GFP_ATOMIC)) == NULL){
?? ?? ? IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
?? ?? ? goto out;
?? ?}
pskb_may_pull的工作是確保skb->data所指區域包含的數據至少和ip頭一樣大。如果條件符合,則無事可做,否則缺漏的部分就會從skb_shinfo(skb)->frags[]里的數據片段(如果有的話)拷貝過來 ,之后再次初始話iph(struct iphdr)。
? ? if(!pskb_may_pull(skb,sizeof(struct iphdr)))
?? ?? ? goto inhdr_error;
? ? iphdr = skb->nh.iph;
接著對ip報頭做一些健康檢查。
? ? if(iph->ihl < 5 || iph->version !=4)
?? ?? ? goto inhdr_error;
現在,重復先前做過的檢查,但是這次是完整的ip頭(包括選項)。
? ? if(!pskb_may_pull(skb,iph->ihl*4))
?? ?? ? goto inhdr_error;
? ? iph = skb->nh.iph;
接著計算校驗和。
? ? if(ip_fast_csum((u8 *)iph,iph->ihl) != 0)
?? ?? ? goto inhdr_error;
然后繼續檢查,確保接收的封包長度大于或等于ip報頭中記錄的長度(因為L2層可能為了滿足最小傳輸尺寸而做了填充,所以封包長度可能大于ip頭中記錄的長度)。同時確保封包的此少和ip報頭一樣大。
? ? {
?? ?? ? _ _u32 len = ntohs(iph->tot_len);
?? ?? ? if(skb->len < len || len < iph->ihl<<2))//<<2是因為報頭的大小以4字節為單位,所以需要先乘以4
?? ??? ?? ? goto inhdr_error;
//pskb_trim_rcsum用于檢查L2是否填充了封包使其達到特定的最小長度,如果有,將其剪裁成正確的大小
?? ?? ? if(pskb_trim_rcsum(skb,len)){
?? ??? ??? ?IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
?? ??? ?? ? goto drop;
?? ??? ?}
?? ?}
最后來到函數的尾端,調用Netfilter子系統。
? ? return NF_HOOL(PF_INET,NF_IP_PRE_ROUTING,? ? skb , dev ,NULL,ip_rcv_finish);
Netfilter子系統(確切的說,是PF_INET,NF_IP_PRE_ROUTING位置)不決定丟棄封包,則后續會執行ip_rcv_finish函數。
ip_rcv_finish函數(static inline? int ip_rcv_finish(struct sk_buff *skb) ):
ip_rcv主要做一些基本的健康檢查,ip_rcv_finish會處理主要工作:
? ? 決定封包傳給本地還是轉發,若轉發,還要找到出口設備和下個跳點。
? ? 分析和處理一些ip選項,并非所有選項都在這處理。
skb->nh字段是在netif_receive_skb里初始化的,當時,還不知道L3協議,所以用nh.raw初始化,現在,可以取得指向IP報頭的指針了。
? ? struct net_device *dev = skb->dev;
? ? struct iphdr *iph = skb->nh.iph;
skb->dst可能包含封包通往其目的的路由信息,如果沒有,詢問路由子系統。
? ? if(skb->dst == NULL){
?? ?? ? if(ip_route_input(skb,iph->daddr,iph->saddr,iph->tos,dev))
?? ??? ??? ?? ? goto drop;
?? ?}
接著,更新Traffic Control(Qos層)所用的統計數據。
? ? #ifdef CONFIG_NET_CLS_ROUTE
?? ??...
? ? #endif
當ip報頭的長度大于20字節時,有一些IP選項要處理。
? ? if(iph->ipl > 5){
?? ?? ? struct ip_options *opt;
? ? ? ? if(skb_cow(skb,skb_headroom(skb))){//skb_cow,如果緩沖區和別人共享,就會做出緩沖區副本
?? ??? ?? ? IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
?? ??? ?? ? goto drop;
?? ??? ?}
?? ?? ? iph = skb->nh.iph;
//解析報頭中的IP選項,將分析結果放在中skb->cb所指的私有數據域字段的ip_option結構內。
?? ?? ? if(ip_options_compile(NULL,skb))
?? ??? ?? ? goto inhdr_error;
?? ?}
在封包是來源地路由的情況下,內核必須檢查該設備的配置是否允許使用該選項。一般情況下,默認是允許的。若設備配置不允許來源地路由選項,則該封包就被丟棄(但不會產生ICMP消息)。當設備允許IP源路由時,調用ip_options_rcv_srr函數設置skb->dst,決定使用哪個設備把該封包轉發至來源地路由列表中的下一個跳點。
? ? if(opt->srr){
?? ?? ? ....
?? ?? ? if(ip_options_rcv_srr(skb))
?? ??? ?? ? goto drop;
?? ?}
ip_rcv_finish函數最后會調用dst_input,完成封包的處理。
IP選項處理:
并非一個封包的所有ip選項都必須在其所有片段中重復,下面是選項相關的主要API:
? ? ip_options_compile:分析IP報頭中的一群選項,然后對一個ip_options結構的實例初始化。
? ? ip_options_build:對IP報頭中選項部分做初始化,傳輸本地封包時會用到該函數。
? ? ip_options_fragment:第一個片段時唯一繼承了原有封包所有選項的片段,其他片段則不會,但是會以空選項填充,使所有片段尺寸一樣,這樣可以簡化分片流程。
? ? ip_forward_options:轉發一個封包時,有些選項必須被處理。
? ? ip_options_get:此函數會接收一群選項,用ip_options_compile 解析,然后把結果存儲在其分配的ip_options結構中。
? ? ip_options_echo:指定入口IP封包及其IP選項后,此函數就可以建立用于回復傳送者的IP選項。
ip_options_compile函數:
原型:int ip_options_compile(struct ip_options *opt ,struct sk_buff *skb)
當skb不為NULL時(本例中opt為NULL),表示正在處理入口封包。當skb為NULL時(本例中,opt不為NULL),表示正在處理本地傳輸的封包。
在傳輸一個本地封包時,opt不為NULL,opt->data包含一個指向IP報頭的指針。處理入口封包時,opt為NULL,報頭包含在skb中,ip_options結構存儲在skb->cb。ip_options_compile函數會根據IP報頭位于何處而對本地變量做初始化。
總結
以上是生活随笔為你收集整理的深入理解Linux网络技术内幕学习笔记第十九章:因特网协议第四版(IPv4):Linux的原理和功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 杭电acm的第1000题c语言解法
- 下一篇: 通过一个Kafka故障解决过程阐述架构师