xref: /freebsd/tests/sys/netpfil/common/pft_ping.py (revision 215fd38e2915d142cb4b0245f137329ef4da5a12)
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.fragment(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_ping(ping_type, send_params):
145    if ping_type == 'icmp':
146        send_icmp_ping(send_params)
147    elif (
148        ping_type == 'tcpsyn' or
149        ping_type == 'tcp3way'
150    ):
151        send_tcp_syn(send_params)
152    else:
153        raise Exception('Unspported ping type')
154
155
156def check_ipv4(expect_params, packet):
157    src_address = expect_params.get('src_address')
158    dst_address = expect_params.get('dst_address')
159    flags = expect_params.get('flags')
160    tos = expect_params.get('tc')
161    ttl = expect_params.get('hlim')
162    ip = packet.getlayer(sp.IP)
163    LOGGER.debug(f'Packet: {ip}')
164    if not ip:
165        LOGGER.debug('Packet is not IPv4!')
166        return False
167    if src_address and ip.src != src_address:
168        LOGGER.debug(f'Wrong IPv4 source {ip.src}, expected {src_address}')
169        return False
170    if dst_address and ip.dst != dst_address:
171        LOGGER.debug(f'Wrong IPv4 destination {ip.dst}, expected {dst_address}')
172        return False
173    chksum = ip.chksum
174    ip.chksum = None
175    new_chksum = sp.IP(sp.raw(ip)).chksum
176    if chksum != new_chksum:
177        LOGGER.debug(f'Wrong IPv4 checksum {chksum}, expected {new_chksum}')
178        return False
179    if flags and ip.flags != flags:
180        LOGGER.debug(f'Wrong IP flags value {ip.flags}, expected {flags}')
181        return False
182    if tos and ip.tos != tos:
183        LOGGER.debug(f'Wrong ToS value {ip.tos}, expected {tos}')
184        return False
185    if ttl and ip.ttl != ttl:
186        LOGGER.debug(f'Wrong TTL value {ip.ttl}, expected {ttl}')
187        return False
188    return True
189
190
191def check_ipv6(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    hlim = expect_params.get('hlim')
196    tc = expect_params.get('tc')
197    ip6 = packet.getlayer(sp.IPv6)
198    if not ip6:
199        LOGGER.debug('Packet is not IPv6!')
200        return False
201    if src_address and ip6.src != src_address:
202        LOGGER.debug(f'Wrong IPv6 source {ip6.src}, expected {src_address}')
203        return False
204    if dst_address and ip6.dst != dst_address:
205        LOGGER.debug(f'Wrong IPv6 destination {ip6.dst}, expected {dst_address}')
206        return False
207    # IPv6 has no IP-level checksum.
208    if flags:
209        raise Exception("There's no fragmentation flags in IPv6")
210    if hlim and ip6.hlim != hlim:
211        LOGGER.debug(f'Wrong Hop Limit value {ip6.hlim}, expected {hlim}')
212        return False
213    if tc and ip6.tc != tc:
214        LOGGER.debug(f'Wrong TC value {ip6.tc}, expected {tc}')
215        return False
216    return True
217
218
219def check_ping_4(expect_params, packet):
220    expect_length = expect_params['length']
221    if not check_ipv4(expect_params, packet):
222        return False
223    icmp = packet.getlayer(sp.ICMP)
224    if not icmp:
225        LOGGER.debug('Packet is not IPv4 ICMP!')
226        return False
227    raw = packet.getlayer(sp.Raw)
228    if not raw:
229        LOGGER.debug('Packet contains no payload!')
230        return False
231    if raw.load != build_payload(expect_length):
232        LOGGER.debug('Payload magic does not match!')
233        return False
234    return True
235
236
237def check_ping_request_4(expect_params, packet):
238    if not check_ping_4(expect_params, packet):
239        return False
240    icmp = packet.getlayer(sp.ICMP)
241    if sp.icmptypes[icmp.type] != 'echo-request':
242        LOGGER.debug('Packet is not IPv4 ICMP Echo Request!')
243        return False
244    return True
245
246
247def check_ping_reply_4(expect_params, packet):
248    if not check_ping_4(expect_params, packet):
249        return False
250    icmp = packet.getlayer(sp.ICMP)
251    if sp.icmptypes[icmp.type] != 'echo-reply':
252        LOGGER.debug('Packet is not IPv4 ICMP Echo Reply!')
253        return False
254    return True
255
256
257def check_ping_request_6(expect_params, packet):
258    expect_length = expect_params['length']
259    if not check_ipv6(expect_params, packet):
260        return False
261    icmp = packet.getlayer(sp.ICMPv6EchoRequest)
262    if not icmp:
263        LOGGER.debug('Packet is not IPv6 ICMP Echo Request!')
264        return False
265    if icmp.data != build_payload(expect_length):
266        LOGGER.debug('Payload magic does not match!')
267        return False
268    return True
269
270
271def check_ping_reply_6(expect_params, packet):
272    expect_length = expect_params['length']
273    if not check_ipv6(expect_params, packet):
274        return False
275    icmp = packet.getlayer(sp.ICMPv6EchoReply)
276    if not icmp:
277        LOGGER.debug('Packet is not IPv6 ICMP Echo Reply!')
278        return False
279    if icmp.data != build_payload(expect_length):
280        LOGGER.debug('Payload magic does not match!')
281        return False
282    return True
283
284
285def check_ping_request(args, packet):
286    src_address = args['expect_params'].get('src_address')
287    dst_address = args['expect_params'].get('dst_address')
288    if not (src_address or dst_address):
289        raise Exception('Source or destination address must be given to match the ping request!')
290    if (
291        (src_address and ':' in src_address) or
292        (dst_address and ':' in dst_address)
293    ):
294        return check_ping_request_6(args['expect_params'], packet)
295    else:
296        return check_ping_request_4(args['expect_params'], packet)
297
298
299def check_ping_reply(args, packet):
300    src_address = args['expect_params'].get('src_address')
301    dst_address = args['expect_params'].get('dst_address')
302    if not (src_address or dst_address):
303        raise Exception('Source or destination address must be given to match the ping reply!')
304    if (
305        (src_address and ':' in src_address) or
306        (dst_address and ':' in dst_address)
307    ):
308        return check_ping_reply_6(args['expect_params'], packet)
309    else:
310        return check_ping_reply_4(args['expect_params'], packet)
311
312
313def check_tcp(expect_params, packet):
314    tcp_flags = expect_params.get('tcp_flags')
315    mss = expect_params.get('mss')
316    seq = expect_params.get('seq')
317    tcp = packet.getlayer(sp.TCP)
318    if not tcp:
319        LOGGER.debug('Packet is not TCP!')
320        return False
321    chksum = tcp.chksum
322    tcp.chksum = None
323    newpacket = sp.Ether(sp.raw(packet[sp.Ether]))
324    new_chksum = newpacket[sp.TCP].chksum
325    if new_chksum and chksum != new_chksum:
326        LOGGER.debug(f'Wrong TCP checksum {chksum}, expected {new_chksum}!')
327        return False
328    if tcp_flags and tcp.flags != tcp_flags:
329        LOGGER.debug(f'Wrong TCP flags {tcp.flags}, expected {tcp_flags}!')
330        return False
331    if seq:
332        if tcp_flags == 'S':
333            tcp_seq = tcp.seq
334        elif tcp_flags == 'SA':
335            tcp_seq = tcp.ack - 1
336        if seq != tcp_seq:
337            LOGGER.debug(f'Wrong TCP Sequence Number {tcp_seq}, expected {seq}')
338            return False
339    if mss:
340        for option in tcp.options:
341            if option[0] == 'MSS':
342                if option[1] != mss:
343                    LOGGER.debug(f'Wrong TCP MSS {option[1]}, expected {mss}')
344                    return False
345    return True
346
347
348def check_tcp_syn_request_4(expect_params, packet):
349    if not check_ipv4(expect_params, packet):
350        return False
351    if not check_tcp(expect_params | {'tcp_flags': 'S'}, packet):
352        return False
353    return True
354
355
356def check_tcp_syn_reply_4(send_params, expect_params, packet):
357    if not check_ipv4(expect_params, packet):
358        return False
359    if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):
360        return False
361    return True
362
363
364def check_tcp_3way_4(args, packet):
365    send_params = args['send_params']
366
367    expect_params_sa = clean_params(args['expect_params'])
368    expect_params_sa['src_address'] = send_params['dst_address']
369    expect_params_sa['dst_address'] = send_params['src_address']
370
371    # Sniff incoming SYN+ACK packet
372    if (
373        check_ipv4(expect_params_sa, packet) and
374        check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)
375    ):
376        ether = sp.Ether()
377        ip_sa = packet.getlayer(sp.IP)
378        tcp_sa = packet.getlayer(sp.TCP)
379        reply_params = clean_params(send_params)
380        reply_params['src_address'] = ip_sa.dst
381        reply_params['dst_address'] = ip_sa.src
382        ip_a = prepare_ipv4(reply_params)
383        tcp_a = sp.TCP(
384            sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',
385            seq=tcp_sa.ack, ack=tcp_sa.seq + 1,
386        )
387        req = ether / ip_a / tcp_a
388        sp.sendp(req, iface=send_params['sendif'], verbose=False)
389        return True
390
391    return False
392
393
394def check_tcp_syn_request_6(expect_params, packet):
395    if not check_ipv6(expect_params, packet):
396        return False
397    if not check_tcp(expect_params | {'tcp_flags': 'S'}, packet):
398        return False
399    return True
400
401
402def check_tcp_syn_reply_6(expect_params, packet):
403    if not check_ipv6(expect_params, packet):
404        return False
405    if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):
406        return False
407    return True
408
409
410def check_tcp_3way_6(args, packet):
411    send_params = args['send_params']
412
413    expect_params_sa = clean_params(args['expect_params'])
414    expect_params_sa['src_address'] = send_params['dst_address']
415    expect_params_sa['dst_address'] = send_params['src_address']
416
417    # Sniff incoming SYN+ACK packet
418    if (
419        check_ipv6(expect_params_sa, packet) and
420        check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)
421    ):
422        ether = sp.Ether()
423        ip6_sa = packet.getlayer(sp.IPv6)
424        tcp_sa = packet.getlayer(sp.TCP)
425        reply_params = clean_params(send_params)
426        reply_params['src_address'] = ip6_sa.dst
427        reply_params['dst_address'] = ip6_sa.src
428        ip_a = prepare_ipv6(reply_params)
429        tcp_a = sp.TCP(
430            sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',
431            seq=tcp_sa.ack, ack=tcp_sa.seq + 1,
432        )
433        req = ether / ip_a / tcp_a
434        sp.sendp(req, iface=send_params['sendif'], verbose=False)
435        return True
436
437    return False
438
439
440def check_tcp_syn_request(args, packet):
441    expect_params = args['expect_params']
442    src_address = expect_params.get('src_address')
443    dst_address = expect_params.get('dst_address')
444    if not (src_address or dst_address):
445        raise Exception('Source or destination address must be given to match the tcp syn request!')
446    if (
447        (src_address and ':' in src_address) or
448        (dst_address and ':' in dst_address)
449    ):
450        return check_tcp_syn_request_6(expect_params, packet)
451    else:
452        return check_tcp_syn_request_4(expect_params, packet)
453
454
455def check_tcp_syn_reply(args, packet):
456    expect_params = args['expect_params']
457    src_address = expect_params.get('src_address')
458    dst_address = expect_params.get('dst_address')
459    if not (src_address or dst_address):
460        raise Exception('Source or destination address must be given to match the tcp syn reply!')
461    if (
462        (src_address and ':' in src_address) or
463        (dst_address and ':' in dst_address)
464    ):
465        return check_tcp_syn_reply_6(expect_params, packet)
466    else:
467        return check_tcp_syn_reply_4(expect_params, packet)
468
469def check_tcp_3way(args, packet):
470    expect_params = args['expect_params']
471    src_address = expect_params.get('src_address')
472    dst_address = expect_params.get('dst_address')
473    if not (src_address or dst_address):
474        raise Exception('Source or destination address must be given to match the tcp syn reply!')
475    if (
476            (src_address and ':' in src_address) or
477            (dst_address and ':' in dst_address)
478    ):
479        return check_tcp_3way_6(args, packet)
480    else:
481        return check_tcp_3way_4(args, packet)
482
483
484def setup_sniffer(
485        recvif, ping_type, sniff_type, expect_params, defrag, send_params,
486):
487    if ping_type == 'icmp' and sniff_type == 'request':
488        checkfn = check_ping_request
489    elif ping_type == 'icmp' and sniff_type == 'reply':
490        checkfn = check_ping_reply
491    elif ping_type == 'tcpsyn' and sniff_type == 'request':
492        checkfn = check_tcp_syn_request
493    elif ping_type == 'tcpsyn' and sniff_type == 'reply':
494        checkfn = check_tcp_syn_reply
495    elif ping_type == 'tcp3way' and sniff_type == 'reply':
496        checkfn = check_tcp_3way
497    else:
498        raise Exception('Unspported ping or sniff type')
499
500    return Sniffer(
501        {'send_params': send_params, 'expect_params': expect_params},
502        checkfn, recvif, defrag=defrag,
503    )
504
505
506def parse_args():
507    parser = argparse.ArgumentParser("pft_ping.py",
508        description="Ping test tool")
509
510    # Parameters of sent ping request
511    parser.add_argument('--sendif', required=True,
512        help='The interface through which the packet(s) will be sent')
513    parser.add_argument('--to', required=True,
514        help='The destination IP address for the ping request')
515    parser.add_argument('--ping-type',
516        choices=('icmp', 'tcpsyn', 'tcp3way'),
517        help='Type of ping: ICMP (default) or TCP SYN or 3-way TCP handshake',
518        default='icmp')
519    parser.add_argument('--fromaddr',
520        help='The source IP address for the ping request')
521
522    # Where to look for packets to analyze.
523    # The '+' format is ugly as it mixes positional with optional syntax.
524    # But we have no positional parameters so I guess it's fine to use it.
525    parser.add_argument('--recvif', nargs='+',
526        help='The interfaces on which to expect the ping request')
527    parser.add_argument('--replyif', nargs='+',
528        help='The interfaces which to expect the ping response')
529
530    # Packet settings
531    parser_send = parser.add_argument_group('Values set in transmitted packets')
532    parser_send.add_argument('--send-flags', type=str,
533        help='IPv4 fragmentation flags')
534    parser_send.add_argument('--send-frag-length', type=int,
535        help='Force IP fragmentation with given fragment length')
536    parser_send.add_argument('--send-hlim', type=int,
537        help='IPv6 Hop Limit or IPv4 Time To Live')
538    parser_send.add_argument('--send-mss', type=int,
539        help='TCP Maximum Segment Size')
540    parser_send.add_argument('--send-seq', type=int,
541        help='TCP sequence number')
542    parser_send.add_argument('--send-sport', type=int,
543        help='TCP source port')
544    parser_send.add_argument('--send-dport', type=int, default=9,
545        help='TCP destination port')
546    parser_send.add_argument('--send-length', type=int, default=len(PAYLOAD_MAGIC),
547        help='ICMP Echo Request payload size')
548    parser_send.add_argument('--send-tc', type=int,
549        help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
550    parser_send.add_argument('--send-tcpopt-unaligned', action='store_true',
551        help='Include unaligned TCP options')
552    parser_send.add_argument('--send-nop', action='store_true',
553        help='Include a NOP IPv4 option')
554
555    # Expectations
556    parser_expect = parser.add_argument_group('Values expected in sniffed packets')
557    parser_expect.add_argument('--expect-flags', type=str,
558        help='IPv4 fragmentation flags')
559    parser_expect.add_argument('--expect-hlim', type=int,
560        help='IPv6 Hop Limit or IPv4 Time To Live')
561    parser_expect.add_argument('--expect-mss', type=int,
562        help='TCP Maximum Segment Size')
563    parser_send.add_argument('--expect-seq', type=int,
564        help='TCP sequence number')
565    parser_expect.add_argument('--expect-tc', type=int,
566        help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
567
568    parser.add_argument('-v', '--verbose', action='store_true',
569        help=('Enable verbose logging. Apart of potentially useful information '
570            'you might see warnings from parsing packets like NDP or other '
571            'packets not related to the test being run. Use only when '
572            'developing because real tests expect empty stderr and stdout.'))
573
574    return parser.parse_args()
575
576
577def main():
578    args = parse_args()
579
580    if args.verbose:
581        LOGGER.setLevel(logging.DEBUG)
582
583    # Split parameters into send and expect parameters. Parameters might be
584    # missing from the command line, always fill the dictionaries with None.
585    send_params = {}
586    expect_params = {}
587    for param_name in (
588        'flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length',
589        'sport', 'dport',
590    ):
591        param_arg = vars(args).get(f'send_{param_name}')
592        send_params[param_name] = param_arg if param_arg else None
593        param_arg = vars(args).get(f'expect_{param_name}')
594        expect_params[param_name] = param_arg if param_arg else None
595
596    expect_params['length'] = send_params['length']
597    send_params['tcpopt_unaligned'] = args.send_tcpopt_unaligned
598    send_params['nop'] = args.send_nop
599    send_params['src_address'] = args.fromaddr if args.fromaddr else None
600    send_params['dst_address'] = args.to
601    send_params['sendif'] = args.sendif
602
603    # We may not have a default route. Tell scapy where to start looking for routes
604    sp.conf.iface6 = args.sendif
605
606    # Configuration sanity checking.
607    if not (args.replyif or args.recvif):
608        raise Exception('With no reply or recv interface specified no traffic '
609            'can be sniffed and verified!'
610        )
611
612    sniffers = []
613
614    if send_params['frag_length']:
615        defrag = True
616    else:
617        defrag = False
618
619    if args.recvif:
620        sniffer_params = copy(expect_params)
621        sniffer_params['src_address'] = None
622        sniffer_params['dst_address'] = args.to
623        for iface in args.recvif:
624            LOGGER.debug(f'Installing receive sniffer on {iface}')
625            sniffers.append(
626                setup_sniffer(iface, args.ping_type, 'request',
627                              sniffer_params, defrag, send_params,
628            ))
629
630    if args.replyif:
631        sniffer_params = copy(expect_params)
632        sniffer_params['src_address'] = args.to
633        sniffer_params['dst_address'] = None
634        for iface in args.replyif:
635            LOGGER.debug(f'Installing reply sniffer on {iface}')
636            sniffers.append(
637                setup_sniffer(iface, args.ping_type, 'reply',
638                              sniffer_params, defrag, send_params,
639            ))
640
641    LOGGER.debug(f'Installed {len(sniffers)} sniffers')
642
643    send_ping(args.ping_type, send_params)
644
645    err = 0
646    sniffer_num = 0
647    for sniffer in sniffers:
648        sniffer.join()
649        if sniffer.correctPackets == 1:
650            LOGGER.debug(f'Expected ping has been sniffed on {sniffer._recvif}.')
651        else:
652            # Set a bit in err for each failed sniffer.
653            err |= 1<<sniffer_num
654            if sniffer.correctPackets > 1:
655                LOGGER.debug(f'Duplicated ping has been sniffed on {sniffer._recvif}!')
656            else:
657                LOGGER.debug(f'Expected ping has not been sniffed on {sniffer._recvif}!')
658        sniffer_num += 1
659
660    return err
661
662
663if __name__ == '__main__':
664    sys.exit(main())
665