--- /dev/null
+# 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.
--- /dev/null
+# $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>
--- /dev/null
+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
--- /dev/null
+#!/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)
--- /dev/null
+#!/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)
--- /dev/null
+#!/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)