summaryrefslogtreecommitdiff
path: root/fw/test/fwtest.rc
blob: 0b923e997699165faf64688c53428af73fd9667a (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
#!/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
}

# 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){
	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

	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
}

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