1. 程式人生 > >Linux內核分析 - 網絡[十四]:IP選項

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

ria copyto 還要 next 操作 目的 start 套接口 詳細講解

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

標簽: linux內核網絡structsocketdst 技術分享 分類:

內核版本: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(套接字特定函數)來設置選項。

[cpp] view plain copy
  1. if (level == SOL_SOCKET)
  2. err = sock_setsockopt(sock, level, optname, optval, optlen);
  3. else
  4. err = sock->ops->setsockopt(sock, level, optname, optval, optlen);

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

[cpp] view plain copy
  1. 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。

[cpp] view plain copy
  1. switch (optname) {
  2. case IP_OPTIONS:

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

[cpp] view plain copy
  1. err = ip_options_get_from_user(sock_net(sk), &opt, optval, optlen);
  2. opt = xchg(&inet->opt, opt);
  3. kfree(opt);

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

[cpp] view plain copy
  1. struct ip_options *opt = ip_options_get_alloc(optlen);
  2. static struct ip_options *ip_options_get_alloc(const int optlen)
  3. {
  4. return kzalloc(sizeof(struct ip_options) + ((optlen + 3) & ~3), GFP_KERNEL);
  5. }

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

[cpp] view plain copy
  1. copy_from_user(opt->__data, data, optlen);
  2. 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()。

[cpp] view plain copy
  1. while (optlen & 3)
  2. opt->__data[optlen++] = IPOPT_END;
  3. opt->optlen = optlen;

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

[cpp] view plain copy
  1. ip_options_compile(net, opt, NULL);
  2. kfree(*optp);
  3. *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所指向的位置是沒有意義的。

[cpp] view plain copy
  1. if (skb != NULL) {
  2. rt = skb_rtable(skb);
  3. optptr = (unsigned char *)&(ip_hdr(skb)[1]);
  4. } else
  5. optptr = opt->__data;
  6. 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之後。

[cpp] view plain copy
  1. for (l = opt->optlen; l > 0; ) {
  2. switch (*optptr) {
  3. case IPOPT_END: ….
  4. case IPOPT_NOOP: ...
  5. …...
  6. optlen = optptr[1];
  7. if (optlen<2 || optlen>l) {
  8. pp_ptr = optptr;
  9. goto error;
  10. }
  11. case …...
  12. …...// 處理代碼段
  13. }
  14. l -= optlen;
  15. optptr += optlen;
  16. }

還是以寬松源路由為例子:

[cpp] view plain copy
  1. case IPOPT_LSRR:

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

[cpp] view plain copy
  1. if (optlen < 3) {
  2. pp_ptr = optptr + 1;
  3. goto error;
  4. }
  5. if (optptr[2] < 4) {
  6. pp_ptr = optptr + 2;
  7. goto error;
  8. }
  9. /* NB: cf RFC-1812 5.2.4.1 */
  10. if (opt->srr) {
  11. pp_ptr = optptr;
  12. goto error;
  13. }

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

[cpp] view plain copy
  1. if (!skb) {
  2. if (optptr[2] != 4 || optlen < 7 || ((optlen-3) & 3)) {
  3. pp_ptr = optptr + 1;
  4. goto error;
  5. }
  6. memcpy(&opt->faddr, &optptr[3], 4);
  7. if (optlen > 7)
  8. memmove(&optptr[3], &optptr[7], optlen-7);
  9. }

技術分享

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

[cpp] view plain copy
  1. opt->is_strictroute = (optptr[0] == IPOPT_SSRR);
  2. 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選項有關。

[cpp] view plain copy
  1. memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
  2. struct inet_skb_parm {
  3. struct ip_options opt; /* Compiled IP options */
  4. unsigned char flags;
  5. };

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

[cpp] view plain copy
  1. if (iph->ihl > 5 && ip_rcv_options(skb))
  2. goto drop;

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

[cpp] view plain copy
  1. iph = ip_hdr(skb);
  2. opt = &(IPCB(skb)->opt);
  3. 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選項的處理要留帶到發送報文時。

[cpp] view plain copy
  1. if (ip_options_compile(dev_net(dev), opt, skb)) {
  2. IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
  3. goto drop;
  4. }

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

[cpp] view plain copy
  1. if (unlikely(opt->srr)) {
  2. ……
  3. if (ip_options_rcv_srr(skb))
  4. goto drop;
  5. }
  6. return 0;

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

[cpp] view plain copy
  1. if (skb->pkt_type != PACKET_HOST)
  2. return -EINVAL;
  3. if (rt->rt_type == RTN_UNICAST) {
  4. if (!opt->is_strictroute)
  5. return 0;
  6. icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl(16<<24));
  7. return -EINVAL;
  8. }
  9. if (rt->rt_type != RTN_LOCAL)
  10. return -EINVAL;

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

[cpp] view plain copy
  1. for (srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4) {
  2. memcpy(&nexthop, &optptr[srrptr-1], 4);
  3. rt = skb_rtable(skb);
  4. skb_dst_set(skb, NULL);
  5. err = ip_route_input(skb, nexthop, iph->saddr, iph->tos, skb->dev);
  6. rt2 = skb_rtable(skb);
  7. if (err || (rt2->rt_type != RTN_UNICAST && rt2->rt_type != RTN_LOCAL)) {
  8. ip_rt_put(rt2);
  9. skb_dst_set(skb, &rt->u.dst);
  10. return -EINVAL;
  11. }
  12. ip_rt_put(rt);
  13. if (rt2->rt_type != RTN_LOCAL)
  14. break;
  15. /* Superfast 8) loopback forward */
  16. memcpy(&iph->daddr, &optptr[srrptr-1], 4);
  17. opt->is_changed = 1;
  18. }

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

[cpp] view plain copy
  1. if (srrptr <= srrspace) {
  2. opt->srr_is_hit = 1;
  3. opt->is_changed = 1;
  4. }

根據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個字節。

[cpp] view plain copy
  1. if (opt->srr_is_hit) {
  2. int srrptr, srrspace;
  3. optptr = raw + opt->srr;
  4. for ( srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4 ) {
  5. if (srrptr + 3 > srrspace)
  6. break;
  7. if (memcmp(&rt->rt_dst, &optptr[srrptr-1], 4) == 0)
  8. break;
  9. }
  10. if (srrptr + 3 <= srrspace) {
  11. opt->is_changed = 1;
  12. ip_rt_get_source(&optptr[srrptr-1], rt);
  13. ip_hdr(skb)->daddr = rt->rt_dst;
  14. optptr[2] = srrptr+4;
  15. } else if (net_ratelimit())
  16. printk(KERN_CRIT "ip_forward(): Argh! Destination lost!\n");
  17. ……
  18. }

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

技術分享

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

技術分享

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

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