NETDEV 協議 八

報文的IP校驗和、ICMP校驗和、TCP/UDP校驗和使用相同的算法,在RFC1071中定義,網上這方面的資料和例子很多,就不解釋算法流程了,而是側重於在實現的變化和技巧。

The checksum algorithm is simply to add up all the 16-bit words in one's complement and then to take the one's complement of the sum.
      校驗和的計算可以分爲兩步:累加、取反。這個劃分很重要,它大大減少了校驗和計算的消耗。校驗和計算首要要明確一點:校驗和計算是很耗時的!原因並不在於算法複雜,而是在於輸入數據的龐大,試想傳送500M文件,則內核要校驗500M字節的數據,並且對於每個報文,都是要進行校驗和。所以協議棧的校驗和實現並不是簡單明瞭的,使用了很多方法來規避這種開銷。

第一:推遲校驗和計算
      按照協議的規定,報文到達每一層,首先驗證校驗和是否正確,丟棄掉不正確的報文,再纔會進行後續操作。對於傳輸層下的協議,內核是這樣做的,因爲IP只需要校驗IP報頭,最多60字節;而對於網絡層上的協議,內核就不是這樣做的,ICMP/TCP/UDP都需要校驗報文的內容,而這部分消耗是很大的。
      以UDP爲例,在報文傳遞到UDP處理時,它並不會去驗證校驗和是否正確,而是直接將報文skb插入到相應socket的接收隊列sk_receive_queue中。等到真正有程序要接收這個報文,從接收隊列中取出時,內核纔去計算校驗和。考量下這種做法,由於推遲了校驗和計算,因此很多錯誤的報文都被接收了,它們會佔用處理報文的流程,直到報文準備進入用戶空間時,這時候才計算了校驗和,發現錯誤並丟棄掉。這樣看似乎平白無故增加了開銷,必竟校驗和的計算是一定要進行的。但這樣做,將校驗和計算推遲到了拷貝報文到用戶空間時,這兩個操作的綁定是很關鍵的。本來,校驗和計算要遍歷一次報文,而拷貝又要遍歷一次報文,這樣就是兩次遍歷操作,合併後用一次遍歷搞定,它所節約的開銷是遠遠多於額外支付的。

第二:分離校驗和計算步驟
      開始提到校驗和的計算分爲兩步:累加、取反,將這兩步分開後,會發現校驗和是可以一部分一部分計算的,最後再用每部分計算的值求和取反。這個特性在另一方面對拷貝和校驗和計算同時進行提供了支持。並且,由於報文可能是分片重組的,這樣報文內容並不是一起存儲在線性地址空間中,而是將分片掛在第一個分片skb的frag_list上,這部分內容是存儲在非線性地址空間的。因此,拷貝會一個分片一個分片的進行,由於校驗和計算的劃分,它也可以一個分片一個分片的計算。csum_partial()和csum_fold()就是爲此而生的,前者計算累加,後者計算取反。
      所以一般校驗和會這樣計算,skb_checksum()計算skb的累加和,並和之前已經計算出的累加和skb->csum相加,然後csum_fold()對最後結果取反,就是得到的校驗和。

sum = csum_fold(skb_checksum(skb, 0, len, skb->csum));


第三:改進校驗和計算
      RFC1071中校驗和計算是每16bit爲單位的,但實際在累加這一步是可以調整的,內核計算是每32bit計算的,單位越大,循環就少,效率也自然會高。下面要說明的是32bit累加與16bit累加結果是一致的。
      假設要計算8個字節的校驗和,這8字節按每16bit分成4份:1,2,3,4。左邊是每16bit累加的過程,右邊是每32bit累加的過程: 

      會出現疑惑的地方就是累加的進位問題,左邊16bit累加進位加到sum中,右邊32bit累加進位也要加到sum中,至於2,4相加產生的進位,和16bit累加進位的結果是一樣的。下面就是32bit累加的代碼段,w>result判斷是否產生了進位,假設X+Y=Z產生了進位溢

unsigned int carry = 0;
do {
 unsigned int w = *(unsigned int *) buff;
 count--;
 buff += 4;
 result += carry;
 result += w;
 carry = (w > result);
} while (count);
result += carry;
result = (result & 0xffff) + (result >> 16);


第四:校驗和計算技巧
      節省校驗和最好的辦法就是不計算校驗和,這在某些情況下是可行的,比如大流量發包時或局域網中,這時效率比正確性更爲重要。skb->ip_summed參數就是爲此目的,CHECKSUM_UNNECESSARY就跳過校驗和計算。或者用戶在發包時設置校驗和字段checksum=0,也會跳過校驗和計算。

 
skb->ip_summed = CHECKSUM_UNNECESSARY;

      另外爲了加速校驗和計算,很多網卡都提供了硬件計算校驗和,特別的,linux使用了skb->ip_summed和skb->csum來使用硬件計算能力來幫助校驗TCP/UDP報文。CHECKSUM_COMPLETE表示硬件進行了計算,計算結果存儲在skb->csum中。

 
skb->ip_summed == CHECKSUM_COMPLETE;

      在很多芯片的實現上,校驗和的計算代碼都是用匯編來實現了,這也是出於校驗和計算的效率考慮。


最後,簡單分析下校驗和計算的兩個核心函數。
do_csum() 校驗和累加
      校驗和計算的主體部分是32bit爲單位計算的,並假設buff起始地址是對齊過的,長度也是對齊過的。因此,傳入的buff要進行處理以滿足假設。 

保證計算的起始地址是字節對齊
      這裏的對齊有16bit對齊和32bit對齊。起始地址是對齊是爲了效率,比如起始地址是奇數,那麼累加時用16bit或32bit就很可能跨越一個int範圍,即讀一個數要兩次內存操作;對齊後讀一個數都只用一次內存操作。
      如果不是偶數字節,則odd=1,處理掉第一個字節,使超地址變成偶數。

odd = 1 & (unsigned long) buff;
if (odd) {
#ifdef __LITTLE_ENDIAN
 result += (*buff << 8);
#else
 result = *buff;
#endif
 len--;
 buff++;
}

      當然處理掉第一個字節後,從buff計算校驗和與從buf+1計算校驗和結果顯然是不同的,下面這步在校驗和計算完成後,就是爲了處理這種差異的。

 
if (odd)
 result = ((result >> 8) & 0xff) | ((result & 0xff) << 8);

      還是以例子說明,一個5字節的buff,起始地址addr(1)=0x1,下面是傳統計算和從偶數地址開始計算的對比,要注意的是累加進程中是循環進位的,即溢出的進位會加到最低位。因此,無論哪種方法,1,3,5累加進位會加到2+4中,而2,4累加進位會加到1+3+5中,這也是最後調換前後8bit的值就可以保證兩者相等原因。 

保證計算的長度是偶數字節
      長度對齊理由很簡單,累加是以16bit爲單位的,因此主體部分只計算偶數字節,如果有多餘的一個字節len & 1,則進行如下處理。

if (len & 1)
#ifdef __LITTLE_ENDIAN
  result += *buff;
#else
  result += (*buff << 8);
#endif

      最後是計算的主體部分,可以看到,它並不是單純的16bit累加,而是用32bit累加do-while循環。當然,爲了進行32bit累加,要將起始地址處理成32bit對齊,長度也要處理成32bit對齊。

count = len >> 1;  /* nr of 16-bit words.. */
if (count) {
 if (2 & (unsigned long) buff) {
  result += *(unsigned short *) buff;
  count--;
  len -= 2;
  buff += 2;
 }
 count >>= 1;  /* nr of 32-bit words.. */
 if (count) {
  unsigned int carry = 0;
  do {
   unsigned int w = *(unsigned int *) buff;
   count--;
   buff += 4;
   result += carry;
   result += w;
   carry = (w > result);
  } while (count);
  result += carry;
  result = (result & 0xffff) + (result >> 16);
 }
 if (len & 2) {
  result += *(unsigned short *) buff;
  buff += 2;
 }
}


csum_fold() 校驗和取反
      取反操作很簡單,~sum

static inline __sum16 csum_fold(__wsum csum)
{
 u32 sum = (__force u32)csum;
 sum = (sum & 0xffff) + (sum >> 16);
 sum = (sum & 0xffff) + (sum >> 16);
 return (__force __sum16)~sum;
}

 在前一篇”IP協議”中對報文接收時IP層的處理進行了分析,本篇分析將針對報文發送時IP層的處理。
      傳輸層處理完後,會調用ip_push_pending_frames()將報文傳遞給IP層:
        ip_push_pending_frames() -> ip_local_out() -> __ip_local_out()
      在ip_push_pending_frames()中,會設置第一個IP分片的報頭字段,tot_len和check不會設置。

int ip_local_out(struct sk_buff *skb)
{
 int err;
 err = __ip_local_out(skb);
 if (likely(err == 1))
  err = dst_output(skb);
 return err;
}

      __ip_local_out():設置IP報頭字節總長度tot_len,校驗和check。

iph->tot_len = htons(skb->len);
ip_send_check(iph);

      最後調用dst_output()發送數據給IP層,dst_output()實際調用skb_dst(skb)->output(skb),skb_dst(skb)就是skb所對應的路由項。skb_dst(skb)指向的是路由項dst_entry,它的input在收到報文時賦值ip_local_deliver(),而output在發送報文時賦值ip_output()。

 
return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)->dev, dst_output);


      在IP層的調用過程如下:
        ip_output() -> ip_finish_output() -> ip_finish_output2() -> hh->hh_output()
      在ip_output()中,設置了dev與協議號,從IP層往下,就是以dev驅動數據傳輸了。

skb->dev = dev;
skb->protocol = htons(ETH_P_IP);


      在ip_finish_output()中,判斷如果報文過大,則先調用ip_fragment()進行分片(後面會對這個函數進行分析),然後調用ip_finish_output2()發送。

 
if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
 return ip_fragment(skb, ip_finish_output2);
else
 return ip_finish_output2(skb);

 

      情況一:ip_fragment()
      ip_fragment()與ip_append_data()是IP層傳送報文很重要的兩個函數,弄清它們之間的關係很重要。
      ip_append_data()是上層構造向IP層傳送數據的skb使用的,它會根據MTU值對傳送數據進行分片,後續分片鏈在第一個分片的frag_list上;如果設備支持SG,那麼同一個分片內容(當分片內容是多次輸入得到的)不一定在一個線性空間上,後續輸入的分片內容存在分片的frags數組中。只有第一個分片纔有frag_list,而每個分片都能擁有frags。由ip_append_data()構造好的skb大致如下圖所示:
 

 

      ip_fragments()字面意思是分片,但實際上分片工作已經由ip_append_data()完成了,它只在上層分片出現問題時重新進行分片。它的主要作用還是完成分片的後續工作。假設一個報文被分成了三份skb1, skb2, skb3,它們將獨立的傳遞到網絡上,但顯然ip_append_data()得到的skb還不是獨立的,skb1包含了整個報文的信息,分片報文也鏈在frag_list上;而skb2, skb3則缺少IP報頭的信息,如分片的偏移,分片的標識,校驗和等。ip_fragments()做的主要工作就是將skb拆分成能獨立發送的報文。由ip_fragments()處理後的skb如圖所示: 

 

      兩張圖只列出了IP報頭tot_len字段的不同,其它諸如check, frag_list, frag_off等字段也是不同的。
      先是對第一個分片的更新,讓它脫離後續分片,成爲獨立包。frag_list置爲空,當然frag_list得保存下來(到frag)中,後續分片要從frag_list中取出。更新skb_datalen和skb->len爲第一個分片自身的值,在之前ip_append_data()處理後它是代表全部分片的值。ip報頭的tot_len, frag_off和check分別設置。關於first_len的值,下面這張圖可以清晰的解釋(frags是支持SG的設備可能會出現的,不支持的話,skb->data_len=0): 

 

 

frag = skb_shinfo(skb)->frag_list;
skb_frag_list_init(skb);
skb->data_len = first_len - skb_headlen(skb);
skb->truesize -= truesizes;
skb->len = first_len;
iph->tot_len = htons(first_len);
iph->frag_off = htons(IP_MF);
ip_send_check(iph);

      下面是循環每個分片的代碼,中間省略了每個分片的處理,這部分單獨拿出來說明,frag是從skb中取出的skb_shinfo(skb)->frag_list。

 
for (;;) {
 if (frag) {
  …… // 分片處理
  if (err || !frag)
   break;
  skb = frag;
  frag = skb->next;
  skb->next = NULL;
 }
}

      對於後續分片,要生成它的IP報頭,設置好其中字段,這裏根據分片的排列設置了片偏移iph->frag_off,以及偏移標識(前續分片打上IP_MF標籤)。ip_copy_metadata()從前一個分片中拷貝些數據,比如pkt_type, protocol, dev, priority, mark, flags等。ip_options_fragment()處理分片的IP選項部分,因爲很多選項只要第一個分片有就可以了,後續分片可以去除。

 
frag->ip_summed = CHECKSUM_NONE;
skb_reset_transport_header(frag);
__skb_push(frag, hlen);
skb_reset_network_header(frag);
memcpy(skb_network_header(frag), iph, hlen);
iph = ip_hdr(frag);
iph->tot_len = htons(frag->len);
ip_copy_metadata(frag, skb);
if (offset == 0)
 ip_options_fragment(frag);
offset += skb->len - hlen;
iph->frag_off = htons(offset>>3);
if (frag->next != NULL)
 iph->frag_off |= htons(IP_MF);
/* Ready, complete checksum */
ip_send_check(iph);

      對於每一個分片,在處理完後,調用發送函數向下發送,這裏output就是ip_finish_output2()。

err = output(skb);

 

      情況二:ip_finish_output2()
      調用相應發送函數發送給下一層。有關hh和neighbour參考”ARP模塊”。

 
if (dst->hh)
 return neigh_hh_output(dst->hh, skb);
else if (dst->neighbour)
 return dst->neighbour->output(skb);

      在創建鄰居表項時neighbour->output()被賦值,比如收到arp報文,在arp_process() -> neigh_event_ns()中創建報文相應的鄰居表項,而neigh->ops和neigh->output根據情況賦予不同的值。

 
if (dev->header_ops->cache)
 neigh->ops = &arp_hh_ops;
else
 neigh->ops = &arp_generic_ops;
if (neigh->nud_state&NUD_VALID)
 neigh->output = neigh->ops->connected_output;
else
 neigh->output = neigh->ops->output;

      鄰居表項創建後,相應的hh緩存項並沒有創建,當向鄰居表項中的主機發送報文時,先調用neigh->output(),假設neigh->ops被賦值arp_generiv_ops,則neigh->output= neigh_resolve_output,而在neigh_resolve_output()函數中,會創建hh緩存項,其中hh->output= dev_queue_xmit()。
      所以,無論哪種情況,hh->output還是neigh->output,最終都是調用dev_queue_xmit()向下層傳送報文的。這也是IP層下傳送報文的統一方式-dev_queue_xmit()。雖然調用接口相同,但IP層下的各個協議模塊都是有設備的概念的,因此每個模塊的設備都不相同,在每個模塊中都會更換skb->dev爲下層的設備,而dev_queue_xmit()最終使用的是skb->dev特定的函數進行發送的,這樣實現了各模塊的接口一致。

dev_queue_xmit() 發送函數
      skb_needs_linearize()判斷是否要對報文進行線性處理,如果需要,它返回1,由__skb_linearize()完成線性處理。線性處理就是將報文的所有內容放到線性地址空間,不能有分片的存在。在發送報文時,ip_append_data()對過長的報文進行了分片frag_list,多次添加時使用了SG特性frags(如果支持)。skb_needs_linearize()就是判斷設備能否處理ip_append_data()所做的分片工作。判斷條件很簡單:skb有分片即frag_list,但設備不支持分片NETIF_F_FRAGLIST;skb應用了SG但設備不支持NETIF_F_SG或者是有一個分片在highmem中。最後的線性化函數__skb_linearize()也很簡單,它調用__pskb_pull_tail(skb, skb->data_len),data_len就是非線性空間的長度,__pskb_pull_taill會將這部分數據拷貝到skb->data,從而完成線性化。明顯看到,不支持分片的設備在做線性化處理時會多一次數據拷貝操作。

if (skb_needs_linearize(skb, dev) && __skb_linearize(skb))
 goto out_kfree_skb;

      ip_summed==CHECKSUM_PARTIAL表示協議棧並沒有計算完校驗和,只計算了IP頭,僞頭等,將傳輸層的數據部分留給了硬件進行計算。dev_can_checksum()判斷設備是否能計算校驗和,如果不能的話,則skb_checksum_help()軟件的計算校驗和。

 
if (skb->ip_summed == CHECKSUM_PARTIAL) {
 skb_set_transport_header(skb, skb->csum_start - skb_headroom(skb));
 if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb))
  goto out_kfree_skb;
}

      每個設備在創建時都會新建傳送隊列,dev->_tx。以B4401網卡創建爲例,alloc_etherdev()創建的隊列_tx數爲1,即單隊列的,dev_pick_tx()取出這個隊列dev->_tx[0] -> txq中。其它支持多隊列的網卡會根據skb->sk_tx_queue_mapping來選擇_tx隊列。

 
txq = dev_pick_tx(dev, skb);
q = rcu_dereference_bh(txq->qdisc);

      支持queue discipline(隊列排序)會由q->enqueue和q->dequeue來管理隊列,發送報文。支持的網卡設備則由其後的代碼來處理報文發送。B4401不支持,其q->enqueue爲空。

if (q->enqueue) {
 rc = __dev_xmit_skb(skb, q, dev, txq);
 goto out;
}

      下面是不支持qdisc的網卡設備發送數據的代碼段:dev->falgs & IFF_UP判斷網卡是否UP狀態,netif_tx_queue_stopped()判斷傳送隊列是否在運行狀態。兩者滿足的話,調用dev_hard_start_xmit()向下傳輸報文。dev_xmit_complete()檢查傳輸結果。

 
if (dev->flags & IFF_UP) {
 ……
 if (!netif_tx_queue_stopped(txq)) {
  rc = dev_hard_start_xmit(skb, dev, txq);
  if (dev_xmit_complete(rc)) {
   HARD_TX_UNLOCK(dev, txq);
   goto out;
  }
 }
 ……
}

      dev_hard_start_xmit()核心語句如下,ops->nod_start_xmit()調用設備skb->dev特定的發送操作將skb向下傳送,緊接檢查發送值rc,更新發送狀態計數。如果此時dev指向vlan設備,則ops->ndo_start_xmit()指向vlan_dev_hard_start_xmit(),它生成vlan報文,更換skb->dev,更新計數,再次調用dev_queue_xmit();如果此時dev指向網卡設備(如b4401),則ops->ndo_start_xmit()指向b44_start_xmit(),它會將數據發送物理介質。

 
rc = ops->ndo_start_xmit(skb, dev);
if (rc == NETDEV_TX_OK)
 txq_trans_update(txq);

      簡單總結下,在不支持QDISC的網卡上,從IP層向下的傳輸,循環的調用dev_queue_xmit()向下層傳輸報文,直到最後真正的網卡設備將數據發送到物理介質上,完成報文的發送。其循環調用的圖示如下:

 

  在發送報文時,可以調用函數setsockopt()來設置相應的選項,本文主要分析IP選項的生成,發送以及接收所執行的流程,選取了LSRR爲例子進行說明,主要分爲選項的生成、選項的轉發、選項的接收三部分。
      先看一個源站路由選項的例子,下文的說明都將以此爲例。
       主機IP:192.168.1.99
       源路由:192.168.1.1 192.168.1.2 192.168.1.100[dest ip]
      源站路由選項在各個主機上的情況:

      該圖與<TCP/IP卷一>上的示例不同,因爲這裏的選項[#R1, R2, D]是以實際傳輸中的形式標註的,下圖是源站路由選項在此過程中的具體形式:

      創建socket時,可以使用setsockopt()來設置創建socket的各種屬性,setsockopt()最終調用系統接口sys_setsockopt()。
sys_setsockopt()
      level(級別)指定系統中解釋選項的代碼:通用的套接口代碼,或某個特定協議的代碼。level==SOL_SOCKET是通用的套接口選項,即不是針對於某個協議的套接口的,使用通過函數sock_setsockopt()來設置選項;level其它值:IPPROTO_IP, IPPROTO_ICMPV6, IPPROTO_IPV6則是特定協議套接口的,使用sock->ops->setsockopt(套接字特定函數)來設置選項。

if (level == SOL_SOCKET)
 err = sock_setsockopt(sock, level, optname, optval, optlen);
else
err = sock->ops->setsockopt(sock, level, optname, optval, optlen);

      下面具體說明這個例子,生成選項 - 使用setsockopt()可以設置IP選項,形式如下:

 
setsockopt(fd, IPPROTO_IP, IP_OPTIONS, &opt, optlen);

      其中傳入的opt格式如下:

      無論是何種報文(對應不同的sock),設置IP選項最終都會調用ip_setsockopt()。比如創建的UDP socket,則調用流程爲:sock->ops->setsockopt() => udp_setsockopt()  -> ip_setsockopt()。而處理IP選項的主要是由do_ip_setsockopt()來完成的。

do_ip_setsockopt() 處理ip選項
      根據optname來決定處理何種類型的選項,決定setsockopt()中參數的optval如何解釋。當是IP_OPTIONS時爲IP選項,按IP選項來處理optval。

switch (optname) {
 case IP_OPTIONS:

      ip_options_get_from_use()根據用戶傳入值optval生成選項結構opt,xchg()這句將inet->opt和opt進行了交換,即將opt賦值給了inet->opt,同時將inet->opt作爲結果返回。

 
err = ip_options_get_from_user(sock_net(sk), &opt, optval, optlen);
opt = xchg(&inet->opt, opt);
kfree(opt);

ip_options_get_from_user()
      分配內存給IP選項,struct ip_options記錄了選項相關的一些內部數據結構,最後的屬性__data[0]才指向真正的IP選項。因此在分配空間時是struct ip_options大小加上optlen大小,當然,還要做4字節對齊。

struct ip_options *opt = ip_options_get_alloc(optlen);
static struct ip_options *ip_options_get_alloc(const int optlen)
{
 return kzalloc(sizeof(struct ip_options) + ((optlen + 3) & ~3), GFP_KERNEL);
}

      分配空間後,拷貝用戶設置的IP選項到opt->__data中;最後調用ip_options_get_finish()完成選項的處理,包括了用戶傳入選項的再處理、一些內部數據的填寫,下面會進行詳細講解。

copy_from_user(opt->__data, data, optlen);
return ip_options_get_finish(net, optp, opt, optlen);

ip_options_get_finish()
      選項頭部的空字節用IPOPT_NOOP來補齊,選項尾部的空字節用IPOPT_END來補齊,IPOPT_NOOP和IPOPT_END都佔用1字節,因此optlen遞增,記錄選項長度到opt中。然後調用ip_options_compile()。

 
while (optlen & 3)
 opt->__data[optlen++] = IPOPT_END;
opt->optlen = optlen;

      ip_options_compile()實際完成選項的處理,它在兩個地方被調用:生成帶IP選項的報文時被調用,此時處理的是用戶傳入的選項;接收帶有IP選項的報文時被調用,此時處理的是報文中的IP選項,下面詳細看下該函數,以LSRR選項爲例子。

 
ip_options_compile(net, opt, NULL);
kfree(*optp);
*optp = opt;


ip_options_compile()
      這裏對應於該函數應用的兩種情況:
      1. 如果是生成帶IP選項的報文,傳入的參數skb爲空(此時skb還沒有創建),optptr指向opt->__data,而上面已經看到用戶設置的選項在函數ip_options_get_from_user()中被拷貝到其中;
      2. 如果接收到帶IP選項的報文,傳入skb不爲空(收到報文時就創建了),optptr指向報文中IP選項的位置。iph指向IP報頭的位置,當然,如果是生成選項,iph所指向的位置是沒有意義的。

 
if (skb != NULL) {
 rt = skb_rtable(skb);
 optptr = (unsigned char *)&(ip_hdr(skb)[1]);
} else
 optptr = opt->__data;
iph = optptr - sizeof(struct iphdr);

      IP選項是按[code, len, ptr, data]這樣的塊排列的,每個塊代表一個選項內容,多個選項可以共存,每個塊4字節對齊,不足的用IPOPT_NOOP補齊。for循環處理每個選項,其中IPOPT_END和IPOPT_NOOP只是特殊的佔位符,需要另外處理。然後按照選項塊的格式,取出選項長度len到optlen,再根據選項的code分別進行處理,可以看到獲取選項塊長度的代碼段在IPOPT_END和IPOPT_NOOP之後。

 
for (l = opt->optlen; l > 0; ) {
 switch (*optptr) {
  case IPOPT_END: ….
  case IPOPT_NOOP: ...
   …...
  optlen = optptr[1];
  if (optlen<2 || optlen>l) {
   pp_ptr = optptr;
   goto error;
  }
  case …... 
   …...// 處理代碼段
 }
 l -= optlen;
 optptr += optlen;
}

      還是以寬鬆源路由爲例子:

 
case IPOPT_LSRR:

      首先會作一些檢查,選項長度optlen不能比3小,到少有3字節的頭部:code, len, ptr。指針ptr不能比4小,因爲頭部就有4字節。這裏optlen是去除了頭部的IPOPT_NOOP後的長度,而ptr的計算是包括IPOPT_NOOP的,因此一個是3一個是4;另外,選項中只能有一個源路由選項,因此當srr有值時,表示正在處理的是第二個源路由選項,則有錯誤。

 
if (optlen < 3) {
 pp_ptr = optptr + 1;
 goto error;
}
if (optptr[2] < 4) {
 pp_ptr = optptr + 2;
 goto error;
}
/* NB: cf RFC-1812 5.2.4.1 */
if (opt->srr) {
 pp_ptr = optptr;
 goto error;
}

      當skb==NULL,對應於第一種情況(生成報文選項時);取出源路由選項的第一跳,記錄到選項opt的faddr中,作爲下一跳地址;源路由選項依次前移。對應於開頭給出的例子,這裏處理後結果如圖所示:

 
if (!skb) {
 if (optptr[2] != 4 || optlen < 7 || ((optlen-3) & 3)) {
  pp_ptr = optptr + 1;
  goto error;
 }
 memcpy(&opt->faddr, &optptr[3], 4);
 if (optlen > 7)
  memmove(&optptr[3], &optptr[7], optlen-7);
}

      最後記錄,is_strictroute是否是嚴格的路由選路,srr表示選項到IP報頭的距離,同樣,它只對處理收到的報文中選項時有效。

 
opt->is_strictroute = (optptr[0] == IPOPT_SSRR);
opt->srr = optptr - iph;

      以上是關於IP選項報文的生成,下面從ip_rcv()來看IP選項報文的接收。
       ip_rcv() -> ip_rcv_finish()
      ip_rcv()中重置IP的控制數據struct inet_skb_param爲0,在IP章節已經說過,控制數據是skb中48字節的一個字段,在各層協議中含義不同,在IP層,它被解釋爲inet_skb_parm,包含opt和flags,其中前者與IP選項有關。

 
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
struct inet_skb_parm {
 struct ip_options opt;  /* Compiled IP options  */
 unsigned char  flags;
};

      ip_rcv_finish()中如果頭部長度字段ihl大於4,則表示含有IP選項,此時調用ip_rcv_optins()來接收IP選項。

 
if (iph->ihl > 5 && ip_rcv_options(skb))
 goto drop;

 

ip_rcv_options()
      iph指向IP頭;opt指向控制數據的opt,對IP選項處理的結構會存放在此,作爲skb的一部分,在其它地方起作用;設置opt->optlen選項長度,這裏的長度包括了開頭的IPOPT_NOOP字段,是4的整數倍。

 
iph = ip_hdr(skb);
opt = &(IPCB(skb)->opt);
opt->optlen = iph->ihl*4 - sizeof(struct iphdr);

      調用ip_options_compile()處理選項,這是該函數被調用的第二種情況(收到帶IP選項報文時),傳入參數skb是報文的skb,函數的詳細說明見上文(還是以LSRR爲例),實際上ip_options_compile()在這種情況下只相應設置了opt->is_strictroute和opt->srr,而不像在生成選項時對IP選項進行處理,對接收到IP選項的處理要留帶到發送報文時。

 
if (ip_options_compile(dev_net(dev), opt, skb)) {
 IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
 goto drop;
}

      如果是LSRR,opt->srr在上一步中被設置,爲選項到報頭的距離,對於帶SSRR或LSRR選項的報文來說,opt->srr值不爲0,進入調用ip_options_rcv_srr()完成LSRR選項的處理。

 
if (unlikely(opt->srr)) {
 ……
 if (ip_options_rcv_srr(skb))
  goto drop;
}
return 0;

 

ip_options_rcv_srr()
      該函數的主要作用是根據源站選項重新設置skb的路由項,從而改變報文的正常流程。它不會對選項進行其它操作,真正的操作在發送時完成。
      首先會進行一些檢查,報文的目的MAC必須是本主機,這裏檢查skb->pkt_type==PACKET_HOST;如果報文的目的IP不是本機(而是在本機的鄰居),則本主只是源路徑的一箇中轉站,此時不用再次查找路由表,直接返回,這裏檢查rt->rt_type==RTN_UNICAST,這種情況在LSRR中是允許的,SSRR是不允許的;如果報文的目的IP對本機來說不是直接可達,則錯誤返回。

 
if (skb->pkt_type != PACKET_HOST)
 return -EINVAL;
if (rt->rt_type == RTN_UNICAST) {
 if (!opt->is_strictroute)
  return 0;
 icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl(16<<24));
 return -EINVAL;
}
if (rt->rt_type != RTN_LOCAL)
 return -EINVAL;

      從LSRR選項中取出下一跳地址,記錄到nexthop中,並查詢路由表從saddr到nexthop的路由項,記錄到skb中。如果沒有這樣的路由項,則返回錯誤;如果有這樣的路由項且不是本機(如果下一跳是本機,則表示報文到達目的主機了),則break跳出循環;如果下一跳就是本機,則拷貝下一跳地址到iph->daddr中。
      需要注意的是這裏重新查找了一次路由表(ip_route_input)。而我們知道,在IP層會查找路由表(ip_rcv_finish函數中),它決定報文是否該被接收還是該被轉發。而這裏重查一次路由表也是源站選項的意義所在,IP報頭中的目的地址並不是最終地址,它只決定路徑中的一站,真正的目的地由選項中的值決定,因此需要根據選項中的值作爲目的地址再查找一次,以便決定接下來的動作,用查找到的路由項rt2作爲報文skb的路由項

for (srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4) {
 memcpy(&nexthop, &optptr[srrptr-1], 4);

 rt = skb_rtable(skb);
 skb_dst_set(skb, NULL);
 err = ip_route_input(skb, nexthop, iph->saddr, iph->tos, skb->dev);
 rt2 = skb_rtable(skb);
 if (err || (rt2->rt_type != RTN_UNICAST && rt2->rt_type != RTN_LOCAL)) {
  ip_rt_put(rt2);
  skb_dst_set(skb, &rt->u.dst);
  return -EINVAL;
 }
 ip_rt_put(rt);
 if (rt2->rt_type != RTN_LOCAL)
  break;
 /* Superfast 8) loopback forward */
 memcpy(&iph->daddr, &optptr[srrptr-1], 4);
 opt->is_changed = 1;
}

      IP選項中的srr_is_hit和is_changed含義是不同的,srr_is_hit表示下一跳地址是從源路由選項中提取的,換言之,本機仍不是目的主機;is_changed表示IP報頭是否被改變,被改變的話就需要重新計算IP報頭的校驗和(這裏由於IP選項LSRR可能會改變IP報頭的目的地址或選項LSRR中的值)。

 
if (srrptr <= srrspace) {
 opt->srr_is_hit = 1;
 opt->is_changed = 1;
}

      根據ip_options_rcv_srr()處理的結果,即再次查詢路由表的結果rt2,決定報文是進行轉發還是進行接收。轉發的話input=ip_forward(),表明主機只是到達目的地址的中轉站;接收的話,input=ip_local_deliver(),表明主機是目的地址。
先看轉發的情況,主機只是到達目的地址的中轉站,調用ip_forward() -> ip_forward_finish() -> ip_forward_options(),該函數完成IP選項的處理。
ip_forward_options()
     optptr指向IP選項頭的位置,其中的for循環找出LSRR選項中與路由項下一跳地址rt->rt_dst相同的選項,記錄在srrptr中。ip_rt_get_source()將本機地址填入LSRR選項(源站選項要求用主機的地址取代選項中的地址),然後設置IP報頭的目的地址爲LSRR選項中的下一跳地址,最後LSRR中指針optptr[2]右移4個字節。

if (opt->srr_is_hit) {
 int srrptr, srrspace;
 optptr = raw + opt->srr;

 for ( srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4 ) {
  if (srrptr + 3 > srrspace)
   break;
  if (memcmp(&rt->rt_dst, &optptr[srrptr-1], 4) == 0)
   break;
 }
 if (srrptr + 3 <= srrspace) {
  opt->is_changed = 1;
  ip_rt_get_source(&optptr[srrptr-1], rt);
  ip_hdr(skb)->daddr = rt->rt_dst;
  optptr[2] = srrptr+4;
 } else if (net_ratelimit())
  printk(KERN_CRIT "ip_forward(): Argh! Destination lost!\n");
 ……
}

      還是以開頭的例子爲例,在主機192.168.1.2上收到來自192.168.1.1的報文,最後轉發出去的報文選項如下圖所示:

      再看接收的情況,主機是報文的最終地址,調用ip_local_deliver()像處理正常IP報文一樣處理該報文,接下來的流程與”IP協議”章節中描述的一樣。最終主機192.168.1.100收到的報文選項如下圖所示:

總結:
      生成源站路由選項時,最後兩項地址是相同的,都是192.168.1.100
      源站路由實現是依靠兩次路由查找改變了報文的流程
      源站路由的更改需要重新計算校驗和

發佈了0 篇原創文章 · 獲贊 7 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章