主要 IPv4 数据结构

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
// IP 报头
struct iphdr

// 必须被传输或转发的封包的选项
struct ip_options

// 传输封包所需的各种信息
struct ipcm_cookie

// IP 封包的片段集
struct ipq

// 内核会为最近连接过的每个远程主机保留一个实例
// 所有 inet_peer 结构的实例都会放在一颗 AVL 树中
struct inet_peer

// SNMP(简单网络管理协议) 采用名为 MIB(管理信息库) 的对象来收集系统的相关统计数据
// ipstats_mib 会保存关于 IP 层的统计资料
struct ipstats_mib

// 存储一个网络设备所有与 IPv4 相关的配置内容,如用户以 ifconfigip 命令所做的变更
struct in_device

// 当在接口上配置 IPv4 地址时,内核会建立一个 in_ifaddr 结构
// 其中包含 4 字节长的地址以及其他几个字段
struct in_ifaddr

// 调整网络设备的行为
// /proc/sys/net/ipv4/conf/
// 每个设备都有一个实例,另外还有一个是存储默认值 (ipv4_devconf_dflt)
struct ipv4_devconf

// 存储主机的配置
struct ipv4_config

// 处理套接字 CORK 选项
struct cork

in_device

  • 存储一个网络设备所有与 IPv4 相关的配置内容,如用户以 ifconfig 或 ip 命令所做的变更

  • 通过 net_device->ip_ptr 连接到 net_device 结构

  • 可使用 in_dev_get 和 __in_dev_get 取出

  • in_dev_get 成功后会对 in_dev 结构增加引用计数,其调用者应使用 in_dev_put 递减引用计数

  • 此结构的分配以及链接到设备由 inetdev_init 来做

sk_buff 和 net_device 结构里与 校验和 相关的字段

net_device 结构

net_device->features 字段会表面设备的能力。

1
2
3
4
5
6
7
8
// 此设备可以在硬件中计算 L4 校验和,只针对使用 IPv4 的 TCP 和 UDP
#define NETIF_F_IP_CSUM 2 /* Can checksum only TCP/UDP over IPv4. */

// 此设备很可靠,不需要使用任何 L4 校验和
#define NETIF_F_NO_CSUM 4 /* Does not require checksum. F.e. loopack. */

// 此设备可以为任何协议在硬件中计算 L4 校验和
#define NETIF_F_HW_CSUM 8 /* Can checksum all the packets. */

sk_buff 结构

skb 指向已接收封包,还是已传输出的封包,skb->csumskb->ip_summed 有不同的意义

已接收封包:

  • skb->csum 可能包含其 L4 校验和
  • skb->ip_summed 记录 L4 校验和 的状态
1
2
3
4
5
6
// csum 中的 校验和 无效
#define CHECKSUM_NONE 0
#define CHECKSUM_PARTIAL 1
// NIC 已经计算并验证了 L4 报头以及伪报头的 校验和
#define CHECKSUM_UNNECESSARY 2
#define CHECKSUM_COMPLETE 3

ip_summed 在传输期间的意义:

1
2
3
4
5
// 协议已处理了校验和,设备不需做任何事情
#define CHECKSUM_NONE 0
#define CHECKSUM_PARTIAL 1
#define CHECKSUM_UNNECESSARY 2
#define CHECKSUM_COMPLETE 3

封包的一般性处理

协议初始化

IPv4 协议是由 ip_init 初始化的。

由 ip_init 完成的主要任务:

  • dev_add_pack 函数为 IP 封包注册 ip_rcv 处理函数

  • 初始化路由子系统,包括与协议无关的缓存

  • 初始化用于管理 IP 断点的基础架构

开机期间,ip_init 会由 inet_init 调用,inet_init 会处理所有与 IPv4 有关的子系统的初始化,包括 L4 协议。

和 Netfilter 互动

防火墙本质上是挂载在网络协议栈代码中的特定位置,当数据包或内核满足特定条件时,数据包必然会经过这些位置;在这些节点上,防火墙允许网络管理员对流量的内容或处理方式进行操作。

内核中的这些挂载点包括:

  • 封包接收

  • 封包转发(路由决策前)

  • 封包转发(路由决策后)

  • 封包传输

负责其运算的函数分为两部分,do_somethingdo_something_finish(少数情况下,do_somethingdo_something2

  • do_something 只包含一些健康检查,也许还有一些处理工作,最后会调用 NF_HOOK

  • 真正执行具体工作的代码位于 do_something_finishdo_something2 中。

1
2
NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);

与路由子系统的交互

IP 层用于查询路由表的三个函数:

1
2
3
4
5
6
7
8
// 决定输入封包的命令,如本地传递、转发或丢弃
int ip_route_input(struct sk_buff*, __be32 dst, __be32 src, u8 tos, struct net_device *devin);

// 传输封包前使用,此函数会返回下个跳点网关以及要使用的出口设备
int ip_route_output_flow(struct rtable **rp, struct flowi *flp, struct sock *sk, int flags);

// 给定一个路由表缓存项目,就可返回相关的 PMTU
dst_pmtu

处理输入 IP 封包

ip_rcv 对封包做检查检查,然后调用 Netfilter 钩子。大多数处理将在 ip_rcv_finish 里发生。

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
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
struct iphdr *iph;
u32 len;

/* When the interface is in promisc. mode, drop all the crap
* that it receives, do not try to analyse it.
*/
if (skb->pkt_type == PACKET_OTHERHOST)
goto drop;

IP_INC_STATS_BH(IPSTATS_MIB_INRECEIVES);

// 检查封包的引用计数是否大于 1,大于 1 则意味着内核的其它部分拥有对缓冲区的引用。
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto out;
}

// 确保 skb->data 所指区域包含的数据区块至少和 IP 报头一样大
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
goto inhdr_error;

iph = skb->nh.iph;

/*
* RFC1122: 3.1.2.2 MUST silently discard any IP frame that fails the checksum.
*
* Is the datagram acceptable?
*
* 1. Length at least the size of an ip header
* 2. Version of 4
* 3. Checksums correctly. [Speed optimisation for later, skip loopback checksums]
* 4. Doesn't have a bogus length
*/

// 基本 IP 报头的尺寸是 20 字节
if (iph->ihl < 5 || iph->version != 4)
goto inhdr_error;

if (!pskb_may_pull(skb, iph->ihl*4))
goto inhdr_error;

iph = skb->nh.iph;

// 校验和
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
goto inhdr_error;

len = ntohs(iph->tot_len);
// 1. 确保已接收的封包长度大于或等于 IP 报头中记录的长度
// 2. 确保封包的尺寸至少和 IP 报头的尺寸一样大
if (skb->len < len || len < (iph->ihl*4))
goto inhdr_error;

/* Our transport medium may have padded the buffer out. Now we know it
* is IP we can trim to the true length of the frame.
* Note this now means skb->len holds ntohs(iph->tot_len).
*/
// 检查 L2 协议是否补满封包以达到特定最小长度
if (pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto drop;
}

/* Remove any debris in the socket control block */
memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));

return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);

inhdr_error:
IP_INC_STATS_BH(IPSTATS_MIB_INHDRERRORS);
drop:
kfree_skb(skb);
out:
return NET_RX_DROP;
}

netif_receive_skb 函数会把指向 L3 协议的指针 (skb->nh) 设在 L2 报文尾端,因此,IP 层函数可以安全地将它转换成 iphdr 结构。

图 19-1 所示是 ip_rcv 启动时一些 sk_buff 字段的值,此时 skb->data 指向 L3 报头。

ip_rcv_finish 函数

处理主要工作:

  • 决定封包是否必须本地传递或转发

  • 分析和处理一些 IP 选项

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
static inline int ip_rcv_finish(struct sk_buff *skb)
{
struct iphdr *iph = skb->nh.iph;

/*
* Initialise the virtual path cache for the packet. It describes
* how the packet travels inside Linux networking.
*/
if (skb->dst == NULL) {
int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
skb->dev);
if (unlikely(err)) {
if (err == -EHOSTUNREACH)
IP_INC_STATS_BH(IPSTATS_MIB_INADDRERRORS);
goto drop;
}
}

#ifdef CONFIG_NET_CLS_ROUTE
if (unlikely(skb->dst->tclassid)) {
struct ip_rt_acct *st = ip_rt_acct + 256*smp_processor_id();
u32 idx = skb->dst->tclassid;
st[idx&0xFF].o_packets++;
st[idx&0xFF].o_bytes+=skb->len;
st[(idx>>16)&0xFF].i_packets++;
st[(idx>>16)&0xFF].i_bytes+=skb->len;
}
#endif

if (iph->ihl > 5 && ip_rcv_options(skb))
goto drop;

return dst_input(skb);

drop:
kfree_skb(skb);
return NET_RX_DROP;
}

skb->dst->input 会设成 ip_local_deliverip_forward,这取决于封包的目的地址。

IP 选项

  • todo