From nobody Sat Jun 13 12:26:50 2026 Received: from mail-ot1-f44.google.com (mail-ot1-f44.google.com [209.85.210.44]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 149BE3F7875 for ; Thu, 7 May 2026 13:16:06 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.44 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778159771; cv=none; b=GhpGw6Eqyaa1oIt0fI190jxZGQUl6o7bYn1lfvjRnx7wM/800GhSrrzyp3/gJvv+sLdSKGgYwvuWvh4/pm41MyIGxedW6/J8S3kFHRM76cjrGPR+oQVJ9pfUCmX0GA+7/wkKfWvELX6ydk2EDu27GpjUlKCy67KnVBPVaIUsOrY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778159771; c=relaxed/simple; bh=n9PlPlt5bI+81D55QDezaLRbvJ2Dk2SUOXLwl4F6JNw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=PhX3zfpwbtHSRjhccmfsKkjvWySte2hcLVhKQhwOTbovbqKDwoZgyNnkirjNVSI0zEUe1q7OWQOSphFYcP6BNgzPysTCP6HOl4+9lAgh1I61KRihKKEyfRxtcEcBCRoVeTrmyqkrBDGfDRyk+z/CRSZaGst62BUJMlPnWWmXwvg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=RZchVjz1; arc=none smtp.client-ip=209.85.210.44 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="RZchVjz1" Received: by mail-ot1-f44.google.com with SMTP id 46e09a7af769-7dbcb467f2bso744482a34.3 for ; Thu, 07 May 2026 06:16:06 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778159764; x=1778764564; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=H1M358LEYhOQWEBlqCJXOtB1EifihKMw51Z40wp/zXE=; b=RZchVjz1uvwz3IuNo10+8IyC7mkiS5sCsIyeYOkwqXNbMvpaeb5XHbmrlM2DKBkD62 cjdWz3Wagi+PSBeMiC7y5/jr5RPyYUS8SOkrP8d85Jt9Q3sZ/rI8fjaJQWDVbMypAaor T2uXuEVa7IL/gOFidKbth+fRnTb4AfdogmQp1MCUSzF4F5i9W3FMmG+wLkTuQPLR3Oqw Jt7VRmOA4FL+zZhsyobnkV8wqQ2dvqr+wUXvbUqGo9d1vo+KlARlawJNbu9k5ZhPylsx EkprZKjDXxor0/Q3fZCdHDfN6a/vcU38H0Us37HZY029Oxy5Q0UXmskSGS7o6vQIfKfj 5YiA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778159764; x=1778764564; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=H1M358LEYhOQWEBlqCJXOtB1EifihKMw51Z40wp/zXE=; b=d1ioB3hXCGK3XEi011w0wNt7sRpcxOezcoHhXw/YvmGAm0+7kHxZlCYyTDerTNMoTT yFHNkz4Bk/xMHruIoSAPZPoE2u3WWhlvR1khDH5/YGg73/vI6VKIHWoLIqAaJ6V8gZ/c l1DHqydHrcRen/nBd1heciMn92NjQT5bfG7hcyudEDd2dThujvpKm0LrnR9A6UT9p7vZ FaJOLO3i+qZw/plCyoor+kPCohGFNydToN5lA2Wk+7orcPqKuzkOuD/JzFG/P4nRPTLT 3hANMB2fJ4IhScbsuR8dMbSJge0jxhhyz2SlT5WsQFUoC+Cgnda08MKhjGWO+GhFkLZP xI1g== X-Forwarded-Encrypted: i=1; AFNElJ/Ew1kaobdKwsMuIFvq8uGtiubWmlz5L/ctBGijsllGctD1FiV96w26J9RvotUAbcCFB4KhUJJbaXpx2jM=@vger.kernel.org X-Gm-Message-State: AOJu0YxR/yveBAVzMueu99qzzJ8CMo1YzQlqwYp2LjIwPHUVoPG7B+su 0FKCtFrSptOT9VKSMaMBaQgnEFMKxFtu0aYnfrn/BZ93SWrrPLV2GupW X-Gm-Gg: AeBDietBzNWEWz6hjSIiam1R3bwYhXmtqu4ZnivXn6bW7g3CBIvkq20WmRNM/ZWu1Wu eOYDFvFPvnEZEdFvxP0qpoqUlMKV4oE1k6fNKsJHyoHlU6GH3yJS+IXywp9/h/YAI2cJN7no6Sl F0sg8aC3dH6LERSjy4hl+TmLmKpCpTMTEjiDI1AvaPGhOFlze37hWP8+1Q6Zyxpui606kObpRvt RUf7R7Uo+xQ1mn13weA7yXbRuX4DSo8f4NxtJ43/9UbokBYg//7m51l/qiVi2wgBG4hmYseTvtm zZMuC35bTwQnOPkH+MyopFWqfQfu4EBXgGf45ZCxxaNGoURFcuq0NF6jQHkRIP+juhxwu3yPCyE ds8iQkjri9Mxge6Cx9UQ4pFM2oF/3AJ2pWVuCrbzqhUKL1fAkr54ww4LReJtK1NK7wAxmljzBBG qswldrPc/E2esLRwi3Z+7T25xydg== X-Received: by 2002:a05:6830:3110:b0:7dc:c7aa:22bd with SMTP id 46e09a7af769-7e1deec720emr5017511a34.6.1778159763836; Thu, 07 May 2026 06:16:03 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7deca826dbesm14047117a34.12.2026.05.07.06.15.57 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 07 May 2026 06:16:03 -0700 (PDT) From: Minxi Hou To: netdev@vger.kernel.org Cc: aconole@redhat.com, echaudro@redhat.com, i.maximets@ovn.org, davem@davemloft.net, edumazet@google.com, kuba@kernel.org, pabeni@redhat.com, horms@kernel.org, shuah@kernel.org, dev@openvswitch.org, linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org, Minxi Hou Subject: [PATCH net-next v7 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing Date: Thu, 7 May 2026 21:15:40 +0800 Message-ID: <20260507131541.2331771-2-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260507131541.2331771-1-houminxi@gmail.com> References: <20260507131541.2331771-1-houminxi@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add VLAN TCI formatting and parsing support to ovs-dpctl.py: - Add _vlan_dpstr() to decompose TCI into vid/pcp/cfi fields, with raw tci=3D0x%04x fallback when cfi=3D0 for round-trip safety. - Add _parse_vlan_from_flowstr() boundary check for missing ')'. - Add encap_ovskey subclass restricting nla_map to L2-L4 attributes (slots 0-21) that appear inside 802.1Q ENCAP, with metadata attributes set to "none". - Check parse() return value for unrecognized trailing content. - Support callable format functions in dpstr() output. - Change OVS_KEY_ATTR_VLAN type from uint16 to be16 to match the kernel __be16 wire format; uint16 decodes in host byte order, which gives wrong values on little-endian architectures. - Change OVS_KEY_ATTR_ENCAP type from none to encap_ovskey to enable recursive parsing of 802.1Q encapsulated flow keys. - Add push_vlan action class with fields matching kernel struct ovs_action_push_vlan (vlan_tpid, vlan_tci as network-order u16). - Add push_vlan dpstr format and parse with range validation (vid 0-4095, pcp 0-7, tpid 0-0xFFFF) and CFI forced to 1. - Remove MAX_ENCAP_DEPTH constant and depth tracking -- the bracket-depth counter in the encap parser already handles nesting; the global depth limit was unnecessary. Signed-off-by: Minxi Hou Reviewed-by: Aaron Conole --- .../selftests/net/openvswitch/ovs-dpctl.py | 322 +++++++++++++++++- 1 file changed, 312 insertions(+), 10 deletions(-) diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/t= esting/selftests/net/openvswitch/ovs-dpctl.py index 848f61fdcee0..98d68277b9e7 100644 --- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py +++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py @@ -370,7 +370,7 @@ class ovsactions(nla): ("OVS_ACTION_ATTR_OUTPUT", "uint32"), ("OVS_ACTION_ATTR_USERSPACE", "userspace"), ("OVS_ACTION_ATTR_SET", "ovskey"), - ("OVS_ACTION_ATTR_PUSH_VLAN", "none"), + ("OVS_ACTION_ATTR_PUSH_VLAN", "push_vlan"), ("OVS_ACTION_ATTR_POP_VLAN", "flag"), ("OVS_ACTION_ATTR_SAMPLE", "sample"), ("OVS_ACTION_ATTR_RECIRC", "uint32"), @@ -427,6 +427,9 @@ class ovsactions(nla): =20 return actstr =20 + class push_vlan(nla): + fields =3D (("vlan_tpid", "!H"), ("vlan_tci", "!H")) + class sample(nla): nla_flags =3D NLA_F_NESTED =20 @@ -633,6 +636,14 @@ class ovsactions(nla): print_str +=3D "ct_clear" elif field[0] =3D=3D "OVS_ACTION_ATTR_POP_VLAN": print_str +=3D "pop_vlan" + elif field[0] =3D=3D "OVS_ACTION_ATTR_PUSH_VLAN": + datum =3D self.get_attr(field[0]) + tpid =3D datum["vlan_tpid"] + tci =3D datum["vlan_tci"] + vid =3D tci & 0x0FFF + pcp =3D (tci >> 13) & 0x7 + print_str +=3D "push_vlan(vid=3D%d,pcp=3D%d" \ + ",tpid=3D0x%04x)" % (vid, pcp, tpid) elif field[0] =3D=3D "OVS_ACTION_ATTR_POP_ETH": print_str +=3D "pop_eth" elif field[0] =3D=3D "OVS_ACTION_ATTR_POP_NSH": @@ -726,7 +737,57 @@ class ovsactions(nla): actstr =3D actstr[strspn(actstr, ", ") :] parsed =3D True =20 - if parse_starts_block(actstr, "clone(", False): + if parse_starts_block(actstr, "push_vlan(", False): + actstr =3D actstr[len("push_vlan("):] + vid =3D 0 + pcp =3D 0 + tpid =3D 0x8100 + if ")" not in actstr: + raise ValueError( + "push_vlan: missing ')'") + paren =3D actstr.index(")") + if not actstr[:paren].strip(): + raise ValueError("push_vlan: no fields") + for kv in actstr[:paren].split(","): + if "=3D" not in kv: + raise ValueError( + "push_vlan: bad field '%s'" + % kv.strip()) + k =3D kv[:kv.index("=3D")].strip() + v =3D kv[kv.index("=3D") + 1:].strip() + if k =3D=3D "vid": + vid =3D int(v, 0) + if vid < 0 or vid > 0xFFF: + raise ValueError( + "push_vlan: vid=3D%d out of " + "range (0-4095)" % vid) + elif k =3D=3D "pcp": + pcp =3D int(v, 0) + if pcp < 0 or pcp > 7: + raise ValueError( + "push_vlan: pcp=3D%d out of " + "range (0-7)" % pcp) + elif k =3D=3D "tpid": + tpid =3D int(v, 0) + if tpid < 0 or tpid > 0xFFFF: + raise ValueError( + "push_vlan: tpid=3D0x%x out " + "of range (0-0xffff)" % tpid) + else: + raise ValueError( + "push_vlan: unknown key '%s'" + % k) + tci =3D (vid & 0x0FFF) | ((pcp & 0x7) << 13) \ + | 0x1000 + pvact =3D self.push_vlan() + pvact["vlan_tpid"] =3D tpid + pvact["vlan_tci"] =3D tci + self["attrs"].append( + ["OVS_ACTION_ATTR_PUSH_VLAN", pvact]) + actstr =3D actstr[paren + 1:] + parsed =3D True + + elif parse_starts_block(actstr, "clone(", False): parencount +=3D 1 subacts =3D ovsactions() actstr =3D actstr[len("clone("):] @@ -901,11 +962,11 @@ class ovskey(nla): nla_flags =3D NLA_F_NESTED nla_map =3D ( ("OVS_KEY_ATTR_UNSPEC", "none"), - ("OVS_KEY_ATTR_ENCAP", "none"), + ("OVS_KEY_ATTR_ENCAP", "encap_ovskey"), ("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 +1697,194 @@ class ovskey(nla): class ovs_key_mpls(nla): fields =3D (("lse", ">I"),) =20 + # 802.1Q CFI (Canonical Format Indicator) bit, always set for Ethernet + _VLAN_CFI_MASK =3D 0x1000 + + @staticmethod + def _vlan_dpstr(tci): + """Format VLAN TCI as vid=3DX,pcp=3DY,cfi=3DZ or tci=3D0xNNNN. + + When cfi=3D1 (standard Ethernet VLAN), outputs decomposed + vid/pcp/cfi fields. When cfi=3D0 (truncated VLAN header), + falls back to raw tci=3D0x%04x to ensure round-trip + correctness: the parser auto-adds cfi=3D1 for vid/pcp + format, so cfi=3D0 would be lost on re-parse.""" + vid =3D tci & 0x0FFF + pcp =3D (tci >> 13) & 0x7 + cfi =3D (tci >> 12) & 0x1 + if cfi: + return "vid=3D%d,pcp=3D%d,cfi=3D%d" % (vid, pcp, cfi) + return "tci=3D0x%04x" % tci + + @staticmethod + def _parse_vlan_from_flowstr(flowstr): + """Parse vlan(tci=3DX) or vlan(vid=3DX[,pcp=3DY,cfi=3DZ]) from flo= wstr. + + 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=3D key sets the raw TCI bitfield (no CFI validation) to al= low + non-Ethernet use cases. Use cfi=3D1 for standard Ethernet VLAN ma= tching. + """ + tci =3D 0 + mask =3D 0 + has_tci =3D False + has_vid =3D has_pcp =3D has_cfi =3D False + _tci_mix_err =3D "vlan(): 'tci' cannot be mixed " \ + "with 'vid'/'pcp'/'cfi'" + first =3D True + while True: + flowstr =3D flowstr.lstrip() + if not flowstr: + raise ValueError("vlan(): missing ')'") + if flowstr[0] =3D=3D ')': + break + if not first: + flowstr =3D flowstr[1:] # skip ',' + if not flowstr: + raise ValueError("vlan(): missing ')' after trailing c= omma") + flowstr =3D flowstr.lstrip() + if flowstr and flowstr[0] =3D=3D ')': + break + if flowstr and flowstr[0] =3D=3D ',': + raise ValueError( + "vlan(): empty or extra comma in field list") + first =3D False + + eq =3D flowstr.find('=3D') + if eq =3D=3D -1: + raise ValueError( + "vlan(): expected key=3Dvalue, got '%s'" % flowstr) + key =3D flowstr[:eq].strip() + flowstr =3D flowstr[eq + 1:] + + end =3D flowstr.find(',') + end2 =3D flowstr.find(')') + if end =3D=3D -1 and end2 =3D=3D -1: + raise ValueError("vlan(): missing ')'") + if end =3D=3D -1 or (end2 !=3D -1 and end2 < end): + end =3D end2 + val =3D flowstr[:end].strip() + flowstr =3D flowstr[end:] + + if not val: + raise ValueError("vlan(): empty value for key '%s'" % key) + try: + v =3D int(val, 16) if val.startswith(('0x', '0X')) else in= t(val) + except ValueError as exc: + raise ValueError( + "vlan(): invalid value '%s' for key '%s'" + % (val, key)) from exc + + if key =3D=3D '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=3D0x%x out of range" % v) + tci =3D v + mask =3D 0xFFFF + has_tci =3D True + elif key =3D=3D '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=3D%d out of range (0-409= 5)" % v) + tci |=3D v + mask |=3D 0x0FFF + has_vid =3D True + elif key =3D=3D '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=3D%d out of range (0-7)"= % v) + tci |=3D (v & 0x7) << 13 + mask |=3D 0xE000 + has_pcp =3D True + elif key =3D=3D 'cfi': + if has_tci: + raise ValueError(_tci_mix_err) + if has_cfi: + raise ValueError("vlan(): duplicate 'cfi'") + if v !=3D 1: + raise ValueError("vlan(): cfi must be 1 for Ethernet") + tci |=3D ovskey._VLAN_CFI_MASK + mask |=3D ovskey._VLAN_CFI_MASK + has_cfi =3D True + else: + raise ValueError("vlan(): unknown key '%s'" % key) + + flowstr =3D flowstr[1:] # skip ')' + # Catch immediate '))' (user error). A ')' after ',' is consumed + # by parse()'s strspn(flowstr, "), ") inter-field separator stripp= ing. + 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 =3D=3D 0: + raise ValueError("vlan(): no fields specified, " + "use vlan(vid=3DX[,pcp=3DY,cfi=3DZ]) or vlan(= tci=3DX)") + if not has_tci: + tci |=3D ovskey._VLAN_CFI_MASK + mask |=3D 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. + """ + depth =3D 1 + end =3D -1 + for i, c in enumerate(flowstr): + if c =3D=3D '(': + depth +=3D 1 + elif c =3D=3D ')': + depth -=3D 1 + if depth < 0: + raise ValueError( + "encap(): unmatched ')' at position %d" % i) + if depth =3D=3D 0: + end =3D i + break + + if end =3D=3D -1: + if depth > 1: + raise ValueError("encap(): missing ')' at end") + raise ValueError("encap(): missing closing ')'") + + inner_str =3D flowstr[:end].strip() + if not inner_str: + raise ValueError("encap(): empty inner flow") + + flowstr =3D flowstr[end + 1:] + if flowstr.lstrip().startswith(')'): + raise ValueError("encap(): unmatched ')' after encap()") + + inner_key =3D encap_ovskey() + inner_mask =3D encap_ovskey() + remaining =3D inner_key.parse(inner_str, inner_mask) + if remaining and re.search(r'[^\s,)]', remaining): + raise ValueError( + "encap(): unrecognized trailing " + "content '%s'" % remaining.strip()) + + return flowstr, inner_key, inner_mask + def parse(self, flowstr, mask=3DNone): for field in ( ("OVS_KEY_ATTR_PRIORITY", "skb_priority", intparse), @@ -1657,6 +1906,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 +2053,9 @@ class ovskey(nla): True, ), ("OVS_KEY_ATTR_ETHERNET", None, None, False, False), + ("OVS_KEY_ATTR_VLAN", "vlan", ovskey._vlan_dpstr, + lambda x: False, True), + ("OVS_KEY_ATTR_ENCAP", None, None, False, False), ( "OVS_KEY_ATTR_ETHERTYPE", "eth_type", @@ -1821,22 +2083,61 @@ class ovskey(nla): v =3D self.get_attr(field[0]) if v is not None: m =3D None if mask is None else mask.get_attr(field[0]) + fmt =3D field[2] # str format or callable if field[4] is False: print_str +=3D v.dpstr(m, more) print_str +=3D "," else: if m is None or field[3](m): - print_str +=3D field[1] + "(" - print_str +=3D field[2] % v - print_str +=3D ")," + val =3D fmt(v) if callable(fmt) else fmt % v + print_str +=3D field[1] + "(" + val + ")," elif more or m !=3D 0: - print_str +=3D field[1] + "(" - print_str +=3D (field[2] % v) + "/" + (field[2] % = m) - print_str +=3D ")," + if callable(fmt): + val =3D fmt(v) + "/" + fmt(m) + else: + val =3D (fmt % v) + "/" + (fmt % m) + print_str +=3D field[1] + "(" + val + ")," =20 return print_str =20 =20 +class encap_ovskey(ovskey): + """Inner flow key attributes valid inside 802.1Q ENCAP. + + Only L2-L4 key attributes (slots 0-21) appear inside ENCAP. + Metadata-only attributes (SKB_MARK, DP_HASH, RECIRC_ID, etc.) + are set to "none" -- they never appear inside ENCAP per + ovs_nla_put_vlan() in net/openvswitch/flow_netlink.c. + + nla_map indexes must match OVS_KEY_ATTR_* enum values in + include/uapi/linux/openvswitch.h. + """ + nla_map =3D ( + ("OVS_KEY_ATTR_UNSPEC", "none"), + ("OVS_KEY_ATTR_ENCAP", "none"), # placeholder, parsed by ovskey + ("OVS_KEY_ATTR_PRIORITY", "none"), # skb metadata, not in ENCAP + ("OVS_KEY_ATTR_IN_PORT", "none"), # skb metadata, not in ENCAP + ("OVS_KEY_ATTR_ETHERNET", "ethaddr"), + ("OVS_KEY_ATTR_VLAN", "be16"), + ("OVS_KEY_ATTR_ETHERTYPE", "be16"), + ("OVS_KEY_ATTR_IPV4", "ovs_key_ipv4"), + ("OVS_KEY_ATTR_IPV6", "ovs_key_ipv6"), + ("OVS_KEY_ATTR_TCP", "ovs_key_tcp"), + ("OVS_KEY_ATTR_UDP", "ovs_key_udp"), + ("OVS_KEY_ATTR_ICMP", "ovs_key_icmp"), + ("OVS_KEY_ATTR_ICMPV6", "ovs_key_icmpv6"), + ("OVS_KEY_ATTR_ARP", "ovs_key_arp"), + ("OVS_KEY_ATTR_ND", "ovs_key_nd"), + ("OVS_KEY_ATTR_SKB_MARK", "none"), # metadata, not in ENCAP + ("OVS_KEY_ATTR_TUNNEL", "none"), # tunnel metadata, not in ENCAP + ("OVS_KEY_ATTR_SCTP", "ovs_key_sctp"), + ("OVS_KEY_ATTR_TCP_FLAGS", "be16"), + ("OVS_KEY_ATTR_DP_HASH", "none"), # metadata, not in ENCAP + ("OVS_KEY_ATTR_RECIRC_ID", "none"), # metadata, not in ENCAP + ("OVS_KEY_ATTR_MPLS", "array(ovs_key_mpls)"), + ) + + class OvsPacket(GenericNetlinkSocket): OVS_PACKET_CMD_MISS =3D 1 # Flow table miss OVS_PACKET_CMD_ACTION =3D 2 # USERSPACE action @@ -2576,6 +2877,7 @@ def print_ovsdp_full(dp_lookup_rep, ifindex, ndb=3DND= B(), vpl=3DOvsVport()): =20 =20 def main(argv): + nlmsg_atoms.encap_ovskey =3D encap_ovskey nlmsg_atoms.ovskey =3D ovskey nlmsg_atoms.ovsactions =3D ovsactions =20 --=20 2.53.0 From nobody Sat Jun 13 12:26:50 2026 Received: from mail-ot1-f48.google.com (mail-ot1-f48.google.com [209.85.210.48]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CE6513EAC82 for ; Thu, 7 May 2026 13:16:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.48 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778159776; cv=none; b=kJpl9A3BTZGYFtT8WWDBvHUCrP/SUqhThFVrGWl8axGQQL4HzGFgWEHIUyQOslIDbxjciCOuHsb+NrX0Ik7VBeOUapjeNvcj28McIJS19PWQXWybUIGJc2iLtkIG6FR/oaM8ggUB8fzgYz2oaWzB7gGI3lj0XiFFBASHZZBGfcI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778159776; c=relaxed/simple; bh=S/+lPNFO1EPV7PWmJnmCZqDJVwmF1vQQCOD5Vbc4xGQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=DwnQ8a8Hc44IF7GAcsDsNw4rmtklCILX8qubDLqQGJO1jKMWoTORdAh2bFudxJfpbId0UI8a3QgfdGgI68iDJj5C3m7Qa3XfvJP2XrBovLBkp/ZPtvQhE5e+rxbFPY2DJXYuOaeK2sw9jRVuz2Ed7dFkHxAPW4AJOF1G1w1qfds= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=iRfX+YQ9; arc=none smtp.client-ip=209.85.210.48 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="iRfX+YQ9" Received: by mail-ot1-f48.google.com with SMTP id 46e09a7af769-7dbd23bc684so504055a34.2 for ; Thu, 07 May 2026 06:16:12 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778159771; x=1778764571; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=nCencyebNV6RkeBeV6Zcfzu1ZdE6veaso03Op6l41qE=; b=iRfX+YQ9vsLaWfs6ATuiFDk9YuJLKXXy8S49lxeUrLmJVx4wxjZD/0qfl5g2MgBPpi pC0UHWf8ag5CMmZoeEYB+FVMgW+dh5CNpSdsq94lNeJqXJxn1WJ7GZZZGZAhsccyWBOu rC67AL0+nXZvNVkOYJx82hUImEyFqr421XqgLF4m/PQIsXaN+HuZXAK/AeKhe+Z8nfmO WvPUyc94zrEGfD+XUdl+xXObXXEeGyKuilf9GYj3gVrwM08NRp5y2z/ByVFhaCGjmcYs lmBZXrYCTPJDxiw/XjKkAPstZDNit4ErnUlrpOXCAXgxl1dg+4hwAquWA5+K9FMkHgRQ YOUA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778159771; x=1778764571; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=nCencyebNV6RkeBeV6Zcfzu1ZdE6veaso03Op6l41qE=; b=SXfq0gKb+GZ7ArfOoTghaVrjNyVvYukJv6gdISYBlAxMCvSGm59Z7z+c6GhKO8Zlfq Db5VlDgBeYT7KKFsyunhJ0IyR2epDiPmLwecNn5ol3jjeq2ZNHdLWbDqcwe7m2MnVCjz 7ozpYplME6S+0KRKW7lXvXtDAsTRbpArBoAW4Ij7QumHQ/tonR64NhJunQAHLnWzMYiF 7AFU+us2UtO87rgo01pLksVXph5g/DbHuo+KxVRl4z2lCvMq7PRS4NoWFuv481rJVAAY sTXna86fpPOkvO5kzbEyvLqEEP4ejL47RtuuEc0akl3pXmYS3AQZZmAtQrqDyStv5qeH Iskw== X-Forwarded-Encrypted: i=1; AFNElJ+Tosd3yQuS3jdOCdksbX2Z+Kx7Ty0yjuWbJflUEH2SWMR9fYsMCNPq+pNIkPCAHcosqvIPIa7BkhjzB8U=@vger.kernel.org X-Gm-Message-State: AOJu0YzNOAud5F7Bm20u/9ZY5wW8X0lWvU+MfoTTA0NqFNAsjpUug/Cn sD+bU/+W0ikgF7Zs4F7Hra5SLDmiNZ7aOW2wVsYeaLUDT9aG7ZTZptEj X-Gm-Gg: AeBDies8IFo+tDUYzyJ8d7YpAc+c+5Uy1rFWj0blAc7k06AwA/Cdg2Uyx3ybGr4MiTP VuZrtnYFz1dQL6Jmlt7gMOwGLDzRGh3bxZUT60r4twGrYFWHNK0Ohx0PdS/IrmeXrq8PKHuxwm5 GQ8F2PVdH1DuJPDcXUQjSgNtLke67V0JaGO74Vz0jvQxpEOkL1axokV4Bzlz90dFKK35lg/dbWf ZFF8uMiJvGhENnBh/VX8WjLttrwvlDBw3sqQ1S8C5CO5DR0nKhy25rrXLd7Tr5INllYmucXaQxw F6rfbFKA0PxzRwYDWbtsLbvquBlisAPNa3WvjlPLs5bPkMeTvCRTQeP5jFsfUr8hEdoGalutpip jmXgiHCiOU2W+qTCgpuxQY/4igqdizhAC9ckLI89A/rfkhUshe8sdHI9FUoCDpDKWqRN2arIg+k rN33UJUW6KdJ/veLRt9YXswpamHg== X-Received: by 2002:a05:6830:3592:b0:7db:b5c9:2d87 with SMTP id 46e09a7af769-7e1def36698mr4757801a34.11.1778159770507; Thu, 07 May 2026 06:16:10 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7deca826dbesm14047117a34.12.2026.05.07.06.16.04 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 07 May 2026 06:16:09 -0700 (PDT) From: Minxi Hou To: netdev@vger.kernel.org Cc: aconole@redhat.com, echaudro@redhat.com, i.maximets@ovn.org, davem@davemloft.net, edumazet@google.com, kuba@kernel.org, pabeni@redhat.com, horms@kernel.org, shuah@kernel.org, dev@openvswitch.org, linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org, Minxi Hou Subject: [PATCH net-next v7 2/2] selftests: openvswitch: add pop_vlan test Date: Thu, 7 May 2026 21:15:41 +0800 Message-ID: <20260507131541.2331771-3-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260507131541.2331771-1-houminxi@gmail.com> References: <20260507131541.2331771-1-houminxi@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add test_pop_vlan() to verify OVS kernel datapath pop_vlan action correctly strips 802.1Q VLAN tags from frames. Test structure: - Baseline: untagged forwarding validates basic connectivity. - Negative: forward without pop_vlan, tagged frame is invisible to ns2 (no VLAN sub-interface), ping fails. - Positive: pop_vlan strips tag on forward path, push_vlan restores tag on return path, ping succeeds. Use static ARP entries to avoid VLAN-tagged ARP complexity. Rely on ping success/failure for verification -- no tcpdump or pcap files needed. Signed-off-by: Minxi Hou Reviewed-by: Aaron Conole --- .../selftests/net/openvswitch/openvswitch.sh | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools= /testing/selftests/net/openvswitch/openvswitch.sh index b327d3061ed5..6d13ee8c2baf 100755 --- a/tools/testing/selftests/net/openvswitch/openvswitch.sh +++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh @@ -27,6 +27,7 @@ tests=3D" upcall_interfaces ovs: test the upcall interfaces tunnel_metadata ovs: test extraction of tunnel metadata drop_reason drop: test drop reasons are emitted + pop_vlan vlan: POP_VLAN action strips tag psample psample: Sampling packets with psample" =20 info() { @@ -830,6 +831,78 @@ test_tunnel_metadata() { return 0 } =20 +test_pop_vlan() { + local sbx=3D"test_pop_vlan" + sbx_add "$sbx" || return $? + ovs_add_dp "$sbx" vlandp || return 1 + + ovs_add_netns_and_veths "$sbx" vlandp \ + ns1 veth1 ns1veth 192.0.2.1/24 || return 1 + ovs_add_netns_and_veths "$sbx" vlandp \ + ns2 veth2 ns2veth 192.0.2.2/24 || return 1 + + # Baseline: untagged bidirectional forwarding + ovs_add_flow "$sbx" vlandp \ + 'in_port(1),eth(),eth_type(0x0806),arp()' '2' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(2),eth(),eth_type(0x0806),arp()' '1' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(1),eth(),eth_type(0x0800),ipv4()' '2' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' '1' || return 1 + ovs_sbx "$sbx" ip netns exec ns1 ping -c 3 -W 2 \ + 192.0.2.2 || return 1 + + # VLAN topology: ns1 uses VLAN sub-interface, ns2 is plain + ip -n ns1 link add link ns1veth name ns1veth.10 \ + type vlan id 10 || return 1 + on_exit "ip -n ns1 link del ns1veth.10 2>/dev/null" + ip -n ns1 addr add 198.51.100.1/24 dev ns1veth.10 || return 1 + ip -n ns1 link set ns1veth.10 up || return 1 + ip -n ns2 addr add 198.51.100.2/24 dev ns2veth || return 1 + + ovs_del_flows "$sbx" vlandp + + # Static ARP: avoids VLAN-tagged ARP complexity + local ns1veth10mac ns2mac + ns1veth10mac=3D$(ip -n ns1 link show ns1veth.10 \ + | awk '/link\/ether/ {print $2}') + ns2mac=3D$(ip -n ns2 link show ns2veth \ + | awk '/link\/ether/ {print $2}') + ip -n ns1 neigh replace 198.51.100.2 lladdr "$ns2mac" \ + dev ns1veth.10 nud permanent || return 1 + ip -n ns2 neigh replace 198.51.100.1 \ + lladdr "$ns1veth10mac" \ + dev ns2veth nud permanent || return 1 + + local vlan_match=3D'in_port(1),eth(),eth_type(0x8100),' + vlan_match+=3D'vlan(vid=3D10),' + vlan_match+=3D'encap(eth_type(0x0800),' + vlan_match+=3D'ipv4(src=3D198.51.100.1,proto=3D1),icmp())' + + # Negative: forward without pop_vlan -- tagged frame + # is invisible to ns2 (no VLAN sub-interface), ping fails + ovs_add_flow "$sbx" vlandp "$vlan_match" '2' || return 1 + ovs_sbx "$sbx" ip netns exec ns1 ping -I ns1veth.10 \ + -c 3 -W 1 198.51.100.2 >/dev/null 2>&1 \ + && { info "FAIL: ping should fail without pop_vlan" + return 1; } + + ovs_del_flows "$sbx" vlandp + + # Positive: pop_vlan strips tag on forward path, + # push_vlan restores tag on return path -- ping succeeds + ovs_add_flow "$sbx" vlandp \ + "$vlan_match" 'pop_vlan,2' || return 1 + ovs_add_flow "$sbx" vlandp \ + 'in_port(2),eth(),eth_type(0x0800),ipv4()' \ + 'push_vlan(vid=3D10,pcp=3D0,tpid=3D0x8100),1' || return 1 + ovs_sbx "$sbx" ip netns exec ns1 ping -I ns1veth.10 \ + -c 3 -W 2 198.51.100.2 || return 1 + + return 0 +} + run_test() { ( tname=3D"$1" --=20 2.53.0