From nobody Mon Feb 20 10:34:57 2023 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4PKzM55TD0z3sbZZ; Mon, 20 Feb 2023 10:34:57 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "R3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 4PKzM53vmZz4QPK; Mon, 20 Feb 2023 10:34:57 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1676889297; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=rVxq6INrQXFQRWrF7pTxl4MddN4LZAejj6CUhqJKEDE=; b=Jx/1t+Exi1f9HMhg9PMllNIzmyHGFBpLRkjJ9cMa8hyNS8c5AbfnFddC5433riMimCxCPG wQq2y7B0FbRsKR7mhDQUwHuYeF0l7Et8DTszzfwfVdiLjyHu0nweKHun9C4Vx6w0EUARUC 7pqJ/QzMa45cxxFm6nUUWaW2OvK5vNU3Q3m6EbndjHcGL8ZdYAVoxgmvvhdiXyDLL4I7e8 q03K8Q5ts0utgY7tqhpROdTGzV7veuGVKAKd0iWjlav5P5Qs/fqzLtD0UHNGalBTPDmMYa 7K2xQl2MR8WtKfACwz90uqVuOBww+LYJ5lk6DypJUrumwdUCZ11tuKrVWb0tGg== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1676889297; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=rVxq6INrQXFQRWrF7pTxl4MddN4LZAejj6CUhqJKEDE=; b=yqITomicViveq+gdd6Fk067Yi7D8QQL6kLCzmoL12jKWr/AIgZOPv7utowUBgF0j/yY3pG 1Le6AIhWVxqoOv8zZTKipO1uchssmCCOFdSQJTP3wOzjmLk7qakbjrXLkE3WXDwymRrGFP 6AOwsVGyMAp0tPLkoojTMa3/8pw7YTVVG/zrhonR7Ac42uUULMA+AWZXrRHW+GRkRmsyec sh49K7+Z3Z6kDpEM1XmybahGMFWxDrjKaSzCds36gfc5eqGiOOHs1+HZQSec0Zq5mK7nev 5wXdUZ47mUGR0JvWbQ878CfqvjZv81VXDgVDDvNCtnga/Ib9YJZBKgiJyYgV5w== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1676889297; a=rsa-sha256; cv=none; b=d62cSW+BAnm2juQJ/m6TzP0iOg7i7kiiMNywj/4W9X8KYCiSvUDLBQUhQZ+5pCalg1ge1V o0m+TddzwHhNgPHyL8etyba5m4qZAjuL07TIfJOpECl5eB25xxUZavpHlaRVaZpdsCO7oK Vf7VURT5DE3/lQJUw14zwdyncFtCIZJyTMB8+6XjMY1PoQ+HU4L1hLjbokYU2ojrIOBz4B o13BhLqC5v3IuXHP3GH7fs+mNGKH8k8oZAIN/7tmR1w1cjC9QTTlG1kjiXSxx9eo1PMl1M wuJL34iVdR4kIQguShkuvKjn/7Tpowe56yUDbH+JTWCWpP+fZd/QQONhkX/uVQ== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 4PKzM52zvNzdMx; Mon, 20 Feb 2023 10:34:57 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from gitrepo.freebsd.org ([127.0.1.44]) by gitrepo.freebsd.org (8.16.1/8.16.1) with ESMTP id 31KAYv3U061764; Mon, 20 Feb 2023 10:34:57 GMT (envelope-from git@gitrepo.freebsd.org) Received: (from git@localhost) by gitrepo.freebsd.org (8.16.1/8.16.1/Submit) id 31KAYvTM061762; Mon, 20 Feb 2023 10:34:57 GMT (envelope-from git) Date: Mon, 20 Feb 2023 10:34:57 GMT Message-Id: <202302201034.31KAYvTM061762@gitrepo.freebsd.org> To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: "Alexander V. Chernikov" Subject: git: 5b8af90fe332 - main - ping: Add ATF-Python tests List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: Sender: owner-dev-commits-src-all@freebsd.org X-BeenThere: dev-commits-src-all@freebsd.org MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: melifaro X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: 5b8af90fe33274dda4c342ee7727432fdf5501d7 Auto-Submitted: auto-generated X-ThisMailContainsUnwantedMimeParts: N The branch main has been updated by melifaro: URL: https://cgit.FreeBSD.org/src/commit/?id=5b8af90fe33274dda4c342ee7727432fdf5501d7 commit 5b8af90fe33274dda4c342ee7727432fdf5501d7 Author: Jose Luis Duran AuthorDate: 2023-02-20 10:23:24 +0000 Commit: Alexander V. Chernikov CommitDate: 2023-02-20 10:34:33 +0000 ping: Add ATF-Python tests ping(8) is an old utility, which has received many changes and updates through the years. Some of these changes may have introduced small bugs, in part due to the lack of tests. Attempt to remedy the current situation by introducing a way to easily add tests. Differential Revision: https://reviews.freebsd.org/D38053 --- sbin/ping/tests/Makefile | 1 + sbin/ping/tests/test_ping.py | 940 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 941 insertions(+) diff --git a/sbin/ping/tests/Makefile b/sbin/ping/tests/Makefile index c6845ac57e5c..044687c03dc3 100644 --- a/sbin/ping/tests/Makefile +++ b/sbin/ping/tests/Makefile @@ -5,6 +5,7 @@ SRCS.in_cksum_test= in_cksum_test.c ../utils.c PACKAGE= tests +ATF_TESTS_PYTEST+= test_ping.py ATF_TESTS_SH+= ping_test # Exclusive because each injection test case uses the same IP addresses TEST_METADATA.ping_test+= is_exclusive="true" diff --git a/sbin/ping/tests/test_ping.py b/sbin/ping/tests/test_ping.py new file mode 100644 index 000000000000..913948d18b3e --- /dev/null +++ b/sbin/ping/tests/test_ping.py @@ -0,0 +1,940 @@ +import pytest + +import logging +import os +import re +import subprocess + +from atf_python.sys.net.vnet import IfaceFactory +from atf_python.sys.net.vnet import SingleVnetTestTemplate +from atf_python.sys.net.tools import ToolsHelper +from typing import List +from typing import Optional + +logging.getLogger("scapy").setLevel(logging.CRITICAL) +import scapy.all as sc + + +def build_response_packet(echo, ip, icmp, oip_ihl, special): + icmp_id_seq_types = [0, 8, 13, 14, 15, 16, 17, 18, 37, 38] + oip = echo[sc.IP] + oicmp = echo[sc.ICMP] + load = echo[sc.ICMP].payload + oip[sc.IP].remove_payload() + oicmp[sc.ICMP].remove_payload() + oicmp.type = 8 + + # As if the original IP packet had these set + oip.ihl = None + oip.len = None + oip.id = 1 + oip.flags = ip.flags + oip.chksum = None + oip.options = ip.options + + # Inner packet (oip) options + if oip_ihl: + oip.ihl = oip_ihl + + # Special options + if special == "no-payload": + load = "" + if special == "tcp": + oip.proto = "tcp" + tcp = sc.TCP(sport=1234, dport=5678) + return ip / icmp / oip / tcp + if special == "udp": + oip.proto = "udp" + udp = sc.UDP(sport=1234, dport=5678) + return ip / icmp / oip / udp + if special == "warp": + # Build a package with a timestamp of INT_MAX + # (time-warped package) + payload_no_timestamp = sc.bytes_hex(load)[16:] + load = (b"\xff" * 8) + sc.hex_bytes(payload_no_timestamp) + if special == "wrong": + # Build a package with a wrong last byte + payload_no_last_byte = sc.bytes_hex(load)[:-2] + load = (sc.hex_bytes(payload_no_last_byte)) + b"\x00" + + if icmp.type in icmp_id_seq_types: + pkt = ip / icmp / load + else: + ip.options = "" + pkt = ip / icmp / oip / oicmp / load + return pkt + + +def generate_ip_options(opts): + if not opts: + return "" + + routers = [ + "192.0.2.10", + "192.0.2.20", + "192.0.2.30", + "192.0.2.40", + "192.0.2.50", + "192.0.2.60", + "192.0.2.70", + "192.0.2.80", + "192.0.2.90", + ] + routers_zero = [0, 0, 0, 0, 0, 0, 0, 0, 0] + if opts == "EOL": + options = sc.IPOption(b"\x00") + elif opts == "NOP": + options = sc.IPOption(b"\x01") + elif opts == "NOP-40": + options = sc.IPOption(b"\x01" * 40) + elif opts == "RR": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_RR(pointer=40, routers=routers) + elif opts == "RR-same": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_RR(pointer=3, routers=routers_zero) + elif opts == "RR-trunc": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_RR(length=7, routers=routers_zero) + elif opts == "LSRR": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_LSRR(routers=routers) + elif opts == "LSRR-trunc": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_LSRR(length=3, routers=routers_zero) + elif opts == "SSRR": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_SSRR(routers=routers) + elif opts == "SSRR-trunc": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption_SSRR(length=3, routers=routers_zero) + elif opts == "unk": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption(b"\x9f") + elif opts == "unk-40": + ToolsHelper.set_sysctl("net.inet.ip.process_options", 0) + options = sc.IPOption(b"\x9f" * 40) + else: + options = "" + return options + + +def pinger( + # Required arguments + # Avoid setting defaults on these arguments, + # as we want to set them explicitly in the tests + iface: str, + /, + src: sc.scapy.fields.SourceIPField, + dst: sc.scapy.layers.inet.DestIPField, + icmp_type: sc.scapy.fields.ByteEnumField, + icmp_code: sc.scapy.fields.MultiEnumField, + # IP arguments + ihl: Optional[sc.scapy.fields.BitField] = None, + flags: Optional[sc.scapy.fields.FlagsField] = None, + opts: Optional[str] = None, + oip_ihl: Optional[sc.scapy.fields.BitField] = None, + special: Optional[str] = None, + # ICMP arguments + # Match names with + icmp_pptr: sc.scapy.fields.ByteField = 0, + icmp_gwaddr: sc.scapy.fields.IPField = "0.0.0.0", + icmp_nextmtu: sc.scapy.fields.ShortField = 0, + icmp_otime: sc.scapy.layers.inet.ICMPTimeStampField = 0, + icmp_rtime: sc.scapy.layers.inet.ICMPTimeStampField = 0, + icmp_ttime: sc.scapy.layers.inet.ICMPTimeStampField = 0, + icmp_mask: sc.scapy.fields.IPField = "0.0.0.0", + request: Optional[str] = None, + # Miscellaneous arguments + count: int = 1, + dup: bool = False, +) -> subprocess.CompletedProcess: + """P I N G E R + + Echo reply faker + + :param str iface: Interface to send packet to + :keyword src: Source packet IP + :type src: class:`scapy.fields.SourceIPField` + :keyword dst: Destination packet IP + :type dst: class:`scapy.layers.inet.DestIPField` + :keyword icmp_type: ICMP type + :type icmp_type: class:`scapy.fields.ByteEnumField` + :keyword icmp_code: ICMP code + :type icmp_code: class:`scapy.fields.MultiEnumField` + + :keyword ihl: Internet Header Length, defaults to None + :type ihl: class:`scapy.fields.BitField`, optional + :keyword flags: IP flags - one of `DF`, `MF` or `evil`, defaults to None + :type flags: class:`scapy.fields.FlagsField`, optional + :keyword opts: Include IP options - one of `EOL`, `NOP`, `NOP-40`, `unk`, + `unk-40`, `RR`, `RR-same`, `RR-trunc`, `LSRR`, `LSRR-trunc`, `SSRR` or + `SSRR-trunc`, defaults to None + :type opts: str, optional + :keyword oip_ihl: Inner packet's Internet Header Length, defaults to None + :type oip_ihl: class:`scapy.fields.BitField`, optional + :keyword special: Send a special packet - one of `no-payload`, `tcp`, + `udp`, `wrong` or `warp`, defaults to None + :type special: str, optional + :keyword icmp_pptr: ICMP pointer, defaults to 0 + :type icmp_pptr: class:`scapy.fields.ByteField` + :keyword icmp_gwaddr: ICMP gateway IP address, defaults to "0.0.0.0" + :type icmp_gwaddr: class:`scapy.fields.IPField` + :keyword icmp_nextmtu: ICMP next MTU, defaults to 0 + :type icmp_nextmtu: class:`scapy.fields.ShortField` + :keyword icmp_otime: ICMP originate timestamp, defaults to 0 + :type icmp_otime: class:`scapy.layers.inet.ICMPTimeStampField` + :keyword icmp_rtime: ICMP receive timestamp, defaults to 0 + :type icmp_rtime: class:`scapy.layers.inet.ICMPTimeStampField` + :keyword icmp_ttime: ICMP transmit timestamp, defaults to 0 + :type icmp_ttime: class:`scapy.layers.inet.ICMPTimeStampField` + :keyword icmp_mask: ICMP address mask, defaults to "0.0.0.0" + :type icmp_mask: class:`scapy.fields.IPField` + :keyword request: Request type - one of `mask` or `timestamp`, + defaults to None + :type request: str, optional + :keyword count: Number of packets to send, defaults to 1 + :type count: int + :keyword dup: Duplicate packets, defaults to `False` + :type dup: bool + + :return: A class:`subprocess.CompletedProcess` with the output from the + ping utility + :rtype: class:`subprocess.CompletedProcess` + """ + tun = sc.TunTapInterface(iface) + subprocess.run(["ifconfig", tun.iface, "up"], check=True) + subprocess.run(["ifconfig", tun.iface, src, dst], check=True) + ip_opts = generate_ip_options(opts) + ip = sc.IP(ihl=ihl, flags=flags, src=dst, dst=src, options=ip_opts) + command = [ + "/sbin/ping", + "-c", + str(count), + "-t", + str(count), + "-v", + ] + if request == "mask": + command += ["-Mm"] + if request == "timestamp": + command += ["-Mt"] + if special: + command += ["-p1"] + if opts in [ + "RR", + "RR-same", + "RR-trunc", + "LSRR", + "LSRR-trunc", + "SSRR", + "SSRR-trunc", + ]: + command += ["-R"] + command += [dst] + with subprocess.Popen( + args=command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True + ) as ping: + for dummy in range(count): + echo = tun.recv() + icmp = sc.ICMP( + type=icmp_type, + code=icmp_code, + id=echo[sc.ICMP].id, + seq=echo[sc.ICMP].seq, + ts_ori=icmp_otime, + ts_rx=icmp_rtime, + ts_tx=icmp_ttime, + gw=icmp_gwaddr, + ptr=icmp_pptr, + addr_mask=icmp_mask, + nexthopmtu=icmp_nextmtu, + ) + pkt = build_response_packet(echo, ip, icmp, oip_ihl, special) + tun.send(pkt) + if dup is True: + tun.send(pkt) + stdout, stderr = ping.communicate() + return subprocess.CompletedProcess( + ping.args, ping.returncode, stdout, stderr + ) + + +def redact(output): + """Redact some elements of ping's output""" + pattern_replacements = [ + ("localhost \([0-9]{1,3}(\.[0-9]{1,3}){3}\)", "localhost"), + ("from [0-9]{1,3}(\.[0-9]{1,3}){3}", "from"), + ("hlim=[0-9]*", "hlim="), + ("ttl=[0-9]*", "ttl="), + ("time=[0-9.-]*", "time="), + ("[0-9\.]+/[0-9.]+", "/"), + ] + for pattern, repl in pattern_replacements: + output = re.sub(pattern, repl, output) + return output + + +class TestPing(SingleVnetTestTemplate): + IPV6_PREFIXES: List[str] = ["2001:db8::1/64"] + IPV4_PREFIXES: List[str] = ["192.0.2.1/24"] + + # Each param in testdata contains a dictionary with the command, + # and the expected outcome (returncode, redacted stdout, and stderr) + testdata = [ + pytest.param( + { + "args": "ping -4 -c1 -s56 -t1 localhost", + "returncode": 0, + "stdout": """\ +PING localhost: 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms + +--- localhost ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_4_c1_s56_t1_localhost", + ), + pytest.param( + { + "args": "ping -6 -c1 -s8 -t1 localhost", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) ::1 --> ::1 +16 bytes from ::1, icmp_seq=0 hlim= time= ms + +--- localhost ping6 statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_6_c1_s8_t1_localhost", + ), + pytest.param( + { + "args": "ping -A -c1 192.0.2.1", + "returncode": 0, + "stdout": """\ +PING 192.0.2.1 (192.0.2.1): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms + +--- 192.0.2.1 ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_A_c1_192_0_2_1", + ), + pytest.param( + { + "args": "ping -A -c1 192.0.2.2", + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_A_c1_192_0_2_2", + ), + pytest.param( + { + "args": "ping -A -c1 2001:db8::1", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1 +16 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms + +--- 2001:db8::1 ping6 statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_A_c1_2001_db8__1", + ), + pytest.param( + { + "args": "ping -A -c1 2001:db8::2", + "returncode": 2, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2 + +--- 2001:db8::2 ping6 statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_A_c1_2001_db8__2", + ), + pytest.param( + { + "args": "ping -A -c3 192.0.2.1", + "returncode": 0, + "stdout": """\ +PING 192.0.2.1 (192.0.2.1): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms +64 bytes from: icmp_seq=1 ttl= time= ms +64 bytes from: icmp_seq=2 ttl= time= ms + +--- 192.0.2.1 ping statistics --- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_A_3_192_0.2.1", + ), + pytest.param( + { + "args": "ping -A -c3 192.0.2.2", + "returncode": 2, + "stdout": """\ +\x07\x07PING 192.0.2.2 (192.0.2.2): 56 data bytes + +--- 192.0.2.2 ping statistics --- +3 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_A_c3_192_0_2_2", + ), + pytest.param( + { + "args": "ping -A -c3 2001:db8::1", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1 +16 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms +16 bytes from 2001:db8::1, icmp_seq=1 hlim= time= ms +16 bytes from 2001:db8::1, icmp_seq=2 hlim= time= ms + +--- 2001:db8::1 ping6 statistics --- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_A_c3_2001_db8__1", + ), + pytest.param( + { + "args": "ping -A -c3 2001:db8::2", + "returncode": 2, + "stdout": """\ +\x07\x07PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2 + +--- 2001:db8::2 ping6 statistics --- +3 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_A_c3_2001_db8__2", + ), + pytest.param( + { + "args": "ping -c1 192.0.2.1", + "returncode": 0, + "stdout": """\ +PING 192.0.2.1 (192.0.2.1): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms + +--- 192.0.2.1 ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_c1_192_0_2_1", + ), + pytest.param( + { + "args": "ping -c1 192.0.2.2", + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_c1_192_0_2_2", + ), + pytest.param( + { + "args": "ping -c1 2001:db8::1", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1 +16 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms + +--- 2001:db8::1 ping6 statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_c1_2001_db8__1", + ), + pytest.param( + { + "args": "ping -c1 2001:db8::2", + "returncode": 2, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2 + +--- 2001:db8::2 ping6 statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_c1_2001_db8__2", + ), + pytest.param( + { + "args": "ping -c1 -S127.0.0.1 -s56 -t1 localhost", + "returncode": 0, + "stdout": """\ +PING localhost from: 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms + +--- localhost ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_c1_S127_0_0_1_s56_t1_localhost", + ), + pytest.param( + { + "args": "ping -c1 -S::1 -s8 -t1 localhost", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) ::1 --> ::1 +16 bytes from ::1, icmp_seq=0 hlim= time= ms + +--- localhost ping6 statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_c1_S__1_s8_t1_localhost", + ), + pytest.param( + { + "args": "ping -c3 192.0.2.1", + "returncode": 0, + "stdout": """\ +PING 192.0.2.1 (192.0.2.1): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms +64 bytes from: icmp_seq=1 ttl= time= ms +64 bytes from: icmp_seq=2 ttl= time= ms + +--- 192.0.2.1 ping statistics --- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_c3_192_0_2_1", + ), + pytest.param( + { + "args": "ping -c3 192.0.2.2", + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes + +--- 192.0.2.2 ping statistics --- +3 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_c3_192_0_2_2", + ), + pytest.param( + { + "args": "ping -c3 2001:db8::1", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1 +16 bytes from 2001:db8::1, icmp_seq=0 hlim= time= ms +16 bytes from 2001:db8::1, icmp_seq=1 hlim= time= ms +16 bytes from 2001:db8::1, icmp_seq=2 hlim= time= ms + +--- 2001:db8::1 ping6 statistics --- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_c3_2001_db8__1", + ), + pytest.param( + { + "args": "ping -c3 2001:db8::2", + "returncode": 2, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2 + +--- 2001:db8::2 ping6 statistics --- +3 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_c3_2001_db8__2", + ), + pytest.param( + { + "args": "ping -q -c1 192.0.2.1", + "returncode": 0, + "stdout": """\ +PING 192.0.2.1 (192.0.2.1): 56 data bytes + +--- 192.0.2.1 ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_q_c1_192_0_2_1", + ), + pytest.param( + { + "args": "ping -q -c1 192.0.2.2", + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_q_c1_192_0_2_2", + ), + pytest.param( + { + "args": "ping -q -c1 2001:db8::1", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1 + +--- 2001:db8::1 ping6 statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_q_c1_2001_db8__1", + ), + pytest.param( + { + "args": "ping -q -c1 2001:db8::2", + "returncode": 2, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2 + +--- 2001:db8::2 ping6 statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_q_c1_2001_db8__2", + ), + pytest.param( + { + "args": "ping -q -c3 192.0.2.1", + "returncode": 0, + "stdout": """\ +PING 192.0.2.1 (192.0.2.1): 56 data bytes + +--- 192.0.2.1 ping statistics --- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + }, + id="_q_c3_192_0_2_1", + ), + pytest.param( + { + "args": "ping -q -c3 192.0.2.2", + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes + +--- 192.0.2.2 ping statistics --- +3 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_q_c3_192_0_2_2", + ), + pytest.param( + { + "args": "ping -q -c3 2001:db8::1", + "returncode": 0, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::1 + +--- 2001:db8::1 ping6 statistics --- +3 packets transmitted, 3 packets received, 0.0% packet loss +round-trip min/avg/max/std-dev = /// ms +""", + "stderr": "", + }, + id="_q_c3_2001_db8__1", + ), + pytest.param( + { + "args": "ping -q -c3 2001:db8::2", + "returncode": 2, + "stdout": """\ +PING6(56=40+8+8 bytes) 2001:db8::1 --> 2001:db8::2 + +--- 2001:db8::2 ping6 statistics --- +3 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + }, + id="_q_c3_2001_db8__2", + ), + ] + + @pytest.mark.parametrize("expected", testdata) + def test_ping(self, expected): + """Test ping""" + ping = subprocess.run( + expected["args"].split(), + capture_output=True, + timeout=15, + text=True, + ) + assert ping.returncode == expected["returncode"] + assert redact(ping.stdout) == expected["stdout"] + assert ping.stderr == expected["stderr"] + + # Each param in ping46_testdata contains a dictionary with the arguments + # and the expected outcome (returncode, redacted stdout, and stderr) + # common to `ping -4` and `ping -6` + ping46_testdata = [ + pytest.param( + { + "args": "-Wx localhost", + "returncode": os.EX_USAGE, + "stdout": "", + "stderr": "ping: invalid timing interval: `x'\n", + }, + marks=pytest.mark.skip("XXX currently failing"), + id="_Wx_localhost", + ), + ] + + @pytest.mark.parametrize("expected", ping46_testdata) + def test_ping_46(self, expected): + """Test ping -4/ping -6""" + for version in [4, 6]: + ping = subprocess.run( + ["ping", f"-{version}"] + expected["args"].split(), + capture_output=True, + timeout=15, + text=True, + ) + assert ping.returncode == expected["returncode"] + assert redact(ping.stdout) == expected["stdout"] + assert ping.stderr == expected["stderr"] + + # Each param in pinger_testdata contains a dictionary with the keywords to + # `pinger()` and a dictionary with the expected outcome (returncode, + # stdout, stderr, and if ping's output is redacted) + pinger_testdata = [ + pytest.param( + { + "src": "192.0.2.1", + "dst": "192.0.2.2", + "icmp_type": 0, + "icmp_code": 0, + }, + { + "returncode": 0, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + "redacted": True, + }, + id="_0_0", + ), + pytest.param( + { + "src": "192.0.2.1", + "dst": "192.0.2.2", + "icmp_type": 0, + "icmp_code": 0, + "opts": "NOP-40", + }, + { + "returncode": 0, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms +wrong total length 124 instead of 84 +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP +NOP + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + "redacted": True, + }, + id="_0_0_opts_NOP_40", + ), + pytest.param( + { + "src": "192.0.2.1", + "dst": "192.0.2.2", + "icmp_type": 0, + "icmp_code": 0, + "opts": "unk", + }, + { + "returncode": 0, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes +64 bytes from: icmp_seq=0 ttl= time= ms +wrong total length 88 instead of 84 +unknown option 9f + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 1 packets received, 0.0% packet loss +round-trip min/avg/max/stddev = /// ms +""", + "stderr": "", + "redacted": True, + }, + marks=pytest.mark.skip("XXX currently failing"), + id="_0_0_opts_unk", + ), + pytest.param( + { + "src": "192.0.2.1", + "dst": "192.0.2.2", + "icmp_type": 3, + "icmp_code": 1, + "opts": "NOP-40", + }, + { + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes +132 bytes from 192.0.2.2: Destination Host Unreachable +Vr HL TOS Len ID Flg off TTL Pro cks Src Dst + 4 f 00 007c 0001 0 0000 40 01 d868 192.0.2.1 192.0.2.2 01010101010101010101010101010101010101010101010101010101010101010101010101010101 + + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + "redacted": False, + }, + marks=pytest.mark.skip("XXX currently failing"), + id="_3_1_opts_NOP_40", + ), + pytest.param( + { + "src": "192.0.2.1", + "dst": "192.0.2.2", + "icmp_type": 3, + "icmp_code": 1, + "flags": "DF", + }, + { + "returncode": 2, + "stdout": """\ +PING 192.0.2.2 (192.0.2.2): 56 data bytes +92 bytes from 192.0.2.2: Destination Host Unreachable +Vr HL TOS Len ID Flg off TTL Pro cks Src Dst + 4 5 00 0054 0001 2 0000 40 01 b6a4 192.0.2.1 192.0.2.2 + + +--- 192.0.2.2 ping statistics --- +1 packets transmitted, 0 packets received, 100.0% packet loss +""", + "stderr": "", + "redacted": False, + }, + marks=pytest.mark.skip("XXX currently failing"), + id="_3_1_flags_DF", + ), + ] + + @pytest.mark.parametrize("pinger_kargs, expected", pinger_testdata) + @pytest.mark.require_progs(["scapy"]) + @pytest.mark.require_user("root") + def test_pinger(self, pinger_kargs, expected): + """Test ping using pinger(), a reply faker""" + iface = IfaceFactory().create_iface("", "tun")[0].name + ping = pinger(iface, **pinger_kargs) + assert ping.returncode == expected["returncode"] + if expected["redacted"]: + assert redact(ping.stdout) == expected["stdout"] + else: + assert ping.stdout == expected["stdout"] + assert ping.stderr == expected["stderr"]