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)

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.

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

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)
+{
+    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)
+{
+    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)) {
                     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)
+{
+    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.
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()
-- 
2.43.0

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

Reply via email to