nat: fix accidental o2i deletion/reuse
Nat session is allocated before the port allocation. During port allocation
candidate address+port are set to o2i 6-tuple and tested against the flow hash.
If insertion fails, the port is busy and rejected. When all N attempts are
unsuccessful, "out-of-ports" error is recorded and the session is to be
deleted.
During session deletion o2i and i2o tuples are deleted from the flow hash.
In case of "out-of-ports" i2o tuple is not valid, however o2i is and it refers
to **some other** session that's known to be allocated.
By backing match tuple up session should be invalidated well enough not to
collide with any valid one.
Type: fix
Signed-off-by: Dmitry Valter <d-valter@yandex-team.ru>
Change-Id: Id30be6f26ecce7a5a63135fb971bb65ce318af82
diff --git a/src/plugins/nat/nat44-ed/nat44_ed_in2out.c b/src/plugins/nat/nat44-ed/nat44_ed_in2out.c
index f41fcac..deec009 100644
--- a/src/plugins/nat/nat44-ed/nat44_ed_in2out.c
+++ b/src/plugins/nat/nat44-ed/nat44_ed_in2out.c
@@ -105,6 +105,9 @@
const u16 port_thread_offset =
(port_per_thread * snat_thread_index) + ED_USER_PORT_OFFSET;
+ /* Backup original match in case of failure */
+ const nat_6t_t match = s->o2i.match;
+
s->o2i.match.daddr = a->addr;
/* first try port suggested by caller */
u16 port = clib_net_to_host_u16 (*outside_port);
@@ -136,6 +139,9 @@
--attempts;
}
while (attempts > 0);
+
+ /* Revert match */
+ s->o2i.match = match;
return 1;
}
diff --git a/test/test_nat44_ed.py b/test/test_nat44_ed.py
index 8a8c968..7279b6a 100644
--- a/test/test_nat44_ed.py
+++ b/test/test_nat44_ed.py
@@ -4,6 +4,7 @@
from io import BytesIO
from random import randint, choice
+import re
import scapy.compat
from framework import tag_fixme_ubuntu2204, is_distro_ubuntu2204
from framework import VppTestCase, VppTestRunner, VppLoInterface
@@ -48,8 +49,9 @@
if not self.vpp_dead:
self.plugin_disable()
- def plugin_enable(self):
- self.vapi.nat44_ed_plugin_enable_disable(sessions=self.max_sessions, enable=1)
+ def plugin_enable(self, max_sessions=None):
+ max_sessions = max_sessions or self.max_sessions
+ self.vapi.nat44_ed_plugin_enable_disable(sessions=max_sessions, enable=1)
def plugin_disable(self):
self.vapi.nat44_ed_plugin_enable_disable(enable=0)
@@ -616,6 +618,16 @@
return pkts
+ def create_udp_stream(self, in_if, out_if, count, base_port=6303):
+ return [
+ (
+ Ether(dst=in_if.local_mac, src=in_if.remote_mac)
+ / IP(src=in_if.remote_ip4, dst=out_if.remote_ip4, ttl=64)
+ / UDP(sport=base_port + i, dport=20)
+ )
+ for i in range(count)
+ ]
+
def create_stream_frag(
self, src_if, dst, sport, dport, data, proto=IP_PROTOS.tcp, echo_reply=False
):
@@ -4821,6 +4833,65 @@
)
self.send_and_expect(self.pg0, p, self.pg1)
+ def test_dynamic_ports_exhausted(self):
+ """NAT44ED dynamic translation test: address ports exhaused"""
+
+ sessions_per_batch = 128
+ n_available_ports = 65536 - 1024
+ n_sessions = n_available_ports + 2 * sessions_per_batch
+
+ # set high enough session limit for ports to be exhausted
+ self.plugin_disable()
+ self.plugin_enable(max_sessions=n_sessions)
+
+ self.nat_add_inside_interface(self.pg0)
+ self.nat_add_outside_interface(self.pg1)
+
+ # set timeouts to high for sessions to reallistically expire
+ config = self.vapi.nat44_show_running_config()
+ old_timeouts = config.timeouts
+ self.vapi.nat_set_timeouts(
+ udp=21600,
+ tcp_established=old_timeouts.tcp_established,
+ tcp_transitory=old_timeouts.tcp_transitory,
+ icmp=old_timeouts.icmp,
+ )
+
+ # in2out after NAT addresses added
+ self.nat_add_address(self.nat_addr)
+
+ for i in range(n_sessions // sessions_per_batch):
+ pkts = self.create_udp_stream(
+ self.pg0,
+ self.pg1,
+ sessions_per_batch,
+ base_port=i * sessions_per_batch + 100,
+ )
+
+ self.pg0.add_stream(pkts)
+ self.pg_start()
+
+ err = self.statistics.get_err_counter(
+ "/err/nat44-ed-in2out-slowpath/out of ports"
+ )
+ if err > sessions_per_batch:
+ break
+
+ # Check for ports to be used no more than once
+ ports = set()
+ sessions = self.vapi.cli("show nat44 sessions")
+ rx = re.compile(
+ f" *o2i flow: match: saddr {self.pg1.remote_ip4} sport [0-9]+ daddr {self.nat_addr} dport ([0-9]+) proto UDP.*"
+ )
+ for line in sessions.splitlines():
+ m = rx.match(line)
+ if m:
+ port = int(m.groups()[0])
+ self.assertNotIn(port, ports)
+ ports.add(port)
+
+ self.assertGreaterEqual(err, sessions_per_batch)
+
if __name__ == "__main__":
unittest.main(testRunner=VppTestRunner)