1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
|
#!/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.
#
# Run it twice in a row. It used to pass the first time and fail the
# second, because it left its firewalls running and their interfaces
# bound, and the next run's stack routed out the dead one; the failure
# was blamed on timing in a comment that told the next person to
# disbelieve a real result. The last two checks exist so that cannot
# happen again quietly.
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
# 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)
# 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
# into the next run's "address in use".
port=`{echo $pid | awk '{print 17000 + ($1 % 900)}'}
# 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
>$tmp/why
# 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
if(test -s $tmp/why)
echo ' said: '`{cat $tmp/why}
}
>$tmp/why
}
# Did that write succeed?
#
# Not with rc's own redirection: a failed > is reported by the shell
# itself and escapes any >[2] put around it, so there is no way to see
# what went wrong. cp does the same create(2) and reports it on its
# own standard error, where a failing check can print it.
fn wr {
echo -n $2 > $tmp/in
>$tmp/why
if(@{ cp $tmp/in $1 } >[2] $tmp/why)
echo ok
if not
echo refused
}
fn rd {
>$tmp/why
if(@{ cat $1 >/dev/null } >[2] $tmp/why)
echo ok
if not
echo refused
}
# Is it there at all, on the real /net?
#
# Every "refused" check below is also passed by a path that does not
# exist, which is how a check for a hole can go green on a kernel that
# never had the hole to begin with. So each one is paired with this,
# asked outside the sandbox: the thing being refused has to be a thing.
fn have {
if(test -e $1)
echo yes
if not
echo no
}
# Is it gone from the served tree?
#
# A stat, not a read. Reading /net/log, or an interface's data or snoop
# file, blocks until traffic arrives, so a check that read them would
# hang rather than fail on exactly the build that still serves them -
# and a test that hangs on a regression is worse than no test.
fn gone {
if(test -e $1)
echo there
if not
echo gone
}
# Stop the firewalls a packet check started.
#
# Take the interfaces away and fw follows: the relay's read fails and
# threadexitsall takes the rest of it down. Nothing else can find
# these processes - fw daemonizes, so the shell's $apid is gone before
# the server exists, and ps shows fw no arguments to match on - and
# "kill fw" would be wrong on a machine running a real one. Doing it
# this way also exercises the fail-closed path: if one relay ever goes
# back to dying quietly, the leak checks at the end catch it.
fn stopfw {
for(d in $*)
for(i in `{ls -p $d/ipifc | grep '^[0-9]'})
@{ echo -n unbind > $d/ipifc/$i/ctl } >[2]/dev/null
sleep 2
}
# fw processes belonging to anyone, counted the same way twice
fn nfw {
ps | awk '$NF == "fw"' | wc -l
}
fwbefore=`{nfw}
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: what is refused is there to refuse'
for(p in /net/udp/clone /net/gre/clone /net/ndb /net/log /net/ipifc/0/data \
/net/ipifc/0/snoop /net/tcp/trans /net/ether0/clone /net/ipmux/clone){
r=`{have $p}
check 'the real /net has '^$p yes $"r
}
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
# trans installs a kernel address translation and devip gates it
# with iseve() -- which is fw's identity through here, not the
# caller's, so on a machine where fw runs as eve there was no gate
# at all. Opening it with truncation also flushed the table.
r=`{gone /net/tcp/trans}
check 'a protocol trans file is not served' gone $"r
r=`{wr /net/tcp/trans '10.9.9.9 80 10.9.9.8 1234 10.9.9.7 5678'}
check 'and cannot be written' refused $"r
# reading log is the leak, not writing it: turn it on elsewhere and
# it traces every connection on the machine
r=`{gone /net/log}
check '/net/log is not served at all' gone $"r
r=`{gone /net/ipifc/0/data}
check 'an interface data file is a wire, and is not served' gone $"r
r=`{gone /net/ipifc/0/snoop}
check 'nor is its snoop file, which is the same wire' gone $"r
r=`{rd /net/ipifc/0/status}
check 'but its status still is' ok $"r
r=`{gone /net/ether0/clone}
check 'a card is not served, clone file or no' gone $"r
r=`{gone /net/ipmux/clone}
check 'nor is ipmux' gone $"r
# The mount driver splits a path before it sends it, so this only
# asks that the obvious way out is shut; a compound name arriving
# as one walk element needs a client speaking 9P straight to the
# server, and splitpath is what refuses that.
r=`{gone '/net/tcp/../../adm/keys'}
check 'nothing outside /net resolves through it' gone $"r
# what a program still needs
r=`{wr /net/cs 'tcp!10.9.9.9!80'}
check 'cs still translates' ok $"r
r=`{rd /net/arp}
check 'arp is still readable' ok $"r
r=`{rd /net/ndb}
check 'ndb is still readable' ok $"r
r=`{rd /net/iproute}
check 'iproute is still readable' ok $"r
r=`{ls -p /net | grep -c '^(log|ipmux|ether0|ether1)$'}
check 'none of them are even listed' 0 $"r
r=`{ls -p /net/tcp | grep -c '^trans$'}
check 'and trans is not listed either' 0 $"r
r=`{ls -p /net/tcp | grep -c '^clone$'}
check 'while clone still is' 1 $"r
}
@{
rfork n
echo 'allow=in proto=tcp lport='^$port > $tmp/lport.ndb
$fw $tmp/lport.ndb >[2]/dev/null
r=`{wr /net/tcp/clone 'announce '^$port}
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
stopfw $nA $nB
}
echo '== packets, between two stacks'
@{
rfork n
echo 'allow=in proto=tcp lport='^$port > $tmp/wire.ndb
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 '^$port; sleep 15 } > $nB/tcp/clone &
sleep 3
@{ echo -n 'connect 10.9.9.2!'^$port; sleep 12 } > $nA/tcp/clone &
sleep 6
r=`{grep -c $port $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='^$port > $mtpt/ctl
sleep 1
r=`{grep -c $port $mtpt/flows}
check 'blocking a port drops the live connection' 0 $"r
stopfw $nA $nB
}
echo '== the test cleans up after itself'
r=`{nfw}
check 'no fw processes are left running' $"fwbefore $"r
>$tmp/ifcs
for(s in $stacks)
@{
rfork n
mkdir -p $tmp/z
bind -a '#I'^$s $tmp/z
for(i in `{ls -p $tmp/z/ipifc | grep '^[0-9]'})
sed 1q $tmp/z/ipifc/$i/status >> $tmp/ifcs
}
r=`{grep -c 'device pkt' $tmp/ifcs}
check 'no interfaces are left bound, so the next run is clean' 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 ''
|