ECMP routing policy selection flows use the policy match without the
chain ID condition applied to non-ECMP policies. A main-chain ECMP
policy can therefore override a lower-priority rule after a jump, and
an ECMP policy in a named chain can match outside that chain.

Use a shared match builder for both policy paths, preserving the
unqualified match when the router has no policy chains. Add IPv4 and
IPv6 trace coverage for main-chain and named-chain ECMP policies,
including jumps to lower-priority allow rules.

Fixes: 33ecea5b9b13 ("Logical Router Policy chains.")
Assisted-by: OpenAI Codex
Signed-off-by: Tim Rozet <[email protected]>
---
This was found while using policy-chain jumps for egress routing in
OVN-Kubernetes. A main-chain priority-100 ECMP reroute could override
an allow at priority 20 in the chain selected by a priority-102 jump.

Testing:
- The new ECMP-chain regression fails without the fix and passes
  with it.
- Focused router-policy and IPv4/IPv6 forwarding tests pass.
- GitHub Actions Build and Test passed:
  https://github.com/trozet/ovn/actions/runs/40391436937
- GitHub Actions ovn-kubernetes passed:
  https://github.com/trozet/ovn/actions/runs/40391436986

 northd/northd.c     | 25 +++++++++++------
 tests/ovn-northd.at | 65 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 82 insertions(+), 8 deletions(-)

diff --git a/northd/northd.c b/northd/northd.c
index 3f59894bd..be961fc8d 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -12463,6 +12463,18 @@ static bool check_bfd_state(const struct 
nbrec_logical_router_policy *rule,
     return true;
 }
 
+static void
+build_routing_policy_match(const struct route_policy *rp, struct ds *match)
+{
+    ds_clear(match);
+    if (rp->chain_id == -1) {
+        ds_put_cstr(match, rp->rule->match);
+    } else {
+        ds_put_format(match, REG_POLICY_CHAIN_ID" == %d && (%s)",
+                      rp->chain_id, rp->rule->match);
+    }
+}
+
 static void
 build_routing_policy_flow(struct lflow_table *lflows, struct ovn_datapath *od,
                           const struct hmap *lr_ports, struct route_policy *rp,
@@ -12525,12 +12537,7 @@ build_routing_policy_flow(struct lflow_table *lflows, 
struct ovn_datapath *od,
         ds_put_cstr(&actions, REG_ECMP_GROUP_ID" = 0; next;");
     }
 
-    if (rp->chain_id == -1) {
-        ds_put_format(&match, "%s", rule->match);
-    } else {
-        ds_put_format(&match, REG_POLICY_CHAIN_ID" == %d && (%s)",
-                      rp->chain_id, rule->match);
-    }
+    build_routing_policy_match(rp, &match);
 
     ovn_lflow_add(lflows, od, S_ROUTER_IN_POLICY, rule->priority,
                  ds_cstr(&match), ds_cstr(&actions), lflow_ref,
@@ -12633,8 +12640,10 @@ build_ecmp_routing_policy_flows(struct lflow_table 
*lflows,
                       "; %s = 1; next;", REG_ECMP_GROUP_ID,
                       ecmp_group_id, REG_ECMP_MEMBER_ID);
     }
-    ovn_lflow_add(lflows, od, S_ROUTER_IN_POLICY, rule->priority, rule->match,
-                  ds_cstr(&actions), lflow_ref, WITH_HINT(&rule->header_));
+    build_routing_policy_match(rp, &match);
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_POLICY, rule->priority,
+                  ds_cstr(&match), ds_cstr(&actions), lflow_ref,
+                  WITH_HINT(&rule->header_));
 cleanup:
     ds_destroy(&match);
     ds_destroy(&actions);
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index d9682f78e..ff0d87d77 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -4220,6 +4220,71 @@ OVN_CLEANUP_NORTHD
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([Router policies - ECMP chains])
+AT_KEYWORDS([policy-based-routing ecmp chains])
+ovn_start
+
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lr0-p0 00:00:00:00:00:01 \
+    10.0.0.1/24 fd00::1/64
+check ovn-nbctl lr-route-add lr0 0.0.0.0/0 10.0.0.4
+check ovn-nbctl lr-route-add lr0 ::/0 fd00::4
+
+AT_CAPTURE_FILE([trace])
+check_policy() {
+    AT_CHECK([ovn-trace --detailed lr0 \
+        "inport == \"lr0-p0\" && eth.src == 00:00:00:00:00:10 && \
+         eth.dst == 00:00:00:00:00:01 && $family.src == $src && \
+         $family.dst == $dst && ip.ttl == 64" > trace], [0])
+    echo "pkt.mark = $mark;" > expout
+    AT_CHECK([grep "pkt.mark = " trace | sed 's/^ *//'], [0], [expout])
+}
+
+for family in ip4 ip6; do
+    if test "$family" = ip4; then
+        src=10.0.0.10
+        prefix=203.0.113.
+        nexthops=10.0.0.2,10.0.0.3
+    else
+        src=fd00::10
+        prefix=2001:db8::
+        nexthops=fd00::2,fd00::3
+    fi
+
+    dnl Without chains, ECMP should still select a next hop.
+    check ovn-nbctl --wait=sb lr-policy-add lr0 100 "$family" \
+        reroute "$nexthops" pkt_mark=100
+    dst=${prefix}10 mark=100 check_policy
+
+    dnl A main-chain ECMP rule must not capture traffic after a jump to a
+    dnl lower-priority allow in another chain.
+    check ovn-nbctl --chain=bypass lr-policy-add lr0 20 "1" \
+        allow pkt_mark=20
+    check ovn-nbctl --wait=sb lr-policy-add lr0 102 \
+        "$family.dst == ${prefix}10" jump bypass
+    dst=${prefix}10 mark=20 check_policy
+    dst=${prefix}11 mark=100 check_policy
+
+    dnl ECMP in a named chain must run only after a jump to that chain.
+    dnl It must not override lower-priority rules in the main or bypass chain.
+    check ovn-nbctl lr-policy-del lr0 100 "$family"
+    check ovn-nbctl --chain=ecmp lr-policy-add lr0 100 "$family" \
+        reroute "$nexthops" pkt_mark=100
+    check ovn-nbctl lr-policy-add lr0 90 "1" allow pkt_mark=90
+    check ovn-nbctl --wait=sb lr-policy-add lr0 102 \
+        "$family.dst == ${prefix}12" jump ecmp
+    dst=${prefix}10 mark=20 check_policy
+    dst=${prefix}11 mark=90 check_policy
+    dst=${prefix}12 mark=100 check_policy
+
+    check ovn-nbctl --wait=sb lr-policy-del lr0
+done
+
+OVN_CLEANUP_NORTHD
+AT_CLEANUP
+])
+
 OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([Router policies - output_port])
 ovn_start
-- 
2.56.0

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to