ip: Router ID included in flow hash
[vpp.git] / test / util.py
1 """ test framework utilities """
2
3 import abc
4 import ipaddress
5 import logging
6 import socket
7 from socket import AF_INET6
8 import sys
9 import os.path
10
11 import scapy.compat
12 from scapy.layers.l2 import Ether
13 from scapy.layers.inet import IP
14 from scapy.layers.inet6 import IPv6, IPv6ExtHdrFragment, IPv6ExtHdrRouting,\
15     IPv6ExtHdrHopByHop
16 from scapy.packet import Raw
17 from scapy.utils import hexdump
18 from scapy.utils6 import in6_mactoifaceid
19
20 from io import BytesIO
21 from vpp_papi import mac_pton
22
23 # Set up an empty logger for the testcase that can be overridden as necessary
24 null_logger = logging.getLogger('VppTestCase.util')
25 null_logger.addHandler(logging.NullHandler())
26
27
28 def ppp(headline, packet):
29     """ Return string containing the output of scapy packet.show() call. """
30     return '%s\n%s\n\n%s\n' % (headline,
31                                hexdump(packet, dump=True),
32                                packet.show(dump=True))
33
34
35 def ppc(headline, capture, limit=10):
36     """ Return string containing ppp() printout for a capture.
37
38     :param headline: printed as first line of output
39     :param capture: packets to print
40     :param limit: limit the print to # of packets
41     """
42     if not capture:
43         return headline
44     tail = ""
45     if limit < len(capture):
46         tail = "\nPrint limit reached, %s out of %s packets printed" % (
47             limit, len(capture))
48     body = "".join([ppp("Packet #%s:" % count, p)
49                     for count, p in zip(range(0, limit), capture)])
50     return "%s\n%s%s" % (headline, body, tail)
51
52
53 def ip4_range(ip4, s, e):
54     tmp = ip4.rsplit('.', 1)[0]
55     return ("%s.%d" % (tmp, i) for i in range(s, e))
56
57
58 def mcast_ip_to_mac(ip):
59     ip = ipaddress.ip_address(ip)
60     if not ip.is_multicast:
61         raise ValueError("Must be multicast address.")
62     ip_as_int = int(ip)
63     if ip.version == 4:
64         mcast_mac = "01:00:5e:%02x:%02x:%02x" % ((ip_as_int >> 16) & 0x7f,
65                                                  (ip_as_int >> 8) & 0xff,
66                                                  ip_as_int & 0xff)
67     else:
68         mcast_mac = "33:33:%02x:%02x:%02x:%02x" % ((ip_as_int >> 24) & 0xff,
69                                                    (ip_as_int >> 16) & 0xff,
70                                                    (ip_as_int >> 8) & 0xff,
71                                                    ip_as_int & 0xff)
72     return mcast_mac
73
74
75 # wrapper around scapy library function.
76 def mk_ll_addr(mac):
77     euid = in6_mactoifaceid(str(mac))
78     addr = "fe80::" + euid
79     return addr
80
81
82 def ip6_normalize(ip6):
83     return socket.inet_ntop(socket.AF_INET6,
84                             socket.inet_pton(socket.AF_INET6, ip6))
85
86
87 def get_core_path(tempdir):
88     return "%s/%s" % (tempdir, get_core_pattern())
89
90
91 def is_core_present(tempdir):
92     return os.path.isfile(get_core_path(tempdir))
93
94
95 def get_core_pattern():
96     with open("/proc/sys/kernel/core_pattern", "r") as f:
97         corefmt = f.read().strip()
98     return corefmt
99
100
101 def check_core_path(logger, core_path):
102     corefmt = get_core_pattern()
103     if corefmt.startswith("|"):
104         logger.error(
105             "WARNING: redirecting the core dump through a"
106             " filter may result in truncated dumps.")
107         logger.error(
108             "   You may want to check the filter settings"
109             " or uninstall it and edit the"
110             " /proc/sys/kernel/core_pattern accordingly.")
111         logger.error(
112             "   current core pattern is: %s" % corefmt)
113
114
115 class NumericConstant:
116
117     desc_dict = {}
118
119     def __init__(self, value):
120         self._value = value
121
122     def __int__(self):
123         return self._value
124
125     def __long__(self):
126         return self._value
127
128     def __str__(self):
129         if self._value in self.desc_dict:
130             return self.desc_dict[self._value]
131         return ""
132
133
134 class Host:
135     """ Generic test host "connected" to VPPs interface. """
136
137     @property
138     def mac(self):
139         """ MAC address """
140         return self._mac
141
142     @property
143     def bin_mac(self):
144         """ MAC address """
145         return mac_pton(self._mac)
146
147     @property
148     def ip4(self):
149         """ IPv4 address - string """
150         return self._ip4
151
152     @property
153     def ip4n(self):
154         """ IPv4 address of remote host - raw, suitable as API parameter."""
155         return socket.inet_pton(socket.AF_INET, self._ip4)
156
157     @property
158     def ip6(self):
159         """ IPv6 address - string """
160         return self._ip6
161
162     @property
163     def ip6n(self):
164         """ IPv6 address of remote host - raw, suitable as API parameter."""
165         return socket.inet_pton(socket.AF_INET6, self._ip6)
166
167     @property
168     def ip6_ll(self):
169         """ IPv6 link-local address - string """
170         return self._ip6_ll
171
172     @property
173     def ip6n_ll(self):
174         """ IPv6 link-local address of remote host -
175         raw, suitable as API parameter."""
176         return socket.inet_pton(socket.AF_INET6, self._ip6_ll)
177
178     def __eq__(self, other):
179         if isinstance(other, Host):
180             return (self.mac == other.mac and
181                     self.ip4 == other.ip4 and
182                     self.ip6 == other.ip6 and
183                     self.ip6_ll == other.ip6_ll)
184         else:
185             return False
186
187     def __ne__(self, other):
188         return not self.__eq__(other)
189
190     def __repr__(self):
191         return "Host { mac:%s ip4:%s ip6:%s ip6_ll:%s }" % (self.mac,
192                                                             self.ip4,
193                                                             self.ip6,
194                                                             self.ip6_ll)
195
196     def __hash__(self):
197         return hash(self.__repr__())
198
199     def __init__(self, mac=None, ip4=None, ip6=None, ip6_ll=None):
200         self._mac = mac
201         self._ip4 = ip4
202         self._ip6 = ip6
203         self._ip6_ll = ip6_ll
204
205
206 class L4_Conn():
207     """ L4 'connection' tied to two VPP interfaces """
208
209     def __init__(self, testcase, if1, if2, af, l4proto, port1, port2):
210         self.testcase = testcase
211         self.ifs = [None, None]
212         self.ifs[0] = if1
213         self.ifs[1] = if2
214         self.address_family = af
215         self.l4proto = l4proto
216         self.ports = [None, None]
217         self.ports[0] = port1
218         self.ports[1] = port2
219         self
220
221     def pkt(self, side, l4args={}, payload="x"):
222         is_ip6 = 1 if self.address_family == AF_INET6 else 0
223         s0 = side
224         s1 = 1 - side
225         src_if = self.ifs[s0]
226         dst_if = self.ifs[s1]
227         layer_3 = [IP(src=src_if.remote_ip4, dst=dst_if.remote_ip4),
228                    IPv6(src=src_if.remote_ip6, dst=dst_if.remote_ip6)]
229         merged_l4args = {'sport': self.ports[s0], 'dport': self.ports[s1]}
230         merged_l4args.update(l4args)
231         p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) /
232              layer_3[is_ip6] /
233              self.l4proto(**merged_l4args) /
234              Raw(payload))
235         return p
236
237     def send(self, side, flags=None, payload=""):
238         l4args = {}
239         if flags is not None:
240             l4args['flags'] = flags
241         self.ifs[side].add_stream(self.pkt(side,
242                                            l4args=l4args, payload=payload))
243         self.ifs[1 - side].enable_capture()
244         self.testcase.pg_start()
245
246     def recv(self, side):
247         p = self.ifs[side].wait_for_packet(1)
248         return p
249
250     def send_through(self, side, flags=None, payload=""):
251         self.send(side, flags, payload)
252         p = self.recv(1 - side)
253         return p
254
255     def send_pingpong(self, side, flags1=None, flags2=None):
256         p1 = self.send_through(side, flags1)
257         p2 = self.send_through(1 - side, flags2)
258         return [p1, p2]
259
260
261 class L4_CONN_SIDE:
262     L4_CONN_SIDE_ZERO = 0
263     L4_CONN_SIDE_ONE = 1
264
265
266 def fragment_rfc791(packet, fragsize, logger=null_logger):
267     """
268     Fragment an IPv4 packet per RFC 791
269     :param packet: packet to fragment
270     :param fragsize: size at which to fragment
271     :note: IP options are not supported
272     :returns: list of fragments
273     """
274     logger.debug(ppp("Fragmenting packet:", packet))
275     packet = packet.__class__(scapy.compat.raw(packet))  # recalc. all values
276     if len(packet[IP].options) > 0:
277         raise Exception("Not implemented")
278     if len(packet) <= fragsize:
279         return [packet]
280
281     pre_ip_len = len(packet) - len(packet[IP])
282     ip_header_len = packet[IP].ihl * 4
283     hex_packet = scapy.compat.raw(packet)
284     hex_headers = hex_packet[:(pre_ip_len + ip_header_len)]
285     hex_payload = hex_packet[(pre_ip_len + ip_header_len):]
286
287     pkts = []
288     ihl = packet[IP].ihl
289     otl = len(packet[IP])
290     nfb = int((fragsize - pre_ip_len - ihl * 4) / 8)
291     fo = packet[IP].frag
292
293     p = packet.__class__(hex_headers + hex_payload[:nfb * 8])
294     p[IP].flags = "MF"
295     p[IP].frag = fo
296     p[IP].len = ihl * 4 + nfb * 8
297     del p[IP].chksum
298     pkts.append(p)
299
300     p = packet.__class__(hex_headers + hex_payload[nfb * 8:])
301     p[IP].len = otl - nfb * 8
302     p[IP].frag = fo + nfb
303     del p[IP].chksum
304
305     more_fragments = fragment_rfc791(p, fragsize, logger)
306     pkts.extend(more_fragments)
307
308     return pkts
309
310
311 def fragment_rfc8200(packet, identification, fragsize, logger=null_logger):
312     """
313     Fragment an IPv6 packet per RFC 8200
314     :param packet: packet to fragment
315     :param fragsize: size at which to fragment
316     :note: IP options are not supported
317     :returns: list of fragments
318     """
319     packet = packet.__class__(scapy.compat.raw(packet))  # recalc. all values
320     if len(packet) <= fragsize:
321         return [packet]
322     logger.debug(ppp("Fragmenting packet:", packet))
323     pkts = []
324     counter = 0
325     routing_hdr = None
326     hop_by_hop_hdr = None
327     upper_layer = None
328     seen_ipv6 = False
329     ipv6_nr = -1
330     l = packet.getlayer(counter)
331     while l is not None:
332         if l.__class__ is IPv6:
333             if seen_ipv6:
334                 # ignore 2nd IPv6 header and everything below..
335                 break
336             ipv6_nr = counter
337             seen_ipv6 = True
338         elif l.__class__ is IPv6ExtHdrFragment:
339             raise Exception("Already fragmented")
340         elif l.__class__ is IPv6ExtHdrRouting:
341             routing_hdr = counter
342         elif l.__class__ is IPv6ExtHdrHopByHop:
343             hop_by_hop_hdr = counter
344         elif seen_ipv6 and not upper_layer and \
345                 not l.__class__.__name__.startswith('IPv6ExtHdr'):
346             upper_layer = counter
347         counter = counter + 1
348         l = packet.getlayer(counter)
349
350     logger.debug(
351         "Layers seen: IPv6(#%s), Routing(#%s), HopByHop(#%s), upper(#%s)" %
352         (ipv6_nr, routing_hdr, hop_by_hop_hdr, upper_layer))
353
354     if upper_layer is None:
355         raise Exception("Upper layer header not found in IPv6 packet")
356
357     last_per_fragment_hdr = ipv6_nr
358     if routing_hdr is None:
359         if hop_by_hop_hdr is not None:
360             last_per_fragment_hdr = hop_by_hop_hdr
361     else:
362         last_per_fragment_hdr = routing_hdr
363     logger.debug("Last per-fragment hdr is #%s" % (last_per_fragment_hdr))
364
365     per_fragment_headers = packet.copy()
366     per_fragment_headers[last_per_fragment_hdr].remove_payload()
367     logger.debug(ppp("Per-fragment headers:", per_fragment_headers))
368
369     ext_and_upper_layer = packet.getlayer(last_per_fragment_hdr)[1]
370     hex_payload = scapy.compat.raw(ext_and_upper_layer)
371     logger.debug("Payload length is %s" % len(hex_payload))
372     logger.debug(ppp("Ext and upper layer:", ext_and_upper_layer))
373
374     fragment_ext_hdr = IPv6ExtHdrFragment()
375     logger.debug(ppp("Fragment header:", fragment_ext_hdr))
376
377     len_ext_and_upper_layer_payload = len(ext_and_upper_layer.payload)
378     if not len_ext_and_upper_layer_payload and \
379        hasattr(ext_and_upper_layer, "data"):
380         len_ext_and_upper_layer_payload = len(ext_and_upper_layer.data)
381
382     if len(per_fragment_headers) + len(fragment_ext_hdr) +\
383             len(ext_and_upper_layer) - len_ext_and_upper_layer_payload\
384             > fragsize:
385         raise Exception("Cannot fragment this packet - MTU too small "
386                         "(%s, %s, %s, %s, %s)" % (
387                             len(per_fragment_headers), len(fragment_ext_hdr),
388                             len(ext_and_upper_layer),
389                             len_ext_and_upper_layer_payload, fragsize))
390
391     orig_nh = packet[IPv6].nh
392     p = per_fragment_headers
393     del p[IPv6].plen
394     del p[IPv6].nh
395     p = p / fragment_ext_hdr
396     del p[IPv6ExtHdrFragment].nh
397     first_payload_len_nfb = int((fragsize - len(p)) / 8)
398     p = p / Raw(hex_payload[:first_payload_len_nfb * 8])
399     del p[IPv6].plen
400     p[IPv6ExtHdrFragment].nh = orig_nh
401     p[IPv6ExtHdrFragment].id = identification
402     p[IPv6ExtHdrFragment].offset = 0
403     p[IPv6ExtHdrFragment].m = 1
404     p = p.__class__(scapy.compat.raw(p))
405     logger.debug(ppp("Fragment %s:" % len(pkts), p))
406     pkts.append(p)
407     offset = first_payload_len_nfb * 8
408     logger.debug("Offset after first fragment: %s" % offset)
409     while len(hex_payload) > offset:
410         p = per_fragment_headers
411         del p[IPv6].plen
412         del p[IPv6].nh
413         p = p / fragment_ext_hdr
414         del p[IPv6ExtHdrFragment].nh
415         l_nfb = int((fragsize - len(p)) / 8)
416         p = p / Raw(hex_payload[offset:offset + l_nfb * 8])
417         p[IPv6ExtHdrFragment].nh = orig_nh
418         p[IPv6ExtHdrFragment].id = identification
419         p[IPv6ExtHdrFragment].offset = int(offset / 8)
420         p[IPv6ExtHdrFragment].m = 1
421         p = p.__class__(scapy.compat.raw(p))
422         logger.debug(ppp("Fragment %s:" % len(pkts), p))
423         pkts.append(p)
424         offset = offset + l_nfb * 8
425
426     pkts[-1][IPv6ExtHdrFragment].m = 0  # reset more-flags in last fragment
427
428     return pkts
429
430
431 def reassemble4_core(listoffragments, return_ip):
432     buffer = BytesIO()
433     first = listoffragments[0]
434     buffer.seek(20)
435     for pkt in listoffragments:
436         buffer.seek(pkt[IP].frag*8)
437         buffer.write(bytes(pkt[IP].payload))
438     first.len = len(buffer.getvalue()) + 20
439     first.flags = 0
440     del(first.chksum)
441     if return_ip:
442         header = bytes(first[IP])[:20]
443         return first[IP].__class__(header + buffer.getvalue())
444     else:
445         header = bytes(first[Ether])[:34]
446         return first[Ether].__class__(header + buffer.getvalue())
447
448
449 def reassemble4_ether(listoffragments):
450     return reassemble4_core(listoffragments, False)
451
452
453 def reassemble4(listoffragments):
454     return reassemble4_core(listoffragments, True)