注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

从IP层TTL递减看校验和及ICMP  

2016-10-31 20:43:31|  分类: 计算机网络 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、协议栈中的校验和
在IP协议及UDP/TCP协议中都是用了校验和字段,这个字段通常没有人会关注,就好像现在已经没有人知道当时的一个字节中保留的一个校验bit一样。我也是偶尔看我们常用的traceroute功能的时候间接看到了这个字段。traceroute的流程大致是这样的:从1不断的增加IP协议头中TTL字段的数值,期待中间的路由节点发送一个报文过期的ICMP报文,这个流程也是大部分计算机网络资料讲到ICMP协议时都会提到的功能实例。这里其实隐含着一个实现,那就是在IP报文在经过中间的每一跳(hop)时要递减TTL的数值,而这个数值的递减将会导致IP header中校验和字段需要对应的进行调整。这个操作在之前看内核代码的时候通常是直接跳过的,就像开始看代码的时候大部分人会自动跳过引用计数,锁之类的看似不重要但是事实上非常重要、包涵了整个系统重要复杂性的代码。这其实也无可厚非,刚开始看待的时候,大家主要看的就是功能或者是流程性的东西,对于这些周边的功能会自动过滤掉。其中rfc791对于整个IPheader报文的结构定义为下面的内容,这里其实还有容易被忽略的|         Identification        |Flags|      Fragment Offset    |字段等,它们其实也是实现整个IP功能的重要数据结构,只是暂时和这里讨论的问题没有直接的关系。
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
二、校验的计算及使用
RFC中对于该字段计算方法的说明比较简单:
    The checksum algorithm is:

      The checksum field is the 16 bit one's complement of the one's
      complement sum of all 16 bit words in the header.  For purposes of
      computing the checksum, the value of the checksum field is zero.
其中"16 bit one's complement of the one's complement sum of all 16 bit words in the header"说明这里使用的是整个header中"所有16 bits字的1的补码和"的1的补码,在计算这个校验和的时候,这个字段置零。在接收端进行校验的时候,接收端执行相同的操作(先求和,然后求补码,这个结果必须为零)。这里所说的“1的补码和”其实就是“带进位的加法”,也就是对于16bit word相加之后,如果生成了进位,需要将这个进位再次加入计算和中,这样和常规的不带进位的加法相比有一个额外的反馈,从而让丢失的信息相对较少。只是这个计算步骤刚好和"1的补码和"操作类似,所以硬生生的给了一个逼格这么高的名字,让人乍一看非常困惑。
这种运算其实可以关注一个比较重要的特性,那就是对于任何一个非零的数X,它和0xFFFF的补码和为该数本身,这一点非常容易验证。另一个不太明显的特性就是对于X+Y这样的操作,如果X和Y均不为零,那么它们的运算结果必定不为零。
这样的校验和在TCP/UDP协议中同样存在,不过和IP中的校验和比较,它们通常只是在接收端进行一次性校验使用,在IP中间传输的时候并不用考虑。但是也同样存在需要在中间修改这个字段内容的情况,例如在使用SNAT/DNAT的场景下,在这些场景下,通常不仅要修改源/目的地址,并且对于端口的修改同样不可避免,而端口的定义通常在于TCP/UDP中。
三、IP层对于TTL的修改
对于TTL的修改通常发生在网络层对一个报文进行转发的情况下,也就是典型的在linux-2.6.21\net\ipv4\ip_forward.c

int ip_forward(struct sk_buff *skb)
{
……
/*
* According to the RFC, we must first decrease the TTL field. If
* that reaches zero, we must reply an ICMP control message telling
* that the packet's lifetime expired.
*/
if (skb->nh.iph->ttl <= 1)
goto too_many_hops;
……
/* Decrease ttl after skb cow done */
ip_decrease_ttl(iph);
……
too_many_hops:
/* Tell the sender its packet died... */
IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
drop:
kfree_skb(skb);
return NET_RX_DROP;
}
这里对于TTL的递减和校验和的更新看起来非常简单,但是乍一看也有点让人费解,这个操作通过ip_decrease_ttl函数完成:
/* The function in 2.2 was invalid, producing wrong result for
 * check=0xFEFF. It was noticed by Arthur Skawina _year_ ago. --ANK(000625) */
static inline
int ip_decrease_ttl(struct iphdr *iph)
{
u32 check = (__force u32)iph->check;
check += (__force u32)htons(0x0100);
iph->check = (__force __sum16)(check + (check>=0xFFFF));
return --iph->ttl;
}
这里从整体上来看就比较简单了。对于这里的操作动作其实非常明确,就是在Ipheader中的ttl字段中递减,这个递减是通过在函数的最后--iph->ttl完成。为了保证校验和同时不变,就需要对最终的校验和执行一个和这个操作相反的操作,也就是递增。从之前的Ip header的定义可以看到,作为一个16bits word,它位于|  Time to Live |    Protocol   |这个word中,所以它的递增操作是加上一个0x0100而不是直接加上一个0x0001。
四、TCP层在NAT中可能对校验和的修改
正如前面所说的,这个场景主要发生在NAT这样的场景中,这个也是我最早感受到校验和的存在,之后在使用traceroute的时候再次遇到校验和。以TCP协议中端口修改的场景为例,在函数linux-2.6.21\net\ipv4\netfilter\ip_nat_proto_tcp.c:
static int
tcp_manip_pkt(struct sk_buff **pskb,
     unsigned int iphdroff,
     const struct ip_conntrack_tuple *tuple,
     enum ip_nat_manip_type maniptype)
{
……
nf_proto_csum_replace4(&hdr->check, *pskb, oldip, newip, 1);
nf_proto_csum_replace2(&hdr->check, *pskb, oldport, newport, 0);
return 1;
}
以对于4字节字段的修改为例,可以明显的看到,对于这个地方的实现依然间接,但是并没有TTL操作那么飘逸,相对来说比较直观古朴。
linux-2.6.21\net\netfilter\core.c
void nf_proto_csum_replace4(__sum16 *sum, struct sk_buff *skb,
   __be32 from, __be32 to, int pseudohdr)
{
__be32 diff[] = { ~from, to };
if (skb->ip_summed != CHECKSUM_PARTIAL) {
*sum = csum_fold(csum_partial((char *)diff, sizeof(diff),
~csum_unfold(*sum)));
if (skb->ip_summed == CHECKSUM_COMPLETE && pseudohdr)
skb->csum = ~csum_partial((char *)diff, sizeof(diff),
~skb->csum);
} else if (pseudohdr)
*sum = ~csum_fold(csum_partial((char *)diff, sizeof(diff),
csum_unfold(*sum)));
}
这里加上~from,所以之前的from + ~from = 0xFFFF,由于前面说过,任何一个非零数加上0xFFFF都等于该数本身,所以加上~from相当于把这个值首先从校验和中清除掉,然后加上to就得到了修正后的数据。至于为什么说之前的数据一定非零呢?因为IP header中的version肯定非零(而且TTL正常情况下也应该大于等于1)。
五、以UDP为例看下ICMP回包如何找到发送方
当一个IP报文超过生命周期之后,此时中转节点需要发送ICMP报文通知给发送方。但是ICMP也是运行在IP层上的,在IP层中没有唯一标志一个UDP socket所需要的端口信息,那么对于一个ICMP回包接收方如何找到原始发送方socket呢?

首先看下ICMP的发送请求:
/*
 * Send an ICMP message in response to a situation
 *
 * RFC 1122: 3.2.2 MUST send at least the IP header and 8 bytes of header.
 *  MAY send more (we do).
 * MUST NOT change this header information.
 * MUST NOT reply to a multicast/broadcast IP address.
 * MUST NOT reply to a multicast/broadcast MAC address.
 * MUST reply to only the first fragment.
 */

void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info)
{
……
icmp_param.skb  = skb_in;
icmp_param.offset = skb_in->nh.raw - skb_in->data;
……
/* RFC says return as much as we can without exceeding 576 bytes. */

room = dst_mtu(&rt->u.dst);
if (room > 576)
room = 576;
room -= sizeof(struct iphdr) + icmp_param.replyopts.optlen;
room -= sizeof(struct icmphdr);

icmp_param.data_len = skb_in->len - icmp_param.offset;
if (icmp_param.data_len > room)
icmp_param.data_len = room;
……
注释说明RFC规定必须至少回传网络层协议的前8个字节,对于UDP来说,这前8个字节是
struct udphdr {
__be16 source;
__be16 dest;
__be16 len;
__sum16 check;
};
对于TCP来说,
struct tcphdr {
__be16 source;
__be16 dest;
__be32 seq;
……
所以通过ICMP的回包,对于常见的UDP和TCP来说,都可以找到端口号,这个可能也是TCP/UDP把端口号放在协议最开始的一个原因吧。具体对于linux的实现来说,它回传字段的策略"RFC says return as much as we can without exceeding 576 bytes"。
六、ICMP接收端的处理
ip_rcv===>>>ip_rcv_finish===>>>ip_local_deliver===>>>ip_local_deliver_finish===>>>icmp_rcv===>>>icmp_unreach===>>>udp_err==>>>__udp4_lib_err
static void icmp_unreach(struct sk_buff *skb)
{
……
iph = (struct iphdr *)skb->data;
protocol = iph->protocol;

/*
* Deliver ICMP message to raw sockets. Pretty useless feature?
*/

/* Note: See raw.c and net/raw.h, RAWV4_HTABLE_SIZE==MAX_INET_PROTOS */
hash = protocol & (MAX_INET_PROTOS - 1);
read_lock(&raw_v4_lock);
if ((raw_sk = sk_head(&raw_v4_htable[hash])) != NULL) {
while ((raw_sk = __raw_v4_lookup(raw_sk, protocol, iph->daddr,
iph->saddr,
skb->dev->ifindex)) != NULL) {
raw_err(raw_sk, skb, info);
raw_sk = sk_next(raw_sk);
iph = (struct iphdr *)skb->data;
}
}
read_unlock(&raw_v4_lock);
rcu_read_lock();
ipprot = rcu_dereference(inet_protos[hash]);
if (ipprot && ipprot->err_handler)
ipprot->err_handler(skb, info);
rcu_read_unlock();
}
对于udp来说,这里ipprot->err_handle执行到udp_err==>>>__udp4_lib_err
void __udp4_lib_err(struct sk_buff *skb, u32 info, struct hlist_head udptable[])
{
struct inet_sock *inet;
struct iphdr *iph = (struct iphdr*)skb->data;
struct udphdr *uh = (struct udphdr*)(skb->data+(iph->ihl<<2));
int type = skb->h.icmph->type;
int code = skb->h.icmph->code;
struct sock *sk;
int harderr;
int err;

sk = __udp4_lib_lookup(iph->daddr, uh->dest, iph->saddr, uh->source,
      skb->dev->ifindex, udptable    );
……
/*
*      RFC1122: OK.  Passes ICMP errors back to application, as per
* 4.1.3.3.
*/
if (!inet->recverr) {
if (!harderr || sk->sk_state != TCP_ESTABLISHED)
goto out;
} else {
ip_icmp_error(sk, skb, err, uh->dest, info, (u8*)(uh+1));
}
sk->sk_err = err;
sk->sk_error_report(sk);
out:
sock_put(sk);
}
函数最后的sk->sk_error_report(sk)将会最终传递给socket系统调用,这也是为什么udp无状态但是可以知道链路错误的原因。
七、raw套接口的处理
1、接收入口
接收主要有两个入口,一个是正常的本地接收
static inline int ip_local_deliver_finish(struct sk_buff *skb)
{
……
/* If there maybe a raw socket we must check - if not we
* don't care less
*/
if (raw_sk && !raw_v4_input(skb, skb->nh.iph, hash))
raw_sk = NULL;
……
}
一个是ICMP报文的特殊处理,也就是在前面icmp_unreach函数注释中说明的"Pretty useless feature?"地方。两者使用的socket的查找都是通过__raw_v4_lookup来实现的。这个函数对于raw的查找只使用了protocol、src、dst三个信息,这意味着使用raw socket的时候,可以接收到某一类型协议(socket(int domain, int type, int protocol)系统调用的第三个参数的所有报文)
struct sock *__raw_v4_lookup(struct sock *sk, unsigned short num,
    __be32 raddr, __be32 laddr,
    int dif)
{
struct hlist_node *node;

sk_for_each_from(sk, node) {
struct inet_sock *inet = inet_sk(sk);

if (inet->num == num &&
   !(inet->daddr && inet->daddr != raddr) &&
   !(inet->rcv_saddr && inet->rcv_saddr != laddr) &&
   !(sk->sk_bound_dev_if && sk->sk_bound_dev_if != dif))
goto found; /* gotcha */
}
sk = NULL;
found:
return sk;
}
2、ICMP类型socket创建的内核配置
在do_raw_setsockopt函数中:
static int do_raw_setsockopt(struct sock *sk, int level, int optname,
 char __user *optval, int optlen)
{
if (optname == ICMP_FILTER) {
if (inet_sk(sk)->num != IPPROTO_ICMP)
return -EOPNOTSUPP;
else
return raw_seticmpfilter(sk, optval, optlen);
}
return -ENOPROTOOPT;
}
可以看到,可以创建ICMP(相对于UDP/TCP)类型的socket用来接收系统收到的所有ICMP消息,并且可以通过ICMP_FILTER命令来设置过滤一些特定协议(TCP/UDP等)的ICMP报文。
我们知道,对于是否可以创建socket,这个对于af_inet来说,主要是通过linux-2.6.21\net\ipv4\af_inet.c来控制的:
/* Upon startup we insert all the elements in inetsw_array[] into
 * the linked list inetsw.
 */
static struct inet_protosw inetsw_array[] =
{
{
.type =       SOCK_STREAM,
.protocol =   IPPROTO_TCP,
.prot =       &tcp_prot,
.ops =        &inet_stream_ops,
.capability = -1,
.no_check =   0,
.flags =      INET_PROTOSW_PERMANENT |
     INET_PROTOSW_ICSK,
},

{
.type =       SOCK_DGRAM,
.protocol =   IPPROTO_UDP,
.prot =       &udp_prot,
.ops =        &inet_dgram_ops,
.capability = -1,
.no_check =   UDP_CSUM_DEFAULT,
.flags =      INET_PROTOSW_PERMANENT,
       },


       {
      .type =       SOCK_RAW,
      .protocol =   IPPROTO_IP, /* wild card */
      .prot =       &raw_prot,
      .ops =        &inet_sockraw_ops,
      .capability = CAP_NET_RAW,
      .no_check =   UDP_CSUM_DEFAULT,
      .flags =      INET_PROTOSW_REUSE,
       }
};
这里可以看到,当type选择SOCK_RAW之后,最后的protocol是通配符类型的IPPROTO_IP,所以可以创建TCP/UDP/ICMP类型的socket。
3、traceroute如何使用ICMP
int
main(int argc, char **argv)
{
……
if (useicmp) {
outip->ip_p = IPPROTO_ICMP;

outicmp = (struct icmp *)outp;
outicmp->icmp_type = ICMP_ECHO;
outicmp->icmp_id = htons(ident);

outdata = (struct outdata *)(outp + 8); /* XXX magic number */

u_short port = 32768 + 666; /* start udp dest port # for probe packets */

void
send_probe(register int seq, int ttl, register struct timeval *tp)
{

if (useicmp)
outicmp->icmp_seq = htons(seq);
else
outudp->uh_dport = htons(port + seq);
……
可以看到,主要是通过对TCP/UDP没有感知的ICMP 的ECHO命令来完成中间路由的侦测,不过也可能由于目标机器屏蔽了ICMP报文而没有回包。所以缺省traceroute使用的是UDP协议。
  评论这张
 
阅读(42)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017