OVS conntrack NAT has three distinct binding behaviors at commit time,
expressed through four ct(commit,nat(...)) action forms.  This mirrors
the kernel's nf_nat_setup_info() handling in nf_conntrack_core.c:

  ct(commit,nat)              no SRC/DST  ->  no bind on NEW flows
  ct(commit,nat(src))         direction   ->  null binding.  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)

The userspace commit path (conn_not_found) routed every ct(commit,nat(...))
nest through the full-bind tuple selection.  A bare ct(commit,nat) has no
direction and no address to bind, so this produced an invalid reverse
tuple instead of leaving the flow unbound.

Select the binding behavior explicitly from whether a direction and/or an
explicit IP/port range is present, so that the no-bind, null-binding, and
full-bind cases each take their intended path, matching the kernel.

Signed-off-by: Eli Britstein <[email protected]>
---
 NEWS                    |   3 +
 lib/conntrack.c         |  87 ++++++++++++++++++++++-----
 tests/system-traffic.at | 129 ++++++++++++++++++++++++++++++++++++++++
 3 files changed, 205 insertions(+), 14 deletions(-)

diff --git a/NEWS b/NEWS
index 9bec75b1b..23c1eb3eb 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,9 @@ Post-v4.0.0
        +new+trk packet recirculated through a second zone is no longer
        translated by a bare ct(nat) action; an explicit src or dst NAT action
        is now required to translate a new packet.
+     * A bare ct(commit,nat) action (no src/dst direction and no address or
+       port range) now commits the connection without a NAT binding instead of
+       installing an invalid reverse tuple, matching the Linux kernel datapath.
 
 
 v4.0.0 - 17 Aug 2026
diff --git a/lib/conntrack.c b/lib/conntrack.c
index 168954c35..f7eecd24f 100644
--- a/lib/conntrack.c
+++ b/lib/conntrack.c
@@ -118,7 +118,10 @@ static void *clean_thread_main(void *f_);
 
 static bool
 nat_get_unique_tuple(struct conntrack *ct, struct conn *conn,
-                     const struct nat_action_info_t *nat_info);
+                     const struct nat_action_info_t *nat_info, bool null_bind);
+
+static bool
+nat_has_direction(const struct nat_action_info_t *nat);
 
 static uint8_t
 reverse_icmp_type(uint8_t type);
@@ -1021,6 +1024,22 @@ ct_verify_helper(const char *helper, enum 
ct_alg_ctl_type ct_alg_ctl)
     }
 }
 
+/* True when NAT defines an explicit IP and/or L4 port range, as opposed to a
+ * direction-only nat(src)/nat(dst). */
+static bool
+nat_has_explicit_range(const struct nat_action_info_t *nat)
+{
+    return nat && (nat->min_port || nat->max_port
+                   || !is_all_zeros(&nat->min_addr, sizeof nat->min_addr)
+                   || !is_all_zeros(&nat->max_addr, sizeof nat->max_addr));
+}
+
+static bool
+nat_has_direction(const struct nat_action_info_t *nat)
+{
+    return nat && (nat->nat_action & (NAT_ACTION_SRC | NAT_ACTION_DST));
+}
+
 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 +1114,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 +1122,27 @@ 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)
+                       && nat_has_direction(nat_action_info)) {
+                nc->nat_action = nat_action_info->nat_action;
+                if (!nat_get_unique_tuple(ct, nc, nat_action_info, false)) {
+                    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_get_unique_tuple(ct, nc, nat_action_info, true)) {
                     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],
@@ -1229,7 +1255,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)) {
         return false;
     }
 
@@ -2599,10 +2625,16 @@ another_round:
  *          tries to find a source port in the ephemeral
  *          range (after testing the port used by the sender).
  *
- * If none can be found, return exhaustion to the caller. */
+ * If none can be found, return exhaustion to the caller.
+ *
+ * When 'null_bind' is true, a direction-only nat(src)/nat(dst) without an
+ * explicit range is requested.  In that case the original tuple is kept
+ * unchanged unless the reverse tuple collides with an existing connection, in
+ * which case only the L4 port is remapped (matching the kernel's null
+ * binding). */
 static bool
 nat_get_unique_tuple(struct conntrack *ct, struct conn *conn,
-                     const struct nat_action_info_t *nat_info)
+                     const struct nat_action_info_t *nat_info, bool null_bind)
 {
     struct conn_key *fwd_key = &conn->key_node[CT_DIR_FWD].key;
     struct conn_key *rev_key = &conn->key_node[CT_DIR_REV].key;
@@ -2615,6 +2647,33 @@ nat_get_unique_tuple(struct conntrack *ct, struct conn 
*conn,
     union ct_addr min_addr, max_addr, addr;
     uint32_t hash, port_off, basis;
 
+    if (null_bind) {
+        struct conn *collision = NULL;
+
+        if (!pat_proto) {
+            return true;
+        }
+
+        /* Remap ports only when the reverse tuple collides with an existing
+         * connection. */
+        if (!conn_lookup(ct, rev_key, time_msec(), &collision, NULL)
+            || collision == conn) {
+            return true;
+        }
+
+        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;
+    }
+
     memset(&min_addr, 0, sizeof min_addr);
     memset(&max_addr, 0, sizeof max_addr);
     memset(&addr, 0, sizeof addr);
diff --git a/tests/system-traffic.at b/tests/system-traffic.at
index 2f6128ce6..254332afb 100644
--- a/tests/system-traffic.at
+++ b/tests/system-traffic.at
@@ -4720,6 +4720,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