nat: avoid hairpinning infinite loop problem 84/30284/3
authorElias Rudberg <elias.rudberg@bahnhof.net>
Fri, 4 Dec 2020 18:32:55 +0000 (19:32 +0100)
committerOle Tr�an <otroan@employees.org>
Tue, 8 Dec 2020 08:49:24 +0000 (08:49 +0000)
Fix in nat44 hairpinning code to check if anything was actually
changed in the snat_hairpinning() routine, and return 0 if nothing
changed. This helps avoid an infinite loop repeating the three
nodes nat44-hairpinning-->ip4-lookup-->ip4-local in case there
was no change. Also add a corresponding test case.

Type: fix

Signed-off-by: Elias Rudberg <elias.rudberg@bahnhof.net>
Change-Id: I95f48476bd002ac4c6789afe504681f1963e5d38
Signed-off-by: Elias Rudberg <elias.rudberg@bahnhof.net>
src/plugins/nat/nat44_hairpinning.c
src/plugins/nat/test/test_nat44.py

index 9eadcf3..2859046 100644 (file)
@@ -108,6 +108,7 @@ snat_hairpinning (vlib_main_t * vm, vlib_node_runtime_t * node,
   ip4_address_t sm0_addr;
   u16 sm0_port;
   u32 sm0_fib_index;
+  u32 old_sw_if_index = vnet_buffer (b0)->sw_if_index[VLIB_TX];
   /* Check if destination is static mappings */
   if (!snat_static_mapping_match
       (sm, ip0->dst_address, udp0->dst_port, sm->outside_fib_index, proto0,
@@ -159,6 +160,17 @@ snat_hairpinning (vlib_main_t * vm, vlib_node_runtime_t * node,
       vnet_buffer (b0)->sw_if_index[VLIB_TX] = s0->in2out.fib_index;
     }
 
+  /* Check if anything has changed and if not, then return 0. This
+     helps avoid infinite loop, repeating the three nodes
+     nat44-hairpinning-->ip4-lookup-->ip4-local, in case nothing has
+     changed. */
+  old_dst_addr0 = ip0->dst_address.as_u32;
+  old_dst_port0 = tcp0->dst;
+  if (new_dst_addr0 == old_dst_addr0
+      && new_dst_port0 == old_dst_port0
+      && vnet_buffer (b0)->sw_if_index[VLIB_TX] == old_sw_if_index)
+    return 0;
+
   /* Destination is behind the same NAT, use internal address and port */
   if (new_dst_addr0)
     {
index c71b706..0dcf6d5 100644 (file)
@@ -2374,6 +2374,94 @@ class TestNAT44(MethodHolder):
                 self.logger.error(ppp("Unexpected or invalid packet:", packet))
                 raise
 
+    def test_hairpinning_avoid_inf_loop(self):
+        """ NAT44 hairpinning - 1:1 NAPT avoid infinite loop """
+
+        host = self.pg0.remote_hosts[0]
+        server = self.pg0.remote_hosts[1]
+        host_in_port = 1234
+        host_out_port = 0
+        server_in_port = 5678
+        server_out_port = 8765
+
+        self.nat44_add_address(self.nat_addr)
+        flags = self.config_flags.NAT_IS_INSIDE
+        self.vapi.nat44_interface_add_del_feature(
+            sw_if_index=self.pg0.sw_if_index,
+            flags=flags, is_add=1)
+        self.vapi.nat44_interface_add_del_feature(
+            sw_if_index=self.pg1.sw_if_index,
+            is_add=1)
+
+        # add static mapping for server
+        self.nat44_add_static_mapping(server.ip4, self.nat_addr,
+                                      server_in_port, server_out_port,
+                                      proto=IP_PROTOS.tcp)
+
+        # add another static mapping that maps pg0.local_ip4 address to itself
+        self.nat44_add_static_mapping(self.pg0.local_ip4, self.pg0.local_ip4)
+
+        # send packet from host to VPP (the packet should get dropped)
+        p = (Ether(src=host.mac, dst=self.pg0.local_mac) /
+             IP(src=host.ip4, dst=self.pg0.local_ip4) /
+             TCP(sport=host_in_port, dport=server_out_port))
+        self.pg0.add_stream(p)
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        # Here VPP used to crash due to an infinite loop
+
+        cnt = self.statistics.get_counter('/nat44/hairpinning')[0]
+        # send packet from host to server
+        p = (Ether(src=host.mac, dst=self.pg0.local_mac) /
+             IP(src=host.ip4, dst=self.nat_addr) /
+             TCP(sport=host_in_port, dport=server_out_port))
+        self.pg0.add_stream(p)
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        capture = self.pg0.get_capture(1)
+        p = capture[0]
+        try:
+            ip = p[IP]
+            tcp = p[TCP]
+            self.assertEqual(ip.src, self.nat_addr)
+            self.assertEqual(ip.dst, server.ip4)
+            self.assertNotEqual(tcp.sport, host_in_port)
+            self.assertEqual(tcp.dport, server_in_port)
+            self.assert_packet_checksums_valid(p)
+            host_out_port = tcp.sport
+        except:
+            self.logger.error(ppp("Unexpected or invalid packet:", p))
+            raise
+
+        after = self.statistics.get_counter('/nat44/hairpinning')[0]
+        if_idx = self.pg0.sw_if_index
+        self.assertEqual(after[if_idx] - cnt[if_idx], 1)
+
+        # send reply from server to host
+        p = (Ether(src=server.mac, dst=self.pg0.local_mac) /
+             IP(src=server.ip4, dst=self.nat_addr) /
+             TCP(sport=server_in_port, dport=host_out_port))
+        self.pg0.add_stream(p)
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+        capture = self.pg0.get_capture(1)
+        p = capture[0]
+        try:
+            ip = p[IP]
+            tcp = p[TCP]
+            self.assertEqual(ip.src, self.nat_addr)
+            self.assertEqual(ip.dst, host.ip4)
+            self.assertEqual(tcp.sport, server_out_port)
+            self.assertEqual(tcp.dport, host_in_port)
+            self.assert_packet_checksums_valid(p)
+        except:
+            self.logger.error(ppp("Unexpected or invalid packet:", p))
+            raise
+
+        after = self.statistics.get_counter('/nat44/hairpinning')[0]
+        if_idx = self.pg0.sw_if_index
+        self.assertEqual(after[if_idx] - cnt[if_idx], 2)
+
     def test_interface_addr(self):
         """ Acquire NAT44 addresses from interface """
         self.vapi.nat44_add_del_interface_addr(