summaryrefslogtreecommitdiff
path: root/fw
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2026-08-18 19:40:14 -0400
committerCalvin Morrison <calvin@pobox.com>2026-08-18 19:40:14 -0400
commit2e39d35729fba0faae026213df7b62ebee0f573f (patch)
tree1c30aa7729dc450494015e415762a127425bfc8e /fw
parentb758d92ca80b25c0391dce4c7df73ef93aeeec99 (diff)
fw: a regression test for the things that have broken
Every check is a bug that once shipped, which is the only reason to have any of them. Two would have caught real ones early: a rule set containing ip= edited through ctl (the %M bug, where both paths were tested but never together), and a rule set written in two writes (each Twrite replaced the whole set). Runs against two IP stacks it makes for itself, so it needs no network and does not disturb the machine's. Card mode is deliberately not covered: it takes the card away, and a test that can leave you with no network is a test nobody runs. The harness had two bugs of its own worth recording. Counters kept in variables reported one pass out of seventeen, because every check runs inside an @{} that needs its own namespace and an assignment there never reaches the parent; results go to a file now. And a failed redirect is reported by the outer shell rather than the block, so the message cannot be captured from inside - the checks test whether a write was refused, not what it said. One check is timing-sensitive and marked as such: it passes standalone and fails here intermittently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'fw')
-rwxr-xr-xfw/test/fwtest.rc200
1 files changed, 200 insertions, 0 deletions
diff --git a/fw/test/fwtest.rc b/fw/test/fwtest.rc
new file mode 100755
index 0000000..0e947f3
--- /dev/null
+++ b/fw/test/fwtest.rc
@@ -0,0 +1,200 @@
+#!/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 <name> <expected> <got>
+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 ''