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 True the table is added, if False 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"
218 table_index=table_index,
220 memory_size=memory_size,
221 skip_n_vectors=skip_n_vectors,
222 match_n_vectors=match_n_vectors,
223 next_table_index=next_table_index,
224 miss_next_index=miss_next_index,
225 current_data_flag=current_data_flag,
226 current_data_offset=current_data_offset,
230 err_msg = f"Failed to create a classify table on host {node[u'host']}"
232 with PapiSocketExecutor(node) as papi_exec:
233 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
235 return int(reply[u"new_table_index"]), int(reply[u"skip_n_vectors"]),\
236 int(reply[u"match_n_vectors"])
239 def _classify_add_del_session(
240 node, is_add, table_index, match,
241 opaque_index=Constants.BITWISE_NON_ZERO,
242 hit_next_index=Constants.BITWISE_NON_ZERO, advance=0,
243 action=0, metadata=0):
244 """Add or delete a classify session.
246 :param node: VPP node to create classify session.
247 :param is_add: If True the session is added, if False the session
249 :param table_index: Index of the table to add/del the session.
250 :param match: For add, match value for session, required, needs to
251 include bytes in front with length of skip_n_vectors of target table
252 times sizeof (u32x4) (values of those bytes will be ignored).
253 :param opaque_index: For add, opaque_index of new session.
255 :param hit_next_index: For add, hit_next_index of new session.
257 :param advance: For add, advance value for session. (Default value = 0)
258 :param action: 0: No action (by default) metadata is not used.
259 1: Classified IP packets will be looked up from the specified ipv4
260 fib table (configured by metadata as VRF id).
261 Only valid for L3 input ACL node
262 2: Classified IP packets will be looked up from the specified ipv6
263 fib table (configured by metadata as VRF id).
264 Only valid for L3 input ACL node
265 3: Classified packet will be steered to source routing policy of
266 given index (in metadata).
267 This is only valid for IPv6 packets redirected to a source
269 :param metadata: Valid only if action != 0. VRF id if action is 1 or 2.
270 SR policy index if action is 3. (Default value = 0)
273 :type table_index: int
275 :type opaque_index: int
276 :type hit_next_index: int
281 cmd = u"classify_add_del_session"
284 table_index=table_index,
285 hit_next_index=hit_next_index,
286 opaque_index=opaque_index,
290 match_len=len(match),
293 err_msg = f"Failed to create a classify session on host {node[u'host']}"
295 with PapiSocketExecutor(node) as papi_exec:
296 papi_exec.add(cmd, **args).get_reply(err_msg)
299 def _macip_acl_add(node, rules, tag=""):
302 :param node: VPP node to add MACIP ACL.
303 :param rules: List of rules for given ACL.
309 cmd = u"macip_acl_add"
316 err_msg = f"Failed to add MACIP ACL on host {node[u'host']}"
318 with PapiSocketExecutor(node) as papi_exec:
319 papi_exec.add(cmd, **args).get_reply(err_msg)
322 def _acl_interface_set_acl_list(node, sw_if_index, acl_type, acls):
323 """Set ACL list for interface.
325 :param node: VPP node to set ACL list for interface.
326 :param sw_if_index: sw_if_index of the used interface.
327 :param acl_type: Type of ACL(s) - input or output.
328 :param acls: List of ACLs.
330 :type sw_if_index: int
334 cmd = u"acl_interface_set_acl_list"
335 n_input = len(acls) if acl_type == u"input" else 0
337 sw_if_index=sw_if_index,
343 err_msg = f"Failed to set acl list for interface {sw_if_index} " \
344 f"on host {node[u'host']}"
346 with PapiSocketExecutor(node) as papi_exec:
347 papi_exec.add(cmd, **args).get_reply(err_msg)
350 def _acl_add_replace(node, acl_idx, rules, tag=""):
351 """ Add/replace ACLs.
353 :param node: VPP node to add MACIP ACL.
354 :param acl_idx: ACL index.
355 :param rules: List of rules for given ACL.
362 cmd = u"acl_add_replace"
364 tag=tag.encode("utf-8"),
365 acl_index=4294967295 if acl_idx is None else acl_idx,
370 err_msg = f"Failed to add/replace ACLs on host {node[u'host']}"
372 with PapiSocketExecutor(node) as papi_exec:
373 papi_exec.add(cmd, **args).get_reply(err_msg)
376 def vpp_creates_classify_table_l3(node, ip_version, direction, netmask):
377 """Create classify table for IP address filtering.
379 :param node: VPP node to create classify table.
380 :param ip_version: Version of IP protocol.
381 :param direction: Direction of traffic - src/dst.
382 :param netmask: IPv4 or Ipv6 (depending on the parameter 'ip_version')
383 netmask (decimal, e.g. 255.255.255.255).
385 :type ip_version: str
388 :returns: (table_index, skip_n, match_n)
389 table_index: Classify table index.
390 skip_n: Number of skip vectors.
391 match_n: Number of match vectors.
392 :rtype: tuple(int, int, int)
393 :raises ValueError: If the parameters 'ip_version' or 'direction' have
397 ip4=Classify._build_ip_mask,
398 ip6=Classify._build_ip6_mask
401 if ip_version in (u"ip4", u"ip6"):
402 netmask = ip_address(netmask).packed
404 raise ValueError(f"IP version {ip_version} is not supported.")
406 if direction == u"src":
407 mask = mask_f[ip_version](src_ip=netmask.hex())
408 elif direction == u"dst":
409 mask = mask_f[ip_version](dst_ip=netmask.hex())
411 raise ValueError(f"Direction {direction} is not supported.")
413 # Add l2 ethernet header to mask
414 mask = 14 * b'\0' + mask
416 # Get index of the first significant mask octet
417 i = len(mask) - len(mask.lstrip(b'\0'))
419 # Compute skip_n parameter
421 # Remove octets to be skipped from the mask
422 mask = mask[skip_n*16:]
423 # Pad mask to an even multiple of the vector size
424 mask = mask + (16 - len(mask) % 16 if len(mask) % 16 else 0) * b'\0'
425 # Compute match_n parameter
426 match_n = len(mask) // 16
428 return Classify._classify_add_del_table(
432 match_n_vectors=match_n,
433 skip_n_vectors=skip_n
437 def vpp_configures_classify_session_l3(
438 node, acl_method, table_index, skip_n, match_n, ip_version,
439 direction, address, hit_next_index=None,
440 opaque_index=Constants.BITWISE_NON_ZERO, action=0, metadata=0):
441 """Configuration of classify session for IP address filtering.
443 :param node: VPP node to setup classify session.
444 :param acl_method: ACL method - deny/permit.
445 :param table_index: Classify table index.
446 :param skip_n: Number of skip vectors.
447 :param match_n: Number of vectors to match.
448 :param ip_version: Version of IP protocol.
449 :param direction: Direction of traffic - src/dst.
450 :param address: IPv4 or IPv6 address.
451 :param hit_next_index: hit_next_index of new session.
452 (Default value = None)
453 :param opaque_index: opaque_index of new session. (Default value = ~0)
454 :param action: 0: No action (by default) metadata is not used.
455 1: Classified IP packets will be looked up from the specified ipv4
456 fib table (configured by metadata as VRF id).
457 Only valid for L3 input ACL node
458 2: Classified IP packets will be looked up from the specified ipv6
459 fib table (configured by metadata as VRF id).
460 Only valid for L3 input ACL node
461 3: Classified packet will be steered to source routing policy of
462 given index (in metadata).
463 This is only valid for IPv6 packets redirected to a source
465 :param metadata: Valid only if action != 0. VRF id if action is 1 or 2.
466 SR policy index if action is 3. (Default value = 0)
468 :type acl_method: str
469 :type table_index: int
472 :type ip_version: str
475 :type hit_next_index: int
476 :type opaque_index: int
479 :raises ValueError: If the parameter 'direction' has incorrect value.
482 ip4=Classify._build_ip_match,
483 ip6=Classify._build_ip6_match
485 acl_hit_next_index = dict(
486 permit=Constants.BITWISE_NON_ZERO,
490 if ip_version in (u"ip4", u"ip6"):
491 address = ip_address(address).packed
493 raise ValueError(f"IP version {ip_version} is not supported.")
495 if direction == u"src":
496 match = match_f[ip_version](src_ip=address)
497 elif direction == u"dst":
498 match = match_f[ip_version](dst_ip=address)
500 raise ValueError(f"Direction {direction} is not supported.")
502 # Prepend match with l2 ethernet header part
503 match = 14 * b'\0' + match
505 # Pad match to match skip_n_vector + match_n_vector size
506 match = match + ((match_n + skip_n) * 16 - len(match)
507 if len(match) < (match_n + skip_n) * 16
510 Classify._classify_add_del_session(
513 table_index=table_index,
514 hit_next_index=hit_next_index if hit_next_index is not None
515 else acl_hit_next_index[acl_method],
516 opaque_index=opaque_index,
523 def get_classify_table_data(node, table_index):
524 """Retrieve settings for classify table by ID.
526 :param node: VPP node to retrieve classify data from.
527 :param table_index: Index of a specific classify table.
529 :type table_index: int
530 :returns: Classify table settings.
533 cmd = u"classify_table_info"
534 err_msg = f"Failed to get 'classify_table_info' on host {node[u'host']}"
536 table_id=int(table_index)
538 with PapiSocketExecutor(node) as papi_exec:
539 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
543 def get_classify_session_data(node, table_index):
544 """Retrieve settings for all classify sessions in a table.
546 :param node: VPP node to retrieve classify data from.
547 :param table_index: Index of a classify table.
549 :type table_index: int
550 :returns: List of classify session settings.
553 cmd = u"classify_session_dump"
555 table_id=int(table_index)
557 with PapiSocketExecutor(node) as papi_exec:
558 details = papi_exec.add(cmd, **args).get_details()
563 def show_classify_tables_verbose(node):
564 """Show classify tables verbose.
566 :param node: Topology node.
568 :returns: Classify tables verbose data.
571 return PapiSocketExecutor.run_cli_cmd(
572 node, u"show classify tables verbose"
576 def vpp_log_plugin_acl_settings(node):
577 """Retrieve configured settings from the ACL plugin and write to robot
580 :param node: VPP node.
583 PapiSocketExecutor.dump_and_log(node, [u"acl_dump", ])
586 def vpp_log_plugin_acl_interface_assignment(node):
587 """Retrieve interface assignment from the ACL plugin and write to robot
590 :param node: VPP node.
593 PapiSocketExecutor.dump_and_log(node, [u"acl_interface_list_dump", ])
596 def set_acl_list_for_interface(node, interface, acl_type, acl_idx=None):
597 """Set the list of input or output ACLs applied to the interface. It
598 unapplies any previously applied ACLs.
600 :param node: VPP node to set ACL on.
601 :param interface: Interface name or sw_if_index.
602 :param acl_type: Type of ACL(s) - input or output.
603 :param acl_idx: Index(ies) of ACLs to be applied on the interface.
605 :type interface: str or int
609 if isinstance(interface, str):
610 sw_if_index = Topology.get_interface_sw_index(node, interface)
612 sw_if_index = int(interface)
614 acls = acl_idx if isinstance(acl_idx, list) else list()
616 Classify._acl_interface_set_acl_list(
617 node=node, sw_if_index=sw_if_index, acl_type=acl_type, acls=acls
621 def add_replace_acl_multi_entries(node, acl_idx=None, rules=None, tag=u""):
622 """Add a new ACL or replace the existing one. To replace an existing
623 ACL, pass the ID of this ACL.
625 :param node: VPP node to set ACL on.
626 :param acl_idx: ID of ACL. (Optional)
627 :param rules: Required rules. (Optional)
628 :param tag: ACL tag (Optional).
634 reg_ex_src_ip = re.compile(r"(src [0-9a-fA-F.:/\d{1,2}]*)")
635 reg_ex_dst_ip = re.compile(r"(dst [0-9a-fA-F.:/\d{1,2}]*)")
636 reg_ex_sport = re.compile(r"(sport \d{1,5})")
637 reg_ex_dport = re.compile(r"(dport \d{1,5})")
638 reg_ex_proto = re.compile(r"(proto \d{1,5})")
641 for rule in rules.split(u", "):
643 acl_rule[u"is_permit"] = 1 if u"permit" in rule else 0
644 acl_rule[u"is_ipv6"] = 1 if u"ipv6" in rule else 0
646 groups = re.search(reg_ex_src_ip, rule)
648 grp = groups.group(1).split(u" ")[1].split(u"/")
649 acl_rule[u"src_ip_addr"] = ip_address(grp[0]).packed
650 acl_rule[u"src_ip_prefix_len"] = int(grp[1])
652 groups = re.search(reg_ex_dst_ip, rule)
654 grp = groups.group(1).split(u" ")[1].split(u"/")
655 acl_rule[u"dst_ip_addr"] = ip_address(grp[0]).packed
656 acl_rule[u"dst_ip_prefix_len"] = int(grp[1])
658 groups = re.search(reg_ex_sport, rule)
660 port = int(groups.group(1).split(u" ")[1])
661 acl_rule[u"srcport_or_icmptype_first"] = port
662 acl_rule[u"srcport_or_icmptype_last"] = port
664 acl_rule[u"srcport_or_icmptype_first"] = 0
665 acl_rule[u"srcport_or_icmptype_last"] = 65535
667 groups = re.search(reg_ex_dport, rule)
669 port = int(groups.group(1).split(u" ")[1])
670 acl_rule[u"dstport_or_icmpcode_first"] = port
671 acl_rule[u"dstport_or_icmpcode_last"] = port
673 acl_rule[u"dstport_or_icmpcode_first"] = 0
674 acl_rule[u"dstport_or_icmpcode_last"] = 65535
676 groups = re.search(reg_ex_proto, rule)
678 proto = int(groups.group(1).split(' ')[1])
679 acl_rule[u"proto"] = proto
681 acl_rule[u"proto"] = 0
683 acl_rules.append(acl_rule)
685 Classify._acl_add_replace(
686 node, acl_idx=acl_idx, rules=acl_rules, tag=tag)
689 def add_macip_acl_multi_entries(node, rules=u""):
690 """Add a new MACIP ACL.
692 :param node: VPP node to set MACIP ACL on.
693 :param rules: Required MACIP rules.
697 reg_ex_ip = re.compile(r"(ip [0-9a-fA-F.:/\d{1,2}]*)")
698 reg_ex_mac = re.compile(r"(mac \S\S:\S\S:\S\S:\S\S:\S\S:\S\S)")
699 reg_ex_mask = re.compile(r"(mask \S\S:\S\S:\S\S:\S\S:\S\S:\S\S)")
702 for rule in rules.split(u", "):
704 acl_rule[u"is_permit"] = 1 if u"permit" in rule else 0
705 acl_rule[u"is_ipv6"] = 1 if u"ipv6" in rule else 0
707 groups = re.search(reg_ex_mac, rule)
709 mac = groups.group(1).split(u" ")[1].replace(u":", u"")
710 acl_rule[u"src_mac"] = bytes.fromhex(mac)
712 groups = re.search(reg_ex_mask, rule)
714 mask = groups.group(1).split(u" ")[1].replace(u":", u"")
715 acl_rule[u"src_mac_mask"] = bytes.fromhex(mask)
717 groups = re.search(reg_ex_ip, rule)
719 grp = groups.group(1).split(u" ")[1].split(u"/")
720 acl_rule[u"src_ip_addr"] = ip_address((grp[0])).packed
721 acl_rule[u"src_ip_prefix_len"] = int(grp[1])
723 acl_rules.append(acl_rule)
725 Classify._macip_acl_add(node=node, rules=acl_rules)
728 def vpp_log_macip_acl_settings(node):
729 """Retrieve configured MACIP settings from the ACL plugin and write to
732 :param node: VPP node.
735 PapiSocketExecutor.dump_and_log(node, [u"macip_acl_dump", ])
738 def add_del_macip_acl_interface(node, interface, action, acl_idx):
739 """Apply/un-apply the MACIP ACL to/from a given interface.
741 :param node: VPP node to set MACIP ACL on.
742 :param interface: Interface name or sw_if_index.
743 :param action: Required action - add or del.
744 :param acl_idx: ACL index to be applied on the interface.
746 :type interface: str or int
748 :type acl_idx: str or int
749 :raises RuntimeError: If unable to set MACIP ACL for the interface.
751 if isinstance(interface, str):
752 sw_if_index = Topology.get_interface_sw_index(node, interface)
754 sw_if_index = interface
756 is_add = 1 if action == u"add" else 0
758 cmd = u"macip_acl_interface_add_del"
759 err_msg = f"Failed to get 'macip_acl_interface' on host {node[u'host']}"
762 sw_if_index=int(sw_if_index),
763 acl_index=int(acl_idx)
765 with PapiSocketExecutor(node) as papi_exec:
766 papi_exec.add(cmd, **args).get_reply(err_msg)
769 def vpp_log_macip_acl_interface_assignment(node):
770 """Get interface list and associated MACIP ACLs and write to robot log.
772 :param node: VPP node.
775 cmd = u"macip_acl_interface_get"
776 err_msg = f"Failed to get 'macip_acl_interface' on host {node[u'host']}"
777 with PapiSocketExecutor(node) as papi_exec:
778 reply = papi_exec.add(cmd).get_reply(err_msg)