#!/bin/rc # fwtest [fw] - check fw against the things that have broken before. # # Every check here is a bug that once shipped. Run it after touching # anything; it needs no network of its own and does not disturb the # machine's, because the packet checks run between two IP stacks it # makes for itself. # # Card mode is not covered: it takes the machine's card away, and a # test that can leave you with no network is a test nobody runs. rfork ne fw=$1 if(~ $#fw 0) fw=/bin/fw if(! test -x $fw){ echo fwtest: no $fw >[1=2] exit nofw } tmp=/tmp/fwtest.$pid mkdir -p $tmp nA=$tmp/nA nB=$tmp/nB mkdir -p $nA $nB mtpt=$tmp/ctl mkdir -p $mtpt # Results go to a file, not to variables: every check below runs inside # an @{} that needs its own namespace, and an assignment in there never # reaches the parent. Counting in variables silently reported one pass # out of seventeen. res=$tmp/results >$res # check fn check { if(~ $2 $3){ echo ok >> $res echo ' ok ' $1 } if not { echo FAIL >> $res echo ' FAIL ' $1 echo ' want: '$2 echo ' got: '$3 } } # Did that write succeed? Not what it said: rc reports a failed # redirect from the outer shell, so the message cannot be captured from # in here. Refused or not is the thing being tested anyway. fn wr { if(@{ echo -n $2 > $1 } >[2]/dev/null) echo ok if not echo refused } fn rd { if(@{ cat $1 >/dev/null } >[2]/dev/null) echo ok if not echo refused } echo '== rules: parsing' cat > $tmp/bad.ndb <<'!' allow=out prot=tcp ! r=`{$fw $tmp/bad.ndb >[2=1] | sed 's/.*: //' | sed 1q} check 'a mistyped attribute is fatal' 'unknown attribute' $"r cat > $tmp/empty.ndb <<'!' # nothing ! echo '== namespace mode' @{ rfork n $fw $tmp/empty.ndb >[2]/dev/null r=`{wr /net/tcp/clone 'connect 10.0.0.1!80'} check 'an empty rule set denies connect' refused $"r r=`{wr /net/udp/clone 'headers'} check 'headers is refused (it sends without connect)' refused $"r r=`{wr /net/gre/clone 'raw'} check 'gre raw is refused' refused $"r r=`{wr /net/tcp/clone 'ttl 32'} check 'ttl is allowed' ok $"r r=`{wr /net/ndb 'x'} check '/net/ndb is not writable' refused $"r r=`{wr /net/log 'tcp'} check '/net/log is not writable' refused $"r r=`{rd /net/ipifc/0/data} check 'an interface data file is not readable' refused $"r r=`{rd /net/ipifc/0/status} check 'but its status still is' ok $"r } cat > $tmp/lport.ndb <<'!' allow=in proto=tcp lport=17099 ! @{ rfork n $fw $tmp/lport.ndb >[2]/dev/null r=`{wr /net/tcp/clone 'announce 17099'} check 'announce matches lport, not port' ok $"r r=`{wr /net/tcp/clone 'announce 17098'} check 'a different port is denied' refused $"r } echo '== rules: round-trip through ctl' cat > $tmp/ip.ndb <<'!' allow=out proto=tcp ip=10.9.0.0/24 port=80 deny=* log=yes ! @{ rfork n bind -a '#I20' $nA bind -a '#I21' $nB $fw -m $mtpt $tmp/ip.ndb $nA^'!'^10.9.9.1^'!'^/24 $nB^'!'^10.9.9.2^'!'^/24 >[2]/dev/null & sleep 3 r=`{grep -c '%M' $mtpt/rules} check 'a mask prints as a mask, not %M%' 0 $"r r=`{wr $mtpt/ctl 'append deny=out proto=udp'} check 'a rule set with ip= survives a ctl edit' ok $"r r=`{grep -c . $mtpt/rules} check 'the appended rule is there' 3 $"r # two writes, one open: the fragmented case @{ echo 'allow=out proto=tcp port=80' echo 'deny=* log=yes' } > $mtpt/rules sleep 1 r=`{grep -c . $mtpt/rules} check 'a rule set written in two writes is not truncated' 2 $"r r=`{wr $mtpt/ctl 'reload '^$tmp/bad.ndb} check 'reloading a bad file is refused' refused $"r r=`{grep -c . $mtpt/rules} check 'and leaves the old rules alone' 2 $"r r=`{wr $mtpt/ctl 'delete 0'} check 'delete 0 is refused' refused $"r } echo '== packets, between two stacks' cat > $tmp/wire.ndb <<'!' allow=in proto=tcp lport=17099 ! @{ rfork n bind -a '#I22' $nA bind -a '#I23' $nB $fw -m $mtpt $tmp/wire.ndb $nA^'!'^10.9.9.1^'!'^/24 $nB^'!'^10.9.9.2^'!'^/24 >[2]/dev/null & sleep 3 r=`{cat $nA/ipifc/0/status | sed 1q | awk '{print $4}'} check 'the pkt interface does not claim a 4096 mtu' 1500 $"r @{ echo -n 'announce 17099'; sleep 25 } > $nB/tcp/clone & sleep 3 @{ echo -n 'connect 10.9.9.2!17099'; sleep 20 } > $nA/tcp/clone & sleep 6 # Timing-sensitive: the handshake has to complete through fw # before this looks. It passes when run on its own and fails # here intermittently, so a failure of this one check alone is # not evidence of a fault - check it by hand before believing it. r=`{grep -c 17099 $mtpt/flows} check 'a permitted connection crosses, and is tracked' 1 $"r # one rule, both directions: state, not a second rule r=`{grep -c . $mtpt/rules} check 'it took one rule to do that' 1 $"r echo -n 'prepend deny=in proto=tcp lport=17099' > $mtpt/ctl sleep 1 r=`{grep -c 17099 $mtpt/flows} check 'blocking a port drops the live connection' 0 $"r } echo npass=`{grep -c '^ok' $res} nfail=`{grep -c '^FAIL' $res} echo $"npass' passed, '$"nfail' failed' rm -rf $tmp if(! ~ $"nfail 0) exit failed exit ''