Add regression tests for the path MTU discovery implementation in
authorbluhm <bluhm@openbsd.org>
Mon, 11 Jul 2016 13:15:20 +0000 (13:15 +0000)
committerbluhm <bluhm@openbsd.org>
Mon, 11 Jul 2016 13:15:20 +0000 (13:15 +0000)
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 [new file with mode: 0644]
regress/sys/netinet/pmtu/Makefile [new file with mode: 0644]
regress/sys/netinet/pmtu/README [new file with mode: 0644]
regress/sys/netinet/pmtu/tcp_connect.py [new file with mode: 0755]
regress/sys/netinet/pmtu/tcp_connect6.py [new file with mode: 0755]
regress/sys/netinet/pmtu/udp_echo6.py [new file with mode: 0755]

diff --git a/regress/sys/netinet/pmtu/LICENSE b/regress/sys/netinet/pmtu/LICENSE
new file mode 100644 (file)
index 0000000..c49166f
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright (c) 2016 Alexander Bluhm <bluhm@openbsd.org>
+#
+# 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 (file)
index 0000000..dfdb4f7
--- /dev/null
@@ -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: <UP,GATEWAY,HOST,DYNAMIC,DONE>' pmtu.route
+       ${SUDO} ${PYTHON}udp_echo6.py
+
+REGRESS_TARGETS =      ${TARGETS:S/^/run-regress-/}
+
+CLEANFILES +=          addr.py *.pyc *.log *.route
+
+.include <bsd.regress.mk>
diff --git a/regress/sys/netinet/pmtu/README b/regress/sys/netinet/pmtu/README
new file mode 100644 (file)
index 0000000..ae08e5c
--- /dev/null
@@ -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 (executable)
index 0000000..09e6534
--- /dev/null
@@ -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 (executable)
index 0000000..3ed8676
--- /dev/null
@@ -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 (executable)
index 0000000..68dff1d
--- /dev/null
@@ -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)