1 # Copyright (c) 2021 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.InterfaceUtil import InterfaceUtil
24 from resources.libraries.python.IPUtil import IPUtil
25 from resources.libraries.python.PapiExecutor import PapiSocketExecutor
29 """Classify utilities."""
32 def _build_mac_mask(dst_mac=u"", src_mac=u"", ether_type=u""):
33 """Build MAC ACL mask data in bytes format.
35 :param dst_mac: Source MAC address <0-ffffffffffff>.
36 :param src_mac: Destination MAC address <0-ffffffffffff>.
37 :param ether_type: Ethernet type <0-ffff>.
41 :returns MAC ACL mask in bytes format.
45 f"{dst_mac.replace(u':', u'')!s:0>12}"
46 f"{src_mac.replace(u':', u'')!s:0>12}"
52 proto=u"", src_ip=u"", dst_ip=u"", src_port=u"", dst_port=u""):
53 """Build IP ACL mask data in bytes format.
55 :param proto: Protocol number <0-ff>.
56 :param src_ip: Source ip address <0-ffffffff>.
57 :param dst_ip: Destination ip address <0-ffffffff>.
58 :param src_port: Source port number <0-ffff>.
59 :param str dst_port: Destination port number <0-ffff>.
65 :returns: IP mask in bytes format.
69 f"{proto!s:0>20}{src_ip!s:0>12}{dst_ip!s:0>8}{src_port!s:0>4}"
75 next_hdr=u"", src_ip=u"", dst_ip=u"", src_port=u"", dst_port=u""):
76 """Build IPv6 ACL mask data in bytes format.
78 :param next_hdr: Next header number <0-ff>.
79 :param src_ip: Source ip address <0-ffffffff>.
80 :param dst_ip: Destination ip address <0-ffffffff>.
81 :param src_port: Source port number <0-ffff>.
82 :param dst_port: Destination port number <0-ffff>.
88 :returns: IPv6 ACL mask in bytes format.
92 f"{next_hdr!s:0>14}{src_ip!s:0>34}{dst_ip!s:0>32}{src_port!s:0>4}"
97 def _build_mac_match(dst_mac=u"", src_mac=u"", ether_type=u""):
98 """Build MAC ACL match data in bytes format.
100 :param dst_mac: Source MAC address <x:x:x:x:x:x>.
101 :param src_mac: Destination MAC address <x:x:x:x:x:x>.
102 :param ether_type: Ethernet type <0-ffff>.
105 :type ether_type: str
106 :returns: MAC ACL match data in bytes format.
109 return bytes.fromhex(
110 f"{dst_mac.replace(u':', u'')!s:0>12}"
111 f"{src_mac.replace(u':', u'')!s:0>12}"
112 f"{ether_type!s:0>4}"
117 proto=0, src_ip=4*b"\0", dst_ip=4*b"\0", src_port=0, dst_port=0):
118 """Build IP ACL match data in bytes format.
120 :param proto: Protocol number with valid option "x".
121 :param src_ip: Source ip address in packed format.
122 :param dst_ip: Destination ip address in packed format.
123 :param src_port: Source port number "x".
124 :param dst_port: Destination port number "x".
130 :returns: IP ACL match data in byte-string format.
133 return bytes.fromhex(
134 f"{hex(proto)[2:]!s:0>20}{src_ip.hex()!s:0>12}{dst_ip.hex()!s:0>8}"
135 f"{hex(src_port)[2:]!s:0>4}{hex(dst_port)[2:]!s:0>4}"
139 def _build_ip6_match(
140 next_hdr=0, src_ip=16*b"\0", dst_ip=16*b"\0", src_port=0,
142 """Build IPv6 ACL match data in byte-string format.
144 :param next_hdr: Next header number with valid option "x".
145 :param src_ip: Source ip6 address in packed format.
146 :param dst_ip: Destination ip6 address in packed format.
147 :param src_port: Source port number "x".
148 :param dst_port: Destination port number "x".
154 :returns: IPv6 ACL match data in bytes format.
157 return bytes.fromhex(
158 f"{hex(next_hdr)[2:]!s:0>14}{src_ip.hex()!s:0>34}"
159 f"{dst_ip.hex()!s:0>32}{hex(src_port)[2:]!s:0>4}"
160 f"{hex(dst_port)[2:]!s:0>4}"
164 def _classify_add_del_table(
165 node, is_add, mask, match_n_vectors=Constants.BITWISE_NON_ZERO,
166 table_index=Constants.BITWISE_NON_ZERO, nbuckets=2,
167 memory_size=2097152, skip_n_vectors=Constants.BITWISE_NON_ZERO,
168 next_table_index=Constants.BITWISE_NON_ZERO,
169 miss_next_index=Constants.BITWISE_NON_ZERO,
170 current_data_flag=0, current_data_offset=0):
171 """Add or delete a classify table.
173 :param node: VPP node to create classify table.
174 :param is_add: If True the table is added, if False table is deleted.
175 :param mask: ACL mask in hexstring format.
176 :param match_n_vectors: Number of vectors to match (Default value = ~0).
177 :param table_index: Index of the classify table. (Default value = ~0)
178 :param nbuckets: Number of buckets when adding a table.
180 :param memory_size: Memory size when adding a table.
181 (Default value = 2097152)
182 :param skip_n_vectors: Number of skip vectors (Default value = ~0).
183 :param next_table_index: Index of next table. (Default value = ~0)
184 :param miss_next_index: Index of miss table. (Default value = ~0)
185 :param current_data_flag: Option to use current node's packet payload
186 as the starting point from where packets are classified.
187 This option is only valid for L2/L3 input ACL for now.
188 0: by default, classify data from the buffer's start location
189 1: classify packets from VPP node's current data pointer.
190 :param current_data_offset: A signed value to shift the start location
191 of the packet to be classified.
192 For example, if input IP ACL node is used, L2 header's first byte
193 can be accessible by configuring current_data_offset to -14
194 if there is no vlan tag.
195 This is valid only if current_data_flag is set to 1.
200 :type match_n_vectors: int
201 :type table_index: int
203 :type memory_size: int
204 :type skip_n_vectors: int
205 :type next_table_index: int
206 :type miss_next_index: int
207 :type current_data_flag: int
208 :type current_data_offset: int
209 :returns: (table_index, skip_n, match_n)
210 table_index: Classify table index.
211 skip_n: Number of skip vectors.
212 match_n: Number of match vectors.
213 :rtype: tuple(int, int, int)
215 cmd = u"classify_add_del_table"
219 table_index=table_index,
221 memory_size=memory_size,
222 skip_n_vectors=skip_n_vectors,
223 match_n_vectors=match_n_vectors,
224 next_table_index=next_table_index,
225 miss_next_index=miss_next_index,
226 current_data_flag=current_data_flag,
227 current_data_offset=current_data_offset,
231 err_msg = f"Failed to create a classify table on host {node[u'host']}"
233 with PapiSocketExecutor(node) as papi_exec:
234 reply = papi_exec.add(cmd, **args).get_reply(err_msg)
236 return int(reply[u"new_table_index"]), int(reply[u"skip_n_vectors"]),\
237 int(reply[u"match_n_vectors"])
240 def _classify_add_del_session(
241 node, is_add, table_index, match,
242 opaque_index=Constants.BITWISE_NON_ZERO,
243 hit_next_index=Constants.BITWISE_NON_ZERO, advance=0,
244 action=0, metadata=0):
245 """Add or delete a classify session.
247 :param node: VPP node to create classify session.
248 :param is_add: If True the session is added, if False the session
250 :param table_index: Index of the table to add/del the session.
251 :param match: For add, match value for session, required, needs to
252 include bytes in front with length of skip_n_vectors of target table
253 times sizeof (u32x4) (values of those bytes will be ignored).
254 :param opaque_index: For add, opaque_index of new session.
256 :param hit_next_index: For add, hit_next_index of new session.
258 :param advance: For add, advance value for session. (Default value = 0)
259 :param action: 0: No action (by default) metadata is not used.
260 1: Classified IP packets will be looked up from the specified ipv4
261 fib table (configured by metadata as VRF id).
262 Only valid for L3 input ACL node
263 2: Classified IP packets will be looked up from the specified ipv6
264 fib table (configured by metadata as VRF id).
265 Only valid for L3 input ACL node
266 3: Classified packet will be steered to source routing policy of
267 given index (in metadata).
268 This is only valid for IPv6 packets redirected to a source
270 :param metadata: Valid only if action != 0. VRF id if action is 1 or 2.
271 SR policy index if action is 3. (Default value = 0)
274 :type table_index: int
276 :type opaque_index: int
277 :type hit_next_index: int
282 cmd = u"classify_add_del_session"
285 table_index=table_index,
286 hit_next_index=hit_next_index,
287 opaque_index=opaque_index,
291 match_len=len(match),
294 err_msg = f"Failed to create a classify session on host {node[u'host']}"
296 with PapiSocketExecutor(node) as papi_exec:
297 papi_exec.add(cmd, **args).get_reply(err_msg)
300 def _macip_acl_add(node, rules, tag=""):
303 :param node: VPP node to add MACIP ACL.
304 :param rules: List of rules for given ACL.
310 cmd = u"macip_acl_add"
317 err_msg = f"Failed to add MACIP ACL on host {node[u'host']}"
319 with PapiSocketExecutor(node) as papi_exec:
320 papi_exec.add(cmd, **args).get_reply(err_msg)
323 def _acl_interface_set_acl_list(node, sw_if_index, acl_type, acls):
324 """Set ACL list for interface.
326 :param node: VPP node to set ACL list for interface.
327 :param sw_if_index: sw_if_index of the used interface.
328 :param acl_type: Type of ACL(s) - input or output.
329 :param acls: List of ACLs.
331 :type sw_if_index: int
335 cmd = u"acl_interface_set_acl_list"
337 sw_if_index=sw_if_index,
339 n_input=len(acls) if acl_type == u"input" else 0,
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"
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 Classify._acl_interface_set_acl_list(
611 sw_if_index=int(InterfaceUtil.get_interface_index(node, interface)),
613 acls=acl_idx if isinstance(acl_idx, list) else list()
617 def add_replace_acl_multi_entries(node, acl_idx=None, rules=None, tag=u""):
618 """Add a new ACL or replace the existing one. To replace an existing
619 ACL, pass the ID of this ACL.
621 :param node: VPP node to set ACL on.
622 :param acl_idx: ID of ACL. (Optional)
623 :param rules: Required rules. (Optional)
624 :param tag: ACL tag (Optional).
630 reg_ex_src_ip = re.compile(r"(src [0-9a-fA-F.:/\d{1,2}]*)")
631 reg_ex_dst_ip = re.compile(r"(dst [0-9a-fA-F.:/\d{1,2}]*)")
632 reg_ex_sport = re.compile(r"(sport \d{1,5})")
633 reg_ex_dport = re.compile(r"(dport \d{1,5})")
634 reg_ex_proto = re.compile(r"(proto \d{1,5})")
637 for rule in rules.split(u", "):
639 is_permit=2 if u"permit+reflect" in rule
640 else 1 if u"permit" in rule else 0,
644 srcport_or_icmptype_first=0,
645 srcport_or_icmptype_last=65535,
646 dstport_or_icmpcode_first=0,
647 dstport_or_icmpcode_last=65535,
652 groups = re.search(reg_ex_src_ip, rule)
654 grp = groups.group(1).split(u" ")[1].split(u"/")
655 acl_rule[u"src_prefix"] = IPUtil.create_prefix_object(
656 ip_address(grp[0]), int(grp[1])
659 groups = re.search(reg_ex_dst_ip, rule)
661 grp = groups.group(1).split(u" ")[1].split(u"/")
662 acl_rule[u"dst_prefix"] = IPUtil.create_prefix_object(
663 ip_address(grp[0]), int(grp[1])
666 groups = re.search(reg_ex_sport, rule)
668 port = int(groups.group(1).split(u" ")[1])
669 acl_rule[u"srcport_or_icmptype_first"] = port
670 acl_rule[u"srcport_or_icmptype_last"] = port
672 groups = re.search(reg_ex_dport, rule)
674 port = int(groups.group(1).split(u" ")[1])
675 acl_rule[u"dstport_or_icmpcode_first"] = port
676 acl_rule[u"dstport_or_icmpcode_last"] = port
678 groups = re.search(reg_ex_proto, rule)
680 proto = int(groups.group(1).split(' ')[1])
681 acl_rule[u"proto"] = proto
683 acl_rules.append(acl_rule)
685 Classify._acl_add_replace(
686 node, acl_idx=acl_idx, rules=acl_rules, tag=tag
690 def add_macip_acl_multi_entries(node, rules=u""):
691 """Add a new MACIP ACL.
693 :param node: VPP node to set MACIP ACL on.
694 :param rules: Required MACIP rules.
698 reg_ex_ip = re.compile(r"(ip [0-9a-fA-F.:/\d{1,2}]*)")
699 reg_ex_mac = re.compile(r"(mac \S\S:\S\S:\S\S:\S\S:\S\S:\S\S)")
700 reg_ex_mask = re.compile(r"(mask \S\S:\S\S:\S\S:\S\S:\S\S:\S\S)")
703 for rule in rules.split(u", "):
705 is_permit=2 if u"permit+reflect" in rule
706 else 1 if u"permit" in rule else 0,
712 groups = re.search(reg_ex_mac, rule)
714 mac = groups.group(1).split(u" ")[1].replace(u":", u"")
715 acl_rule[u"src_mac"] = bytes.fromhex(mac)
717 groups = re.search(reg_ex_mask, rule)
719 mask = groups.group(1).split(u" ")[1].replace(u":", u"")
720 acl_rule[u"src_mac_mask"] = bytes.fromhex(mask)
722 groups = re.search(reg_ex_ip, rule)
724 grp = groups.group(1).split(u" ")[1].split(u"/")
725 acl_rule[u"src_prefix"] = IPUtil.create_prefix_object(
726 ip_address((grp[0])), int(grp[1])
729 acl_rules.append(acl_rule)
731 Classify._macip_acl_add(node=node, rules=acl_rules)
734 def vpp_log_macip_acl_settings(node):
735 """Retrieve configured MACIP settings from the ACL plugin and write to
738 :param node: VPP node.
741 PapiSocketExecutor.dump_and_log(node, [u"macip_acl_dump", ])
744 def add_del_macip_acl_interface(node, interface, action, acl_idx):
745 """Apply/un-apply the MACIP ACL to/from a given interface.
747 :param node: VPP node to set MACIP ACL on.
748 :param interface: Interface name or sw_if_index.
749 :param action: Required action - add or del.
750 :param acl_idx: ACL index to be applied on the interface.
752 :type interface: str or int
754 :type acl_idx: str or int
755 :raises RuntimeError: If unable to set MACIP ACL for the interface.
757 cmd = u"macip_acl_interface_add_del"
758 err_msg = f"Failed to get 'macip_acl_interface' on host {node[u'host']}"
760 is_add=bool(action == u"add"),
761 sw_if_index=int(InterfaceUtil.get_interface_index(node, interface)),
762 acl_index=int(acl_idx)
764 with PapiSocketExecutor(node) as papi_exec:
765 papi_exec.add(cmd, **args).get_reply(err_msg)
768 def vpp_log_macip_acl_interface_assignment(node):
769 """Get interface list and associated MACIP ACLs and write to robot log.
771 :param node: VPP node.
774 cmd = u"macip_acl_interface_get"
775 err_msg = f"Failed to get 'macip_acl_interface' on host {node[u'host']}"
776 with PapiSocketExecutor(node) as papi_exec:
777 reply = papi_exec.add(cmd).get_reply(err_msg)