From nobody Tue May 26 05:39:45 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 B8B24402BB6 for ; Tue, 12 May 2026 07:09:09 +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=1778569753; cv=none; b=seuYfnQzXsE7dGJYVQUqqE7XoP3/0LKa09YLNJ99zA7SFaldfTGpM8XhgXfitx8unGm34plNfJaSZiCwwAgMAvqd3vKeluECh5xSQRVdJhlwVNEFuYBulpUNT1dyyvwP5dRL6NcvHTUklke59F3OwQ9E3NO6f6ZCViinFKVojpg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778569753; c=relaxed/simple; bh=F0D9uBMI5aGMEztB75k87gNX78j9wlAlaSDvefDdL9o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=FIbH3cSmdwjFyOe3s5DMYsVp/lOqHdVmWqdpFXuicUHj3AmqjVMqItIqfR/wJr8iCn8KaGnAj8/K3EF0lU0hszAtUaQwpSEe8TCiA7G3/suNbxPdJVOCr60zP99UKInO2gOoFDpMSxA6OEqTYlSKbujdPQEL1xva5UV2aB6SKaQ= 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=D1zw4MJp; 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="D1zw4MJp" Received: by mail-ot1-f44.google.com with SMTP id 46e09a7af769-7de46b8e432so4950025a34.1 for ; Tue, 12 May 2026 00:09:09 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778569748; x=1779174548; 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=D1zw4MJpyLUTR2K3pMA6DS1B8RcOqMaNcmSYdn0yuWxVspKBxXBlP7urdWIxx8Mr/X NK32xukjUsHyW4hfjT0bwi+paMZp8DsyfdZRKSoxvWqKaBSEDNEMQ+tTQYxwupncGNSt SP8+thqEVCJGzEi3iO7Pm7v2/CuaUA7CESP3MYT61L1aiYwTjpFTzd9+rCWJvCVBfL64 k6qoqfcoXPr0+dTpWRu/btRmgS8q/F/gdwG0Hywqqxc2v4pbq9v1XM9Z0ii9xV24sH+f /XQQn+0FGi6T8Bp0ugQrjOqv4/OIrm4EpsiT2NEVw3UYrwO3gWYQgICBSOinLgIzS6qH qMHg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778569748; x=1779174548; 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=mqdQuR5A1fdZSg/mql8PZqtDdrdUIn/gneXoj/+cp46XuEvPtaZFs94Qet4M+6O+bE p74v2/xH37a1ew2SylANSsUxLfDsNfpiNlT7AIXe65SRxlO1AoJO26sqS8kZFJmIUABs 6DPZVVct3nM7IdwlYBvYTJ2yxh1da4Hzlv651qA0zlcNcA4e3VEouwNfwi7DIL4WYfKv ScseQl8QpFq71UV2ZUP/sXgPleA50OWRVJ5xEACORyhitv/kgLLnYQ+mCUPvxIpqrz1c KXA30Wn47rk5DrHE6CRfq7u5W04eq1zUWgpMnm8KKYSXmv1Fj6YUnNTVKbV3+O0KL9ql 3L+Q== X-Forwarded-Encrypted: i=1; AFNElJ9GGvUWOpSd6+CjYR0YAIKDAjHdV5a/MdIayQiL/3WYfBs3AjozlNQ3JGM/q0aF86vlnOBtYGNWp4CI9XY=@vger.kernel.org X-Gm-Message-State: AOJu0YzON/hYwRp7SNbP0DQ+GTqG1hr1S6J8AtRwjPFMYL70W+9TSVWl vGjCkBxri6cH1iRB36jDnyXShdcgwqPzIoKXBshCXBKESMV1FLc9Cojo X-Gm-Gg: Acq92OED/BmLMMQ2ARyvu03C6EJuCn+T4euTKCQj6nen2no6WQDzxNgMBcZfwbvwhFg 1hUhOpIsjgMiXpNUFizpZPGWfNj8OXOCbAPGwCJnO0zTcyrUkxFSyCUxFFeSAQzMSORAI70I89d Z23iIFYdx5wS4md4ub7gYxyfVUtCoYCaJy1Hyxhf62/2xzXnWOkfCyu2bmiQKswXDIbPPwpwCTQ pVPSAyE4hFaRFcP9uEYdJ0Z8IYlFGsZYWWYR9Mj0zcjEGqi4yu028hBtnlYYGaWe7TO9ivwCj2c QhY+dRyWWoJ5GW00QTedKb1WBmAuiOYGWHzW53rBUgYteYwjhO0kbPKmoxDXJicMQnNJ02xch6K QCWLHYei14sDg9b/zgU2UVk5M5ywQBQtClMGjE92iAkxUJGoU5JhMdTDXTl32qfd2GQ7Ja4ZyrR dO9sXMSp6vXxMlcHg= X-Received: by 2002:a05:6830:44aa:b0:7db:a184:2b35 with SMTP id 46e09a7af769-7e1def0c8f1mr16606767a34.8.1778569748052; Tue, 12 May 2026 00:09:08 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7e367a18481sm8417658a34.0.2026.05.12.00.09.01 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 12 May 2026 00:09:06 -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 net-next v10 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing Date: Tue, 12 May 2026 15:08:40 +0800 Message-ID: <20260512070841.1183581-2-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260512070841.1183581-1-houminxi@gmail.com> References: <20260509135423.3433910-1-houminxi@gmail.com> <20260512070841.1183581-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 05:39:45 2026 Received: from mail-ot1-f47.google.com (mail-ot1-f47.google.com [209.85.210.47]) (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 C5F92441020 for ; Tue, 12 May 2026 07:09:15 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.47 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778569760; cv=none; b=VIgFMeKCvzP6uiJ8aoMwXiC+3rYddZCZ8eVTwqVAE6LhLNbpcZzPSF5CXi07XpVCn6Taqi4HT1NkQ4kH8ay4uQnKBs74rTb1+cg6V6csmDb2dyRtBMz8CowqwA8dxK1L1wHydnRhKapAp5svvWpiuyiB5ONRrkD785Wk1y3vRnE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1778569760; c=relaxed/simple; bh=GQeOcJ4x5WRRquDnsAee0Zm2Twr2g7DVIbPAVoTtP+o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=E+rTTwjZmke+rqw0/JYsXJUyIzUCDfAGBniPXl2cHbvGWptvVmUttdzvcNjHVD0ssfAlVdSMZQtYQ2A3Jng3VRi9Dq57SuOLsXqGJv5fnWNGW+taDjzBWIsKjU6FMGjjr2LJPUkqRoAX+IgJ4UMMLMBlwzWLR/+IJQbmRz16cSk= 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=IoY5bW/s; arc=none smtp.client-ip=209.85.210.47 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="IoY5bW/s" Received: by mail-ot1-f47.google.com with SMTP id 46e09a7af769-7dcdd1b492eso4650276a34.1 for ; Tue, 12 May 2026 00:09:15 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1778569754; x=1779174554; 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=IoY5bW/sfWTh5wk4leCRcGmEgbvQNkK/BMPCEUhqCi/+y6F3X5AGO3RTwN/D+pkozh iImJOTeK4S2/cmKNHxq2mM+ggay7qOePV8W+aSh7tKIlGgTCxSCcp6LNRLOXMn3O5rcM F+iajYmse97EZlNLBuEJBMmn9yuAXNBAhEpRHqNmJ/dV3AhQeD0uS3hs7GDS62tHjyRy Q78Fc7cwCLfLqgTb3jCpeYUkrB6eNbpz1w85XqebkAaIQ4r7rLxQSpcxPqe9GLdhr0RA d4D/sZm/KbooAjF4P8txgNVWQdjD6wP+T8ul3lRqKOS59ZWn3eeccKgjORW/BxMDbTEB siGQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1778569754; x=1779174554; 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=PBxX7ecF21q1LE/msdG35bYaXrx1tWAkWIbfncooGwAhesWpmHVYWbDh1CcoJjhySK eYDtJhzk+Ho73a/JIKXMZyRJdgNpbs7C2iOvY6mI855RB0apy371VUfpqWmbduL0MYCB G5qtAVuwDEqOu17YRLmDsZ8wWAiSX8dIT6iUdBT44Y5F/kEJLEXEXvzOOJoflnSfAYpp 3xSK3VpUm8EiOxHDBBrYA0gUk6aQ9Tmd8BouYU72KdhnCWL392iDO6RcE3uXSF/SGAvX y8JDmNdpsMrwFQTY32wzBojAXRiv/mUcvDRIPqa59qyzBO0MMgdsHhqnHHh3FFln70Ez yRNQ== X-Forwarded-Encrypted: i=1; AFNElJ/zoR47AD4N4xZOQGwrfXY9JE1J1reFE8oSfKnUyeBwRYkvBZx170WMEsG0JDooAp+NRQnPJ5oOL+mpDQw=@vger.kernel.org X-Gm-Message-State: AOJu0Ywya4G3oPvWoghTxePqvBBaLihVL+dhccw5kxf1OBHjIyww+3fy HpjKdlXR7eARVQewg2W9EOP5kEW3PdqCwCBNerNOvXBtrIrLj5kRO1Op X-Gm-Gg: Acq92OGNv5piKZU2g/cFSQeI5tioDyYAxgMr7ki53YTfAAxAR4MIkoEXDMaWNcIdMwt 9IOuXAto8UamqyuGLtbyZecBdHKA6BmkncFSdWWKFpygTecYIaFauVetytsYMLR+MEEp207azoR Ycf+/io2bTU5wgvXcNhYiise+pIOHe82Wp5kJOGJ8q+RAGfbEJ6vH+eshDahMxUw4B3TpJEsZbk k2JN96pW/UbSaf2q7Z9H6cMOcKtdg1B0NK/omWp7bu1awMlyF+cnz5NU67iwH1UU0uUUh1HTwCT nbfT4JxJZI+bYFQNLytdG9VDI3jpsLNHzMUUx47INIvBCtMQECFw+03vSCngAYTjipnopQKdUQn oDGcUk50IMc5nPHXylGuamE5ZxSf1Lfn2XjAkbx0r60z+v/IuQZqTqrn/gSN596/Q4s+UJttbAl ItJQK5wUipnwmbx/o= X-Received: by 2002:a05:6830:c8d:b0:7dc:d7b8:aa80 with SMTP id 46e09a7af769-7e3bd3a1d5cmr1022984a34.17.1778569754252; Tue, 12 May 2026 00:09:14 -0700 (PDT) Received: from houminxi ([72.244.37.221]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7e367a18481sm8417658a34.0.2026.05.12.00.09.08 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 12 May 2026 00:09:12 -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 net-next v10 2/2] selftests: openvswitch: add pop_vlan test Date: Tue, 12 May 2026 15:08:41 +0800 Message-ID: <20260512070841.1183581-3-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260512070841.1183581-1-houminxi@gmail.com> References: <20260509135423.3433910-1-houminxi@gmail.com> <20260512070841.1183581-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