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

摘要:
traceroute过程大致如下:1.增加IP协议头中TTL字段的值,并期望中间路由节点发送过期的ICMP消息。这个过程也是大多数计算机网络材料在讨论ICMP协议时都会提到的功能示例。这里暗示了一个实现,即IP消息的TTL值将在中间的每个跃点处减小,该值的减小将导致IP报头中的校验和字段进行相应的调整。
一、协议栈中的校验和
在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 etipv4ip_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 etipv4 etfilterip_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 et etfiltercore.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 etipv4af_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协议。

免责声明:文章转载自《从IP层TTL递减看校验和及ICMP》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Linux进程地址空间之初探:一关于 angular 项目 结合 RequireJs 的问题整理下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

TCP、UDP详解与抓包工具使用

参考:https://www.cnblogs.com/HPAHPA/p/7737641.html TCP、UDP详解 1、传输层存在的必要性 由于网络层的分组传输是不可靠的,无法了解数据到达终点的时间,无法了解数据未达终点的状态。因此有必要增强网络层提供服务的服务质量。 2、引入传输层的原因 面向连接的传输服务与面向连接的网络服务类似,都分为建立连接、数据...

由socket fd泄漏想到的一些问题

一、报文跨层传递 所有的网络协议栈都告诉我们:TCP/IP协议栈是分层的,低一层的协议无需也不能感觉到上层的协议,这个观念在我的脑海中根深蒂固,并且由衷的赞叹这种设计的思想,但是在经过一些简单的思考就会发现,这种分层并不是绝对的,正如这世间的一切。一个直观的问题是,一样米养百样人,同样的网卡上,可以跑IP/ARP协议,也可以有ICMP/IGMP/TCP/...

icmp 流量抓取 转发 代理(2)

客户端C到服务器S的icmp包经过本机P时被截获,在上一篇中已经介绍了如何获取原始目的地址,你必须将数据转发到原始目的地址S,并且在收到从原始目的地址的响应之后转发给客户端。此时,要实现透明代理,则你返回给客户端的icmp响应的源地址必须为客户端请求的原始目的地址S。由于使用的是raw socket,无法用IP_TRANSPARENT的socket选项绑定...

TCP握手

1.TCP的三次握手四次挥手   第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。   第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一...

TCP协议粘包问题详解

TCP协议粘包问题详解前言   在本章节中,我们将探讨TCP协议基于流式传输的最大一个问题,即粘包问题。本章主要介绍TCP粘包的原理与其三种解决粘包的方案。并且还会介绍为什么UDP协议不会产生粘包。   基于TCP协议的socket实现远程命令输入   我们准备做一个可以在Client端远程执行Server端shell命令并拿到其执行结果的程序,而涉及...

OSI结构和TCP/IP模型

   TCP/IP层次模型共分为五层:应用层HTTP、传输层TCP、网络层IP、数据链路层Data-link、物理层physical。     应用层—应用层是全部用户所面向的应用程序的统称。ICP/IP协议族在这一层面有着非常多协议来支持不同的应用。如我们进行万维网(WWW)訪问用到了HTTP协议、文件传输用FTP协议、电子邮件发送用SMTP、域名的解...