89b8c3c2068ed4a3bd0d9e74c512c7839c6d79ae
[csit.git] / GPL / traffic_scripts / PacketVerifier.py
1 # Copyright (c) 2021 Cisco and/or its affiliates.
2 #
3 # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
4 #
5 # Licensed under the Apache License 2.0 or
6 # GNU General Public License v2.0 or later;  you may not use this file
7 # except in compliance with one of these Licenses. You
8 # may obtain a copy of the Licenses at:
9 #
10 #     http://www.apache.org/licenses/LICENSE-2.0
11 #     https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12 #
13 # Note: If this file is linked with Scapy, which is GPLv2+, your use of it
14 # must be under GPLv2+.  If at any point in the future it is no longer linked
15 # with Scapy (or other GPLv2+ licensed software), you are free to choose
16 # Apache 2.
17 #
18 # Unless required by applicable law or agreed to in writing, software
19 # distributed under the License is distributed on an "AS IS" BASIS,
20 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 # See the License for the specific language governing permissions and
22 # limitations under the License.
23
24 """PacketVerifier module.
25
26   Example. ::
27
28     | >>> from scapy.all import *
29     | >>> from PacketVerifier import *
30     | >>> rxq = RxQueue('eth1')
31     | >>> txq = TxQueue('eth1')
32     | >>> src_mac = "AA:BB:CC:DD:EE:FF"
33     | >>> dst_mac = "52:54:00:ca:5d:0b"
34     | >>> src_ip = "11.11.11.10"
35     | >>> dst_ip = "11.11.11.11"
36     | >>> sent_packets = []
37     | >>> pkt_send = Ether(src=src_mac, dst=dst_mac) /
38     | ... IP(src=src_ip, dst=dst_ip) /
39     | ... ICMP()
40     | >>> sent_packets.append(pkt_send)
41     | >>> txq.send(pkt_send)
42     | >>> pkt_send = Ether(src=src_mac, dst=dst_mac) /
43     | ... ARP(hwsrc=src_mac, psrc=src_ip, hwdst=dst_mac, pdst=dst_ip, op=2)
44     | >>> sent_packets.append(pkt_send)
45     | >>> txq.send(pkt_send)
46     | >>> rxq.recv(100, sent_packets).show()
47     | ###[ Ethernet ]###
48     |   dst       = aa:bb:cc:dd:ee:ff
49     |   src       = 52:54:00:ca:5d:0b
50     |   type      = 0x800
51     | ###[ IP ]###
52     |   version   = 4L
53     |   ihl       = 5L
54     |   tos       = 0x0
55     |   len       = 28
56     |   id        = 43183
57     |   flags     =
58     |   frag      = 0L
59     |   ttl       = 64
60     |   proto     = icmp
61     |   chksum    = 0xa607
62     |   src       = 11.11.11.11
63     |   dst       = 11.11.11.10
64     |   options
65     | ###[ ICMP ]###
66     |   type      = echo-reply
67     |   code      = 0
68     |   chksum    = 0xffff
69     |   id        = 0x0
70     |   seq       = 0x0
71     | ###[ Padding ]###
72     |   load = 'RT\x00\xca]\x0b\xaa\xbb\xcc\xdd\xee\xff\x08\x06\x00\x01\x08\x00'
73
74   Example end.
75 """
76
77 import os
78 import select
79
80 from scapy.all import ETH_P_IP, ETH_P_IPV6, ETH_P_ALL, ETH_P_ARP
81 from scapy.config import conf
82 from scapy.layers.inet6 import IPv6
83 from scapy.layers.l2 import Ether, ARP
84 from scapy.packet import Raw, Padding
85
86 # Enable libpcap's L2listen
87 conf.use_pcap = True
88
89 __all__ = [
90     u"RxQueue", u"TxQueue", u"Interface", u"create_gratuitous_arp_request",
91     u"auto_pad", u"checksum_equal"
92 ]
93
94 # TODO: http://stackoverflow.com/questions/320232/
95 # ensuring-subprocesses-are-dead-on-exiting-python-program
96
97
98 class PacketVerifier:
99     """Base class for TX and RX queue objects for packet verifier."""
100     def __init__(self, interface_name):
101         os.system(
102             f"sudo echo 1 > /proc/sys/net/ipv6/conf/{interface_name}/"
103             f"disable_ipv6"
104         )
105         os.system(f"sudo ip link set {interface_name} up promisc on")
106         self._ifname = interface_name
107
108
109 def extract_one_packet(buf):
110     """Extract one packet from the incoming buf buffer.
111
112     Takes string as input and looks for first whole packet in it.
113     If it finds one, it returns substring from the buf parameter.
114
115     :param buf: String representation of incoming packet buffer.
116     :type buf: str
117     :returns: String representation of first packet in buf.
118     :rtype: str
119     """
120     pkt_len = 0
121
122     if len(buf) < 60:
123         return None
124
125     try:
126         ether_type = Ether(buf[0:14]).type
127     except AttributeError:
128         raise RuntimeError(f"No EtherType in packet {buf!r}")
129
130     if ether_type == ETH_P_IP:
131         # 14 is Ethernet fame header size.
132         # 4 bytes is just enough to look for length in ip header.
133         # ip total length contains just the IP packet length so add the Ether
134         #     header.
135         pkt_len = Ether(buf[0:14+4]).len + 14
136         if len(buf) < 60:
137             return None
138     elif ether_type == ETH_P_IPV6:
139         if not Ether(buf[0:14+6]).haslayer(IPv6):
140             raise RuntimeError(f"Invalid IPv6 packet {buf!r}")
141         # ... to add to the above, 40 bytes is the length of IPV6 header.
142         #   The ipv6.len only contains length of the payload and not the header
143         pkt_len = Ether(buf)[u"IPv6"].plen + 14 + 40
144         if len(buf) < 60:
145             return None
146     elif ether_type == ETH_P_ARP:
147         pkt = Ether(buf[:20])
148         if not pkt.haslayer(ARP):
149             raise RuntimeError(u"Incomplete ARP packet")
150         # len(eth) + arp(2 hw addr type + 2 proto addr type
151         #                + 1b len + 1b len + 2b operation)
152
153         pkt_len = 14 + 8
154         pkt_len += 2 * pkt.getlayer(ARP).hwlen
155         pkt_len += 2 * pkt.getlayer(ARP).plen
156
157         del pkt
158     elif ether_type == 32821:  # RARP (Reverse ARP)
159         pkt = Ether(buf[:20])
160         pkt.type = ETH_P_ARP  # Change to ARP so it works with scapy
161         pkt = Ether(pkt)
162         if not pkt.haslayer(ARP):
163             pkt.show()
164             raise RuntimeError(u"Incomplete RARP packet")
165
166         # len(eth) + arp(2 hw addr type + 2 proto addr type
167         #                + 1b len + 1b len + 2b operation)
168         pkt_len = 14 + 8
169         pkt_len += 2 * pkt.getlayer(ARP).hwlen
170         pkt_len += 2 * pkt.getlayer(ARP).plen
171
172         del pkt
173     else:
174         raise RuntimeError(f"Unknown protocol {ether_type}")
175
176     if pkt_len < 60:
177         pkt_len = 60
178
179     if len(buf) < pkt_len:
180         return None
181
182     return buf[0:pkt_len]
183
184
185 def packet_reader(interface_name, queue):
186     """Sub-process routine that reads packets and puts them to queue.
187
188     This function is meant to be run in separate subprocess and is in tight
189     loop reading raw packets from interface passed as parameter.
190
191     :param interface_name: Name of interface to read packets from.
192     :param queue: Queue in which this function will push incoming packets.
193     :type interface_name: str
194     :type queue: multiprocessing.Queue
195     """
196     sock = conf.L2listen(iface=interface_name, type=ETH_P_ALL)
197
198     while True:
199         pkt = sock.recv(0x7fff)
200         queue.put(pkt)
201
202
203 class RxQueue(PacketVerifier):
204     """Receive queue object.
205
206     This object creates raw socket, reads packets from it and provides
207     function to access them.
208
209     :param interface_name: Which interface to bind to.
210     :type interface_name: str
211     """
212     def __init__(self, interface_name):
213         PacketVerifier.__init__(self, interface_name)
214         self._sock = conf.L2listen(iface=interface_name, type=ETH_P_ALL)
215
216     def recv(self, timeout=3, ignore=None, verbose=True):
217         """Read next received packet.
218
219         Returns scapy's Ether() object created from next packet in the queue.
220         Queue is being filled in parallel in subprocess. If no packet
221         arrives in given timeout None is returned.
222
223         If the list of packets to ignore is given, they are logged
224         but otherwise ignored upon arrival, not adding to the timeout.
225         Each time a packet is ignored, it is removed from the ignored list.
226
227         :param timeout: How many seconds to wait for next packet.
228         :param ignore: List of packets that should be ignored.
229         :param verbose: Used to suppress detailed logging of received packets.
230         :type timeout: int
231         :type ignore: list
232         :type verbose: bool
233
234         :returns: Ether() initialized object from packet data.
235         :rtype: scapy.Ether
236         """
237         time_end = time.monotonic() + timeout
238         ignore = ignore if ignore else list()
239         # Auto pad all packets in ignore list
240         ignore = [str(auto_pad(ig_pkt)) for ig_pkt in ignore]
241         while 1:
242             time_now = time.monotonic()
243             if time_now >= time_end:
244                 return None
245             timedelta = time_end - time_now
246             rlist, _, _ = select.select([self._sock], [], [], timedelta)
247             if self._sock not in rlist:
248                 # Might have been an interrupt.
249                 continue
250             pkt = self._sock.recv(0x7fff)
251             pkt_pad = str(auto_pad(pkt))
252             print(f"Received packet on {self._ifname} of len {len(pkt)}")
253             if verbose:
254                 if hasattr(pkt, u"show2"):
255                     pkt.show2()
256                 else:
257                     # Never happens in practice, but Pylint does not know that.
258                     print(f"Unexpected instance: {pkt!r}")
259                 print()
260             if pkt_pad in ignore:
261                 ignore.remove(pkt_pad)
262                 print(u"Received packet ignored.")
263                 continue
264             return pkt
265
266
267 class TxQueue(PacketVerifier):
268     """Transmission queue object.
269
270     This object is used to send packets over RAW socket on a interface.
271
272     :param interface_name: Which interface to send packets from.
273     :type interface_name: str
274     """
275     def __init__(self, interface_name):
276         PacketVerifier.__init__(self, interface_name)
277         self._sock = conf.L2socket(iface=interface_name, type=ETH_P_ALL)
278
279     def send(self, pkt, verbose=True):
280         """Send packet out of the bound interface.
281
282         :param pkt: Packet to send.
283         :param verbose: Used to suppress detailed logging of sent packets.
284         :type pkt: string or scapy Packet derivative.
285         :type verbose: bool
286         """
287         pkt = auto_pad(pkt)
288         print(f"Sending packet out of {self._ifname} of len {len(pkt)}")
289         if verbose:
290             pkt.show2()
291             print()
292
293         self._sock.send(pkt)
294
295
296 class Interface:
297     """Class for network interfaces. Contains methods for sending and receiving
298      packets."""
299     def __init__(self, if_name):
300         """Initialize the interface class.
301
302         :param if_name: Name of the interface.
303         :type if_name: str
304         """
305         self.if_name = if_name
306         self.sent_packets = []
307         self.rxq = RxQueue(if_name)
308         self.txq = TxQueue(if_name)
309
310     def send_pkt(self, pkt):
311         """Send the provided packet out the interface."""
312         self.sent_packets.append(pkt)
313         self.txq.send(pkt)
314
315     def recv_pkt(self, timeout=3):
316         """Read one packet from the interface's receive queue.
317
318         :param timeout: Timeout value in seconds.
319         :type timeout: int
320         :returns: Ether() initialized object from packet data.
321         :rtype: scapy.Ether
322         """
323         return self.rxq.recv(timeout, self.sent_packets)
324
325
326 def create_gratuitous_arp_request(src_mac, src_ip):
327     """Creates scapy representation of gratuitous ARP request."""
328     return (Ether(src=src_mac, dst=u"ff:ff:ff:ff:ff:ff") /
329             ARP(psrc=src_ip, hwsrc=src_mac, pdst=src_ip)
330             )
331
332
333 def auto_pad(packet):
334     """Pads zeroes at the end of the packet if the total packet length is less
335     then 60 bytes in case of IPv4 or 78 bytes in case of IPv6.
336     """
337     # TODO: add document explaining deduction of FCS part
338     min_len = 78 if packet.haslayer(IPv6) else 60
339     pad_layer = Raw if packet.haslayer(Raw) \
340         else Padding if packet.haslayer(Padding) else None
341     if pad_layer:
342         packet[pad_layer].load += (b"\0" * (min_len - len(packet)))
343     return packet
344
345
346 def checksum_equal(chksum1, chksum2):
347     """Compares two checksums in one's complement notation.
348
349     Checksums to be compared are calculated as 16 bit one's complement of the
350     one's complement sum of 16 bit words of some buffer.
351     In one's complement notation 0x0000 (positive zero) and 0xFFFF
352     (negative zero) are equivalent.
353
354     :param chksum1: First checksum.
355     :param chksum2: Second checksum.
356     :type chksum1: uint16
357     :type chksum2: uint16
358
359     :returns: True if checksums are equivalent, False otherwise.
360     :rtype: boolean
361     """
362     if chksum1 == 0xFFFF:
363         chksum1 = 0
364     if chksum2 == 0xFFFF:
365         chksum2 = 0
366     return chksum1 == chksum2