From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-pf1-f169.google.com (mail-pf1-f169.google.com [209.85.210.169]) (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 1AF463B27D4 for ; Fri, 1 May 2026 13:39:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.169 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1777642783; cv=none; b=T0i8TWMl/+UY2pl9lFDFdYMQMz7iogNl2MZzuJz3cnhSWe0+aYJSOsznyLDDZ0rfoZA/kREzW0C4zixH5DVW40EOiT/r0oQcmPj9Kt945TB8p9sUUf9e9osjh11g7d28wbC3ES+5ySJwTdRidjxmW8qqts2nyIJ2LDi/wrP0fYs= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1777642783; c=relaxed/simple; bh=Y0UWSrjC5k4StzAITY/AGPh+Kwc3y0vDbgyBhXz+Znw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=kA/QVBNQ/u2uMi80Rs8wYoqK17550C5K4DP9N70xporsP22r808HL3iaalnMc+0fJ4/sI/VuGwNPvXrQjMg20wqge5hvRzoW/eoFHoxDf6a6YOpB0fuKP2Uo9b+AJIYB6EulXtEbrcxXCDKKc/sPSxJ2frehXdSQilMHchLNS2s= 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=Gl1QDSFw; arc=none smtp.client-ip=209.85.210.169 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="Gl1QDSFw" Received: by mail-pf1-f169.google.com with SMTP id d2e1a72fcca58-82f0884bcfaso1353857b3a.1 for ; Fri, 01 May 2026 06:39:41 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1777642781; x=1778247581; 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=RBUdc3yx6OeZeNgFbIeAinqkyWh6EhE9Q31G7NtAQnk=; b=Gl1QDSFwUOWPtMG013XIFvhu5+WpCKKE+KytRmJLGs2qe3p9bPXKM52vtSv8P3lHto vLD1fZtyH8sos5ZinJbqYLDFvOOWKh8rPU7s5OrKq1VMsq3P1pHkBtKClKL0c05gILSQ PTmSTCZ4usw1Os2FZMGuXpAHxcPMYJNSJHfk4eCDnsvFFNNWXCJSlR3c+tVpspXP2pRh LUAIpqLWpFKRcakyJ6sJQP8R6yUCZbX7p+BHIi/qk5MnEwbveklnpO3DmTXQPKATACpG nnTPN6P+KnoL0rPXu/Hdi2jgguaBkBJsdcHeLPvNxginGkDPoNpQpe8lFr/fePvWTG9P O4nQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1777642781; x=1778247581; 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=RBUdc3yx6OeZeNgFbIeAinqkyWh6EhE9Q31G7NtAQnk=; b=s0JGZrjAm9MvuLQibuYrRuCE0Wh74dnel4cgOzM3Td38CumG/47sOH7tsKd2/GqznC IlBA7VnRVWN1y2YxMkc9LcN/oSMydxTfinu8SWFLRS6TXK3zStH2xcsgmbvpiVPt1MBC j7ZyEX2XWBA48s9MLbCyLfJA7gK5DKV3G0sX+7Fepv6lyFhiia9pExkNYp+ab48RMgZQ ES5lMAkE3SgTa93zevG1dnRo63iNwrW4woDL1CWQFW7KB0BqTdOfyyQQEp/XaYD3Ol5b Af/GdYq6T3l5dKpjV1HJYi3dukBALPMU7UErh1eykg9KQ4vQ7hbj2w5tjU0+JWoHMEwo Ur9g== X-Forwarded-Encrypted: i=1; AFNElJ/NWZzO/dfPHzzuZYxamP2Js1gEen069Z+5xOLl4Rjf2mqysy1Z6aCsOWb9rrXR5IgvV+VZMZH9fY3dZGk=@vger.kernel.org X-Gm-Message-State: AOJu0Yw7+eHDxzHadOQKzJLsgdh9reUMObQfr1S20Cq2SKcBwPGnu7XN /LSUP7hyJtn+wGwbiXhRskd8NutEUuGJ3CBgSYJlque2swi8Kk/XQfAe X-Gm-Gg: AeBDievKiY4Gbgw5OQDGNmKiynM0L1nHojwZEjOziPJHGRio7fd5b3YLhHWadEFOmZ+ 1A1yabBtxyek6GN87FA5Y+5WIwXroVD8gePovSD8/pqRO8llsVyTsSqQCeOqxugS7WUiuwKIJ4q uz2muTj0YAZ1crMd80ayZmkJ7ZhS568dKjJqIS130A5mTipXJpt0mIPFfj9uUu51lavbKP2ZVid 6e6b6nBaKFaL3QBlm3h4W5RjvuiXJFQZZrlu4+mx+wcLwq5mCqiZB0SAH1tbfroOKvqIm6/JB7Q cJshQwIOaJuJWoVAkPfubCU0UUWtHs++LBwPCheAAskdyBlwHB0x7mmIwvRjQcEFwl5B+m6dJgq xSV3qra3pIf0ro5Hbnh2hMPEQhsa7OOdX22uNHn+ocsex19jlSpP8jRpji8/A8aDOlfpeaopIgd ZaW5kld5uD43uT3zAsEBKckf90OJg= X-Received: by 2002:a05:6a00:f8d:b0:82c:77cd:50e8 with SMTP id d2e1a72fcca58-834fdbeae25mr9005825b3a.27.1777642781380; Fri, 01 May 2026 06:39:41 -0700 (PDT) Received: from houminxi ([38.207.130.223]) by smtp.gmail.com with ESMTPSA id d2e1a72fcca58-835158aa1ffsm2548386b3a.22.2026.05.01.06.39.37 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 01 May 2026 06:39:40 -0700 (PDT) From: Minxi Hou To: netdev@vger.kernel.org Cc: linux-kselftest@vger.kernel.org, dev@openvswitch.org, Minxi Hou , Aaron Conole , Eelco Chaudron , Ilya Maximets , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , Simon Horman , Shuah Khan , linux-kernel@vger.kernel.org Subject: [PATCH net-next v2 1/2] selftests: openvswitch: add vlan() and encap() flow string parsing Date: Fri, 1 May 2026 21:39:22 +0800 Message-ID: <20260501133924.3100680-2-houminxi@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260501133924.3100680-1-houminxi@gmail.com> References: <20260501133924.3100680-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: 8bit Extend the ovs-dpctl.py flow parser to support vlan() and encap() match strings. vlan() accepts tci=, vid=, pcp=, and cfi= parameters and generates the OVS_KEY_ATTR_VLAN attribute with a TCI value in network byte order. encap() parses nested flow strings and returns OVS_KEY_ATTR_ENCAP with inner key attributes as a recursive NLA container. The encap nla_map type is changed from "none" to "nested" so that pyroute2 recursively encodes the inner flow key attributes. The VLAN nla_map type is changed from "uint16" to "be16" to match the kernel's big-endian wire format. Signed-off-by: Minxi Hou --- v1 -> v2: rebase to latest net-next/main, drop --base=auto .../selftests/net/openvswitch/ovs-dpctl.py | 190 +++++++++++++++++- 1 file changed, 188 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py index 848f61fdcee0..317be7878937 100644 --- a/tools/testing/selftests/net/openvswitch/ovs-dpctl.py +++ b/tools/testing/selftests/net/openvswitch/ovs-dpctl.py @@ -901,11 +901,11 @@ class ovskey(nla): nla_flags = NLA_F_NESTED nla_map = ( ("OVS_KEY_ATTR_UNSPEC", "none"), - ("OVS_KEY_ATTR_ENCAP", "none"), + ("OVS_KEY_ATTR_ENCAP", "nested"), ("OVS_KEY_ATTR_PRIORITY", "uint32"), ("OVS_KEY_ATTR_IN_PORT", "uint32"), ("OVS_KEY_ATTR_ETHERNET", "ethaddr"), - ("OVS_KEY_ATTR_VLAN", "uint16"), + ("OVS_KEY_ATTR_VLAN", "be16"), ("OVS_KEY_ATTR_ETHERTYPE", "be16"), ("OVS_KEY_ATTR_IPV4", "ovs_key_ipv4"), ("OVS_KEY_ATTR_IPV6", "ovs_key_ipv6"), @@ -1636,6 +1636,180 @@ class ovskey(nla): class ovs_key_mpls(nla): fields = (("lse", ">I"),) + # 802.1Q CFI (Canonical Format Indicator) bit, always set for Ethernet + _VLAN_CFI_MASK = 0x1000 + _MAX_ENCAP_DEPTH = 4 + _encap_depth = 0 # single-threaded usage assumed + + @staticmethod + def _parse_vlan_from_flowstr(flowstr): + """Parse vlan(tci=X) or vlan(vid=X[,pcp=Y,cfi=Z]) from flowstr. + + Returns (remaining_flowstr, key_tci, mask_tci). + TCI values use standard bit layout (VID bits 0-11, + CFI bit 12, PCP bits 13-15); byte order conversion to + big-endian happens in pyroute2 be16 NLA serialization. + The mask covers only the fields the caller specified: + vid -> 0x0FFF, pcp -> 0xE000, cfi -> 0x1000, tci -> 0xFFFF. + + The tci= key sets the raw TCI bitfield (no CFI validation) to allow + non-Ethernet use cases. Use cfi=1 for standard Ethernet VLAN matching. + """ + tci = 0 + mask = 0 + has_tci = False + has_vid = has_pcp = has_cfi = False + _tci_mix_err = "vlan(): 'tci' cannot be mixed " \ + "with 'vid'/'pcp'/'cfi'" + first = True + while True: + flowstr = flowstr.lstrip() + if not flowstr: + raise ValueError("vlan(): missing ')'") + if flowstr[0] == ')': + break + if not first: + flowstr = flowstr[1:] # skip ',' + if not flowstr: + raise ValueError("vlan(): missing ')' after trailing comma") + flowstr = flowstr.lstrip() + if flowstr and flowstr[0] == ')': + break + if flowstr and flowstr[0] == ',': + raise ValueError( + "vlan(): empty or extra comma in field list") + first = False + + eq = flowstr.find('=') + if eq == -1: + raise ValueError("vlan(): expected key=value, got '%s'" % flowstr) + key = flowstr[:eq].strip() + flowstr = flowstr[eq + 1:] + + end = flowstr.find(',') + end2 = flowstr.find(')') + if end == -1 or (end2 != -1 and end2 < end): + end = end2 + val = flowstr[:end].strip() + flowstr = flowstr[end:] + + if not val: + raise ValueError("vlan(): empty value for key '%s'" % key) + try: + v = int(val, 16) if val.startswith(('0x', '0X')) else int(val) + except ValueError: + raise ValueError("vlan(): invalid value '%s' for key '%s'" % + (val, key)) + + if key == 'tci': + if has_tci: + raise ValueError("vlan(): duplicate 'tci'") + if has_vid or has_pcp or has_cfi: + raise ValueError(_tci_mix_err) + if v > 0xFFFF or v < 0: + raise ValueError("vlan(): tci=0x%x out of range" % v) + tci = v + mask = 0xFFFF + has_tci = True + elif key == 'vid': + if has_tci: + raise ValueError(_tci_mix_err) + if has_vid: + raise ValueError("vlan(): duplicate 'vid'") + if v < 0 or v > 0xFFF: + raise ValueError("vlan(): vid=%d out of range (0-4095)" % v) + tci |= v + mask |= 0x0FFF + has_vid = True + elif key == 'pcp': + if has_tci: + raise ValueError(_tci_mix_err) + if has_pcp: + raise ValueError("vlan(): duplicate 'pcp'") + if v < 0 or v > 7: + raise ValueError("vlan(): pcp=%d out of range (0-7)" % v) + tci |= (v & 0x7) << 13 + mask |= 0xE000 + has_pcp = True + elif key == 'cfi': + if has_tci: + raise ValueError(_tci_mix_err) + if has_cfi: + raise ValueError("vlan(): duplicate 'cfi'") + if v != 1: + raise ValueError("vlan(): cfi must be 1 for Ethernet") + tci |= ovskey._VLAN_CFI_MASK + mask |= ovskey._VLAN_CFI_MASK + has_cfi = True + else: + raise ValueError("vlan(): unknown key '%s'" % key) + + flowstr = flowstr[1:] # skip ')' + # Catch immediate '))' (user error). A ')' after ',' is consumed + # by parse()'s strspn(flowstr, "), ") inter-field separator stripping. + if flowstr.lstrip().startswith(')'): + raise ValueError("vlan(): unmatched ')'") + # parse() strips trailing ',', ')', ' ' as inter-field separators, + # so we do not need to call strspn here. + + if mask == 0: + raise ValueError("vlan(): no fields specified, " + "use vlan(vid=X[,pcp=Y,cfi=Z]) or vlan(tci=X)") + if not has_tci: + tci |= ovskey._VLAN_CFI_MASK + mask |= ovskey._VLAN_CFI_MASK + return flowstr, tci, mask + + @staticmethod + def _parse_encap_from_flowstr(flowstr): + """Parse encap(inner_flow) from flowstr. + + Returns (remaining_flowstr, inner_key_dict, inner_mask_dict) + where each dict has an 'attrs' key for recursive NLA encoding. + Parenthesis-depth tracking handles nested encap() calls but not + quoted strings containing literal parentheses. + """ + if ovskey._encap_depth >= ovskey._MAX_ENCAP_DEPTH: + raise ValueError("encap(): max nesting depth %d exceeded" % + ovskey._MAX_ENCAP_DEPTH) + try: + ovskey._encap_depth += 1 + depth = 1 + end = -1 + for i, c in enumerate(flowstr): + if c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth < 0: + raise ValueError("encap(): unmatched ')' at position %d" % i) + if depth == 0: + end = i + break + + if end == -1: + if depth > 1: + raise ValueError("encap(): missing ')' at end") + raise ValueError("encap(): missing closing ')'") + + inner_str = flowstr[:end].strip() + if not inner_str: + raise ValueError("encap(): empty inner flow") + + flowstr = flowstr[end + 1:] + if flowstr.lstrip().startswith(')'): + raise ValueError("encap(): unmatched ')' after encap()") + # parse() strips trailing ',', ')', ' ' as inter-field separators, + # so we do not need to call strspn here. + + inner_key = ovskey() + inner_mask = ovskey() + inner_key.parse(inner_str, inner_mask) + + return flowstr, inner_key, inner_mask + finally: + ovskey._encap_depth -= 1 + def parse(self, flowstr, mask=None): for field in ( ("OVS_KEY_ATTR_PRIORITY", "skb_priority", intparse), @@ -1657,6 +1831,16 @@ class ovskey(nla): "eth_type", lambda x: intparse(x, "0xffff"), ), + ( + "OVS_KEY_ATTR_VLAN", + "vlan", + ovskey._parse_vlan_from_flowstr, + ), + ( + "OVS_KEY_ATTR_ENCAP", + "encap", + ovskey._parse_encap_from_flowstr, + ), ( "OVS_KEY_ATTR_IPV4", "ipv4", @@ -1794,6 +1978,8 @@ class ovskey(nla): True, ), ("OVS_KEY_ATTR_ETHERNET", None, None, False, False), + ("OVS_KEY_ATTR_VLAN", "vlan", "0x%04x", lambda x: False, True), + ("OVS_KEY_ATTR_ENCAP", None, None, False, False), ( "OVS_KEY_ATTR_ETHERTYPE", "eth_type", -- 2.53.0