这里只是捡重点的说,需要原版去看ovs-ofctl(8)。
这里只关注OpenvSwitch的DPDK相关的conntrack和NAT。
conntrack匹配项说明
match项表示为ct_state=flags/mask或者ct_state=+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)
通过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) 在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][,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的连接:
应用层流表
//优先级最低,丢包
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发起连接
//一直持续,主要是将报文纳入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发起连接
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:
应用层流表
//优先级最低,不管
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流表
//一直持续,主要是将报文纳入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是没有支持的
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
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的配置参数继续传递到了该函数,所以需要看一下。
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
,创建连接表
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
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连接的数据结构
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
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);
}
}
}
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的操作
1 条评论
厉害