From nobody Sat Jun 13 16:18:27 2026 Received: from mail-ot1-f50.google.com (mail-ot1-f50.google.com [209.85.210.50]) (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 6603B466B5D for ; Wed, 6 May 2026 13:12:45 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.50 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778073168; cv=none; b=X7v0ryTjXVB2BPsgk+3shzu+1lxY2fkb1U3rqNr5HdqpYkM57qPy9iDFDFH++oiDsPLLZlJk1A7UBXLDuV8mZHOlaiUNtb5Vo74KLzFA8RT5B5DU2O5CoHLwpTU11dnfbUbdHBco4UHE4pJlcJqP2ebmECE6jvMNuEpgt91BYkM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778073168; c=relaxed/simple; bh=hEYXTy36B1S+siQ5tPyffcehRVHrtA5bva4tlKs0+cU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=lHkvV4z7hjdLz2IzNC1AjPrAfPYeA+zF/aurXcLp8GV6sEXLAZtV/WZzSw/whCSnepKY1jjykjom7zjAJYI72NEYyNN464r/HckCaQvnSiptvSSzSrtjYlAIEjWTPMxNguLQeZg9Af2JBIAcFej8w2+WSssSX+s1T227HYlHUoQ= 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=oKz5zjXO; arc=none smtp.client-ip=209.85.210.50 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="oKz5zjXO" Received: by mail-ot1-f50.google.com with SMTP id 46e09a7af769-7de4ed0593fso3790482a34.1 for ; Wed, 06 May 2026 06:12:45 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778073164; x=1778677964; 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=4RTucY8aaP+myLH4AwJ8twqF7kqXmyO5eBRlw0Iai4Y=; b=oKz5zjXOd/bzfXtYbZSoiC/SAYBWPD96bynw5RwflGAEWG43vAwKTJWUVm4bmkk6Zg qbGofUu5XQTcoGnBnIby6dodwSz0u1xG0t9QrlibsQenFqpoc0WgEwUptwqOpWoJrFzk qX0sByHpFmXGayCfiD2Cw2luOcJGgyRGAIv9DBs3NZRxFTjme1SYiEP9YPFSjO/ZdXx6 iEBXdALCK/sp7BdMJLQjRlwL+KgjYDiK0qhaDpdRpH1VIRYmvw/V0WrHnn/F0LGQQc0z nepcbmRWHdldnvIEK9q7gwXPNZGXoaADLEsoIx4ujcfg0e8uF7U3+LvgXCyJhz4Gz4Yb jXIg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778073164; x=1778677964; 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=4RTucY8aaP+myLH4AwJ8twqF7kqXmyO5eBRlw0Iai4Y=; b=MJpU3vZhGvnt11fWKshn6iMPfQMJhOt1bhMq4lw0WwRGcBLEUMFb8BoNWqepzs329b Cb8bi9b7XcG9t/bRA0yr79+Mlj/xOFac/Qw9eeszh3BLvsAt2FAFpU5epyjcH5NrIRCT MSiNQpEHjbrmbbEtfRgEHMZXYJe1QA9S6SC5IEvLgUcCcV3eGuWpYvSyXza7MCpAO1SB lFDElcId9S/XbZMoobpqmU8dLUbX1vlpdBcRzbt4cRJP+HghlGXZfbeMNUU8hteLYK0o 1wxVWZll35aLUl5vB3fW+wyWLyF3WloiXkFk+ZqtyQGphMohi1sZZ0cN4/+yiFO/FJ5X VO+A== X-Forwarded-Encrypted: i=1; AFNElJ8ClKVAu9g+GA4WauB+qxWI3Riwn5+HeJh75ISCvqh6bSvpGXCacpF4+ZU2rj9uHMuGVFbD6WpFfJlW5VU=@vger.kernel.org X-Gm-Message-State: AOJu0YxkmDXK/IygGwW/ieHZsjwxQoIeW7i12BXYJdEAvebQ2++k9BeO 49P99Qitz8fSje7pGt8dhbbEIEm0NvsjF2E6799Ie6mDMbNpF7c6PSHh X-Gm-Gg: AeBDieuY+3tGQMpKEsIdPwhLlnJvyh+GyqWWpWDrsu4brghQDC1LyLVAdODRodv/FMd 1fwbPFAWQnBvjGDv37twHJwZtfyXlCfkRIINNl+qMd4RCG6GzAeJ2o0ylx7kfEG7xDyL5CorHoy On/Z/sbA7VUeMzy5otxzOjGp7a5VcInYyQb4TQc/PdEFVz0eHfJcdHxoBWA3/PccXtRLvnJk8xv bd2JkPKJMe/3TNB05p1QBPMIf8SyNcnvp3wxmiSKkr+hoblmiOF63mNwC4UMROzjomzv4jxk04S B5VD6V9T561VELk8r6SW61RQA9WUK70w0tT/pOBjdswCleFTUVqtoCsQXgmmdLYlTNRgE83vHlv EG6moCkxY1xXG6bpFWN3vMPqdYDEKSVCVVk1Vx7ebnv68wapkITuCVqOOpkdQ3AzCTjbze6Ss1Y 9m665RGZr8oHx7QEJfMZOMIu5ppA== X-Received: by 2002:a05:6808:1a14:b0:479:dcb1:dce3 with SMTP id 5614622812f47-48041ea1cd5mr1677721b6e.0.1778073164242; Wed, 06 May 2026 06:12:44 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 586e51a60fabf-434548d9decsm17327859fac.3.2026.05.06.06.12.39 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 06 May 2026 06:12:43 -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 v6 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing Date: Wed, 6 May 2026 21:12:17 +0800 Message-ID: <20260506131218.1880852-2-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260506131218.1880852-1-houminxi@gmail.com> References: <20260506131218.1880852-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 --- .../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..c3fc615f3744 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"), # 0 + ("OVS_KEY_ATTR_ENCAP", "none"), # 1 -- placeholder, no recu= rsion + ("OVS_KEY_ATTR_PRIORITY", "none"), # 2 -- skb metadata, not = in ENCAP + ("OVS_KEY_ATTR_IN_PORT", "none"), # 3 -- skb metadata, not i= n ENCAP + ("OVS_KEY_ATTR_ETHERNET", "ethaddr"), # 4 + ("OVS_KEY_ATTR_VLAN", "be16"), # 5 + ("OVS_KEY_ATTR_ETHERTYPE", "be16"), # 6 + ("OVS_KEY_ATTR_IPV4", "ovs_key_ipv4"), # 7 + ("OVS_KEY_ATTR_IPV6", "ovs_key_ipv6"), # 8 + ("OVS_KEY_ATTR_TCP", "ovs_key_tcp"), # 9 + ("OVS_KEY_ATTR_UDP", "ovs_key_udp"), # 10 + ("OVS_KEY_ATTR_ICMP", "ovs_key_icmp"), # 11 + ("OVS_KEY_ATTR_ICMPV6", "ovs_key_icmpv6"), # 12 + ("OVS_KEY_ATTR_ARP", "ovs_key_arp"), # 13 + ("OVS_KEY_ATTR_ND", "ovs_key_nd"), # 14 + ("OVS_KEY_ATTR_SKB_MARK", "none"), # 15 -- metadata, not in E= NCAP + ("OVS_KEY_ATTR_TUNNEL", "none"), # 16 -- tunnel metadata, n= ot in ENCAP + ("OVS_KEY_ATTR_SCTP", "ovs_key_sctp"), # 17 + ("OVS_KEY_ATTR_TCP_FLAGS", "be16"), # 18 + ("OVS_KEY_ATTR_DP_HASH", "none"), # 19 -- metadata, not in E= NCAP + ("OVS_KEY_ATTR_RECIRC_ID", "none"), # 20 -- metadata, not in E= NCAP + ("OVS_KEY_ATTR_MPLS", "array(ovs_key_mpls)"), # 21 + ) + + 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 16:18:27 2026 Received: from mail-oa1-f45.google.com (mail-oa1-f45.google.com [209.85.160.45]) (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 3B625477E57 for ; Wed, 6 May 2026 13:12:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.160.45 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778073172; cv=none; b=Eszd5sYWLSLvn9Hzf8Kw/q5YpTYPCa+lPgVrL0+JnXEVGb4wXiggDWHYTXmPWv9hIUM1HBg5QKSg2OQwNisqPKYyKFVF4nEmFKxRTxHwB09H7aerbc0VYTL+7nREYwXom6z4x0NpZ0zDX3Ckz6hRuUF9HgLRMqJwDUPSPxTFL34= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778073172; c=relaxed/simple; bh=7anuFwbxVPib5Qwdrz+mL3upuMV9kt6FzTMasaXomEY=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=iGPFO7+UY3YwnhNNQxEFW0jdCvdRQZqx6bnrWZaPTqhk3EGS3g9ufrsI1XAzS3jVQ12ABjF/snHocP2zec+A2oCsTTlGjk57UWfGd5NU279xIg7dqWIZdeZTRpveQ1j/N5q7vq7FPr/eKq+xbSNSozBc3vSKE3Zasa0eagFd1cI= 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=ndtEbbpy; arc=none smtp.client-ip=209.85.160.45 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="ndtEbbpy" Received: by mail-oa1-f45.google.com with SMTP id 586e51a60fabf-42fdab683a9so4667620fac.1 for ; Wed, 06 May 2026 06:12:50 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778073169; x=1778677969; 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=TtDHstlKXDMwZVDImgv2Go/2LwRbeKrLJRmqWEJNiDM=; b=ndtEbbpyjL6PTXhGzuuXUUNYgkpn41uTsrQGl96ZzWth1JedXi82z/MV7y/CWRkfLY yLJ0Suk7lsfSJrOWqEuWxkHv8wN/8uhAAYS8rY+U5HBuSlqEtoAFyPDsW8EdYiSTTsHP +8eW5XDBXIoxNY0nUcbafblovAtET+oV3wjHGEVEPCedkIGHaU3F25rnFEwzVxu0DzsI DYn9XvYaJWVHpz1MGANLgMpOX/iC/r7A98KdkWsoWvvUQr5laCmeJVwkPE0S2Pc7yfp9 Pzo2flvKcUBJ+dtajWe/rziU8/usCKaYm8YOK/ZbO7p6wGsi6diarzyN/dlB5Vn7J4mB hN1w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778073169; x=1778677969; 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=TtDHstlKXDMwZVDImgv2Go/2LwRbeKrLJRmqWEJNiDM=; b=EzzW/VO0xJvqEDSkh+h6UKk5+lfasfUh43VSmy7QB/EmTtlaHBnThBinp752M2uEU8 AJYuzwrRavFjFZ9a3HkDoU2P5esTZ2nPcV3fF7syRZTQM9lO1c1djfesGSa2y7t895tV KBcOJ8StKF5Fy4Pp3zIXsqnwdIf+RwPtVyz8bGqPFK4uWp/fxxHjH+dxmEekClSb3Jfq 8m7fsl0edIisSrnH+lO2bsUIpnjZcY4SABMB/XhITDUWX9I4LyzAs5q0VqceUeK63sB9 e7f8UN9NaSc13//Cl0hAP2I+4UnMyGdJ0GgE8eS/FCqP+UYIxTBFBF+3CXtqNzH9WHsy yvgQ== X-Forwarded-Encrypted: i=1; AFNElJ9HWRSDLeokjdFNI+PCLBZP3TCTjLZ1jDwhP1DmHxsBYA4m0mFA/vvjq3u8YFM9tSqtkAmncex7/CklvRo=@vger.kernel.org X-Gm-Message-State: AOJu0YyI6XT4O50PN6JJmsHXM5ooe+jgsoKBjYFL3CUz8+/g/MEXnf/g rUUluvuZC0DeD56yDcU5K4RROTjQxHaPaG/oq4trwtaS1HWoYhAsUkB0 X-Gm-Gg: AeBDiev0MgdSUlM8TBHIoXUCyjoFMnaNpvw870Pt/qHmYP1cWiLTGBkPFozHcSAwuEP o31zsqDRmUNtIT3QdZLbvO5EuL93HDsxbLMTJ2coY+gnQQ2B+nrjD4lmvF8E63GSJ5UYyItA6gK qaB2FX9LP7TfkfRQNt0jAviw6SbVycxqeVuH+n2o+1/B8ZsBJs97JrTz0Np/xuQ8W/evFEaVWR4 HkD6AGitshSJaOnkVnHBXatVfNOw7MWz28NxlNTsWtniGDVbZuwQ+t2kw2+5soqvgZgh1cIYoJG sh8sPZoWUT+FLZB3BQ7+npsQB3L5npSC2rOIgzLmyicjc6mpevBau9SXqd7jdj+ZKDBNeO1y2Nd CxEsCJUNoL4ZPBjP9tftLjDRSEQ1b2YdXLjtHN7QqpJEbJDJubH7vV4smVyK0shgAk67cRKbabN 6QDVHDeVRvqWO33wSiSFTc8/Mdww== X-Received: by 2002:a05:6870:c18e:b0:42d:c200:4214 with SMTP id 586e51a60fabf-434f663b9ecmr1936790fac.18.1778073169035; Wed, 06 May 2026 06:12:49 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 586e51a60fabf-434548d9decsm17327859fac.3.2026.05.06.06.12.44 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 06 May 2026 06:12:48 -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 v6 2/2] selftests: openvswitch: add pop_vlan test Date: Wed, 6 May 2026 21:12:18 +0800 Message-ID: <20260506131218.1880852-3-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260506131218.1880852-1-houminxi@gmail.com> References: <20260506131218.1880852-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 --- .../selftests/net/openvswitch/openvswitch.sh | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools= /testing/selftests/net/openvswitch/openvswitch.sh index b327d3061ed5..a5636881760b 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,82 @@ test_tunnel_metadata() { return 0 } =20 +test_pop_vlan() { + modprobe -q 8021q 2>/dev/null || true + [ -d /sys/module/8021q ] || \ + { info "CONFIG_VLAN_8021Q missing"; return $ksft_skip; } + + 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