IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> OVS 内核CT实现 -> 正文阅读

[网络协议]OVS 内核CT实现

引言

所有连接跟踪模块的作用,都是在首包到来时,识别并在CT表中生成表项;而当该连接的特定数据包到来时,在CT表内匹配表项,如果匹配命中,则执行响应预设动作。OVS 的连接跟踪实现也不例外。

OVS CT 定义

CT 匹配域

这里明确几个在OVS内的术语:

new 通过ct action指定报文经过conntrack模块处理,不一定有commit。
est 表示conntrack模块看到了报文双向数据流,一定是在commit 的conntrack后
rel 表示和已经存在的conntrack相关,比如icmp不可达消息或者ftp的数据流
rpl 表示反方向的报文
inv 无效的,表示conntrack模块没有正确识别到报文,比如L3/L4 protocol handler没有加载,或者L3/L4 protocol handler认为报文错误
trk 表示报文经过了conntrack模块处理,如果这个flag不设置,其他flag都不能被设置。任何进来的数据包,都是-trk状态,只有该数据包经过ct模块处理了,才会变为+trk状态。什么叫经过ct模块处理?流表的action指定了ct,并且报文通过了协议验证:pkt->md.ct_state = CS_TRACKED
snat 表示报文经过了snat,源ip或者port
dnat 表示报文经过了dnat,目的ip或者port

commit:ovs内的数据包都是处理完,生命期就结束;commit action用于明确定义在CT表中生成表项,用于未来匹配。

table: fork一份pipeline,报文copy一份送给connection tracker,然后从当前指定table重入

ct_nw_srcct_nw_dstct_nw_protoct_tp_srcct_tp_dst:ct 五元组

ct_zone:表示独立的CT 上下文,其拥有独立的ct表。作为action动作时,用于新建新的zone上下文,可以通过ct zone action来设置。没有新建过,则所有ct都在zone 0下进行。

ct_mark:可以通过 ct exec(set_field: 1->ct_mark)来设置。报文第一次匹配后,通过此action设置ct_mark到报文的metadata,重新注入datapath时,用来匹配流表指定的ct_mark。

ct_label:128的值,可以通过 ct exec(set_field: 1->ct_label)来设置,用法和ct_mark类似

CT 动作

ovs通过ct action实现ct功能,格式如下:

ct([argument]...)
ct(commit[, argument]...)

这是两种不同的模式,即带commit和不带commit。ct 支持下面的参数:

commit 只有执行了commit,才会在conntrack模块创建conntrack表项
force 强制删除已存在的conntrack表项
table 跳转到指定的table执行
zone 设置zone,隔离conntrack
exec 执行其他action,目前只支持设置ct_mark和ct_label,比如exec(set_field: 1->ct_mark)
alg=<ftp/tftp> 指定alg类型,目前只支持ftp和tftp
nat 指定ip和port

示例使用:

#添加nat表项
ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, in_port=veth_l0, actions=ct(commit,nat(src=10.1.1.240-10.2.2.2:2222-3333))"

//在一个ct里指定多次nat,只有最后一个nat生效,可参考do_xlate_actions中,ctx->ct_nat_action = ofpact_get_NAT(a)只有一个ctx->ct_nat_action 
ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, actions=ct(commit,nat(src=10.1.1.240-10.2.2.2:2222-3333), nat(dst=10.1.1.240-10.2.2.2:2222-3333)), veth_r0"

//可以通过指定多个ct,实现fullnat,即同时转换源目的ip。
//但是这两个ct必须指定不同的zone,否则只有第一个ct生效。因为在 handle_nat 中,判断只有zone不一样才会进行后续的nat操作
//错误方式,指定了src和dst nat,但是zone相同,只有前面的snat生效
ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, actions=ct(commit,nat(src=10.1.1.240-10.2.2.2:2222-3333)), ct(commit,nat(dst=10.1.1.240-10.2.2.2:2222-3333)), veth_r0"

//正确方式,使用不同zone,指定fullnat
ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, actions=ct(commit,zone=100, nat(src=10.1.1.240-10.2.2.2:2222-3333)), ct(commit, zone=200, nat(dst=10.1.1.240-10.2.2.2:2222-3333)), veth_r0"

TCP 使用范例

这里以TCP为例解释OVS中CT模块的使用。

TCP SYN包

首先下流表匹配连接首包SYN:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, in_port=veth_10, actions=ct(table=0)"

第一条规则代表当收到从veh_10口进来的tcp包时,如果是从未见过的五元组(-trk)状态,则将数据包投入CT表中查询。这里ct(table=0)是一个skb clone操作。由于没有后续操作,原有数据包丢弃不用。新clone的数据包,进入CT模块。

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+new, tcp, in_port=veth_10, actions=ct(commit),veth_r0"

第二条规则代表当数据包从CT模块回来后,此时已经有了track 跟踪(+trk),且为新的五元组(+new),所以我们下发指令:在CT表内新建表项,并将该数据包通过veth_r0转发出去。

流表下好后,我们通过scapy发送一个SYN包:

sendp(Ether()/IP(src="1.1.1.1", dst="1.1.1.2")/TCP(sport=1024, dport=2048, flags=0x02, seq=100), iface="veth_11")

可查阅当前CT表内容

[root@fedora lovelylich]# ovs-appctl dpctl/dump-conntrack | grep 1.1.1.1
tcp,orig=(src=1.1.1.1,dst=1.1.1.2,sport=1024,dport=2048),reply=(src=1.1.1.2,dst=1.1.1.1,sport=2048,dport=1024),protoinfo=(state=SYN_SENT)
[root@fedora lovelylich]#

注意:对于此时的重传SYN包,这两条规则都会再次重新命中。

TCP SYN-ACK 包

同样下两条流表匹配SYN-ACK包:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, in_port=veth_r0, actions=ct(table=0)"
ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_r0, actions=veth_10"

对于SYN-ACK包而言,从CT系统转一圈回来后,CT中的表项即转换成了+est态。我们以scapy发synack包确认:

sendp(Ether()/IP(src="1.1.1.2", dst="1.1.1.1")/TCP(sport=2048, dport=1024, flags=0x12, seq=200, ack=101), iface="veth_r1")

注意:虽然此时仅收双向包,CT表项就转为了est态,但该est态存在时间较短,如果一段时间后仍没有收到第三次ack,则该ct表项将被清理掉。只有真正收到了第三个ack包,ct中的est表项才会长时间存在。

查看此时CT表:

[root@fedora lovelylich]# ovs-appctl dpctl/dump-conntrack | grep 1.1.1.1
tcp,orig=(src=1.1.1.1,dst=1.1.1.3,sport=1024,dport=2048),reply=(src=1.1.1.3,dst=1.1.1.1,sport=2048,dport=1024),protoinfo=(state=ESTABLISHED)
[root@fedora lovelylich]#

TCP ACK 包

我们下流表匹配最后ACK包:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_10, actions=veth_r0"

发送ACK包:

sendp(Ether()/IP(src="1.1.1.1", dst="1.1.1.3")/TCP(sport=1024, dport=2048, flags=0x10, seq=101, ack=201), iface="veth_11")

此后,ct表中的表项将存在很长时间,即便我们什么数据都不在发送。

TCP 断连

TCP 断连过程中,CT表项状态将根据收到的数据包进行响应的状态转换:

收到第一次FIN-ACK时,转入FIN_WAIT_1

收到第二次反向FIN-ACK时,转入LAST_ACK

收到最后ACK时,转入TIME_WAIT状态。

但无论当前CT时何状态(即便是TIME_WAIT状态),这些FIN/ACK数据包,都将与正常数据包一样,匹配命中+est+trk状态。

OVS 内核态 CT 实现

CT 实现框架

OVS 侧实现 - 进入 CT 前

ofctl下发的ct流表action,最终由parse_CT来解析存入ofpact中:

static char * OVS_WARN_UNUSED_RESULT
parse_CT(char *arg, const struct ofpact_parse_params *pp)
{
    const size_t ct_offset = ofpacts_pull(pp->ofpacts);
    struct ofpact_conntrack *oc;
    char *error = NULL;
    char *key, *value;

    oc = ofpact_put_CT(pp->ofpacts);
    oc->flags = 0;
    oc->recirc_table = NX_CT_RECIRC_NONE;
    while (ofputil_parse_key_value(&arg, &key, &value)) {
        if (!strcmp(key, "commit")) {
    ......
}

在数据包刚到来时,还执行任何匹配和action动作时,此时状态被默认设置为-trk,该状态是在ovs_flow_key_extract->ovs_ct_update_key设置的:

static void ovs_ct_update_key(const struct sk_buff *skb,
			      const struct ovs_conntrack_info *info,
			      struct sw_flow_key *key, bool post_ct,
			      bool keep_nat_flags)
{
	ct = nf_ct_get(skb, &ctinfo);
	if (ct) {
		......
	} else if (post_ct) {
		state = OVS_CS_F_TRACKED | OVS_CS_F_INVALID;
	}
	__ovs_ct_update_key(key, state, zone, ct);
}
static void __ovs_ct_update_key(struct sw_flow_key *key, u8 state,
				const struct nf_conntrack_zone *zone,
				const struct nf_conn *ct)
{
	key->ct_state = state; // 这里设置数据包的默认状态为-trk
	key->ct_zone = zone->id;
	key->ct.mark = ovs_ct_get_mark(ct);
	ovs_ct_get_labels(ct, &key->ct.labels);
	key->ct_orig_proto = 0;
}

由于此时还没有关联到ct 表项(无论ct系统中是否已有),所以ct==NULL,state也被赋值为0,所以OVS_CS_F_TRACKED trk 标志没有被设置,也即-trk状态。所以,所有刚进入ovs的数据包,都是untrack的,所以匹配untrack流表。只有经过netfilter ct 系统转一圈后才会有trk 标记。

在dp层没有流表,触发upcall解析时,应用层时调用do_xlate_actions完成ofpact内容解析的:

static void
do_xlate_actions(const struct ofpact *ofpacts, size_t ofpacts_len,
                 struct xlate_ctx *ctx, bool is_last_action,
                 bool group_bucket_action)
{
		case OFPACT_CT:
            compose_conntrack_action(ctx, ofpact_get_CT(a), last);
            break;
}

这里的ofpact_get_CT是通过宏定义的,本质是从ofpact指针转换为ofpact_conntrack指针:

OFPACT(CT,              ofpact_conntrack,   ofpact, "ct")

而在compose_conntrack_action函数中,通过解析将ofpact_conntrack中的值,转化组建成为netlink 消息,发送给内核态模块。而内核态的openvswitch模块采用netlink机制与应用层通信,此前已经通过注册dp_packet_genl_ops注册了回调函数ovs_packet_cmd_execute。

static struct genl_ops dp_packet_genl_ops[] = {
	{ .cmd = OVS_PACKET_CMD_EXECUTE,
	  .validate = GENL_DONT_VALIDATE_STRICT | GENL_DONT_VALIDATE_DUMP,
	  .flags = GENL_UNS_ADMIN_PERM, /* Requires CAP_NET_ADMIN privilege. */
	  .policy = packet_policy,
	  .doit = ovs_packet_cmd_execute
	}
};

在收到应用层的netlink响应数据时,内核genl_family_rcv_msg_doitnetlink消息分发函数会执行对应的回调,在这里就是ovs_packet_cmd_execute函数。

随后,ovs_packet_cmd_execute->__ovs_nla_copy_actions->__ovs_nla_copy_actions->ovs_ct_copy_action->parse_ct负责解析从应用层传下来的action列表nlattr属性中的ct action部分,并存入ovs_conntrack_info结构体中:

static int parse_ct(const struct nlattr *attr, struct ovs_conntrack_info *info,
		    const char **helper, bool log)
{
	nla_for_each_nested(a, attr, rem) {
		switch (type) {
		case OVS_CT_ATTR_FORCE_COMMIT:
			info->force = true;
			/* fall through. */
		case OVS_CT_ATTR_COMMIT:
			info->commit = true;
			break;
#ifdef CONFIG_NF_CONNTRACK_ZONES
		case OVS_CT_ATTR_ZONE:
			info->zone.id = nla_get_u16(a);
			break;
#endif
		......

解析结果放入ovs_conntrack_info后,最后通过调用ovs_nla_add_action将需要执行的action列表添加到sw_flow_actions->actions数组中,留待ovs_execute_actions中执行actions时使用:

static int ovs_packet_cmd_execute(struct sk_buff *skb, struct genl_info *info)
{
	......
	//生成sw_flow_actions动作列表
	err = ovs_nla_copy_actions(net, a[OVS_PACKET_ATTR_ACTIONS],
				   &flow->key, &acts, log);
	if (err)
		goto err_flow_free;
	
	rcu_assign_pointer(flow->sf_acts, acts);
	......
	sf_acts = rcu_dereference(flow->sf_acts);
	......
	local_bh_disable();
	//执行动作
	err = ovs_execute_actions(dp, packet, sf_acts, &flow->key);
	local_bh_enable();
	rcu_read_unlock();

接下来对skb 执行action列表中的所有动作,比如output到某网口,直到所有action执行完毕,最后consume_skb()释放掉当前数据包。在do_execute_actions执行action列表的过程中,如果发现有OVS_ACTION_ATTR_CT action,就会调用nla_data(a)重新取出此前在parse_ct中通过ovs_nla_add_action添加的ovs_conntrack_info结构体,并进入ovs_ct_execute()根据ovs_conntrack_info中的相关信息,执行ct对应的处理逻辑。

static int do_execute_actions(struct datapath *dp, struct sk_buff *skb,
			      struct sw_flow_key *key,
			      const struct nlattr *attr, int len)
{
	const struct nlattr *a;
	int rem;

	for (a = attr, rem = len; rem > 0;
	     a = nla_next(a, &rem)) {
	    case OVS_ACTION_ATTR_CT:
			......
			err = ovs_ct_execute(ovs_dp_get_net(dp), skb, key,
					     nla_data(a));
			......
			break;

这里的ovs_ct_execute就是负责执行相应的动作,如set-mark,set-label,commit等。如果都不是(比如actions=ct(table=0)),也会进入ct模块进行查询,查询不到则也会新增表项。但区别是ovs_ct_commit 一定会将表项移入confirmed list,使得ct表项存在时间更长。

int ovs_ct_execute(struct net *net, struct sk_buff *skb,
		   struct sw_flow_key *key,
		   const struct ovs_conntrack_info *info)
{
	//由于netfilter的ct工作在ip层,而ovs的数据包在二层,所以这里pull掉二层头
	//偏移data指针到三层,后续从ovs提交skb给netfilter ct模块
	nh_ofs = skb_network_offset(skb);
	skb_pull_rcsum(skb, nh_ofs);

	err = ovs_skb_network_trim(skb);
	if (err)
		return err;

	if (info->commit)
		//带有commit的ct action
		err = ovs_ct_commit(net, key, info, skb);
	else
		//不带commit的ct action
		err = ovs_ct_lookup(net, key, info, skb);
	//重新恢复二层头
	skb_push(skb, nh_ofs);
	skb_postpush_rcsum(skb, skb->data, nh_ofs);
	if (err)
		kfree_skb(skb);
	return err;
}

Linux 内核侧实现

先看Linux 内核 CT系统的大概框架。Linux 内核的CT系统构建于netfilter hook点之上,如下:
在这里插入图片描述

其中在PRE_ROUTING和OUTPUT处截获进出的包进行CT处理,但此时ct表项暂存到unconfirmed list中,并在INPUT和POST_ROUTING处确认掉unconfirmed list中的ct表项,并真正移入ct hash表。

与其他ct 系统不一样的是,内核ct 实现有unconfirmed 和confirmed 的概念。这是因为linux 内核的ct 还要考虑linux 内核协议栈本身的框架,协议栈可能在local_out/post_routing 中间的各个点失败(比如路由查找失败、mtu检查等等)导致放弃发送。收包时亦是如此。所以,在local_out的时候新建的nf_conn表项存在于unconfirmed链中,直到post_routing真正出去时,才真正移入ct hash表。

但OVS 本身是没有这些hook点的!OVS工作在二层,netfilter 是在三层 IP 层的!所以无论进入ct 系统,抑或是离开,都跟netfilter hook点没有关系! OVS 是直接通过调用nf_conntrack_in/nf_conntrack_confirm来使用内核CT模块的。

针对到OVS而言,数据包skb在ovs 内执行相关前期准备与流表动作后,是通过nf_conntrack_in进入标准内核CT 模块(Linux内核也是通过此函数入口,将数据包送入netfilter ct系统的):

unsigned int
nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
{
    ......
	//获取4层协议类型
	dataoff = get_l4proto(skb, skb_network_offset(skb), state->pf, &protonum);
    ......
repeat:
    //这里如果没有已有ct项,会尝试新建ct
	ret = resolve_normal_ct(tmpl, skb, dataoff,
				protonum, state);
	......
    //获取原有或者新建的ct表项
	ct = nf_ct_get(skb, &ctinfo);
	......
    //处理该连接的协议状态变迁
	ret = nf_conntrack_handle_packet(ct, skb, dataoff, ctinfo, state);
	......
	if (ctinfo == IP_CT_ESTABLISHED_REPLY &&
	    !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
		nf_conntrack_event_cache(IPCT_REPLY, ct);
out:
	if (tmpl)
		nf_ct_put(tmpl);

	return ret;
}

此时,在resolve_normal_ct()中,如果发现当前是新连接,则会在init_conntrack新建一个ct 表项并初始化:

/* On success, returns 0, sets skb->_nfct | ctinfo */
static int
resolve_normal_ct(struct nf_conn *tmpl,
		  struct sk_buff *skb,
		  unsigned int dataoff,
		  u_int8_t protonum,
		  const struct nf_hook_state *state)
{
	//先获取连接五元组
	if (!nf_ct_get_tuple(skb, skb_network_offset(skb),
			     dataoff, state->pf, protonum, state->net,
			     &tuple)) {
	}

	//然后根据元组查找ct表
	zone = nf_ct_zone_tmpl(tmpl, skb, &tmp);
	hash = hash_conntrack_raw(&tuple, state->net);
	h = __nf_conntrack_find_get(state->net, zone, &tuple, hash);
	if (!h) {
		//如果没有找到,就新建一个ct表项,并链入unconfirmed list
		h = init_conntrack(state->net, tmpl, &tuple,
				   skb, dataoff, hash);
		if (!h)
			return 0;
	}
    ......

这里新建表项时,实际包含了两个工作,新建nf_conn结构体用以表示一条连接(不考虑方向),以及新建两个方向的五元组ct节点,即

struct nf_conntrack_tuple_hash {
	struct hlist_nulls_node hnnode;
	struct nf_conntrack_tuple tuple;
};

struct nf_conn {
	struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
    unsigned long status;
}

这里IP_CT_DIR_MAX=2,代表连接的两个方向。这里的tuplehash是属于nf_conn结构体的,所以创建nf_conn结构体就是新建了两个方向的ct节点。这样,当reply包达到时,可以直接查找到对应的ct表项。这两个hnode都是现在就链入ct 表,他们的tuple分别代表连接的两个方向的五元组,但该条连接的状态信息,仍旧由nf_conn->status统一维护。同时把连接双向的五元组都会建立好,并将ORIGINAL方向的hnode链入percpu 的 unconfirm list链表,目前还不会直接链入ct表。

resolve_normal_ct新建完ct表项,或者查找到已存在表项时,找到对应的nf_conn连接状态维护结构体,最后会设置skb->_nfct的状态:

static int
resolve_normal_ct(struct nf_conn *tmpl,
		  struct sk_buff *skb,
		  unsigned int dataoff,
		  u_int8_t protonum,
		  const struct nf_hook_state *state)
{
    ......
	ct = nf_ct_tuplehash_to_ctrack(h);

	//接下来根据新建或者查找到的ct表项,设置当前skb->ct状态为ctinfo状态
	if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) {
		ctinfo = IP_CT_ESTABLISHED_REPLY;
	} else {
		/* Once we've had two way comms, always ESTABLISHED. */
		if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
			pr_debug("normal packet for %p\n", ct);
			ctinfo = IP_CT_ESTABLISHED;
		} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
			pr_debug("related packet for %p\n", ct);
			ctinfo = IP_CT_RELATED;
		} else {
			pr_debug("new packet for %p\n", ct);
			ctinfo = IP_CT_NEW;
		}
	}
    //设置skb的_nfct指针,指向新建的nf_conn结构体,并且同时设置skb的ct状态
	nf_ct_set(skb, ct, ctinfo);
	return 0;
}

!!!注意:这里设置的是skb->_nfct指针低三位bit位所代表的连接状态,这与nf_conn->status是不一样的。nf__conn->state代表的是CT表项中从CT模块角度看到的连接状态,无论当前skb是否已经结束生命期,该连接状态都始终存在,直到新的数据包到来,再次触发状态迁移。而这里的skb->_nfct代表的是该skb数据包的ct状态,此状态用于skb离开CT系统处理后,在规则匹配中使用,该状态仅存在于数据包存在周期。

这里对于新数据包而言,则是将ctinfo 置位为IP_CT_NEW状态。netfilter中skb->_nfct状态总共由7种状态定义,这些状态定义的是数据包本身的状态,用于在ct 规则中做匹配用,比如IP_CT_NEW匹配的就是+new标记:

/* Connection state tracking for netfilter.  This is separated from,
   but required by, the NAT layer; it can also be used by an iptables
   extension. */
enum ip_conntrack_info {
	/* Part of an established connection (either direction). */
	IP_CT_ESTABLISHED,
	/* Like NEW, but related to an existing connection, or ICMP error
	   (in either direction). */
	IP_CT_RELATED,
	/* Started a new connection to track (only
           IP_CT_DIR_ORIGINAL); may be a retransmission. */
	IP_CT_NEW,
	/* >= this indicates reply direction */
	IP_CT_IS_REPLY,
	IP_CT_ESTABLISHED_REPLY = IP_CT_ESTABLISHED + IP_CT_IS_REPLY,
	IP_CT_RELATED_REPLY = IP_CT_RELATED + IP_CT_IS_REPLY,
	/* No NEW in reply direction. */
	/* Number of distinct IP_CT types. */
	IP_CT_NUMBER,
	IP_CT_UNTRACKED = 7,
};

这些状态存于skb->_nfct字段的低三位,高29位则作为指针,指向skb对应的ct 表项地址。可通过nf_ct_get() 函数方便的获取和设置。

static inline void
nf_ct_set(struct sk_buff *skb, struct nf_conn *ct, enum ip_conntrack_info info)
{
	skb_set_nfct(skb, (unsigned long)ct | info);
}

/* Return conntrack_info and tuple hash for given skb. */
static inline struct nf_conn *
nf_ct_get(const struct sk_buff *skb, enum ip_conntrack_info *ctinfo)
{
	unsigned long nfct = skb_get_nfct(skb);

	*ctinfo = nfct & NFCT_INFOMASK;
	return (struct nf_conn *)(nfct & NFCT_PTRMASK);
}

每种协议,都需要实现自己的新建ct表项方法,用以完成协议特定的初始化操作,比如tcp的tcp_new甚至会在nf_conn中记录最大ack值等信息。udp则是没有自己特定的新建时初始化操作。

最后,新建/查找到ct表项后,由nf_conntrack_handle_packet负责处理协议特定的ct状态变迁。该函数实际是个代理,会根据ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple.dst.protonum取得当前skb的协议类型,转而调用对应协议的处理函数,比如udp是nf_conntrack_udp_packet,tcp是nf_conntrack_tcp_packet,这些函数负责处理自己协议特定的状态变迁。

每个协议都在nf_conn的proto定义了自己的私有成员域:

struct nf_conn {
	......
	/* Storage reserved for other modules, must be the last member */
	union nf_conntrack_proto proto;
};
/* per conntrack: protocol private data */
union nf_conntrack_proto {
	/* insert conntrack proto private data here */
	.....
	struct ip_ct_tcp tcp;
	struct nf_ct_udp udp;
	struct nf_ct_gre gre;
	.....
};
struct nf_ct_udp {
	unsigned long	stream_ts;
};
struct ip_ct_tcp {
    ......
	u_int8_t	state;		/* state of the connection (enum tcp_conntrack) */
	......
};

简单的udp协议是直接使用nf_conn->status字段作为自身连接状态。而复杂如tcp,则是在nf_conn->proto->(tcp/udp)->state 状态单独维护自己协议的状态变迁,甚至还记录了连接建立过程中的协商字段。

这里假设当前是udp协议数据包(该协议的状态跳转比较简单,TCP协议的状态变化我们在后文单独讲解),则进入udp处理流程。如果当前udp包是一个回复包,在resolve_normal_ct中直接根据数据包的发送方向判定为回复包,并设置skb->_nfct状态为IP_CT_ESTABLISHED_REPLY:

static int
resolve_normal_ct(struct nf_conn *tmpl,
		  struct sk_buff *skb,
		  unsigned int dataoff,
		  u_int8_t protonum,
		  const struct nf_hook_state *state)
{
    ......
	ct = nf_ct_tuplehash_to_ctrack(h);

	//接下来根据新建或者查找到的ct表项,设置当前skb->ct状态为ctinfo状态
	if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) {
		ctinfo = IP_CT_ESTABLISHED_REPLY;
	} else {

此时则代表双向数据包我们都已经收到,于是将skb->_nfct置为IP_CT_ESTABLISHED_REPLY,代表该数据包既是连接建立,又是连接的回复包。在离开CT后,在OVS侧(后文ovs_ct_get_state可以看到)OVS 再次转化该状态,这样skb就匹配上离开CT系统后的OVS 规则+trk+est状态:

IP_CT_ESTABLISHED_REPLY = IP_CT_ESTABLISHED + IP_CT_IS_REPLY,

再看ct 系统nf_conn本身的状态维护。在nf_conntrack_in最后,也就是数据包即将离开CT 模块时,检查是否是回复包,如果是,同时设置nf_conn->status字段为看到回复包了IPS_SEEN_REPLY_BIT:

unsigned int
nf_conntrack_in(struct sk_buff *skb, const struct nf_hook_state *state)
{
	......
	if (ctinfo == IP_CT_ESTABLISHED_REPLY &&
	    !test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
		nf_conntrack_event_cache(IPCT_REPLY, ct);
	......
	return ret;
}

这样,设置了nf_conn为已发现回复包后,对于此后所有的数据包,无论从哪个方向来,都会发现连接已设置了IPS_SEEN_REPLY_BIT,在resolve_normal_ct里面,直接将这些数据包设置为+est状态(当然,+est状态还是OVS来转化的,CT只是设置skb为IP_CT_ESTABLISHED:

	/* It exists; we have (non-exclusive) reference. */
	if (NF_CT_DIRECTION(h) == IP_CT_DIR_REPLY) {
		ctinfo = IP_CT_ESTABLISHED_REPLY;
	} else {
		/* Once we've had two way comms, always ESTABLISHED. */
		if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
			pr_debug("normal packet for %p\n", ct);
			ctinfo = IP_CT_ESTABLISHED;
		} else if (test_bit(IPS_EXPECTED_BIT, &ct->status)) {
			pr_debug("related packet for %p\n", ct);
			ctinfo = IP_CT_RELATED;
		} else {
			pr_debug("new packet for %p\n", ct);
			ctinfo = IP_CT_NEW;
		}
	}

如果连接是单方向的新建包/重传包,这种情况只是在nf_ct_refresh_acct中延期ct表项的超时时间,没有其他额外设置。所以,此时的skb->_nfct的状态仍然是前述resolve_normal_ct中设置的IP_CT_NEW状态,而nf_conn->status自从分配后没有变化,所以仍然是__nf_conntrack_alloc中分配时默认设置的0。

/* Returns verdict for packet, and may modify conntracktype */
int nf_conntrack_udp_packet(struct nf_conn *ct,
			    struct sk_buff *skb,
			    unsigned int dataoff,
			    enum ip_conntrack_info ctinfo,
			    const struct nf_hook_state *state)
{
	......
	/* If we've seen traffic both ways, this is some kind of UDP
	 * stream. Set Assured.
	 */
	if (test_bit(IPS_SEEN_REPLY_BIT, &ct->status)) {
		unsigned long extra = timeouts[UDP_CT_UNREPLIED];

		/* Still active after two seconds? Extend timeout. */
		if (time_after(jiffies, ct->proto.udp.stream_ts))
			extra = timeouts[UDP_CT_REPLIED];

		nf_ct_refresh_acct(ct, ctinfo, skb, extra);

		/* never set ASSURED for IPS_NAT_CLASH, they time out soon */
		if (unlikely((ct->status & IPS_NAT_CLASH)))
			return NF_ACCEPT;

		/* Also, more likely to be important, and not a probe */
		if (!test_and_set_bit(IPS_ASSURED_BIT, &ct->status))
			nf_conntrack_event_cache(IPCT_ASSURED, ct);
	} else {
		nf_ct_refresh_acct(ct, ctinfo, skb, timeouts[UDP_CT_UNREPLIED]);
	}
	return NF_ACCEPT;
}

假如这里是udp的回复包,在数据包第一次以-trk状态进入CT系统并离开CT时,会设置nf_conn->state为IPS_SEEN_REPLY_BIT。这样,当udp数据包第二次+trk+est进入CT系统后,在nf_conntrack_udp_packet此时才修改udp连接的状态:nf_conn->status,将设置为IPS_ASSURED_BIT。

ct 模块定义了连接的以下状态集合,其中主要是IPS_SEEN_REPLY、IPS_CONFIRMED、IPS_ASSURED。其余主要是为ftp、nat服务的。

enum ip_conntrack_status {
    IPS_EXPECTED      = (1 << IPS_EXPECTED_BIT),
    IPS_SEEN_REPLY    = (1 << IPS_SEEN_REPLY_BIT),
    IPS_ASSURED       = (1 << IPS_ASSURED_BIT),
    IPS_CONFIRMED     = (1 << IPS_CONFIRMED_BIT),
    IPS_SRC_NAT       = (1 << IPS_SRC_NAT_BIT),
    IPS_DST_NAT       = (1 << IPS_DST_NAT_BIT),
    IPS_NAT_MASK      = (IPS_DST_NAT | IPS_SRC_NAT),
    ......
};

至此,内核ct 系统结束,再次回到ovs 处理逻辑。

OVS 侧实现 - 离开 CT 后

数据包经由nf_conntrack_in调用,在netfilter ct 系统跑一圈后出来,OVS通过ovs_ct_update_key来将key->ct_state更新,用于匹配。其中,ovs_ct_update_key->ovs_ct_get_state实现netfilter skb->_nfct 状态(比如IP_CT_NEW)和ovs 需要的匹配状态(比如OVS_CS_F_NEW)之间的转换映射,并为经过CT系统的数据报都打上+trk标记:OVS_CS_F_TRACKED。

所以当数据包跑一圈后,对应的skb 状态为IP_CT_NEW时,对应的sw_flow_key ct_state状态则为 OVS_CS_F_TRACKED | OVS_CS_F_NEW:

/* Map SKB connection state into the values used by flow definition. */
static u8 ovs_ct_get_state(enum ip_conntrack_info ctinfo)
{
	u8 ct_state = OVS_CS_F_TRACKED;

	switch (ctinfo) {
	case IP_CT_ESTABLISHED_REPLY:
	case IP_CT_RELATED_REPLY:
		ct_state |= OVS_CS_F_REPLY_DIR;
		break;
	default:
		break;
	}

	switch (ctinfo) {
	case IP_CT_ESTABLISHED:
	case IP_CT_ESTABLISHED_REPLY:
		ct_state |= OVS_CS_F_ESTABLISHED;
		break;
	case IP_CT_RELATED:
	case IP_CT_RELATED_REPLY:
		ct_state |= OVS_CS_F_RELATED;
		break;
	case IP_CT_NEW:
		ct_state |= OVS_CS_F_NEW;
		break;
	default:
		break;
	}

	return ct_state;
}

所以,这时数据包回到OVS并再次进行匹配时,就是+trk+new状态了。就会匹配上比如下面这条规则:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+new, tcp, in_port=veth_10, actions=ct(commit),veth_r0"

这时会再次进入ovs_ct_execute函数,并执行相关的CT action,比如commit。假设命中的是commit action,则不同于Linux netfilter CT 实现在LOCAL_IN/POST_ROUTING处将ct表项从unconfirmed list移入ct hash 表,OVS此时是直接调用Linux 内核 CT的接口nf_conntrack_confirm将skb对应的nf_conn从unconfirmed list 移入真正的hash表。另外,linux ct 在confirm时,会同时把nf_conn状态置位为IPS_CONFIRMED状态:

static void __nf_conntrack_insert_prepare(struct nf_conn *ct)
{
	......
	atomic_inc(&ct->ct_general.use);
	ct->status |= IPS_CONFIRMED;
	......
}

内核 CT 下的TCP 状态迁移

先说核心框架。对于tcp而言,由于ct表项在有数据包到来时,需要完成状态迁移,所以每个协议都需要自定义自己的状态迁移表,比如tcp的状态迁移表则定义如下:

/*
 * The TCP state transition table needs a few words...
 *
 * We are the man in the middle. All the packets go through us
 * but might get lost in transit to the destination.
 * It is assumed that the destinations can't receive segments
 * we haven't seen.
 *
 * The checked segment is in window, but our windows are *not*
 * equivalent with the ones of the sender/receiver. We always
 * try to guess the state of the current sender.
 *
 * The meaning of the states are:
 *
 * NONE:	initial state
 * SYN_SENT:	SYN-only packet seen
 * SYN_SENT2:	SYN-only packet seen from reply dir, simultaneous open
 * SYN_RECV:	SYN-ACK packet seen
 * ESTABLISHED:	ACK packet seen
 * FIN_WAIT:	FIN packet seen
 * CLOSE_WAIT:	ACK seen (after FIN)
 * LAST_ACK:	FIN seen (after FIN)
 * TIME_WAIT:	last ACK seen
 * CLOSE:	closed connection (RST)
 *
 * Packets marked as IGNORED (sIG):
 *	if they may be either invalid or valid
 *	and the receiver may send back a connection
 *	closing RST or a SYN/ACK.
 *
 * Packets marked as INVALID (sIV):
 *	if we regard them as truly invalid packets
 */
static const u8 tcp_conntracks[2][6][TCP_CONNTRACK_MAX] = {
	{
/* ORIGINAL */
/* 	     sNO, sSS, sSR, sES, sFW, sCW, sLA, sTW, sCL, sS2	*/
/*syn*/	   { sSS, sSS, sIG, sIG, sIG, sIG, sIG, sSS, sSS, sS2 },
/*
 *	sNO -> sSS	Initialize a new connection
 *	sSS -> sSS	Retransmitted SYN
 *	sS2 -> sS2	Late retransmitted SYN
 *	sSR -> sIG
 *	sES -> sIG	Error: SYNs in window outside the SYN_SENT state
 *			are errors. Receiver will reply with RST
 *			and close the connection.
 *			Or we are not in sync and hold a dead connection.
 *	sFW -> sIG
 *	sCW -> sIG
 *	sLA -> sIG
 *	sTW -> sSS	Reopened connection (RFC 1122).
 *	sCL -> sSS
 */
/* 	     sNO, sSS, sSR, sES, sFW, sCW, sLA, sTW, sCL, sS2	*/
/*synack*/ { sIV, sIV, sSR, sIV, sIV, sIV, sIV, sIV, sIV, sSR },
/*
 *	sNO -> sIV	Too late and no reason to do anything
 *	sSS -> sIV	Client can't send SYN and then SYN/ACK
 *	sS2 -> sSR	SYN/ACK sent to SYN2 in simultaneous open
 *	sSR -> sSR	Late retransmitted SYN/ACK in simultaneous open
 *	sES -> sIV	Invalid SYN/ACK packets sent by the client
 *	sFW -> sIV
 *	sCW -> sIV
 *	sLA -> sIV
 *	sTW -> sIV
 *	sCL -> sIV
 */
  .......
};

状态名字比较抽象,对应关系如下,其实就是普通的TCP状态:

#define sNO TCP_CONNTRACK_NONE
#define sSS TCP_CONNTRACK_SYN_SENT
#define sSR TCP_CONNTRACK_SYN_RECV
#define sES TCP_CONNTRACK_ESTABLISHED
#define sFW TCP_CONNTRACK_FIN_WAIT
#define sCW TCP_CONNTRACK_CLOSE_WAIT
#define sLA TCP_CONNTRACK_LAST_ACK
#define sTW TCP_CONNTRACK_TIME_WAIT
#define sCL TCP_CONNTRACK_CLOSE
#define sS2 TCP_CONNTRACK_SYN_SENT2
#define sIV TCP_CONNTRACK_MAX
#define sIG TCP_CONNTRACK_IGNORE

该转移表通过填入当前收包方向、当前收包的tcp标志位、现有ct的状态来获取新的状态值。举个例子:对于新的ct表项,收到syn包的情况下,是这样获取新的状态的:

new_state = tcp_conntracks[0][TCP_SYN_SET][TCP_CONNTRACK_NONE];

所以得到sSS 状态,也就是TCP_CONNTRACK_SYN_SENT。另外在负责处理tcp 收到新数据包时,ct表项的状态迁移的nf_conntrack_tcp_packet函数中,也采用该迁移表维护ct表项的状态变化。

此外,对于处于连接不同状态的CT节点,TCP也有着不同的超时值,以便于节省内存,避免DDOS攻击弱点。这里定义了tcp的ct表项在各个状态下的超时值:

static const unsigned int tcp_timeouts[TCP_CONNTRACK_TIMEOUT_MAX] = {
	[TCP_CONNTRACK_SYN_SENT]	= 2 MINS,
	[TCP_CONNTRACK_SYN_RECV]	= 60 SECS,
	[TCP_CONNTRACK_ESTABLISHED]	= 5 DAYS,
	[TCP_CONNTRACK_FIN_WAIT]	= 2 MINS,
	[TCP_CONNTRACK_CLOSE_WAIT]	= 60 SECS,
	[TCP_CONNTRACK_LAST_ACK]	= 30 SECS,
	[TCP_CONNTRACK_TIME_WAIT]	= 2 MINS,
	[TCP_CONNTRACK_CLOSE]		= 10 SECS,
	[TCP_CONNTRACK_SYN_SENT2]	= 2 MINS,
/* RFC1122 says the R2 limit should be at least 100 seconds.
   Linux uses 15 packets as limit, which corresponds
   to ~13-30min depending on RTO. */
	[TCP_CONNTRACK_RETRANS]		= 5 MINS,
	[TCP_CONNTRACK_UNACK]		= 5 MINS,
};

关于 NAT 实现

Linux 内核的 CT 是支持NAT 转换的,OVS也利用了这点实现动态NAT(不同于modify action)。

有几点需要注意:

  1. NAT操作必须在helper(实现CT ALG功能的函数)之前完成,以便于helper函数知道NAT动作的存在
  2. NAT更改IP地址,导致可能需要执行序列号调整等操作,这需要注意。

TCP 三次握手之旅

前面是综述各个关键函数,以及实现逻辑,但CT连接状态迁移、skb的状态和OVS规则匹配涉及包序,在前文讲骨骼框架中无法细述。故接下来以最复杂的TCP三次握手为例,说明OVS 内核 CT 的实现逻辑,重点关注状态迁移,对于实现函数和实现方法,则简要带过,不再赘述。

此外,从上面框架描述也能看出,OVS中CT流程主要分为以下四步:

  1. 匹配最初规则;
  2. 查找/创建CT表项,设置包状态;
  3. 处理协议特定状态;
  4. 新状态回到OVS

SYN 包到来时

匹配规则:首先syn包到来时,由于skb->_nfct状态仍为0,也就是既没有ct表项与之关联,也没有ct状态设置。所以此时数据包匹配的是-trk状态。

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, in_port=veth_10, actions=ct(table=0)"

由该规则action通过nf_conntrack_in转入CT模块处理。

查找/创建CT表项,设置包状态:CT模块发现新连接数据包,于是init_conntrack创建nf_conn结构,创建双向tuple,并设置nf_conn->status为0,最后链入unconfirmed list链表。

同时,resolve_normal_ct检查nf_conn->status,由于状态为0,所以把这个新连接的数据包skb->_nfct设置为IP_CT_NEW。至此,数据包匹配OVS规则的+trk+new状态。

处理协议特定状态:CT 本身的状态迁移处理完成,最后进入协议特定的状态处理。tcp是在nf_conntrack_tcp_packet处理的。

这是新的tcp 连接,所以初始化nf_conn->proto.tcp字段,包括以skb为信息源,初始化其中的td_end(最大序列号)、窗口尺寸以及tcp 协议选项等。

最后,通过tcp的协议状态跳转表tcp_conntracks,设置该CT连接的协议特定连接状态为TCP_CONNTRACK_SYN_SENT,以及该CT表项的超时时间。

ct->proto.tcp.state = new_state

新状态回到OVS:完成CT模块处理后,skb 带着新的+trk+new状态,流转回OVS,继续匹配新的规则,执行新的action。比如经过CT处理后的SYN包就匹配下面这条规则:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+new, tcp, in_port=veth_10, actions=ct(commit),veth_r0"

commit action是在ovs_ct_commit中处理的,主要是设置nf_conn的label、mark值。最后,将skb对应的nf_conn从unconfirmed list链表移入CT hash表,随后结束CT 处理,从veth_r0送出。

SYN-ACK 包到来时

匹配规则:同样,当SYN-ACK到来时,数据包没有设置过状态,匹配如下规则:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, in_port=veth_r0, actions=ct(table=0)"

查找/创建CT表项,设置包状态:同样转入CT模块处理。直接找到之前已经创建的CT表项,同时由于当前数据包是回包方向,所以设置数据包状态skb->_nfct为IP_CT_ESTABLISHED_REPLY,也就是匹配+trk+est+rpl状态。

处理协议特定状态:查找到CT表项,也更新了skb状态,同样,继续处理协议特定的状态迁移:依然在nf_conntrack_tcp_packet中,由于是反方向的SYN-ACK包,原有状态为TCP_CONNTRACK_SYN_SENT,查找tcp_conntracks跳转表,更新ct->proto.tcp.state新的状态为sSR,即TCP_CONNTRACK_SYN_RECV,同时更新超时时间。

最后在即将离开CT模块时,由于已经收到回复包,所以设置连接状态nf_conn->status为IPS_SEEN_REPLY_BIT。

新状态回到OVS:完成CT模块处理后,skb 带着新的+trk+new状态,流转回OVS,继续匹配新的规则,执行新的action。比如经过CT处理后的SYN-ACK包就匹配下面这条规则,最后经由veth_10口送出。

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_r0, actions=veth_10"

ACK 包到来时

匹配规则:数据包没有设置过状态,匹配如下规则:

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=-trk, tcp, in_port=veth_10, actions=ct(table=0)"

查找/创建CT表项,设置包状态:同样转入CT模块处理。直接找到之前已经创建的CT表项,CT表项状态已设置IPS_SEEN_REPLY_BIT,所以设置数据包状态skb->_nfct为IP_CT_ESTABLISHED,也就是匹配+trk+est状态。

处理协议特定状态:查找到CT表项,也更新了skb状态,同样,继续处理协议特定的状态迁移:依然在nf_conntrack_tcp_packet中,由于是正向ACK包,原有状态为TCP_CONNTRACK_SYN_RECV,查找tcp_conntracks跳转表,更新ct->proto.tcp.state新的状态为sES,即TCP_CONNTRACK_ESTABLISHED,同时更新超时时间。

新状态回到OVS:完成CT模块处理后,skb 带着新的+trk+est状态,流转回OVS,继续匹配新的规则,执行新的action。比如经过CT处理后的SYN-ACK包就匹配下面这条规则,最后经由veth_10口送出。

ovs-ofctl add-flow br0 "table=0, priority=50, ct_state=+trk+est, tcp, in_port=veth_10, actions=veth_r0"

至此,协议特定的三次握手完成。其实我们能看到,在SYNACK到来时,OVS就已经标记数据包为+est了,这点需要注意。

遗留问题

  1. NAT 实现未详细跟踪实现
  2. ALG 实现未详细跟踪实现
  3. IP分片与CT表流入流出逻辑分析

参考文档

  1. https://docs.openvswitch.org/en/latest/tutorials/ovs-conntrack/
  2. http://arthurchiao.art/blog/conntrack-design-and-implementation-zh/
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-31 15:49:47  更:2021-08-31 15:50:37 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 22:28:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码