ip_rcv_finish 函数的末尾,如果目标地址并非本地接口地址,内核就需要将数据包转发到对应的主机。反之,如果目标地址是本地地址,内核则需要为上层协议处理该数据包做好准备。

20.1 转发

  • ip_forward

  • ip_forward_finish

得益于第 19 章中描述的 ip_rcv_finish 里对 ip_route_input 的调用,sk_buff 缓冲区已包含转发数据包所需的全部信息。

转发步骤如下:

  1. 处理 IP 选项。如果 IP 头部中的选项要求,可能需要记录本地 IP 地址和时间戳。

  2. 根据 IP 头部字段,确保数据包可以被转发。

  3. 对 IP 头部的生存时间(TTL)字段减 1,如果 TTL 变为 0,则丢弃该数据包。

  4. 根据与路由相关的 MTU,在需要时处理分片。

  5. 将数据包发送到出站设备。

如果数据包因某种原因无法被转发,必须通过 ICMP 消息通知源主机,并描述所遇到的问题。

即使数据包最终会被转发,也可能会发送一条 ICMP 消息作为警告,例如当数据包通过次优路由转发并触发重定向时。

在接下来的小节中,我们将分析 ip_forward 函数中的这些及其他操作。

20.1.1 ICMP 重定向

当主机系统(通常是路由器)收到的转发任务,更适合由另一台路由器来完成时,该主机就会发送 ICMP 重定向消息(更多细节见第 25 章和第 31 章)。

当数据包采用源路由时,路由器会认为发送方选择该路由有充分理由,不会自行质疑。它会遵从指定的路由,不会发送 ICMP 重定向消息。这个特殊情况会在「ip_forward 函数」一节中讲解。

20.1.2 ip_forward 函数

ip_forwardip_rcv_finish 调用(见第 18 章的图 18‑1),用于处理所有目的地址并非本地系统的入站数据包。该函数以与数据包关联的缓冲区 skb 作为输入参数,所有必需的信息都包含在这个结构体中。

路由信息 skb->dst 已由 ip_rcv_finish 内部调用 ip_route_input 完成初始化(更多细节见第 33 章)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
int ip_forward(struct sk_buff *skb)
{
u32 mtu;
struct iphdr *iph; /* Our header */
struct rtable *rt; /* Route we use */
struct ip_options *opt = &(IPCB(skb)->opt);
struct net *net;
SKB_DR(reason);

/* that should never happen */
// 确认我们正在处理的数据包,在二层(L2) 上确实是发往本机的
if (skb->pkt_type != PACKET_HOST)
goto drop;

if (unlikely(skb->sk))
goto drop;

if (skb_warn_if_lro(skb))
goto drop;

if (!xfrm4_policy_check(NULL, XFRM_POLICY_FWD, skb)) {
SKB_DR_SET(reason, XFRM_POLICY);
goto drop;
}

// IP 报头中有 Router Alert 选项
// 全局列表 ip_ra_chain,包含了本地套接字列表,都设定了 IP_ROUTER_ALERT 选项
if (IPCB(skb)->opt.router_alert && ip_call_ra_chain(skb))
return NET_RX_SUCCESS;

skb_forward_csum(skb);
net = dev_net(skb->dev);

/*
* 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 (ip_hdr(skb)->ttl <= 1)
goto too_many_hops;

if (!xfrm4_route_forward(skb)) {
SKB_DR_SET(reason, XFRM_POLICY);
goto drop;
}

// rt 指向类型为 rtable 的数据结构,该结构包含转发引擎所需的全部信息,包括下一跳(rt_gateway)。
rt = skb_rtable(skb);

if (opt->is_strictroute && rt->rt_uses_gateway)
goto sr_failed;

__IP_INC_STATS(net, IPSTATS_MIB_OUTFORWDATAGRAMS);

IPCB(skb)->flags |= IPSKB_FORWARDED;
mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
if (ip_exceeds_mtu(skb, mtu)) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(mtu));
SKB_DR_SET(reason, PKT_TOO_BIG);
goto drop;
}

/* We are about to mangle packet. Copy it! */
if (skb_cow(skb, LL_RESERVED_SPACE(rt->dst.dev)+rt->dst.header_len))
goto drop;
iph = ip_hdr(skb);

/* Decrease ttl after skb cow done */
// 递减 TTL, 更新 IP 校验和
ip_decrease_ttl(iph);

/*
* We now generate an ICMP HOST REDIRECT giving the route
* we calculated.
*/
if (IPCB(skb)->flags & IPSKB_DOREDIRECT && !opt->srr &&
!skb_sec_path(skb))
// ICMP REDIRECT
ip_rt_send_redirect(skb);

if (READ_ONCE(net->ipv4.sysctl_ip_fwd_update_priority))
skb->priority = rt_tos2priority(iph->tos);

return NF_HOOK(NFPROTO_IPV4, NF_INET_FORWARD,
net, NULL, skb, skb->dev, rt->dst.dev,
ip_forward_finish);

sr_failed:
/*
* Strict routing permits no gatewaying
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_SR_FAILED, 0);
goto drop;

too_many_hops:
/* Tell the sender its packet died... */
__IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
// 告知来源地,这里丢弃了数据包
icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
SKB_DR_SET(reason, IP_INHDR);
drop:
kfree_skb_reason(skb, reason);
return NET_RX_DROP;
}

ip_call_ra_chain

当接收到的 ingress IP 数据包是分片形式时,ip_call_ra_chain 会先对整个 IP 数据包进行重组,然后才将其递交给 ip_ra_chain 链表上的原始套接字。

管理该告警选项的函数可以在 net/ipv4/ip_sockglue.c 中找到。

20.1.3 ip_forward_finish 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int ip_forward_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct ip_options *opt = &(IPCB(skb)->opt);

#ifdef CONFIG_NET_SWITCHDEV
if (skb->offload_l3_fwd_mark) {
consume_skb(skb);
return 0;
}
#endif

if (unlikely(opt->optlen))
ip_forward_options(skb);

skb_clear_tstamp(skb);
return dst_output(net, sk, skb);
}

20.1.4 dst_output 函数

  • linux 2.5.75

  • 目的地址是单播:ip_output

  • 多播:ip_mc_output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* Output packet to network from transport.  */
static inline int dst_output(struct sk_buff *skb)
{
int err;

for (;;) {
err = skb->dst->output(skb);

if (likely(err == 0))
return err;
if (unlikely(err != NET_XMIT_BYPASS))
return err;
}
}

int ip_output(struct sk_buff *skb)
{
IP_INC_STATS(IpOutRequests);

if ((skb->len > dst_pmtu(skb->dst) || skb_shinfo(skb)->frag_list) &&
!skb_shinfo(skb)->tso_size)
return ip_fragment(skb, ip_finish_output);
else
return ip_finish_output(skb);
}

int ip_finish_output(struct sk_buff *skb)
{
struct net_device *dev = skb->dst->dev;

skb->dev = dev;
skb->protocol = htons(ETH_P_IP);

return NF_HOOK(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,
ip_finish_output2);
}

本地传递

第 35 章会讲解转发(路由)引擎如何判断本机是否为数据包的目的主机。

我们在第 19 章 “ip_rcv_finish 函数” 一节的末尾已经看到,在 ip_rcv_finish 开头调用的 ip_route_input,会在数据包到达目的主机时,把 skb->dst->input 初始化为 ip_local_deliver(与之相对,如果需要转发则会设为 ip_forward)。

此外,Netfilter 拥有最终决定权,判断通用的 do_something 函数(如 ip_local_deliver)是否可以调用对应的 do_something_finish 函数(在本例中是 ip_local_deliver_finish)来完成处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/

if (skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET)) {
// 重组
skb = ip_defrag(skb);
if (!skb)
return 0;
}

return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
}