xref: /freebsd/sbin/ping/tests/test_ping.py (revision a50d73d5782a351ad83e8d1f84d11720a12e70d3)
1import pytest
2
3import logging
4import os
5import re
6import subprocess
7
8from atf_python.sys.net.vnet import IfaceFactory
9from atf_python.sys.net.vnet import SingleVnetTestTemplate
10from atf_python.sys.net.tools import ToolsHelper
11from typing import List
12from typing import Optional
13
14logging.getLogger("scapy").setLevel(logging.CRITICAL)
15import scapy.all as sc
16
17
18def build_response_packet(echo, ip, icmp, oip_ihl, special):
19    icmp_id_seq_types = [0, 8, 13, 14, 15, 16, 17, 18, 37, 38]
20    oip = echo[sc.IP]
21    oicmp = echo[sc.ICMP]
22    load = echo[sc.ICMP].payload
23    oip[sc.IP].remove_payload()
24    oicmp[sc.ICMP].remove_payload()
25    oicmp.type = 8
26
27    # As if the original IP packet had these set
28    oip.ihl = None
29    oip.len = None
30    oip.id = 1
31    oip.flags = ip.flags
32    oip.chksum = None
33    oip.options = ip.options
34
35    # Inner packet (oip) options
36    if oip_ihl:
37        oip.ihl = oip_ihl
38
39    # Special options
40    if special == "no-payload":
41        load = ""
42    if special == "tcp":
43        oip.proto = "tcp"
44        tcp = sc.TCP(sport=1234, dport=5678)
45        return ip / icmp / oip / tcp
46    if special == "udp":
47        oip.proto = "udp"
48        udp = sc.UDP(sport=1234, dport=5678)
49        return ip / icmp / oip / udp
50    if special == "warp":
51        # Build a package with a timestamp of INT_MAX
52        # (time-warped package)
53        payload_no_timestamp = sc.bytes_hex(load)[16:]
54        load = (b"\xff" * 8) + sc.hex_bytes(payload_no_timestamp)
55    if special == "wrong":
56        # Build a package with a wrong last byte
57        payload_no_last_byte = sc.bytes_hex(load)[:-2]
58        load = (sc.hex_bytes(payload_no_last_byte)) + b"\x00"
59    if special == "not-mine":
60        # Modify the ICMP Identifier field
61        oicmp.id += 1
62
63    if icmp.type in icmp_id_seq_types:
64        pkt = ip / icmp / load
65    else:
66        ip.options = ""
67        pkt = ip / icmp / oip / oicmp / load
68    return pkt
69
70
71def generate_ip_options(opts):
72    if not opts:
73        return ""
74
75    routers = [
76        "192.0.2.10",
77        "192.0.2.20",
78        "192.0.2.30",
79        "192.0.2.40",
80        "192.0.2.50",
81        "192.0.2.60",
82        "192.0.2.70",
83        "192.0.2.80",
84        "192.0.2.90",
85    ]
86    routers_zero = [0, 0, 0, 0, 0, 0, 0, 0, 0]
87    if opts == "EOL":
88        options = sc.IPOption(b"\x00")
89    elif opts == "NOP":
90        options = sc.IPOption(b"\x01")
91    elif opts == "NOP-40":
92        options = sc.IPOption(b"\x01" * 40)
93    elif opts == "RR":
94        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
95        options = sc.IPOption_RR(pointer=40, routers=routers)
96    elif opts == "RR-same":
97        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
98        options = sc.IPOption_RR(pointer=3, routers=routers_zero)
99    elif opts == "RR-trunc":
100        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
101        options = sc.IPOption_RR(length=7, routers=routers_zero)
102    elif opts == "LSRR":
103        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
104        options = sc.IPOption_LSRR(routers=routers)
105    elif opts == "LSRR-trunc":
106        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
107        options = sc.IPOption_LSRR(length=3, routers=routers_zero)
108    elif opts == "SSRR":
109        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
110        options = sc.IPOption_SSRR(routers=routers)
111    elif opts == "SSRR-trunc":
112        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
113        options = sc.IPOption_SSRR(length=3, routers=routers_zero)
114    elif opts == "unk":
115        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
116        options = sc.IPOption(b"\x9f")
117    elif opts == "unk-40":
118        ToolsHelper.set_sysctl("net.inet.ip.process_options", 0)
119        options = sc.IPOption(b"\x9f" * 40)
120    else:
121        options = ""
122    return options
123
124
125def pinger(
126    # Required arguments
127    # Avoid setting defaults on these arguments,
128    # as we want to set them explicitly in the tests
129    iface: str,
130    /,
131    src: sc.scapy.fields.SourceIPField,
132    dst: sc.scapy.layers.inet.DestIPField,
133    icmp_type: sc.scapy.fields.ByteEnumField,
134    icmp_code: sc.scapy.fields.MultiEnumField,
135    # IP arguments
136    ihl: Optional[sc.scapy.fields.BitField] = None,
137    flags: Optional[sc.scapy.fields.FlagsField] = None,
138    opts: Optional[str] = None,
139    oip_ihl: Optional[sc.scapy.fields.BitField] = None,
140    special: Optional[str] = None,
141    # ICMP arguments
142    # Match names with <netinet/ip_icmp.h>
143    icmp_pptr: sc.scapy.fields.ByteField = 0,
144    icmp_gwaddr: sc.scapy.fields.IPField = "0.0.0.0",
145    icmp_nextmtu: sc.scapy.fields.ShortField = 0,
146    icmp_otime: sc.scapy.layers.inet.ICMPTimeStampField = 0,
147    icmp_rtime: sc.scapy.layers.inet.ICMPTimeStampField = 0,
148    icmp_ttime: sc.scapy.layers.inet.ICMPTimeStampField = 0,
149    icmp_mask: sc.scapy.fields.IPField = "0.0.0.0",
150    request: Optional[str] = None,
151    # Miscellaneous arguments
152    count: int = 1,
153    dup: bool = False,
154    verbose: bool = True,
155) -> subprocess.CompletedProcess:
156    """P I N G E R
157
158    Echo reply faker
159
160    :param str iface: Interface to send packet to
161    :keyword src: Source packet IP
162    :type src: class:`scapy.fields.SourceIPField`
163    :keyword dst: Destination packet IP
164    :type dst: class:`scapy.layers.inet.DestIPField`
165    :keyword icmp_type: ICMP type
166    :type icmp_type: class:`scapy.fields.ByteEnumField`
167    :keyword icmp_code: ICMP code
168    :type icmp_code: class:`scapy.fields.MultiEnumField`
169
170    :keyword ihl: Internet Header Length, defaults to None
171    :type ihl: class:`scapy.fields.BitField`, optional
172    :keyword flags: IP flags - one of `DF`, `MF` or `evil`, defaults to None
173    :type flags: class:`scapy.fields.FlagsField`, optional
174    :keyword opts: Include IP options - one of `EOL`, `NOP`, `NOP-40`, `unk`,
175        `unk-40`, `RR`, `RR-same`, `RR-trunc`, `LSRR`, `LSRR-trunc`, `SSRR` or
176        `SSRR-trunc`, defaults to None
177    :type opts: str, optional
178    :keyword oip_ihl: Inner packet's Internet Header Length, defaults to None
179    :type oip_ihl: class:`scapy.fields.BitField`, optional
180    :keyword special: Send a special packet - one of `no-payload`, `not-mine`,
181        `tcp`, `udp`, `wrong` or `warp`, defaults to None
182    :type special: str, optional
183    :keyword icmp_pptr: ICMP pointer, defaults to 0
184    :type icmp_pptr: class:`scapy.fields.ByteField`
185    :keyword icmp_gwaddr: ICMP gateway IP address, defaults to "0.0.0.0"
186    :type icmp_gwaddr: class:`scapy.fields.IPField`
187    :keyword icmp_nextmtu: ICMP next MTU, defaults to 0
188    :type icmp_nextmtu: class:`scapy.fields.ShortField`
189    :keyword icmp_otime: ICMP originate timestamp, defaults to 0
190    :type icmp_otime: class:`scapy.layers.inet.ICMPTimeStampField`
191    :keyword icmp_rtime: ICMP receive timestamp, defaults to 0
192    :type icmp_rtime: class:`scapy.layers.inet.ICMPTimeStampField`
193    :keyword icmp_ttime: ICMP transmit timestamp, defaults to 0
194    :type icmp_ttime: class:`scapy.layers.inet.ICMPTimeStampField`
195    :keyword icmp_mask: ICMP address mask, defaults to "0.0.0.0"
196    :type icmp_mask: class:`scapy.fields.IPField`
197    :keyword request: Request type - one of `mask` or `timestamp`,
198        defaults to None
199    :type request: str, optional
200    :keyword count: Number of packets to send, defaults to 1
201    :type count: int
202    :keyword dup: Duplicate packets, defaults to `False`
203    :type dup: bool
204    :keyword verbose: Turn on/off verbosity, defaults to `True`
205    :type verbose: bool
206
207    :return: A class:`subprocess.CompletedProcess` with the output from the
208        ping utility
209    :rtype: class:`subprocess.CompletedProcess`
210    """
211    tun = sc.TunTapInterface(iface)
212    subprocess.run(["ifconfig", tun.iface, "up"], check=True)
213    subprocess.run(["ifconfig", tun.iface, src, dst], check=True)
214    ip_opts = generate_ip_options(opts)
215    ip = sc.IP(ihl=ihl, flags=flags, src=dst, dst=src, options=ip_opts)
216    command = [
217        "/sbin/ping",
218        "-c",
219        str(count),
220        "-t",
221        str(count),
222    ]
223    if verbose:
224        command += ["-v"]
225    if request == "mask":
226        command += ["-Mm"]
227    if request == "timestamp":
228        command += ["-Mt"]
229    if special:
230        command += ["-p1"]
231    if opts in [
232        "RR",
233        "RR-same",
234        "RR-trunc",
235        "LSRR",
236        "LSRR-trunc",
237        "SSRR",
238        "SSRR-trunc",
239    ]:
240        command += ["-R"]
241    command += [dst]
242    with subprocess.Popen(
243        args=command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
244    ) as ping:
245        for dummy in range(count):
246            echo = tun.recv()
247            icmp = sc.ICMP(
248                type=icmp_type,
249                code=icmp_code,
250                id=echo[sc.ICMP].id,
251                seq=echo[sc.ICMP].seq,
252                ts_ori=icmp_otime,
253                ts_rx=icmp_rtime,
254                ts_tx=icmp_ttime,
255                gw=icmp_gwaddr,
256                ptr=icmp_pptr,
257                addr_mask=icmp_mask,
258                nexthopmtu=icmp_nextmtu,
259            )
260            pkt = build_response_packet(echo, ip, icmp, oip_ihl, special)
261            tun.send(pkt)
262            if dup is True:
263                tun.send(pkt)
264        stdout, stderr = ping.communicate()
265    return subprocess.CompletedProcess(
266        ping.args, ping.returncode, stdout, stderr
267    )
268
269
270def redact(output):
271    """Redact some elements of ping's output"""
272    pattern_replacements = [
273        ("localhost \([0-9]{1,3}(\.[0-9]{1,3}){3}\)", "localhost"),
274        ("from [0-9]{1,3}(\.[0-9]{1,3}){3}", "from"),
275        ("hlim=[0-9]*", "hlim="),
276        ("ttl=[0-9]*", "ttl="),
277        ("time=[0-9.-]*", "time="),
278        ("\(-[0-9\.]+[0-9]+ ms\)", "(- ms)"),
279        ("[0-9\.]+/[0-9.]+", "/"),
280    ]
281    for pattern, repl in pattern_replacements:
282        output = re.sub(pattern, repl, output)
283    return output
284
285
286class TestPing(SingleVnetTestTemplate):
287    IPV6_PREFIXES: List[str] = ["2001:db8::1/64"]
288    IPV4_PREFIXES: List[str] = ["192.0.2.1/24"]
289
290    # Each param in testdata contains a dictionary with the command,
291    # and the expected outcome (returncode, redacted stdout, and stderr)
292    testdata = [
293        pytest.param(
294            {
295                "args": "ping -4 -c1 -s56 -t1 localhost",
296                "returncode": 0,
297                "stdout": """\
298PING localhost: 56 data bytes
29964 bytes from: icmp_seq=0 ttl= time= ms
300
301--- localhost ping statistics ---
3021 packets transmitted, 1 packets received, 0.0% packet loss
303round-trip min/avg/max/stddev = /// ms
304""",
305                "stderr": "",
306            },
307            id="_4_c1_s56_t1_localhost",
308        ),
309        pytest.param(
310            {
311                "args": "ping -6 -c1 -s8 -t1 localhost",
312                "returncode": 0,
313                "stdout": """\
314PING6(56=40+8+8 bytes) ::1 --> ::1
31516 bytes from ::1, icmp_seq=0 hlim= time= ms
316
317--- localhost ping6 statistics ---
3181 packets transmitted, 1 packets received, 0.0% packet loss
319round-trip min/avg/max/std-dev = /// ms
320""",
321                "stderr": "",
322            },
323            id="_6_c1_s8_t1_localhost",
324        ),
325        pytest.param(
326            {
327                "args": "ping -A -c1 192.0.2.1",
328                "returncode": 0,
329                "stdout": """\
330PING 192.0.2.1 (192.0.2.1): 56 data bytes
33164 bytes from: icmp_seq=0 ttl= time= ms
332
333--- 192.0.2.1 ping statistics ---
3341 packets transmitted, 1 packets received, 0.0% packet loss
335round-trip min/avg/max/stddev = /// ms
336""",
337                "stderr": "",
338            },
339            id="_A_c1_192_0_2_1",
340        ),
341        pytest.param(
342            {
343                "args": "ping -A -c1 192.0.2.2",
344                "returncode": 2,
345                "stdout": """\
346PING 192.0.2.2 (192.0.2.2): 56 data bytes
347
348--- 192.0.2.2 ping statistics ---
3491 packets transmitted, 0 packets received, 100.0% packet loss
350""",
351                "stderr": "",
352            },
353            id="_A_c1_192_0_2_2",
354        ),
355        pytest.param(
356            {
357                "args": "ping -A -c1 2001:db8::1",
358                "returncode": 0,
359                "stdout": """\
360PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1
36116 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms
362
363--- 2001:db8::1 ping6 statistics ---
3641 packets transmitted, 1 packets received, 0.0% packet loss
365round-trip min/avg/max/std-dev = /// ms
366""",
367                "stderr": "",
368            },
369            id="_A_c1_2001_db8__1",
370        ),
371        pytest.param(
372            {
373                "args": "ping -A -c1 2001:db8::2",
374                "returncode": 2,
375                "stdout": """\
376PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2
377
378--- 2001:db8::2 ping6 statistics ---
3791 packets transmitted, 0 packets received, 100.0% packet loss
380""",
381                "stderr": "",
382            },
383            id="_A_c1_2001_db8__2",
384        ),
385        pytest.param(
386            {
387                "args": "ping -A -c3 192.0.2.1",
388                "returncode": 0,
389                "stdout": """\
390PING 192.0.2.1 (192.0.2.1): 56 data bytes
39164 bytes from: icmp_seq=0 ttl= time= ms
39264 bytes from: icmp_seq=1 ttl= time= ms
39364 bytes from: icmp_seq=2 ttl= time= ms
394
395--- 192.0.2.1 ping statistics ---
3963 packets transmitted, 3 packets received, 0.0% packet loss
397round-trip min/avg/max/stddev = /// ms
398""",
399                "stderr": "",
400            },
401            id="_A_3_192_0.2.1",
402        ),
403        pytest.param(
404            {
405                "args": "ping -A -c3 192.0.2.2",
406                "returncode": 2,
407                "stdout": """\
408\x07\x07PING 192.0.2.2 (192.0.2.2): 56 data bytes
409
410--- 192.0.2.2 ping statistics ---
4113 packets transmitted, 0 packets received, 100.0% packet loss
412""",
413                "stderr": "",
414            },
415            id="_A_c3_192_0_2_2",
416        ),
417        pytest.param(
418            {
419                "args": "ping -A -c3 2001:db8::1",
420                "returncode": 0,
421                "stdout": """\
422PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1
42316 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms
42416 bytes from 2001:db8::1, icmp_seq=1 hlim= time= ms
42516 bytes from 2001:db8::1, icmp_seq=2 hlim= time= ms
426
427--- 2001:db8::1 ping6 statistics ---
4283 packets transmitted, 3 packets received, 0.0% packet loss
429round-trip min/avg/max/std-dev = /// ms
430""",
431                "stderr": "",
432            },
433            id="_A_c3_2001_db8__1",
434        ),
435        pytest.param(
436            {
437                "args": "ping -A -c3 2001:db8::2",
438                "returncode": 2,
439                "stdout": """\
440\x07\x07PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2
441
442--- 2001:db8::2 ping6 statistics ---
4433 packets transmitted, 0 packets received, 100.0% packet loss
444""",
445                "stderr": "",
446            },
447            id="_A_c3_2001_db8__2",
448        ),
449        pytest.param(
450            {
451                "args": "ping -c1 192.0.2.1",
452                "returncode": 0,
453                "stdout": """\
454PING 192.0.2.1 (192.0.2.1): 56 data bytes
45564 bytes from: icmp_seq=0 ttl= time= ms
456
457--- 192.0.2.1 ping statistics ---
4581 packets transmitted, 1 packets received, 0.0% packet loss
459round-trip min/avg/max/stddev = /// ms
460""",
461                "stderr": "",
462            },
463            id="_c1_192_0_2_1",
464        ),
465        pytest.param(
466            {
467                "args": "ping -c1 192.0.2.2",
468                "returncode": 2,
469                "stdout": """\
470PING 192.0.2.2 (192.0.2.2): 56 data bytes
471
472--- 192.0.2.2 ping statistics ---
4731 packets transmitted, 0 packets received, 100.0% packet loss
474""",
475                "stderr": "",
476            },
477            id="_c1_192_0_2_2",
478        ),
479        pytest.param(
480            {
481                "args": "ping -c1 2001:db8::1",
482                "returncode": 0,
483                "stdout": """\
484PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1
48516 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms
486
487--- 2001:db8::1 ping6 statistics ---
4881 packets transmitted, 1 packets received, 0.0% packet loss
489round-trip min/avg/max/std-dev = /// ms
490""",
491                "stderr": "",
492            },
493            id="_c1_2001_db8__1",
494        ),
495        pytest.param(
496            {
497                "args": "ping -c1 2001:db8::2",
498                "returncode": 2,
499                "stdout": """\
500PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2
501
502--- 2001:db8::2 ping6 statistics ---
5031 packets transmitted, 0 packets received, 100.0% packet loss
504""",
505                "stderr": "",
506            },
507            id="_c1_2001_db8__2",
508        ),
509        pytest.param(
510            {
511                "args": "ping -c1 -S127.0.0.1 -s56 -t1 localhost",
512                "returncode": 0,
513                "stdout": """\
514PING localhost from: 56 data bytes
51564 bytes from: icmp_seq=0 ttl= time= ms
516
517--- localhost ping statistics ---
5181 packets transmitted, 1 packets received, 0.0% packet loss
519round-trip min/avg/max/stddev = /// ms
520""",
521                "stderr": "",
522            },
523            id="_c1_S127_0_0_1_s56_t1_localhost",
524        ),
525        pytest.param(
526            {
527                "args": "ping -c1 -S::1 -s8 -t1 localhost",
528                "returncode": 0,
529                "stdout": """\
530PING6(56=40+8+8 bytes) ::1 --> ::1
53116 bytes from ::1, icmp_seq=0 hlim= time= ms
532
533--- localhost ping6 statistics ---
5341 packets transmitted, 1 packets received, 0.0% packet loss
535round-trip min/avg/max/std-dev = /// ms
536""",
537                "stderr": "",
538            },
539            id="_c1_S__1_s8_t1_localhost",
540        ),
541        pytest.param(
542            {
543                "args": "ping -c3 192.0.2.1",
544                "returncode": 0,
545                "stdout": """\
546PING 192.0.2.1 (192.0.2.1): 56 data bytes
54764 bytes from: icmp_seq=0 ttl= time= ms
54864 bytes from: icmp_seq=1 ttl= time= ms
54964 bytes from: icmp_seq=2 ttl= time= ms
550
551--- 192.0.2.1 ping statistics ---
5523 packets transmitted, 3 packets received, 0.0% packet loss
553round-trip min/avg/max/stddev = /// ms
554""",
555                "stderr": "",
556            },
557            id="_c3_192_0_2_1",
558        ),
559        pytest.param(
560            {
561                "args": "ping -c3 192.0.2.2",
562                "returncode": 2,
563                "stdout": """\
564PING 192.0.2.2 (192.0.2.2): 56 data bytes
565
566--- 192.0.2.2 ping statistics ---
5673 packets transmitted, 0 packets received, 100.0% packet loss
568""",
569                "stderr": "",
570            },
571            id="_c3_192_0_2_2",
572        ),
573        pytest.param(
574            {
575                "args": "ping -c3 2001:db8::1",
576                "returncode": 0,
577                "stdout": """\
578PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1
57916 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms
58016 bytes from 2001:db8::1, icmp_seq=1 hlim= time= ms
58116 bytes from 2001:db8::1, icmp_seq=2 hlim= time= ms
582
583--- 2001:db8::1 ping6 statistics ---
5843 packets transmitted, 3 packets received, 0.0% packet loss
585round-trip min/avg/max/std-dev = /// ms
586""",
587                "stderr": "",
588            },
589            id="_c3_2001_db8__1",
590        ),
591        pytest.param(
592            {
593                "args": "ping -c3 2001:db8::2",
594                "returncode": 2,
595                "stdout": """\
596PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2
597
598--- 2001:db8::2 ping6 statistics ---
5993 packets transmitted, 0 packets received, 100.0% packet loss
600""",
601                "stderr": "",
602            },
603            id="_c3_2001_db8__2",
604        ),
605        pytest.param(
606            {
607                "args": "ping -q -c1 192.0.2.1",
608                "returncode": 0,
609                "stdout": """\
610PING 192.0.2.1 (192.0.2.1): 56 data bytes
611
612--- 192.0.2.1 ping statistics ---
6131 packets transmitted, 1 packets received, 0.0% packet loss
614round-trip min/avg/max/stddev = /// ms
615""",
616                "stderr": "",
617            },
618            id="_q_c1_192_0_2_1",
619        ),
620        pytest.param(
621            {
622                "args": "ping -q -c1 192.0.2.2",
623                "returncode": 2,
624                "stdout": """\
625PING 192.0.2.2 (192.0.2.2): 56 data bytes
626
627--- 192.0.2.2 ping statistics ---
6281 packets transmitted, 0 packets received, 100.0% packet loss
629""",
630                "stderr": "",
631            },
632            id="_q_c1_192_0_2_2",
633        ),
634        pytest.param(
635            {
636                "args": "ping -q -c1 2001:db8::1",
637                "returncode": 0,
638                "stdout": """\
639PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1
640
641--- 2001:db8::1 ping6 statistics ---
6421 packets transmitted, 1 packets received, 0.0% packet loss
643round-trip min/avg/max/std-dev = /// ms
644""",
645                "stderr": "",
646            },
647            id="_q_c1_2001_db8__1",
648        ),
649        pytest.param(
650            {
651                "args": "ping -q -c1 2001:db8::2",
652                "returncode": 2,
653                "stdout": """\
654PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2
655
656--- 2001:db8::2 ping6 statistics ---
6571 packets transmitted, 0 packets received, 100.0% packet loss
658""",
659                "stderr": "",
660            },
661            id="_q_c1_2001_db8__2",
662        ),
663        pytest.param(
664            {
665                "args": "ping -q -c3 192.0.2.1",
666                "returncode": 0,
667                "stdout": """\
668PING 192.0.2.1 (192.0.2.1): 56 data bytes
669
670--- 192.0.2.1 ping statistics ---
6713 packets transmitted, 3 packets received, 0.0% packet loss
672round-trip min/avg/max/stddev = /// ms
673""",
674                "stderr": "",
675            },
676            id="_q_c3_192_0_2_1",
677        ),
678        pytest.param(
679            {
680                "args": "ping -q -c3 192.0.2.2",
681                "returncode": 2,
682                "stdout": """\
683PING 192.0.2.2 (192.0.2.2): 56 data bytes
684
685--- 192.0.2.2 ping statistics ---
6863 packets transmitted, 0 packets received, 100.0% packet loss
687""",
688                "stderr": "",
689            },
690            id="_q_c3_192_0_2_2",
691        ),
692        pytest.param(
693            {
694                "args": "ping -q -c3 2001:db8::1",
695                "returncode": 0,
696                "stdout": """\
697PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1
698
699--- 2001:db8::1 ping6 statistics ---
7003 packets transmitted, 3 packets received, 0.0% packet loss
701round-trip min/avg/max/std-dev = /// ms
702""",
703                "stderr": "",
704            },
705            id="_q_c3_2001_db8__1",
706        ),
707        pytest.param(
708            {
709                "args": "ping -q -c3 2001:db8::2",
710                "returncode": 2,
711                "stdout": """\
712PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2
713
714--- 2001:db8::2 ping6 statistics ---
7153 packets transmitted, 0 packets received, 100.0% packet loss
716""",
717                "stderr": "",
718            },
719            id="_q_c3_2001_db8__2",
720        ),
721    ]
722
723    @pytest.mark.parametrize("expected", testdata)
724    def test_ping(self, expected):
725        """Test ping"""
726        ping = subprocess.run(
727            expected["args"].split(),
728            capture_output=True,
729            timeout=15,
730            text=True,
731        )
732        assert ping.returncode == expected["returncode"]
733        assert redact(ping.stdout) == expected["stdout"]
734        assert ping.stderr == expected["stderr"]
735
736    # Each param in ping46_testdata contains a dictionary with the arguments
737    # and the expected outcome (returncode, redacted stdout, and stderr)
738    # common to `ping -4` and `ping -6`
739    ping46_testdata = [
740        pytest.param(
741            {
742                "args": "-Wx localhost",
743                "returncode": os.EX_USAGE,
744                "stdout": "",
745                "stderr": "ping: invalid timing interval: `x'\n",
746            },
747            id="_Wx_localhost",
748        ),
749    ]
750
751    @pytest.mark.parametrize("expected", ping46_testdata)
752    def test_ping_46(self, expected):
753        """Test ping -4/ping -6"""
754        for version in [4, 6]:
755            ping = subprocess.run(
756                ["ping", f"-{version}"] + expected["args"].split(),
757                capture_output=True,
758                timeout=15,
759                text=True,
760            )
761            assert ping.returncode == expected["returncode"]
762            assert redact(ping.stdout) == expected["stdout"]
763            assert ping.stderr == expected["stderr"]
764
765    # Each param in pinger_testdata contains a dictionary with the keywords to
766    # `pinger()` and a dictionary with the expected outcome (returncode,
767    # stdout, stderr, and if ping's output is redacted)
768    pinger_testdata = [
769        pytest.param(
770            {
771                "src": "192.0.2.1",
772                "dst": "192.0.2.2",
773                "icmp_type": 0,
774                "icmp_code": 0,
775            },
776            {
777                "returncode": 0,
778                "stdout": """\
779PING 192.0.2.2 (192.0.2.2): 56 data bytes
78064 bytes from: icmp_seq=0 ttl= time= ms
781
782--- 192.0.2.2 ping statistics ---
7831 packets transmitted, 1 packets received, 0.0% packet loss
784round-trip min/avg/max/stddev = /// ms
785""",
786                "stderr": "",
787                "redacted": True,
788            },
789            id="_0_0",
790        ),
791        pytest.param(
792            {
793                "src": "192.0.2.1",
794                "dst": "192.0.2.2",
795                "icmp_type": 0,
796                "icmp_code": 0,
797                "opts": "EOL",
798            },
799            {
800                "returncode": 0,
801                "stdout": """\
802PING 192.0.2.2 (192.0.2.2): 56 data bytes
80364 bytes from: icmp_seq=0 ttl= time= ms
804wrong total length 88 instead of 84
805
806--- 192.0.2.2 ping statistics ---
8071 packets transmitted, 1 packets received, 0.0% packet loss
808round-trip min/avg/max/stddev = /// ms
809""",
810                "stderr": "",
811                "redacted": True,
812            },
813            id="_0_0_opts_EOL",
814        ),
815        pytest.param(
816            {
817                "src": "192.0.2.1",
818                "dst": "192.0.2.2",
819                "icmp_type": 0,
820                "icmp_code": 0,
821                "opts": "LSRR",
822            },
823            {
824                "returncode": 0,
825                "stdout": """\
826PING 192.0.2.2 (192.0.2.2): 56 data bytes
82764 bytes from: icmp_seq=0 ttl= time= ms
828LSRR: 	192.0.2.10
829	192.0.2.20
830	192.0.2.30
831	192.0.2.40
832	192.0.2.50
833	192.0.2.60
834	192.0.2.70
835	192.0.2.80
836	192.0.2.90
837
838--- 192.0.2.2 ping statistics ---
8391 packets transmitted, 1 packets received, 0.0% packet loss
840round-trip min/avg/max/stddev = /// ms
841""",
842                "stderr": "",
843                "redacted": True,
844            },
845            id="_0_0_opts_LSRR",
846        ),
847        pytest.param(
848            {
849                "src": "192.0.2.1",
850                "dst": "192.0.2.2",
851                "icmp_type": 0,
852                "icmp_code": 0,
853                "opts": "LSRR-trunc",
854            },
855            {
856                "returncode": 0,
857                "stdout": """\
858PING 192.0.2.2 (192.0.2.2): 56 data bytes
85964 bytes from: icmp_seq=0 ttl= time= ms
860LSRR: 	(truncated route)
861
862
863--- 192.0.2.2 ping statistics ---
8641 packets transmitted, 1 packets received, 0.0% packet loss
865round-trip min/avg/max/stddev = /// ms
866""",
867                "stderr": "",
868                "redacted": True,
869            },
870            id="_0_0_opts_LSRR_trunc",
871        ),
872        pytest.param(
873            {
874                "src": "192.0.2.1",
875                "dst": "192.0.2.2",
876                "icmp_type": 0,
877                "icmp_code": 0,
878                "opts": "SSRR",
879            },
880            {
881                "returncode": 0,
882                "stdout": """\
883PING 192.0.2.2 (192.0.2.2): 56 data bytes
88464 bytes from: icmp_seq=0 ttl= time= ms
885SSRR: 	192.0.2.10
886	192.0.2.20
887	192.0.2.30
888	192.0.2.40
889	192.0.2.50
890	192.0.2.60
891	192.0.2.70
892	192.0.2.80
893	192.0.2.90
894
895--- 192.0.2.2 ping statistics ---
8961 packets transmitted, 1 packets received, 0.0% packet loss
897round-trip min/avg/max/stddev = /// ms
898""",
899                "stderr": "",
900                "redacted": True,
901            },
902            id="_0_0_opts_SSRR",
903        ),
904        pytest.param(
905            {
906                "src": "192.0.2.1",
907                "dst": "192.0.2.2",
908                "icmp_type": 0,
909                "icmp_code": 0,
910                "opts": "SSRR-trunc",
911            },
912            {
913                "returncode": 0,
914                "stdout": """\
915PING 192.0.2.2 (192.0.2.2): 56 data bytes
91664 bytes from: icmp_seq=0 ttl= time= ms
917SSRR: 	(truncated route)
918
919
920--- 192.0.2.2 ping statistics ---
9211 packets transmitted, 1 packets received, 0.0% packet loss
922round-trip min/avg/max/stddev = /// ms
923""",
924                "stderr": "",
925                "redacted": True,
926            },
927            id="_0_0_opts_SSRR_trunc",
928        ),
929        pytest.param(
930            {
931                "src": "192.0.2.1",
932                "dst": "192.0.2.2",
933                "icmp_type": 0,
934                "icmp_code": 0,
935                "opts": "RR",
936            },
937            {
938                "returncode": 0,
939                "stdout": """\
940PING 192.0.2.2 (192.0.2.2): 56 data bytes
94164 bytes from: icmp_seq=0 ttl= time= ms
942RR: 	192.0.2.10
943	192.0.2.20
944	192.0.2.30
945	192.0.2.40
946	192.0.2.50
947	192.0.2.60
948	192.0.2.70
949	192.0.2.80
950	192.0.2.90
951
952--- 192.0.2.2 ping statistics ---
9531 packets transmitted, 1 packets received, 0.0% packet loss
954round-trip min/avg/max/stddev = /// ms
955""",
956                "stderr": "",
957                "redacted": True,
958            },
959            id="_0_0_opts_RR",
960        ),
961        pytest.param(
962            {
963                "src": "192.0.2.1",
964                "dst": "192.0.2.2",
965                "icmp_type": 0,
966                "icmp_code": 0,
967                "opts": "RR-same",
968            },
969            {
970                "returncode": 0,
971                "stdout": """\
972PING 192.0.2.2 (192.0.2.2): 56 data bytes
97364 bytes from: icmp_seq=0 ttl= time= ms	(same route)
974
975--- 192.0.2.2 ping statistics ---
9761 packets transmitted, 1 packets received, 0.0% packet loss
977round-trip min/avg/max/stddev = /// ms
978""",
979                "stderr": "",
980                "redacted": True,
981            },
982            id="_0_0_opts_RR_same",
983        ),
984        pytest.param(
985            {
986                "src": "192.0.2.1",
987                "dst": "192.0.2.2",
988                "icmp_type": 0,
989                "icmp_code": 0,
990                "opts": "RR-trunc",
991            },
992            {
993                "returncode": 0,
994                "stdout": """\
995PING 192.0.2.2 (192.0.2.2): 56 data bytes
99664 bytes from: icmp_seq=0 ttl= time= ms
997RR: 	(truncated route)
998
999--- 192.0.2.2 ping statistics ---
10001 packets transmitted, 1 packets received, 0.0% packet loss
1001round-trip min/avg/max/stddev = /// ms
1002""",
1003                "stderr": "",
1004                "redacted": True,
1005            },
1006            id="_0_0_opts_RR_trunc",
1007        ),
1008        pytest.param(
1009            {
1010                "src": "192.0.2.1",
1011                "dst": "192.0.2.2",
1012                "icmp_type": 0,
1013                "icmp_code": 0,
1014                "opts": "NOP",
1015            },
1016            {
1017                "returncode": 0,
1018                "stdout": """\
1019PING 192.0.2.2 (192.0.2.2): 56 data bytes
102064 bytes from: icmp_seq=0 ttl= time= ms
1021wrong total length 88 instead of 84
1022NOP
1023
1024--- 192.0.2.2 ping statistics ---
10251 packets transmitted, 1 packets received, 0.0% packet loss
1026round-trip min/avg/max/stddev = /// ms
1027""",
1028                "stderr": "",
1029                "redacted": True,
1030            },
1031            id="_0_0_opts_NOP",
1032        ),
1033        pytest.param(
1034            {
1035                "src": "192.0.2.1",
1036                "dst": "192.0.2.2",
1037                "icmp_type": 0,
1038                "icmp_code": 0,
1039                "opts": "NOP-40",
1040            },
1041            {
1042                "returncode": 0,
1043                "stdout": """\
1044PING 192.0.2.2 (192.0.2.2): 56 data bytes
104564 bytes from: icmp_seq=0 ttl= time= ms
1046wrong total length 124 instead of 84
1047NOP
1048NOP
1049NOP
1050NOP
1051NOP
1052NOP
1053NOP
1054NOP
1055NOP
1056NOP
1057NOP
1058NOP
1059NOP
1060NOP
1061NOP
1062NOP
1063NOP
1064NOP
1065NOP
1066NOP
1067NOP
1068NOP
1069NOP
1070NOP
1071NOP
1072NOP
1073NOP
1074NOP
1075NOP
1076NOP
1077NOP
1078NOP
1079NOP
1080NOP
1081NOP
1082NOP
1083NOP
1084NOP
1085NOP
1086NOP
1087
1088--- 192.0.2.2 ping statistics ---
10891 packets transmitted, 1 packets received, 0.0% packet loss
1090round-trip min/avg/max/stddev = /// ms
1091""",
1092                "stderr": "",
1093                "redacted": True,
1094            },
1095            id="_0_0_opts_NOP_40",
1096        ),
1097        pytest.param(
1098            {
1099                "src": "192.0.2.1",
1100                "dst": "192.0.2.2",
1101                "icmp_type": 0,
1102                "icmp_code": 0,
1103                "opts": "unk",
1104            },
1105            {
1106                "returncode": 0,
1107                "stdout": """\
1108PING 192.0.2.2 (192.0.2.2): 56 data bytes
110964 bytes from: icmp_seq=0 ttl= time= ms
1110wrong total length 88 instead of 84
1111unknown option 9f
1112
1113--- 192.0.2.2 ping statistics ---
11141 packets transmitted, 1 packets received, 0.0% packet loss
1115round-trip min/avg/max/stddev = /// ms
1116""",
1117                "stderr": "",
1118                "redacted": True,
1119            },
1120            id="_0_0_opts_unk",
1121        ),
1122        pytest.param(
1123            {
1124                "src": "192.0.2.1",
1125                "dst": "192.0.2.2",
1126                "icmp_type": 3,
1127                "icmp_code": 1,
1128                "opts": "NOP-40",
1129            },
1130            {
1131                "returncode": 2,
1132                "stdout": """\
1133PING 192.0.2.2 (192.0.2.2): 56 data bytes
1134132 bytes from 192.0.2.2: Destination Host Unreachable
1135Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
1136 4  f  00 007c 0001   0 0000  40  01 d868 192.0.2.1  192.0.2.2 01010101010101010101010101010101010101010101010101010101010101010101010101010101
1137
1138
1139--- 192.0.2.2 ping statistics ---
11401 packets transmitted, 0 packets received, 100.0% packet loss
1141""",
1142                "stderr": "",
1143                "redacted": False,
1144            },
1145            id="_3_1_opts_NOP_40",
1146        ),
1147        pytest.param(
1148            {
1149                "src": "192.0.2.1",
1150                "dst": "192.0.2.2",
1151                "icmp_type": 3,
1152                "icmp_code": 1,
1153                "flags": "DF",
1154            },
1155            {
1156                "returncode": 2,
1157                "stdout": """\
1158PING 192.0.2.2 (192.0.2.2): 56 data bytes
115992 bytes from 192.0.2.2: Destination Host Unreachable
1160Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
1161 4  5  00 0054 0001   2 0000  40  01 b6a4 192.0.2.1  192.0.2.2
1162
1163
1164--- 192.0.2.2 ping statistics ---
11651 packets transmitted, 0 packets received, 100.0% packet loss
1166""",
1167                "stderr": "",
1168                "redacted": False,
1169            },
1170            id="_3_1_flags_DF",
1171        ),
1172        pytest.param(
1173            {
1174                "src": "192.0.2.1",
1175                "dst": "192.0.2.2",
1176                "icmp_type": 3,
1177                "icmp_code": 1,
1178                "special": "tcp",
1179            },
1180            {
1181                "returncode": 2,
1182                "stdout": """\
1183PATTERN: 0x01
1184PING 192.0.2.2 (192.0.2.2): 56 data bytes
1185
1186--- 192.0.2.2 ping statistics ---
11871 packets transmitted, 0 packets received, 100.0% packet loss
1188""",
1189                "stderr": """\
1190ping: quoted data too short (40 bytes) from 192.0.2.2
1191""",
1192                "redacted": False,
1193            },
1194            id="_3_1_special_tcp",
1195        ),
1196        pytest.param(
1197            {
1198                "src": "192.0.2.1",
1199                "dst": "192.0.2.2",
1200                "icmp_type": 3,
1201                "icmp_code": 1,
1202                "special": "udp",
1203            },
1204            {
1205                "returncode": 2,
1206                "stdout": """\
1207PATTERN: 0x01
1208PING 192.0.2.2 (192.0.2.2): 56 data bytes
1209
1210--- 192.0.2.2 ping statistics ---
12111 packets transmitted, 0 packets received, 100.0% packet loss
1212""",
1213                "stderr": """\
1214ping: quoted data too short (28 bytes) from 192.0.2.2
1215""",
1216                "redacted": False,
1217            },
1218            id="_3_1_special_udp",
1219        ),
1220        pytest.param(
1221            {
1222                "src": "192.0.2.1",
1223                "dst": "192.0.2.2",
1224                "icmp_type": 3,
1225                "icmp_code": 1,
1226                "verbose": False,
1227            },
1228            {
1229                "returncode": 2,
1230                "stdout": """\
1231PING 192.0.2.2 (192.0.2.2): 56 data bytes
123292 bytes from 192.0.2.2: Destination Host Unreachable
1233Vr HL TOS  Len   ID Flg  off TTL Pro  cks      Src      Dst
1234 4  5  00 0054 0001   0 0000  40  01 f6a4 192.0.2.1  192.0.2.2
1235
1236
1237--- 192.0.2.2 ping statistics ---
12381 packets transmitted, 0 packets received, 100.0% packet loss
1239""",
1240                "stderr": "",
1241                "redacted": False,
1242            },
1243            id="_3_1_verbose_false",
1244        ),
1245        pytest.param(
1246            {
1247                "src": "192.0.2.1",
1248                "dst": "192.0.2.2",
1249                "icmp_type": 3,
1250                "icmp_code": 1,
1251                "special": "not-mine",
1252                "verbose": False,
1253            },
1254            {
1255                "returncode": 2,
1256                "stdout": """\
1257PATTERN: 0x01
1258PING 192.0.2.2 (192.0.2.2): 56 data bytes
1259
1260--- 192.0.2.2 ping statistics ---
12611 packets transmitted, 0 packets received, 100.0% packet loss
1262""",
1263                "stderr": "",
1264                "redacted": False,
1265            },
1266            id="_3_1_special_not_mine_verbose_false",
1267        ),
1268        pytest.param(
1269            {
1270                "src": "192.0.2.1",
1271                "dst": "192.0.2.2",
1272                "icmp_type": 0,
1273                "icmp_code": 0,
1274                "special": "warp",
1275            },
1276            {
1277                "returncode": 0,
1278                "stdout": """\
1279PATTERN: 0x01
1280PING 192.0.2.2 (192.0.2.2): 56 data bytes
128164 bytes from: icmp_seq=0 ttl= time= ms
1282
1283--- 192.0.2.2 ping statistics ---
12841 packets transmitted, 1 packets received, 0.0% packet loss
1285round-trip min/avg/max/stddev = /// ms
1286""",
1287                "stderr": """\
1288ping: time of day goes back (- ms), clamping time to 0
1289""",
1290                "redacted": True,
1291            },
1292            id="_0_0_special_warp",
1293        ),
1294    ]
1295
1296    @pytest.mark.parametrize("pinger_kargs, expected", pinger_testdata)
1297    @pytest.mark.require_progs(["scapy"])
1298    @pytest.mark.require_user("root")
1299    def test_pinger(self, pinger_kargs, expected):
1300        """Test ping using pinger(), a reply faker"""
1301        iface = IfaceFactory().create_iface("", "tun")[0].name
1302        ping = pinger(iface, **pinger_kargs)
1303        assert ping.returncode == expected["returncode"]
1304        if expected["redacted"]:
1305            assert redact(ping.stdout) == expected["stdout"]
1306            assert redact(ping.stderr) == expected["stderr"]
1307        else:
1308            assert ping.stdout == expected["stdout"]
1309            assert ping.stderr == expected["stderr"]
1310