1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 """Classify utilities library."""
18 from ipaddress import ip_address
20 from robot.api import logger
22 from resources.libraries.python.Constants import Constants
23 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
24 from resources.libraries.python.topology import Topology
28 """Classify utilities."""
31 def _build_mac_mask(dst_mac=u"", src_mac=u"", ether_type=u""):
32 """Build MAC ACL mask data in bytes format.
34 :param dst_mac: Source MAC address <0-ffffffffffff>.
35 :param src_mac: Destination MAC address <0-ffffffffffff>.
36 :param ether_type: Ethernet type <0-ffff>.
40 :returns MAC ACL mask in bytes format.
44 f"{dst_mac.replace(u':', u'')!s:0>12}"
45 f"{src_mac.replace(u':', u'')!s:0>12}"
51 proto=u"", src_ip=u"", dst_ip=u"", src_port=u"", dst_port=u""):
52 """Build IP ACL mask data in bytes format.
54 :param proto: Protocol number <0-ff>.
55 :param src_ip: Source ip address <0-ffffffff>.
56 :param dst_ip: Destination ip address <0-ffffffff>.
57 :param src_port: Source port number <0-ffff>.
58 :param str dst_port: Destination port number <0-ffff>.
64 :returns: IP mask in bytes format.
68 f"{proto!s:0>20}{src_ip!s:0>12}{dst_ip!s:0>8}{src_port!s:0>4}"
74 next_hdr=u"", src_ip=u"", dst_ip=u"", src_port=u"", dst_port=u""):
75 """Build IPv6 ACL mask data in bytes format.
77 :param next_hdr: Next header number <0-ff>.
78 :param src_ip: Source ip address <0-ffffffff>.
79 :param dst_ip: Destination ip address <0-ffffffff>.
80 :param src_port: Source port number <0-ffff>.
81 :param dst_port: Destination port number <0-ffff>.
87 :returns: IPv6 ACL mask in bytes format.
91 f"{next_hdr!s:0>14}{src_ip!s:0>34}{dst_ip!s:0>32}{src_port!s:0>4}"
96 def _build_mac_match(dst_mac=u"", src_mac=u"", ether_type=u""):
97 """Build MAC ACL match data in bytes format.
99 :param dst_mac: Source MAC address <x:x:x:x:x:x>.
100 :param src_mac: Destination MAC address <x:x:x:x:x:x>.
101 :param ether_type: Ethernet type <0-ffff>.
104 :type ether_type: str
105 :returns: MAC ACL match data in bytes format.
108 return bytes.fromhex(
109 f"{dst_mac.replace(u':', u'')!s:0>12}"
110 f"{src_mac.replace(u':', u'')!s:0>12}"
111 f"{ether_type!s:0>4}"
116 proto=0, src_ip=4*b"\0", dst_ip=4*b"\0", src_port=0, dst_port=0):
117 """Build IP ACL match data in bytes format.
119 :param proto: Protocol number with valid option "x".
120 :param src_ip: Source ip address in packed format.
121 :param dst_ip: Destination ip address in packed format.
122 :param src_port: Source port number "x".
123 :param dst_port: Destination port number "x".
129 :returns: IP ACL match data in byte-string format.
132 return bytes.fromhex(
133 f"{hex(proto)[2:]!s:0>20}{src_ip.hex()!s:0>12}{dst_ip.hex()!s:0>8}"
134 f"{hex(src_port)[2:]!s:0>4}{hex(dst_port)[2:]!s:0>4}"
138 def _build_ip6_match(
139 next_hdr=0, src_ip=16*b"\0", dst_ip=16*b"\0", src_port=0,
141 """Build IPv6 ACL match data in byte-string format.
143 :param next_hdr: Next header number with valid option "x".
144 :param src_ip: Source ip6 address in packed format.
145 :param dst_ip: Destination ip6 address in packed format.
146 :param src_port: Source port number "x".
147 :param dst_port: Destination port number "x".
153 :returns: IPv6 ACL match data in bytes format.
156 return bytes.fromhex(
157 f"{hex(next_hdr)[2:]!s:0>14}{src_ip.hex()!s:0>34}"
158 f"{dst_ip.hex()!s:0>32}{hex(src_port)[2:]!s:0>4}"
159 f"{hex(dst_port)[2:]!s:0>4}"
163 def _classify_add_del_table(
164 node, is_add, mask, match_n_vectors=Constants.BITWISE_NON_ZERO,
165 table_index=Constants.BITWISE_NON_ZERO, nbuckets=2,
166 memory_size=2097152, skip_n_vectors=Constants.BITWISE_NON_ZERO,
167 next_table_index=Constants.BITWISE_NON_ZERO,
168 miss_next_index=Constants.BITWISE_NON_ZERO,
169 current_data_flag=0, current_data_offset=0):
170 """Add or delete a classify table.
172 :param node: VPP node to create classify table.
173 :param is_add: If 1 the table is added, if 0 the table is deleted.
174 :param mask: ACL mask in hexstring format.
175 :param match_n_vectors: Number of vectors to match (Default value = ~0).
176 :param table_index: Index of the classify table. (Default value = ~0)
177 :param nbuckets: Number of buckets when adding a table.
179 :param memory_size: Memory size when adding a table.
180 (Default value = 2097152)
181 :param skip_n_vectors: Number of skip vectors (Default value = ~0).
182 :param next_table_index: Index of next table. (Default value = ~0)
183 :param miss_next_index: Index of miss table. (Default value = ~0)
184 :param current_data_flag: Option to use current node's packet payload
185 as the starting point from where packets are classified.
186 This option is only valid for L2/L3 input ACL for now.
187 0: by default, classify data from the buffer's start location
188 1: classify packets from VPP node's current data pointer.
189 :param current_data_offset: A signed value to shift the start location
190 of the packet to be classified.
191 For example, if input IP ACL node is used, L2 header's first byte
192 can be accessible by configuring current_data_offset to -14
193 if there is no vlan tag.
194 This is valid only if current_data_flag is set to 1.
199 :type match_n_vectors: int
200 :type table_index: int
202 :type memory_size: int
203 :type skip_n_vectors: int
204 :type next_table_index: int
205 :type miss_next_index: int
206 :type current_data_flag: int
207 :type current_data_offset: int
208 :returns: (table_index, skip_n, match_n)
209 table_index: Classify table index.
210 skip_n: Number of skip vectors.
211 match_n: Number of match vectors.
212 :rtype: tuple(int, int, int)
214 cmd = u"classify_add_del_table"
217 table_index=table_index,
219 memory_size=memory_size,
220 skip_n_vectors=skip_n_vectors,
221 match_n_vectors=match_n_vectors,
222 next_table_index=next_table_index,
223 miss_next_index=miss_next_index,
224 current_data_flag=current_data_flag,
225 current_data_offset=current_data_offset,
229 err_msg = f"Failed to create a classify table on host {node[u'host']}"
231 with PapiSocketExecutor(node) as papi_exec:
232 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
234 return int(reply[u"new_table_index"]), int(reply[u"skip_n_vectors"]),\
235 int(reply[u"match_n_vectors"])
238 def _classify_add_del_session(
239 node, is_add, table_index, match,
240 opaque_index=Constants.BITWISE_NON_ZERO,
241 hit_next_index=Constants.BITWISE_NON_ZERO, advance=0,
242 action=0, metadata=0):
243 """Add or delete a classify session.
245 :param node: VPP node to create classify session.
246 :param is_add: If 1 the session is added, if 0 the session is deleted.
247 :param table_index: Index of the table to add/del the session.
248 :param match: For add, match value for session, required, needs to
249 include bytes in front with length of skip_n_vectors of target table
250 times sizeof (u32x4) (values of those bytes will be ignored).
251 :param opaque_index: For add, opaque_index of new session.
253 :param hit_next_index: For add, hit_next_index of new session.
255 :param advance: For add, advance value for session. (Default value = 0)
256 :param action: 0: No action (by default) metadata is not used.
257 1: Classified IP packets will be looked up from the specified ipv4
258 fib table (configured by metadata as VRF id).
259 Only valid for L3 input ACL node
260 2: Classified IP packets will be looked up from the specified ipv6
261 fib table (configured by metadata as VRF id).
262 Only valid for L3 input ACL node
263 3: Classified packet will be steered to source routig policy of
264 given index (in metadata).
265 This is only valid for IPv6 packets redirected to a source
267 :param metadata: Valid only if action != 0
268 VRF id if action is 1 or 2. SR policy index if action is 3.
272 :type table_index: int
274 :type opaque_index: int
275 :type hit_next_index: int
280 cmd = u"classify_add_del_session"
283 table_index=table_index,
284 hit_next_index=hit_next_index,
285 opaque_index=opaque_index,
289 match_len=len(match),
292 err_msg = f"Failed to create a classify session on host {node[u'host']}"
294 with PapiSocketExecutor(node) as papi_exec:
295 papi_exec.add(cmd, **args).get_reply(err_msg)
298 def _macip_acl_add(node, rules, tag=""):
301 :param node: VPP node to add MACIP ACL.
302 :param rules: List of rules for given ACL.
308 cmd = u"macip_acl_add"
315 err_msg = f"Failed to add MACIP ACL on host {node[u'host']}"
317 with PapiSocketExecutor(node) as papi_exec:
318 papi_exec.add(cmd, **args).get_reply(err_msg)
321 def _acl_interface_set_acl_list(node, sw_if_index, acl_type, acls):
322 """Set ACL list for interface.
324 :param node: VPP node to set ACL list for interface.
325 :param sw_if_index: sw_if_index of the used interface.
326 :param acl_type: Type of ACL(s) - input or output.
327 :param acls: List of ACLs.
329 :type sw_if_index: int
333 cmd = u"acl_interface_set_acl_list"
334 n_input = len(acls) if acl_type == u"input" else 0
336 sw_if_index=sw_if_index,
342 err_msg = f"Failed to set acl list for interface {sw_if_index} " \
343 f"on host {node[u'host']}"
345 with PapiSocketExecutor(node) as papi_exec:
346 papi_exec.add(cmd, **args).get_reply(err_msg)
349 def _acl_add_replace(node, acl_idx, rules, tag=""):
350 """ Add/replace ACLs.
352 :param node: VPP node to add MACIP ACL.
353 :param acl_idx: ACL index.
354 :param rules: List of rules for given ACL.
361 cmd = u"acl_add_replace"
363 tag=tag.encode("utf-8"),
364 acl_index=4294967295 if acl_idx is None else acl_idx,
369 err_msg = f"Failed to add/replace ACLs on host {node[u'host']}"
371 with PapiSocketExecutor(node) as papi_exec:
372 papi_exec.add(cmd, **args).get_reply(err_msg)
375 def vpp_creates_classify_table_l3(node, ip_version, direction, netmask):
376 """Create classify table for IP address filtering.
378 :param node: VPP node to create classify table.
379 :param ip_version: Version of IP protocol.
380 :param direction: Direction of traffic - src/dst.
381 :param netmask: IPv4 or Ipv6 (depending on the parameter 'ip_version')
382 netmask (decimal, e.g. 255.255.255.255).
384 :type ip_version: str
387 :returns: (table_index, skip_n, match_n)
388 table_index: Classify table index.
389 skip_n: Number of skip vectors.
390 match_n: Number of match vectors.
391 :rtype: tuple(int, int, int)
392 :raises ValueError: If the parameters 'ip_version' or 'direction' have
396 ip4=Classify._build_ip_mask,
397 ip6=Classify._build_ip6_mask
400 if ip_version in (u"ip4", u"ip6"):
401 netmask = ip_address(netmask).packed
403 raise ValueError(f"IP version {ip_version} is not supported.")
405 if direction == u"src":
406 mask = mask_f[ip_version](src_ip=netmask.hex())
407 elif direction == u"dst":
408 mask = mask_f[ip_version](dst_ip=netmask.hex())
410 raise ValueError(f"Direction {direction} is not supported.")
412 # Add l2 ethernet header to mask
413 mask = 14 * b'\0' + mask
415 # Get index of the first significant mask octet
416 i = len(mask) - len(mask.lstrip(b'\0'))
418 # Compute skip_n parameter
420 # Remove octets to be skipped from the mask
421 mask = mask[skip_n*16:]
422 # Pad mask to an even multiple of the vector size
423 mask = mask + (16 - len(mask) % 16 if len(mask) % 16 else 0) * b'\0'
424 # Compute match_n parameter
425 match_n = len(mask) // 16
427 return Classify._classify_add_del_table(
431 match_n_vectors=match_n,
432 skip_n_vectors=skip_n
436 def vpp_configures_classify_session_l3(
437 node, acl_method, table_index, skip_n, match_n, ip_version,
438 direction, address, hit_next_index=Constants.BITWISE_NON_ZERO,
439 opaque_index=Constants.BITWISE_NON_ZERO):
440 """Configuration of classify session for IP address filtering.
442 :param node: VPP node to setup classify session.
443 :param acl_method: ACL method - deny/permit.
444 :param table_index: Classify table index.
445 :param skip_n: Number of skip vectors.
446 :param match_n: Number of vectors to match.
447 :param ip_version: Version of IP protocol.
448 :param direction: Direction of traffic - src/dst.
449 :param address: IPv4 or IPv6 address.
450 :param hit_next_index: hit_next_index of new session.
452 :param opaque_index: opaque_index of new session. (Default value = ~0)
454 :type acl_method: str
455 :type table_index: int
458 :type ip_version: str
461 :type hit_next_index: int
462 :type opaque_index: int
463 :raises ValueError: If the parameter 'direction' has incorrect value.
466 ip4=Classify._build_ip_match,
467 ip6=Classify._build_ip6_match
474 if ip_version in (u"ip4", u"ip6"):
475 address = ip_address(address).packed
477 raise ValueError(f"IP version {ip_version} is not supported.")
479 if direction == u"src":
480 match = match_f[ip_version](src_ip=address)
481 elif direction == u"dst":
482 match = match_f[ip_version](dst_ip=address)
484 raise ValueError(f"Direction {direction} is not supported.")
486 # Prepend match with l2 ethernet header part
487 match = 14 * b'\0' + match
489 # Pad match to match skip_n_vector + match_n_vector size
490 match = match + ((match_n + skip_n) * 16 - len(match)
491 if len(match) < (match_n + skip_n) * 16
494 Classify._classify_add_del_session(
497 table_index=table_index,
498 hit_next_index=hit_next_index,
499 opaque_index=opaque_index,
501 action=action[acl_method]
505 def get_classify_table_data(node, table_index):
506 """Retrieve settings for classify table by ID.
508 :param node: VPP node to retrieve classify data from.
509 :param table_index: Index of a specific classify table.
511 :type table_index: int
512 :returns: Classify table settings.
515 cmd = u"classify_table_info"
516 err_msg = f"Failed to get 'classify_table_info' on host {node[u'host']}"
518 table_id=int(table_index)
520 with PapiSocketExecutor(node) as papi_exec:
521 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
525 def get_classify_session_data(node, table_index):
526 """Retrieve settings for all classify sessions in a table.
528 :param node: VPP node to retrieve classify data from.
529 :param table_index: Index of a classify table.
531 :type table_index: int
532 :returns: List of classify session settings.
535 cmd = u"classify_session_dump"
537 table_id=int(table_index)
539 with PapiSocketExecutor(node) as papi_exec:
540 details = papi_exec.add(cmd, **args).get_details()
545 def show_classify_tables_verbose(node):
546 """Show classify tables verbose.
548 :param node: Topology node.
550 :returns: Classify tables verbose data.
553 return PapiSocketExecutor.run_cli_cmd(
554 node, u"show classify tables verbose"
558 def vpp_log_plugin_acl_settings(node):
559 """Retrieve configured settings from the ACL plugin and write to robot
562 :param node: VPP node.
565 PapiSocketExecutor.dump_and_log(node, [u"acl_dump", ])
568 def vpp_log_plugin_acl_interface_assignment(node):
569 """Retrieve interface assignment from the ACL plugin and write to robot
572 :param node: VPP node.
575 PapiSocketExecutor.dump_and_log(node, [u"acl_interface_list_dump", ])
578 def set_acl_list_for_interface(node, interface, acl_type, acl_idx=None):
579 """Set the list of input or output ACLs applied to the interface. It
580 unapplies any previously applied ACLs.
582 :param node: VPP node to set ACL on.
583 :param interface: Interface name or sw_if_index.
584 :param acl_type: Type of ACL(s) - input or output.
585 :param acl_idx: Index(ies) of ACLs to be applied on the interface.
587 :type interface: str or int
591 if isinstance(interface, str):
592 sw_if_index = Topology.get_interface_sw_index(node, interface)
594 sw_if_index = int(interface)
596 acls = acl_idx if isinstance(acl_idx, list) else list()
598 Classify._acl_interface_set_acl_list(
599 node=node, sw_if_index=sw_if_index, acl_type=acl_type, acls=acls
603 def add_replace_acl_multi_entries(node, acl_idx=None, rules=None, tag=u""):
604 """Add a new ACL or replace the existing one. To replace an existing
605 ACL, pass the ID of this ACL.
607 :param node: VPP node to set ACL on.
608 :param acl_idx: ID of ACL. (Optional)
609 :param rules: Required rules. (Optional)
610 :param tag: ACL tag (Optional).
616 reg_ex_src_ip = re.compile(r"(src [0-9a-fA-F.:/\d{1,2}]*)")
617 reg_ex_dst_ip = re.compile(r"(dst [0-9a-fA-F.:/\d{1,2}]*)")
618 reg_ex_sport = re.compile(r"(sport \d{1,5})")
619 reg_ex_dport = re.compile(r"(dport \d{1,5})")
620 reg_ex_proto = re.compile(r"(proto \d{1,5})")
623 for rule in rules.split(u", "):
625 acl_rule[u"is_permit"] = 1 if u"permit" in rule else 0
626 acl_rule[u"is_ipv6"] = 1 if u"ipv6" in rule else 0
628 groups = re.search(reg_ex_src_ip, rule)
630 grp = groups.group(1).split(u" ")[1].split(u"/")
631 acl_rule[u"src_ip_addr"] = ip_address(grp[0]).packed
632 acl_rule[u"src_ip_prefix_len"] = int(grp[1])
634 groups = re.search(reg_ex_dst_ip, rule)
636 grp = groups.group(1).split(u" ")[1].split(u"/")
637 acl_rule[u"dst_ip_addr"] = ip_address(grp[0]).packed
638 acl_rule[u"dst_ip_prefix_len"] = int(grp[1])
640 groups = re.search(reg_ex_sport, rule)
642 port = int(groups.group(1).split(u" ")[1])
643 acl_rule[u"srcport_or_icmptype_first"] = port
644 acl_rule[u"srcport_or_icmptype_last"] = port
646 acl_rule[u"srcport_or_icmptype_first"] = 0
647 acl_rule[u"srcport_or_icmptype_last"] = 65535
649 groups = re.search(reg_ex_dport, rule)
651 port = int(groups.group(1).split(u" ")[1])
652 acl_rule[u"dstport_or_icmpcode_first"] = port
653 acl_rule[u"dstport_or_icmpcode_last"] = port
655 acl_rule[u"dstport_or_icmpcode_first"] = 0
656 acl_rule[u"dstport_or_icmpcode_last"] = 65535
658 groups = re.search(reg_ex_proto, rule)
660 proto = int(groups.group(1).split(' ')[1])
661 acl_rule[u"proto"] = proto
663 acl_rule[u"proto"] = 0
665 acl_rules.append(acl_rule)
667 Classify._acl_add_replace(
668 node, acl_idx=acl_idx, rules=acl_rules, tag=tag)
671 def add_macip_acl_multi_entries(node, rules=u""):
672 """Add a new MACIP ACL.
674 :param node: VPP node to set MACIP ACL on.
675 :param rules: Required MACIP rules.
679 reg_ex_ip = re.compile(r"(ip [0-9a-fA-F.:/\d{1,2}]*)")
680 reg_ex_mac = re.compile(r"(mac \S\S:\S\S:\S\S:\S\S:\S\S:\S\S)")
681 reg_ex_mask = re.compile(r"(mask \S\S:\S\S:\S\S:\S\S:\S\S:\S\S)")
684 for rule in rules.split(u", "):
686 acl_rule[u"is_permit"] = 1 if u"permit" in rule else 0
687 acl_rule[u"is_ipv6"] = 1 if u"ipv6" in rule else 0
689 groups = re.search(reg_ex_mac, rule)
691 mac = groups.group(1).split(u" ")[1].replace(u":", u"")
692 acl_rule[u"src_mac"] = bytes.fromhex(mac)
694 groups = re.search(reg_ex_mask, rule)
696 mask = groups.group(1).split(u" ")[1].replace(u":", u"")
697 acl_rule[u"src_mac_mask"] = bytes.fromhex(mask)
699 groups = re.search(reg_ex_ip, rule)
701 grp = groups.group(1).split(u" ")[1].split(u"/")
702 acl_rule[u"src_ip_addr"] = ip_address((grp[0])).packed
703 acl_rule[u"src_ip_prefix_len"] = int(grp[1])
705 acl_rules.append(acl_rule)
707 Classify._macip_acl_add(node=node, rules=acl_rules)
710 def vpp_log_macip_acl_settings(node):
711 """Retrieve configured MACIP settings from the ACL plugin and write to
714 :param node: VPP node.
717 PapiSocketExecutor.dump_and_log(node, [u"macip_acl_dump", ])
720 def add_del_macip_acl_interface(node, interface, action, acl_idx):
721 """Apply/un-apply the MACIP ACL to/from a given interface.
723 :param node: VPP node to set MACIP ACL on.
724 :param interface: Interface name or sw_if_index.
725 :param action: Required action - add or del.
726 :param acl_idx: ACL index to be applied on the interface.
728 :type interface: str or int
730 :type acl_idx: str or int
731 :raises RuntimeError: If unable to set MACIP ACL for the interface.
733 if isinstance(interface, str):
734 sw_if_index = Topology.get_interface_sw_index(node, interface)
736 sw_if_index = interface
738 is_add = 1 if action == u"add" else 0
740 cmd = u"macip_acl_interface_add_del"
741 err_msg = f"Failed to get 'macip_acl_interface' on host {node[u'host']}"
744 sw_if_index=int(sw_if_index),
745 acl_index=int(acl_idx)
747 with PapiSocketExecutor(node) as papi_exec:
748 papi_exec.add(cmd, **args).get_reply(err_msg)
751 def vpp_log_macip_acl_interface_assignment(node):
752 """Get interface list and associated MACIP ACLs and write to robot log.
754 :param node: VPP node.
757 cmd = u"macip_acl_interface_get"
758 err_msg = f"Failed to get 'macip_acl_interface' on host {node[u'host']}"
759 with PapiSocketExecutor(node) as papi_exec:
760 reply = papi_exec.add(cmd).get_reply(err_msg)