原文链接:https://mp.weixin.qq.com/s/vIiEtSH4DDBb2IhwfZvNRw
本文要点
引言
ICMP结构
ICMP的protosw结构
输入处理:icmp_input函数
差错处理
请求处理
回显询问
时间戳询问
地址掩码询问
信息询问
路由器发现
重定向处理
回答处理
输出处理
icmp_error函数
icmp_reflect函数
icmp_send函数
icmp_sysctl函数
小结
引言
ICMP在IP系统间传递差错和管理报文,是任何IP实现必要和要求的组成部分。ICMP有自己的传输协议号1,允许ICMP报文在IP数据内携带。应用程序可以直接从原始IP接口发送或者接收ICMP报文。
ICMP报文分成两类:差错和查询。查询报文是用一对请求和回答定义的。差错报文通常包含引起错误的IP数据报的第一个分片的IP首部和选项,加上该分片数据部分的前8个字节。标准假定这8个字节包含了该分组运输层首部的所有分用信息,这样运输层协议可以向正确的进程提交ICMP差错报文。
TCP和UDP端口号在它们首部的前8个字节内出现。
图1和图2显示了所有目前定义的ICMP报文。双线上面是ICMP请求和回答报文;双线下面的是ICMP差错报文。
图1 ICMP报文类型和代码
图2 ICMP报文类型和代码(续)
图1、2、3中含有大量信息:
PRC_栏显示了Net/3处理的与协议无关的差错码和ICMP报文之间的映射。对请求和回答,这一列是空的,因为这种情况下不会产生差错。如果对一个ICMP差错,这一行为空,说明Net/3不识别该码,并自动丢弃该差错报文。
图4显示了图3中所列函数的位置。
icmp_input栏是icmp_input为每个ICMP报文调用的函数。
UDP栏是为UDP插口处理ICMP报文的函数。
TCP栏是为TCP插口处理ICMP报文的函数。注意,是tcp_quence处理ICMP源站抑制差错,而不是tcp_notify。
如果errno栏为空,内核不向进程报告ICMP报文。
表的最后一行显示,在用于接收ICMP报文的进程的接收点上,不识别的ICMP报文被提交给原来的IP协议。
图3 ICMP报文类型和代码(续)
图4 ICMP输入处理时调用的函数
在Net/3中,ICMP是作为IP之上的一个运输层协议实现的,它不产生差错和请求;它代表其它协议格式化并发送报文。ICMP传递到达的差错,并向适当的运输协议或者等待ICMP报文的进行发出回答。另一方面,ICMP用一个合适的ICMP回答响应大多数ICMP请求。图5对此作了总结。
图5 ICMP报文处理
ICMP结构
Net/3通过图6中的icmp结构访问某个ICMP报文。
图6 icmp结构
42~45 icmp_type标识特定报文,icmp_code进一步指定该报文(图1第一栏)。计算icmp_cksum的算法与IP首部检验和相同,保护整个ICMP报文。
46~79 联合icmp_hun(首部联合)和icmp_dun(数据联合)根据icmp_type和icmp_code访问多种ICMP报文。每种ICMP报文都使用icmp_hun;只有一部分报文用icmp_dun。没有使用的字段必须设置为0。
80~86 我们已经看到,利用其它嵌套的结构,#define宏可以简化结结构成员的访问。
图7显示了ICMP报文的整体结构,并再次强调ICMP报文是封装在IP数据报里的。
图7 一个ICMP报文
ICMP的protosw结构
inetsw[4](图9)的protosw结构描述了ICMP,并且支持内核和进程对协议的访问。图8显示了该结构。在内核里,icmp_input处理到达的ICMP报文,进程产生的外出ICMP报文由rip_output处理。
图8 ICMP的inetsw项
图9 数值为1的ip_p选择了inetsw[4]
输入处理:icmp_input函数
回想起ipintr对数据报进行分用是根据IP首部中的传输协议编号ip_p。对于ICMP报文,ip_p是1,并通过ip_protox选择inetsw[4]。
当一个ICMP报文到达时,IP层通过inetsw[4]的pr_input函数,间接调用icmp_input。
我们看到,在icmp_input中,每个ICMP报文要被处理3次:被icmp_input处理一次;被与ICMP差错报文中的IP分组相关联的传输协议处理一次;被记录收到ICMP报文的进程处理一次。ICMP输入处理过程的总的构成情况如图10所示。
图10 ICMP的输入处理过程
我们将分五个部分讨论icmp_input:
验证收到的报文
ICMP差错报文
ICMP请求报文
ICMP重定向报文
ICMP回答报文
icmp_input函数的第一部分如图11所示。
图11 icmp_input函数
a. 静态结构
131~134 因为icmp_input是在中断时调用的,此时堆栈的大小是有限的。所以,为了在每次调用icmp_input时,避免动态分配千万的延迟,以及使堆栈最小,这4个结构是静态分配的。icmp_input把这4个结构用作临时变量。
icmpsrc的命名引起误解,因为icmp_input把它用作临时sockaddr_in变量,而它也从未包含过源站地址。这是Net/2版本的历史遗留问题,不予以讨论。
b. 确认报文
135~139 icmp_input中参数m是指向一个ICMP数据报的指针,hlen是该数据报IP首部的字节长度。图12列出了几个在icmp_input里用于简化检测无效ICMP报文的常量。
图12 ICMP引用的用来验证报文的常量
140~160 icmp_input从ip_len取出ICMP报文的大小,并把它存放在icmplen中。第8章讲过,ipintr从ip_len中排除了IP首部的长度。如果报文长度太短,不是有效报文,就生成icps_tooshort,并丢弃该报文。如果在第一个mbuf中,ICMP首部和IP首部不是连续的,则由m_pullup保证ICMP首部以及封装的IP分组的IP首部在同一个mbuf中。
c. 验证检验和
161~170 icmp_input隐藏mbuf中的IP首部,并且in_cksum验证ICMP的检验和。如果报文被破坏,就增加icps_checksum,并丢弃该报文。
d. 验证类型
171~175 如果报文类型(icmp_type)不在识别范围内,icmp_input就跳过switch执行raw语句。如果在识别范围内,icmp_input复制icmp_code,switch按照icmp_type处理该报文。
在ICMP switch语句处理完后,icmp_input向rip_input发送ICMP报文,后者把ICMP报文发布给准备接收的进程。只有那些被破坏的报文以及只由内核处理的ICMP请求报文才不传给rip_input。在这两种情况下,icmp_input都立即返回,并跳过raw处的源程序。
e. 原始ICMP输入
317~325 icmp_input把到达的报文传给rip_input,rip_input依据报文里含有的协议及源站和目的站地址信息,把报文发布给正在监听的进程。
原始IP机制允许进程直接发送和接收ICMP报文,有如下几个原因:
新的ICMP报文可以由进程处理而无需修改内核(例如路由器通告)。
可以用进程而无需用内核模块来实现ICMP请求和处理回答机制(ping和traceroute)。
进程可以增加对报文的内核处理。例如,内核在更新完它的路由表后,会把ICMP重定向报文传给一个路由守护程序。
差错处理
我们首先考虑ICMP差错报文。当主机发出的数据报无法成功地提交给目的主机时,它就接收这些报文。目的主机或者中间路由器生成这些报文,并将它们返回到原来的系统。图13显示了多种ICMP差错报文的格式。
图13 ICMP差错报文
图14中的源程序来自图11中的switch语句。
图14 icmp_input函数:差错报文
172~216 对ICMP差错的处理是最少的,因为这主要是运输层协议的责任。icmp_input把icmp_type和icmp_code映射到一个与协议无关的差错码集上,该差错码是由PRC_常量(图15)表示的。PRC_常量有一个隐含的顺序,正好与ICMP的code相对应。这就解释了为什么code是按一个PRC_常量递增的。
217~225 如果识别出type和code,icmp_input就跳转到deliver。如果没有识别出来,就跳到badcode。
图15 与协议无关的差错码
如果对所报文的差错而言,报文长度不正确,icps_badlen加1,并丢弃该报文。Net/3总是丢弃无效的ICMP报文,也不生成有关该无效报文的ICMP差错。这样, 就避免在两个有缺陷的实现之间形成无限的差错报文序列。
226~231 icmp_input调用运输层协议的pr_ctlinput函数,该函数根据原始数据报的ip_p,把到达分组分用到正确的协议,从而构造出原始的IP数据报。差错码(code)、原始IP数据报的目的地址(icmpsrc)以及一个指向无效数据报的指针(icmp_ip)被传给pr_ctlinput。
232~234 最后,icps_datacode加1,并终止switch语句的执行。
请求处理
Net/3响应具有正确格式的ICMP请求报文,但把无效ICMP报文传给rip_input。
除路由器通告报文外,大多数Net/3所接收的ICMP报文都生成回答报文。为避免为回答报文分配新的mbuf,icmp_input把请求的缓存转换成回答的缓存,并返回给发送方。
1. 回显询问:ICMP_ECHO和ICMP_ECHOREPLY
尽管ICMP非常简单,但是ICMP回显请求和回答却是网络管理员最有力的诊断工具。发出ICMP回显语法称为“ping”一个主机,也就是调用ping程序一次。许多系统提供该程序来手工发送ICMP回答请求。
图16是ICMP回显请求和回答报文的结构。
图16 ICMP回显请求和回答
icmp_code总是0。icmp_id和icmp_seq设置成请求的发送方,回答中也不做修改。源系统可以用这些字段匹配请求和回答。icmp_data中到达的所有数据也被反射。图17是ICMP回显处理和icmp_input实现反射ICMP请求的源程序。
图17 icmp_input函数:回显请求和回答
235~237 通过把icmp_type变成ICMP_ECHOREPLY,并跳转到reflect发送回答,icmp_input把回显请求转换成了回显回答。
277~282 在为每个ICMP请求构造回答之后,icmp_input执行reflect处的程序。在这里,存储数据报正确的长度被恢复,在icps_reflect和icps_outhist[]中分别计算请求的数量和ICMP报文的类型。icmp_reflect把回答发回给请求方。
2. 时间戳询问:ICMP_TSTAMP和ICMP_TSTAMPREPLY
ICMP时间戳报文如图18所示。
图18 ICMP时间戳请求和回答
icmp_code总是0。icmp_id和icmp_seq的作用与它们在ICMP回显报文中的一样。请求的发送方设置icmp_otime(发出请求的时间);icmp_rtime(收到请求的时间)和icmp_ttime(发出回答的时间)由回答的发送方设置。所有时间都是从UTC午夜开始的毫秒数。
图19是实现时间戳报文的程序。
图19 icmp_input函数:时间戳请求和回答
238~246 icmp_input对ICMP的响应,包括:把icmp_type改为ICMP_TSTAMPREPLY,记录当前icmp_rtime和icmp_ttime,并跳转到reflect发送回答。
3. 地址掩码询问:ICMP_MASKREQ和ICMP_MASKREPLY
ICMP地址掩码请求和回答如图20所示。
图20 ICMP地址掩码请求和回答
除非系统被明确地配置成地址掩码的授权代理,否则禁止向其发送掩码回答。这样,就避免系统与所有向它发出请求的系统共享不正确的地址掩码。如果没有管理员授权回答,系统也要忽略地址掩码请求。
如果全局整数icmpmaskrepl非零,Net/3会响应地址掩码请求。icmpmaskrepl的默认值是0,icmp_sysctl可以通过sysctl(8)程序修改它。
地址掩码报文的处理如图21所示。
图21 icmp_input函数:地址掩码请求和回答
247~256 如果没有配置响应掩码请求,或者该请求报文太短,就中止switch的执行,并把报文传给rip_input(图11)。
a. 选择子网掩码
257~267 如果地址掩码请求被发到0.0.0.0或255.255.255.255,源地址被保存 icmpdst中。在这里,ifaof_ifpforaddr把icmpdst作为源站地址,在同一网络上查找in_ifaddr结构。如果源站地址是0.0.0.0或255.255.255.255,ifaof_ifpforaddr返回一个指针,该指针指向与接收接口相关的第一个IP地址。
default情况为ifaof_ifpforaddr保存目的地址。
b. 转换成回答
269~270 通过改变icmp_type,并把所选子网掩码ia_sockmask复制到icmp_mask,就完成了把请求转换成回答的工作。
c. 选择目的地址
271~276 如果请求的源站地址全0,并且源站不知道自己的地址,Net/3必须广播这个回答,使源站系统接收到这个报文。在这种情况下,如果接收接口位于某个广播或点到点网络上,该回答的目的地址将分别是ia_broadaddr和ia_dstaddr。icmp_input把回答的目的地址放在ip_src里,因为reflect处的程序会把源站和目的站地址倒过来。不改变单播请求的地址。
4. 信息询问:ICMP_IREQ和ICMP_IREQREPLY
ICMP信息报文已经过时了。它们企图广播一个源和目的站地址字段的网络部分为全0的请求,使系统发现连接的IP网络的数据。响应该请求的主机将回返一个填好网络号的报文。主机还需要其它办法找到地址的主机部分。
5. 路由器发现:ICMP_ROUTERADVERT和ICMP_ROUTERSOLICIT
Net/3内核不直接处理这些报文,而由rip_input把它们传给一个用户级守护程序,由它发送和响应这种报文。
重定向处理
图22显示了ICMP重定向报文的格式。
图22 ICMP重定向报文
icmp_input中要讨论的最后一个case是ICMP_REDIRECT。如概说《TCP/IP详解 卷2》第8章 IP:网际协议讨论,当分组被发给错误的路由器时,产生重定向报文。该路由器把分组转发给正确的路由器,并发回一个ICMP重定向报文,系统把信息记入它自己的路由表。
图23显示了icmp_input用来处理重定向报文的程序。
图23 icmp_input函数:重定向报文
a. 验证
282~290 如果重定向报文中含有未识别的ICMP码,icmp_input就跳到badcode;如果报文具有无效长度或者封装的IP分组具有无效首部长度,则中止switch。图12显示了ICMP差错报文的最小长度是36(ICMP_ADVLENMIN)。ICMP_ADVLEN是当icp所指向的分组有IP选项时,ICMP差错报文的最小长度。
291~300 icmp_input分别把重定向报文的源站地址,为原始分组推荐的路由器(第一跳目的地)和原始分组的最终目的地址分配给icmpgw、icmpdst和icmpsrc。
b. 更新路由
301~306 重定向信息被传给rtredirect,由这个函数更新路由表。重定向的目的地址(保存在icmpsrc)被传给pfctlinput,由它通告重定向的所有协议域,使协议有机会把缓存的目的站的路由作废。
回答处理
内核不处理任何ICMP回答报文。ICMP请求由进程产生,内核从不产生请求。所以,内核把它接收到的所有回答传给等待ICMP报文的进程。另外,ICMP路由器发现报文被传给rip_input。图24显示了回答处理的源代码。
图24 icmp_input函数:回答报文
307~322 内核无需对ICMP回答报文做出任何反应,所以在switch语句后的raw语句继续执行。注意,switch语句的default情况也把控制传给在raw处的代码。
输出处理
有几种方法产生外出的ICMP报文。比如icmp_error来产生和发送ICMP差错报文;icmp_reflect发送ICMP回答报文。同时,进程也可能通过原始ICMP协议生成ICMP报文。图25显示了这些函数与ICMP外出处理之间的关系。
图25 ICMP外出处理
icmp_error函数
icmp_error在IP或运输层协议的请求下,构造一个ICMP差错请求报文,并把它传给icmp_reflect,在那里该报文被返回无效数据报的源站。
我们分三个部分介绍这个函数:
确认该报文(图26)
构造首部(图28)
把原来的数据报包含进来(图29)
图26 icmp_error函数:验证
46~57 参数:n,指向包含无效数据报缓存链的指针;type和code,ICMP差错类型和代码;dest,ICMP重定向报文中的下一跳路由器地址;以及destifp,指向原始IP分组外出接口的指针。mtod把缓存链指针n转换成oip,oip是指向缓存中ip结构的指针。原始IP分组的字节长度保存在ioplen中。
58~75 icps_error统计除重定向报文以外的所有ICMP差错。Net/3不把重定向报文看作错误。
icmp_error丢弃无效数据报oip,并有在以下情况下,不发送差错报文:
除IP_MF和IP_DF外,ip_off的某些位非零。这表明oip不是数据报的第一个分片,而且ICMP决不能为跟踪数据报的分片而生成差错报文。
无效数据报本身是一个ICMP差错报文。如果icmp_type是ICMP请求或者响应类型,则ICMP_INFOTYPE返回真;如果是一个差错类型,则返回假。
数据报作为链路层广播或者多播到达(由M_BCASH和M_MCAST标志表明)。
该数据报是发给IP广播或者IP多播地址的。
数据报的源站地址不是单播地址(也即,源站地址是一个全零地址、环回地址、广播地址、多播地址或E类地址)。
这些限制的目的是为了避免有错的广播数据报触发网络上所有主机都发出ICMP差错报文。当网络上所有主机同时要发送差错报文时,产生的广播风暴会使整个网络的通信崩溃。
图27是ICMP差错报文的构造 ,图28的程序构造差错报文。
图27 ICMP差错报文的构造
图28 icmp_error函数:报文首部构造
76~106 icmp_error以下面的方式构造ICMP差错报文首部:
m_gethdr分配一个新的分组首部缓存。MH_ALIGN定位缓存的数据指针,使ICMP首部、IP首部(选项)和最多8字节的数据被放在缓存的最后。
icmp_type、icmp_code、icmp_gwaddr(用于重定向)、icmp_pptr(用于参数问题)和icmp_nextmtu(用于要求分片报文)被初始化。
一旦构造好ICMP首部,就必须把原始数据报的一部分附到首部上,如图29所示。
图29 icmp_error函数:包含原始数据报
107~125 无效数据取的IP首部、选项和数据(icmplen)字节被复制到ICMP差错报文中。同时,首部的长度被加回无效数据报的ip_len中,因为之前ip_len减去了首部长度。
因为MH_ALIGN把ICMP报文分配在缓存的最后,所以缓存的前面应该有足够的空间存放IP首部。无效数据报的IP首部(除选项外)被复制到ICMP报文的前面。
然后再恢复正确的数据报长度(ip_len)、首部长度(ip_hl)和协议(ip_p)后,IP首部就完整了。TOS字段(ip_tos)被清除。
126~129 完整的报文被传给icmp_reflect,由icmp_reflect把它发回源主机。丢弃无效数据报。
icmp_reflect函数
icmp_reflect把ICMP回答或差错报文发回给请求或者无效数据报的源站。注意,icmp_reflect在发送数据报之前,把它的源站地址和目的地址倒过来。与ICMP报文的源站和目的站地址有关的规则非常复杂,图30对其中几个函数的作用作了小结。
图30 ICMP丢弃和地址小结
我们分三个部分讨论icmp_reflect函数:源站和目的站地址选择、选项构造及组装和发送。图31显示了该函数的第一部分。
图31 icmp_reflect函数:地址选择
a. 设置目的地址
329~345 icmp_reflect一开始,就复制了ip_dst,并把请求或者差错报文的源站地址ip_src移到ip_dst。icmp_error和icmp_reflect保证:ip_src对差错报文而言是有效的目的地址。ip_output丢掉所在发往广播地址的分组。
b. 选择源站地址
346~371 icmp_reflect在in_ifaddr中找到具有单播或者广播地址的接口,该接口地址与原始数据报的目的地址匹配,这样icmp_reflect就为报文选好源地址。在多接口主机上,匹配的接口可能不是接收该数据报的接口。如果没有匹配,就选择正在接收的接口的in_ifaddr结构,或者in_ifaddr中的第一个地址。该函数把ip_src设成所选的地址,并把ip_ttl改为255,因为这是一个新的数据报。
程序的下一部分(图32)为ICMP报文构造选项。
图32 icmp_reflect函数:选项构造
c. 取得逆转后的源路由
372~385 如果到达的数据报没有选项,控制被传给430行(图33)。icmp_error传给icmp_reflect的差错报文从来没有IP选项,所以后面的程序只用于那些被转换成回答并直接传给icmp_reflect的ICMP请求。
cp指向回答的选项的开始。ip_srcroute逆转并返回所有在ipintre处理数据报时保存下来的源路由选项。如果ip_srcroute返回0,即请求中没有源路路由选项,icmp_reflect分配并初始化一个mbuf,作为空的ipoption结构。
d. 加上记录路由和时间戳选项
386~416 如果opts指向某个缓存,for循环搜索原始IP首部选项,在ip_srcroute返回的源路由后面加上记录路由和时间戳选项。
在ICMP报文发送之前必须移走原始首部里的选项。这由图33中的程序完成。
图33 icmp_reflect函数:最后的组装
e. 移走原始选项
417~429 icmp_reflect把ICMP报文移到IP首部的后面,这样就从原始请求中移走了选项。如图34所示,新选项在opt所指向的mbuf里,被ip_output再次插入。
图34 icmp_reflect:移走选项
f. 发送报文和清除
430~435 在报文和选项被传给icmp_send之前,要明确地清除广播和多播标志位。然后释放掉存放选项的缓存。
icmp_send函数
icmp_send(图35)处理所有输出的ICMP报文,并在它们传给IP层之前计算ICMP检验和。
图35 icmp_send函数
440~457 与icmp_input检测ICMP检验和一样,Net/3调整缓存的数据指针和长度,隐藏IP首部,让in_cksum只看到ICMP报文。计算好的检验和放在首部的icmp_cksum,然后把数据报和所有选项传给ip_output。ICMP层并不维护路由调整缓存,所以icmp_send只会给ip_output一个空指针(第4个参数),而不是控制标志。特别是不传IP_ALLOWBROADCAST,所以ip_output丢弃所有具有广播目的地址的ICMP报文。
icmp_sysctl函数
IP的icmp_sysctl函数只支持图36列出的选项。系统管理员可以用sysctl程序修改该选项。
图36 icmp_sysctl参数
图37显示了icmp_sysctl函数。
图37 icmp_sysctl函数
468~478 如果缺少所要求的ICMP sysctl名,就返回ENOTDIR。
479~486 ICMP级以下没有选项,所以,如果不识别选项,该函数就调用sysctl_int修改icmpmaskrepl或者返回ENOPROTOOPT。
小结
ICMP协议是作为IP上面的运输层实现的,但它与IP层紧密结合一起。我们看到,内核直接响应ICMP请求报文,但把差错和回答传给合适的运输层或者应用程序处理。
更多最新文章尽在公众号:大白爱爬山,欢迎关注!