Eli Britstein <[email protected]> writes:

> OVS conntrack NAT has three distinct behaviors at commit time,
> depending on the ct(commit,nat(...)) nest.  The kernel has implemented
> this since [1]:
>
>   ct(commit,nat)              no SRC/DST  ->  no bind on NEW flows
>   ct(commit,nat(src))         direction   ->  null binding (OVN marker).
>                                               Remap ports on reverse-tuple
>                                               collision only)
>   ct(commit,nat(dst))         direction   ->  null binding
>   ct(commit,nat(src|dst=...)) IP/port     ->  full bind (nf_nat_setup_info)

Looks like 4 behaviors but your commit claims 3.

> Prior userspace had only one path at commit (conn_not_found): any
> ct(commit,nat(...)) nest took the full-bind path.  That ignored no-bind
> and null-binding cases, and treated all-zero ranges as full binds.

Userspace supports null bind since 61e48c2d1db2 ("conntrack: Handle SNAT
with all-zero IP address.").  So this isn't correct.

Also this patch does a few things all at once, so it is difficult to
consider it properly.  For example, you introduce the
nat_log_tuple_exhaustion call and call it in one place.  But that is a
change all on its own independent of the rest of the changes.

This patch also needs quite a bit of documentation.  See
Documentation/ref/ovs-actions.7.rst - we need to make sure we adequately
distinguish between no bind, null bind, and full bind cases.  Maybe
Paolo has some opinions on a capabilities bit (like we did for
ZERO_IP_SNAT) so that OVN deployments know that this exists.

> [1] 
> https://github.com/torvalds/linux/blob/master/net/netfilter/nf_conntrack_core.c

Should point to a commit ID if you will say "since"

> Signed-off-by: Eli Britstein <[email protected]>
> ---
>  lib/conntrack.c         | 141 ++++++++++++++++++++++++++++++++++++----
>  tests/system-traffic.at | 129 ++++++++++++++++++++++++++++++++++++
>  2 files changed, 256 insertions(+), 14 deletions(-)
>
> diff --git a/lib/conntrack.c b/lib/conntrack.c
> index 168954c35..2ea70dd7d 100644
> --- a/lib/conntrack.c
> +++ b/lib/conntrack.c
> @@ -120,6 +120,16 @@ static bool
>  nat_get_unique_tuple(struct conntrack *ct, struct conn *conn,
>                       const struct nat_action_info_t *nat_info);
>  
> +static bool
> +nat_null_binding(struct conntrack *ct, struct conn *conn,
> +                 const struct nat_action_info_t *nat_info);
> +
> +static bool
> +nat_has_explicit_range(const struct nat_action_info_t *nat, ovs_be16 
> dl_type);
> +
> +static bool
> +nat_has_direction(const struct nat_action_info_t *nat);
> +
>  static uint8_t
>  reverse_icmp_type(uint8_t type);
>  static uint8_t
> @@ -1021,6 +1031,48 @@ ct_verify_helper(const char *helper, enum 
> ct_alg_ctl_type ct_alg_ctl)
>      }
>  }
>  
> +/* True when NAT defines an explicit IP/port range (vs direction-only).
> + * All-zero min with no distinct max is direction-only, not an explicit
> + * range. */
> +static bool
> +nat_has_explicit_range(const struct nat_action_info_t *nat, ovs_be16 dl_type)

This isn't really needed - we already check for an explicit range when
doing the min check:

                    case OVS_NAT_ATTR_IP_MIN:
                        memcpy(&nat_action_info.min_addr,
                               nl_attr_get(b_nest),
                               nl_attr_get_size(b_nest));
                        ip_min_specified = true;
                        break;
                    case OVS_NAT_ATTR_IP_MAX:
                        memcpy(&nat_action_info.max_addr,
                               nl_attr_get(b_nest),
                               nl_attr_get_size(b_nest));
                        ip_max_specified = true;
                        break;

Probably just better to use a flag (this is also the way kernel does it)
and then drop most of this function into just a bit test.  We have the
min/max specified bools locally in dpif-netdev.c so we can just expose
something that says the range is included.

> +{
> +    if (!nat) {
> +        return false;
> +    }
> +
> +    if (nat->min_port || nat->max_port) {
> +        return true;
> +    }
> +
> +    if (dl_type == htons(ETH_TYPE_IP)) {
> +        return nat->min_addr.ipv4 != 0
> +               || nat->max_addr.ipv4 != nat->min_addr.ipv4;
> +    } else if (dl_type == htons(ETH_TYPE_IPV6)) {
> +        return !ipv6_mask_is_any(&nat->min_addr.ipv6)
> +               || (!ipv6_mask_is_any(&nat->max_addr.ipv6)
> +                   && memcmp(&nat->max_addr.ipv6, &nat->min_addr.ipv6,
> +                             sizeof nat->max_addr.ipv6));
> +    }
> +
> +    return false;
> +}
> +
> +static bool
> +nat_has_direction(const struct nat_action_info_t *nat)

nat_action can also carry port bits - today they are only filled when
SRC|DST actions are specified.  But if a future bit comes along that
gets stuffed into there, it will be wrong.  Should mask with
(NAT_ACTION_SRC | NAT_ACTION_DST)

> +{
> +    return nat && nat->nat_action;
> +}
> +
> +static void
> +nat_log_tuple_exhaustion(void)
> +{
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
> +
> +    VLOG_WARN_RL(&rl, "Unable to NAT due to tuple space exhaustion - "
> +                 "if DoS attack, use firewalling and/or zone partitioning.");
> +}
> +
>  static struct conn *
>  conn_not_found(struct conntrack *ct, struct dp_packet *pkt,
>                 struct conn_lookup_ctx *ctx, bool commit, long long now,
> @@ -1095,8 +1147,6 @@ conn_not_found(struct conntrack *ct, struct dp_packet 
> *pkt,
>          }
>  
>          if (nat_action_info) {
> -            nc->nat_action = nat_action_info->nat_action;
> -
>              if (alg_exp) {
>                  if (alg_exp->nat_rpl_dst) {
>                      rev_key_node->key.dst.addr = alg_exp->alg_nat_repl_addr;
> @@ -1105,18 +1155,28 @@ conn_not_found(struct conntrack *ct, struct dp_packet 
> *pkt,
>                      rev_key_node->key.src.addr = alg_exp->alg_nat_repl_addr;
>                      nc->nat_action = NAT_ACTION_DST;
>                  }
> -            } else {
> -                bool nat_res = nat_get_unique_tuple(ct, nc, nat_action_info);
> -                if (!nat_res) {
> +            } else if (nat_has_explicit_range(nat_action_info,
> +                                              fwd_key_node->key.dl_type)
> +                       && nat_has_direction(nat_action_info)) {
> +                nc->nat_action = nat_action_info->nat_action;
> +                if (!nat_get_unique_tuple(ct, nc, nat_action_info)) {
> +                    goto nat_res_exhaustion;
> +                }
> +            } else if (nat_has_direction(nat_action_info)) {
> +                nc->nat_action |= nat_action_info->nat_action
> +                                  & (NAT_ACTION_SRC | NAT_ACTION_DST);
> +                if (!nat_null_binding(ct, nc, nat_action_info)) {

These two different cases of resource exhaustion might need something
to distinguish them for a user.

>                      goto nat_res_exhaustion;
>                  }
>              }
>  
> -            nat_packet(pkt, nc, false, ctx->icmp_related);
> -            uint32_t rev_hash = conn_key_hash(&rev_key_node->key,
> -                                              ct->hash_basis);
> -            cmap_insert(&ct->conns[ctx->key.zone],
> -                        &rev_key_node->cm_node, rev_hash);
> +            if (nc->nat_action) {
> +                nat_packet(pkt, nc, false, ctx->icmp_related);
> +                uint32_t rev_hash = conn_key_hash(&rev_key_node->key,
> +                                                  ct->hash_basis);
> +                cmap_insert(&ct->conns[ctx->key.zone],
> +                            &rev_key_node->cm_node, rev_hash);
> +            }
>          }
>  
>          cmap_insert(&ct->conns[ctx->key.zone],
> @@ -1140,9 +1200,7 @@ conn_not_found(struct conntrack *ct, struct dp_packet 
> *pkt,
>       * can limit DoS impact. */
>  nat_res_exhaustion:
>      delete_conn__(nc);
> -    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 5);
> -    VLOG_WARN_RL(&rl, "Unable to NAT due to tuple space exhaustion - "
> -                 "if DoS attack, use firewalling and/or zone partitioning.");
> +    nat_log_tuple_exhaustion();
>      return NULL;
>  }
>  
> @@ -1229,7 +1287,7 @@ check_orig_tuple(struct conntrack *ct, struct dp_packet 
> *pkt,
>           !pkt->md.ct_orig_tuple.ipv4.ipv4_proto) ||
>          (ctx_in->key.dl_type == htons(ETH_TYPE_IPV6) &&
>           !pkt->md.ct_orig_tuple.ipv6.ipv6_proto) ||
> -        nat_action_info) {
> +        nat_has_explicit_range(nat_action_info, ctx_in->key.dl_type)) {
>          return false;
>      }
>  
> @@ -2579,6 +2637,61 @@ another_round:
>      return false;
>  }
>  
> +/* Returns true if no remapping is needed or remapping succeeded.
> + * Returns false if a reverse-tuple collision was detected but a unique
> + * L4 port could not be allocated (tuple-space exhaustion). */
> +static bool
> +nat_null_binding(struct conntrack *ct, struct conn *conn,
> +                 const struct nat_action_info_t *nat_info)
> +{

This is far too similar to nat_get_unique_tuple().  I think it isn't
needed.

> +    struct conn_key *fwd_key = &conn->key_node[CT_DIR_FWD].key;
> +    struct conn_key *rev_key = &conn->key_node[CT_DIR_REV].key;
> +    bool pat_proto = fwd_key->nw_proto == IPPROTO_TCP ||
> +                     fwd_key->nw_proto == IPPROTO_UDP ||
> +                     fwd_key->nw_proto == IPPROTO_SCTP ||
> +                     fwd_key->nw_proto == IPPROTO_ICMP;
> +    uint16_t min_sport, max_sport, curr_sport;
> +
> +    if (!pat_proto) {
> +        return true;
> +    }
> +
> +    /* Remap ports only when the reverse tuple collides with an existing
> +     * connection. */
> +    {
> +        struct conn *collision = NULL;
> +
> +        if (!conn_lookup(ct, rev_key, time_msec(), &collision, NULL)) {
> +            return true;
> +        }
> +
> +        if (collision == conn) {
> +            return true;
> +        }
> +    }
> +
> +    if (nat_info->nat_action & (NAT_ACTION_SRC | NAT_ACTION_DST)) {
> +        uint16_t direction = nat_info->nat_action
> +                             & (NAT_ACTION_SRC | NAT_ACTION_DST);
> +
> +        conn->nat_action |= direction;
> +
> +        set_sport_range(nat_info, fwd_key, 0, &curr_sport,
> +                        &min_sport, &max_sport);
> +        if (!nat_get_unique_l4(ct, rev_key, &rev_key->dst.port,
> +                               rev_key->nw_proto == IPPROTO_ICMP
> +                               ? &rev_key->src.port : NULL,
> +                               curr_sport, min_sport, max_sport)) {
> +            return false;
> +        }
> +
> +        conn->nat_action |= NAT_ACTION_SRC_PORT;
> +        return true;
> +    }
> +
> +    return true;
> +}
> +
>  /* This function tries to get a unique tuple.
>   * Every iteration checks that the reverse tuple doesn't
>   * collide with any existing one.

NOTE: I didn't evaluate the tests, but thanks for including them with
compose-packet rather than some packet bytes.

> diff --git a/tests/system-traffic.at b/tests/system-traffic.at
> index 4ad51223d..b34da49f9 100644
> --- a/tests/system-traffic.at
> +++ b/tests/system-traffic.at
> @@ -4719,6 +4719,135 @@ n_packets=0
>  OVS_TRAFFIC_VSWITCHD_STOP
>  AT_CLEANUP
>  
> +AT_SETUP([conntrack - empty nat on first ct(commit)])
> +CHECK_CONNTRACK()
> +CHECK_CONNTRACK_NAT()
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_NAMESPACES(at_ns0)
> +ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
> +
> +AT_DATA([flows.txt], [dnl
> +table=0,priority=100,in_port=1,udp,actions=ct(commit,nat,table=2)
> +table=2,priority=0,actions=drop
> +])
> +
> +AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
> +AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> +
> +flow_l3="eth_src=50:54:00:00:00:09,eth_dst=50:54:00:00:00:0a,dl_type=0x0800,nw_src=10.1.1.1,nw_dst=10.1.1.2,nw_proto=17,nw_ttl=64,nw_frag=no"
> +
> +AT_CHECK([syn_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> udp_src=12345,udp_dst=5201"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          "in_port=1,packet=${syn_pkt},actions=resubmit(,0)"])
> +
> +OVS_WAIT_UNTIL([ovs-appctl dpctl/dump-conntrack | grep -q 
> "orig=.src=10\.1\.1\.1,"])
> +
> +AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep "orig=.src=10\.1\.1\.1,"], 
> [0], [dnl
> +udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=12345,dport=5201),reply=(src=10.1.1.2,dst=10.1.1.1,sport=5201,dport=12345)
> +])
> +
> +OVS_TRAFFIC_VSWITCHD_STOP
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack - nat(src) null binding collision])
> +CHECK_CONNTRACK()
> +CHECK_CONNTRACK_NAT()
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_NAMESPACES(at_ns0)
> +ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
> +
> +AT_DATA([flows.txt], [dnl
> +table=0,priority=100,in_port=1,udp,actions=ct(table=1,nat)
> +table=1,cookie=0x1,priority=200,udp,nw_dst=172.1.1.2,tp_dst=80,ct_state=+new+trk,actions=ct(commit,nat(dst=10.1.1.2:80),table=2)
> +table=1,cookie=0x2,priority=200,udp,nw_dst=10.1.1.2,tp_dst=80,ct_state=+new+trk,actions=ct(commit,nat(src),table=2)
> +table=1,priority=0,actions=drop
> +table=2,priority=0,actions=drop
> +])
> +
> +AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
> +AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> +
> +flow_l3="eth_src=50:54:00:00:00:09,eth_dst=50:54:00:00:00:0a,dl_type=0x0800,nw_src=10.1.1.1,nw_proto=17,nw_ttl=64,nw_frag=no"
> +
> +AT_CHECK([dnat_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> nw_dst=172.1.1.2, udp_src=30001,udp_dst=80"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          "in_port=1,packet=${dnat_pkt},actions=resubmit(,0)"])
> +
> +OVS_WAIT_UNTIL([ovs-appctl dpctl/dump-conntrack | grep -q 
> "orig=.src=10\.1\.1\.1,dst=172\.1\.1\.2,"])
> +
> +AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep 
> "orig=.src=10\.1\.1\.1,dst=172\.1\.1\.2,"], [0], [dnl
> +udp,orig=(src=10.1.1.1,dst=172.1.1.2,sport=30001,dport=80),reply=(src=10.1.1.2,dst=10.1.1.1,sport=80,dport=30001)
> +])
> +
> +AT_CHECK([plain_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> nw_dst=10.1.1.2, udp_src=30001,udp_dst=80"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          "in_port=1,packet=${plain_pkt},actions=resubmit(,0)"])
> +
> +AT_CHECK([sh -c 'line=$(ovs-appctl dpctl/dump-conntrack | grep 
> "orig=(src=10.1.1.1,dst=10.1.1.2,sport=30001,dport=80)"); test -n "$line"; 
> echo "$line" | grep -q "reply=(src=10.1.1.2,dst=10.1.1.1,sport=80,dport="; 
> echo "$line" | grep -vq ",dport=30001)"'])
> +
> +AT_CHECK([plain_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> nw_dst=10.1.1.2, udp_src=30001,udp_dst=80"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          
> "in_port=1,packet=${plain_pkt},actions=ct(commit,zone=1,nat,table=2)"])
> +
> +AT_CHECK([ovs-appctl dpctl/dump-conntrack zone=1 | grep 
> "orig=.src=10\.1\.1\.1,"], [0], [dnl
> +udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=30001,dport=80),reply=(src=10.1.1.2,dst=10.1.1.1,sport=80,dport=30001),zone=1
> +])
> +
> +OVS_TRAFFIC_VSWITCHD_STOP
> +AT_CLEANUP
> +
> +AT_SETUP([conntrack - nat(dst) null binding collision])
> +CHECK_CONNTRACK()
> +CHECK_CONNTRACK_NAT()
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_NAMESPACES(at_ns0)
> +ADD_VETH(p0, at_ns0, br0, "10.1.1.1/24")
> +
> +AT_DATA([flows.txt], [dnl
> +table=0,priority=100,in_port=1,udp,actions=ct(table=1,nat)
> +table=1,cookie=0x1,priority=200,udp,nw_dst=172.1.1.2,tp_dst=80,ct_state=+new+trk,actions=ct(commit,nat(dst=10.1.1.2:80),table=2)
> +table=1,cookie=0x2,priority=200,udp,nw_dst=10.1.1.2,tp_dst=80,ct_state=+new+trk,actions=ct(commit,nat(dst),table=2)
> +table=1,priority=0,actions=drop
> +table=2,priority=0,actions=drop
> +])
> +
> +AT_CHECK([ovs-ofctl --bundle add-flows br0 flows.txt])
> +AT_CHECK([ovs-appctl dpctl/flush-conntrack])
> +
> +flow_l3="eth_src=50:54:00:00:00:09,eth_dst=50:54:00:00:00:0a,dl_type=0x0800,nw_src=10.1.1.1,nw_proto=17,nw_ttl=64,nw_frag=no"
> +
> +AT_CHECK([dnat_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> nw_dst=172.1.1.2, udp_src=30001,udp_dst=80"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          "in_port=1,packet=${dnat_pkt},actions=resubmit(,0)"])
> +
> +OVS_WAIT_UNTIL([ovs-appctl dpctl/dump-conntrack | grep -q 
> "orig=.src=10\.1\.1\.1,dst=172\.1\.1\.2,"])
> +
> +AT_CHECK([ovs-appctl dpctl/dump-conntrack | grep 
> "orig=.src=10\.1\.1\.1,dst=172\.1\.1\.2,"], [0], [dnl
> +udp,orig=(src=10.1.1.1,dst=172.1.1.2,sport=30001,dport=80),reply=(src=10.1.1.2,dst=10.1.1.1,sport=80,dport=30001)
> +])
> +
> +AT_CHECK([plain_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> nw_dst=10.1.1.2, udp_src=30001,udp_dst=80"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          "in_port=1,packet=${plain_pkt},actions=resubmit(,0)"])
> +
> +AT_CHECK([sh -c 'line=$(ovs-appctl dpctl/dump-conntrack | grep 
> "orig=(src=10.1.1.1,dst=10.1.1.2,sport=30001,dport=80)"); test -n "$line"; 
> echo "$line" | grep -q "reply=(src=.*,dst=10.1.1.1,sport=80,dport="; echo 
> "$line" | grep -vq ",dport=30001)"'])
> +
> +AT_CHECK([plain_pkt=$(ovs-ofctl compose-packet --bare "$flow_l3, 
> nw_dst=10.1.1.2, udp_src=30001,udp_dst=80"); dnl
> +          ovs-ofctl -O OpenFlow13 packet-out br0 dnl
> +          
> "in_port=1,packet=${plain_pkt},actions=ct(commit,zone=1,nat,table=2)"])
> +
> +AT_CHECK([ovs-appctl dpctl/dump-conntrack zone=1 | grep 
> "orig=.src=10\.1\.1\.1,"], [0], [dnl
> +udp,orig=(src=10.1.1.1,dst=10.1.1.2,sport=30001,dport=80),reply=(src=10.1.1.2,dst=10.1.1.1,sport=80,dport=30001),zone=1
> +])
> +
> +dnl The kernel datapath rejects bare ct commit with nat dst and EINVAL.
> +OVS_TRAFFIC_VSWITCHD_STOP(["dnl
> +/execute ct.*Invalid argument/d"])
> +AT_CLEANUP
> +
>  AT_SETUP([conntrack - generic IP protocol])
>  CHECK_CONNTRACK()
>  OVS_TRAFFIC_VSWITCHD_START()

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to