这篇文章解释了ip报文在内核里面的实现,我们会根据报文穿过 ip协议协议层来介绍ip的基本属性。 为了确保我们的解释清晰易理解,我们假定这是一个普通的ip报文没有特殊属性。
所有关于ip的特殊属性, 例如 分片和整形, 源路由,组播,等等都会在下一个章节介绍。
一个IP 报文以三种方式进入ip协议栈
报文通过网络适配器存储在对应CPU的输入 队列里面, 一旦报文在数据链路层被确认为三层报文 协议类型
ETH_PROTO_IP , 报文被传输到ip_rcv() 函数。
ip报文的第二个入口位于传输协议的接口, 那些报文使用了TCP UDP 和其他一些使用的IP协议的报文。
它们使用 ip_queue_xmit() 接口去包装一个传输层PDU 到 IP报文然后去发送它,其他接口可以生成IP报文
在传输层的下层。
第三种方式,IP协议层生成ip报文。 主要是多播报文,巨型帧的分片产生的新报文,还有不包括特殊负载的
ICMP或者 IGMP报文,这种报文用的特殊方式创建 例如 icmp_send()
一旦一个报文进入IP协议层,有几种方式处理。我们通常区分计算机在Internet协议方面可以承担的两个不同角色,其中第一种情况是第二种情况的特殊情况:
终端系统: Linux计算机通常被配置为终端系统—它被用作工作站或服务器,
主要承担运行用户应用程序或提供应用程序服务的任务。
此外,Web服务器和网络打印机只不过是终端系统(关于IP层)。终端系统的基本特性是不转发IP数据包。
这意味着您可以通过终端系统只有一个网络适配器这一事实轻松地识别它。
即使一个系统有几个网络访问可以配置为一个主机,如果包转发是禁用的
路由器: 路由器将到达网络适配器的IP数据包传递给第二个网络适配器。
这意味着路由器有几个网络适配器,可以在这些接口之间转发数据包。
当信息包到达路由器时,通常有两种选择:它们可以在本地传递信息包(例如,它们可以在本地传递信息包)。
将它们发送到传输层)或转发它们。第一种情况与包到达终端系统的过程相同,其中包总是在本地交付。
因此,路由器可以被认为是终端系统的泛化,具有额外的功能
Linux允许您在运行时启用和禁用包转发机制,前提是在创建内核时集成了对转发的支持。
目录/proc/sys/net/ipv4/包含一个虚拟文件ip_forward。
有一种方法可以从proc目录中更改系统设置。如果将0写入此文件,则禁用包转发。
要激活IP包转发,可以使用命令echo ‘1’ > /proc/sys/net/ipv4/ip_forward。
下图显示一个IP包在Linux中通过Internet协议实现的路径。
灰色椭圆表示被调用的函数,矩形表示netfilter钩子在Internet协议中的位置。
Linux中Internet协议实现的体系结构
下面介绍ip报文在内核里面实现的不同方式,我们首先介绍接收的报文然后做转发或者移送本机,下一章节
介绍报文怎么从传输层到ip层。
进入封包到第3层边界的路径。一旦NET_RX微线程从输入队列中删除了一个包,netif_rx_action()将选择适当的第3层协议。
接下来,选择Internet协议,并根据以太网协议字段(ETH_PROTO_IP)中的标识符或其他MAC传输协议的适当字段调用ip_rcv()函数。
ip_rcv() net/ipv4/ip_input.c
ip_rcv(skb、dev、pkt_type)为IP协议做一些工作。
首先,该函数拒绝未发送到本地计算机的数据包。
例如,混杂模式允许网络设备接受实际发送到另一台计算机的数据包。
在较低的层中,这样的包被包类型(skb->pkt_type PACKET_OTHERHOST)过滤。
随后,检查数据包的基本正确性准则:
报文长度是否大于 IP报文头部?
是不是 ipv4?
检查checksum是否正确?
报文长度是否不对?
如果实际的包大小与套接字缓冲区(skb->len)中维护的信息不匹配,则当前的包数据范围由skb_trim(skb, iph->total_len)调整。
(参见4.1节)。既然包是正确的,就调用netfilter钩子NF_IP_PRE_ROUTING。Netfilter允许您根据需要通过特定的函数扩展各种协议的过程。
Netfilter钩子总是驻留在某些协议的策略点上,例如用于防火墙、QoS和地址转换功能。这些例子将在后面的章节中讨论。
netfilter钩子由宏调用,处理netfilter扩展后的函数以函数指针的形式传递给这个宏。如果未配置netfilter,则该宏将确保直接跳转到此后续函数。
在图14-4中,我们可以看到这个过程继续使用ip_rcv_finish(skb)。
ip_rcv_finish() net/ipv4/ip_input.c
ip_rcv_finish(skb)中调用ip_route_input()函数来确定包的路由。
套接字缓冲区的skb->dst指针被设置为路由缓存中的一个条目,该条目不仅存储在IP层上的目标,而且如果存在,还存储到硬标头缓存中的一个条目(用于第2层帧包标头的缓存)。
如果ip_route_input()找不到路由,则丢弃数据包。
在下一步中,ip_rcv_finish()检查IP包头是否包含选项。如果是这种情况,则分析选项,并创建ip_options结构。所有选项集都以有效的形式存储在这个结构中。
第14.3节描述了如何处理IP选项。
最后,在ip_rcv_finish()中,IP协议的过程到达了发送给本地计算机的数据包和要转发的数据包之间的连接点。关于IP包的进一步路径的信息存储在路由条目skb->dst中。注意,这里使用了Linux内核中经常使用的一个技巧。
如果使用一个开关(变量值)来选择不同的函数,那么我们只需向每个函数插入一个指针。这为我们节省了关于程序应该如何继续的每个决定的if或switch指令。在这里使用的例子中,指针skb->dst->input()指向的函数应该被用来进一步处理一个包:
ip_local_deliver()是在应该发送到本地计算机的单播和多播包的情况下输入的。
ip_forward()处理应该转发的所有单播包。
ip_mr_input()用于应该转发的多播包。
从上面的讨论中我们可以看出,包可以采用不同的路径。下一节描述如何处理要转发的包(skb->dst->输入= ip_forward)。随后,我们将看到skb->dst->input = ip_local_deliver如何处理要在本地传递的包
如果一台计算机有几个网络适配器,并且启用了包IP转发(/proc/sys/net/ipv4/ip_forward 1),那么发送给其他计算机的包由ip_forward()函数处理。这个函数完成转发数据包所需的所有工作。
最重要的任务路由已经在ip_input()中完成,因为必须能够发现包是在本地交付还是必须被转发
ip_forward() net/ipv4/ip_forward.c
ip_forward(skb)的主要任务是处理Internet协议的一些条件(例如包的生存期)和包选项。首先,删除未使用pkt_type == PACKET_HOST标记的包。接下来,检查包的覆盖范围。如果它的TTL字段的值是1(在它递减之前),那么这个包就被删除了。
RFC 791规定,如果发生这样的操作,必须将ICMP包返回给发送方,以通知发送方(ICMP_TIME_EXCEEDED).
一旦检查了重定向消息(如果适用),就会检查套接字缓冲区,以查看是否有足够的内存供headroom使用。这意味着函数skb_cow(skb, headroom)用于检查在输出网络设备(out_dev->hard_header_len)中是否仍然有足够的空间放置MAC头文件。
如果不是这样,那么skb_realloc_headroom()将创建足够的空间。随后,IP包的TTL字段减少1。
当实际的包长度(包括MAC报头)已知时,将检查它是否真的适合新的输出网络设备的帧格式。如果太长(skb->len > mtu),并且由于在IP报头中设置了Don’t-Fragment位,因此不允许分段,则数据包被丢弃,ICMP消息ICMP_FRAG_NEEDED被发送给发送方。无论如何,数据包还没有被分割;破碎是延迟。
对这种情况的早期测试只会阻止潜在的候选Don’t-Fragment运行整个IP协议处理过程
ip_forward_finish( ) net/ipv4/ip_forward.c
在图14-4中,我们可以看到ip_forward()函数被一个netfilter钩子分成了两部分。一旦NF_IP_FORWARD钩子被处理,这个过程将继续使用ip_forward_finish()。
这个函数实际上只有很少的功能(除非启用了FASTROUTE)。一旦在ip_forward_options()中处理了IP选项(如果使用了这些选项),就会调用ip_send()函数来检查包是否必须分段,并最终执行分段(如果适用的话)。(见部分14.2.3。)
ip_send() include/net/ip.h
ip_send(skb)决定包是否应该立即传递给ip_finish_output(),还是ip_fragment()首先将其调整为合适的第2层帧大小。(见部分14.2.3。)
ip_finish_output() net/ipv4/ip_output.c
ip_finish_output(skb)启动Internet协议的最后一个任务。首先,将skb->dev指针设置为输出网络设备dev,将第2层数据包类型设置为ETH_P_IP。随后,处理netfilter钩子NF_IP_POST_ROUTING。netfilter的确切操作和Internet协议中不同的连接点集在第19.3节中进行了描述。
netfilter钩子通常在调用后继续使用内联函数ip_finish_output2()。
ip_finish_output2() net/ipv4/ip_output.c
此时,数据包离开Internet协议,如果需要,使用地址解析协议(ARP)。第15章描述了地址解析协议。就目前而言,理解以下内容就足够了:
如果使用的路由条目(skb->dst)已经包含了对第2层报头缓存(dst->hh)的引用,那么第2层的包报头将直接复制到IP包报头前面的socket缓冲区的包数据空间中。这里使用的output()函数是dev_queue_xmit(),如果硬件标头缓存中的条目有效,就会调用它。dev_queue_xmit()确保socket缓冲区立即通过网络设备dev发送。
如果在硬标头缓存中还没有条目,则调用相应的地址解析例程,该例程通常是函数_resolve_output()。
上面描述的过程经过了优化,因此包可以快速通过路由器,而不需要特殊的选项。然而,在与相应的处理例程(例如,netfilter、多播、ICMP、分段或IP包选项)相连接的地方变得很明显。