On 9/29/26 12:19, Timothy Redaelli via dev wrote:
The 'filter' column of the Mirror table was parsed with
parse_ofp_exact_flow(), which requires every field that is mentioned to
be given as an exact value.  Specifications like "ip,nw_src=10.0.0.0/24"
were therefore rejected, even though the matching side already stores the
filter as a miniflow plus minimask and unions that mask into the
megaflow, so partially masked fields work end to end.

Add parse_ofp_flow_match(), which parses a flow specification into a
'struct match' using ofp_parse_field(), so each value may carry a mask.
Use it for the mirror filter.  Like parse_ofp_exact_flow(), it rejects
fields whose prerequisites are not met and fields that are set more than
once.  parse_ofp_exact_flow() is left alone, as its other callers do want
exact values.

Reported-at: https://github.com/openvswitch/ovs-issues/issues/349
Reported-at: https://redhat.atlassian.net/browse/FDP-2474
Signed-off-by: Timothy Redaelli <[email protected]>
---
v2:
   - Restore the prerequisite and duplicate field checks that
     parse_ofp_exact_flow() does.  (Mike)
   - Add negative test cases for them.
   - Rebased on current main.

  NEWS                           |  3 ++
  include/openvswitch/ofp-flow.h |  4 ++
  lib/ofp-flow.c                 | 76 ++++++++++++++++++++++++++++++++++
  ofproto/ofproto-dpif-mirror.c  | 11 +++--
  tests/ofproto-dpif.at          | 64 ++++++++++++++++++++++++++++
  vswitchd/vswitch.xml           |  8 ++--
  6 files changed, 157 insertions(+), 9 deletions(-)


Hi Timothy,

I think there may be some changes to handling error conditions like missing values where they can be noop now instead of hitting error.

Also the new fn shares the same structure as the "exact" one so a common fn could be created, with two wrappers parse_ofp_exact_flow/parse_ofp_masked_flow. I'll reply with an untested version like this.

thanks,
Kevin.

diff --git a/NEWS b/NEWS
index de1a030ad..33ab0585b 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,8 @@
  Post-v4.0.0
  --------------------
+   - ovs-vswitchd:
+     * Mirror filters now accept masked fields, e.g. "ip,nw_src=10.0.0.0/24"
+       in the "filter" column of the Mirror table.
v4.0.0 - 17 Aug 2026
diff --git a/include/openvswitch/ofp-flow.h b/include/openvswitch/ofp-flow.h
index f2223d90b..6c4310b75 100644
--- a/include/openvswitch/ofp-flow.h
+++ b/include/openvswitch/ofp-flow.h
@@ -155,6 +155,10 @@ char *parse_ofp_exact_flow(struct flow *flow, struct 
flow_wildcards *wc,
                             const struct tun_table *tun_table, const char *s,
                             const struct ofputil_port_map *port_map);
+char *parse_ofp_flow_match(struct match *match,
+                           const struct tun_table *tun_table, const char *s,
+                           const struct ofputil_port_map *port_map);
+
  /* Flow stats or aggregate stats request, independent of protocol. */
  struct ofputil_flow_stats_request {
      bool aggregate;             /* Aggregate results? */
diff --git a/lib/ofp-flow.c b/lib/ofp-flow.c
index 3bc744f78..7340a3cfc 100644
--- a/lib/ofp-flow.c
+++ b/lib/ofp-flow.c
@@ -2016,3 +2016,79 @@ exit:
      }
      return error;
  }
+
+/* Parses a specification of a flow from 's' into 'match'.  's' must take the
+ * form FIELD=VALUE[,FIELD=VALUE]... where each FIELD is the name of an
+ * mf_field.  Unlike parse_ofp_exact_flow(), each VALUE may include a mask
+ * (e.g. "nw_src=10.0.0.0/24"), so a field can be partially wildcarded.
+ * Fields must be specified in a natural order for satisfying prerequisites.
+ * If the map 'port_map' is specified, converts port names into port numbers.
+ *
+ * Returns NULL on success, otherwise a malloc()'d string that explains the
+ * problem. */
+char *
+parse_ofp_flow_match(struct match *match, const struct tun_table *tun_table,
+                     const char *s, const struct ofputil_port_map *port_map)
+{
+    enum ofputil_protocol usable_protocols = OFPUTIL_P_ANY;
+    char *pos, *key, *value_s;
+    char *error = NULL;
+    char *copy;
+
+    match_init_catchall(match);
+    match->flow.tunnel.metadata.tab = tun_table;
+
+    pos = copy = xstrdup(s);
+    while (ofputil_parse_key_value(&pos, &key, &value_s)) {
+        const struct ofp_protocol *p;
+        if (ofp_parse_protocol(key, &p)) {
+            if (match->wc.masks.dl_type) {
+                error = xasprintf("%s: Ethernet type set multiple times", s);
+                goto exit;
+            }
+            match_set_dl_type(match, htons(p->dl_type));
+
+            if (p->nw_proto) {
+                if (match->wc.masks.nw_proto) {
+                    error = xasprintf("%s: network protocol set "
+                                      "multiple times", s);
+                    goto exit;
+                }
+                match_set_nw_proto(match, p->nw_proto);
+            }
+            match_set_default_packet_type(match);
+        } else {
+            const struct mf_field *mf = mf_from_name(key);
+
+            if (!mf) {
+                error = xasprintf("%s: unknown field %s", s, key);
+                goto exit;
+            }
+
+            if (!mf_are_match_prereqs_ok(mf, match)) {
+                error = xasprintf("%s: prerequisites not met for setting %s",
+                                  s, key);
+                goto exit;
+            }
+
+            if (!mf_is_all_wild(mf, &match->wc)) {
+                error = xasprintf("%s: field %s set multiple times", s, key);
+                goto exit;
+            }
+
+            error = ofp_parse_field(mf, value_s, port_map, match,
+                                    &usable_protocols);
+            if (error) {
+                goto exit;
+            }
+        }
+    }
+
+exit:
+    free(copy);
+
+    if (error) {
+        match_init_catchall(match);
+    }
+    return error;
+}
diff --git a/ofproto/ofproto-dpif-mirror.c b/ofproto/ofproto-dpif-mirror.c
index e8a2830fb..346f0b79d 100644
--- a/ofproto/ofproto-dpif-mirror.c
+++ b/ofproto/ofproto-dpif-mirror.c
@@ -331,13 +331,11 @@ mirror_set(struct mbridge *mbridge, const struct ofproto 
*ofproto,
if (ms->filter && strlen(ms->filter)) {
              struct ofputil_port_map map = OFPUTIL_PORT_MAP_INITIALIZER(&map);
-            struct flow_wildcards wc;
-            struct flow flow;
+            struct match match;
              char *err;
ofproto_append_ports_to_map(&map, ofproto->ports);
-            err = parse_ofp_exact_flow(&flow, &wc,
-                                       ofproto_get_tun_tab(ofproto),
+            err = parse_ofp_flow_match(&match, ofproto_get_tun_tab(ofproto),
                                         ms->filter, &map);
              ofputil_port_map_destroy(&map);
              if (err) {
@@ -352,14 +350,15 @@ mirror_set(struct mbridge *mbridge, const struct ofproto 
*ofproto,
               * behavior, and it would be overly complex to detect all possible
               * issues.  So instead we attempt to extract the in_port and error
               * if successful. */
-            if (wc.masks.in_port.ofp_port) {
+            if (match.wc.masks.in_port.ofp_port) {
                  VLOG_WARN("filter is invalid due to in_port field.");
                  mirror_destroy(mbridge, mirror->aux);
                  return EINVAL;
              }
mirror->filter_str = xstrdup(ms->filter);
-            ovsrcu_set(&mirror->filter_mask, filtermask_create(&flow, &wc));
+            ovsrcu_set(&mirror->filter_mask,
+                       filtermask_create(&match.flow, &match.wc));
          }
      }
diff --git a/tests/ofproto-dpif.at b/tests/ofproto-dpif.at
index efb36e058..cc3c66023 100644
--- a/tests/ofproto-dpif.at
+++ b/tests/ofproto-dpif.at
@@ -5908,6 +5908,70 @@ OVS_VSWITCHD_STOP(["/filter is invalid: invalid: unknown 
field invalid/d
  /mirror mymirror configuration is invalid/d"])
  AT_CLEANUP
+AT_SETUP([ofproto-dpif - mirroring, filter with wildcards])
+AT_KEYWORDS([mirror mirrors mirroring])
+OVS_VSWITCHD_START
+add_of_ports br0 1 2 3
+AT_CHECK([ovs-vsctl \
+          set Bridge br0 mirrors=@m -- \
+          --id=@p3 get Port p3 -- \
+          --id=@m create Mirror name=mymirror select_all=true output_port=@p3 \
+          filter="\"ip,nw_src=192.168.0.0/24\""], [0], [ignore])
+
+AT_CHECK([ovs-ofctl add-flow br0 "in_port=1 actions=output:2"])
+
+match_flow="eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=6,tos=0,ttl=128,frag=no),tcp(dst=80)"
+nomatch_flow="eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=10.0.0.1,dst=192.168.0.2,proto=6,tos=0,ttl=128,frag=no),tcp(dst=80)"
+
+dnl A masked filter should be accepted and only matching flows mirrored.
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(1),$match_flow"], [0], 
[stdout])
+AT_CHECK_UNQUOTED([tail -1 stdout], [0],
+  [Datapath actions: 3,2
+])
+
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(1),$nomatch_flow"], [0], 
[stdout])
+AT_CHECK_UNQUOTED([tail -1 stdout], [0],
+  [Datapath actions: 2
+])
+
+dnl A masked L4 port filter should compose with the wildcards too.
+AT_CHECK([ovs-vsctl set mirror mymirror 
filter="\"tcp,tcp_dst=0x0050/0xfff0\""], [0])
+
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(1),$match_flow"], [0], 
[stdout])
+AT_CHECK_UNQUOTED([tail -1 stdout], [0],
+  [Datapath actions: 3,2
+])
+
+nomatch_port_flow="eth(src=50:54:00:00:00:05,dst=50:54:00:00:00:07),eth_type(0x0800),ipv4(src=192.168.0.1,dst=192.168.0.2,proto=6,tos=0,ttl=128,frag=no),tcp(dst=443)"
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(1),$nomatch_port_flow"], 
[0], [stdout])
+AT_CHECK_UNQUOTED([tail -1 stdout], [0],
+  [Datapath actions: 2
+])
+
+dnl Missing prerequisites and duplicate fields are still rejected.
+AT_CHECK([ovs-vsctl set mirror mymirror filter="\"nw_src=192.168.0.0/24\""], 
[0])
+AT_CHECK([ovs-vsctl set mirror mymirror \
+          filter="\"ip,nw_src=192.168.0.0/24,nw_src=10.0.0.0/8\""], [0])
+AT_CHECK([ovs-vsctl set mirror mymirror filter="\"ip,ipv6\""], [0])
+
+dnl Each of the above three lines should produce two log messages.
+OVS_WAIT_UNTIL([test $(grep -Ec "filter is invalid|mirror mymirror configuration is 
invalid" ovs-vswitchd.log) -eq 6])
+AT_CHECK([grep -c "prerequisites not met for setting nw_src" 
ovs-vswitchd.log], [0], [1
+])
+AT_CHECK([grep -c "field nw_src set multiple times" ovs-vswitchd.log], [0], [1
+])
+AT_CHECK([grep -c "Ethernet type set multiple times" ovs-vswitchd.log], [0], [1
+])
+
+AT_CHECK([ovs-appctl ofproto/trace ovs-dummy "in_port(1),$match_flow"], [0], 
[stdout])
+AT_CHECK_UNQUOTED([tail -1 stdout], [0],
+  [Datapath actions: 2
+])
+
+OVS_VSWITCHD_STOP(["/filter is invalid: /d
+/mirror mymirror configuration is invalid/d"])
+AT_CLEANUP
+
  AT_SETUP([ofproto-dpif - mirroring, select_all])
  AT_KEYWORDS([mirror mirrors mirroring])
  OVS_VSWITCHD_START
diff --git a/vswitchd/vswitch.xml b/vswitchd/vswitch.xml
index 4eec70fe4..1da1a2f29 100644
--- a/vswitchd/vswitch.xml
+++ b/vswitchd/vswitch.xml
@@ -5319,9 +5319,11 @@ ovs-vsctl add-port br0 p1 -- \
            When set, only packets that match <ref column="filter"/> are
            selected for mirroring.  Packets that do not match are ignored
            by thie mirror.  The <ref column="filter"/> syntax is described
-          in <code>ovs-fields</code>(7).  However, the <code>in_port</code>
-          field is not supported; <ref column="select_src_port"/> should be
-          used to limit the mirror to a source port.
+          in <code>ovs-fields</code>(7).  A field may be given with a mask,
+          e.g. <code>ip,nw_src=10.0.0.0/24</code>.
+          However, the <code>in_port</code> field is not supported;
+          <ref column="select_src_port"/> should be used to limit the
+          mirror to a source port.
          </p>
          <p>
            This filter is applied after <ref column="select_all"/>, <ref

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

Reply via email to