歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux綜合 >> Linux內核

Linux內核分析 - 網絡[十四]:IP選項

內核版本:2.6.34

在發送報文時,可以調用函數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

源站路由實現是依靠兩次路由查找改變了報文的流程

源站路由的 更改需要重新計算校驗和

Copyright © Linux教程網 All Rights Reserved