这里只是捡重点的说,需要原版去看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的操作
最后修改:2021 年 08 月 18 日
如果觉得我的文章对你有用,请随意赞赏