歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux綜合 >> Linux內核 >> Linux內核分析 - 網絡[十一]:ICMP模塊

Linux內核分析 - 網絡[十一]:ICMP模塊

日期:2017/3/3 16:38:13   编辑:Linux內核

內核版本:2.6.34

ICMP模塊比較簡單,要注意的是icmp的速率限制策略,向IP層傳輸數據ip_append_data()和 ip_push_pending_frames()。

在net/ipv4/af_inet.c中的inet_init()注冊icmp協議,從這裡也可以看出,ICMP模塊是綁 定在IP模塊之上的。inet_add_protocol()會將icmp_protocol加入到全局量inet_protos中。

if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)     
 printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");     
icmp_protocol定義如下:     
static const struct net_protocol icmp_protocol = {     
 .handler = icmp_rcv,     
 .no_policy = 1,     
 .netns_ok = 1,     
};

除了注冊icmp協議,還要對icmp模塊初始化,這部分由icmp_init()完成。

if (icmp_init() < 0)     
 panic("Failed to create the ICMP control socket.\n");

icmp_init()函數做的事很簡單,register_pernet_subsys(&icmp_sk_ops),而注冊icmp網絡子系統過程中會調用 icmp_sk_ops.init(即icmp_sk_init函數)來完成它的初始化,下面具體看icmp_sk_init()函數。

首先為net為配CPU數目 (nr_cpu_ids)個struct sock結構體空間,這裡的net是全局的網絡名,一般是init_inet。
net->ipv4.icmp_sk = kzalloc(nr_cpu_ids * sizeof(struct sock *), GFP_KERNEL);

每個CPU i,它的sock結構體位於net中的icmp_sk[i]。於每 個CPU i,初始化剛剛分配的icmp_sk[i]:

 -第一步,inet_ctl_sock_create()創建sk,並在net->ipv4.icmp_sk[i] = sk中將其賦值給icmp_sk[i]。

 -第二步:ICMP發送緩存區大小sk_sndbuf設置為128K

for_each_possible_cpu(i) { 

    
 struct sock *sk;     
 err = inet_ctl_sock_create(&sk, PF_INET,     
    SOCK_RAW, IPPROTO_ICMP, net);     
 if (err < 0)     
  goto fail;     
         
 net->ipv4.icmp_sk[i] = sk;     
 sk->sk_sndbuf =     
  (2 * ((64 * 1024) + sizeof(struct sk_buff)));     
 sock_set_flag(sk, SOCK_USE_WRITE_QUEUE);     
 inet_sk(sk)->pmtudisc = IP_PMTUDISC_DONT;     
}

忽略發往廣播地址的icmp echo報文;忽略發往廣播地址的錯誤的響應報文;

net-

>ipv4.sysctl_icmp_echo_ignore_all = 0;     
net->ipv4.sysctl_icmp_echo_ignore_broadcasts = 1;     
net->ipv4.sysctl_icmp_ignore_bogus_error_responses = 1;

設置icmp處理速率,這裡的ratelimit和ratemask參 數在後面限速處理時會具體用到。

net->ipv4.sysctl_icmp_ratelimit = 1 * HZ;     
net->ipv4.sysctl_icmp_ratemask = 0x1818;     
net->ipv4.sysctl_icmp_errors_use_inbound_ifaddr = 0;

初始化工作完成後,還是從icmp的接收開始,icmp_rcv 完成icmp報文的處理。
取得icmp報頭,此時skb->transport_header是在IP模塊處理中的ip_local_deliver_finish() 將其設置為了指向icmp報頭的位置。

icmph = icmp_hdr(skb);

根據icmp的類型type交由不同的處理函數去完成。

icmp_pointers[icmph->type].handler(skb);

icmp_pointers是在icmp.c中定義的全局量,部分如下:

static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = {     
 [ICMP_ECHOREPLY] = {     
  .handler = icmp_discard,     
 },     
 [1] = {     
  .handler = icmp_discard,     
  .error = 1,     
 },     
……     
}

比如對於收到的icmp報文type為0或1(響應答復或目的不可達),協議棧要做的就是丟棄掉它 – icmp_discard()。下 面以icmp echo和icmp timestamp為例說明。

收到icmp echo報文執行icmp_echo()

icmp_param是回復時信息,它直接 拷貝了echo的ICMP報頭icmp_hdr(skb),僅僅改變了報頭的type = ICMP_ECHO_REPLY,然後調用icmp_reply()處理發送。

struct icmp_bxm icmp_param;     
icmp_param.data.icmph    = *icmp_hdr(skb);     
icmp_param.data.icmph.type = ICMP_ECHOREPLY;     
icmp_param.skb     = skb;     
icmp_param.offset    = 0;     
icmp_param.data_len    = skb->len;     
icmp_param.head_len    = sizeof(struct icmphdr);     
icmp_reply(&icmp_param, skb);

收到icmp timestamp報文後執行icmp_timestamp()

經過IP層處理,skb- >data指向icmp報頭的位置,而報頭最小為4字節,所以這裡判斷skb->len < 4,是則丟棄該報文。從這裡也可以看出 ,時間戳請求報文可以只有4節字頭部,而沒有時間戳信息。

if (skb->len < 4)     
 goto out_err;

這段代碼設置時間戳響應的時間戳信息,包括接收時間戳和發送時間戳,兩者分別代表主機收到報文 的時間,發送響應報文的時間,而從這部分代碼也可以看出icmp_param.data.times[2] = icmp_param.data.times[1]協議棧簡 單的將接收和發送時間戳置為相同的。時間戳的計算很簡單,格林尼治時間的當天時間的微秒數。最後skb_copy_bits()從skb的 ICMP報文內容拷貝4節字的時間到icmp_param_data.times[0],即發起時間戳,所以最後情形如下:

  icmp_param_data.times[0] 發起時間戳,從請求報文中拷貝

 icmp_param_data.times[0] 接收時間戳,處理ICMP報頭時的 時間

 icmp_param_data.times[0] 發送時間戳,設置為與接收時間戳相同

getnstimeofday(&tv);     
icmp_param.data.times[1] = htonl((tv.tv_sec % 86400) * MSEC_PER_SEC +     
  tv.tv_nsec / NSEC_PER_MSEC);     
icmp_param.data.times[2] = icmp_param.data.times[1];     
if (skb_copy_bits(skb, 0, &icmp_param.data.times[0], 4))     
 BUG();

前面已經說過,icmp_param就是要發送ICMP報文的內容,上面設置了內容,接下來設置報頭,同樣是直接拷貝 了ICMP請求的報頭,改變type為ICMP_TIMESTAMPREPLY。注意這裡的data_len設置為0,因為它與icmp echo不同,一定是沒有分 片的,即沒有paged_data部分。head_len設置為icmphdrlen+12,這裡是為了調用icmp_reply()回復時的統一,實現表示ICMP部 分的長度,主要是有分片時會根據head_len來跳過報頭而只拷貝每個分片的內容。

icmp_param.data.icmph    = 

*icmp_hdr(skb);     
icmp_param.data.icmph.type = ICMP_TIMESTAMPREPLY;     
icmp_param.data.icmph.code = 0;     
icmp_param.skb     = skb;     
icmp_param.offset    = 0;     
icmp_param.data_len    = 0;     
icmp_param.head_len    = sizeof(struct icmphdr) + 12;

最後調用icmp_reply()回復,這與icmp_echo()是相同的 。

icmp_reply(&icmp_param, skb);

注意兩者設置icmp_param參數時的區別:

 icmp_echo()中 icmp_param.data_len=skb->len;

        icmp_param.head_len=sizeof(struct icmphdr);

  icmp_timestamp()中icmp_param.data_len=0。

        icmp_param.head_len=sizeof(struct icmphdr) +12;

icmp_reply()

通過ip_route_output_key()查找路由信息,存放在rt中。路由項在這裡有兩個作用:一是限速是 針對每個路由項的,在icmpv4_xrlim_allow()中會用到;二是將報文傳遞給IP層需要用到rt。仔細觀察流程可以發現,報文在協 議棧傳遞過程中,在IP層會  查找一次路由表獲取到了rt,而在這裡又查找了一次路由表,似乎是重復了。其實不是,IP層查 找是在報文接收階段,這裡的查找是在報文的發送階段。

{     
 struct flowi fl = { .nl_u = { .ip4_u =     
    { .daddr = daddr,     
    .saddr = rt->rt_spec_dst,     
    .tos = RT_TOS(ip_hdr(skb)->tos) } },     
    .proto = IPPROTO_ICMP };     
 security_skb_classify_flow(skb, &fl);     
 if (ip_route_output_key(net, &rt, &fl))     
  goto out_unlock;     
}

協議棧對於部分ICMP報文進行了限速,但這種限速不是整體的,而是針對每個路由項的,即限制每個地址發送ICMP報 文的限率。icmpv4_xrlim_allow()判斷該icmp報文是否需要被限速,如果能接收,則調用icmp_puash_reply()發送響應。

if (icmpv4_xrlim_allow(net, rt, icmp_param->data.icmph.type,     
      icmp_param->data.icmph.code))     
 icmp_push_reply(icmp_param, &ipc, &rt);

icmpv4_xrlim_allow() -> xrlim_allow() 限速處理

速 率有關的參數是在icmp_init() -> icmp_sk_init()創建ICMP的sock時設置的,ratelimit是限制的速率,即TBF代碼段中的 timeout,可以理解成一個令牌;ratemask是被限制速率的ICMP的報文類型,(1 << type & retemask) == 1判斷是否 限速,type即ICMP類型,可見默認情況下[3]dest unreachable, [4]source quench, [11]time exceeded, [12]parameter problem才會被限速。

net->ipv4.sysctl_icmp_ratelimit = 1 * HZ;     
net->ipv4.sysctl_icmp_ratemask = 0x1818;

限速使用了Token Bucket Filter(令牌環過濾器)思想,大致是每個 到來的令牌從數據隊列中收集一個數據包,然後從桶中刪除。令牌被耗盡時,數據包將停止發送一段時間。

ICMP的限速使用 的就是這種思想,不過時間作為令牌,它的增長是連續的;每來一個報文,拿走一個令牌,則是一個時間段timeout,令牌也限 定了最大數目是XRLIM_BURST_FACTOR為6;簡單來講就是每過timeout時間,令牌數就加1,當令牌數達到6時不再增加;而來一 個報文,令牌數就減一,當令牌數為空時,不再減少,該報文也被丟棄;在這種情況下,在過timeout時間,才會處理下一個報 文。實現的代碼段如下:

#define XRLIM_BURST_FACTOR 6     
int xrlim_allow(struct dst_entry *dst, int timeout)     
{     
 unsigned long now, token = dst->rate_tokens;     
 int rc = 0;     
         
 now = jiffies;     
 token += now - dst->rate_last;     
 dst->rate_last = now;     
 if (token > XRLIM_BURST_FACTOR * timeout)     
  token = XRLIM_BURST_FACTOR * timeout;     
 if (token >= timeout) {     
  token -= timeout;     
  rc = 1;     
 }     
 dst->rate_tokens = token;     
 return rc;     
}

dst->rate_tokens記錄上一次的令牌,dst->rate_last記錄上一次訪問時間,now – dst->rate_last為經 過的時間即增加的令牌數;當token>=timeout時即至少還有一個令牌,反回rc=1表示仍有令牌,不用限速;否則返回rc=0, 限速。

icmp_push_reply() 發送回復報文

取出icmp使用的sock sk

sk = icmp_sk(dev_net((*rt)- >u.dst.dev));

if中的ip_append_data()函數表示把數據添加到sk->sk_write_queue,這個函數是用於上層向IP層 傳輸報文,它會進行分片的操作,實際是幫IP層做了分片。具體函數調用參見後面的ip_append_data()函數分析。正常情況 ip_append_data()返回0,即if的執行語句不會被觸發。

if (ip_append_data(sk, icmp_glue_bits, icmp_param,     
      icmp_param->data_len+icmp_param->head_len,     
      icmp_param->head_len,     
      ipc, rt, MSG_DONTWAIT) < 0)     
  ip_flush_pending_frames(sk);

else if進入條件是sk->sk_write_queue中已有數據,顯然在if的判斷語句中已 經將報文添加到了sk->sk_write_queue中,所以會進入else if執行語句調用ip_push_pending_frames()將報文傳遞給IP層。 而在ip_append_data()函數中可以看到,它只是拷貝了報文內容,並沒有生成ICMP報頭,ICMP報頭生成當然也是在通過 ip_push_pending_frames()將報文發給IP層前生成的。取出skb,計算所有分片一起的校驗和,然過通過 csum_partial_copy_nocheck()生成新的icmp報頭,最後調用ip_push_pending_frames()發送數據到IP層。函數 ip_push_pending_frames()函數分析也參見後文。

else if ((skb = skb_peek(&sk->sk_write_queue)) != 

NULL) {     
 struct icmphdr *icmph = icmp_hdr(skb);     
 __wsum csum = 0;     
 struct sk_buff *skb1;     
         
 skb_queue_walk(&sk->sk_write_queue, skb1) {     
  sum = csum_add(csum, skb1->csum);     
 }     
 csum = csum_partial_copy_nocheck((void *)&icmp_param->data,     
   (char *)icmph,     
   icmp_param->head_len, csum);     
 icmph->checksum = csum_fold(csum);     
 skb->ip_summed = CHECKSUM_NONE;     
 ip_push_pending_frames(sk);     
}

ip_append_data() 添加要傳遞到IP層的數據

傳入參數的解釋:

getfrag() – 復制數據,這裡使用函數指針 隱藏了復制細節,因為針對icmp, udp的復制是不同的;

from – 被復制的數據,在icmp模塊中該參數傳入的是struct icmp_bxm;

length – IP報文內容長度

transhdrlen – 傳輸報頭長度,盡管ICMP歸為網絡層協議,但這裡的transhdrlen 也是包括它的,所以更好的解釋是表示IP上一層的報頭,比如ICMP報頭,IGMP報頭,UDP報頭等長度

ip_append_data()函 數比較復雜,這裡以兩個例子來解釋這個函數:發送50 Byte的echo報文,發送600 Byte的echo報文。56字節echo報文在IP層不 需要分片;600字節echo報文在IP層需要分片。ip_append_data()還可以多次調用來收集數據,而在ICMP模塊中這點並不能體現 出來,在以後UDP或TCP時再以解釋多次調用的情況。

example 1:50 Byte echo報文 [假設MTU=520]

如果 sk_write_queue為空,則證明是第一個分片,50字節的報文只需要一個分片。這裡會設置exthdrlen,表示鏈路層額外的報頭長 ,一般情況下是0,所以此時length和transhdrlen值仍是傳入的值。而sk->sk_sndmsg_page和sk->sk_sndmsg_off與發散/ 聚合IO有關,這裡先不考慮。

if (skb_queue_empty(&sk->sk_write_queue)) {     
 ….     
sk->sk_sndmsg_page = NULL;     
 sk->sk_sndmsg_off = 0;     
if ((exthdrlen = rt->u.dst.header_len) != 0) {     
  length += exthdrlen;     
  transhdrlen += exthdrlen;     
 }     
 …     
}

設置各種參數的值,hh_len表示以太網報頭的長度,16字節對齊;fragheaderlen表示分片報頭長度,即IP報頭; maxfraglen表示最大分片長度。各參數值:hh_len = 16, fragheaderlen = 20, maxfraglen = 516,注意要求的節字對齊。

hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);     
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);     
maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;

此時sk- >sk_write_queue還為空,跳轉至alloc_new_skb執行分配新的skb。

if ((skb = skb_peek_tail(&sk-

>sk_write_queue)) == NULL)     
 goto alloc_new_skb;

fraggap在上一個skb沒有8字節對齊時設置為多余的字節數,否則的話fraggap=0;datalen表示 IP報文長度(不包括IP報頭),fraglen表示以太網幀報文長度(不包括以太網頭),alloclen表示要分配的內容長度,下面代碼省 略了一些內容。各參數值:       fraggap=0, datalen=50, fraglen=70, alloclen=70。

fraggap = 0;     
datalen = length + fraggap;     
fraglen = datalen + fragheaderlen;     
alloclen = datalen + fragheaderlen;

分配報文skb空間,大小為alloclen+hh_len+15,alloclen + hh_len就是報文 的長度,15個字節為預留部分。

if (transhdrlen) {     
 skb = sock_alloc_send_skb(sk,     
  alloclen + hh_len + 15,     
  (flags & MSG_DONTWAIT), &err);     
}

skb_reserve()保留skb頭的hh_len大小,skb_put()擴展skb大小到fraglen,然後設置network_header和 transport_header指向skb的正確位置,data指向ICMP報頭的位置,具體可以看下面的圖示:

skb_reserve(skb, 

hh_len);     
……     
data = skb_put(skb, fraglen);     
skb_set_network_header(skb, exthdrlen);     
skb->transport_header = (skb->network_header + fragheaderlen);     
data += fragheaderlen;

copy是要拷貝的長度,為傳輸層報頭後的內容大小。getfrag()函數實現數據的拷貝,在icmp模塊中, getfrag()指向icmp_glue_bits()函數,它從[from] + offset處拷貝copy個字節到data + transhdrlen處。

copy = 

datalen - transhdrlen - fraggap;     
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {     
 err = -EFAULT;     
 kfree_skb(skb);     
 goto error;     
}

偏移offset加上已經拷貝的字節數copy,fraggap=0,length減去的就是IP報文內容長度,由於報文才56字節,一個 分片足夠,所以length=0,然後把新生成的skb放入sk->sk_write_queue中,然後執行下次while循環。各參數值:copy=42, offset=42, length=0, 更新transhdrlen=0。

offset += copy;     
length -= datalen - fraggap;     
transhdrlen = 0;     
……     
__skb_queue_tail(&sk->sk_write_queue, skb);     
continue;

while循環判斷條件是length > 0,因此跳出循環,完成了向IP層發送的數據生成,結果如下,注意, ICMP報頭還是沒有填寫的:

example 2:600 Byte echo 報文[假設MTU=520]

同樣,開始時sk->sk_write_queue()為空,初始 的設置與上述例子完全相同,不同處在於datalen此時比最大分片還要大,因此要設置datalen=maxfraglen-fragheaderlen。

if (datalen > mtu - fragheaderlen)     
 datalen = maxfraglen - fragheaderlen;

在完全第一個分片後,同樣會將分片skb放入sk_write_queue隊列,並進入 下一次while循環。此時各參數的值:datalen=496, fraglen=516, alloclen=516, skb->len=516,

copy=488, 

offset=488, length=600-496=104, 更新transhdrlen=0。     
__skb_queue_tail(&sk->sk_write_queue, skb);     
continue;

再次進入while循環,此時不同的是length=104,證明還有數據需要拷貝,此時會對待拷貝的數據進行判斷 ,下面所指的填充滿是針對maxfraglen而言的。
  @copy > 0,表示上個報文未被填充滿,這種情況在多次調用 ip_append_data()時會發生,這裡都是一次調用ip_append_data()的情況,所以不會出現,此時會填充數據到上個skb中
  @copy = 0,表示上個報文被填充滿,這個例子現在就是這種情況,此時會分配新的skb
  @copy < 0,表示上 個報文多填充了數據,這時因為maxfraglen是mtu8字節對齊後的值,所以maxfraglen范圍是[mtu-7, mtu],而在某些特殊情況下 ,比如上個報文已被填滿(實際還可能有[1, 7]字節的空間),待填充字節數n < 8,這時會把這n個節字補在最後一個報文的 尾部。
 對這個例子而言,上個skb剛好被填充滿,copy=0,此時分配新的skb。

copy = mtu - skb->len;   

  
if (copy < length)     
 copy = maxfraglen - skb->len;

分配新skb的流程與上個skb的分配過程相同,變化的只是偏移量offset,另外, icmp報頭只存在於第一個分片中,因為它也屬於IP內容的一部分,在這次拷貝完成後length=0,函數返回,最後結果如下:

ip_push_pending_frames() 將待發送的報文傳遞給網絡層

待發送的報文分片都在sk->sk_write_queue上,這 裡要做的就是從sk_write_queue上取出所有分片,合並成一個報文,添加IP報頭信息,使用ip_local_out()傳遞給網絡層處理。

要注意的是這裡的合並並不是真正的合並,只有第一個分片形成了skb,剩下的分片都放到了skb_shinfo(skb)- >frag_list上,雖然最後向下傳遞的只是一個skb,並實際上分片工作已經完成了,網絡層並不需要再次分片,由網絡的上層 完成分片是出於效率的考慮,雖然與協議標准有所出入。

首先從sk_write_queue上取出第一個分片,skb是最終向下傳遞的報 文,tail_skb指向skb的frag_list鏈表尾,即最後一個分片。

if ((skb = __skb_dequeue(&sk-

>sk_write_queue)) == NULL)     
 goto out;     
tail_skb = &(skb_shinfo(skb)->frag_list);

將skb->data指向ip報頭的位置

if (skb->data 

< skb_network_header(skb))     
 __skb_pull(skb, skb_network_offset(skb));

tmp_skb表示現在要插入skb的分片,首先通過__skb_pull()除去這些 分片的IP報頭,因為分片共用skb的IP報頭。然後通過tail_skb處理將tmp_skb鏈入frag_list中;最後增加報文長度計數,以前 說明過,skb->len代表linear buffer + paged buffer,skb->data_len代表paged_buffer,這裡插入的分片是增加了 paged buffer大小,所以對skb->len和skb->data_len都增加分片的長度。

while ((tmp_skb = __skb_dequeue

(&sk->sk_write_queue)) != NULL) {     
 __skb_pull(tmp_skb, skb_network_header_len(skb));     
 *tail_skb = tmp_skb;     
 tail_skb = &(tmp_skb->next);     
 skb->len += tmp_skb->len;     
 skb->data_len += tmp_skb->len;     
 skb->truesize += tmp_skb->truesize;     
 tmp_skb->destructor = NULL;     
 tmp_skb->sk = NULL;     
}

這裡是生成skb的IP報頭,設置其中的值

iph = (struct iphdr *)skb->data;     
iph->version = 4;     
…….     
skb->mark = sk->sk_mark;

最終通過ip_local_out()傳遞給IP層

err = ip_local_out(skb);

Copyright © Linux教程網 All Rights Reserved