在 Netfilter 是如何工作的(五) 中连接跟踪信息使用的创建-确认机制的
Netfilter
在报文进入系统的入口处,将连接跟踪信息记录在报文上,在出口进行confirm
.确认后的连接信息
本文以一个本机上送过程中的TCP/IPv4
的SYN
握手报文为例,详细分析连接跟踪机制的工作流程。
入口 & 出口
由于是本机上送流程,因此SYN
报文的入口是 PRE_ROUTING,而出口则是 LOCAL_IN。Netfilter 在初始化时,会在这两个 HOOK 点注册连接跟踪相关的处理函数。
static struct nf_hook_ops ipv4_conntrack_ops[] = {
{
.hook = ipv4_conntrack_in, // 入口回调函数
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_PRE_ROUTING, // PRE_ROUTING HOOK 点
.priority = NF_IP_PRI_CONNTRACK, // 优先级
},
......
{
.hook = ipv4_confirm, // 出口的回调函数
.pf = NFPROTO_IPV4,
.hooknum = NF_INET_LOCAL_IN, // LOCAL_IN HOOK 点
.priority = NF_IP_PRI_CONNTRACK_CONFIRM, // 优先级
},
}
入口
SYN 报文进入连接跟踪的入口是 ipv4_conntrack_in()
,然后调用nf_conntrack_in
, 后面这个函数较长,因此我们这里分段来看
ipv4_conntrack_in
|
|-- nf_conntrack_in
nf_conntrack_in(struct net *net, u_int8_t pf, unsigned int hooknum, struct sk_buff *skb)
{
// ......
if (skb->nfct) {
/* Previously seen (loopback or untracked)? Ignore. */
tmpl = (struct nf_conn *)skb->nfct;
if (!nf_ct_is_template(tmpl)) {
NF_CT_STAT_INC_ATOMIC(net, ignore);
return NF_ACCEPT;
}
skb->nfct = NULL;
}
// ......
}
首先是检查 skb 上是否已经关联了连接跟踪信息。这里不是入口吗?为什么 skb 上会由连接跟踪信息 ?! 注释写了,如果这个包是从 loopback 口收到的话,这时它就有可能已经关联的链接跟踪信息了,这时就会把已关联的连接跟踪信息作为模板(tmpl)。而在我们的情境中,SYN 报文从外部网卡收到,所以显然这里 skb 是不会有连接跟踪信息的。
接着往下看:
l3proto = __nf_ct_l3proto_find(pf);
ret = l3proto->get_l4proto(skb, skb_network_offset(skb),
&dataoff, &protonum);
l4proto = __nf_ct_l4proto_find(pf, protonum);
这一段代码片段完成了两件事
- 将协议族 pf 转换得到 L3 协议对应的函数操作集(operations),在我们的例子中,入参
pf = AF_INET
,也就是 IP 协议,因此这里会得到l3proto = nf_conntrack_l3proto_ipv4
- 进一步解析报文 L4 对应的协议号,这里会调用
ipv4_get_l4proto
,在我们的例子中,会得到 TCP 对应的协议号,protonum = 6
,进而得到l4proto = nf_conntrack_l4proto_tcp4
接下来就是入口函数的核心流程resolve_normal_ct()
, 正常情况下,这个函数会返回连接跟踪信息的指针ct
和简要信息ctinfo
,并将它们分别设置到skb->nfcf
和skb->ctinfo
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
int set_reply = 0;
ct = resolve_normal_ct(net, tmpl, skb, dataoff, pf, protonum,
l3proto, l4proto, &set_reply, &ctinfo);
下面将 resolve_normal_ct
展开
第一步调用nf_ct_get_tuple
, 将报文的五元组信息填充到struct nf_conntrack_tuple
结构的变量中
static inline struct nf_conn *
resolve_normal_ct(struct net *net, struct nf_conn *tmpl,
struct sk_buff *skb,
unsigned int dataoff,
u_int16_t l3num,
u_int8_t protonum,
struct nf_conntrack_l3proto *l3proto,
struct nf_conntrack_l4proto *l4proto,
int *set_reply,
enum ip_conntrack_info *ctinfo)
{
const struct nf_conntrack_zone *zone;
struct nf_conntrack_tuple tuple;
struct nf_conntrack_tuple_hash *h;
// @tuple 是出参, 对tcp来说是 tcp_pkt_to_tuple, 只是填了dport和sport
if (!nf_ct_get_tuple(skb, skb_network_offset(skb),
dataoff, l3num, protonum, net, &tuple, l3proto,
l4proto)) {
pr_debug("resolve_normal_ct: Can't get tuple\n");
return NULL;
}
// ......
}
第二步,计算tuple
的哈希值,然后根据此哈希值查找连接跟踪信息表中是否已存在这个哈希值的记录,将其值赋予h
......
zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
hash = hash_conntrack_raw(&tuple); // 计算hash
h = __nf_conntrack_find_get(net, zone, &tuple, hash); // 查找该tuple的元组信息是否已经存在
在我们的例子中,假设原本没有这样的记录,于是这里会使用init_conntrack
创建一条新的连接跟踪信息
if (!h) {
h = init_conntrack(net, tmpl, &tuple, l3proto, l4proto,
skb, dataoff, hash);
最后,将连接跟踪信息设置到报文skb
上。特别注意,这里只会将连接信息保存在skb
上,并不会将这条信息插入到连接跟踪信息表中,插入到表中这是出口 confirm 阶段的工作。
ct = nf_ct_tuplehash_to_ctrack(h);
......
} else {
......
} else {
pr_debug("nf_conntrack_in: new packet for %p\n", ct);
*ctinfo = IP_CT_NEW;
}
*set_reply = 0;
}
skb->nfct = &ct->ct_general;
skb->nfctinfo = *ctinfo;
出口
出口处,Netfilter 将连接跟踪信息设置到报文 skb 上,这样在出口的地方就可以将其取出,再插入到连接信息的哈希表中,完成确认
ipv4_confirm
|
|-- nf_conntrack_confirm
|
|-- __nf_conntrack_confirm
/* Confirm a connection given skb; places it in hash table */
int
__nf_conntrack_confirm(struct sk_buff *skb)
{
struct nf_conn *ct;
enum ip_conntrack_info ctinfo;
// ......
ct = nf_ct_get(skb, &ctinfo);
// ......
__nf_conntrack_hash_insert(ct, hash, reply_hash);
// ......
return NF_ACCEPT;
}