When an external router sends an ICMP "Redirect to Host" (IPv4 type 5) or ICMPv6 Redirect (type 137) to an OVN Logical Router Port IP, the packet is not matched by any specific handler in lr_in_ip_input. On gateway and distributed-gateway routers this causes the packet to fall through to conntrack stages where ct() fails, spamming OVS logs. On plain routers the priority-80 ICMP-unreachable catch-all handles them, but that is guarded by !is_gw_router so gateway routers are left unprotected.
Per RFC 1812 (IPv4) and RFC 4861 section 8 (IPv6), a router must not act on ICMP Redirect messages addressed to it. Linux enforces this by defaulting accept_redirects to off when forwarding is enabled. Add priority-95 flows in lr_in_ip_input that silently drop ICMP Redirect packets (IPv4 type 5 and ICMPv6 type 137) destined to any router-owned IP, before the conntrack stages. This gives uniform behavior across all router types and eliminates the ct failure log spam. Reported-at: https://issues.redhat.com/browse/FDP-1936 Assisted-by: Claude Opus 4.6, OpenCode Signed-off-by: Ales Musil <[email protected]> --- Documentation/ref/ovn-logical-flows.7.rst | 9 +++++++++ northd/northd.c | 24 +++++++++++++++++++++++ tests/ovn-northd.at | 22 +++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/Documentation/ref/ovn-logical-flows.7.rst b/Documentation/ref/ovn-logical-flows.7.rst index 1a9168ac8..5c8894ae0 100644 --- a/Documentation/ref/ovn-logical-flows.7.rst +++ b/Documentation/ref/ovn-logical-flows.7.rst @@ -2377,6 +2377,15 @@ contains the following flows to implement very basic IP host functionality. flags.loopback = 1; next; +- ICMP Redirect drop. A router must not act on ICMP Redirect messages + addressed to it (RFC 1812 for IPv4; RFC 4861, section 8 for IPv6). For + each IP address *A* owned by a router port, a priority-95 flow silently + drops ICMP Redirect packets destined to *A*. For IPv4, the flow matches + ``ip4.dst == A && icmp4 && icmp4.type == 5``. For IPv6, the flow matches + ``ip6.dst == A && icmp6 && icmp6.type == 137``. These flows are installed + before the conntrack stages, so Redirect packets are neither run through + ``ct()`` (which would fail and log errors) nor forwarded. + - Reply to ARP requests. These flows reply to ARP requests for the router's own IP address. The ARP diff --git a/northd/northd.c b/northd/northd.c index 88e3ece88..b5f5c46ad 100644 --- a/northd/northd.c +++ b/northd/northd.c @@ -17507,6 +17507,16 @@ build_ipv6_input_flows_for_lrouter_port( ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90, ds_cstr(match), lrp_actions, lflow_ref, WITH_HINT(&op->nbrp->header_)); + + /* ICMPv6 Redirect (type 137) addressed to the router's own IP. + * Same rationale as IPv4 (RFC 4861 s8). */ + ds_clear(match); + ds_put_cstr(match, "ip6.dst == "); + op_put_v6_networks(match, op); + ds_put_cstr(match, " && icmp6 && icmp6.type == 137"); + ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 95, + ds_cstr(match), debug_drop_action(), lflow_ref, + WITH_HINT(&op->nbrp->header_)); } /* ND reply. These flows reply to ND solicitations for the @@ -17710,6 +17720,20 @@ build_lrouter_ipv4_ip_input(struct ovn_port *op, "next; "; ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 90, ds_cstr(match), icmp_actions, lflow_ref, WITH_HINT(&op->nbrp->header_)); + + /* ICMP Redirect (type 5) addressed to the router's own IP. + * A router must not act on Redirects sent to it (RFC 1812; + * Linux accept_redirects defaults off when forwarding). Drop + * here, before conntrack, so they are neither run through ct + * in the unsnat/dnat stages (which fails and spams the log) + * nor forwarded. */ + ds_clear(match); + ds_put_cstr(match, "ip4.dst == "); + op_put_v4_networks(match, op, false); + ds_put_cstr(match, " && icmp4 && icmp4.type == 5"); + ovn_lflow_add(lflows, op->od, S_ROUTER_IN_IP_INPUT, 95, + ds_cstr(match), debug_drop_action(), lflow_ref, + WITH_HINT(&op->nbrp->header_)); } /* BFD msg handling */ diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at index 6d191c1a0..a97a317cc 100644 --- a/tests/ovn-northd.at +++ b/tests/ovn-northd.at @@ -1899,6 +1899,8 @@ AT_CHECK([grep "lr_in_ip_input" sbflows | grep 'ip.\.dst == {' | grep drop | ovn table=??(lr_in_ip_input ), priority=60 , match=(ip4.dst == {192.168.0.1}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:20}), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == {10.10.0.1, 192.168.1.1} && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10} && icmp6 && icmp6.type == 137), action=(drop;) ]) # create SNAT with external IP equal to LRP's IP @@ -1912,6 +1914,8 @@ AT_CHECK([grep "lr_in_ip_input" sbflows | grep 'ip.\.dst == {' | grep drop | ovn table=??(lr_in_ip_input ), priority=60 , match=(ip4.dst == {10.10.0.1, 192.168.1.1}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:20}), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == {10.10.0.1, 192.168.1.1} && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10} && icmp6 && icmp6.type == 137), action=(drop;) ]) check ovn-nbctl lr-nat-del lr0 @@ -1929,6 +1933,8 @@ AT_CHECK([grep "lr_in_ip_input" sbflows | grep 'ip.\.dst == {' | grep drop | ovn table=??(lr_in_ip_input ), priority=60 , match=(ip4.dst == {192.168.0.1}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:10}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:20}), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == {10.10.0.1, 192.168.1.1} && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10} && icmp6 && icmp6.type == 137), action=(drop;) ]) check ovn-nbctl lr-nat-del lr0 @@ -1946,6 +1952,8 @@ AT_CHECK([grep "lr_in_ip_input" sbflows | grep 'ip.\.dst == {' | grep drop | ovn table=??(lr_in_ip_input ), priority=60 , match=(ip4.dst == {192.168.0.1}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:20}), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == {10.10.0.1, 192.168.1.1} && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10} && icmp6 && icmp6.type == 137), action=(drop;) ]) check ovn-nbctl lb-del lb1 @@ -1963,6 +1971,8 @@ AT_CHECK([grep "lr_in_ip_input" sbflows | grep 'ip.\.dst == {' | grep drop | ovn table=??(lr_in_ip_input ), priority=60 , match=(ip4.dst == {192.168.0.1}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:10}), action=(drop;) table=??(lr_in_ip_input ), priority=60 , match=(ip6.dst == {fe80::200:ff:fe00:20}), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == {10.10.0.1, 192.168.1.1} && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == {2000::1, fe80::200:ff:fe00:10} && icmp6 && icmp6.type == 137), action=(drop;) ]) OVN_CLEANUP_NORTHD @@ -14839,6 +14849,12 @@ AT_CHECK([grep "lr_in_ip_input" lr0flows | ovn_strip_lflows], [0], [dnl table=??(lr_in_ip_input ), priority=92 , match=(inport == "lr0-public" && arp.op == 1 && arp.tpa == 172.168.0.100 && is_chassis_resident("cr-lr0-public")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;) table=??(lr_in_ip_input ), priority=92 , match=(inport == "lr0-public" && arp.op == 1 && arp.tpa == 172.168.0.110 && is_chassis_resident("sw0-port1")), action=(eth.dst = eth.src; eth.src = 30:54:00:00:00:03; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = 30:54:00:00:00:03; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;) table=??(lr_in_ip_input ), priority=92 , match=(inport == "lr0-public" && arp.op == 1 && arp.tpa == 172.168.0.120 && is_chassis_resident("cr-lr0-public")), action=(eth.dst = eth.src; eth.src = xreg0[[0..47]]; arp.op = 2; /* ARP reply */ arp.tha = arp.sha; arp.sha = xreg0[[0..47]]; arp.tpa <-> arp.spa; outport = inport; flags.loopback = 1; output;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == 10.0.0.1 && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == 172.168.0.10 && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == 20.0.0.1 && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == fe80::200:ff:fe00:ff01 && icmp6 && icmp6.type == 137), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == fe80::200:ff:fe00:ff02 && icmp6 && icmp6.type == 137), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == fe80::200:ff:fe00:ff03 && icmp6 && icmp6.type == 137), action=(drop;) ]) AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl @@ -15021,6 +15037,12 @@ AT_CHECK([grep "lr_in_ip_input" lr0flows | ovn_strip_lflows], [0], [dnl table=??(lr_in_ip_input ), priority=90 , match=(ip6.dst == fe80::200:ff:fe00:ff01 && icmp6.type == 128 && icmp6.code == 0), action=(ip6.dst <-> ip6.src; ip.ttl = 255; icmp6.type = 129; flags.loopback = 1; next; ) table=??(lr_in_ip_input ), priority=90 , match=(ip6.dst == fe80::200:ff:fe00:ff02 && icmp6.type == 128 && icmp6.code == 0), action=(ip6.dst <-> ip6.src; ip.ttl = 255; icmp6.type = 129; flags.loopback = 1; next; ) table=??(lr_in_ip_input ), priority=90 , match=(ip6.dst == fe80::200:ff:fe00:ff03 && icmp6.type == 128 && icmp6.code == 0), action=(ip6.dst <-> ip6.src; ip.ttl = 255; icmp6.type = 129; flags.loopback = 1; next; ) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == 10.0.0.1 && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == 172.168.0.10 && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip4.dst == 20.0.0.1 && icmp4 && icmp4.type == 5), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == fe80::200:ff:fe00:ff01 && icmp6 && icmp6.type == 137), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == fe80::200:ff:fe00:ff02 && icmp6 && icmp6.type == 137), action=(drop;) + table=??(lr_in_ip_input ), priority=95 , match=(ip6.dst == fe80::200:ff:fe00:ff03 && icmp6 && icmp6.type == 137), action=(drop;) ]) AT_CHECK([grep "lr_in_unsnat" lr0flows | ovn_strip_lflows], [0], [dnl -- 2.55.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
