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