From: Aaron Conole <aconole@redhat.com>
To: Minxi Hou <houminxi@gmail.com>
Cc: netdev@vger.kernel.org, linux-kselftest@vger.kernel.org,
dev@openvswitch.org, Eelco Chaudron <echaudro@redhat.com>,
Ilya Maximets <i.maximets@ovn.org>,
"David S. Miller" <davem@davemloft.net>,
Eric Dumazet <edumazet@google.com>,
Jakub Kicinski <kuba@kernel.org>,
Paolo Abeni <pabeni@redhat.com>, Simon Horman <horms@kernel.org>,
Shuah Khan <shuah@kernel.org>,
linux-kernel@vger.kernel.org
Subject: Re: [PATCH net-next v2 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing
Date: Mon, 04 May 2026 11:43:18 -0400 [thread overview]
Message-ID: <f7tpl3bxjs9.fsf@redhat.com> (raw)
In-Reply-To: <20260501133924.3100680-2-houminxi@gmail.com> (Minxi Hou's message of "Fri, 1 May 2026 21:39:22 +0800")
Minxi Hou <houminxi@gmail.com> writes:
> Extend the ovs-dpctl.py flow parser to support vlan() and encap()
> match strings. vlan() accepts tci=, vid=, pcp=, and cfi=
> parameters and generates the OVS_KEY_ATTR_VLAN attribute with a TCI
> value in network byte order. encap() parses nested flow strings
> and returns OVS_KEY_ATTR_ENCAP with inner key attributes as a
> recursive NLA container.
>
> The encap nla_map type is changed from "none" to "nested" so that
> pyroute2 recursively encodes the inner flow key attributes. The
> VLAN nla_map type is changed from "uint16" to "be16" to match the
> kernel's big-endian wire format.
^ That's kindof a fix, BUT it also seems like we never actually used it
anywhere, so I hope it is okay to not treat it that way.
> Signed-off-by: Minxi Hou <houminxi@gmail.com>
> ---
> v1 -> v2: rebase to latest net-next/main, drop --base=auto
>
> .../selftests/net/openvswitch/ovs-dpctl.py | 190 +++++++++++++++++-
> 1 file changed, 188 insertions(+), 2 deletions(-)
>
> diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
> index 848f61fdcee0..317be7878937 100644
> --- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
> +++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py
> @@ -901,11 +901,11 @@ class ovskey(nla):
> nla_flags = NLA_F_NESTED
> nla_map = (
> ("OVS_KEY_ATTR_UNSPEC", "none"),
> - ("OVS_KEY_ATTR_ENCAP", "none"),
> + ("OVS_KEY_ATTR_ENCAP", "nested"),
> ("OVS_KEY_ATTR_PRIORITY", "uint32"),
> ("OVS_KEY_ATTR_IN_PORT", "uint32"),
> ("OVS_KEY_ATTR_ETHERNET", "ethaddr"),
> - ("OVS_KEY_ATTR_VLAN", "uint16"),
> + ("OVS_KEY_ATTR_VLAN", "be16"),
> ("OVS_KEY_ATTR_ETHERTYPE", "be16"),
> ("OVS_KEY_ATTR_IPV4", "ovs_key_ipv4"),
> ("OVS_KEY_ATTR_IPV6", "ovs_key_ipv6"),
> @@ -1636,6 +1636,180 @@ class ovskey(nla):
> class ovs_key_mpls(nla):
> fields = (("lse", ">I"),)
>
> + # 802.1Q CFI (Canonical Format Indicator) bit, always set for Ethernet
> + _VLAN_CFI_MASK = 0x1000
> + _MAX_ENCAP_DEPTH = 4
It would be more useful if we could set the MAX_ENCAP_DEPTH separately,
so that we have the option to break assumptions. Maybe just skip a
depth check here completely.
> + _encap_depth = 0 # single-threaded usage assumed
> +
> + @staticmethod
> + def _parse_vlan_from_flowstr(flowstr):
> + """Parse vlan(tci=X) or vlan(vid=X[,pcp=Y,cfi=Z]) from flowstr.
> +
> + Returns (remaining_flowstr, key_tci, mask_tci).
> + TCI values use standard bit layout (VID bits 0-11,
> + CFI bit 12, PCP bits 13-15); byte order conversion to
> + big-endian happens in pyroute2 be16 NLA serialization.
> + The mask covers only the fields the caller specified:
> + vid -> 0x0FFF, pcp -> 0xE000, cfi -> 0x1000, tci -> 0xFFFF.
> +
> + The tci= key sets the raw TCI bitfield (no CFI validation) to allow
> + non-Ethernet use cases. Use cfi=1 for standard Ethernet VLAN matching.
> + """
> + tci = 0
> + mask = 0
> + has_tci = False
> + has_vid = has_pcp = has_cfi = False
> + _tci_mix_err = "vlan(): 'tci' cannot be mixed " \
> + "with 'vid'/'pcp'/'cfi'"
> + first = True
> + while True:
> + flowstr = flowstr.lstrip()
> + if not flowstr:
> + raise ValueError("vlan(): missing ')'")
> + if flowstr[0] == ')':
> + break
> + if not first:
> + flowstr = flowstr[1:] # skip ','
> + if not flowstr:
> + raise ValueError("vlan(): missing ')' after trailing comma")
> + flowstr = flowstr.lstrip()
> + if flowstr and flowstr[0] == ')':
> + break
> + if flowstr and flowstr[0] == ',':
> + raise ValueError(
> + "vlan(): empty or extra comma in field list")
> + first = False
> +
> + eq = flowstr.find('=')
> + if eq == -1:
> + raise ValueError("vlan(): expected key=value, got '%s'" % flowstr)
> + key = flowstr[:eq].strip()
> + flowstr = flowstr[eq + 1:]
> +
> + end = flowstr.find(',')
> + end2 = flowstr.find(')')
> + if end == -1 or (end2 != -1 and end2 < end):
> + end = end2
> + val = flowstr[:end].strip()
> + flowstr = flowstr[end:]
> +
> + if not val:
> + raise ValueError("vlan(): empty value for key '%s'" % key)
> + try:
> + v = int(val, 16) if val.startswith(('0x', '0X')) else int(val)
> + except ValueError:
> + raise ValueError("vlan(): invalid value '%s' for key '%s'" %
> + (val, key))
> +
> + if key == 'tci':
> + if has_tci:
> + raise ValueError("vlan(): duplicate 'tci'")
> + if has_vid or has_pcp or has_cfi:
> + raise ValueError(_tci_mix_err)
> + if v > 0xFFFF or v < 0:
> + raise ValueError("vlan(): tci=0x%x out of range" % v)
> + tci = v
> + mask = 0xFFFF
> + has_tci = True
> + elif key == 'vid':
> + if has_tci:
> + raise ValueError(_tci_mix_err)
> + if has_vid:
> + raise ValueError("vlan(): duplicate 'vid'")
> + if v < 0 or v > 0xFFF:
> + raise ValueError("vlan(): vid=%d out of range (0-4095)" % v)
> + tci |= v
> + mask |= 0x0FFF
> + has_vid = True
> + elif key == 'pcp':
> + if has_tci:
> + raise ValueError(_tci_mix_err)
> + if has_pcp:
> + raise ValueError("vlan(): duplicate 'pcp'")
> + if v < 0 or v > 7:
> + raise ValueError("vlan(): pcp=%d out of range (0-7)" % v)
> + tci |= (v & 0x7) << 13
> + mask |= 0xE000
> + has_pcp = True
> + elif key == 'cfi':
> + if has_tci:
> + raise ValueError(_tci_mix_err)
> + if has_cfi:
> + raise ValueError("vlan(): duplicate 'cfi'")
> + if v != 1:
> + raise ValueError("vlan(): cfi must be 1 for Ethernet")
> + tci |= ovskey._VLAN_CFI_MASK
> + mask |= ovskey._VLAN_CFI_MASK
> + has_cfi = True
> + else:
> + raise ValueError("vlan(): unknown key '%s'" % key)
> +
> + flowstr = flowstr[1:] # skip ')'
> + # Catch immediate '))' (user error). A ')' after ',' is consumed
> + # by parse()'s strspn(flowstr, "), ") inter-field separator stripping.
> + if flowstr.lstrip().startswith(')'):
> + raise ValueError("vlan(): unmatched ')'")
> + # parse() strips trailing ',', ')', ' ' as inter-field separators,
> + # so we do not need to call strspn here.
> +
> + if mask == 0:
> + raise ValueError("vlan(): no fields specified, "
> + "use vlan(vid=X[,pcp=Y,cfi=Z]) or vlan(tci=X)")
> + if not has_tci:
> + tci |= ovskey._VLAN_CFI_MASK
> + mask |= ovskey._VLAN_CFI_MASK
> + return flowstr, tci, mask
> +
> + @staticmethod
> + def _parse_encap_from_flowstr(flowstr):
> + """Parse encap(inner_flow) from flowstr.
> +
> + Returns (remaining_flowstr, inner_key_dict, inner_mask_dict)
> + where each dict has an 'attrs' key for recursive NLA encoding.
> + Parenthesis-depth tracking handles nested encap() calls but not
> + quoted strings containing literal parentheses.
> + """
> + if ovskey._encap_depth >= ovskey._MAX_ENCAP_DEPTH:
> + raise ValueError("encap(): max nesting depth %d exceeded" %
> + ovskey._MAX_ENCAP_DEPTH)
> + try:
> + ovskey._encap_depth += 1
> + depth = 1
> + end = -1
> + for i, c in enumerate(flowstr):
> + if c == '(':
> + depth += 1
> + elif c == ')':
> + depth -= 1
> + if depth < 0:
> + raise ValueError("encap(): unmatched ')' at position %d" % i)
> + if depth == 0:
> + end = i
> + break
> +
> + if end == -1:
> + if depth > 1:
> + raise ValueError("encap(): missing ')' at end")
> + raise ValueError("encap(): missing closing ')'")
> +
> + inner_str = flowstr[:end].strip()
> + if not inner_str:
> + raise ValueError("encap(): empty inner flow")
> +
> + flowstr = flowstr[end + 1:]
> + if flowstr.lstrip().startswith(')'):
> + raise ValueError("encap(): unmatched ')' after encap()")
> + # parse() strips trailing ',', ')', ' ' as inter-field separators,
> + # so we do not need to call strspn here.
> +
> + inner_key = ovskey()
> + inner_mask = ovskey()
> + inner_key.parse(inner_str, inner_mask)
> +
> + return flowstr, inner_key, inner_mask
> + finally:
> + ovskey._encap_depth -= 1
> +
> def parse(self, flowstr, mask=None):
> for field in (
> ("OVS_KEY_ATTR_PRIORITY", "skb_priority", intparse),
> @@ -1657,6 +1831,16 @@ class ovskey(nla):
> "eth_type",
> lambda x: intparse(x, "0xffff"),
> ),
> + (
> + "OVS_KEY_ATTR_VLAN",
> + "vlan",
> + ovskey._parse_vlan_from_flowstr,
> + ),
> + (
> + "OVS_KEY_ATTR_ENCAP",
> + "encap",
> + ovskey._parse_encap_from_flowstr,
> + ),
> (
> "OVS_KEY_ATTR_IPV4",
> "ipv4",
> @@ -1794,6 +1978,8 @@ class ovskey(nla):
> True,
> ),
> ("OVS_KEY_ATTR_ETHERNET", None, None, False, False),
> + ("OVS_KEY_ATTR_VLAN", "vlan", "0x%04x", lambda x: False, True),
> + ("OVS_KEY_ATTR_ENCAP", None, None, False, False),
> (
> "OVS_KEY_ATTR_ETHERTYPE",
> "eth_type",
next prev parent reply other threads:[~2026-05-04 15:43 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-01 13:39 [PATCH net-next v2 0/2] selftests: openvswitch: add pop_vlan test Minxi Hou
2026-05-01 13:39 ` [PATCH net-next v2 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing Minxi Hou
2026-05-04 15:43 ` Aaron Conole [this message]
2026-05-04 15:52 ` 侯敏熙
2026-05-01 13:39 ` [PATCH net-next v2 2/2] selftests: openvswitch: add pop_vlan test Minxi Hou
2026-05-04 15:51 ` Aaron Conole
2026-05-04 16:14 ` 侯敏熙
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=f7tpl3bxjs9.fsf@redhat.com \
--to=aconole@redhat.com \
--cc=davem@davemloft.net \
--cc=dev@openvswitch.org \
--cc=echaudro@redhat.com \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=houminxi@gmail.com \
--cc=i.maximets@ovn.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox