赵占旭的博客

OpenvSwitch Conntrack & NAT

这里只是捡重点的说,需要原版去看ovs-ofctl(8)

这里只关注OpenvSwitch的DPDK相关的conntrack和NAT。

conntrack匹配项说明


match项表示为ct_state=flags/mask或者ct_state=[+flag…][-flag…]

单独的报文处于两个状态:untracked和tracked。前者是指报文还没有进入conntrack中,用-trk表示,这时候一般是用来执行ct的action的,然后就会变成后者,即+trk。

连接有两个状态:uncommitted和committed。连接默认是uncommitted。要确认连接信息,比如连接是否established状态,连接状态必须是committed。当带有commit参数的ct_action处理一个报文时,连接会变成committed状态知道连接终止。连接committed状态持续时间超过报文的持续时间。

一般的用法是报文untracked的时候,就会调用ct(commit)的actions,这样报文也进入conntrack,状态也变成了commited,后续conntrack就可以继续更新状态。

以下是flags的描述:

  • 0x01:new 新建连接的开始,这个表示一般存在于uncommitted状态的连接中。
  • 0x02:est 这是已经存在,并且准备好的连接,存在于committed连接。
  • 0x04:rel 这是和一个存在连接相关的连接。比如ICMP 目的不可达消息或者FTP数据连接。存在于committed连接。
  • 0x08:rpl 这个流是反方向,意味着没有初始化连接。存在于committed连接。
  • 0x10:inv 无效状态,意味着连接跟踪不能识别连接。这个标志包含了一个连接跟踪可能遇到的任何问题,例如:L3/L4协议处理程序没有加载或者不可用。在Linux内核datapath,意味着nf_conntrack_ipv4或者nf_conntrack_ipv6模块没有加载。L3/L4协议处理程序确定报文分组格式不对。报文协议的长度不对。
  • 0x20:trk 报文状态是tracked,意味着他先前已经经历了连接跟踪。如果该标志没有设置,其他的标志也不会被设置。如果这个标志设置了,报文状态为tracked,其他的标志才会被设置。这个字段是Open vSwitch2.5才引入的。

以下的字段于连接跟踪有关,并且只用于跟踪的数据报文。ct_action也用于填充和修改这些字段。

  • ct_zone=zone 匹配给出的16位连接区域。这代表了CT执行的最近的连接跟中上下文。每个zone是一个独立的连接跟踪上下文,所以你想在多个上下文中跟踪一个报文,你必须使用ct_action多次。
  • ct_mark=value[/mask] 匹配给出的32位连接mark值。这表示元数据和报文所属的连接有联系。
  • ct_label=value[/mask] 匹配128位的连接label值。这表示元数据和报文所属的连接有联系。

conntrack actions执行


actions项表示为ct([argument][,argument…])

通过conntrack发送报文。依赖ct_state,支持以下参数:

  • commit 提交连接到connection tracking 模块。连接信息的存储时间远高于报文的生存时间。一些ct_state只为commited连接。
  • table=number 将流程处理一分为二,前面都是按照untracked的报文处理。后者是将报文发送到connection tracker模块,并且回注到OpenFlow流程,并且在num的table里面处理,设置ct_state和其他ct匹配的字段。如果table没有指定,提交到connection tracker的报文不会回注到OpenFlow。强烈建议指定比当前table靠后的table,以防止循环。
  • zone=value或者zone=src[start…end] 一个16位上下文id,将连接隔离到不同的domains,不同的zones允许重叠的网络地址。如果zone没有指定,默认使用zone0。zone可以用一个16进制的数字指定,也可以用NXM的src字段指定。start和end指定一个16位的区间。报文回注到table指定的处理流程时,这个值会拷贝到ct_zone的match字段。
  • exec([action][,action]) 在conntrack上下文执行actions。这是一个和指定flow相同格式的actions。actions修改ct_mark或者ct_label字段,这些也可以放在exec里面。例如:
    • set_field:value->ct_mark 存储一个32位的元数据到连接。如果连接是commited,当报文发送到table指定的conntrack时,这个连接的报文查找将填充ct_mark流表字段。
    • set_field:value->ct_label 存储一个128位的元数据到连接。如果连接的状态是commited,当报文发送到table指定的conntrack时,连接的报文将填充ct_label刘字段。
    • commit参数需要指定使用exec(…)
  • alg=alg 指定应用层网关alg到指定的连接状态的conntrack。支持的类型有:
    • ftp 查找协商的FTP数据连接。如果后面相关的FTP数据连接到达时,报文通过ct时,ct action在ct_state字段设置rel标志。
    • tftp 查找协商的TFTP数据连接。如果后面相关的TFTP数据连接到达时,报文通过ct时,ct action在ct_state字段设置rel标志。
  • nat[((src|dst)=addr1[-addr2][:port1[-port2]][,flags])] 为跟踪的连接进行地址和端口转换。只有commit的连接才能进行NAT操作。
    • addr1[-addr2] 翻译的地址
    • port1[-port2] 翻译的端口
    • flags包含以下几种
      • random 范围的随机选取,与hash互斥
      • hash 进行hash选取,与random互斥
      • persistent 系统启动的时候就提供了固定的映射
  • ct_clear 清空

ct action用作有状态的防火墙结构,提交的流量匹配ct_state允许establish连接,但是拒绝新连接。

ACL示例


以下流表提供了一个例子,实现了简单的ACL,允许端口1到端口2的连接,只允许端口2到端口1的establish的连接:

应用层流表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//优先级最低,丢包
table=0,priority=1,action=drop
//arp报文不管
table=0,priority=10,arp,action=normal
//没有加入conntrack的报文,执行ct加入conntrack,并且发送到table 1
table=0,priority=100,ip,ct_state=-trk,action=ct(table=1)
//1口的报文,并且是新连接的则,执行ct的commit,创建连接,并且报文发给2口
table=1,in_port=1,ip,ct_state=+trk+new,action=ct(commit),2
//1口的报文,连接已经建立完成,establish状态,直接发给2口
table=1,in_port=1,ip,ct_state=+trk+est,action=2
//2口到1口的报文,状态是new的,丢弃
table=1,in_port=2,ip,ct_state=+trk+new,action=drop
//2口到1口的报文,状态是establish,直接发给1口
table=1,in_port=2,ip,ct_state=+trk+est,action=1

如果ct操作分片的ip报文,首先隐式的重组报文,并且发送到conntrack,输出时再重新分片。重组发生在zone的上下文,意味着发送到不同zone的分片将不能重组。
处理分片报文时暂停,当最后一个分片收到时,重组然后继续处理。因为报文排序不由IP协议保证,不能确定哪些IP分片导致充足。因此,我们强烈建议多流不应该执行ct来重组相同IP的分片。

datapath流表

datapath的流表结果,端口1向2发起连接

1
2
3
4
5
6
7
8
9
10
//一直持续,主要是将报文纳入conntrack中,然后actions的ct表示根据报文更新conntrack状态,第一次更新为new
ct_state(-trk),recirc_id(0),in_port(1),eth_type(0x0800),ipv4(frag=no), packets:331672, bytes:500255400, used:0.000s, flags:SP., actions:ct,recirc(0x8)
//持续时间很短,只是新建的时候存在一会,发给端口2
ct_state(+new+trk),recirc_id(0x8),in_port(1),eth_type(0x0800),ipv4(frag=no), packets:1, bytes:74, used:1.763s, flags:S, actions:ct(commit),2
//一直持续,主要是符合条件的报文发给端口2
ct_state(-new+est+trk),recirc_id(0x8),in_port(1),eth_type(0x0800),ipv4(frag=no), packets:331650, bytes:500224980, used:0.000s, flags:P., actions:2
//一直持续,将反方向的报文纳入conntrack中,然后根据报文更新conntrack状态,这次更新为est
ct_state(-trk),recirc_id(0),in_port(2),eth_type(0x0800),ipv4(frag=no), packets:163789, bytes:10846030, used:0.000s, flags:SP., actions:ct,recirc(0x9)
//一直持续,主要是符合条件的报文发给端口1
ct_state(-new+est+trk),recirc_id(0x9),in_port(2),eth_type(0x0800),ipv4(frag=no), packets:163789, bytes:10846030, used:0.000s, flags:SP., actions:1

端口2向1发起连接

1
2
ct_state(+new+trk),recirc_id(0x6),in_port(2),eth_type(0x0800),ipv4(frag=no), packets:1, bytes:74, used:1.353s, flags:S, actions:drop
ct_state(-trk),recirc_id(0),in_port(2),eth_type(0x0800),ipv4(frag=no), packets:1, bytes:74, used:1.353s, flags:S, actions:ct,recirc(0x6)

NAT示例


以下流表提供了一个例子,实现了简单的NAT,允许端口1(192.168.1.10/24)的报文SNAT到10.0.0.1/24和端口5000-50000:

应用层流表

1
2
3
4
5
6
7
8
9
10
//优先级最低,不管
table=0,priority=1,action=normal
//没有加入conntrack的报文,执行ct加入conntrack,执行NAT,并且发送到table 1
table=0,priority=10,ip,ct_state=-trk,action=ct(nat,table=1)
//1口的报文,并且是新连接的则,执行ct的commit,创建连接,执行NAT规则,并且报文发给2口
table=1,in_port=1,ip,ct_state=+trk+new,action=ct(nat(src=10.0.0.1-10.0.0.255:5000-50000),commit),2
//1口的报文,连接已经建立完成,establish状态,直接发给2口
table=1,in_port=1,ip,ct_state=+trk+est,action=2
//2口到1口的报文,状态是establish,直接发给1口
table=1,in_port=2,ip,ct_state=+trk+est,action=1

datapath流表

1
2
3
4
5
6
7
8
9
10
//一直持续,主要是将报文纳入conntrack中,然后actions的ct表示根据报文更新conntrack状态,第一次更新为new,执行NAT操作
ct_state(-trk),recirc_id(0),in_port(1),eth_type(0x0800),ipv4(frag=no), packets:325157, bytes:489925194, used:0.000s, flags:SP., actions:ct(nat),recirc(0x7)
//持续时间很短,只是新建的时候存在一会,创建NAT规则,发给端口2
ct_state(+new+trk),recirc_id(0x7),in_port(3),eth_type(0x0800),ipv4(frag=no), packets:1, bytes:74, used:1.451s, flags:S, actions:ct(commit,nat(src=10.0.0.1-10.0.0.255:5000-50000)),2
//一直持续,主要是符合条件的报文发给端口2
ct_state(-new+est+trk),recirc_id(0x7),in_port(3),eth_type(0x0800),ipv4(frag=no), packets:325155, bytes:489925054, used:0.000s, flags:P., actions:2
//一直持续,将反方向的报文纳入conntrack中,然后根据报文更新conntrack状态,这次更新为est,执行NAT操作
ct_state(-trk),recirc_id(0),in_port(2),eth_type(0x0800),ipv4(frag=no), packets:162817, bytes:10752486, used:0.000s, flags:SP., actions:ct(nat),recirc(0x8)
//一直持续,主要是符合条件的报文发给端口1
ct_state(-new+est+trk),recirc_id(0x8),in_port(2),eth_type(0x0800),ipv4(frag=no), packets:162815, bytes:10752354, used:0.000s, flags:SP., actions:1

datapath代码分析


这里主要关注action,未关注match部分。

pmd_thread_main-->dp_netdev_process_rxq_port-->dp_netdev_input-->dp_netdev_input__主要有match函数emc_processing和action操作函数packet_batch_execute

  • emc_processing主要将不匹配的报文去大表里面查,匹配的则放到流表对应的队列中。
  • packet_batch_execute-->dp_execute_cb主要是根据action去操作队列里面的报文。

现在主要关注conntrack部分的代码

dp_execute_cb

  • OVS_ACTION_ATTR_OUTPUT,调用dp_netdev_lookup_port查找端口,然后调用netdev_send进行报文发送。
  • OVS_ACTION_ATTR_TUNNEL_PUSH,调用push_tnl_action进行tunnel封装,然后调用dp_netdev_recirculate-->dp_netdev_input__重新查表操作。
  • OVS_ACTION_ATTR_TUNNEL_POP,调用netdev_pop_header解封装,然后调用dp_netdev_recirculate-->dp_netdev_input__重新查表操作。
  • OVS_ACTION_ATTR_RECIRC,给报文赋值recirc,然后调用dp_netdev_recirculate-->dp_netdev_input__重新查表操作。
  • OVS_ACTION_ATTR_CT,解析CT内的一些sub_type,然后调用conntrack_execute操作跟踪的连接。这里需要注意的是OVS_CT_ATTR_NAT是不支持的

conntrack_execute

  • 每个报文调用conn_key_extract解析L2、3、4协议头,计算出src hash和 dst hash。然后加入bucket_list
  • 循环bucket_list以及bucket下的maps,调用conn_key_lookup找到连接,调用process_one更新连接状态。
  • 根据情况set_mark或者set_lable

process_one

  • 首先判断有没有连接,如果有,则调用状态更新函数conn_update,主要是根据不同的协议调用不同的更新函数。
  • 如说是状态CT_UPDATE_NEW,则表示是新的连接,需要删除旧的,重新建连接。
  • 如果没有连接,调用conn_not_found创建连接。

NAT补丁


patch文件源自这里,后续可能还会更新,现在这里感谢作者的付出和共享。

前面提到的dp_execute_cb函数中没有支持OVS_CT_ATTR_NAT,所以这次的补丁我们就从这里看起。

patch之前的部分代码,NAT是没有支持的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void dp_execute_cb(void *aux_, struct dp_packet_batch *packets_,
const struct nlattr *a, bool may_steal)
{
switch ((enum ovs_action_attr)type) {
case OVS_ACTION_ATTR_CT:
NL_ATTR_FOR_EACH_UNSAFE (b, left, nl_attr_get(a),
nl_attr_get_size(a)) {
enum ovs_ct_attr sub_type = nl_attr_type(b);
switch(sub_type) {
//NAT此处是不支持的
case OVS_CT_ATTR_NAT:
case OVS_CT_ATTR_UNSPEC:
case __OVS_CT_ATTR_MAX:
OVS_NOT_REACHED();
}
}

conntrack_execute(&dp->conntrack, packets_, aux->flow->dl_type, commit,
zone, setmark, setlabel, helper);
break;
}
}

patch之后的,NAT部分已经有了实现,主要是拿到一些NAT的配置信息,实现依然是使用的现有的接口conntrack_execute

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
static void
dp_execute_cb(void *aux_, struct dp_packet_batch *packets_,
const struct nlattr *a, bool may_steal)
{
switch ((enum ovs_action_attr)type) {
case OVS_ACTION_ATTR_CT:
NL_ATTR_FOR_EACH_UNSAFE (b, left, nl_attr_get(a),
nl_attr_get_size(a)) {
enum ovs_ct_attr sub_type = nl_attr_type(b);
switch(sub_type) {
case OVS_CT_ATTR_NAT:
const struct nlattr *b_nest;
unsigned int left_nest;
bool ip_min_specified = false;
bool proto_num_min_specified = false;
bool ip_max_specified = false;
bool proto_num_max_specified = false;
memset(&nat_action_info, 0, sizeof nat_action_info);
nat_action_info_ref = &nat_action_info;

NL_NESTED_FOR_EACH_UNSAFE (b_nest, left_nest, b) {
enum ovs_nat_attr sub_type_nest = nl_attr_type(b_nest);

switch(sub_type_nest) {
case OVS_NAT_ATTR_SRC:
case OVS_NAT_ATTR_DST:
nat_config = true;
nat_action_info.nat_action |=
((sub_type_nest == OVS_NAT_ATTR_SRC)
? NAT_ACTION_SRC : NAT_ACTION_DST);
break;
case OVS_NAT_ATTR_IP_MIN:
memcpy(&nat_action_info.min_addr,
(char *) b_nest + NLA_HDRLEN, b_nest->nla_len);
ip_min_specified = true;
break;
case OVS_NAT_ATTR_IP_MAX:
memcpy(&nat_action_info.max_addr,
(char *) b_nest + NLA_HDRLEN, b_nest->nla_len);
ip_max_specified = true;
break;
case OVS_NAT_ATTR_PROTO_MIN:
nat_action_info.min_port = nl_attr_get_u16(b_nest);
proto_num_min_specified = true;
break;
case OVS_NAT_ATTR_PROTO_MAX:
nat_action_info.max_port = nl_attr_get_u16(b_nest);
proto_num_max_specified = true;
break;
//以上是NAT的ip和端口的配置,这里比较关键的是映射机制没有设置!!!
case OVS_NAT_ATTR_PERSISTENT:
case OVS_NAT_ATTR_PROTO_HASH:
case OVS_NAT_ATTR_PROTO_RANDOM:
break;
case OVS_NAT_ATTR_UNSPEC:
case __OVS_NAT_ATTR_MAX:
OVS_NOT_REACHED();
}
}

if (ip_min_specified && !ip_max_specified) {
memcpy(&nat_action_info.max_addr,
&nat_action_info.min_addr,
sizeof(nat_action_info.max_addr));
}
if (proto_num_min_specified && !proto_num_max_specified) {
nat_action_info.max_port = nat_action_info.min_port;
}
if (proto_num_min_specified || proto_num_max_specified) {
if (nat_action_info.nat_action & NAT_ACTION_SRC) {
nat_action_info.nat_action |= NAT_ACTION_SRC_PORT;
} else if (nat_action_info.nat_action & NAT_ACTION_DST) {
nat_action_info.nat_action |= NAT_ACTION_DST_PORT;
}
}
break;

case OVS_CT_ATTR_UNSPEC:
case __OVS_CT_ATTR_MAX:
OVS_NOT_REACHED();
}
}

if (nat_config && !commit) {
VLOG_WARN("NAT specified without commit.");
}

//这里可以看到NAT的配置信息是传递到这个接口了
conntrack_execute(&dp->conntrack, packets_, aux->flow->dl_type, commit,
zone, setmark, setlabel, helper,
nat_action_info_ref);
break;
}
}

conntrack_execute这里不列出来了,因为这块是代码的调整,后续可以细看一下为什么调整,主要就是将一些代码调整到函数process_one,而NAT的配置参数继续传递到了该函数,所以需要看一下。

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
static void process_one(struct conntrack *ct, struct dp_packet *pkt,
struct conn_lookup_ctx *ctx, uint16_t zone,
bool commit, long long now, const uint32_t *setmark,
const struct ovs_key_ct_labels *setlabel,
const struct nat_action_info_t *nat_action_info)
{
struct conn *conn;
unsigned bucket = hash_to_bucket(ctx->hash);
ct_lock_lock(&ct->buckets[bucket].lock);
conn_key_lookup(&ct->buckets[bucket], ctx, now);
conn = ctx->conn;
struct conn conn_for_un_nat_copy;
memset(&conn_for_un_nat_copy, 0, sizeof conn_for_un_nat_copy);

//这块的代码类似函数conn_key_extract
//然后执行的conn_key_lookup是取自上一级函数conntrack_execute
if (OVS_LIKELY(conn)) {
if (conn->conn_type == CT_CONN_TYPE_UN_NAT) {
ctx->reply = 1;

struct conn_lookup_ctx ctx2;
ctx2.conn = NULL;
ctx2.key = conn->rev_key;
ctx2.hash = conn_key_hash(&conn->rev_key, ct->hash_basis);

ct_lock_unlock(&ct->buckets[bucket].lock);
bucket = hash_to_bucket(ctx2.hash);

ct_lock_lock(&ct->buckets[bucket].lock);
conn_key_lookup(&ct->buckets[bucket], &ctx2, now);

if (ctx2.conn) {
conn = ctx2.conn;
} else {
/* It is a race condition where conn has timed out and removed
* between unlock of the rev_conn and lock of the forward conn;
* nothing to do. */
ct_lock_unlock(&ct->buckets[bucket].lock);
return;
}
}
}

bool create_new_conn = false;
if (OVS_LIKELY(conn)) {
//此处更新conntrack信息,是原来的操作
create_new_conn = conn_update_state(ct, pkt, ctx, &conn, now, bucket);
//这里的操作NAT的函数是新的接口,主要是对报文的NAT操作,需要重点关注
if (nat_action_info && !create_new_conn) {
handle_nat(pkt, conn, zone, ctx->reply);
}
} else {
if (ctx->related) {
pkt->md.ct_state = CS_INVALID;
} else {
create_new_conn = true;
}
}

if (OVS_UNLIKELY(create_new_conn)) {
//连接没有创建之前,上面的操作都没有,先是执行这里创建连接。
//该函数是之前就有的,用于创建新的连接的,但是现在增加了NAT参数,需要关注一下。
conn = conn_not_found(ct, pkt, ctx, commit, now, nat_action_info,
&conn_for_un_nat_copy);
}

write_ct_md(pkt, zone, conn ? conn->mark : 0,
conn ? conn->label : OVS_U128_ZERO);

//以下的两个操作也是取自上一级函数conntrack_execute
if (conn && setmark) {
set_mark(pkt, conn, setmark[0], setmark[1]);
}

if (conn && setlabel) {
set_label(pkt, conn, &setlabel[0], &setlabel[1]);
}

ct_lock_unlock(&ct->buckets[bucket].lock);

//此处也是新添加的操作,主要是创建反向NAT连接表,需要重点关注
if (conn_for_un_nat_copy.conn_type == CT_CONN_TYPE_UN_NAT) {
create_un_nat_conn(ct, &conn_for_un_nat_copy, now);
}
}

首先看最简单的conn_not_found,创建连接表

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
static struct conn *conn_not_found(struct conntrack *ct,
struct dp_packet *pkt, struct conn_lookup_ctx *ctx,
bool commit, long long now,
const struct nat_action_info_t *nat_action_info,
struct conn *conn_for_un_nat_copy)
{
unsigned bucket = hash_to_bucket(ctx->hash);
struct conn *nc = NULL;

if (!valid_new(pkt, &ctx->key)) {
pkt->md.ct_state = CS_INVALID;
return nc;
}
//更新conntrack状态为new
pkt->md.ct_state = CS_NEW;

if (commit) {
unsigned int n_conn_limit;

atomic_read_relaxed(&ct->n_conn_limit, &n_conn_limit);

if (atomic_count_get(&ct->n_conn) >= n_conn_limit) {
COVERAGE_INC(conntrack_full);
return nc;
}

nc = new_conn(&ct->buckets[bucket], pkt, &ctx->key, now);
ctx->conn = nc;
memcpy(&nc->rev_key, &nc->key, sizeof nc->rev_key);
conn_key_reverse(&nc->rev_key);

//由此以上没有变动,主要是创建连接,不同的协议调用不同的接口
//由此以下是新加的代码,主要是计算IP Port范围元组的hash值,感觉是用的HASH模式
//设置conn_type,然后调用nat_packet进行NAT转换,这个后面会说
//conntrack因为只记录了元组信息,所以正反向都在这个连接里面记录了key和rev_key就可以
//而NAT需要做操作,所以正反向的而连接针对的操作不一样,可以放在一个连接里面,
//也可以放在两个连接里面,而作者选择了放在两个连接里面,这里只是给NAT申请了正向连接
//反向连接是另外一个函数create_un_nat_conn做的
if (nat_action_info) {
nc->nat_info = xzalloc(sizeof *nat_action_info);
memcpy(nc->nat_info, nat_action_info, sizeof *nc->nat_info);
ct_rwlock_wrlock(&ct->nat_resources_lock);

//此处比较重要,到底NAT采用的是random还是hash还是其他映射方式由这里决定
bool nat_res = nat_select_range_tuple(ct, nc,
conn_for_un_nat_copy);

if (!nat_res) {
free(nc->nat_info);
nc->nat_info = NULL;
free (nc);
ct_rwlock_unlock(&ct->nat_resources_lock);
return NULL;
}

if (conn_for_un_nat_copy &&
nc->conn_type == CT_CONN_TYPE_DEFAULT) {
*nc = *conn_for_un_nat_copy;
conn_for_un_nat_copy->conn_type = CT_CONN_TYPE_UN_NAT;
}
ct_rwlock_unlock(&ct->nat_resources_lock);

//这个操作就是根据connection的规则对报文的IP Port进行修改
nat_packet(pkt, nc);
}

//由此以下没有变动,主要是添加connection信息到hash表中
hmap_insert(&ct->buckets[bucket].connections, &nc->node, ctx->hash);
atomic_count_inc(&ct->n_conn);
}
return nc;
}

nat_select_range_tuple主要是在nat连接创建的时候,确定连接需要修改的IP和Port

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
static bool nat_select_range_tuple(struct conntrack *ct, const struct conn *conn,
struct conn *nat_conn)
{
#define MIN_NAT_EPHEMERAL_PORT 1024
#define MAX_NAT_EPHEMERAL_PORT 65535

uint16_t min_port;
uint16_t max_port;
uint16_t first_port;

//根据几元组计算hash,主要是最小最大IP Port和3、4层协议类型以及zone
uint32_t hash = nat_range_hash(conn, ct->hash_basis);

if ((conn->nat_info->nat_action & NAT_ACTION_SRC) &&
(!(conn->nat_info->nat_action & NAT_ACTION_SRC_PORT))) {
//当只设置了IP SNAT的时候,port固定
min_port = ntohs(conn->key.src.port);
max_port = ntohs(conn->key.src.port);
first_port = min_port;
} else if ((conn->nat_info->nat_action & NAT_ACTION_DST) &&
(!(conn->nat_info->nat_action & NAT_ACTION_DST_PORT))) {
//当只设置了IP DNAT的时候,port固定
min_port = ntohs(conn->key.dst.port);
max_port = ntohs(conn->key.dst.port);
first_port = min_port;
} else {
//根据hash取出来一个Port作为第一个Port
uint16_t deltap = conn->nat_info->max_port - conn->nat_info->min_port;
uint32_t port_index = hash % (deltap + 1);
first_port = conn->nat_info->min_port + port_index;
min_port = conn->nat_info->min_port;
max_port = conn->nat_info->max_port;
}

uint32_t deltaa = 0;
uint32_t address_index;
struct ct_addr ct_addr;
memset(&ct_addr, 0, sizeof ct_addr);
struct ct_addr max_ct_addr;
memset(&max_ct_addr, 0, sizeof max_ct_addr);
max_ct_addr = conn->nat_info->max_addr;

if (conn->key.dl_type == htons(ETH_TYPE_IP)) {
deltaa = ntohl(conn->nat_info->max_addr.ipv4_aligned) -
ntohl(conn->nat_info->min_addr.ipv4_aligned);
address_index = hash % (deltaa + 1);
//根据hash计算出一个IP作为第一个IP
ct_addr.ipv4_aligned = htonl(
ntohl(conn->nat_info->min_addr.ipv4_aligned) + address_index);
} else {
deltaa = nat_ipv6_addrs_delta(&conn->nat_info->min_addr.ipv6_aligned,
&conn->nat_info->max_addr.ipv6_aligned);
/* deltaa must be within 32 bits for full hash coverage. A 64 or
* 128 bit hash is unnecessary and hence not used here. Most code
* is kept common with V4; nat_ipv6_addrs_delta() will do the
* enforcement via max_ct_addr. */
max_ct_addr = conn->nat_info->min_addr;
nat_ipv6_addr_increment(&max_ct_addr.ipv6_aligned, deltaa);

address_index = hash % (deltaa + 1);
ct_addr.ipv6_aligned = conn->nat_info->min_addr.ipv6_aligned;
nat_ipv6_addr_increment(&ct_addr.ipv6_aligned, address_index);
}

uint16_t port = first_port;
bool all_ports_tried = false;
bool original_ports_tried = false;
struct ct_addr first_addr = ct_addr;
*nat_conn = *conn;
while (true) {
//设置rev_key值需要修改的IP和Port,在nat_packet中使用
//通过循环,会根据IP和Port的范围完成每一项的设置
if (conn->nat_info->nat_action & NAT_ACTION_SRC) {
nat_conn->rev_key.dst.addr = ct_addr;
nat_conn->rev_key.dst.port = htons(port);
} else {
nat_conn->rev_key.src.addr = ct_addr;
nat_conn->rev_key.src.port = htons(port);
}

struct nat_conn_key_node *nat_conn_key_node =
nat_conn_keys_lookup(&ct->nat_conn_keys, &nat_conn->rev_key,
ct->hash_basis);

if (!nat_conn_key_node) {
//未找到时创建,说明没有该IP和端口未使用,直接返回
struct nat_conn_key_node *nat_conn_key =
xzalloc(sizeof *nat_conn_key);
memcpy(&nat_conn_key->key, &nat_conn->rev_key,
sizeof nat_conn_key->key);
memcpy(&nat_conn_key->value, &nat_conn->key,
sizeof nat_conn_key->value);
uint32_t nat_conn_key_hash = conn_key_hash(&nat_conn_key->key,
ct->hash_basis);
hmap_insert(&ct->nat_conn_keys, &nat_conn_key->node,
nat_conn_key_hash);
return true;
} else if (!all_ports_tried) {
//未遍历完所有的Port时调用
//Port一直自加1,知道最大之后,回到最小
//直到碰到被设置的第一个Port,遍历完成
if (min_port == max_port) {
all_ports_tried = true;
} else if (port == max_port) {
port = min_port;
} else {
port++;
}
if (port == first_port) {
all_ports_tried = true;
}
} else {
//前面都遍历完端口时会调用
if (memcmp(&ct_addr, &max_ct_addr, sizeof ct_addr)) {
//如果当前的IP不是最大IP,则进行++的操作
if (conn->key.dl_type == htons(ETH_TYPE_IP)) {
ct_addr.ipv4_aligned = htonl(
ntohl(ct_addr.ipv4_aligned) + 1);
} else {
nat_ipv6_addr_increment(&ct_addr.ipv6_aligned, 1);
}
} else {
//如果已经到了最大IP,则回到最小的IP
ct_addr = conn->nat_info->min_addr;
}
if (!memcmp(&ct_addr, &first_addr, sizeof ct_addr)) {
if (!original_ports_tried) {
original_ports_tried = true;
ct_addr = conn->nat_info->min_addr;
min_port = MIN_NAT_EPHEMERAL_PORT;
max_port = MAX_NAT_EPHEMERAL_PORT;
} else {
break;
}
}
first_port = min_port;
port = first_port;
all_ports_tried = false;
}
}
return false;
}

接下来看一下接口create_un_nat_conn,这个函数主要是创建反向NAT连接的数据结构

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
static void create_un_nat_conn(struct conntrack *ct,
struct conn *conn_for_un_nat_copy, long long now)
{
struct conn *nc = xzalloc(sizeof *nc);
memcpy(nc, conn_for_un_nat_copy, sizeof *nc);
//获取正向的connection的key值
nc->key = conn_for_un_nat_copy->rev_key;
nc->rev_key = conn_for_un_nat_copy->key;
uint32_t un_nat_hash = conn_key_hash(&nc->key, ct->hash_basis);
unsigned un_nat_conn_bucket = hash_to_bucket(un_nat_hash);
ct_lock_lock(&ct->buckets[un_nat_conn_bucket].lock);
ct_rwlock_rdlock(&ct->nat_resources_lock);

//这里首先查一下需要创建的这个连接的反向连接的信息,即正向连接的信息
//确保有正向连接,才接下来创建这个反向连接,和正向连接的创建类似
//这里也是新添加的一个函数,可以看一下
struct conn *rev_conn = conn_lookup(ct, &nc->key, now);

struct nat_conn_key_node *nat_conn_key_node =
nat_conn_keys_lookup(&ct->nat_conn_keys, &nc->key, ct->hash_basis);
if (nat_conn_key_node && !memcmp(&nat_conn_key_node->value,
&nc->rev_key, sizeof nat_conn_key_node->value) && !rev_conn) {

hmap_insert(&ct->buckets[un_nat_conn_bucket].connections,
&nc->node, un_nat_hash);
} else {
free(nc);
}
ct_rwlock_unlock(&ct->nat_resources_lock);
ct_lock_unlock(&ct->buckets[un_nat_conn_bucket].lock);
}

最后看一下函数handle_nat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void handle_nat(struct dp_packet *pkt, struct conn *conn,
uint16_t zone, bool reply)
{
//对ct_state的限定条件没看懂
if ((conn->nat_info) &&
(!(pkt->md.ct_state & (CS_SRC_NAT | CS_DST_NAT)) ||
(pkt->md.ct_state & (CS_SRC_NAT | CS_DST_NAT) &&
zone != pkt->md.ct_zone))){
if (pkt->md.ct_state & (CS_SRC_NAT | CS_DST_NAT)) {
pkt->md.ct_state &= ~(CS_SRC_NAT | CS_DST_NAT);
}
//一下就是正向和反向流量的NAT操作,
//主要是修改报文头的IP或者Port,重新计算校验
if (reply) {
un_nat_packet(pkt, conn);
} else {
nat_packet(pkt, conn);
}
}
}
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
107
static void nat_packet(struct dp_packet *pkt, const struct conn *conn)
{
//代码没怎么复用,所以有点多,粗略看一下
if (conn->nat_info->nat_action & NAT_ACTION_SRC) {
pkt->md.ct_state |= CS_SRC_NAT;
//修改源IP,这里面会重新计算校验和
//下面也都会重新计算,所以一次改IP和Port的时候会计算两次
if (conn->key.dl_type == htons(ETH_TYPE_IP)) {
struct ip_header *nh = dp_packet_l3(pkt);
packet_set_ipv4_addr(pkt, &nh->ip_src,
conn->rev_key.dst.addr.ipv4_aligned);
} else if (conn->key.dl_type == htons(ETH_TYPE_IPV6)) {
struct ovs_16aligned_ip6_hdr *nh6 = dp_packet_l3(pkt);
struct in6_addr ipv6_addr;
memcpy(&ipv6_addr, conn->rev_key.dst.addr.ipv6.be32,
sizeof ipv6_addr);
packet_set_ipv6_addr(pkt, conn->key.nw_proto,
nh6->ip6_src.be32, &ipv6_addr, true);
}

//修改源Port
if (conn->key.nw_proto == IPPROTO_TCP) {
struct tcp_header *th = dp_packet_l4(pkt);
packet_set_tcp_port(pkt, conn->rev_key.dst.port, th->tcp_dst);
} else if (conn->key.nw_proto == IPPROTO_UDP) {
struct udp_header *uh = dp_packet_l4(pkt);
packet_set_udp_port(pkt, conn->rev_key.dst.port, uh->udp_dst);
}
} else if (conn->nat_info->nat_action & NAT_ACTION_DST) {
pkt->md.ct_state |= CS_DST_NAT;

//修改目的IP
if (conn->key.dl_type == htons(ETH_TYPE_IP)) {
struct ip_header *nh = dp_packet_l3(pkt);
packet_set_ipv4_addr(pkt, &nh->ip_dst,
conn->rev_key.src.addr.ipv4_aligned);
} else if (conn->key.dl_type == htons(ETH_TYPE_IPV6)) {
struct ovs_16aligned_ip6_hdr *nh6 = dp_packet_l3(pkt);

struct in6_addr ipv6_addr;
memcpy(&ipv6_addr, conn->rev_key.dst.addr.ipv6.be32,
sizeof ipv6_addr);
packet_set_ipv6_addr(pkt, conn->key.nw_proto,
nh6->ip6_dst.be32, &ipv6_addr, true);
}
//修改目的Port
if (conn->key.nw_proto == IPPROTO_TCP) {
struct tcp_header *th = dp_packet_l4(pkt);
packet_set_tcp_port(pkt, th->tcp_src, conn->rev_key.src.port);
} else if (conn->key.nw_proto == IPPROTO_UDP) {
struct udp_header *uh = dp_packet_l4(pkt);
packet_set_udp_port(pkt, uh->udp_src, conn->rev_key.src.port);
}
}
}

static void
un_nat_packet(struct dp_packet *pkt, const struct conn *conn)
{
//跟前面差不多,这个是反向流的修改
//只需要注意这边的SRC和DST是要互换一下的
if (conn->nat_info->nat_action & NAT_ACTION_SRC) {
pkt->md.ct_state |= CS_SRC_NAT;
if (conn->key.dl_type == htons(ETH_TYPE_IP)) {
struct ip_header *nh = dp_packet_l3(pkt);
packet_set_ipv4_addr(pkt, &nh->ip_dst,
conn->key.src.addr.ipv4_aligned);
} else if (conn->key.dl_type == htons(ETH_TYPE_IPV6)) {
struct ovs_16aligned_ip6_hdr *nh6 = dp_packet_l3(pkt);
struct in6_addr ipv6_addr;
memcpy(&ipv6_addr, conn->key.src.addr.ipv6.be32,
sizeof ipv6_addr);
packet_set_ipv6_addr(pkt, conn->key.nw_proto,
nh6->ip6_dst.be32, &ipv6_addr, true);
}

if (conn->key.nw_proto == IPPROTO_TCP) {
struct tcp_header *th = dp_packet_l4(pkt);
packet_set_tcp_port(pkt, th->tcp_src, conn->key.src.port);
} else if (conn->key.nw_proto == IPPROTO_UDP) {
struct udp_header *uh = dp_packet_l4(pkt);
packet_set_udp_port(pkt, uh->udp_src, conn->key.src.port);
}
} else if (conn->nat_info->nat_action & NAT_ACTION_DST) {
pkt->md.ct_state |= CS_DST_NAT;
if (conn->key.dl_type == htons(ETH_TYPE_IP)) {
struct ip_header *nh = dp_packet_l3(pkt);
packet_set_ipv4_addr(pkt, &nh->ip_src,
conn->key.dst.addr.ipv4_aligned);
} else if (conn->key.dl_type == htons(ETH_TYPE_IPV6)) {
struct ovs_16aligned_ip6_hdr *nh6 = dp_packet_l3(pkt);
struct in6_addr ipv6_addr;
memcpy(&ipv6_addr, conn->key.dst.addr.ipv6.be32,
sizeof ipv6_addr);
packet_set_ipv6_addr(pkt, conn->key.nw_proto,
nh6->ip6_src.be32, &ipv6_addr, true);
}

if (conn->key.nw_proto == IPPROTO_TCP) {
struct tcp_header *th = dp_packet_l4(pkt);
packet_set_tcp_port(pkt, conn->key.dst.port, th->tcp_dst);
} else if (conn->key.nw_proto == IPPROTO_UDP) {
struct udp_header *uh = dp_packet_l4(pkt);
packet_set_udp_port(pkt, conn->key.dst.port, uh->udp_dst);
}
}
}

除去上面说的接口,还有其他一些地方的修改,这里简单的列一下

  • conntrack_init添加了创建记录NAT connection key值的hash表
  • conntrack_destroy删除上述hash表
  • nat_destroy主要是销毁指定的NAT配置,主要是删除上面的key的hash表,connection的hash表以及反向的key的hash表和connection的hash表
  • conn_update_state函数是更新conntrack的状态,原版的是在process_one实现的,patch单独提出来了,添加了删除connection的时候一起删除nat的操作nat_destroy
  • sweep_bucket主要是清楚超时的conntrack,也是添加了删除nat的操作nat_destroy
  • nat_select_range_tuple调用的一系列的接口我们不关注了
    • nat_ipv6_addrs_delta
    • nat_ipv6_addr_increment
    • nat_range_hash
  • nat_conn_keys_lookup比较简单,就是hash表中查找
  • nat_conn_keys_remove就是hash表删除
  • conntrack_flush主要是清空conntrack,这里添加了清空NAT的操作

注意:所有文章非特别说明皆为原创。为保证信息与源同步,转载时请务必注明文章出处!谢谢合作 :-)

原始链接:http://zhaozhanxu.com/2017/02/10/SDN/OVS/2017-02-10-conntrack/

许可协议: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。