| """ test framework utilities """ |
| |
| import abc |
| import ipaddress |
| import logging |
| import socket |
| from socket import AF_INET6 |
| import six |
| import sys |
| import os.path |
| |
| import scapy.compat |
| from scapy.layers.l2 import Ether |
| from scapy.layers.inet import IP |
| from scapy.layers.inet6 import IPv6, IPv6ExtHdrFragment, IPv6ExtHdrRouting,\ |
| IPv6ExtHdrHopByHop |
| from scapy.packet import Raw |
| from scapy.utils import hexdump |
| from scapy.utils6 import in6_mactoifaceid |
| |
| from io import BytesIO |
| from vpp_papi import mac_pton |
| |
| # Set up an empty logger for the testcase that can be overridden as necessary |
| null_logger = logging.getLogger('VppTestCase.util') |
| null_logger.addHandler(logging.NullHandler()) |
| |
| |
| def ppp(headline, packet): |
| """ Return string containing the output of scapy packet.show() call. """ |
| return '%s\n%s\n\n%s\n' % (headline, |
| hexdump(packet, dump=True), |
| packet.show(dump=True)) |
| |
| |
| def ppc(headline, capture, limit=10): |
| """ Return string containing ppp() printout for a capture. |
| |
| :param headline: printed as first line of output |
| :param capture: packets to print |
| :param limit: limit the print to # of packets |
| """ |
| if not capture: |
| return headline |
| tail = "" |
| if limit < len(capture): |
| tail = "\nPrint limit reached, %s out of %s packets printed" % ( |
| limit, len(capture)) |
| body = "".join([ppp("Packet #%s:" % count, p) |
| for count, p in zip(range(0, limit), capture)]) |
| return "%s\n%s%s" % (headline, body, tail) |
| |
| |
| def ip4_range(ip4, s, e): |
| tmp = ip4.rsplit('.', 1)[0] |
| return ("%s.%d" % (tmp, i) for i in range(s, e)) |
| |
| |
| def mcast_ip_to_mac(ip): |
| ip = ipaddress.ip_address(ip) |
| if not ip.is_multicast: |
| raise ValueError("Must be multicast address.") |
| ip_as_int = int(ip) |
| if ip.version == 4: |
| mcast_mac = "01:00:5e:%02x:%02x:%02x" % ((ip_as_int >> 16) & 0x7f, |
| (ip_as_int >> 8) & 0xff, |
| ip_as_int & 0xff) |
| else: |
| mcast_mac = "33:33:%02x:%02x:%02x:%02x" % ((ip_as_int >> 24) & 0xff, |
| (ip_as_int >> 16) & 0xff, |
| (ip_as_int >> 8) & 0xff, |
| ip_as_int & 0xff) |
| return mcast_mac |
| |
| |
| # wrapper around scapy library function. |
| def mk_ll_addr(mac): |
| euid = in6_mactoifaceid(str(mac)) |
| addr = "fe80::" + euid |
| return addr |
| |
| |
| def ip6_normalize(ip6): |
| return socket.inet_ntop(socket.AF_INET6, |
| socket.inet_pton(socket.AF_INET6, ip6)) |
| |
| |
| def get_core_path(tempdir): |
| return "%s/%s" % (tempdir, get_core_pattern()) |
| |
| |
| def is_core_present(tempdir): |
| return os.path.isfile(get_core_path(tempdir)) |
| |
| |
| def get_core_pattern(): |
| with open("/proc/sys/kernel/core_pattern", "r") as f: |
| corefmt = f.read().strip() |
| return corefmt |
| |
| |
| def check_core_path(logger, core_path): |
| corefmt = get_core_pattern() |
| if corefmt.startswith("|"): |
| logger.error( |
| "WARNING: redirecting the core dump through a" |
| " filter may result in truncated dumps.") |
| logger.error( |
| " You may want to check the filter settings" |
| " or uninstall it and edit the" |
| " /proc/sys/kernel/core_pattern accordingly.") |
| logger.error( |
| " current core pattern is: %s" % corefmt) |
| |
| |
| class NumericConstant(object): |
| |
| desc_dict = {} |
| |
| def __init__(self, value): |
| self._value = value |
| |
| def __int__(self): |
| return self._value |
| |
| def __long__(self): |
| return self._value |
| |
| def __str__(self): |
| if self._value in self.desc_dict: |
| return self.desc_dict[self._value] |
| return "" |
| |
| |
| class Host(object): |
| """ Generic test host "connected" to VPPs interface. """ |
| |
| @property |
| def mac(self): |
| """ MAC address """ |
| return self._mac |
| |
| @property |
| def bin_mac(self): |
| """ MAC address """ |
| return mac_pton(self._mac) |
| |
| @property |
| def ip4(self): |
| """ IPv4 address - string """ |
| return self._ip4 |
| |
| @property |
| def ip4n(self): |
| """ IPv4 address of remote host - raw, suitable as API parameter.""" |
| return socket.inet_pton(socket.AF_INET, self._ip4) |
| |
| @property |
| def ip6(self): |
| """ IPv6 address - string """ |
| return self._ip6 |
| |
| @property |
| def ip6n(self): |
| """ IPv6 address of remote host - raw, suitable as API parameter.""" |
| return socket.inet_pton(socket.AF_INET6, self._ip6) |
| |
| @property |
| def ip6_ll(self): |
| """ IPv6 link-local address - string """ |
| return self._ip6_ll |
| |
| @property |
| def ip6n_ll(self): |
| """ IPv6 link-local address of remote host - |
| raw, suitable as API parameter.""" |
| return socket.inet_pton(socket.AF_INET6, self._ip6_ll) |
| |
| def __eq__(self, other): |
| if isinstance(other, Host): |
| return (self.mac == other.mac and |
| self.ip4 == other.ip4 and |
| self.ip6 == other.ip6 and |
| self.ip6_ll == other.ip6_ll) |
| else: |
| return False |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def __repr__(self): |
| return "Host { mac:%s ip4:%s ip6:%s ip6_ll:%s }" % (self.mac, |
| self.ip4, |
| self.ip6, |
| self.ip6_ll) |
| |
| def __hash__(self): |
| return hash(self.__repr__()) |
| |
| def __init__(self, mac=None, ip4=None, ip6=None, ip6_ll=None): |
| self._mac = mac |
| self._ip4 = ip4 |
| self._ip6 = ip6 |
| self._ip6_ll = ip6_ll |
| |
| |
| class ForeignAddressFactory(object): |
| count = 0 |
| prefix_len = 24 |
| net_template = '10.10.10.{}' |
| net = net_template.format(0) + '/' + str(prefix_len) |
| |
| def get_ip4(self): |
| if self.count > 255: |
| raise Exception("Network host address exhaustion") |
| self.count += 1 |
| return self.net_template.format(self.count) |
| |
| |
| class L4_Conn(): |
| """ L4 'connection' tied to two VPP interfaces """ |
| |
| def __init__(self, testcase, if1, if2, af, l4proto, port1, port2): |
| self.testcase = testcase |
| self.ifs = [None, None] |
| self.ifs[0] = if1 |
| self.ifs[1] = if2 |
| self.address_family = af |
| self.l4proto = l4proto |
| self.ports = [None, None] |
| self.ports[0] = port1 |
| self.ports[1] = port2 |
| self |
| |
| def pkt(self, side, l4args={}, payload="x"): |
| is_ip6 = 1 if self.address_family == AF_INET6 else 0 |
| s0 = side |
| s1 = 1 - side |
| src_if = self.ifs[s0] |
| dst_if = self.ifs[s1] |
| layer_3 = [IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4), |
| IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6)] |
| merged_l4args = {'sport': self.ports[s0], 'dport': self.ports[s1]} |
| merged_l4args.update(l4args) |
| p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) / |
| layer_3[is_ip6] / |
| self.l4proto(**merged_l4args) / |
| Raw(payload)) |
| return p |
| |
| def send(self, side, flags=None, payload=""): |
| l4args = {} |
| if flags is not None: |
| l4args['flags'] = flags |
| self.ifs[side].add_stream(self.pkt(side, |
| l4args=l4args, payload=payload)) |
| self.ifs[1 - side].enable_capture() |
| self.testcase.pg_start() |
| |
| def recv(self, side): |
| p = self.ifs[side].wait_for_packet(1) |
| return p |
| |
| def send_through(self, side, flags=None, payload=""): |
| self.send(side, flags, payload) |
| p = self.recv(1 - side) |
| return p |
| |
| def send_pingpong(self, side, flags1=None, flags2=None): |
| p1 = self.send_through(side, flags1) |
| p2 = self.send_through(1 - side, flags2) |
| return [p1, p2] |
| |
| |
| class L4_CONN_SIDE: |
| L4_CONN_SIDE_ZERO = 0 |
| L4_CONN_SIDE_ONE = 1 |
| |
| |
| def fragment_rfc791(packet, fragsize, logger=null_logger): |
| """ |
| Fragment an IPv4 packet per RFC 791 |
| :param packet: packet to fragment |
| :param fragsize: size at which to fragment |
| :note: IP options are not supported |
| :returns: list of fragments |
| """ |
| logger.debug(ppp("Fragmenting packet:", packet)) |
| packet = packet.__class__(scapy.compat.raw(packet)) # recalc. all values |
| if len(packet[IP].options) > 0: |
| raise Exception("Not implemented") |
| if len(packet) <= fragsize: |
| return [packet] |
| |
| pre_ip_len = len(packet) - len(packet[IP]) |
| ip_header_len = packet[IP].ihl * 4 |
| hex_packet = scapy.compat.raw(packet) |
| hex_headers = hex_packet[:(pre_ip_len + ip_header_len)] |
| hex_payload = hex_packet[(pre_ip_len + ip_header_len):] |
| |
| pkts = [] |
| ihl = packet[IP].ihl |
| otl = len(packet[IP]) |
| nfb = int((fragsize - pre_ip_len - ihl * 4) / 8) |
| fo = packet[IP].frag |
| |
| p = packet.__class__(hex_headers + hex_payload[:nfb * 8]) |
| p[IP].flags = "MF" |
| p[IP].frag = fo |
| p[IP].len = ihl * 4 + nfb * 8 |
| del p[IP].chksum |
| pkts.append(p) |
| |
| p = packet.__class__(hex_headers + hex_payload[nfb * 8:]) |
| p[IP].len = otl - nfb * 8 |
| p[IP].frag = fo + nfb |
| del p[IP].chksum |
| |
| more_fragments = fragment_rfc791(p, fragsize, logger) |
| pkts.extend(more_fragments) |
| |
| return pkts |
| |
| |
| def fragment_rfc8200(packet, identification, fragsize, logger=null_logger): |
| """ |
| Fragment an IPv6 packet per RFC 8200 |
| :param packet: packet to fragment |
| :param fragsize: size at which to fragment |
| :note: IP options are not supported |
| :returns: list of fragments |
| """ |
| packet = packet.__class__(scapy.compat.raw(packet)) # recalc. all values |
| if len(packet) <= fragsize: |
| return [packet] |
| logger.debug(ppp("Fragmenting packet:", packet)) |
| pkts = [] |
| counter = 0 |
| routing_hdr = None |
| hop_by_hop_hdr = None |
| upper_layer = None |
| seen_ipv6 = False |
| ipv6_nr = -1 |
| l = packet.getlayer(counter) |
| while l is not None: |
| if l.__class__ is IPv6: |
| if seen_ipv6: |
| # ignore 2nd IPv6 header and everything below.. |
| break |
| ipv6_nr = counter |
| seen_ipv6 = True |
| elif l.__class__ is IPv6ExtHdrFragment: |
| raise Exception("Already fragmented") |
| elif l.__class__ is IPv6ExtHdrRouting: |
| routing_hdr = counter |
| elif l.__class__ is IPv6ExtHdrHopByHop: |
| hop_by_hop_hdr = counter |
| elif seen_ipv6 and not upper_layer and \ |
| not l.__class__.__name__.startswith('IPv6ExtHdr'): |
| upper_layer = counter |
| counter = counter + 1 |
| l = packet.getlayer(counter) |
| |
| logger.debug( |
| "Layers seen: IPv6(#%s), Routing(#%s), HopByHop(#%s), upper(#%s)" % |
| (ipv6_nr, routing_hdr, hop_by_hop_hdr, upper_layer)) |
| |
| if upper_layer is None: |
| raise Exception("Upper layer header not found in IPv6 packet") |
| |
| last_per_fragment_hdr = ipv6_nr |
| if routing_hdr is None: |
| if hop_by_hop_hdr is not None: |
| last_per_fragment_hdr = hop_by_hop_hdr |
| else: |
| last_per_fragment_hdr = routing_hdr |
| logger.debug("Last per-fragment hdr is #%s" % (last_per_fragment_hdr)) |
| |
| per_fragment_headers = packet.copy() |
| per_fragment_headers[last_per_fragment_hdr].remove_payload() |
| logger.debug(ppp("Per-fragment headers:", per_fragment_headers)) |
| |
| ext_and_upper_layer = packet.getlayer(last_per_fragment_hdr)[1] |
| hex_payload = scapy.compat.raw(ext_and_upper_layer) |
| logger.debug("Payload length is %s" % len(hex_payload)) |
| logger.debug(ppp("Ext and upper layer:", ext_and_upper_layer)) |
| |
| fragment_ext_hdr = IPv6ExtHdrFragment() |
| logger.debug(ppp("Fragment header:", fragment_ext_hdr)) |
| |
| len_ext_and_upper_layer_payload = len(ext_and_upper_layer.payload) |
| if not len_ext_and_upper_layer_payload and \ |
| hasattr(ext_and_upper_layer, "data"): |
| len_ext_and_upper_layer_payload = len(ext_and_upper_layer.data) |
| |
| if len(per_fragment_headers) + len(fragment_ext_hdr) +\ |
| len(ext_and_upper_layer) - len_ext_and_upper_layer_payload\ |
| > fragsize: |
| raise Exception("Cannot fragment this packet - MTU too small " |
| "(%s, %s, %s, %s, %s)" % ( |
| len(per_fragment_headers), len(fragment_ext_hdr), |
| len(ext_and_upper_layer), |
| len_ext_and_upper_layer_payload, fragsize)) |
| |
| orig_nh = packet[IPv6].nh |
| p = per_fragment_headers |
| del p[IPv6].plen |
| del p[IPv6].nh |
| p = p / fragment_ext_hdr |
| del p[IPv6ExtHdrFragment].nh |
| first_payload_len_nfb = int((fragsize - len(p)) / 8) |
| p = p / Raw(hex_payload[:first_payload_len_nfb * 8]) |
| del p[IPv6].plen |
| p[IPv6ExtHdrFragment].nh = orig_nh |
| p[IPv6ExtHdrFragment].id = identification |
| p[IPv6ExtHdrFragment].offset = 0 |
| p[IPv6ExtHdrFragment].m = 1 |
| p = p.__class__(scapy.compat.raw(p)) |
| logger.debug(ppp("Fragment %s:" % len(pkts), p)) |
| pkts.append(p) |
| offset = first_payload_len_nfb * 8 |
| logger.debug("Offset after first fragment: %s" % offset) |
| while len(hex_payload) > offset: |
| p = per_fragment_headers |
| del p[IPv6].plen |
| del p[IPv6].nh |
| p = p / fragment_ext_hdr |
| del p[IPv6ExtHdrFragment].nh |
| l_nfb = int((fragsize - len(p)) / 8) |
| p = p / Raw(hex_payload[offset:offset + l_nfb * 8]) |
| p[IPv6ExtHdrFragment].nh = orig_nh |
| p[IPv6ExtHdrFragment].id = identification |
| p[IPv6ExtHdrFragment].offset = int(offset / 8) |
| p[IPv6ExtHdrFragment].m = 1 |
| p = p.__class__(scapy.compat.raw(p)) |
| logger.debug(ppp("Fragment %s:" % len(pkts), p)) |
| pkts.append(p) |
| offset = offset + l_nfb * 8 |
| |
| pkts[-1][IPv6ExtHdrFragment].m = 0 # reset more-flags in last fragment |
| |
| return pkts |
| |
| |
| def reassemble4_core(listoffragments, return_ip): |
| buffer = BytesIO() |
| first = listoffragments[0] |
| buffer.seek(20) |
| for pkt in listoffragments: |
| buffer.seek(pkt[IP].frag*8) |
| buffer.write(bytes(pkt[IP].payload)) |
| first.len = len(buffer.getvalue()) + 20 |
| first.flags = 0 |
| del(first.chksum) |
| if return_ip: |
| header = bytes(first[IP])[:20] |
| return first[IP].__class__(header + buffer.getvalue()) |
| else: |
| header = bytes(first[Ether])[:34] |
| return first[Ether].__class__(header + buffer.getvalue()) |
| |
| |
| def reassemble4_ether(listoffragments): |
| return reassemble4_core(listoffragments, False) |
| |
| |
| def reassemble4(listoffragments): |
| return reassemble4_core(listoffragments, True) |