From b83a2f275f25b1d486ba6fa49a887921f584f78c Mon Sep 17 00:00:00 2001 From: bluhm Date: Mon, 11 Jul 2016 13:15:20 +0000 Subject: [PATCH] Add regression tests for the path MTU discovery implementation in the kernel. Generate TCP and TCP6 and UDP6 packets with Scapy, check the kernel's reaction to ICMP fragmentation needed and ICMP6 packet too big. OK mpi@ --- regress/sys/netinet/pmtu/LICENSE | 13 ++ regress/sys/netinet/pmtu/Makefile | 146 +++++++++++++++++++++++ regress/sys/netinet/pmtu/README | 76 ++++++++++++ regress/sys/netinet/pmtu/tcp_connect.py | 40 +++++++ regress/sys/netinet/pmtu/tcp_connect6.py | 40 +++++++ regress/sys/netinet/pmtu/udp_echo6.py | 63 ++++++++++ 6 files changed, 378 insertions(+) create mode 100644 regress/sys/netinet/pmtu/LICENSE create mode 100644 regress/sys/netinet/pmtu/Makefile create mode 100644 regress/sys/netinet/pmtu/README create mode 100755 regress/sys/netinet/pmtu/tcp_connect.py create mode 100755 regress/sys/netinet/pmtu/tcp_connect6.py create mode 100755 regress/sys/netinet/pmtu/udp_echo6.py diff --git a/regress/sys/netinet/pmtu/LICENSE b/regress/sys/netinet/pmtu/LICENSE new file mode 100644 index 00000000000..c49166fbf4c --- /dev/null +++ b/regress/sys/netinet/pmtu/LICENSE @@ -0,0 +1,13 @@ +# Copyright (c) 2016 Alexander Bluhm +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/regress/sys/netinet/pmtu/Makefile b/regress/sys/netinet/pmtu/Makefile new file mode 100644 index 00000000000..dfdb4f7a8c1 --- /dev/null +++ b/regress/sys/netinet/pmtu/Makefile @@ -0,0 +1,146 @@ +# $OpenBSD: Makefile,v 1.1.1.1 2016/07/11 13:15:20 bluhm Exp $ + +# The following ports must be installed: +# +# python-2.7 interpreted object-oriented programming language +# py-libdnet python interface to libdnet +# scapy powerful interactive packet manipulation in python + +# Check wether all required python packages are installed. If some +# are missing print a warning and skip the tests, but do not fail. +PYTHON_IMPORT != python2.7 -c 'from scapy.all import *' 2>&1 || true +.if ! empty(PYTHON_IMPORT) +regress: + @echo '${PYTHON_IMPORT}' + @echo install python and the scapy module for additional tests +.endif + +# This test needs a manual setup of two machines +# Set up machines: LOCAL REMOTE +# LOCAL is the machine where this makefile is running. +# REMOTE is running OpenBSD with echo and chargen server to test PMTU +# FAKE is an non existing machine in a non existing network. +# REMOTE_SSH is the hostname to log in on the REMOTE machine. + +# Configure Addresses on the machines. +# Adapt interface and addresse variables to your local setup. +# +LOCAL_IF ?= +REMOTE_SSH ?= + +LOCAL_ADDR ?= +REMOTE_ADDR ?= +FAKE_NET ?= +FAKE_NET_ADDR ?= + +LOCAL_ADDR6 ?= +REMOTE_ADDR6 ?= +FAKE_NET6 ?= +FAKE_NET_ADDR6 ?= + +.if empty (LOCAL_IF) || empty (REMOTE_SSH) || \ + empty (LOCAL_ADDR) || empty (LOCAL_ADDR6) || \ + empty (REMOTE_ADDR) || empty (REMOTE_ADDR6) || \ + empty (FAKE_NET) || empty (FAKE_NET6) || \ + empty (FAKE_NET_ADDR) || empty (FAKE_NET_ADDR6) +regress: + @echo This tests needs a remote machine to operate on + @echo LOCAL_IF REMOTE_SSH LOCAL_ADDR LOCAL_ADDR6 REMOTE_ADDR + @echo REMOTE_ADDR6 FAKE_NET FAKE_NET6 FAKE_NET_ADDR FAKE_NET_ADDR6 + @echo are empty. Fill out these variables for additional tests. +.endif + +depend: addr.py + +# Create python include file containing the addresses. +addr.py: Makefile + rm -f $@ $@.tmp + echo 'LOCAL_IF = "${LOCAL_IF}"' >>$@.tmp + echo 'LOCAL_MAC = "${LOCAL_MAC}"' >>$@.tmp + echo 'REMOTE_MAC = "${REMOTE_MAC}"' >>$@.tmp +.for var in LOCAL REMOTE FAKE_NET + echo '${var}_ADDR = "${${var}_ADDR}"' >>$@.tmp + echo '${var}_ADDR6 = "${${var}_ADDR6}"' >>$@.tmp +.endfor + echo 'FAKE_NET = "${FAKE_NET}"' >>$@.tmp + echo 'FAKE_NET6 = "${FAKE_NET6}"' >>$@.tmp + mv $@.tmp $@ + +# Set variables so that make runs with and without obj directory. +# Only do that if necessary to keep visible output short. +.if ${.CURDIR} == ${.OBJDIR} +PYTHON = python2.7 -u ./ +.else +PYTHON = PYTHONPATH=${.OBJDIR} python2.7 -u ${.CURDIR}/ +.endif + +.PHONY: clean-arp + +# Clear local and remote path mtu routes, set fake net route +reset-route: + @echo '\n======== $@ ========' + -${SUDO} route -n delete -host ${REMOTE_ADDR} + ssh -t ${REMOTE_SSH} ${SUDO} sh -c "'\ + route -n delete -inet -host ${LOCAL_ADDR};\ + route -n delete -inet -net ${FAKE_NET};\ + route -n delete -inet -host ${FAKE_NET_ADDR};\ + route -n add -inet -net ${FAKE_NET} ${LOCAL_ADDR}'" +reset-route6: + @echo '\n======== $@ ========' + -${SUDO} route -n delete -host ${REMOTE_ADDR6} + ssh -t ${REMOTE_SSH} ${SUDO} sh -c "'\ + route -n delete -inet6 -host ${LOCAL_ADDR6};\ + route -n delete -inet6 -net ${FAKE_NET6};\ + route -n delete -inet6 -host ${FAKE_NET_ADDR6};\ + route -n add -inet6 -net ${FAKE_NET6} ${LOCAL_ADDR6}'" + +# Clear host routes and ping all addresses. This ensures that +# the IP addresses are configured and all routing table are set up +# to allow bidirectional packet flow. +TARGETS += ping ping6 +run-regress-ping: reset-route + @echo '\n======== $@ ========' +.for ip in LOCAL_ADDR REMOTE_ADDR + @echo Check ping ${ip} + ping -n -c 1 ${${ip}} +.endfor +run-regress-ping6: reset-route + @echo '\n======== $@ ========' +.for ip in LOCAL_ADDR REMOTE_ADDR + @echo Check ping6 ${ip}6 + ping6 -n -c 1 ${${ip}6} +.endfor + +TARGETS += pmtu pmtu6 +run-regress-pmtu: addr.py reset-route + @echo '\n======== $@ ========' + @echo Send ICMP fragmentation needed after fake TCP connect + ${SUDO} ${PYTHON}tcp_connect.py +run-regress-pmtu6: addr.py reset-route6 + @echo '\n======== $@ ========' + @echo Send ICMP6 packet too big after fake TCP connect + ${SUDO} ${PYTHON}tcp_connect6.py + +TARGETS += udp6 +run-regress-udp6: addr.py reset-route6 + @echo '\n======== $@ ========' + @echo Send ICMP6 packet too big after UDP echo + ${SUDO} ${PYTHON}udp_echo6.py + +TARGETS += gateway6 +run-regress-gateway6: run-regress-udp6 + @echo '\n======== $@ ========' + @echo Remove gateway route of a dynamic PMTU route + ssh ${REMOTE_SSH} ${SUDO} route -n delete -inet6 -host ${LOCAL_ADDR6} + ssh ${REMOTE_SSH} route -n get -inet6 -host ${FAKE_NET_ADDR6}\ + >pmtu.route + cat pmtu.route + grep -q 'gateway: ${LOCAL_ADDR6}' pmtu.route + grep -q 'flags: ' pmtu.route + ${SUDO} ${PYTHON}udp_echo6.py + +REGRESS_TARGETS = ${TARGETS:S/^/run-regress-/} + +CLEANFILES += addr.py *.pyc *.log *.route + +.include diff --git a/regress/sys/netinet/pmtu/README b/regress/sys/netinet/pmtu/README new file mode 100644 index 00000000000..ae08e5c2cec --- /dev/null +++ b/regress/sys/netinet/pmtu/README @@ -0,0 +1,76 @@ +Regression tests for path MTU discovery implementation in the kernel. + +The test suite runs on the machine LOCAL, the kernel under test is +running on REMOTE. On LOCAL a Scapy program is simulating a +connection to REMOTE TCP chargen service. The source address is a +non existing address on FAKE_NET. The LOCAL machine acts as a +router between REMOTE and virtual FAKE_NET_ADDR and can create ICMP +packets. + +After the three-way handshake REMOTE fills the virtual TCP receive +buffer of FAKE_NET_ADDR with generated chars. The data is not +acknowledged. Then LOCAL sends a fragmentation-needed ICMP packet +and expects REMOTE to retransmit the TCP data. It is checked that +the TCP packet from the REMOTE side has the MTU size that was +announced in the ICMP packet. + +The same TCP test is done with IPv6 and a packet too big ICMP6. + +An IPv6 UDP packet with 1400 octets payload is sent from FAKE_NET +to REMOTE. The echo answer triggers an ICMP6 packet too big with +1300 MTU limit from LOCAL. The response to the next UDP echo packet +has to be fragmented by REMOTE. + +After removing the gateway route of the PMTU route on REMOTE, the +first IPv6 UDP echo must have 1400 octets payload. A single ICMP6 +packet too big must change the existing PMTU route so that the next +echo is fragmented. + +EXAMPLE + +To run this test I use the following configuration files. +You should choose a different set of MAC and IP addresses. + +- My local machine where I run the regression test: + +/etc/hosts +# to login to qemu with SSH via IPv6 link-local +fe80::725f:caff:fe21:8d70%tap0 q70 + +/etc/hostname.tap0 +lladdr fe:e1:ba:d0:d5:6d up +inet 10.188.70.17 255.255.255.0 +inet6 fdd7:e83e:66bc:70:3e97:eff:fea7:9b2 + +- My qemu where the kernel under test is running + +/etc/hostname.vio0 +lladdr 70:5f:ca:21:8d:70 +inet 10.188.70.70 255.255.255.0 +inet6 fdd7:e83e:66bc:70:725f:caff:fe21:8d70 + +/etc/inetd.conf +chargen stream tcp nowait root internal +chargen stream tcp6 nowait root internal +echo dgram udp6 wait root internal + +/etc/rc.conf.local +inetd_flags= +sshd_flags= + +- My environment when executing the test + +LOCAL_IF=tap0 +LOCAL_MAC=fe:e1:ba:d0:d5:6d +REMOTE_MAC=70:5f:ca:21:8d:70 +REMOTE_SSH=q70 + +LOCAL_ADDR=10.188.70.17 +REMOTE_ADDR=10.188.70.70 +FAKE_NET=10.188.188.0/24 +FAKE_NET_ADDR=10.188.188.188 + +LOCAL_ADDR6=fdd7:e83e:66bc:70:3e97:eff:fea7:9b2 +REMOTE_ADDR6=fdd7:e83e:66bc:70:725f:caff:fe21:8d70 +FAKE_NET6=fdd7:e83e:66bc:188::/64 +FAKE_NET_ADDR6=fdd7:e83e:66bc:188::188 diff --git a/regress/sys/netinet/pmtu/tcp_connect.py b/regress/sys/netinet/pmtu/tcp_connect.py new file mode 100755 index 00000000000..09e65341f4f --- /dev/null +++ b/regress/sys/netinet/pmtu/tcp_connect.py @@ -0,0 +1,40 @@ +#!/usr/local/bin/python2.7 + +import os +from addr import * +from scapy.all import * + +ip=IP(src=FAKE_NET_ADDR, dst=REMOTE_ADDR) +port=os.getpid() & 0xffff + +print "Send SYN packet, receive SYN+ACK." +syn=TCP(sport=port, dport='chargen', seq=1, flags='S', window=(2**16)-1) +synack=sr1(ip/syn, iface=LOCAL_IF, timeout=5) + +print "Send ack packet, receive chargen data." +ack=TCP(sport=synack.dport, dport=synack.sport, seq=2, flags='A', + ack=synack.seq+1, window=(2**16)-1) +data=sr1(ip/ack, iface=LOCAL_IF, timeout=5) + +print "Fill our receive buffer." +time.sleep(1) + +print "Send ICMP fragmentation needed packet with MTU 1300." +icmp=ICMP(type="dest-unreach", code="fragmentation-needed", + nexthopmtu=1300)/data +send(IP(src=LOCAL_ADDR, dst=REMOTE_ADDR)/icmp, iface=LOCAL_IF) + +print "Path MTU discovery will resend first data with length 1300." +data=sr1(ip/ack, iface=LOCAL_IF, timeout=5) + +print "Cleanup the other's socket with a reset packet." +rst=TCP(sport=synack.dport, dport=synack.sport, seq=2, flags='AR', + ack=synack.seq+1) +send(ip/rst, iface=LOCAL_IF) + +len = data.len +print "len=%d" % len +if len != 1300: + print "ERROR: TCP data packet len is %d, expected 1300." % len + exit(1) +exit(0) diff --git a/regress/sys/netinet/pmtu/tcp_connect6.py b/regress/sys/netinet/pmtu/tcp_connect6.py new file mode 100755 index 00000000000..3ed8676876b --- /dev/null +++ b/regress/sys/netinet/pmtu/tcp_connect6.py @@ -0,0 +1,40 @@ +#!/usr/local/bin/python2.7 + +import os +from addr import * +from scapy.all import * + +e=Ether(src=LOCAL_MAC, dst=REMOTE_MAC) +ip6=IPv6(src=FAKE_NET_ADDR6, dst=REMOTE_ADDR6) +port=os.getpid() & 0xffff + +print "Send SYN packet, receive SYN+ACK." +syn=TCP(sport=port, dport='chargen', seq=1, flags='S', window=(2**16)-1) +synack=srp1(e/ip6/syn, iface=LOCAL_IF, timeout=5) + +print "Send ack packet, receive chargen data." +ack=TCP(sport=synack.dport, dport=synack.sport, seq=2, flags='A', + ack=synack.seq+1, window=(2**16)-1) +data=srp1(e/ip6/ack, iface=LOCAL_IF, timeout=5) + +print "Fill our receive buffer." +time.sleep(1) + +print "Send ICMP6 packet too big packet with MTU 1300." +icmp6=ICMPv6PacketTooBig(mtu=1300)/data.payload +sendp(e/IPv6(src=LOCAL_ADDR6, dst=REMOTE_ADDR6)/icmp6, iface=LOCAL_IF) + +print "Path MTU discovery will resend first data with length 1300." +data=srp1(e/ip6/ack, iface=LOCAL_IF, timeout=5) + +print "Cleanup the other's socket with a reset packet." +rst=TCP(sport=synack.dport, dport=synack.sport, seq=2, flags='AR', + ack=synack.seq+1) +sendp(e/ip6/rst, iface=LOCAL_IF) + +len = data.plen + len(IPv6()) +print "len=%d" % len +if len != 1300: + print "ERROR: TCP data packet len is %d, expected 1300." % len + exit(1) +exit(0) diff --git a/regress/sys/netinet/pmtu/udp_echo6.py b/regress/sys/netinet/pmtu/udp_echo6.py new file mode 100755 index 00000000000..68dff1d8479 --- /dev/null +++ b/regress/sys/netinet/pmtu/udp_echo6.py @@ -0,0 +1,63 @@ +#!/usr/local/bin/python2.7 + +import os +import string +import random +from addr import * +from scapy.all import * + +e=Ether(src=LOCAL_MAC, dst=REMOTE_MAC) +ip6=IPv6(src=FAKE_NET_ADDR6, dst=REMOTE_ADDR6) +port=os.getpid() & 0xffff + +print "Send UDP packet with 1400 octets payload, receive echo." +data=''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + + string.digits) for _ in range(1400)) +udp=UDP(sport=port, dport='echo')/data +echo=srp1(e/ip6/udp, iface=LOCAL_IF, timeout=5) + +print "Send ICMP6 packet too big packet with MTU 1300." +icmp6=ICMPv6PacketTooBig(mtu=1300)/echo.payload +sendp(e/IPv6(src=LOCAL_ADDR6, dst=REMOTE_ADDR6)/icmp6, iface=LOCAL_IF) + +print "Clear route cache at echo socket by sending from different address." +sendp(e/IPv6(src=LOCAL_ADDR6, dst=REMOTE_ADDR6)/udp, iface=LOCAL_IF) + +print "Path MTU discovery will send UDP fragment with maximum length 1300." +# srp1 cannot be used, fragment answer will not match on outgoing udp packet +if os.fork() == 0: + time.sleep(1) + sendp(e/ip6/udp, iface=LOCAL_IF) + os._exit(0) + +ans=sniff(iface=LOCAL_IF, timeout=3, filter= + "ip6 and src "+ip6.dst+" and dst "+ip6.src+" and proto ipv6-frag") + +for a in ans: + fh=a.payload.payload + if fh.offset != 0 or fh.nh != (ip6/udp).nh: + continue + uh=fh.payload + if uh.sport != udp.dport or uh.dport != udp.sport: + continue + frag=a + break +else: + print "ERROR: no matching IPv6 fragment UDP answer found" + exit(1) + +print "UDP echo has IPv6 and UDP header, so expected payload len is 1448" +elen = echo.plen + len(IPv6()) +print "elen=%d" % elen +if elen != 1448: + print "ERROR: UDP echo paylod len is %d, expected 1448." % elen + exit(1) + +print "Fragments contain multiple of 8 octets, so expected len is 1296" +flen = frag.plen + len(IPv6()) +print "flen=%d" % flen +if flen != 1296: + print "ERROR: UDP fragment len is %d, expected 1296." % flen + exit(1) + +exit(0) -- 2.20.1