Index: tests/sys/netpfil/pf/Makefile =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/Makefile @@ -0,0 +1,10 @@ +# $FreeBSD$ + +TESTSDIR= ${TESTSBASE}/sys/netpfil/pf +BINDIR= ${TESTSDIR} + +ATF_TESTS_SH= pf_test + +SUBDIR+= files + +.include Index: tests/sys/netpfil/pf/files/Makefile =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/files/Makefile @@ -0,0 +1,8 @@ +# $FreeBSD$ + +TESTSDIR= ${TESTSBASE}/sys/netpfil/pf/files +BINDIR= ${TESTSDIR} + +FILES= pf_test_conf.sh scrub.py conf.py + +.include Index: tests/sys/netpfil/pf/files/conf.py =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/files/conf.py @@ -0,0 +1,10 @@ +# python2 + +# Read conf variables from pf_test_conf.sh. + +conffile = open('pf_test_conf.sh') + +for line in conffile: + # Simple test that line is of the form var=val. + if len(line.split('=', 1)) == 2: + exec(line) Index: tests/sys/netpfil/pf/files/pf_test_conf.sh =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/files/pf_test_conf.sh @@ -0,0 +1,23 @@ +# You need to set these variables for the tests to work. +# Keep the contents of the file in the form 'var=val' as below, +# as this file will be read from python as well. + +SSH='root@192.168.0.2' + +LOCAL_IF_1='tap1' +REMOTE_IF_1='vtnet1' +LOCAL_MAC_1='00:bd:6e:8c:ff:01' +REMOTE_MAC_1='00:a0:98:eb:76:05' +LOCAL_ADDR_1='192.168.1.1' +REMOTE_ADDR_1='192.168.1.2' + +LOCAL_IF_2='tap2' +REMOTE_IF_2='vtnet2' +LOCAL_MAC_2='00:bd:54:7d:98:02' +REMOTE_MAC_2='00:a0:98:8f:42:f7' +LOCAL_ADDR_2='192.168.2.1' +REMOTE_ADDR_2='192.168.2.2' + +LOCAL_IF_3='tap3' +LOCAL_ADDR_2='192.168.2.1' +LOCAL_ADDR_3='192.168.3.1' Index: tests/sys/netpfil/pf/files/scrub.py =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/files/scrub.py @@ -0,0 +1,59 @@ +# /usr/bin/env python2 + +import multiprocessing as mp +import scapy.all as sp +import conf +import time +import random +import util + +raw_500 = ('abcdefghijklmnopqrstuvwxyz' * 22)[random.randrange(26):][:500] + +ether1 = sp.Ether(src=conf.LOCAL_MAC_1, dst=conf.REMOTE_MAC_1) +ether2 = sp.Ether(src=conf.LOCAL_MAC_2, dst=conf.REMOTE_MAC_2) +ip1 = sp.IP(src=conf.LOCAL_ADDR_1, + dst=conf.LOCAL_ADDR_3, id=random.randrange(1 << 16)) +ip2 = sp.IP(src=conf.LOCAL_ADDR_2, + dst=conf.LOCAL_ADDR_3, id=random.randrange(1 << 16)) +icmp = sp.ICMP(type='echo-request', + id=random.randrange(1 << 16), seq=random.randrange(1 << 16)) + +p1 = ether1 / ip1 / icmp / raw_500 +p2 = ether2 / ip2 / icmp / raw_500 + +def sendpackets(): + time.sleep(1) + sp.sendp(sp.fragment(p1, 300), iface=conf.LOCAL_IF_1, verbose=False) + sp.sendp(sp.fragment(p2, 300), iface=conf.LOCAL_IF_2, verbose=False) + +sender = mp.Process(target=sendpackets) +sender.start() + +sniffed = [] +sp.sniff(iface=conf.LOCAL_IF_3, prn=sniffed.append, timeout=5) + +sender.join() + +success1, success2 = False, False + +defr = util.Defragmenter() +pp1, pp2 = p1.payload, p2.payload # IP layer +k1, k2 = util.pkey(pp1), util.pkey(pp2) +for p in sniffed: + pp = defr.more(p) + if pp is None: + continue + k = util.pkey(pp) + + # Success for interface 1 if packet received in 1 fragment, + # i.e. scrub active on remote side. + success1 = success1 or (k == k1 and defr.stats[k] == 1 and + str(pp.payload) == str(pp1.payload)) + + # Success for interface 2 if packet received in 2 fragments, + # i.e. no scrub on remote side. + success2 = success2 or (k == k2 and defr.stats[k] == 2 and + str(pp.payload) == str(pp2.payload)) + +if not (success1 and success2): + exit(1) Index: tests/sys/netpfil/pf/files/util.py =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/files/util.py @@ -0,0 +1,63 @@ +# python2 + +import scapy.all as sp + +def pkey(packet): + '''Packet key.''' + return (packet.src, packet.dst, packet.proto, packet.id) + +class Defragmenter(object): + def __init__(self): + self.frags = dict() + self.stats = dict() + def more(self, packet): + '''Add fragmented packet, return whole packet if complete.''' + + # Find IP layer. + p = packet + while p.name != 'NoPayload': + if p.name == 'IP': + break + p = p.payload + else: + return + + # # Return directly if not fragmented. + # if not ((p.flags & 1) or p.frag): # & 1 for MF + # return p + + # Add fragment to its packet group. + key, val = pkey(p), (p.frag, p) + if key in self.frags: + self.frags[key].append(val) + self.stats[key] += 1 + else: + self.frags[key] = [val] + self.stats[key] = 1 + frag = self.frags[key] + frag.sort() + + # Now all fragments in the group are sorted, + # go through them and connect them. + i = 0 + while i + 1 < len(frag): + f1, p1 = frag[i] + f2, p2 = frag[i + 1] + len1, len2 = len(p1.payload), len(p2.payload) + if len1 == (f2 - f1) * 8: + header1 = sp.IP(tos=p1.tos, flags=p1.flags, ttl=p1.ttl, + src=p1.src, dst=p1.dst, + proto=p1.proto, id=p1.id) + # Now copy MF flag from p2. + header1.flags = (header1.flags & ~1) | (p2.flags & 1) + p = header1 / (str(p1.payload) + str(p2.payload)) + frag[i:i + 2] = [(f1, p)] + else: + i += 1 + + # Return packet if complete. + p = frag[0][1] + isfirst, islast = (not p.frag), (not (p.flags & 1)) + if len(frag) == 1 and isfirst and islast: + del self.frags[key] + return p Index: tests/sys/netpfil/pf/pf_test.sh =================================================================== --- /dev/null +++ tests/sys/netpfil/pf/pf_test.sh @@ -0,0 +1,125 @@ +# Make will add a shebang line at the top of this file. + +# These tests connect to a remote test machine, load a rules file, +# possibly start some services, and run some tests. The tests cleanup +# the test machine in the end. +# +# SSH root access to the test machine is required for the tests to +# work. + +. "$(atf_get_srcdir)/files/pf_test_conf.sh" + +# Starts two instances of nc on the remote machine, listening on two +# different ports, of which one port is blocked-with-return by the +# remote pf. The test tries then to connect to the two instances from +# the local machine. The test succeeds if one connection succeeds but +# the other one fails. +atf_test_case block_return cleanup +block_return_head () { + atf_set descr 'Block-with-return a port and test that it is blocked.' +} +block_return_body () { + rules="block return in on $REMOTE_IF_1 proto tcp to port 50000" + atf_check ssh "$SSH" kldload -n pf + echo "$rules" | atf_check -e ignore ssh "$SSH" pfctl -ef - + atf_check daemon -p nc.50000.pid ssh "$SSH" nc -l 50000 + atf_check daemon -p nc.50001.pid ssh "$SSH" nc -l 50001 + atf_check -s exit:1 -e empty nc -z "$REMOTE_ADDR_1" 50000 + atf_check -s exit:0 -e ignore nc -z "$REMOTE_ADDR_1" 50001 +} +block_return_cleanup () { + atf_check -e ignore ssh "$SSH" pfctl -dFa + [ -e nc.50000.pid ] && kill `cat nc.50000.pid` + [ -e nc.50001.pid ] && kill `cat nc.50001.pid` +} + +atf_test_case block_drop cleanup +block_drop_head () { + atf_set descr 'Block-with-drop a port and test that it is blocked.' +} +block_drop_body () { + rules="block drop in on $REMOTE_IF_1 proto tcp to port 50000" + atf_check ssh "$SSH" kldload -n pf + echo "$rules" | atf_check -e ignore ssh "$SSH" pfctl -ef - + atf_check daemon -p nc.50000.pid ssh "$SSH" nc -l 50000 + atf_check daemon -p nc.50001.pid ssh "$SSH" nc -l 50001 + atf_check -s exit:1 -e empty nc -z -w 4 "$REMOTE_ADDR_1" 50000 + atf_check -s exit:0 -e ignore nc -z "$REMOTE_ADDR_1" 50001 +} +block_drop_cleanup () { + atf_check -e ignore ssh "$SSH" pfctl -dFa + [ -e nc.50000.pid ] && kill `cat nc.50000.pid` + [ -e nc.50001.pid ] && kill `cat nc.50001.pid` +} + +# # This test uses 2 interfaces to connect to the test machine, +# # $REMOTE_IF_1 and $REMOTE_IF_2. The test machine is doing reassembly +# # on one of the two interfaces. We send one echo request on each +# # interface of size 3000, which will be fragmented before being sent. +# # We capture the traffic on the test machine's pflog and transfer the +# # capture file to the host machine for processing. The capture file +# # should show a reassembled echo request packet on one interface and +# # the original fragmented set of packets on the other. +# atf_test_case scrub_todo cleanup +# scrub_todo_head () { +# atf_set descr 'Scrub on one of two interfaces and test difference.' +# } +# scrub_todo_body () { +# # files to be used in local directory: tempdir.var tcpdump.pid +# # files to be used in remote temporary directory: pflog.pcap +# rules="scrub in on $REMOTE_IF_1 all fragment reassemble +# pass log (all, to pflog0) on { $REMOTE_IF_1 $REMOTE_IF_2 }" +# atf_check ssh "$SSH" kldload -n pf pflog +# echo "$rules" | atf_check -e ignore ssh "$SSH" pfctl -ef - +# # TODO not sure why this doesn't work with atf_check +# #atf_check -o file:tempdir.var ssh "$SSH" mktemp -dt pf_test.tmp +# ssh "$SSH" mktemp -dt pf_test.tmp > tempdir.var +# tempdir="`cat tempdir.var`" +# atf_check daemon -p tcpdump.pid \ +# ssh "$SSH" tcpdump -U -i pflog0 -w "$tempdir/pflog.pcap" +# atf_check -o ignore ping -c1 -s3000 "$REMOTE_ADDR_1" +# atf_check -o ignore ping -c1 -s3000 "$REMOTE_ADDR_2" +# sleep 2 # wait for tcpdump to pick up everything +# kill "`cat tcpdump.pid`" +# sleep 2 # wait for tcpdump to write out everything +# atf_check scp "$SSH:$tempdir/pflog.pcap" ./ +# # TODO following will be removed when the test is complete, but +# # since processing isn't implemented yet, we just save the file +# # for now. +# atf_check cp pflog.pcap "$(atf_get_srcdir)/" +# # TODO process pflog.pcap for verification +# } +# scrub_todo_cleanup () { +# kill "`cat tcpdump.pid`" +# tempdir="`cat tempdir.var`" +# ssh "$SSH" "rm -r \"$tempdir\" ; +# pfctl -dFa" +# } + +atf_test_case scrub_forward cleanup +scrub_forward_head () { + atf_set descr 'Scrub defrag with forward on one \ +of two interfaces and test difference.' +} +scrub_forward_body () { + rules="scrub in on $REMOTE_IF_1 all fragment reassemble + pass log (all, to pflog0) on { $REMOTE_IF_1 $REMOTE_IF_2 }" + cd "$(atf_get_srcdir)" + atf_check ssh "$SSH" kldload -n pf + echo "$rules" | atf_check -e ignore ssh "$SSH" pfctl -ef - + atf_check -o ignore ssh "$SSH" sysctl net.inet.ip.forwarding=1 + cd files && + atf_check python2 scrub.py && + cd .. +} +scrub_forward_cleanup () { + ssh "$SSH" "pfctl -dFa ; + sysctl net.inet.ip.forwarding=0" +} + +atf_init_test_cases () { + atf_add_test_case block_return + atf_add_test_case block_drop + # atf_add_test_case scrub_todo + atf_add_test_case scrub_forward +}