git: e35cfc606a29 - main - Add test cases for ping with IP options in the response

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Sun, 25 Dec 2022 22:03:14 UTC
The branch main has been updated by asomers:

URL: https://cgit.FreeBSD.org/src/commit/?id=e35cfc606a299ef40767e708362529c370f767f5

commit e35cfc606a299ef40767e708362529c370f767f5
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2022-10-29 22:03:41 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2022-12-26 05:59:58 +0000

    Add test cases for ping with IP options in the response
    
    MFC after:      1 week
    Reviewed by:    markj
    Differential Revision: https://reviews.freebsd.org/D37210
---
 sbin/ping/tests/Makefile     |  4 +++
 sbin/ping/tests/injection.py | 83 ++++++++++++++++++++++++++++++++++++++++++++
 sbin/ping/tests/ping_test.sh | 53 ++++++++++++++++++++++++++++
 3 files changed, 140 insertions(+)

diff --git a/sbin/ping/tests/Makefile b/sbin/ping/tests/Makefile
index c89d522a0dee..c6845ac57e5c 100644
--- a/sbin/ping/tests/Makefile
+++ b/sbin/ping/tests/Makefile
@@ -6,9 +6,13 @@ SRCS.in_cksum_test= in_cksum_test.c ../utils.c
 PACKAGE= tests
 
 ATF_TESTS_SH+=	ping_test
+# Exclusive because each injection test case uses the same IP addresses
+TEST_METADATA.ping_test+=	is_exclusive="true"
+
 ${PACKAGE}FILES+= ping_c1_s56_t1.out
 ${PACKAGE}FILES+= ping_6_c1_s8_t1.out
 ${PACKAGE}FILES+= ping_c1_s56_t1_S127.out
 ${PACKAGE}FILES+= ping_c1_s8_t1_S1.out
+${PACKAGE}FILES+= injection.py
 
 .include <bsd.test.mk>
diff --git a/sbin/ping/tests/injection.py b/sbin/ping/tests/injection.py
new file mode 100644
index 000000000000..d46359a0c9f7
--- /dev/null
+++ b/sbin/ping/tests/injection.py
@@ -0,0 +1,83 @@
+#! /usr/bin/env python3
+# Used to inject various malformed packets
+
+import errno
+import logging
+import subprocess
+import sys
+
+logging.getLogger("scapy").setLevel(logging.CRITICAL)
+
+from scapy.all import IP, ICMP, IPOption
+import scapy.layers.all
+from scapy.layers.inet import ICMPEcho_am
+from scapy.layers.tuntap import TunTapInterface
+
+SRC_ADDR = "192.0.2.14"
+DST_ADDR = "192.0.2.15"
+
+mode = sys.argv[1]
+ip = None
+
+# fill opts with nop (0x01)
+opts = b''
+for x in range(40):
+    opts += b'\x01'
+
+
+# Create and configure a tun interface with an RFC5737 nonrouteable address
+create_proc = subprocess.run(
+    args=["ifconfig", "tun", "create"],
+    capture_output=True,
+    check=True,
+    text=True)
+iface = create_proc.stdout.strip()
+tun = TunTapInterface(iface)
+with open("tun.txt", "w") as f:
+    f.write(iface)
+subprocess.run(["ifconfig", tun.iface, "up"])
+subprocess.run(["ifconfig", tun.iface, SRC_ADDR, DST_ADDR])
+
+ping = subprocess.Popen(
+        args=["/sbin/ping", "-v", "-c1", "-t1", DST_ADDR],
+        text=True
+)
+# Wait for /sbin/ping to ping us
+echo_req = tun.recv()
+
+# Construct the response packet
+if mode == "opts":
+    # Sending reply with IP options
+    echo_reply = IP(
+        dst=SRC_ADDR,
+        src=DST_ADDR,
+        options=IPOption(opts)
+    )/ICMP(type=0, code=0, id=echo_req.payload.id)/echo_req.payload.payload
+elif mode == "pip":
+    # packet in packet (inner has options)
+
+    inner = IP(
+        dst=SRC_ADDR,
+        src=DST_ADDR,
+        options=IPOption(opts)
+    )/ICMP(type=0, code=0, id=echo_req.payload.id)/echo_req.payload.payload
+    outer = IP(
+        dst=SRC_ADDR,
+        src=DST_ADDR
+    )/ICMP(type=3, code=1)  # host unreach
+
+    echo_reply = outer/inner
+elif mode == "reply":
+    # Sending normal echo reply
+    echo_reply = IP(
+        dst=SRC_ADDR,
+        src=DST_ADDR,
+    )/ICMP(type=0, code=0, id=echo_req.payload.id)/echo_req.payload.payload
+else:
+    print("unknown mode {}".format(mode))
+    exit(1)
+
+tun.send(echo_reply)
+outs, errs = ping.communicate()
+
+sys.exit(ping.returncode)
diff --git a/sbin/ping/tests/ping_test.sh b/sbin/ping/tests/ping_test.sh
index c79a792d0eb0..9f821ed96360 100644
--- a/sbin/ping/tests/ping_test.sh
+++ b/sbin/ping/tests/ping_test.sh
@@ -153,6 +153,56 @@ ping6_46_body()
 	    ping6 -4 -6 localhost
 }
 
+atf_test_case "inject_opts" "cleanup"
+inject_opts_head()
+{
+	atf_set "descr" "Inject an ECHO REPLY with IP options"
+	atf_set "require.user" "root"
+	atf_set "require.progs" "python3" "scapy"
+}
+inject_opts_body()
+{
+	atf_check -s exit:0 -o match:"wrong total length" -o match:"NOP" python3 $(atf_get_srcdir)/injection.py opts
+}
+inject_opts_cleanup()
+{
+	ifconfig `cat tun.txt` destroy
+}
+
+atf_test_case "inject_pip" "cleanup"
+inject_pip_head()
+{
+	atf_set "descr" "Inject an ICMP error with a quoted packet with IP options"
+	atf_set "require.user" "root"
+	atf_set "require.progs" "python3" "scapy"
+}
+inject_pip_body()
+{
+	atf_check -s exit:2 -o match:"Destination Host Unreachable" -o not-match:"01010101" python3 $(atf_get_srcdir)/injection.py pip
+}
+inject_pip_cleanup()
+{
+	ifconfig `cat tun.txt` destroy
+}
+
+# This is redundant with the ping_ tests, but it serves to ensure that scapy.py
+# is working correctly.
+atf_test_case "inject_reply" "cleanup"
+inject_reply_head()
+{
+	atf_set "descr" "Basic ping test with packet injection"
+	atf_set "require.user" "root"
+	atf_set "require.progs" "python3" "scapy"
+}
+inject_reply_body()
+{
+	atf_check -s exit:0 -o match:"1 packets transmitted, 1 packets received" python3 $(atf_get_srcdir)/injection.py reply
+}
+inject_reply_cleanup()
+{
+	ifconfig `cat tun.txt` destroy
+}
+
 atf_init_test_cases()
 {
 	atf_add_test_case ping_c1_s56_t1
@@ -164,6 +214,9 @@ atf_init_test_cases()
 	atf_add_test_case ping6_c1t4
 	atf_add_test_case ping_46
 	atf_add_test_case ping6_46
+	atf_add_test_case inject_opts
+	atf_add_test_case inject_pip
+	atf_add_test_case inject_reply
 }
 
 check_ping_statistics()