From nobody Tue May 26 04:50:11 2026 Received: from mail-ot1-f51.google.com (mail-ot1-f51.google.com [209.85.210.51]) (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 4751F256C6C for ; Tue, 12 May 2026 06:39:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.51 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778567961; cv=none; b=SK307kdc/wC1GC8FmhHFUrYZTK3qlVh3/0C6ox0kGbX8HNhS/bCDDsQGlpThDaInbQCx5d11eAqwUdbNJYl0mN7V/82mqbnj0IKY8yChSnnDcIKY1pdSrR9cFCxQngnfH9Xc1LfPNDXbAtvV47UtMlA+sBTCd0t1/O+9oz2ZJKE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778567961; c=relaxed/simple; bh=F0D9uBMI5aGMEztB75k87gNX78j9wlAlaSDvefDdL9o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=nw2DL5nm47Viq2zwvWtn1c+a/QBN8JW2fNgFK/vFAokZgqHfCfjIfWKFLcYPcMfx2dyr3nBeJumx8v32V02sUi4DLjrQKOuGVIDX1RKofBAuo6MQI7SoHGmFQ2all2UynBXwppjY4cPElfNOsD/K9KDhYXI/QS5cDrgqumawstM= 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=WVT1Q9Ci; arc=none smtp.client-ip=209.85.210.51 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="WVT1Q9Ci" Received: by mail-ot1-f51.google.com with SMTP id 46e09a7af769-7dbec19732eso4790841a34.3 for ; Mon, 11 May 2026 23:39:12 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778567950; x=1779172750; 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=ptrFwQmMmjwCIA4td+4pg3ne0cwkuwhONvtajOVMhB0=; b=WVT1Q9CivYkwRF3QTrdhq4YH+2u9yFajKr4SSfRGHTvopbps5WiCqpgmsaxk6NsQNP VbT4OYO0SgsE4H+jAPZtnKyg7mrBjbnWX+0D9ETsyse8JII5pJ3yKiLP1XU9MoRhUKY7 4N7Bm1ognzVX0fYwqRuyhA+dMFR5pl+CIGc8EebS+gsE3buDJKK/TEKFYZC6YE7FzP+P fawFZqgze7X8aFozQ5hYKInbRC536ZIYatqFSV8wU9u6AzmZo0wW6gm4oqMtfCNeHXK/ th2ht2SmAdNJ5I4NWXeHDmTT/iZR7UXVVSLbOL0fIYnxQFR30gu7FB+0Jl4x3vACreQw 59mw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778567950; x=1779172750; 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=ptrFwQmMmjwCIA4td+4pg3ne0cwkuwhONvtajOVMhB0=; b=JKGihzf8wSJVoJLyxPjgdaL3nEaFx2U5UFsA208yYrGAL6/+qh9S8SMMprEHxLUNfT vMSfoxgq2/C6gwTfPYisBrXwEMepPRhCnx6ZzGBgSrFgEd2vr4EabbT7W14L4pc95WJX dhb1a8QfheVWmRILG5IhmIsL4eCmPJIY0n/ESezvQvON5+8ow6mELnXtvTZA+bj/ajpS Gng6FMISEZjJsG3Q2T8/fgZK4vhdIAv1aRvs7t2aNvaO/T43ApKryh8ii19JOBU3p1SY YP5Kbem1Fw7S4YorgGoiKdd6ZgrgjrzTZ4/XhtzG/JKPIMVPk9ht79cLWaQr0CEUzH4L bQBA== X-Forwarded-Encrypted: i=1; AFNElJ/9oOn73WZ91pHEJfrY6xnvIfUkkkfXgSZhA/kJHgSRmaNBDolGwHU2W2bUbmNVEyeZYkfDL4sIjPcF1Bc=@vger.kernel.org X-Gm-Message-State: AOJu0YxX9oKJrgbqLQR3oQN3ML14MOdouF/oFVGPe1vrpRfhQoBLQkdR rLL0l3HYYmgNd9zRXFJn0W3sHWSbvgrCUihiU7zefK81cONyc9H8pqMn X-Gm-Gg: Acq92OGWZ/n6sRCwgXQL+Hph7vU8lDRRplToRQqAiPCtxBx8ZoWenizLC66YnOeRkS/ EOf/pV837WBT5gphO6mQ1HvA0KAqkjcVVfm1+Tw9Wxux38s0zpdOHuio3ZaEEEQHxKFIwHG2KwN h3s1VPKDA/OXC8G0HwU1hJUBLNS7sF1x4FJ930l8QHT5E3Qs1SbI0g/l63BwFOnIi56SENm4vIb 5VBJ0/rrwpOtSgEykGth05s0PsmSrED4ms1MDUWUomE3k576S++W7206SfNNFSsjDJGhKCZCH5e 3NVR013l0gE/V15xPMz+x0eS2fT9K8EAYdD5J/jm2BcKn2j1o7m0cj26rWd7JHv+6trCYCTUsxs tK/cFDbXQjaOakzfF1QiPNeVq8/ZouMGhtCV/3jsrysojeabUZprEvl5RnUB2+sojxh4fRc7xiK wlTlUbGoBUbvfWaP8vikX58FnrBw== X-Received: by 2002:a05:6830:7105:b0:7dc:e30d:6498 with SMTP id 46e09a7af769-7e1dee81549mr16665875a34.1.1778567950459; Mon, 11 May 2026 23:39:10 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7e367d9d788sm8183783a34.24.2026.05.11.23.39.05 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 11 May 2026 23:39:09 -0700 (PDT) From: Minxi Hou To: netdev@vger.kernel.org Cc: dev@openvswitch.org, linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org, 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, Minxi Hou Subject: [PATCH v10 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing Date: Tue, 12 May 2026 14:38:49 +0800 Message-ID: <20260512063850.1133046-2-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260512063850.1133046-1-houminxi@gmail.com> References: <20260509135423.3433910-1-houminxi@gmail.com> <20260512063850.1133046-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 encap 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. Signed-off-by: Minxi Hou Reviewed-by: Aaron Conole --- .../selftests/net/openvswitch/ovs-dpctl.py | 324 +++++++++++++++++- 1 file changed, 314 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 bbe35e2718d2..3b6a26e265a4 100644 --- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py +++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py @@ -369,7 +369,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"), @@ -426,6 +426,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 @@ -632,6 +635,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": @@ -725,7 +736,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("):] @@ -900,11 +961,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"), @@ -1635,6 +1696,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, 0) + 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 ')' in nested encap") + raise ValueError("encap(): missing ')'") + + 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), @@ -1656,6 +1905,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", @@ -1793,6 +2052,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", @@ -1820,22 +2082,63 @@ 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 field[0] =3D=3D "OVS_KEY_ATTR_VLAN": + val =3D "tci=3D0x%04x/0x%04x" % (v, m) + elif 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 @@ -2583,6 +2886,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 Tue May 26 04:50:11 2026 Received: from mail-ot1-f49.google.com (mail-ot1-f49.google.com [209.85.210.49]) (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 B520C32E696 for ; Tue, 12 May 2026 06:39:19 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.49 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778567969; cv=none; b=e5Bfgm6gtPkKJBEehDDwETFqz/HXng8ie0sQ4R0kSPFBmtut/joh/FpxH7ySJyqMEZzsxr/BodQsgvntOtusG69e4+1EsGrAaFjh4Caaw2M35SC9yR5+gBoplmEn5drtPf3eMLcC5puHj4ldMFwjZTlfiVG/zzsFwjV2oeYZX0M= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778567969; c=relaxed/simple; bh=GQeOcJ4x5WRRquDnsAee0Zm2Twr2g7DVIbPAVoTtP+o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=uwBW6C14L2kR34RsPMxjl6PBaNHc0TGT4hRQaavFedXc34HPeGRTuc3IsX4XGmjyb1lhnWWuERSwfvbSy/QTruO7VC7oh9qmegL6WtXWazadT3kLhjOkoKauTv8APfFfAYM4jF2at/8EaGbSUyvoBS72gL99GJeAq/rA8uwAMM8= 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=QbCCn2/o; arc=none smtp.client-ip=209.85.210.49 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="QbCCn2/o" Received: by mail-ot1-f49.google.com with SMTP id 46e09a7af769-7dbec19732eso4790903a34.3 for ; Mon, 11 May 2026 23:39:18 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778567956; x=1779172756; 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=CzF0t66wEszl2BcU24gnomwmHovPmL+M9vg4PdgRwyk=; b=QbCCn2/o24e7gJIgYYugmJ3OwDw7113fUb0SMJUpYXErZhIRwXp6LOVWgqTF3teBfE iZXSbTUwt3g/5uim69DPkAC5HXRZ1zyJDUAgMbW5tR2sOX7Yl8wJXokkSJVFNwsQ2xwR S+GCf4W4rI8tvAc+F7zZZiBWDY/YAmTIslrJeMrF4Fd9Cin8Ys+Z2DgFfh6JmQuhD+vZ iJunyOmOdnW9Wk9MQSSpK5RSzSj7mi2Lk1gWwQGnMExQ7hxWv2Y5DkX5smU/m9aji/Ws 3OLOxMuNj5zWFsH2UnIP59fnpKRM6y/50TENEwPEgOEBY6Sj5qsXIVTbCqSGWre9kurV VC7Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778567956; x=1779172756; 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=CzF0t66wEszl2BcU24gnomwmHovPmL+M9vg4PdgRwyk=; b=axc2ELMuGeYbPAT/DbqUSszhEW0ZbrGOLsLZD0bV/t+j8uZgPi6l8mxOxy1g6eMl/x brwpQIfQvqv8i/qNjCMnnWSkCv8G3Rtz5d9HeBlPGezIw0A7I8UckR2kPmpzgRKhKr+J fuLh+cI9gnAd0C8ykdCnYeG1PY9ggp4FvF+C8iTzk990NJoU3/1xH3U/B4CvW2R0AT/T 30dk+XQ4fxiyrArmmadcJF9ASaU8U8mMTm7TsF47uvxOEdYPowoCGEkzNR+h2YKXKFlM C1upjpb7c5lHRGoVdAXFAocRud/Hh+GYvdjT/PVy0lOQlR1wpLy8x2yZq3uHCggpxUmD OIzg== X-Forwarded-Encrypted: i=1; AFNElJ/AOhqPDLd4fjO+oX3tnInYKB0ntZjOgEnkV5bkjnaM6eegNX6sHNLG1n7xzMKi2bIHJz+TSyMCQMd2lEw=@vger.kernel.org X-Gm-Message-State: AOJu0YxaH+loWKnBB/BCTXWF6QW6wQVieAVm2mMk/vQbWIOMgP8jZ8gJ 6rJM1dPBK2cOn0ovN8SSH+DaPO3MFZMoJR4GSHPjHcz52gNyyUle7Fz3 X-Gm-Gg: Acq92OGczE3rTwMREQ5PmKcd/5JOkQs4pynDlcbe7N6qq03NxvFBvv/LLclWFUhoZ+8 5yw29DscrF3G6xPL2dvvqWVtOx4MkAUTy+hzhDQHfFgcgaLgRCZVW9uR8LHCZ80TLpCri99hXn4 xtUEH+KzazqtkhSRd/EFmnpWV1ZdgC1/zPFPOqZ2TZLZ8UasL9Y8X/lqSLutyxhrwSjG7geXSzm lmflXYmU37Oo7YbiU3Ls5IvyiQ6sfh+tshb8hwlDW2OtaKTfzv1X2BfFzGb9uceAwvwx2Zo1ceh zRyV2QeY+z4M25khdDEMmPhBfTArnsYFmfR5B6BMB7csw/xZCSQtLwuZ7PlBGwKuf6htvqVF2H2 aFE9sUsu9N6/oUSq4XCa1YN6SfThZ0tz3HjPou0gYPn+6AmSheXpThPKGG91yN7avES//PrK38I s6Rd3UjDdUXl2bqfCQnX/bflH+jQ== X-Received: by 2002:a05:6830:6d48:b0:7dc:d0cc:922 with SMTP id 46e09a7af769-7e1df194678mr16537449a34.27.1778567956356; Mon, 11 May 2026 23:39:16 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7e367d9d788sm8183783a34.24.2026.05.11.23.39.10 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 11 May 2026 23:39:15 -0700 (PDT) From: Minxi Hou To: netdev@vger.kernel.org Cc: dev@openvswitch.org, linux-kselftest@vger.kernel.org, linux-kernel@vger.kernel.org, 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, Minxi Hou Subject: [PATCH v10 2/2] selftests: openvswitch: add pop_vlan test Date: Tue, 12 May 2026 14:38:50 +0800 Message-ID: <20260512063850.1133046-3-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260512063850.1133046-1-houminxi@gmail.com> References: <20260509135423.3433910-1-houminxi@gmail.com> <20260512063850.1133046-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 | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/tools/testing/selftests/net/openvswitch/openvswitch.sh b/tools= /testing/selftests/net/openvswitch/openvswitch.sh index 3cdd953f6813..8cd5b3d894ab 100755 --- a/tools/testing/selftests/net/openvswitch/openvswitch.sh +++ b/tools/testing/selftests/net/openvswitch/openvswitch.sh @@ -28,6 +28,7 @@ tests=3D" tunnel_metadata ovs: test extraction of tunnel metadata tunnel_refcount ovs: test tunnel vport reference cleanup drop_reason drop: test drop reasons are emitted + pop_vlan vlan: POP_VLAN action strips tag psample psample: Sampling packets with psample" =20 info() { @@ -864,6 +865,83 @@ test_tunnel_refcount() { ovs_wait dev_removed dp-${tun_type} || return 1 ovs_wait dev_removed ovs-${tun_type}0 || return 1 done + + return 0 +} + +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}') + [ -z "$ns1veth10mac" ] && \ + { info "failed to get ns1veth10mac"; return 1; } + ns2mac=3D$(ip -n ns2 link show ns2veth \ + | awk '/link\/ether/ {print $2}') + [ -z "$ns2mac" ] && \ + { info "failed to get ns2mac"; return 1; } + 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 } =20 --=20 2.53.0