主要参考了《深入Linux内核架构》和《精通Linux内核网络》相关章节
本节只讨论ICMPv4协议
**近些年来,ICMP协议已逐渐成为开发监控和测量应用程序的基础。**不幸的是,ICMP协议也时常被用作安全攻击的基础,例如,DoS或者远程指纹收集(remote fingerprintcollection)。因此,网络管理员时常把路由器和防火墙配置成过滤掉多数ICMP消息类型。偶尔,网络管理员过滤掉太多了,因而违反了RFC建议案。无论消息是否被过滤掉,消息通常有速率限制。因此,建立在ICMP之上的任何应用程序就测量和监控目的而言,不见得都可靠。然而,因为测量本来就不是原本设计目标,ICMP通常无法让监控应用程序收集到其所需的所有信息。相反的,为了收集完整信息,另外又开发了专用应用程序,而其基础通常是TCP或UDP。
ICMP(第四层协议)主要用作发送有关网络层(L3)错误和控制消息的机制,让你能够通过发送ICMP消息来获取有关通信环境中问题的反馈。
ICMP相对比较简单,但对于确保系统正确的行为而言至关重要。
ICMPv4消息分两类:错误消息和信息消息(RFC 1812中称为“查询消息”)。ICMPv4被用于ping和traceroute等诊断工具。
ICMP消息可以由内核及用户空间应用程序传输。用户空间应用程序会使用raw IP套接字界面。有两个著名的网络除错工具是traceroute和ping,都是使用ICMP协议。其他使用raw IP套接字接口来进行传输或监听ICMP消息的是各种路由协议
著名工具ping实际上是一个用户空间应用程序(位于iputils包中)。它打开一个原始套接字并发送一条ICMP_ECHO消息,进而收到以ICMP_REPLY消息的方式返回的响应,显示出来回时间及其他信息。
**traceroute可用于寻找发出此命令的主机与特定目的地IP地址间的路由线路。**该路由线路就是一份途经路由器的IP地址列表。
traceroute可以使用UDP或ICMP来达到它的目标(注2)。默认情况下,traceroute使用UDP,但是,你可以用-工切换选项迫使它使用ICMP。后面就会知道,UDP法也得依赖ICMP消息才能成功。这两种做法背后都有相当多的窍门。
我们来说明这种基于ICMP的技术如何运作。当入口IP封包的IP报头的TTL字段为1而且需要做转发时,接收者会丢弃该封包而传送一条类型为ICMP_TIME_EXCEEDED而代码为工CMP_EXC_TTL的ICMP消息给来源地。**traceroute就是利用这条规则,一次找出一个居中跳点:传送ICMP_ECHO消息给目的地IP地址,而TTL字段的值会递增(从1起算),如此就可确保所有居中主机都会产生ICMP_TIME_EXCEEDED消息,而最后一个主机(也就是目的主机)则会以ICMP_ECHOREPLY消息回复。**图25-5是其范例。
图中没有包含ICMP回复消息的TTL字段值,因为不同操作系统会使用不同的值(64和255是最常见者)。
**基于UDP协议的技术也有点相似:依然是利用TTL字段的处理方式,只不过不是用ICMP_ECHO消息,而是使用UDP封包和很高的目的地端口号(终端主机不太可能使用)。**当IP封包来到终端主机时,终端主机会返回类型为ICMP_DEST_UNREACH而代码为工CMP_PORT__UNREACH的ICMP消息。图25-6是这样的范例。
就ICMP和UDP这两种情况而言,**居中主机是通过独立的“探测”封包逐一被发现出来的。**这样有两种结果值得一提:
ICMPv4的初始化是在引导阶段调用的方法inet_init()中完成的。方法inet_init()调用方法icmp_init(),后者再调用方法icmp_sk_init()。与其他IPv4协议一样,ICMPv4的注册也是在inet_init()中完成的。
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
ICMPv4报头由类型(8位)、代码(8位)、校验和( 16位)和32位的可变部分(其内容取决于ICMPv4类型和代码)组成。ICMPv4报头的后面是有效载荷,其中包含原始数据包的IPv4报头和部分有效载荷。RFC1812指出,在确保ICMPv4数据报不超过576字节的前提下,有效载荷应尽可能多地包含原始数据报的内容。长度为576字节是根据RFC 791确定的。该RFC指出:所有主机都必须能够接收长达576字节的数据报。
struct icmphdr {
__u8 type;
/* 这一字段可以分辨出ICMP消息的类型。有些时候,类型就足以明确分辨消息,而其他时候则需要代码来区分同一种消息类型的不同变种。 */
__u8 code;
__sum16 checksum;
union {
struct {
__be16 id;
__be16 sequence;
} echo;
__be32 gateway;
struct {
__be16 __unused;
__be16 mtu;
} frag;
__u8 reserved[4];
} un;
};
**当内核在处理入口IP封包而检测到错误时,就会传送ICMP错误消息。所有ICMP错误消息在ICMP有效载荷中都包含相同信息:触发ICMP消息传输的IP封包的IP报头,外加一部分IP有效载荷。**所得IP封包必须不能超过576个字节,包括外面的IP报头以及ICMP报头〔后一条规则是在RFC1812的4.3.2.3节中陈述的(更新了RFC 792的报头的定义)。根据旧版RFC 792所述,ICMP有效载荷只需包括原有的IP报头,外加原有传输报头 (64位)]。
**图25-2是ICMP_FRAG_NEEDED错误消息的范例(根据RFC 792)。图25-2 (a)就是触发ICMP消息传输的片段,而图25-2 (b)是ICMP消息。**注意,ICMP有效载荷也包括原有IP报头及一段传输报头。Linux和 RFC 1812兼容,因此,包括图25-2 (a)中所示的额外的区块(尺寸至多576字节)。
原有IP报头的“协议”字段会由ICMP消息的目的主机使用,用于分辨出正确的传输协议(此例为TCP),此外,ICMP有效载荷中一部分的传输报头(包含源端口和目的端口号)可以让同一个目的主机用于分辨出本地套接字。因此,目的主机就可多少追查出其造成错误的原因。
就每种ICMP类型而言,都有一个icmp_control数据结构实例(定义在net/ipv4/icmp.c中)。其中有一个字段,指向一个函数的指针,而该函数会被调用处理入口ICMP消息。下面是它的字段:
ICMPv4模块定义了一个icmp_control对象数组——icmp_pointers。它将ICMPv4消息类型作为索引。下面来看看结构icmp_control的定义以及数组icmp_pointers。
net\ipv4\icmp.c
struct icmp_control {
bool (*handler)(struct sk_buff *skb);
short error; /* This ICMP is classed as an error message */
};
/*
* This table is the definition of how we handle ICMP.
*/
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] {
...
NR_ICMP_TYPES是最大的ICMPv4消息类型编号,其值为18( include/uapi/linux/icmp.h )。这个数组中的icmp_control对象都是错误消息,如“目的地不可达”消息( ICMP_DEST_UNREACH ),因为字段error为1;字段error为0时,表示信息消息,如回应((ICMP_ECHO)。有些处理程序被分派给多种消息类型。下面来讨论处理程序及其管理的ICMPv4消息类型。
ping_rcv
方法ping_ rcv()负责处理接收ping应答( ICMP_ ECHOREPLY )的工作。它是在ICMP套接字代码(netipv4/ping.c)中实现的。
在3.0之前的内核中,要发送ping,必须在用户空间创建一个原始套接字。有ping应答( ICMP_ ECHOREPLY消息)到来时,由发送ping的套接字进行处理。为帮助理解这是如何实现的,来看看ip_ local _deliver_ _finish()。 这个方法会处理到来的IPv4数据包,并将其交给相应的套接字。
**在Linux内核3.0中集成ICMP套接字(ping套接字)时,情况发生了变化。ping套接字将在3.3节讨论。这里需要指出的是,引入ICMP套接字后,==ping的发送方可以不是原始套接字。==例如,你可以这样创建一个套接字,socket(PF_INET,SOCK_DGRAN,PROT_ICMP),并使用它来发送ping数据包。**这个套接字不是原始套接字,因此回应应答不会被交给原始套接字,因为没有侦听它的原始套接字。为避免这种问题,ICMPv4模块使用回调函数ping_rcv()来处理ICMP_ECHOREPLY消息的接收工作。ping模块位于IPv4层( net/ipv4/ping.c ),但netlipv4/ping.c中的大多数代码都是双栈代码(适用于IPv4和IPv6),因此方法ping_rcv()也负责处理ICMPV6_ECHO_REPLY消息(请参见net/ipv6/icmp.c中的icmpv6_rcv() )。本章后面将更详细地讨论ICMP套接字。
icmp_discard
icmp_discard()是一个空处理程序,用于不存在的消息类型(编号在头文件中没有声明的消息类型)以及不需要做任何处理的消息,如ICMP_TIMESTAMPREPLY。ICMP_TIMESTAMP和ICMP_TIMESTAMPREPLY消息用于同步时间。发送方在ICMP_TIMESTAMP请求中发送始发( originate )时间戳,而接收方发送包含3个时间戳的ICMP_TIMESTAMPREPLY——时间戳请求的发送方发送的始发时间戳、接收时间戳和传输时间戳。有一些比ICMPv4时间戳消息更常用的时间同步协议,如网络时间协议(Network Time Protocol,NTP)。这里还需要说说地址掩码(AddressMask)请求(ICMP_ADDRESS )。它通常由主机发送给路由器,旨在获取合适的子网掩码。收到这种消息后,接收方应使用地址掩码应答消息进行应答。
ICMP_ADDRESS和 ICMP_ADDRESSREPLY消息以前由方法 icmp_address()和 icmp.address_reply()处理,而现在也会被icmp_discard()处理。原因是:可通过其他方式获得子网掩码,如DHCP。
icmp_unreach
消息类型ICMP_DEST_UNREACH、ICMP_TIME_EXCEED、ICMP_PARAME-TERPROB和ICMP_QUENCH由icmp_unreach()处理。
在很多情况下都会发送ICMP_DEST_UNREACH消息,3.1.4节将介绍其中一部分。在以下两种情况下会发送ICMP_TIME_EXCEEDED消息。
在ip_forward()中,数据包的TTL都会被减1。RFC 1700推荐将IPv4数据包的TTL设置为64。如果TTL变成了0,就表明应该将数据包丢弃,因为可能存在环路。因此,在ip_forward()中,如果发现TTL为0,将调用方法icmp_send()。
在以下两种情况下会发送ICMP_TIME_EXCEEDED消息。
在ip_forward()中,数据包的TTL都会被减1。RFC 1700推荐将IPv4数据包的TTL设置为64。如果TTL变成了0,就表明应该将数据包丢弃,因为可能存在环路。因此,在ip_forward()中,如果发现TTL为0,将调用方法icmp_send()。
ip_options
在方法ip_options_compile()或ip_options_rcv_srr() ( net/ipv4/ip_options.c )中,未能成功地分析IPv4报头选项时,将发送一条ICMP_PARAMETERPROB消息。选项是IPv4报头中可选的变长字段(最多40字节)。IP选项将在第4章讨论。
消息类型ICMP_QUENCH实际上已被摒弃。RFC 1812的4.3.3.3节(“Source Quench”)指出:路由器不会发送ICMP信源抑制消息,它还可能会忽略收到的ICMP信源抑制消息。ICMP_QUENCH消息旨在缓解拥塞,但事实证明这种解决方案不管用。
icmp_redirect
ICMP_REDIRECT消息由icmp_redirect()处理。RFC 1122的3.2.2.2节指出:主机不应发送ICMP重定向消息,重定向消息仅供网关发送。
以前,icmp_redirect()处理ICMP_REDIRECT消息时调用ip_rt_redirect(),但现在不需要这样做。因为协议处理程序能够妥善地将重定向消息传播给路由选择代码。事实上,在内核3.6中,方法ip_rt_redirect()已被删除。因此,方法icmp_redirect()首先执行完整性检查,再调用icmp_socket_deliver()。后者会将数据包交给原始套接字并调用协议错误处理程序(如果有的话)。
ICMP_REDIRECT消息将在第6章进行更详细的讨论。
icmp_echo
icmp_echo()处理回应(ping)请求(ICMP_ECHO )。它调用icmp_reply()发送回应应答(ICMP_ECHOREPLY )。如果设置了net->ipv4.sysctl_icmp_echo_ignore_all,将不会发送应答。关于如何配置ICMPv4 procfs条目,请参阅3.5节以及Documentation/networking/ip-sysctl.txt。
icmp_timestamp
icmp_timestamp()处理ICMP时间戳请求(ICMP_TIMESTAMP )。它调用icmp_reply()来发送ICMP_IMESTAMPREPLY。
方法ip_local_deliver_finish()处理目的地为当前机器的数据包。收到ICMP数据包后,此方法便将其交给注册ICMPv4协议的原始套接字。在方法icmp_rcv()中进行处理。
方法ip_local_deliver_finish()处理目的地为当前机器的数据包。收到ICMP数据包后,这个方法便将其交给注册了ICMPv4协议的原始套接字。
在方法icmp_rcv()中,首先将InMsgs SNMP计数器(ICMP_MIB_INMSGS )加1,再核实校验和是否正确。
如果校验和不正确,就将SNMP计数器InCsumErrors和InErrors (ICMP_MIB_CSUMERRORS和ICMP_MIB_INERRORS)都加1,再释放SKB,并返回0。在这种情况下,方法icmp_rcv()不会返回错误。实际上,方法icmp_rcv()总是返回0。为何在校验和不对时返回0呢?因为收到错误的ICMP消息时,除将其丢弃外无需做其他特殊处理。协议处理程序返回负的错误代码时,将再次尝试对数据包进行处理,但在这里不需要这样做。更详细的信息请参阅方法ip_local_deliver_finish()的实现过程。
接下来,检查ICMP报头,以确定ICMP消息的类型,将相应的procfs消息类型计数器(每种ICMP消息类型都有一个procfs计数器)加1,并执行完整性检查,以确认类型编号没有超过最大允许值(NR_ICMP_TYPES )。RFC 1122的第3.2.2节指出,如果收到的ICMP消息的类型未知,必须默默地丢弃它。因此,如果消息类型编号超出了范围,将把InErrors SNMP计数器(ICMP_MIB_INERRORS)加1,并释放SKB。
如果数据包为广播或组播方式,且为ICMP_ECHO或ICMP_TIMESTAMP消息,
接下来,校验广播或组播方式所对应的消息类型,即是否为ICMP_ECHO、ICMP_TIMESTAMP、ICMP_ADDRESS或ICMP_ADDRESSREPLY消息。如果不是上述消息类型,将丢弃数据包并返回0。接下来,根据消息类型从数组icmp_pointers中取回相应的条目,并调用合适的处理程序。
用于发送ICMPv4消息的方法有两个:一是方法icmp_reply(),用于发送两种ICMP请求的响应;二是方法icmp_send(),用于发送当前机器在特定条件下主动发送的ICMPv4消息。
用于发送ICMPv4消息的方法有两个:
在方法icmp_echo()和icmp_timestamp()中,分别调用方法icmp_reply()来响应ICMP_ECHO和ICMP_TIMESTAMP消息。
而在IPv4网络栈的很多地方,如Netfilter、转发代码(ip_forward.c )、ipip和ip_gre等隧道中,都调用了方法icmp_send()。
代码2:ICMP_PROT_UNREACH(协议不可达)
IP报头的协议字段(长8位)指定的协议不存在时,将向发送方发送一条ICMP_DEST_UNREACH/ICMP_PROT_UNREACH消息,**因为没有针对指定协议的协议处理程序(协议处理程序数组将协议号用作索引,因此对于不存在的协议,没有相应的处理程序)。**所谓不存在的协议,指的是下面两种情形之一:Pv4报头中的协议号是错误的,没有包含在协议号列表中(该列表可在include/uapi/linux/in.h中找到);内核不支持该协议,因此该协议没有注册,协议处理程序数组中没有相应的条目。由于这样的数据包无法处理,因此需要向发送方发回ICMPv4“目的地不可达”消息。这种应答中的代码ICMP_PROT_UNREACH指出了导致错误的原因——“协议不可达”。
代码3:ICMP_PORT_UNREACH(端口不可达)
接收UDPv4数据包时,将查找匹配的UDP套接字。如果没有找到匹配的套接字,将检查校验和是否正确。如果不正确,就将数据包默默地丢弃;如果正确,就更新统计信息,并返回一条ICMP“目的地不可达”/“端口不可达”消息。
代码4:ICMP_FRAG_NEEDED(需要分片)
转发数据包时,如果其长度超过了外出链路的MTU,且在IPv4报头(IP_DF)中没有设置分段( DF )位,将把数据包丢弃,并向发送方发回一条代码为ICMP_FRAG_NEEDED的ICMP_DEST_UNREACH消息。
代码5:ICMP_SR_FAILED(目的不可达)
转发数据包时,如果其严格路由选择( strict routing)和网关(gatewaying)选项被设置,将把数据包丢弃,并发回一条代码为ICMP_SR_FAILED的“目的地不可达”消息。
方法icmp_reply()和icmp_send()都支持速率限制,它们调用icmpv4_xrlim_allow()如果速率限制检查允许发送当前数据包(icmpv4_xrlim_allow()返回true),它们就发送该数据包。
在如下情况不会进行速率限制检查:消息的类型未知、数据包为PMTU发现数据包、设备为环回设备、ICMP类型在速率掩码中未指定。
#define ICMP_ECHOREPLY 0 /* Echo Reply */
#define ICMP_DEST_UNREACH 3 /* Destination Unreachable */
#define ICMP_SOURCE_QUENCH 4 /* Source Quench */
#define ICMP_REDIRECT 5 /* Redirect (change route) */
#define ICMP_ECHO 8 /* Echo Request */
#define ICMP_TIME_EXCEEDED 11 /* Time Exceeded */
#define ICMP_PARAMETERPROB 12 /* Parameter Problem */
#define ICMP_TIMESTAMP 13 /* Timestamp Request */
#define ICMP_TIMESTAMPREPLY 14 /* Timestamp Reply */
#define ICMP_INFO_REQUEST 15 /* Information Request */
#define ICMP_INFO_REPLY 16 /* Information Reply */
#define ICMP_ADDRESS 17 /* Address Mask Request */
#define ICMP_ADDRESSREPLY 18 /* Address Mask Reply */
#define NR_ICMP_TYPES 18
内核提供了一种在用户空间中对各种子系统的设置进行配置的方式。方法是:将值写入/proc下的条目中。这些条目被称为procfs条目。所有ICMPv4 procfs条目都由结构netns_ipv4中的变量表示。这个结构是在include/net/netns/ipv4.h中定义的。它是网络命名空间(结构net )中的一个对象。网络命名空间及其实现将在第14章讨论。下面列出了与ICMPv4 netns_ipv4元素对应的sysctl变量的名称,还指出了它们的用途、默认值以及对其进行初始化的方法。
sysctl_icmp_echo_ignore_all
设置了icmp_echo_ignore_all时,将不会对回应请求(ICMP_ECHO)做出应答。
对应的procfs条目为/proc/sys/net/ipv4/icmp_echo_ignore_all,在icmp_sk_init()中被初始化为0。
sysctl_icmp_echo_ignore_broadcasts
收到组播/广播回应(ICMP_ECHO)消息或时间戳(ICMP_TIMESTAMP)消息时,读取sysctl_icmp_echo_ignore_broadcasts,以核实是否允许广播/组播。如果这个变量被设置,将丢弃数据包并返回0。
对应的procfs条目为/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts,在icmp_sk_init()中被初始化为1。
sysctl_icmp_ignore_bogus_error_responses
有些路由器违反RFC1122的规定,在收到广播帧时发送伪造的响应。在方法icmp_unreach()中,会检查这个标志。如果它被设置为TRUE,内核就不会将警告(“发送的ICMP消息类型非法……”)写人日志。
对应的procfs条目为/proc/sys/net/ipv4/icmp_ignore_bogus_error_responses,在icmp_sk_init()中被初始化为1。
sysctl_icmp_ratelimit
对于类型与ICMP速率掩码(参见本节后面的icmp_ratemask )匹配的ICMP数据包,将其最大速率限制为指定值。如果该值为0,则表示禁用速率限制;否则表示响应间的最小间隔,单位为毫秒。
对应的procfs条目为/proc/sys/net/ipv4/icmp_ratelimit,在icmp_sk_init()中被初始化为1*HZ。5. sysctl_icmp_ratemask
指定要对哪些ICMP消息类型进行速率限制的掩码。每位对应于一种ICMPv4消息类型。对应的procfs条目为/proc/sys/net/ipv4/icmp_ratemask ,在icmp_sk_init()中被初始化为Ox1818。
sysctl_icmp_errors_use_inbound_ifaddr
在方法icmp_send()中,将检查这个变量的值。如果它没有被设置,发送ICMP错误消息时将使用出站接口的主地址;否则,发送ICMP消息时将使用导致ICMP错误的数据包的入站接口的主地址。
对应的procfs条目为/proc/sys/net/ipv4/icmp_errors_use_inbound_ifaddr,在icmp_sk_init()中被初始化为0。
注意有关ICMP sysctl变量及其类型和默认值的更详细信息,请参阅Documentation/networking/ip-sysctl.txt。