diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-18 22:23:00 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-18 22:23:00 -0400 |
| commit | 38f289588d7f17f86a20a3372390257e3214155b (patch) | |
| tree | 3049755591374a2363bba3cf82883678101c59c7 /fw/test/fwtest.rc | |
| parent | 95cd286e5018e6d12bae869406e38397a3d069c1 (diff) | |
fw: a fragmented datagram crosses
Only the first fragment of a datagram carries the transport header, so
every later one matched no port, and a rule set written in ports --
which is every rule set in fwrules(6) -- denied it. Measured, 3000
bytes of UDP over a 1500 mtu, against a rule permitting the port:
passed 1
dropped 2
The first fragment crossed and the receiver waited for the rest until
it gave up. The alternative this replaced was worse: reading ports out
of a later fragment lets one whose payload bytes happen to look like an
open connection through, which is a firewall evasion older than most
firewalls.
So the first fragment decides and the rest of the train inherits. The
train is what the receiving stack reassembles on -- protocol,
addresses, identification -- and lasts about as long as that stack will
hold the pieces. A train whose head we never saw is judged on its
addresses alone, and so is normally denied: it is either an attack or
the tail of a datagram we already refused. Same measurement after:
passed 3
dropped 0
and one rule decision for the datagram rather than one per fragment.
IPv6 fragments live in an extension header, which fw does not walk, so
none of this reaches them; that stays in BUGS.
Four checks, three of which fail against the previous fw.c. The fourth
-- the far stack's own InDatagrams -- is read as a difference across
the exchange, not a total: an IP stack outlives the run that made it,
and reading the total made the check pass on a build that had dropped
two thirds of the datagram.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'fw/test/fwtest.rc')
| -rwxr-xr-x | fw/test/fwtest.rc | 50 |
1 files changed, 49 insertions, 1 deletions
diff --git a/fw/test/fwtest.rc b/fw/test/fwtest.rc index d4222a8..8ae3f42 100755 --- a/fw/test/fwtest.rc +++ b/fw/test/fwtest.rc @@ -46,7 +46,7 @@ mkdir -p $mtpt # The stacks the packet checks build for themselves. Fixed numbers are # fine because they are unbound again at the end of each block, and the # last check proves it. -stacks=(20 21 22 23) +stacks=(20 21 22 23 24 25) # A port of our own. Conversations outlive the run that made them - a # devip Conv is never freed - so a fixed port makes one run's leftovers @@ -396,6 +396,54 @@ echo '== packets, between two stacks' stopfw $nA $nB } +echo '== a fragmented datagram crosses' +# Only the first fragment carries the transport header, so the rest +# match no port and a rule set written in ports denied them: the first +# fragment crossed and the receiver waited for the others until it gave +# up. 3000 bytes over a 1500 mtu is three fragments. +@{ + rfork n + echo 'allow=in proto=udp lport='^$port > $tmp/frag.ndb + bind -a '#I24' $nA + bind -a '#I25' $nB + $fw -m $mtpt $tmp/frag.ndb $nA^'!'^10.9.9.1^'!'^/24 $nB^'!'^10.9.9.2^'!'^/24 >[2]/dev/null & + sleep 3 + + dd -if /dev/zero -of $tmp/big -bs 3000 -count 1 >[2]/dev/null + # a difference, not a total: an IP stack outlives the run that made + # it, and so does its count of datagrams + before=`{awk '/^InDatagrams/ {print $2}' $nB/udp/stats} + @{ + conv=`{cat /fd/0} + echo -n 'announce 10.9.9.2!'^$port >[1=0] + sleep 12 + } <>[0] $nB/udp/clone & + sleep 2 + @{ + conv=`{cat /fd/0} + echo -n 'connect 10.9.9.2!'^$port >[1=0] + dd -if $tmp/big -of $nA/udp/$conv/data -bs 3000 -count 1 >[2]/dev/null + } <>[0] $nA/udp/clone + sleep 3 + + r=`{awk '/^passed/ {print $2}' $mtpt/stats} + check 'every fragment of it is passed' 3 $"r + r=`{awk '/^dropped/ {print $2}' $mtpt/stats} + check 'and none of them dropped' 0 $"r + + # one decision, not three: the first fragment consults the rules + # and the rest of the train inherits what it decided + r=`{awk '/allow=in/ {print $1}' $mtpt/stats} + check 'the rules are consulted once for the datagram' 1 $"r + + # the far stack's own count, which does not depend on us reading it + after=`{awk '/^InDatagrams/ {print $2}' $nB/udp/stats} + r=`{echo $before $after | awk '{print $2 - $1}'} + check 'and the far side reassembles it' 1 $"r + + stopfw $nA $nB +} + echo '== fwstart' if(test -f $lib/fwstart) @{ |
