Path: blob/main/tests/sys/netpfil/common/pft_ping.py
39535 views
#!/usr/bin/env python31#2# SPDX-License-Identifier: BSD-2-Clause3#4# Copyright (c) 2017 Kristof Provost <[email protected]>5# Copyright (c) 2023 Kajetan Staszkiewicz <[email protected]>6#7# Redistribution and use in source and binary forms, with or without8# modification, are permitted provided that the following conditions9# are met:10# 1. Redistributions of source code must retain the above copyright11# notice, this list of conditions and the following disclaimer.12# 2. Redistributions in binary form must reproduce the above copyright13# notice, this list of conditions and the following disclaimer in the14# documentation and/or other materials provided with the distribution.15#16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND17# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE19# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE20# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT24# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY25# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF26# SUCH DAMAGE.27#2829import argparse30import logging31logging.getLogger("scapy").setLevel(logging.CRITICAL)32import math33import scapy.all as sp34import sys35import socket3637from copy import copy38from sniffer import Sniffer3940logging.basicConfig(format='%(message)s')41LOGGER = logging.getLogger(__name__)4243PAYLOAD_MAGIC = bytes.fromhex('42c0ffee')4445def build_payload(l):46pl = len(PAYLOAD_MAGIC)47ret = PAYLOAD_MAGIC * math.floor(l/pl)48ret += PAYLOAD_MAGIC[0:(l % pl)]49return ret505152def clean_params(params):53# Prepare a copy of safe copy of params54ret = copy(params)55ret.pop('src_address')56ret.pop('dst_address')57ret.pop('flags')58return ret5960def prepare_ipv6(send_params):61src_address = send_params.get('src_address')62dst_address = send_params.get('dst_address')63hlim = send_params.get('hlim')64tc = send_params.get('tc')65ip6 = sp.IPv6(dst=dst_address)66if src_address:67ip6.src = src_address68if hlim:69ip6.hlim = hlim70if tc:71ip6.tc = tc72return ip6737475def prepare_ipv4(send_params):76src_address = send_params.get('src_address')77dst_address = send_params.get('dst_address')78flags = send_params.get('flags')79tos = send_params.get('tc')80ttl = send_params.get('hlim')81opt = send_params.get('nop')82options = ''83if opt:84options='\x00'85ip = sp.IP(dst=dst_address, options=options)86if src_address:87ip.src = src_address88if flags:89ip.flags = flags90if tos:91ip.tos = tos92if ttl:93ip.ttl = ttl94return ip959697def send_icmp_ping(send_params):98send_length = send_params['length']99send_frag_length = send_params['frag_length']100packets = []101ether = sp.Ether()102if ':' in send_params['dst_address']:103ip6 = prepare_ipv6(send_params)104icmp = sp.ICMPv6EchoRequest(data=sp.raw(build_payload(send_length)))105if send_frag_length:106for packet in sp.fragment6(ip6 / icmp, fragSize=send_frag_length):107packets.append(ether / packet)108else:109packets.append(ether / ip6 / icmp)110111else:112ip = prepare_ipv4(send_params)113icmp = sp.ICMP(type='echo-request')114raw = sp.raw(build_payload(send_length))115if send_frag_length:116for packet in sp.fragment(ip / icmp / raw, fragsize=send_frag_length):117packets.append(ether / packet)118else:119packets.append(ether / ip / icmp / raw)120for packet in packets:121sp.sendp(packet, iface=send_params['sendif'], verbose=False)122123124def send_tcp_syn(send_params):125tcpopt_unaligned = send_params.get('tcpopt_unaligned')126seq = send_params.get('seq')127mss = send_params.get('mss')128ether = sp.Ether()129opts=[('Timestamp', (1, 1)), ('MSS', mss if mss else 1280)]130if tcpopt_unaligned:131opts = [('NOP', 0 )] + opts132if ':' in send_params['dst_address']:133ip = prepare_ipv6(send_params)134else:135ip = prepare_ipv4(send_params)136tcp = sp.TCP(137sport=send_params.get('sport'), dport=send_params.get('dport'),138flags='S', options=opts, seq=seq,139)140req = ether / ip / tcp141sp.sendp(req, iface=send_params['sendif'], verbose=False)142143144def send_udp(send_params):145LOGGER.debug(f'Sending UDP ping')146packets = []147send_length = send_params['length']148send_frag_length = send_params['frag_length']149ether = sp.Ether()150if ':' in send_params['dst_address']:151ip6 = prepare_ipv6(send_params)152udp = sp.UDP(153sport=send_params.get('sport'), dport=send_params.get('dport'),154)155raw = sp.Raw(load=build_payload(send_length))156if send_frag_length:157for packet in sp.fragment6(ip6 / udp / raw, fragSize=send_frag_length):158packets.append(ether / packet)159else:160packets.append(ether / ip6 / udp / raw)161else:162ip = prepare_ipv4(send_params)163udp = sp.UDP(164sport=send_params.get('sport'), dport=send_params.get('dport'),165)166raw = sp.Raw(load=build_payload(send_length))167if send_frag_length:168for packet in sp.fragment(ip / udp / raw, fragsize=send_frag_length):169packets.append(ether / packet)170else:171packets.append(ether / ip / udp / raw)172173for packet in packets:174sp.sendp(packet, iface=send_params['sendif'], verbose=False)175176177def send_ping(ping_type, send_params):178if ping_type == 'icmp':179send_icmp_ping(send_params)180elif (181ping_type == 'tcpsyn' or182ping_type == 'tcp3way'183):184send_tcp_syn(send_params)185elif ping_type == 'udp':186send_udp(send_params)187else:188raise Exception('Unsupported ping type')189190191def check_ipv4(expect_params, packet):192src_address = expect_params.get('src_address')193dst_address = expect_params.get('dst_address')194flags = expect_params.get('flags')195tos = expect_params.get('tc')196ttl = expect_params.get('hlim')197ip = packet.getlayer(sp.IP)198LOGGER.debug(f'Packet: {ip}')199if not ip:200LOGGER.debug('Packet is not IPv4!')201return False202if src_address and ip.src != src_address:203LOGGER.debug(f'Wrong IPv4 source {ip.src}, expected {src_address}')204return False205if dst_address and ip.dst != dst_address:206LOGGER.debug(f'Wrong IPv4 destination {ip.dst}, expected {dst_address}')207return False208if flags and ip.flags != flags:209LOGGER.debug(f'Wrong IP flags value {ip.flags}, expected {flags}')210return False211if tos and ip.tos != tos:212LOGGER.debug(f'Wrong ToS value {ip.tos}, expected {tos}')213return False214if ttl and ip.ttl != ttl:215LOGGER.debug(f'Wrong TTL value {ip.ttl}, expected {ttl}')216return False217return True218219220def check_ipv6(expect_params, packet):221src_address = expect_params.get('src_address')222dst_address = expect_params.get('dst_address')223flags = expect_params.get('flags')224hlim = expect_params.get('hlim')225tc = expect_params.get('tc')226ip6 = packet.getlayer(sp.IPv6)227if not ip6:228LOGGER.debug('Packet is not IPv6!')229return False230if src_address and socket.inet_pton(socket.AF_INET6, ip6.src) != \231socket.inet_pton(socket.AF_INET6, src_address):232LOGGER.debug(f'Wrong IPv6 source {ip6.src}, expected {src_address}')233return False234if dst_address and socket.inet_pton(socket.AF_INET6, ip6.dst) != \235socket.inet_pton(socket.AF_INET6, dst_address):236LOGGER.debug(f'Wrong IPv6 destination {ip6.dst}, expected {dst_address}')237return False238# IPv6 has no IP-level checksum.239if flags:240raise Exception("There's no fragmentation flags in IPv6")241if hlim and ip6.hlim != hlim:242LOGGER.debug(f'Wrong Hop Limit value {ip6.hlim}, expected {hlim}')243return False244if tc and ip6.tc != tc:245LOGGER.debug(f'Wrong TC value {ip6.tc}, expected {tc}')246return False247return True248249250def check_ping_4(expect_params, packet):251expect_length = expect_params['length']252if not check_ipv4(expect_params, packet):253return False254icmp = packet.getlayer(sp.ICMP)255if not icmp:256LOGGER.debug('Packet is not IPv4 ICMP!')257return False258raw = packet.getlayer(sp.Raw)259if not raw:260LOGGER.debug('Packet contains no payload!')261return False262if raw.load != build_payload(expect_length):263LOGGER.debug('Payload magic does not match!')264return False265return True266267268def check_ping_request_4(expect_params, packet):269if not check_ping_4(expect_params, packet):270return False271icmp = packet.getlayer(sp.ICMP)272if sp.icmptypes[icmp.type] != 'echo-request':273LOGGER.debug('Packet is not IPv4 ICMP Echo Request!')274return False275return True276277278def check_ping_reply_4(expect_params, packet):279if not check_ping_4(expect_params, packet):280return False281icmp = packet.getlayer(sp.ICMP)282if sp.icmptypes[icmp.type] != 'echo-reply':283LOGGER.debug('Packet is not IPv4 ICMP Echo Reply!')284return False285return True286287288def check_ping_request_6(expect_params, packet):289expect_length = expect_params['length']290if not check_ipv6(expect_params, packet):291return False292icmp = packet.getlayer(sp.ICMPv6EchoRequest)293if not icmp:294LOGGER.debug('Packet is not IPv6 ICMP Echo Request!')295return False296if icmp.data != build_payload(expect_length):297LOGGER.debug('Payload magic does not match!')298return False299return True300301302def check_ping_reply_6(expect_params, packet):303expect_length = expect_params['length']304if not check_ipv6(expect_params, packet):305return False306icmp = packet.getlayer(sp.ICMPv6EchoReply)307if not icmp:308LOGGER.debug('Packet is not IPv6 ICMP Echo Reply!')309return False310if icmp.data != build_payload(expect_length):311LOGGER.debug('Payload magic does not match!')312return False313return True314315316def check_ping_request(args, packet):317src_address = args['expect_params'].get('src_address')318dst_address = args['expect_params'].get('dst_address')319if not (src_address or dst_address):320raise Exception('Source or destination address must be given to match the ping request!')321if (322(src_address and ':' in src_address) or323(dst_address and ':' in dst_address)324):325return check_ping_request_6(args['expect_params'], packet)326else:327return check_ping_request_4(args['expect_params'], packet)328329330def check_ping_reply(args, packet):331src_address = args['expect_params'].get('src_address')332dst_address = args['expect_params'].get('dst_address')333if not (src_address or dst_address):334raise Exception('Source or destination address must be given to match the ping reply!')335if (336(src_address and ':' in src_address) or337(dst_address and ':' in dst_address)338):339return check_ping_reply_6(args['expect_params'], packet)340else:341return check_ping_reply_4(args['expect_params'], packet)342343344def check_tcp(expect_params, packet):345tcp_flags = expect_params.get('tcp_flags')346mss = expect_params.get('mss')347seq = expect_params.get('seq')348tcp = packet.getlayer(sp.TCP)349if not tcp:350LOGGER.debug('Packet is not TCP!')351return False352chksum = tcp.chksum353tcp.chksum = None354newpacket = sp.Ether(sp.raw(packet[sp.Ether]))355new_chksum = newpacket[sp.TCP].chksum356if new_chksum and chksum != new_chksum:357LOGGER.debug(f'Wrong TCP checksum {chksum}, expected {new_chksum}!')358return False359if tcp_flags and tcp.flags != tcp_flags:360LOGGER.debug(f'Wrong TCP flags {tcp.flags}, expected {tcp_flags}!')361return False362if seq:363if tcp_flags == 'S':364tcp_seq = tcp.seq365elif tcp_flags == 'SA':366tcp_seq = tcp.ack - 1367if seq != tcp_seq:368LOGGER.debug(f'Wrong TCP Sequence Number {tcp_seq}, expected {seq}')369return False370if mss:371for option in tcp.options:372if option[0] == 'MSS':373if option[1] != mss:374LOGGER.debug(f'Wrong TCP MSS {option[1]}, expected {mss}')375return False376return True377378379def check_udp(expect_params, packet):380expect_length = expect_params['length']381udp = packet.getlayer(sp.UDP)382if not udp:383LOGGER.debug('Packet is not UDP!')384return False385raw = packet.getlayer(sp.Raw)386if not raw:387LOGGER.debug('Packet contains no payload!')388return False389if raw.load != build_payload(expect_length):390LOGGER.debug(f'Payload magic does not match len {len(raw.load)} vs {expect_length}!')391return False392orig_chksum = udp.chksum393udp.chksum = None394newpacket = sp.Ether(sp.raw(packet[sp.Ether]))395new_chksum = newpacket[sp.UDP].chksum396if new_chksum and orig_chksum != new_chksum:397LOGGER.debug(f'Wrong UDP checksum {orig_chksum}, expected {new_chksum}!')398return False399400return True401402403def check_tcp_syn_request_4(expect_params, packet):404if not check_ipv4(expect_params, packet):405return False406if not check_tcp(expect_params | {'tcp_flags': 'S'}, packet):407return False408return True409410411def check_tcp_syn_reply_4(send_params, expect_params, packet):412if not check_ipv4(expect_params, packet):413return False414if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):415return False416return True417418419def check_tcp_3way_4(args, packet):420send_params = args['send_params']421422expect_params_sa = clean_params(args['expect_params'])423expect_params_sa['src_address'] = send_params['dst_address']424expect_params_sa['dst_address'] = send_params['src_address']425426# Sniff incoming SYN+ACK packet427if (428check_ipv4(expect_params_sa, packet) and429check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)430):431ether = sp.Ether()432ip_sa = packet.getlayer(sp.IP)433tcp_sa = packet.getlayer(sp.TCP)434reply_params = clean_params(send_params)435reply_params['src_address'] = ip_sa.dst436reply_params['dst_address'] = ip_sa.src437ip_a = prepare_ipv4(reply_params)438tcp_a = sp.TCP(439sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',440seq=tcp_sa.ack, ack=tcp_sa.seq + 1,441)442req = ether / ip_a / tcp_a443sp.sendp(req, iface=send_params['sendif'], verbose=False)444return True445446return False447448449def check_udp_request_4(expect_params, packet):450if not check_ipv4(expect_params, packet):451return False452if not check_udp(expect_params, packet):453return False454return True455456457def check_tcp_syn_request_6(expect_params, packet):458if not check_ipv6(expect_params, packet):459return False460if not check_tcp(expect_params | {'tcp_flags': 'S'}, packet):461return False462return True463464465def check_tcp_syn_reply_6(expect_params, packet):466if not check_ipv6(expect_params, packet):467return False468if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):469return False470return True471472473def check_tcp_3way_6(args, packet):474send_params = args['send_params']475476expect_params_sa = clean_params(args['expect_params'])477expect_params_sa['src_address'] = send_params['dst_address']478expect_params_sa['dst_address'] = send_params['src_address']479480# Sniff incoming SYN+ACK packet481if (482check_ipv6(expect_params_sa, packet) and483check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)484):485ether = sp.Ether()486ip6_sa = packet.getlayer(sp.IPv6)487tcp_sa = packet.getlayer(sp.TCP)488reply_params = clean_params(send_params)489reply_params['src_address'] = ip6_sa.dst490reply_params['dst_address'] = ip6_sa.src491ip_a = prepare_ipv6(reply_params)492tcp_a = sp.TCP(493sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',494seq=tcp_sa.ack, ack=tcp_sa.seq + 1,495)496req = ether / ip_a / tcp_a497sp.sendp(req, iface=send_params['sendif'], verbose=False)498return True499500return False501502503def check_udp_request_6(expect_params, packet):504if not check_ipv6(expect_params, packet):505return False506if not check_udp(expect_params, packet):507return False508return True509510def check_tcp_syn_request(args, packet):511expect_params = args['expect_params']512src_address = expect_params.get('src_address')513dst_address = expect_params.get('dst_address')514if not (src_address or dst_address):515raise Exception('Source or destination address must be given to match the tcp syn request!')516if (517(src_address and ':' in src_address) or518(dst_address and ':' in dst_address)519):520return check_tcp_syn_request_6(expect_params, packet)521else:522return check_tcp_syn_request_4(expect_params, packet)523524525def check_tcp_syn_reply(args, packet):526expect_params = args['expect_params']527src_address = expect_params.get('src_address')528dst_address = expect_params.get('dst_address')529if not (src_address or dst_address):530raise Exception('Source or destination address must be given to match the tcp syn reply!')531if (532(src_address and ':' in src_address) or533(dst_address and ':' in dst_address)534):535return check_tcp_syn_reply_6(expect_params, packet)536else:537return check_tcp_syn_reply_4(expect_params, packet)538539def check_tcp_3way(args, packet):540expect_params = args['expect_params']541src_address = expect_params.get('src_address')542dst_address = expect_params.get('dst_address')543if not (src_address or dst_address):544raise Exception('Source or destination address must be given to match the tcp syn reply!')545if (546(src_address and ':' in src_address) or547(dst_address and ':' in dst_address)548):549return check_tcp_3way_6(args, packet)550else:551return check_tcp_3way_4(args, packet)552553554def check_udp_request(args, packet):555expect_params = args['expect_params']556src_address = expect_params.get('src_address')557dst_address = expect_params.get('dst_address')558if not (src_address or dst_address):559raise Exception('Source or destination address must be given to match the tcp syn request!')560if (561(src_address and ':' in src_address) or562(dst_address and ':' in dst_address)563):564return check_udp_request_6(expect_params, packet)565else:566return check_udp_request_4(expect_params, packet)567568569def setup_sniffer(570recvif, ping_type, sniff_type, expect_params, defrag, send_params,571):572if ping_type == 'icmp' and sniff_type == 'request':573checkfn = check_ping_request574elif ping_type == 'icmp' and sniff_type == 'reply':575checkfn = check_ping_reply576elif ping_type == 'tcpsyn' and sniff_type == 'request':577checkfn = check_tcp_syn_request578elif ping_type == 'tcpsyn' and sniff_type == 'reply':579checkfn = check_tcp_syn_reply580elif ping_type == 'tcp3way' and sniff_type == 'reply':581checkfn = check_tcp_3way582elif ping_type == 'udp' and sniff_type == 'request':583checkfn = check_udp_request584else:585raise Exception('Unspported ping and sniff type combination')586587return Sniffer(588{'send_params': send_params, 'expect_params': expect_params},589checkfn, recvif, defrag=defrag,590)591592593def parse_args():594parser = argparse.ArgumentParser("pft_ping.py",595description="Ping test tool")596597# Parameters of sent ping request598parser.add_argument('--sendif', required=True,599help='The interface through which the packet(s) will be sent')600parser.add_argument('--to', required=True,601help='The destination IP address for the ping request')602parser.add_argument('--ping-type',603choices=('icmp', 'tcpsyn', 'tcp3way', 'udp'),604help='Type of ping: ICMP (default) or TCP SYN or 3-way TCP handshake',605default='icmp')606parser.add_argument('--fromaddr',607help='The source IP address for the ping request')608609# Where to look for packets to analyze.610# The '+' format is ugly as it mixes positional with optional syntax.611# But we have no positional parameters so I guess it's fine to use it.612parser.add_argument('--recvif', nargs='+',613help='The interfaces on which to expect the ping request')614parser.add_argument('--replyif', nargs='+',615help='The interfaces which to expect the ping response')616617# Packet settings618parser_send = parser.add_argument_group('Values set in transmitted packets')619parser_send.add_argument('--send-flags', type=str,620help='IPv4 fragmentation flags')621parser_send.add_argument('--send-frag-length', type=int,622help='Force IP fragmentation with given fragment length')623parser_send.add_argument('--send-hlim', type=int,624help='IPv6 Hop Limit or IPv4 Time To Live')625parser_send.add_argument('--send-mss', type=int,626help='TCP Maximum Segment Size')627parser_send.add_argument('--send-seq', type=int,628help='TCP sequence number')629parser_send.add_argument('--send-sport', type=int,630help='TCP source port')631parser_send.add_argument('--send-dport', type=int, default=9,632help='TCP destination port')633parser_send.add_argument('--send-length', type=int, default=len(PAYLOAD_MAGIC),634help='ICMP Echo Request payload size')635parser_send.add_argument('--send-tc', type=int,636help='IPv6 Traffic Class or IPv4 DiffServ / ToS')637parser_send.add_argument('--send-tcpopt-unaligned', action='store_true',638help='Include unaligned TCP options')639parser_send.add_argument('--send-nop', action='store_true',640help='Include a NOP IPv4 option')641642# Expectations643parser_expect = parser.add_argument_group('Values expected in sniffed packets')644parser_expect.add_argument('--expect-flags', type=str,645help='IPv4 fragmentation flags')646parser_expect.add_argument('--expect-hlim', type=int,647help='IPv6 Hop Limit or IPv4 Time To Live')648parser_expect.add_argument('--expect-mss', type=int,649help='TCP Maximum Segment Size')650parser_send.add_argument('--expect-seq', type=int,651help='TCP sequence number')652parser_expect.add_argument('--expect-tc', type=int,653help='IPv6 Traffic Class or IPv4 DiffServ / ToS')654655parser.add_argument('-v', '--verbose', action='store_true',656help=('Enable verbose logging. Apart of potentially useful information '657'you might see warnings from parsing packets like NDP or other '658'packets not related to the test being run. Use only when '659'developing because real tests expect empty stderr and stdout.'))660661return parser.parse_args()662663664def main():665args = parse_args()666667if args.verbose:668LOGGER.setLevel(logging.DEBUG)669670# Split parameters into send and expect parameters. Parameters might be671# missing from the command line, always fill the dictionaries with None.672send_params = {}673expect_params = {}674for param_name in (675'flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length',676'sport', 'dport',677):678param_arg = vars(args).get(f'send_{param_name}')679send_params[param_name] = param_arg if param_arg else None680param_arg = vars(args).get(f'expect_{param_name}')681expect_params[param_name] = param_arg if param_arg else None682683expect_params['length'] = send_params['length']684send_params['tcpopt_unaligned'] = args.send_tcpopt_unaligned685send_params['nop'] = args.send_nop686send_params['src_address'] = args.fromaddr if args.fromaddr else None687send_params['dst_address'] = args.to688send_params['sendif'] = args.sendif689690# We may not have a default route. Tell scapy where to start looking for routes691sp.conf.iface6 = args.sendif692693# Configuration sanity checking.694if not (args.replyif or args.recvif):695raise Exception('With no reply or recv interface specified no traffic '696'can be sniffed and verified!'697)698699sniffers = []700701if send_params['frag_length']:702if (703(send_params['src_address'] and ':' in send_params['src_address']) or704(send_params['dst_address'] and ':' in send_params['dst_address'])705):706defrag = 'IPv6'707else:708defrag = 'IPv4'709else:710defrag = False711712if args.recvif:713sniffer_params = copy(expect_params)714sniffer_params['src_address'] = None715sniffer_params['dst_address'] = args.to716for iface in args.recvif:717LOGGER.debug(f'Installing receive sniffer on {iface}')718sniffers.append(719setup_sniffer(iface, args.ping_type, 'request',720sniffer_params, defrag, send_params,721))722723if args.replyif:724sniffer_params = copy(expect_params)725sniffer_params['src_address'] = args.to726sniffer_params['dst_address'] = None727for iface in args.replyif:728LOGGER.debug(f'Installing reply sniffer on {iface}')729sniffers.append(730setup_sniffer(iface, args.ping_type, 'reply',731sniffer_params, defrag, send_params,732))733734LOGGER.debug(f'Installed {len(sniffers)} sniffers')735736send_ping(args.ping_type, send_params)737738err = 0739sniffer_num = 0740for sniffer in sniffers:741sniffer.join()742if sniffer.correctPackets == 1:743LOGGER.debug(f'Expected ping has been sniffed on {sniffer._recvif}.')744else:745# Set a bit in err for each failed sniffer.746err |= 1<<sniffer_num747if sniffer.correctPackets > 1:748LOGGER.debug(f'Duplicated ping has been sniffed on {sniffer._recvif}!')749else:750LOGGER.debug(f'Expected ping has not been sniffed on {sniffer._recvif}!')751sniffer_num += 1752753return err754755756if __name__ == '__main__':757sys.exit(main())758759760