summaryrefslogtreecommitdiff
path: root/fw/test/fwtest.rc
blob: a7267ed049c8f2c6c5266d7fec6334df4ef37738 (plain)
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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
#!/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
}

# where the source is, for the build check; skipped if it is not here
src=$2
if(~ $#src 0)
	src=/sys/src/cmd/fw

# where the scripts are, likewise
lib=$3
if(~ $#lib 0)
	lib=/lib/fw

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 24 25 26 27 28)

# 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 '== it builds without complaint'
# A warning is a finding nobody had to look for.  Two stood in fw.c for
# as long as the program has existed, which is how long it has been
# since anyone read the output of mk.
if(test -d $src){
	if(@{ cd $src && mk clean && mk all } >$tmp/build >[2=1])
		r=ok
	if not
		r=failed
	check 'the source still builds' ok $"r
	grep warning $tmp/build > $tmp/why
	r=`{grep -c warning $tmp/build}
	check 'and the compiler has nothing to say about it' 0 $"r
}
if not
	echo '	skip  no '^$src^', not checking the build'

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 '== a peer is checked when there is one to check'
# An announce names no peer - nobody has called yet - so a rule naming
# one was skipped, and "deny=in ip=..." did nothing here while doing
# something at the packet layer.  Both rule sets below permit the
# announce by port; only the second permits the caller.  The machine's
# own address stands in for a peer, since it has no loopback.
myip=`{awk '/4u$/ {print $1}' /net/ipselftab | sed 1q}
if(~ $#myip 0)
	echo '	skip  no IPv4 address on this machine to call'
if not {
	{
		echo 'deny=in		proto=tcp	ip='^$myip^'	log=yes'
		echo 'allow=in		proto=tcp	lport='^$port
		echo 'allow=out		proto=tcp	ip='^$myip^'	port='^$port
	} > $tmp/peerno.ndb
	{
		echo 'allow=in		proto=tcp	lport='^$port^'	log=yes'
		echo 'allow=out		proto=tcp	ip='^$myip^'	port='^$port
	} > $tmp/peeryes.ndb

	# listentest <rules>: announce, have somebody call, and let fw
	# decide.  What it decided is in the log; the announce is logged
	# too, so the checks look for "listen" and not for the verb alone.
	fn listentest {
		@{
			rfork n
			$fw $1 >[2]/dev/null
			@{
				conv=`{cat /fd/0}
				echo -n 'announce '^$myip^'!'^$port >[1=0]
				@{ cat /net/tcp/$conv/listen } >/dev/null >[2]/dev/null
			} <>[0] /net/tcp/clone >/dev/null &
			lpid=$apid
			sleep 2
			wr /net/tcp/clone 'connect '^$myip^'!'^$port >/dev/null
			sleep 3
			@{ echo kill > /proc/$lpid/note } >[2]/dev/null
		}
	}

	# differences, not totals: the caller's port is ephemeral, so there
	# is nothing in the line that belongs to this run, and /sys/log/fw
	# keeps what earlier runs put there
	before=`{grep -c 'deny tcp listen '^$myip /sys/log/fw}
	listentest $tmp/peerno.ndb
	sleep 1
	after=`{grep -c 'deny tcp listen '^$myip /sys/log/fw}
	r=`{echo $before $after | awk '{print $2 - $1}'}
	check 'a rule naming the caller refuses the connection' 1 $"r

	before=`{grep -c 'allow tcp listen '^$myip /sys/log/fw}
	listentest $tmp/peeryes.ndb
	sleep 1
	after=`{grep -c 'allow tcp listen '^$myip /sys/log/fw}
	r=`{echo $before $after | awk '{print $2 - $1}'}
	check 'and one permitting it lets the program have it' 1 $"r
}

echo '== logging and accounting'
# /sys/log/fw is fw's only durable output.  syslog(2) does not create
# it, so make it if it is not there and take it away again if we did.
madelog=no
if(! test -f /sys/log/fw){
	>/sys/log/fw
	chmod 666 /sys/log/fw
	madelog=yes
}
logdeny=`{grep -c 'deny tcp connect 127.0.0.2!'^$port^': denied by rule 1' /sys/log/fw}
logallow=`{grep -c 'allow tcp connect 127.0.0.1!'^$port /sys/log/fw}
@{
	rfork n
	{
		echo 'deny=out	proto=tcp	ip=127.0.0.2	log=yes'
		echo 'allow=out	proto=tcp	ip=127.0.0.1	port='^$port^'	log=yes'
	} > $tmp/log.ndb
	$fw $tmp/log.ndb >[2]/dev/null

	wr /net/tcp/clone 'connect 127.0.0.2!'^$port >/dev/null
	wr /net/tcp/clone 'connect 127.0.0.1!'^$port >/dev/null
}
sleep 1
# Differences, not totals.  This asked for a total on the grounds that
# the port belonged to this run, and the port is 17000 + pid%900 while
# the log keeps every run there has ever been: 30 entries over 900
# ports collides about two times in five, and it duly did, on the
# second run of a pair.  Nothing that reads an accumulating file may
# ask it for a total - that is the third check in this suite to learn
# it, after the fragment count and the listen log.
after=`{grep -c 'deny tcp connect 127.0.0.2!'^$port^': denied by rule 1' /sys/log/fw}
r=`{echo $logdeny $after | awk '{print $2 - $1}'}
check 'a denied connection is logged, with the rule that said so' 1 $"r
after=`{grep -c 'allow tcp connect 127.0.0.1!'^$port /sys/log/fw}
r=`{echo $logallow $after | awk '{print $2 - $1}'}
check 'and a permitted one, when the rule asks' 1 $"r
if(~ $madelog yes)
	rm -f /sys/log/fw

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

	# A rule set used to be formatted into 64K and clipped, and since
	# every ctl edit works by formatting the set out, editing the text
	# and parsing it back, editing one rule past the limit deleted
	# every rule after it.  This set is about 104K.
	awk 'BEGIN{for(i = 0; i < 2000; i++)
		printf "allow=out\tproto=tcp\tip=10.9.0.0/24\tport=%d\n", 1000+i}' \
		> $tmp/big.ndb
	r=`{wr $mtpt/ctl 'reload '^$tmp/big.ndb}
	check 'a rule set larger than 64K loads' ok $"r
	r=`{grep -c . $mtpt/rules}
	check 'and all of it comes back' 2000 $"r
	r=`{wr $mtpt/ctl 'append deny=out proto=udp'}
	check 'and survives an edit' ok $"r
	r=`{grep -c . $mtpt/rules}
	check 'with nothing lost off the end' 2001 $"r
	r=`{wr $mtpt/ctl 'save '^$tmp/saved.ndb}
	check 'and saves' ok $"r
	r=`{grep -c . $tmp/saved.ndb}
	check 'all of itself' 2001 $"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

	# A rule edit rebuilds the set, so every count starts at zero; what
	# runs next is revalidate, re-checking the live flow against the new
	# rules.  That is not traffic.  Counting it made "how often has this
	# rule decided something" answer a different question after every
	# edit.
	echo -n 'append deny=out proto=udp' > $mtpt/ctl
	sleep 1
	r=`{awk '/allow=in/ {print $1}' $mtpt/stats}
	check 'rechecking flows after an edit is not a decision' 0 $"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 '== 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 '== a card'
# Card mode has never been tested, because taking the machine's card
# away is how you end up with no network.  A second card that nothing
# is using can be taken safely: bound into a stack of our own, given an
# address there, and handed to fw.  Everything up to the wire is then
# real - reading the address off the interface, taking the card,
# putting a pkt interface in its place, restoring the route.
@{
	rfork n
	bind -a '#l1' /net >[2]/dev/null
	spare=no
	if(test -e /net/ether1)
	if(~ `{grep -c ether1 /net/ipifc/*/status >[2]/dev/null | awk '{n += $1} END {print n+0}'} 0)
		spare=yes
	if(~ $spare no)
		echo '	skip  no spare card to take'
	if not {
		mkdir -p $tmp/nC
		bind -a '#I26' $tmp/nC
		@{
			conv=`{cat /fd/0}
			echo -n 'bind ether /net/ether1' >[1=0]
			echo -n 'add 10.9.9.1 255.255.255.0' >[1=0]
		} <>[0] $tmp/nC/ipifc/clone
		echo -n 'add 0.0.0.0 0.0.0.0 10.9.9.254' > $tmp/nC/iproute

		# no -a and no -g: both come off the interface it is taking
		# over, which is the whole point of not having to repeat them
		$fw -n $tmp/nC -e /net/ether1 -m $mtpt $tmp/empty.ndb >[2] $tmp/carderr
		sleep 2

		r=`{grep -c '/net/ether1 has 10.9.9.1/120, gateway 10.9.9.254' $tmp/carderr}
		check 'the address and gateway are read off the card' 1 $"r
		r=`{grep -c 'took /net/ether1 away' $tmp/carderr}
		check 'and the card is taken from the stack' 1 $"r

		r=`{sed 1q $tmp/nC/ipifc/0/status | awk '{print $2}'}
		check 'what the stack has now is a pkt interface' pkt0 $"r
		r=`{sed 1q $tmp/nC/ipifc/0/status | awk '{print $4}'}
		check 'at the mtu of the card it replaced' 1500 $"r
		r=`{sed -n 2p $tmp/nC/ipifc/0/status | awk '{print $1}'}
		check 'keeping the address' 10.9.9.1 $"r
		r=`{awk '$1 == "0.0.0.0" {print $3}' $tmp/nC/iproute | sed 1q}
		check 'and the route that went with the card' 10.9.9.254 $"r

		# The question a firewall has to answer about its own death:
		# does the network come back open, or not at all.  A note is
		# a fair stand-in for a crash - what matters is the kernel
		# closing what fw held, which happens either way.  No other
		# fw is running here; the leak checks at the end say so.
		kill fw | rc
		sleep 3
		r=`{ps | awk '$NF == "fw"' | wc -l}
		check 'killing one part kills the firewall' 0 $"r
		r=`{awk '/4u$/ {print $1}' $tmp/nC/ipselftab}
		check 'a dead fw leaves the stack no address' '' $"r
		r=`{awk '$1 == "0.0.0.0" {print $3}' $tmp/nC/iproute | sed 1q}
		check 'and no route: the network is down, not open' '' $"r
		# "device" with nothing after it: the pkt interface is gone
		# and the card was never given back
		r=`{sed 1q $tmp/nC/ipifc/0/status | awk '{print $2}'}
		check 'and nothing bound to hand the card back' maxtu $"r

		# recovery, without ever passing through open.  The dead fw
		# also left its control filesystem mounted, and a corpse of a
		# mount fails every operation on it - including the check fw
		# makes before touching a card.  So a restart has that to get
		# past too.
		@{ $fw -n $tmp/nC -e /net/ether1 -m $mtpt $tmp/empty.ndb } >[2]/dev/null
		if(~ $status '')
			r=started
		if not
			r=refused
		check 'it will not restart on a guess' refused $"r

		$fw -n $tmp/nC -e /net/ether1 -a 10.9.9.1/24 -g 10.9.9.254 \
			-m $mtpt $tmp/empty.ndb >[2] $tmp/rerr
		sleep 2
		r=`{grep -c 'cleared '^$mtpt $tmp/rerr}
		check 'the control mount the dead one left is cleared away' 1 $"r
		r=`{awk '/4u$/ {print $1}' $tmp/nC/ipselftab}
		check 'but does when told the address the card no longer has' 10.9.9.1 $"r
		r=`{awk '$1 == "0.0.0.0" {print $3}' $tmp/nC/iproute | sed 1q}
		check 'route and all' 10.9.9.254 $"r

		stopfw $tmp/nC
	}
}

echo '== a dead firewall leaves a sandbox with no network either'
@{
	rfork n
	$fw $tmp/empty.ndb >[2]/dev/null
	# the drop is per process group and this block has its own, so it
	# reaches this shell and its children and nothing else
	echo 'chdev' '&~' 'Iluσ' > /dev/drivers
	kill fw | rc
	sleep 3
	if(@{ ls /net/tcp/clone } >/dev/null >[2]/dev/null)
		r=there
	if not
		r=gone
	check 'the filtered /net dies with it' gone $"r
	if(@{ bind -a '#I' /net } >[2]/dev/null)
		r=allowed
	if not
		r=refused
	check 'and the real one cannot be bound back in its place' refused $"r
}

echo '== supervision'
# fw daemonizes, like every other Plan 9 file server: the process you
# exec posts to /srv, forks the server and exits.  A supervisor watches
# the /srv name instead of the pid, so the name has to mean what it
# says - present exactly while the firewall is filtering.
@{
	rfork n
	mkdir -p $tmp/nD $tmp/nE $tmp/sctl
	bind -a '#I27' $tmp/nD
	bind -a '#I28' $tmp/nE
	$fw -s fw.test.$pid -m $tmp/sctl $tmp/empty.ndb \
		$tmp/nD^'!'^10.9.9.1^'!'^/24 $tmp/nE^'!'^10.9.9.2^'!'^/24 >[2]/dev/null
	check 'the process that starts it exits, as a file server does' '' $"status
	sleep 2

	r=`{if(test -e /srv/fw.test.^$pid) echo yes; if not echo no}
	check 'and leaves its name in /srv' yes $"r
	r=`{ls $tmp/sctl | wc -l}
	check 'with the control files mounted' 4 $"r

	# what a supervisor would restart on.  Taking the control
	# filesystem away used to end the server proc and leave the relays
	# filtering: a firewall nobody could reach, stop, or notice.
	unmount $tmp/sctl
	rm /srv/fw.test.$pid
	sleep 3
	r=`{ps | awk '$NF == "fw"' | wc -l}
	check 'losing the control filesystem stops the whole firewall' 0 $"r
	r=`{sed 1q $tmp/nD/ipifc/0/status | awk '{print $2}'}
	check 'and the interface it made goes with it' maxtu $"r
}

echo '== fwstart'
if(test -f $lib/fwstart)
@{
	rfork n
	# /mnt as it is at boot: a real directory, with no fw in it yet.
	# -c because without it the bound directory forbids creation and
	# fwstart's mkdir fails for a reason the real /mnt would not give.
	mkdir -p $tmp/mnt
	bind -c $tmp/mnt /mnt
	>$tmp/marker
	echo 'fw=nosuchether	rules='^$tmp/empty.ndb > $tmp/fwcfg
	@{ rc $lib/fwstart $tmp/fwcfg } <$tmp/marker >$tmp/fwout >[2=1]
	sleep 1

	# mount(2) needs the mount point to exist, so the old
	# "mntgen /mnt/fw only if /mnt/fw is missing" ran it exactly when
	# it could not work, and every card's control directory failed
	r=`{if(test -d /mnt/fw/whatever) echo yes; if not echo no}
	check 'a control directory appears for each card' yes $"r

	r=`{grep -c nosuchether $tmp/fwout}
	check 'a card that is not there is reported and skipped' 1 $"r

	# anything it leaves running holds the descriptors it was started
	# with, and on a console that is the console
	r=`{cat /proc/*/fd >[2]/dev/null | grep -c $tmp/marker}
	check 'and nothing it started kept our descriptors' 0 $"r
}
if not
	echo '	skip  no '^$lib^'/fwstart'

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 ''