內核版本: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
源站路由實現是依靠兩次路由查找改變了報文的流程
源站路由的 更改需要重新計算校驗和