FIX: Pylint reduce
[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 import time
80
81 from scapy.all import ETH_P_IP, ETH_P_IPV6, ETH_P_ALL, ETH_P_ARP
82 from scapy.config import conf
83 from scapy.layers.inet6 import IPv6
84 from scapy.layers.l2 import Ether, ARP
85 from scapy.packet import Raw, Padding
86
87 # Enable libpcap's L2listen
88 conf.use_pcap = True
89
90 __all__ = [
91     u"RxQueue", u"TxQueue", u"Interface", u"create_gratuitous_arp_request",
92     u"auto_pad", u"checksum_equal"
93 ]
94
95 # TODO: http://stackoverflow.com/questions/320232/
96 # ensuring-subprocesses-are-dead-on-exiting-python-program
97
98
99 class PacketVerifier:
100     """Base class for TX and RX queue objects for packet verifier."""
101     def __init__(self, interface_name):
102         os.system(
103             f"sudo echo 1 > /proc/sys/net/ipv6/conf/{interface_name}/"
104             f"disable_ipv6"
105         )
106         os.system(f"sudo ip link set {interface_name} up promisc on")
107         self._ifname = interface_name
108
109
110 def extract_one_packet(buf):
111     """Extract one packet from the incoming buf buffer.
112
113     Takes string as input and looks for first whole packet in it.
114     If it finds one, it returns substring from the buf parameter.
115
116     :param buf: String representation of incoming packet buffer.
117     :type buf: str
118     :returns: String representation of first packet in buf.
119     :rtype: str
120     """
121     pkt_len = 0
122
123     if len(buf) < 60:
124         return None
125
126     try:
127         ether_type = Ether(buf[0:14]).type
128     except AttributeError:
129         raise RuntimeError(f"No EtherType in packet {buf!r}")
130
131     if ether_type == ETH_P_IP:
132         # 14 is Ethernet fame header size.
133         # 4 bytes is just enough to look for length in ip header.
134         # ip total length contains just the IP packet length so add the Ether
135         #     header.
136         pkt_len = Ether(buf[0:14+4]).len + 14
137         if len(buf) < 60:
138             return None
139     elif ether_type == ETH_P_IPV6:
140         if not Ether(buf[0:14+6]).haslayer(IPv6):
141             raise RuntimeError(f"Invalid IPv6 packet {buf!r}")
142         # ... to add to the above, 40 bytes is the length of IPV6 header.
143         #   The ipv6.len only contains length of the payload and not the header
144         pkt_len = Ether(buf)[u"IPv6"].plen + 14 + 40
145         if len(buf) < 60:
146             return None
147     elif ether_type == ETH_P_ARP:
148         pkt = Ether(buf[:20])
149         if not pkt.haslayer(ARP):
150             raise RuntimeError(u"Incomplete ARP packet")
151         # len(eth) + arp(2 hw addr type + 2 proto addr type
152         #                + 1b len + 1b len + 2b operation)
153
154         pkt_len = 14 + 8
155         pkt_len += 2 * pkt.getlayer(ARP).hwlen
156         pkt_len += 2 * pkt.getlayer(ARP).plen
157
158         del pkt
159     elif ether_type == 32821:  # RARP (Reverse ARP)
160         pkt = Ether(buf[:20])
161         pkt.type = ETH_P_ARP  # Change to ARP so it works with scapy
162         pkt = Ether(pkt)
163         if not pkt.haslayer(ARP):
164             pkt.show()
165             raise RuntimeError(u"Incomplete RARP packet")
166
167         # len(eth) + arp(2 hw addr type + 2 proto addr type
168         #                + 1b len + 1b len + 2b operation)
169         pkt_len = 14 + 8
170         pkt_len += 2 * pkt.getlayer(ARP).hwlen
171         pkt_len += 2 * pkt.getlayer(ARP).plen
172
173         del pkt
174     else:
175         raise RuntimeError(f"Unknown protocol {ether_type}")
176
177     if pkt_len < 60:
178         pkt_len = 60
179
180     if len(buf) < pkt_len:
181         return None
182
183     return buf[0:pkt_len]
184
185
186 def packet_reader(interface_name, queue):
187     """Sub-process routine that reads packets and puts them to queue.
188
189     This function is meant to be run in separate subprocess and is in tight
190     loop reading raw packets from interface passed as parameter.
191
192     :param interface_name: Name of interface to read packets from.
193     :param queue: Queue in which this function will push incoming packets.
194     :type interface_name: str
195     :type queue: multiprocessing.Queue
196     """
197     sock = conf.L2listen(iface=interface_name, type=ETH_P_ALL)
198
199     while True:
200         pkt = sock.recv(0x7fff)
201         queue.put(pkt)
202
203
204 class RxQueue(PacketVerifier):
205     """Receive queue object.
206
207     This object creates raw socket, reads packets from it and provides
208     function to access them.
209
210     :param interface_name: Which interface to bind to.
211     :type interface_name: str
212     """
213     def __init__(self, interface_name):
214         PacketVerifier.__init__(self, interface_name)
215         self._sock = conf.L2listen(iface=interface_name, type=ETH_P_ALL)
216
217     def recv(self, timeout=3, ignore=None, verbose=True):
218         """Read next received packet.
219
220         Returns scapy's Ether() object created from next packet in the queue.
221         Queue is being filled in parallel in subprocess. If no packet
222         arrives in given timeout None is returned.
223
224         If the list of packets to ignore is given, they are logged
225         but otherwise ignored upon arrival, not adding to the timeout.
226         Each time a packet is ignored, it is removed from the ignored list.
227
228         :param timeout: How many seconds to wait for next packet.
229         :param ignore: List of packets that should be ignored.
230         :param verbose: Used to suppress detailed logging of received packets.
231         :type timeout: int
232         :type ignore: list
233         :type verbose: bool
234
235         :returns: Ether() initialized object from packet data.
236         :rtype: scapy.Ether
237         """
238         time_end = time.monotonic() + timeout
239         ignore = ignore if ignore else list()
240         # Auto pad all packets in ignore list
241         ignore = [str(auto_pad(ig_pkt)) for ig_pkt in ignore]
242         while 1:
243             time_now = time.monotonic()
244             if time_now >= time_end:
245                 return None
246             timedelta = time_end - time_now
247             rlist, _, _ = select.select([self._sock], [], [], timedelta)
248             if self._sock not in rlist:
249                 # Might have been an interrupt.
250                 continue
251             pkt = self._sock.recv(0x7fff)
252             pkt_pad = str(auto_pad(pkt))
253             print(f"Received packet on {self._ifname} of len {len(pkt)}")
254             if verbose:
255                 if hasattr(pkt, u"show2"):
256                     pkt.show2()
257                 else:
258                     # Never happens in practice, but Pylint does not know that.
259                     print(f"Unexpected instance: {pkt!r}")
260                 print()
261             if pkt_pad in ignore:
262                 ignore.remove(pkt_pad)
263                 print(u"Received packet ignored.")
264                 continue
265             return pkt
266
267
268 class TxQueue(PacketVerifier):
269     """Transmission queue object.
270
271     This object is used to send packets over RAW socket on a interface.
272
273     :param interface_name: Which interface to send packets from.
274     :type interface_name: str
275     """
276     def __init__(self, interface_name):
277         PacketVerifier.__init__(self, interface_name)
278         self._sock = conf.L2socket(iface=interface_name, type=ETH_P_ALL)
279
280     def send(self, pkt, verbose=True):
281         """Send packet out of the bound interface.
282
283         :param pkt: Packet to send.
284         :param verbose: Used to suppress detailed logging of sent packets.
285         :type pkt: string or scapy Packet derivative.
286         :type verbose: bool
287         """
288         pkt = auto_pad(pkt)
289         print(f"Sending packet out of {self._ifname} of len {len(pkt)}")
290         if verbose:
291             pkt.show2()
292             print()
293
294         self._sock.send(pkt)
295
296
297 class Interface:
298     """Class for network interfaces. Contains methods for sending and receiving
299      packets."""
300     def __init__(self, if_name):
301         """Initialize the interface class.
302
303         :param if_name: Name of the interface.
304         :type if_name: str
305         """
306         self.if_name = if_name
307         self.sent_packets = []
308         self.rxq = RxQueue(if_name)
309         self.txq = TxQueue(if_name)
310
311     def send_pkt(self, pkt):
312         """Send the provided packet out the interface."""
313         self.sent_packets.append(pkt)
314         self.txq.send(pkt)
315
316     def recv_pkt(self, timeout=3):
317         """Read one packet from the interface's receive queue.
318
319         :param timeout: Timeout value in seconds.
320         :type timeout: int
321         :returns: Ether() initialized object from packet data.
322         :rtype: scapy.Ether
323         """
324         return self.rxq.recv(timeout, self.sent_packets)
325
326
327 def create_gratuitous_arp_request(src_mac, src_ip):
328     """Creates scapy representation of gratuitous ARP request."""
329     return (Ether(src=src_mac, dst=u"ff:ff:ff:ff:ff:ff") /
330             ARP(psrc=src_ip, hwsrc=src_mac, pdst=src_ip)
331             )
332
333
334 def auto_pad(packet):
335     """Pads zeroes at the end of the packet if the total packet length is less
336     then 60 bytes in case of IPv4 or 78 bytes in case of IPv6.
337     """
338     # TODO: add document explaining deduction of FCS part
339     min_len = 78 if packet.haslayer(IPv6) else 60
340     pad_layer = Raw if packet.haslayer(Raw) \
341         else Padding if packet.haslayer(Padding) else None
342     if pad_layer:
343         packet[pad_layer].load += (b"\0" * (min_len - len(packet)))
344     return packet
345
346
347 def checksum_equal(chksum1, chksum2):
348     """Compares two checksums in one's complement notation.
349
350     Checksums to be compared are calculated as 16 bit one's complement of the
351     one's complement sum of 16 bit words of some buffer.
352     In one's complement notation 0x0000 (positive zero) and 0xFFFF
353     (negative zero) are equivalent.
354
355     :param chksum1: First checksum.
356     :param chksum2: Second checksum.
357     :type chksum1: uint16
358     :type chksum2: uint16
359
360     :returns: True if checksums are equivalent, False otherwise.
361     :rtype: boolean
362     """
363     if chksum1 == 0xFFFF:
364         chksum1 = 0
365     if chksum2 == 0xFFFF:
366         chksum2 = 0
367     return chksum1 == chksum2