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