xref: /freebsd/tests/sys/netpfil/pf/ether.sh (revision c03c5b1c)
1# $FreeBSD$
2#
3# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
4#
5# Copyright © 2021. Rubicon Communications, LLC (Netgate). All Rights Reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26# SUCH DAMAGE.
27
28. $(atf_get_srcdir)/utils.subr
29
30atf_test_case "mac" "cleanup"
31mac_head()
32{
33	atf_set descr 'Test MAC address filtering'
34	atf_set require.user root
35}
36
37mac_body()
38{
39	pft_init
40
41	epair=$(vnet_mkepair)
42	epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }')
43
44	ifconfig ${epair}a 192.0.2.1/24 up
45
46	vnet_mkjail alcatraz ${epair}b
47	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
48
49	pft_set_rules alcatraz \
50		"ether block from ${epair_a_mac}"
51
52	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
53
54	# Now enable. Ping should fail.
55	jexec alcatraz pfctl -e
56
57	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
58
59	# Should still fail for 'to'
60	pft_set_rules alcatraz \
61		"ether block to ${epair_a_mac}"
62	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
63
64	# Succeeds if we block a different MAC address
65	pft_set_rules alcatraz \
66		"ether block to 00:01:02:03:04:05"
67	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
68
69	# Should still fail for 'to', even if it's in a list
70	pft_set_rules alcatraz \
71		"ether block to { ${epair_a_mac}, 00:01:02:0:04:05 }"
72	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
73
74	# Now try this with an interface specified
75	pft_set_rules alcatraz \
76		"ether block on ${epair}b from ${epair_a_mac}"
77	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
78
79	# Wrong interface should not match
80	pft_set_rules alcatraz \
81		"ether block on ${epair}a from ${epair_a_mac}"
82	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
83
84	# Test negation
85	pft_set_rules alcatraz \
86		"ether block in on ${epair}b from ! ${epair_a_mac}"
87	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
88
89	pft_set_rules alcatraz \
90		"ether block out on ${epair}b to ! ${epair_a_mac}"
91	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
92
93	# Block everything not us
94	pft_set_rules alcatraz \
95		"ether block out on ${epair}b to { ! ${epair_a_mac} }"
96	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
97
98	# Block us now
99	pft_set_rules alcatraz \
100		"ether block out on ${epair}b to { ! 00:01:02:03:04:05 }"
101	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
102
103	# Block with a masked address
104	pft_set_rules alcatraz \
105		"ether block out on ${epair}b to { ! 00:01:02:03:00:00/32 }"
106	jexec alcatraz pfctl -se
107	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
108
109	epair_prefix=$(echo $epair_a_mac | cut -c-8)
110	pft_set_rules alcatraz \
111		"ether block out on ${epair}b to { ${epair_prefix}:00:00:00/24 }"
112	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
113
114	pft_set_rules alcatraz \
115		"ether block out on ${epair}b to { ${epair_prefix}:00:00:00&ff:ff:ff:00:00:00 }"
116	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
117
118	# Check '-F ethernet' works
119	jexec alcatraz pfctl -F ethernet
120	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
121}
122
123mac_cleanup()
124{
125	pft_cleanup
126}
127
128atf_test_case "proto" "cleanup"
129proto_head()
130{
131	atf_set descr 'Test EtherType filtering'
132	atf_set require.user root
133}
134
135proto_body()
136{
137	pft_init
138
139	epair=$(vnet_mkepair)
140	epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }')
141
142	ifconfig ${epair}a 192.0.2.1/24 up
143
144	vnet_mkjail alcatraz ${epair}b
145	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
146
147	pft_set_rules alcatraz \
148		"ether block proto 0x0810"
149	jexec alcatraz pfctl -e
150
151	atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.2
152
153	# Block IP
154	pft_set_rules alcatraz \
155		"ether block proto 0x0800"
156	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
157
158	# Block ARP
159	pft_set_rules alcatraz \
160		"ether block proto 0x0806"
161	arp -d 192.0.2.2
162	atf_check -s exit:2 -o ignore ping -c 1 -t 1 192.0.2.2
163}
164
165proto_cleanup()
166{
167	pft_cleanup
168}
169
170atf_test_case "direction" "cleanup"
171direction_head()
172{
173	atf_set descr 'Test directionality of ether rules'
174	atf_set require.user root
175	atf_set require.progs jq
176}
177
178direction_body()
179{
180	pft_init
181
182	epair=$(vnet_mkepair)
183	epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }')
184	epair_b_mac=$(ifconfig ${epair}b ether | awk '/ether/ { print $2; }')
185
186	ifconfig ${epair}a 192.0.2.1/24 up
187
188	vnet_mkjail alcatraz ${epair}b
189	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
190
191	pft_set_rules alcatraz \
192		"ether block in proto 0x0806"
193	jexec alcatraz pfctl -e
194
195	arp -d 192.0.2.2
196	jexec alcatraz arp -d 192.0.2.1
197
198	# We don't allow the jail to receive ARP requests, so if we try to ping
199	# from host to jail the host can't resolve the MAC address
200	ping -c 1 -t 1 192.0.2.2
201
202	mac=$(arp -an --libxo json \
203	    | jq '."arp"."arp-cache"[] |
204	    select(."ip-address"=="192.0.2.2")."mac-address"')
205	atf_check_not_equal "$mac" "$epair_b_mac"
206
207	# Clear ARP table again
208	arp -d 192.0.2.2
209	jexec alcatraz arp -d 192.0.2.1
210
211	# However, we allow outbound ARP, so the host will learn our MAC if the
212	# jail tries to ping
213	jexec alcatraz ping -c 1 -t 1 192.0.2.1
214
215	mac=$(arp -an --libxo json \
216	    | jq '."arp"."arp-cache"[] |
217	    select(."ip-address"=="192.0.2.2")."mac-address"')
218	atf_check_equal "$mac" "$epair_b_mac"
219
220	# Now do the same, but with outbound ARP blocking
221	pft_set_rules alcatraz \
222		"ether block out proto 0x0806"
223
224	# Clear ARP table again
225	arp -d 192.0.2.2
226	jexec alcatraz arp -d 192.0.2.1
227
228	# The jail can't send ARP requests to us, so we'll never learn our MAC
229	# address
230	jexec alcatraz ping -c 1 -t 1 192.0.2.1
231
232	mac=$(jexec alcatraz arp -an --libxo json \
233	    | jq '."arp"."arp-cache"[] |
234	    select(."ip-address"=="192.0.2.1")."mac-address"')
235	atf_check_not_equal "$mac" "$epair_a_mac"
236}
237
238direction_cleanup()
239{
240	pft_cleanup
241}
242
243atf_test_case "captive" "cleanup"
244captive_head()
245{
246	atf_set descr 'Test a basic captive portal-like setup'
247	atf_set require.user root
248}
249
250captive_body()
251{
252	# Host is client, jail 'gw' is the captive portal gateway, jail 'srv'
253	# is a random (web)server. We use the echo protocol rather than http
254	# for the test, because that's easier.
255	pft_init
256
257	epair_gw=$(vnet_mkepair)
258	epair_srv=$(vnet_mkepair)
259	epair_gw_a_mac=$(ifconfig ${epair_gw}a ether | awk '/ether/ { print $2; }')
260
261	vnet_mkjail gw ${epair_gw}b ${epair_srv}a
262	vnet_mkjail srv ${epair_srv}b
263
264	ifconfig ${epair_gw}a 192.0.2.2/24 up
265	route add -net 198.51.100.0/24 192.0.2.1
266	jexec gw ifconfig ${epair_gw}b 192.0.2.1/24 up
267	jexec gw ifconfig lo0 127.0.0.1/8 up
268	jexec gw sysctl net.inet.ip.forwarding=1
269
270	jexec gw ifconfig ${epair_srv}a 198.51.100.1/24 up
271	jexec srv ifconfig ${epair_srv}b 198.51.100.2/24 up
272	jexec srv route add -net 192.0.2.0/24 198.51.100.1
273
274	# Sanity check
275	atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2
276
277	pft_set_rules gw \
278		"ether pass quick proto 0x0806" \
279		"ether pass tag captive" \
280		"rdr on ${epair_gw}b proto tcp to port echo tagged captive -> 127.0.0.1 port echo"
281	jexec gw pfctl -e
282
283	# ICMP should still work, because we don't redirect it.
284	atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2
285
286	# Run the echo server only on the gw, so we know we've redirectly
287	# correctly if we get an echo message.
288	jexec gw /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf
289
290	# Confirm that we're getting redirected
291	atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7"
292
293	jexec gw killall inetd
294
295	# Now pretend we've authenticated, so add the client's MAC address
296	pft_set_rules gw \
297		"ether pass quick proto 0x0806" \
298		"ether pass quick from ${epair_gw_a_mac}" \
299		"ether pass tag captive" \
300		"rdr on ${epair_gw}b proto tcp to port echo tagged captive -> 127.0.0.1 port echo"
301
302	# No redirect, so failure.
303	atf_check -s exit:1 -x "echo foo | nc -N 198.51.100.2 7"
304
305	# Start a server in srv
306	jexec srv /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf
307
308	# And now we can talk to that one.
309	atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7"
310}
311
312captive_cleanup()
313{
314	pft_cleanup
315}
316
317atf_test_case "captive_long" "cleanup"
318captive_long_head()
319{
320	atf_set descr 'More complex captive portal setup'
321	atf_set require.user root
322}
323
324captive_long_body()
325{
326	# Host is client, jail 'gw' is the captive portal gateway, jail 'srv'
327	# is a random (web)server. We use the echo protocol rather than http
328	# for the test, because that's easier.
329	pft_init
330
331	if ! kldstat -q -m dummynet; then
332		atf_skip "This test requires dummynet"
333	fi
334
335	epair_gw=$(vnet_mkepair)
336	epair_srv=$(vnet_mkepair)
337	epair_gw_a_mac=$(ifconfig ${epair_gw}a ether | awk '/ether/ { print $2; }')
338
339	vnet_mkjail gw ${epair_gw}b ${epair_srv}a
340	vnet_mkjail srv ${epair_srv}b
341
342	ifconfig ${epair_gw}a 192.0.2.2/24 up
343	route add -net 198.51.100.0/24 192.0.2.1
344	jexec gw ifconfig ${epair_gw}b 192.0.2.1/24 up
345	jexec gw ifconfig lo0 127.0.0.1/8 up
346	jexec gw sysctl net.inet.ip.forwarding=1
347
348	jexec gw ifconfig ${epair_srv}a 198.51.100.1/24 up
349	jexec srv ifconfig ${epair_srv}b 198.51.100.2/24 up
350	jexec srv route add -net 192.0.2.0/24 198.51.100.1
351
352	jexec gw dnctl pipe 1 config bw 300KByte/s
353
354	# Sanity check
355	atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2
356
357	pft_set_rules gw \
358		"ether anchor \"captiveportal\" on { ${epair_gw}b } {" \
359			"ether pass quick proto { 0x0806, 0x8035, 0x888e, 0x88c7, 0x8863, 0x8864 }" \
360			"ether pass tag \"captive\"" \
361		"}" \
362		"rdr on ${epair_gw}b proto tcp to port daytime tagged captive -> 127.0.0.1 port echo"
363	jexec gw pfctl -e
364
365	# ICMP should still work, because we don't redirect it.
366	atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2
367
368	jexec gw /usr/sbin/inetd -p gw.pid $(atf_get_srcdir)/echo_inetd.conf
369	jexec srv /usr/sbin/inetd -p srv.pid $(atf_get_srcdir)/daytime_inetd.conf
370
371	echo foo | nc -N 198.51.100.2 13
372
373	# Confirm that we're getting redirected
374	atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 13"
375
376	# Now update the rules to allow our client to pass without redirect
377	pft_set_rules gw \
378		"ether anchor \"captiveportal\" on { ${epair_gw}b } {" \
379			"ether pass quick proto { 0x0806, 0x8035, 0x888e, 0x88c7, 0x8863, 0x8864 }" \
380			"ether pass quick from { ${epair_gw_a_mac} } dnpipe 1" \
381			"ether pass tag \"captive\"" \
382		"}" \
383		"rdr on ${epair_gw}b proto tcp to port daytime tagged captive -> 127.0.0.1 port echo"
384
385	# We're not being redirected and get datime information now
386	atf_check -s exit:0 -o match:"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)" -x "echo foo | nc -N 198.51.100.2 13"
387
388	jexec gw killall inetd
389	jexec srv killall inetd
390}
391
392captive_long_cleanup()
393{
394	pft_cleanup
395}
396
397atf_test_case "dummynet" "cleanup"
398dummynet_head()
399{
400	atf_set descr 'Test dummynet for L2 traffic'
401	atf_set require.user root
402}
403
404dummynet_body()
405{
406	pft_init
407
408	if ! kldstat -q -m dummynet; then
409		atf_skip "This test requires dummynet"
410	fi
411
412	epair=$(vnet_mkepair)
413	vnet_mkjail alcatraz ${epair}b
414
415	ifconfig ${epair}a 192.0.2.1/24 up
416	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
417
418	# Sanity check
419	atf_check -s exit:0 -o ignore ping -i .1 -c 3 -s 1200 192.0.2.2
420
421	jexec alcatraz dnctl pipe 1 config bw 30Byte/s
422	jexec alcatraz pfctl -e
423	pft_set_rules alcatraz \
424		"ether pass in dnpipe 1"
425
426	# single ping succeeds just fine
427	atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2
428
429	# Saturate the link
430	ping -i .1 -c 5 -s 1200 192.0.2.2
431
432	# We should now be hitting the limits and get this packet dropped.
433	atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2
434}
435
436dummynet_cleanup()
437{
438	pft_cleanup
439}
440
441atf_test_case "anchor" "cleanup"
442anchor_head()
443{
444	atf_set descr 'Test ether anchors'
445	atf_set require.user root
446}
447
448anchor_body()
449{
450	pft_init
451
452	epair=$(vnet_mkepair)
453	epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }')
454
455	vnet_mkjail alcatraz ${epair}b
456
457	ifconfig ${epair}a 192.0.2.1/24 up
458	jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
459
460	# Sanity check
461	atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2
462
463	jexec alcatraz pfctl -e
464	pft_set_rules alcatraz \
465		"ether anchor \"foo\" in on lo0 {" \
466			"ether block" \
467		"}"
468
469	# That only filters on lo0, so we should still be able to pass traffic
470	atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2
471
472	pft_set_rules alcatraz \
473		"ether block in" \
474		"ether anchor \"foo\" in on ${epair}b {" \
475			"ether pass" \
476		"}"
477	atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2
478
479	pft_set_rules alcatraz \
480		"ether pass" \
481		"ether anchor \"bar\" in on ${epair}b {" \
482			"ether block" \
483		"}"
484	atf_check -s exit:2 -o ignore ping -c 1 -t 2 192.0.2.2
485
486	pft_set_rules alcatraz \
487		"ether block in" \
488		"ether anchor \"baz\" on ${epair}b {" \
489			"ether pass in from 01:02:03:04:05:06" \
490		"}" \
491		"ether pass in from ${epair_a_mac}"
492	atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2
493}
494
495anchor_cleanup()
496{
497	pft_cleanup
498}
499
500atf_init_test_cases()
501{
502	atf_add_test_case "mac"
503	atf_add_test_case "proto"
504	atf_add_test_case "direction"
505	atf_add_test_case "captive"
506	atf_add_test_case "captive_long"
507	atf_add_test_case "dummynet"
508	atf_add_test_case "anchor"
509}
510