xref: /freebsd/tests/sys/netpfil/common/pft_ping.py (revision a64729f5077d77e13b9497cb33ecb3c82e606ee8)
1#!/usr/bin/env python3
2#
3# SPDX-License-Identifier: BSD-2-Clause
4#
5# Copyright (c) 2017 Kristof Provost <kp@FreeBSD.org>
6# Copyright (c) 2023 Kajetan Staszkiewicz <vegeta@tuxpowered.net>
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions
10# are met:
11# 1. Redistributions of source code must retain the above copyright
12#    notice, this list of conditions and the following disclaimer.
13# 2. Redistributions in binary form must reproduce the above copyright
14#    notice, this list of conditions and the following disclaimer in the
15#    documentation and/or other materials provided with the distribution.
16#
17# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27# SUCH DAMAGE.
28#
29
30import argparse
31import logging
32logging.getLogger("scapy").setLevel(logging.CRITICAL)
33import math
34import scapy.all as sp
35import sys
36
37from copy import copy
38from sniffer import Sniffer
39
40logging.basicConfig(format='%(message)s')
41LOGGER = logging.getLogger(__name__)
42
43PAYLOAD_MAGIC = bytes.fromhex('42c0ffee')
44
45def build_payload(l):
46    pl = len(PAYLOAD_MAGIC)
47    ret = PAYLOAD_MAGIC * math.floor(l/pl)
48    ret += PAYLOAD_MAGIC[0:(l % pl)]
49    return ret
50
51
52def clean_params(params):
53    # Prepare a copy of safe copy of params
54    ret = copy(params)
55    ret.pop('src_address')
56    ret.pop('dst_address')
57    ret.pop('flags')
58    return ret
59
60def prepare_ipv6(send_params):
61    src_address = send_params.get('src_address')
62    dst_address = send_params.get('dst_address')
63    hlim = send_params.get('hlim')
64    tc = send_params.get('tc')
65    ip6 = sp.IPv6(dst=dst_address)
66    if src_address:
67        ip6.src = src_address
68    if hlim:
69        ip6.hlim = hlim
70    if tc:
71        ip6.tc = tc
72    return ip6
73
74
75def prepare_ipv4(send_params):
76    src_address = send_params.get('src_address')
77    dst_address = send_params.get('dst_address')
78    flags = send_params.get('flags')
79    tos = send_params.get('tc')
80    ttl = send_params.get('hlim')
81    opt = send_params.get('nop')
82    options = ''
83    if opt:
84        options='\x00'
85    ip = sp.IP(dst=dst_address, options=options)
86    if src_address:
87        ip.src = src_address
88    if flags:
89        ip.flags = flags
90    if tos:
91        ip.tos = tos
92    if ttl:
93        ip.ttl = ttl
94    return ip
95
96
97def send_icmp_ping(send_params):
98    send_length = send_params['length']
99    send_frag_length = send_params['frag_length']
100    packets = []
101    ether = sp.Ether()
102    if ':' in send_params['dst_address']:
103        ip6 = prepare_ipv6(send_params)
104        icmp = sp.ICMPv6EchoRequest(data=sp.raw(build_payload(send_length)))
105        if send_frag_length:
106            for packet in sp.fragment6(ip6 / icmp, fragSize=send_frag_length):
107                packets.append(ether / packet)
108        else:
109            packets.append(ether / ip6 / icmp)
110
111    else:
112        ip = prepare_ipv4(send_params)
113        icmp = sp.ICMP(type='echo-request')
114        raw = sp.raw(build_payload(send_length))
115        if send_frag_length:
116            for packet in sp.fragment(ip / icmp / raw, fragsize=send_frag_length):
117                packets.append(ether / packet)
118        else:
119            packets.append(ether / ip / icmp / raw)
120    for packet in packets:
121        sp.sendp(packet, iface=send_params['sendif'], verbose=False)
122
123
124def send_tcp_syn(send_params):
125    tcpopt_unaligned = send_params.get('tcpopt_unaligned')
126    seq = send_params.get('seq')
127    mss = send_params.get('mss')
128    ether = sp.Ether()
129    opts=[('Timestamp', (1, 1)), ('MSS', mss if mss else 1280)]
130    if tcpopt_unaligned:
131        opts = [('NOP', 0 )] + opts
132    if ':' in send_params['dst_address']:
133        ip = prepare_ipv6(send_params)
134    else:
135        ip = prepare_ipv4(send_params)
136    tcp = sp.TCP(
137        sport=send_params.get('sport'), dport=send_params.get('dport'),
138        flags='S', options=opts, seq=seq,
139    )
140    req = ether / ip / tcp
141    sp.sendp(req, iface=send_params['sendif'], verbose=False)
142
143
144def send_udp(send_params):
145    LOGGER.debug(f'Sending UDP ping')
146    packets = []
147    send_length = send_params['length']
148    send_frag_length = send_params['frag_length']
149    ether = sp.Ether()
150    if ':' in send_params['dst_address']:
151        ip6 = prepare_ipv6(send_params)
152        udp = sp.UDP(
153            sport=send_params.get('sport'), dport=send_params.get('dport'),
154        )
155        raw = sp.Raw(load=build_payload(send_length))
156        if send_frag_length:
157            for packet in sp.fragment6(ip6 / udp / raw, fragSize=send_frag_length):
158                packets.append(ether / packet)
159        else:
160            packets.append(ether / ip6 / udp / raw)
161    else:
162        ip = prepare_ipv4(send_params)
163        udp = sp.UDP(
164            sport=send_params.get('sport'), dport=send_params.get('dport'),
165        )
166        raw = sp.Raw(load=build_payload(send_length))
167        if send_frag_length:
168            for packet in sp.fragment(ip / udp / raw, fragsize=send_frag_length):
169                packets.append(ether / packet)
170        else:
171            packets.append(ether / ip / udp / raw)
172
173    for packet in packets:
174        sp.sendp(packet, iface=send_params['sendif'], verbose=False)
175
176
177def send_ping(ping_type, send_params):
178    if ping_type == 'icmp':
179        send_icmp_ping(send_params)
180    elif (
181        ping_type == 'tcpsyn' or
182        ping_type == 'tcp3way'
183    ):
184        send_tcp_syn(send_params)
185    elif ping_type == 'udp':
186        send_udp(send_params)
187    else:
188        raise Exception('Unsupported ping type')
189
190
191def check_ipv4(expect_params, packet):
192    src_address = expect_params.get('src_address')
193    dst_address = expect_params.get('dst_address')
194    flags = expect_params.get('flags')
195    tos = expect_params.get('tc')
196    ttl = expect_params.get('hlim')
197    ip = packet.getlayer(sp.IP)
198    LOGGER.debug(f'Packet: {ip}')
199    if not ip:
200        LOGGER.debug('Packet is not IPv4!')
201        return False
202    if src_address and ip.src != src_address:
203        LOGGER.debug(f'Wrong IPv4 source {ip.src}, expected {src_address}')
204        return False
205    if dst_address and ip.dst != dst_address:
206        LOGGER.debug(f'Wrong IPv4 destination {ip.dst}, expected {dst_address}')
207        return False
208    if flags and ip.flags != flags:
209        LOGGER.debug(f'Wrong IP flags value {ip.flags}, expected {flags}')
210        return False
211    if tos and ip.tos != tos:
212        LOGGER.debug(f'Wrong ToS value {ip.tos}, expected {tos}')
213        return False
214    if ttl and ip.ttl != ttl:
215        LOGGER.debug(f'Wrong TTL value {ip.ttl}, expected {ttl}')
216        return False
217    return True
218
219
220def check_ipv6(expect_params, packet):
221    src_address = expect_params.get('src_address')
222    dst_address = expect_params.get('dst_address')
223    flags = expect_params.get('flags')
224    hlim = expect_params.get('hlim')
225    tc = expect_params.get('tc')
226    ip6 = packet.getlayer(sp.IPv6)
227    if not ip6:
228        LOGGER.debug('Packet is not IPv6!')
229        return False
230    if src_address and ip6.src != src_address:
231        LOGGER.debug(f'Wrong IPv6 source {ip6.src}, expected {src_address}')
232        return False
233    if dst_address and ip6.dst != dst_address:
234        LOGGER.debug(f'Wrong IPv6 destination {ip6.dst}, expected {dst_address}')
235        return False
236    # IPv6 has no IP-level checksum.
237    if flags:
238        raise Exception("There's no fragmentation flags in IPv6")
239    if hlim and ip6.hlim != hlim:
240        LOGGER.debug(f'Wrong Hop Limit value {ip6.hlim}, expected {hlim}')
241        return False
242    if tc and ip6.tc != tc:
243        LOGGER.debug(f'Wrong TC value {ip6.tc}, expected {tc}')
244        return False
245    return True
246
247
248def check_ping_4(expect_params, packet):
249    expect_length = expect_params['length']
250    if not check_ipv4(expect_params, packet):
251        return False
252    icmp = packet.getlayer(sp.ICMP)
253    if not icmp:
254        LOGGER.debug('Packet is not IPv4 ICMP!')
255        return False
256    raw = packet.getlayer(sp.Raw)
257    if not raw:
258        LOGGER.debug('Packet contains no payload!')
259        return False
260    if raw.load != build_payload(expect_length):
261        LOGGER.debug('Payload magic does not match!')
262        return False
263    return True
264
265
266def check_ping_request_4(expect_params, packet):
267    if not check_ping_4(expect_params, packet):
268        return False
269    icmp = packet.getlayer(sp.ICMP)
270    if sp.icmptypes[icmp.type] != 'echo-request':
271        LOGGER.debug('Packet is not IPv4 ICMP Echo Request!')
272        return False
273    return True
274
275
276def check_ping_reply_4(expect_params, packet):
277    if not check_ping_4(expect_params, packet):
278        return False
279    icmp = packet.getlayer(sp.ICMP)
280    if sp.icmptypes[icmp.type] != 'echo-reply':
281        LOGGER.debug('Packet is not IPv4 ICMP Echo Reply!')
282        return False
283    return True
284
285
286def check_ping_request_6(expect_params, packet):
287    expect_length = expect_params['length']
288    if not check_ipv6(expect_params, packet):
289        return False
290    icmp = packet.getlayer(sp.ICMPv6EchoRequest)
291    if not icmp:
292        LOGGER.debug('Packet is not IPv6 ICMP Echo Request!')
293        return False
294    if icmp.data != build_payload(expect_length):
295        LOGGER.debug('Payload magic does not match!')
296        return False
297    return True
298
299
300def check_ping_reply_6(expect_params, packet):
301    expect_length = expect_params['length']
302    if not check_ipv6(expect_params, packet):
303        return False
304    icmp = packet.getlayer(sp.ICMPv6EchoReply)
305    if not icmp:
306        LOGGER.debug('Packet is not IPv6 ICMP Echo Reply!')
307        return False
308    if icmp.data != build_payload(expect_length):
309        LOGGER.debug('Payload magic does not match!')
310        return False
311    return True
312
313
314def check_ping_request(args, packet):
315    src_address = args['expect_params'].get('src_address')
316    dst_address = args['expect_params'].get('dst_address')
317    if not (src_address or dst_address):
318        raise Exception('Source or destination address must be given to match the ping request!')
319    if (
320        (src_address and ':' in src_address) or
321        (dst_address and ':' in dst_address)
322    ):
323        return check_ping_request_6(args['expect_params'], packet)
324    else:
325        return check_ping_request_4(args['expect_params'], packet)
326
327
328def check_ping_reply(args, packet):
329    src_address = args['expect_params'].get('src_address')
330    dst_address = args['expect_params'].get('dst_address')
331    if not (src_address or dst_address):
332        raise Exception('Source or destination address must be given to match the ping reply!')
333    if (
334        (src_address and ':' in src_address) or
335        (dst_address and ':' in dst_address)
336    ):
337        return check_ping_reply_6(args['expect_params'], packet)
338    else:
339        return check_ping_reply_4(args['expect_params'], packet)
340
341
342def check_tcp(expect_params, packet):
343    tcp_flags = expect_params.get('tcp_flags')
344    mss = expect_params.get('mss')
345    seq = expect_params.get('seq')
346    tcp = packet.getlayer(sp.TCP)
347    if not tcp:
348        LOGGER.debug('Packet is not TCP!')
349        return False
350    chksum = tcp.chksum
351    tcp.chksum = None
352    newpacket = sp.Ether(sp.raw(packet[sp.Ether]))
353    new_chksum = newpacket[sp.TCP].chksum
354    if new_chksum and chksum != new_chksum:
355        LOGGER.debug(f'Wrong TCP checksum {chksum}, expected {new_chksum}!')
356        return False
357    if tcp_flags and tcp.flags != tcp_flags:
358        LOGGER.debug(f'Wrong TCP flags {tcp.flags}, expected {tcp_flags}!')
359        return False
360    if seq:
361        if tcp_flags == 'S':
362            tcp_seq = tcp.seq
363        elif tcp_flags == 'SA':
364            tcp_seq = tcp.ack - 1
365        if seq != tcp_seq:
366            LOGGER.debug(f'Wrong TCP Sequence Number {tcp_seq}, expected {seq}')
367            return False
368    if mss:
369        for option in tcp.options:
370            if option[0] == 'MSS':
371                if option[1] != mss:
372                    LOGGER.debug(f'Wrong TCP MSS {option[1]}, expected {mss}')
373                    return False
374    return True
375
376
377def check_udp(expect_params, packet):
378    expect_length = expect_params['length']
379    udp = packet.getlayer(sp.UDP)
380    if not udp:
381        LOGGER.debug('Packet is not UDP!')
382        return False
383    raw = packet.getlayer(sp.Raw)
384    if not raw:
385        LOGGER.debug('Packet contains no payload!')
386        return False
387    if raw.load != build_payload(expect_length):
388        LOGGER.debug(f'Payload magic does not match len {len(raw.load)} vs {expect_length}!')
389        return False
390    orig_chksum = udp.chksum
391    udp.chksum = None
392    newpacket = sp.Ether(sp.raw(packet[sp.Ether]))
393    new_chksum = newpacket[sp.UDP].chksum
394    if new_chksum and orig_chksum != new_chksum:
395        LOGGER.debug(f'Wrong UDP checksum {orig_chksum}, expected {new_chksum}!')
396        return False
397
398    return True
399
400
401def check_tcp_syn_request_4(expect_params, packet):
402    if not check_ipv4(expect_params, packet):
403        return False
404    if not check_tcp(expect_params | {'tcp_flags': 'S'}, packet):
405        return False
406    return True
407
408
409def check_tcp_syn_reply_4(send_params, expect_params, packet):
410    if not check_ipv4(expect_params, packet):
411        return False
412    if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):
413        return False
414    return True
415
416
417def check_tcp_3way_4(args, packet):
418    send_params = args['send_params']
419
420    expect_params_sa = clean_params(args['expect_params'])
421    expect_params_sa['src_address'] = send_params['dst_address']
422    expect_params_sa['dst_address'] = send_params['src_address']
423
424    # Sniff incoming SYN+ACK packet
425    if (
426        check_ipv4(expect_params_sa, packet) and
427        check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)
428    ):
429        ether = sp.Ether()
430        ip_sa = packet.getlayer(sp.IP)
431        tcp_sa = packet.getlayer(sp.TCP)
432        reply_params = clean_params(send_params)
433        reply_params['src_address'] = ip_sa.dst
434        reply_params['dst_address'] = ip_sa.src
435        ip_a = prepare_ipv4(reply_params)
436        tcp_a = sp.TCP(
437            sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',
438            seq=tcp_sa.ack, ack=tcp_sa.seq + 1,
439        )
440        req = ether / ip_a / tcp_a
441        sp.sendp(req, iface=send_params['sendif'], verbose=False)
442        return True
443
444    return False
445
446
447def check_udp_request_4(expect_params, packet):
448    if not check_ipv4(expect_params, packet):
449        return False
450    if not check_udp(expect_params, packet):
451        return False
452    return True
453
454
455def check_tcp_syn_request_6(expect_params, packet):
456    if not check_ipv6(expect_params, packet):
457        return False
458    if not check_tcp(expect_params | {'tcp_flags': 'S'}, packet):
459        return False
460    return True
461
462
463def check_tcp_syn_reply_6(expect_params, packet):
464    if not check_ipv6(expect_params, packet):
465        return False
466    if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):
467        return False
468    return True
469
470
471def check_tcp_3way_6(args, packet):
472    send_params = args['send_params']
473
474    expect_params_sa = clean_params(args['expect_params'])
475    expect_params_sa['src_address'] = send_params['dst_address']
476    expect_params_sa['dst_address'] = send_params['src_address']
477
478    # Sniff incoming SYN+ACK packet
479    if (
480        check_ipv6(expect_params_sa, packet) and
481        check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)
482    ):
483        ether = sp.Ether()
484        ip6_sa = packet.getlayer(sp.IPv6)
485        tcp_sa = packet.getlayer(sp.TCP)
486        reply_params = clean_params(send_params)
487        reply_params['src_address'] = ip6_sa.dst
488        reply_params['dst_address'] = ip6_sa.src
489        ip_a = prepare_ipv6(reply_params)
490        tcp_a = sp.TCP(
491            sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',
492            seq=tcp_sa.ack, ack=tcp_sa.seq + 1,
493        )
494        req = ether / ip_a / tcp_a
495        sp.sendp(req, iface=send_params['sendif'], verbose=False)
496        return True
497
498    return False
499
500
501def check_udp_request_6(expect_params, packet):
502    if not check_ipv6(expect_params, packet):
503        return False
504    if not check_udp(expect_params, packet):
505        return False
506    return True
507
508def check_tcp_syn_request(args, packet):
509    expect_params = args['expect_params']
510    src_address = expect_params.get('src_address')
511    dst_address = expect_params.get('dst_address')
512    if not (src_address or dst_address):
513        raise Exception('Source or destination address must be given to match the tcp syn request!')
514    if (
515        (src_address and ':' in src_address) or
516        (dst_address and ':' in dst_address)
517    ):
518        return check_tcp_syn_request_6(expect_params, packet)
519    else:
520        return check_tcp_syn_request_4(expect_params, packet)
521
522
523def check_tcp_syn_reply(args, packet):
524    expect_params = args['expect_params']
525    src_address = expect_params.get('src_address')
526    dst_address = expect_params.get('dst_address')
527    if not (src_address or dst_address):
528        raise Exception('Source or destination address must be given to match the tcp syn reply!')
529    if (
530        (src_address and ':' in src_address) or
531        (dst_address and ':' in dst_address)
532    ):
533        return check_tcp_syn_reply_6(expect_params, packet)
534    else:
535        return check_tcp_syn_reply_4(expect_params, packet)
536
537def check_tcp_3way(args, packet):
538    expect_params = args['expect_params']
539    src_address = expect_params.get('src_address')
540    dst_address = expect_params.get('dst_address')
541    if not (src_address or dst_address):
542        raise Exception('Source or destination address must be given to match the tcp syn reply!')
543    if (
544            (src_address and ':' in src_address) or
545            (dst_address and ':' in dst_address)
546    ):
547        return check_tcp_3way_6(args, packet)
548    else:
549        return check_tcp_3way_4(args, packet)
550
551
552def check_udp_request(args, packet):
553    expect_params = args['expect_params']
554    src_address = expect_params.get('src_address')
555    dst_address = expect_params.get('dst_address')
556    if not (src_address or dst_address):
557        raise Exception('Source or destination address must be given to match the tcp syn request!')
558    if (
559            (src_address and ':' in src_address) or
560            (dst_address and ':' in dst_address)
561    ):
562        return check_udp_request_6(expect_params, packet)
563    else:
564        return check_udp_request_4(expect_params, packet)
565
566
567def setup_sniffer(
568        recvif, ping_type, sniff_type, expect_params, defrag, send_params,
569):
570    if ping_type == 'icmp' and sniff_type == 'request':
571        checkfn = check_ping_request
572    elif ping_type == 'icmp' and sniff_type == 'reply':
573        checkfn = check_ping_reply
574    elif ping_type == 'tcpsyn' and sniff_type == 'request':
575        checkfn = check_tcp_syn_request
576    elif ping_type == 'tcpsyn' and sniff_type == 'reply':
577        checkfn = check_tcp_syn_reply
578    elif ping_type == 'tcp3way' and sniff_type == 'reply':
579        checkfn = check_tcp_3way
580    elif ping_type == 'udp' and sniff_type == 'request':
581        checkfn = check_udp_request
582    else:
583        raise Exception('Unspported ping and sniff type combination')
584
585    return Sniffer(
586        {'send_params': send_params, 'expect_params': expect_params},
587        checkfn, recvif, defrag=defrag,
588    )
589
590
591def parse_args():
592    parser = argparse.ArgumentParser("pft_ping.py",
593        description="Ping test tool")
594
595    # Parameters of sent ping request
596    parser.add_argument('--sendif', required=True,
597        help='The interface through which the packet(s) will be sent')
598    parser.add_argument('--to', required=True,
599        help='The destination IP address for the ping request')
600    parser.add_argument('--ping-type',
601        choices=('icmp', 'tcpsyn', 'tcp3way', 'udp'),
602        help='Type of ping: ICMP (default) or TCP SYN or 3-way TCP handshake',
603        default='icmp')
604    parser.add_argument('--fromaddr',
605        help='The source IP address for the ping request')
606
607    # Where to look for packets to analyze.
608    # The '+' format is ugly as it mixes positional with optional syntax.
609    # But we have no positional parameters so I guess it's fine to use it.
610    parser.add_argument('--recvif', nargs='+',
611        help='The interfaces on which to expect the ping request')
612    parser.add_argument('--replyif', nargs='+',
613        help='The interfaces which to expect the ping response')
614
615    # Packet settings
616    parser_send = parser.add_argument_group('Values set in transmitted packets')
617    parser_send.add_argument('--send-flags', type=str,
618        help='IPv4 fragmentation flags')
619    parser_send.add_argument('--send-frag-length', type=int,
620        help='Force IP fragmentation with given fragment length')
621    parser_send.add_argument('--send-hlim', type=int,
622        help='IPv6 Hop Limit or IPv4 Time To Live')
623    parser_send.add_argument('--send-mss', type=int,
624        help='TCP Maximum Segment Size')
625    parser_send.add_argument('--send-seq', type=int,
626        help='TCP sequence number')
627    parser_send.add_argument('--send-sport', type=int,
628        help='TCP source port')
629    parser_send.add_argument('--send-dport', type=int, default=9,
630        help='TCP destination port')
631    parser_send.add_argument('--send-length', type=int, default=len(PAYLOAD_MAGIC),
632        help='ICMP Echo Request payload size')
633    parser_send.add_argument('--send-tc', type=int,
634        help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
635    parser_send.add_argument('--send-tcpopt-unaligned', action='store_true',
636        help='Include unaligned TCP options')
637    parser_send.add_argument('--send-nop', action='store_true',
638        help='Include a NOP IPv4 option')
639
640    # Expectations
641    parser_expect = parser.add_argument_group('Values expected in sniffed packets')
642    parser_expect.add_argument('--expect-flags', type=str,
643        help='IPv4 fragmentation flags')
644    parser_expect.add_argument('--expect-hlim', type=int,
645        help='IPv6 Hop Limit or IPv4 Time To Live')
646    parser_expect.add_argument('--expect-mss', type=int,
647        help='TCP Maximum Segment Size')
648    parser_send.add_argument('--expect-seq', type=int,
649        help='TCP sequence number')
650    parser_expect.add_argument('--expect-tc', type=int,
651        help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
652
653    parser.add_argument('-v', '--verbose', action='store_true',
654        help=('Enable verbose logging. Apart of potentially useful information '
655            'you might see warnings from parsing packets like NDP or other '
656            'packets not related to the test being run. Use only when '
657            'developing because real tests expect empty stderr and stdout.'))
658
659    return parser.parse_args()
660
661
662def main():
663    args = parse_args()
664
665    if args.verbose:
666        LOGGER.setLevel(logging.DEBUG)
667
668    # Split parameters into send and expect parameters. Parameters might be
669    # missing from the command line, always fill the dictionaries with None.
670    send_params = {}
671    expect_params = {}
672    for param_name in (
673        'flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length',
674        'sport', 'dport',
675    ):
676        param_arg = vars(args).get(f'send_{param_name}')
677        send_params[param_name] = param_arg if param_arg else None
678        param_arg = vars(args).get(f'expect_{param_name}')
679        expect_params[param_name] = param_arg if param_arg else None
680
681    expect_params['length'] = send_params['length']
682    send_params['tcpopt_unaligned'] = args.send_tcpopt_unaligned
683    send_params['nop'] = args.send_nop
684    send_params['src_address'] = args.fromaddr if args.fromaddr else None
685    send_params['dst_address'] = args.to
686    send_params['sendif'] = args.sendif
687
688    # We may not have a default route. Tell scapy where to start looking for routes
689    sp.conf.iface6 = args.sendif
690
691    # Configuration sanity checking.
692    if not (args.replyif or args.recvif):
693        raise Exception('With no reply or recv interface specified no traffic '
694            'can be sniffed and verified!'
695        )
696
697    sniffers = []
698
699    if send_params['frag_length']:
700        if (
701            (send_params['src_address'] and ':' in send_params['src_address']) or
702            (send_params['dst_address'] and ':' in send_params['dst_address'])
703        ):
704            defrag = 'IPv6'
705        else:
706            defrag = 'IPv4'
707    else:
708        defrag = False
709
710    if args.recvif:
711        sniffer_params = copy(expect_params)
712        sniffer_params['src_address'] = None
713        sniffer_params['dst_address'] = args.to
714        for iface in args.recvif:
715            LOGGER.debug(f'Installing receive sniffer on {iface}')
716            sniffers.append(
717                setup_sniffer(iface, args.ping_type, 'request',
718                              sniffer_params, defrag, send_params,
719            ))
720
721    if args.replyif:
722        sniffer_params = copy(expect_params)
723        sniffer_params['src_address'] = args.to
724        sniffer_params['dst_address'] = None
725        for iface in args.replyif:
726            LOGGER.debug(f'Installing reply sniffer on {iface}')
727            sniffers.append(
728                setup_sniffer(iface, args.ping_type, 'reply',
729                              sniffer_params, defrag, send_params,
730            ))
731
732    LOGGER.debug(f'Installed {len(sniffers)} sniffers')
733
734    send_ping(args.ping_type, send_params)
735
736    err = 0
737    sniffer_num = 0
738    for sniffer in sniffers:
739        sniffer.join()
740        if sniffer.correctPackets == 1:
741            LOGGER.debug(f'Expected ping has been sniffed on {sniffer._recvif}.')
742        else:
743            # Set a bit in err for each failed sniffer.
744            err |= 1<<sniffer_num
745            if sniffer.correctPackets > 1:
746                LOGGER.debug(f'Duplicated ping has been sniffed on {sniffer._recvif}!')
747            else:
748                LOGGER.debug(f'Expected ping has not been sniffed on {sniffer._recvif}!')
749        sniffer_num += 1
750
751    return err
752
753
754if __name__ == '__main__':
755    sys.exit(main())
756