这篇笔记先来看看IP分片重组功能涉及的一些数据结构以及核心函数,下一篇笔记则将这些过程串起来,看看整个分片重组过程是如何进行的。
数据结构
IP分片重组控制信息
在网络命名空间中,为IPv4分片重组定义了frags变量,用于保存分片重组功能需要的一些全局控制信息。
struct net {
...
struct netns_ipv4 ipv4;
}
struct netns_ipv4 {
...
struct netns_frags frags;
}
struct netns_frags {
// 当前待重组的IP报文个数,是哈希表中IP分片队列的个数,并非IP分片的个数
int nqueues;
// 当前保存的所有待重组分片占用的全部内存,不仅仅是分片本身的大小,还有为了管理而增加的额外开销,
// 该值不允许超过下面的上限值high_thresh,超过后,后续的IP片段将会由于没有内存而被丢弃
atomic_t mem;
// 所有IP片段组成一个LRU列表,便于在内存紧张时进行片段回收
struct list_head lru_list;
// 配置参数/proc/sys/net/ipv4/ipfrag_time,默认30s
int timeout;
// 配置参数/proc/sys/net/ipv4/ipfrag_high_thresh
int high_thresh;
// 配置参数/proc/sys/net/ipv4/ipfrag_low_thresh
int low_thresh;
};
IPv4分片哈希表
在代码实现上,内核将片段重组功能进行了抽象剥离,非协议相关的部分在net/ipv4/inet_fragment.c(不知道为什么还在ipv4目录下)中实现,IPv4相关的在net/ipv4/ip_fragment.c中实现。在相关函数的命令上也很好区分,以ip_xxx开头的是IPv4相关的,以inet_xxx相关的,是非协议相关的。
在实现重组功能时,IP层显然需要先缓存所有收到的IP片段,等同一个IP报文的所有片段都到达后把它们重组到一起再递交给L4协议。所以,IPv4协议定义了哈希表用于保存当前已收到的所有分片。
#define INETFRAGS_HASHSZ 64
struct inet_frags {
// 所有待重组的分段组织在该哈希表中
struct hlist_head hash[INETFRAGS_HASHSZ];
// 保护哈希表的读写锁
rwlock_t lock;
// 参与哈希值计算的一个随机数
u32 rnd;
// 哈希表中保存的元素的大小,用于队列元素的创建过程。对于IPv4,是sizeof(struct ipq)
int qsize;
// 配置参数/proc/sys/net/ipv4/ipfrag_secret_interval,内核初始化为10分钟
int secret_interval;
struct timer_list secret_timer;
// 一组回调函数,在分片重组过程中被回调
unsigned int (*hashfn)(struct inet_frag_queue *);
// 当分配一个新的IP分片队列时回调,用于初始化该新建的IP分片队列
void (*constructor)(struct inet_frag_queue *q, void *arg);
void (*destructor)(struct inet_frag_queue *);
void (*skb_free)(struct sk_buff *);
// 用于判断arg指定的IP分片是否属于一个IP分片队列q(见ip_find())
int (*match)(struct inet_frag_queue *q, void *arg);
// 分片队列超时清理回调函数
void (*frag_expire)(unsigned long data);
};
// IPv4协议用于分片重组的哈希表信息
static struct inet_frags ip4_frags;
IP分片队列: struct ipq
IPv4在哈希表中保存的结构实际上是struct ipq,该结构保存了属于同一个IP报文的所有IP片段。
struct inet_frag_queue {
// 将IP分片队列接入全局的哈希表ip4_frag.hash中
struct hlist_node list;
// 指向网络命名空间中的net->ipv4.frags
struct netns_frags *net;
// 将IP分片以LRU链表组织(inet.ipv4.frags.lru_list),方便内存紧张时的回收处理
struct list_head lru_list; /* lru list member */
spinlock_t lock;
// 引用计数,每个IP片段都会持有一个该队列的引用计数
atomic_t refcnt;
// 一个IP报文,如果在指定时间内(/proc/sys/net/ipv4/ipfrag_time)内不能完成重组,则所有片段都会丢弃
struct timer_list timer; /* when will this queue expire? */
// IP分片列表,已经按照offset顺序排好了
struct sk_buff *fragments; /* list of received fragments */
// 上一次收到IP分片的时间戳
ktime_t stamp;
// 当前收到的该IP报文的最大偏移量,随着片段的接收,该值会不断更新,实际一个IP报文
// 有多少字节只能在收到最后一个片段后才能知道
int len; /* total length of orig datagram */
// 当前已经收到的IP分片的数据量总和
int meat;
// 可取下面三个标记,分别表示第一个分片、最后一个分片、以及是否全部分片接收完成
__u8 last_in; /* first/last segment arrived? */
#define COMPLETE 4
#define FIRST_IN 2
#define LAST_IN 1
};
/* Describe an entry in the "incomplete datagrams" queue. */
struct ipq {
// 通用的IP分片队列
struct inet_frag_queue q;
// user是用来标识分片重组是由谁发起的,因为正常理解来看,重组应该由IP层在数据传递过程中发起,
// 但是一些防火墙功能,必须要处理完整的IP报文,所以也存在提前进行组装的可能
u32 user;
// 来自IP报头的几个字段
__be32 saddr;
__be32 daddr;
__be16 id;
u8 protocol;
// 记录IP分片的输入网卡索引
int iif;
unsigned int rid;
// 和IP层的IP地址管理有关,先忽略
struct inet_peer *peer;
};
关于user,内核定义了如下可能的值,我们这里只关注IP_DEFRAG_LOCAL_DELIVER这一种最常规的场景:
enum ip_defrag_users
{
IP_DEFRAG_LOCAL_DELIVER,
IP_DEFRAG_CALL_RA_CHAIN,
IP_DEFRAG_CONNTRACK_IN,
IP_DEFRAG_CONNTRACK_OUT,
IP_DEFRAG_VS_IN,
IP_DEFRAG_VS_OUT,
IP_DEFRAG_VS_FWD
};
上面这些数据结构之间的组织关键件下图:
初始化
在IPv4初始化过程中,会调用ipfrag_init()对IP分片子模块进行初始化,不过这部分初始化只和IP片段的重组有关系,发送过程中的IP分段并不涉及。
ip_frag_init()
void __init ipfrag_init(void)
{
// 初始化网络命名空间中的IPv4分片重组控制信息
register_pernet_subsys(&ip4_frags_ops);
// 初始化全局哈希表ip4_frags
ip4_frags.hashfn = ip4_hashfn;
ip4_frags.constructor = ip4_frag_init;
ip4_frags.destructor = ip4_frag_free;
ip4_frags.skb_free = NULL;
ip4_frags.qsize = sizeof(struct ipq);
ip4_frags.match = ip4_frag_match;
ip4_frags.frag_expire = ip_expire;
// 超时时间为10分钟
ip4_frags.secret_interval = 10 * 60 * HZ;
inet_frags_init(&ip4_frags);
}
void inet_frags_init(struct inet_frags *f)
{
int i;
for (i = 0; i < INETFRAGS_HASHSZ; i++)
INIT_HLIST_HEAD(&f->hash[i]);
rwlock_init(&f->lock);
// 生成随机数,并直接启动定时器,定时间隔由secret_interval指定,定时器函数是inet_frag_secret_rebuild()
f->rnd = (u32) ((num_physpages ^ (num_physpages>>7)) ^ (jiffies ^ (jiffies >> 6)));
setup_timer(&f->secret_timer, inet_frag_secret_rebuild, (unsigned long)f);
f->secret_timer.expires = jiffies + f->secret_interval;
add_timer(&f->secret_timer);
}
网络命名空间相关初始化
void inet_frags_init_net(struct netns_frags *nf)
{
nf->nqueues = 0;
atomic_set(&nf->mem, 0);
INIT_LIST_HEAD(&nf->lru_list);
}
static int ipv4_frags_init_net(struct net *net)
{
/*
* Fragment cache limits. We will commit 256K at one time. Should we
* cross that limit we will prune down to 192K. This should cope with
* even the most extreme cases without allowing an attacker to
* measurably harm machine performance.
*/
net->ipv4.frags.high_thresh = 256 * 1024;
net->ipv4.frags.low_thresh = 192 * 1024;
/*
* Important NOTE! Fragment queue must be destroyed before MSL expires.
* RFC791 is wrong proposing to prolongate timer each fragment arrival
* by TTL.
*/
net->ipv4.frags.timeout = IP_FRAG_TIME;
// 初始化frags中的其它几个字段
inet_frags_init_net(&net->ipv4.frags);
// 在/proc/sys/net/ipv4/目录下创建控制参数文件
return ip4_frags_ctl_register(net);
}
static struct pernet_operations ip4_frags_ops = {
.init = ipv4_frags_init_net,
.exit = ipv4_frags_exit_net,
};
IPv4关键函数实现
如上,全局的ip4_frags中有一组回调函数,这组回调函数影响了IP层分片重组功能的实现。
ip4_frag_match()
该函数用于判断一个IP片段是否属于一个IP分片队列,调用场景见ip_find()。
static int ip4_frag_match(struct inet_frag_queue *q, void *a)
{
struct ipq *qp;
struct ip4_create_arg *arg = a;
// 对于IPv4,实际的IP分片队列结构是ipq
qp = container_of(q, struct ipq, q);
// 很好理解
return (qp->id == arg->iph->id &&
qp->saddr == arg->iph->saddr &&
qp->daddr == arg->iph->daddr &&
qp->protocol == arg->iph->protocol &&
qp->user == arg->user);
}
ip4_frag_init()
每当新建一个IP分片队列后,则回调该函数,见inet_frag_alloc()。
static void ip4_frag_init(struct inet_frag_queue *q, void *a)
{
struct ipq *qp = container_of(q, struct ipq, q);
struct ip4_create_arg *arg = a;
qp->protocol = arg->iph->protocol;
qp->id = arg->iph->id;
qp->saddr = arg->iph->saddr;
qp->daddr = arg->iph->daddr;
qp->user = arg->user;
qp->peer = sysctl_ipfrag_max_dist ? inet_getpeer(arg->iph->saddr, 1) : NULL;
}