xref: /freebsd/tests/sys/netpfil/pf/counters.sh (revision df21a004be237a1dccd03c7b47254625eea62fa9)
1#
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2025 Kajetan Staszkiewicz
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26
27. $(atf_get_srcdir)/utils.subr
28
29get_counters()
30{
31	echo " === rules ==="
32	rules=$(mktemp) || exit
33	(jexec router pfctl -qvvsn ; jexec router pfctl -qvvsr) | normalize_pfctl_s > $rules
34	cat $rules
35
36	echo " === tables ==="
37	tables=$(mktemp) || exit 1
38	jexec router pfctl -qvvsT > $tables
39	cat $tables
40
41	echo " === states ==="
42	states=$(mktemp) || exit 1
43	jexec router pfctl -qvvss | normalize_pfctl_s > $states
44	cat $states
45
46	echo " === nodes ==="
47	nodes=$(mktemp) || exit 1
48	jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes
49	cat $nodes
50}
51
52atf_test_case "match_pass_state" "cleanup"
53match_pass_state_head()
54{
55	atf_set descr 'Counters on match and pass rules'
56	atf_set require.user root
57}
58
59match_pass_state_body()
60{
61	setup_router_server_ipv6
62
63	# Thest counters for a statefull firewall. Expose the behaviour of
64	# increasing table counters if a table is used multiple times.
65	# The table "tbl_in" is used both in match and pass rule. It's counters
66	# are incremented twice. The tables "tbl_out_match" and "tbl_out_pass"
67	# are used only once and have their countes increased only once.
68	# Test source node counters for this simple scenario too.
69	pft_set_rules router \
70		"set state-policy if-bound" \
71		"table <tbl_in>  { ${net_tester_host_tester} }" \
72		"table <tbl_out_pass> { ${net_server_host_server} }" \
73		"table <tbl_out_match> { ${net_server_host_server} }" \
74		"block" \
75		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
76		"match in  on ${epair_tester}b inet6 proto tcp from <tbl_in>  scrub (random-id)" \
77		"pass  in  on ${epair_tester}b inet6 proto tcp from <tbl_in>  keep state (max-src-states 3 source-track rule)" \
78		"match out on ${epair_server}a inet6 proto tcp to   <tbl_out_match> scrub (random-id)" \
79		"pass  out on ${epair_server}a inet6 proto tcp to   <tbl_out_pass> keep state"
80
81	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
82	atf_check -s exit:0 -o match:"This is a test" -x \
83		"echo 'This is a test' | nc -w3 ${net_server_host_server} echo"
84	# Let FINs pass through.
85	sleep 1
86	get_counters
87
88	for rule_regexp in \
89		"@3 match in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
90		"@4 pass in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
91		"@5 match out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
92		"@6 pass out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
93	; do
94		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
95	done
96
97	table_counters_single="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
98	table_counters_double="Evaluations: NoMatch: 0 Match: 2 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 12 Bytes: 910 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 8 Bytes: 622 Out/XPass: Packets: 0 Bytes: 0"
99	for table_test in \
100		"tbl_in___${table_counters_double}" \
101		"tbl_out_match___${table_counters_single}" \
102		"tbl_out_pass___${table_counters_single}" \
103	; do
104		table_name=${table_test%%___*}
105		table_regexp=${table_test##*___}
106		table=$(mktemp) || exit 1
107		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
108		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
109	done;
110
111	for state_regexp in \
112		"${epair_tester}b tcp ${net_server_host_server}.* <- ${net_tester_host_tester}.* 6:4 pkts, 455:311 bytes, rule 4," \
113		"${epair_server}a tcp ${net_server_host_tester}.* -> ${net_server_host_server}.* 6:4 pkts, 455:311 bytes, rule 6," \
114	; do
115		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
116	done
117
118	for node_regexp in \
119		"${net_tester_host_tester} -> :: .* 10 pkts, 766 bytes, filter rule 4, limit source-track"\
120	; do
121		grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'"
122	done
123}
124
125match_pass_state_cleanup()
126{
127	pft_cleanup
128}
129
130atf_test_case "match_pass_no_state" "cleanup"
131match_pass_no_state_head()
132{
133	atf_set descr 'Counters on match and pass rules without keep state'
134	atf_set require.user root
135}
136
137match_pass_no_state_body()
138{
139	setup_router_server_ipv6
140
141	# Test counters for a stateless firewall.
142	# The table "tbl_in" is used both in match and pass rule in the inbound
143	# direction. The "In/Pass" counter is incremented twice. The table
144	# "tbl_inout" matches the same host on inbound and outbound direction.
145	# It will also be incremented twice. The tables "tbl_out_match" and
146	# "tbl_out_pass" will have their counters increased only once.
147	pft_set_rules router \
148		"table <tbl_in>        { ${net_tester_host_tester} }" \
149		"table <tbl_inout>     { ${net_tester_host_tester} }" \
150		"table <tbl_out_match> { ${net_server_host_server} }" \
151		"table <tbl_out_pass>  { ${net_server_host_server} }" \
152		"block" \
153		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
154		"match in  on ${epair_tester}b inet6 proto tcp from <tbl_inout>" \
155		"match in  on ${epair_tester}b inet6 proto tcp from <tbl_in>" \
156		"pass  in  on ${epair_tester}b inet6 proto tcp from <tbl_in> no state" \
157		"pass  out on ${epair_tester}b inet6 proto tcp to   <tbl_in> no state" \
158		"match in  on ${epair_server}a inet6 proto tcp from <tbl_out_match>" \
159		"pass  in  on ${epair_server}a inet6 proto tcp from <tbl_out_pass>  no state" \
160		"match out on ${epair_server}a inet6 proto tcp from <tbl_inout> no state" \
161		"pass  out on ${epair_server}a inet6 proto tcp to   <tbl_out_pass>  no state"
162
163	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
164	atf_check -s exit:0 -o match:"This is a test" -x \
165		"echo 'This is a test' | nc -w3 ${net_server_host_server} echo"
166	sleep 1
167	get_counters
168
169	for rule_regexp in \
170		"@3 match in on ${epair_tester}b .* Packets: 6 Bytes: 455 " \
171		"@4 match in on ${epair_tester}b .* Packets: 6 Bytes: 455 " \
172		"@5 pass in on ${epair_tester}b .* Packets: 6 Bytes: 455 " \
173		"@6 pass out on ${epair_tester}b .* Packets: 4 Bytes: 311 " \
174		"@7 match in on ${epair_server}a .* Packets: 4 Bytes: 311 " \
175		"@8 pass in on ${epair_server}a .* Packets: 4 Bytes: 311 " \
176		"@10 pass out on ${epair_server}a .* Packets: 6 Bytes: 455 " \
177	; do
178		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
179	done
180
181	for table_test in \
182		"tbl_in___Evaluations: NoMatch: 0 Match: 16 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 12 Bytes: 910 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 4 Bytes: 311 Out/XPass: Packets: 0 Bytes: 0" \
183		"tbl_out_match___Evaluations: NoMatch: 0 Match: 4 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 0 Bytes: 0 Out/XPass: Packets: 0 Bytes: 0" \
184		"tbl_out_pass___Evaluations: NoMatch: 0 Match: 10 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0" \
185		"tbl_inout___Evaluations: NoMatch: 0 Match: 12 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 6 Bytes: 455 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0" \
186	; do
187		table_name=${table_test%%___*}
188		table_regexp=${table_test##*___}
189		table=$(mktemp) || exit 1
190		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
191		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
192	done;
193}
194
195match_pass_no_state_cleanup()
196{
197	pft_cleanup
198}
199
200atf_test_case "match_block" "cleanup"
201match_block_head()
202{
203	atf_set descr 'Counters on match and block rules'
204	atf_set require.user root
205}
206
207match_block_body()
208{
209	setup_router_server_ipv6
210
211	# Stateful firewall with a blocking rule. The rule will have its
212	# counters increased because it matches and applies correctly.
213	# The "match" rule before the "pass" rule will have its counters
214	# increased for blocked traffic too.
215	pft_set_rules router \
216		"set state-policy if-bound" \
217		"table <tbl_in_match> { ${net_server_host_server} }" \
218		"table <tbl_in_block> { ${net_server_host_server} }" \
219		"block" \
220		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
221		"match  in on ${epair_tester}b inet6 proto tcp to   <tbl_in_match> scrub (random-id)" \
222		"block  in on ${epair_tester}b inet6 proto tcp to   <tbl_in_block>" \
223		"pass  out on ${epair_server}a inet6 proto tcp keep state"
224
225	# Wait 3 seconds, that will cause 2 SYNs to be sent out.
226	echo 'This is a test' | nc -w3 ${net_server_host_server} echo
227	sleep 1
228	get_counters
229
230	for rule_regexp in \
231		"@3 match in on ${epair_tester}b .* Packets: 2 Bytes: 160 States: 0 " \
232		"@4 block drop in on ${epair_tester}b .* Packets: 2 Bytes: 160 States: 0 " \
233	; do
234		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
235	done
236
237	# OpenBSD has (In|Out)/Match. We don't (yet) have it in FreeBSD
238	# so we follow the action of the "pass" rule ("block" for this test)
239	# in "match" rules.
240	for table_test in \
241		"tbl_in_match___Evaluations: NoMatch: 0 Match: 2 In/Block: Packets: 2 Bytes: 160 In/Pass: Packets: 0 Bytes: 0 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 0 Bytes: 0 Out/XPass: Packets: 0 Bytes: 0" \
242		"tbl_in_block___Evaluations: NoMatch: 0 Match: 2 In/Block: Packets: 2 Bytes: 160 In/Pass: Packets: 0 Bytes: 0 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 0 Bytes: 0 Out/XPass: Packets: 0 Bytes: 0" \
243	; do
244		table_name=${table_test%%___*}
245		table_regexp=${table_test##*___}
246		table=$(mktemp) || exit 1
247		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
248		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
249	done;
250}
251
252match_block_cleanup()
253{
254	pft_cleanup
255}
256
257atf_test_case "match_fail" "cleanup"
258match_fail_head()
259{
260	atf_set descr 'Counters on match and failing pass rules'
261	atf_set require.user root
262}
263
264match_fail_body()
265{
266	setup_router_server_ipv6
267
268	# Statefull firewall with a failing "pass" rule.
269	# When the rule can't apply it will not have its counters increased.
270	pft_set_rules router \
271		"set state-policy if-bound" \
272		"table <tbl_in_match> { ${net_server_host_server} }" \
273		"table <tbl_in_fail> { ${net_server_host_server} }" \
274		"block" \
275		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
276		"match  in on ${epair_tester}b inet6 proto tcp to <tbl_in_match> scrub (random-id)" \
277		"pass   in on ${epair_tester}b inet6 proto tcp to <tbl_in_fail> keep state (max 1)" \
278		"pass  out on ${epair_server}a inet6 proto tcp keep state"
279
280	# The first test will pass and increase the counters for all rules.
281	echo 'This is a test' | nc -w3 ${net_server_host_server} echo
282	# The second test will go through the "match" rules but fail
283	# on the "pass" rule due to 'keep state (max 1)'.
284	# Wait 3 seconds, that will cause 2 SYNs to be sent out.
285	echo 'This is a test' | nc -w3 ${net_server_host_server} echo
286	sleep 1
287	get_counters
288
289	for rule_regexp in \
290		"@3 match in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
291		"@4 pass in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
292	; do
293		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
294	done
295
296	$table_counters_single="Evaluations: NoMatch: 0 Match: 3 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 6 Bytes: 455 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 4 Bytes: 311 Out/XPass: Packets: 0 Bytes: 0"
297	for table_test in \
298		"tbl_in_match___${table_counters_single}" \
299		"tbl_in_fail___${table_counters_single}" \
300	; do
301		table_name=${table_test%%___*}
302		table_regexp=${table_test##*___}
303		table=$(mktemp) || exit 1
304		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
305		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
306	done;
307}
308
309match_fail_cleanup()
310{
311	pft_cleanup
312}
313
314atf_test_case "nat_natonly" "cleanup"
315nat_natonly_head()
316{
317	atf_set descr 'Counters on only a NAT rule creating state'
318	atf_set require.user root
319}
320
321nat_natonly_body()
322{
323	setup_router_server_ipv6
324
325	# NAT is applied on the "nat" rule.
326	# The "nat" rule matches on pre-NAT addresses. There is no separate
327	# "pass" rule so the "nat" rule creates the state.
328	pft_set_rules router \
329		"set state-policy if-bound" \
330		"table <tbl_src_nat> { ${net_tester_host_tester} }" \
331		"table <tbl_dst_nat> { ${net_server_host_server} }" \
332		"nat on ${epair_server}a inet6 proto tcp from <tbl_src_nat> to <tbl_dst_nat> -> ${net_server_host_router}"
333
334	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
335	atf_check -s exit:0 -o match:"This is a test" -x \
336		"echo 'This is a test' | nc -w3 ${net_server_host_server} echo"
337	sleep 1
338	get_counters
339
340	for rule_regexp in \
341		"@0 nat on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
342	; do
343		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
344	done
345
346	# All tables have counters increased for In/Pass and Out/Pass, not XPass.
347	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
348	for table_test in \
349		"tbl_src_nat___${table_counters}" \
350		"tbl_dst_nat___${table_counters}" \
351	; do
352		table_name=${table_test%%___*}
353		table_regexp=${table_test##*___}
354		table=$(mktemp) || exit 1
355		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
356		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
357	done;
358
359	for state_regexp in \
360		"all tcp ${net_server_host_router}.* -> ${net_server_host_server}.* 6:4 pkts, 455:311 bytes" \
361	; do
362		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
363	done
364}
365
366nat_natonly_cleanup()
367{
368	pft_cleanup
369}
370
371atf_test_case "nat_nat" "cleanup"
372nat_nat_head()
373{
374	atf_set descr 'Counters on NAT, match and pass rules with keep state'
375	atf_set require.user root
376}
377
378nat_nat_body()
379{
380	setup_router_server_ipv6
381
382	# NAT is applied in the NAT ruleset.
383	# The "nat" rule matches on pre-NAT addresses.
384	# The "match" rule matches on post-NAT addresses.
385	# The "pass" rule matches on post-NAT addresses and creates the state.
386	pft_set_rules router \
387		"set state-policy if-bound" \
388		"table <tbl_src_nat> { ${net_tester_host_tester} }" \
389		"table <tbl_dst_nat> { ${net_server_host_server} }" \
390		"table <tbl_src_match> { ${net_server_host_router} }" \
391		"table <tbl_dst_match> { ${net_server_host_server} }" \
392		"table <tbl_src_pass> { ${net_server_host_router} }" \
393		"table <tbl_dst_pass> { ${net_server_host_server} }" \
394		"nat on ${epair_server}a inet6 proto tcp from <tbl_src_nat> to <tbl_dst_nat> -> ${net_server_host_router}" \
395		"block" \
396		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
397		"pass  in  on ${epair_tester}b inet6 proto tcp keep state" \
398		"match out on ${epair_server}a inet6 proto tcp from <tbl_src_match> to <tbl_dst_match> scrub (random-id)" \
399		"pass  out on ${epair_server}a inet6 proto tcp from <tbl_src_pass>  to <tbl_dst_pass>  keep state"
400
401	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
402	atf_check -s exit:0 -o match:"This is a test" -x \
403		"echo 'This is a test' | nc -w3 ${net_server_host_server} echo"
404	sleep 1
405	get_counters
406
407	for rule_regexp in \
408		"@0 nat on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
409		"@4 match out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
410		"@5 pass out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
411	; do
412		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
413	done
414
415	# All tables have counters increased for In/Pass and Out/Pass, not XPass nor Block.
416	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
417	for table_test in \
418		"tbl_src_nat___${table_counters}" \
419		"tbl_dst_nat___${table_counters}" \
420		"tbl_src_match___${table_counters}" \
421		"tbl_dst_match___${table_counters}" \
422		"tbl_src_pass___${table_counters}" \
423		"tbl_dst_pass___${table_counters}" \
424	; do
425		table_name=${table_test%%___*}
426		table_regexp=${table_test##*___}
427		table=$(mktemp) || exit 1
428		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
429		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
430	done;
431
432	for state_regexp in \
433		"${epair_server}a tcp ${net_server_host_router}.* -> ${net_server_host_server}.* 6:4 pkts, 455:311 bytes, rule 5," \
434	; do
435		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
436	done
437}
438
439nat_nat_cleanup()
440{
441	pft_cleanup
442}
443
444atf_test_case "nat_match" "cleanup"
445nat_match_head()
446{
447	atf_set descr 'Counters on match with NAT and pass rules'
448	atf_set require.user root
449}
450
451nat_match_body()
452{
453	setup_router_server_ipv6
454
455	# NAT is applied on the "match" rule.
456	# The "match" rule up to and including the NAT rule match on pre-NAT addresses.
457	# The "match" rule after NAT matches on post-NAT addresses.
458	# The "pass" rule matches on post-NAT addresses and creates the state.
459	pft_set_rules router \
460		"set state-policy if-bound" \
461		"table <tbl_src_match1> { ${net_tester_host_tester} }" \
462		"table <tbl_dst_match1> { ${net_server_host_server} }" \
463		"table <tbl_src_match2> { ${net_tester_host_tester} }" \
464		"table <tbl_dst_match2> { ${net_server_host_server} }" \
465		"table <tbl_src_match3> { ${net_server_host_router} }" \
466		"table <tbl_dst_match3> { ${net_server_host_server} }" \
467		"table <tbl_src_pass> { ${net_server_host_router} }" \
468		"table <tbl_dst_pass> { ${net_server_host_server} }" \
469		"block" \
470		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
471		"pass  in  on ${epair_tester}b inet6 proto tcp keep state" \
472		"match out on ${epair_server}a inet6 proto tcp from <tbl_src_match1> to <tbl_dst_match1> scrub (random-id)" \
473		"match out on ${epair_server}a inet6 proto tcp from <tbl_src_match2> to <tbl_dst_match2> nat-to ${net_server_host_router}" \
474		"match out on ${epair_server}a inet6 proto tcp from <tbl_src_match3> to <tbl_dst_match3> scrub (random-id)" \
475		"pass  out on ${epair_server}a inet6 proto tcp from <tbl_src_pass>  to <tbl_dst_pass>  keep state"
476
477	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
478	atf_check -s exit:0 -o match:"This is a test" -x \
479		"echo 'This is a test' | nc -w3 ${net_server_host_server} echo"
480	sleep 1
481	get_counters
482
483	for rule_regexp in \
484		"@4 match out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
485		"@5 match out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
486		"@6 match out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
487		"@7 pass out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
488	; do
489		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
490	done
491
492	# All tables have counters increased for In/Pass and Out/Pass, not XPass nor Block.
493	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
494	for table_test in \
495		"tbl_src_match1___${table_counters}" \
496		"tbl_dst_match1___${table_counters}" \
497		"tbl_src_match2___${table_counters}" \
498		"tbl_dst_match2___${table_counters}" \
499		"tbl_src_match3___${table_counters}" \
500		"tbl_dst_match3___${table_counters}" \
501		"tbl_src_pass___${table_counters}" \
502		"tbl_dst_pass___${table_counters}" \
503	; do
504		table_name=${table_test%%___*}
505		table_regexp=${table_test##*___}
506		table=$(mktemp) || exit 1
507		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
508		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
509	done;
510
511	for state_regexp in \
512		"${epair_server}a tcp ${net_server_host_tester}.* -> ${net_server_host_server}.* 6:4 pkts, 455:311 bytes, rule 7, " \
513	; do
514		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
515	done
516}
517
518nat_match_cleanup()
519{
520	pft_cleanup
521}
522
523atf_test_case "nat_pass" "cleanup"
524nat_pass_head()
525{
526	atf_set descr 'Counters on match, and pass with NAT rules'
527	atf_set require.user root
528}
529
530nat_pass_body()
531{
532	setup_router_server_ipv6
533
534	# NAT is applied on the "pass" rule which also creates the state.
535	# All rules match on pre-NAT addresses.
536	pft_set_rules router \
537		"set state-policy if-bound" \
538		"table <tbl_src_match> { ${net_tester_host_tester} }" \
539		"table <tbl_dst_match> { ${net_server_host_server} }" \
540		"table <tbl_src_pass> { ${net_tester_host_tester} }" \
541		"table <tbl_dst_pass> { ${net_server_host_server} }" \
542		"block" \
543		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
544		"pass  in  on ${epair_tester}b inet6 proto tcp keep state" \
545		"match out on ${epair_server}a inet6 proto tcp from <tbl_src_match> to <tbl_dst_match> scrub (random-id)" \
546		"pass  out on ${epair_server}a inet6 proto tcp from <tbl_src_pass>  to <tbl_dst_pass>  nat-to ${net_server_host_router} keep state"
547
548	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
549	atf_check -s exit:0 -o match:"This is a test" -x \
550		"echo 'This is a test' | nc -w3 ${net_server_host_server} echo"
551	sleep 1
552	get_counters
553
554	for rule_regexp in \
555		"@4 match out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
556		"@5 pass out on ${epair_server}a .* Packets: 10 Bytes: 766 States: 1 " \
557	; do
558		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
559	done
560
561	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 311 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
562	for table_test in \
563		"tbl_src_match___${table_counters}" \
564		"tbl_dst_match___${table_counters}" \
565		"tbl_src_pass___${table_counters}" \
566		"tbl_dst_pass___${table_counters}" \
567	; do
568		table_name=${table_test%%___*}
569		table_regexp=${table_test##*___}
570		table=$(mktemp) || exit 1
571		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
572		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
573	done;
574
575	for state_regexp in \
576		"${epair_server}a tcp ${net_server_host_router}.* -> ${net_server_host_server}.* 6:4 pkts, 455:311 bytes, rule 5," \
577	; do
578		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
579	done
580}
581
582nat_pass_cleanup()
583{
584	pft_cleanup
585}
586
587atf_test_case "rdr_match" "cleanup"
588rdr_match_head()
589{
590	atf_set descr 'Counters on match with RDR and pass rules'
591	atf_set require.user root
592}
593
594rdr_match_body()
595{
596	setup_router_server_ipv6
597
598	# Similar to the nat_match test but for the RDR action.
599	# Hopefully we don't need all other tests duplicated for RDR.
600	# Send traffic to a non-existing host, RDR it to the server.
601	#
602	# The "match" rule up to and including the RDR rule match on pre-RDR dst address.
603	# The "match" rule after NAT matches on post-RDR dst address.
604	# The "pass" rule matches on post-RDR dst address.
605	net_server_host_notserver=${net_server_host_server%%::*}::3
606	pft_set_rules router \
607		"set state-policy if-bound" \
608		"table <tbl_src_match1> { ${net_tester_host_tester} }" \
609		"table <tbl_dst_match1> { ${net_server_host_notserver} }" \
610		"table <tbl_src_match2> { ${net_tester_host_tester} }" \
611		"table <tbl_dst_match2> { ${net_server_host_notserver} }" \
612		"table <tbl_src_match3> { ${net_tester_host_tester} }" \
613		"table <tbl_dst_match3> { ${net_server_host_server} }" \
614		"table <tbl_src_pass> { ${net_tester_host_tester} }" \
615		"table <tbl_dst_pass> { ${net_server_host_server} }" \
616		"block" \
617		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
618		"pass  out on ${epair_server}a inet6 proto tcp keep state" \
619		"match  in on ${epair_tester}b inet6 proto tcp from <tbl_src_match1> to <tbl_dst_match1> scrub (random-id)" \
620		"match  in on ${epair_tester}b inet6 proto tcp from <tbl_src_match2> to <tbl_dst_match2> rdr-to ${net_server_host_server}" \
621		"match  in on ${epair_tester}b inet6 proto tcp from <tbl_src_match3> to <tbl_dst_match3> scrub (random-id)" \
622		"pass   in on ${epair_tester}b inet6 proto tcp from <tbl_src_pass>  to <tbl_dst_pass>  keep state"
623
624	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
625	atf_check -s exit:0 -o match:"This is a test" -x \
626		"echo 'This is a test' | nc -w3 ${net_server_host_notserver} echo"
627	sleep 1
628	get_counters
629
630	for rule_regexp in \
631		"@4 match in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
632		"@5 match in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
633		"@6 match in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
634		"@7 pass in on ${epair_tester}b .* Packets: 10 Bytes: 766 States: 1 " \
635	; do
636		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
637	done
638
639	# All tables have counters increased for In/Pass and Out/Pass, not XPass nor Block.
640	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 6 Bytes: 455 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 4 Bytes: 311 Out/XPass: Packets: 0 Bytes: 0"
641	for table_test in \
642		"tbl_src_match1___${table_counters}" \
643		"tbl_dst_match1___${table_counters}" \
644		"tbl_src_match2___${table_counters}" \
645		"tbl_dst_match2___${table_counters}" \
646		"tbl_src_match3___${table_counters}" \
647		"tbl_dst_match3___${table_counters}" \
648		"tbl_src_pass___${table_counters}" \
649		"tbl_dst_pass___${table_counters}" \
650	; do
651		table_name=${table_test%%___*}
652		table_regexp=${table_test##*___}
653		table=$(mktemp) || exit 1
654		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
655		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
656	done;
657
658	for state_regexp in \
659		"${epair_tester}b tcp ${net_server_host_server}.* 6:4 pkts, 455:311 bytes, rule 7, " \
660	; do
661		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
662	done
663}
664
665rdr_match_cleanup()
666{
667	pft_cleanup
668}
669
670atf_test_case "nat64_in" "cleanup"
671nat64_in_head()
672{
673	atf_set descr 'Counters on match and inbound af-to rules'
674	atf_set require.user root
675}
676
677nat64_in_body()
678{
679	setup_router_server_nat64
680
681	pft_set_rules router \
682		"set state-policy if-bound" \
683		"table <tbl_src_match> { ${net_tester_6_host_tester} }" \
684		"table <tbl_dst_match> { 64:ff9b::${net_server1_4_host_server} }" \
685		"table <tbl_src_pass>  { ${net_tester_6_host_tester} }" \
686		"table <tbl_dst_pass>  { 64:ff9b::${net_server1_4_host_server} }" \
687		"block log" \
688		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
689		"match  in on ${epair_tester}b inet6 proto tcp from <tbl_src_match> to <tbl_dst_match> scrub (random-id)" \
690		"pass   in on ${epair_tester}b inet6 proto tcp from <tbl_src_pass>  to <tbl_dst_pass> \
691			af-to inet from (${epair_server1}a) \
692			keep state"
693
694	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
695	atf_check -s exit:0 -o match:"This is a test" -x \
696		"echo 'This is a test' | nc -w3 64:ff9b::${net_server1_4_host_server} echo"
697	sleep 1
698	get_counters
699
700	# The amount of packets is counted properly but sizes are not because
701	# pd->tot_len is always post-nat, even when updating pre-nat counters.
702	for rule_regexp in \
703		"@3 match in on ${epair_tester}b .* Packets: 10 Bytes: 686 States: 1 " \
704		"@4 pass in on ${epair_tester}b .* Packets: 10 Bytes: 686 States: 1 " \
705	; do
706		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
707	done
708
709	# All tables have counters increased for In/Pass and Out/Pass, not XPass nor Block.
710	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 231 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
711	for table_test in \
712		"tbl_src_match___${table_counters}" \
713		"tbl_dst_match___${table_counters}" \
714		"tbl_src_pass___${table_counters}" \
715		"tbl_dst_pass___${table_counters}" \
716	; do
717		table_name=${table_test%%___*}
718		table_regexp=${table_test##*___}
719		table=$(mktemp) || exit 1
720		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
721		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
722	done;
723
724	for state_regexp in \
725		"${epair_server1}a tcp ${net_server_host_tester}.* 6:4 pkts, 455:231 bytes, rule 4, " \
726	; do
727		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
728	done
729
730	echo " === interfaces === "
731	echo " === tester === "
732	jexec router pfctl -qvvsI -i ${epair_tester}b
733	echo " === server === "
734	jexec router pfctl -qvvsI -i ${epair_server1}a
735	echo " === "
736}
737
738nat64_in_cleanup()
739{
740	pft_cleanup
741}
742
743atf_test_case "nat64_out" "cleanup"
744nat64_out_head()
745{
746	atf_set descr 'Counters on match and outbound af-to rules'
747	atf_set require.user root
748}
749
750nat64_out_body()
751{
752	setup_router_server_nat64
753
754	# af-to in outbound path requires routes for the pre-af-to traffic.
755	jexec router route add -inet6 64:ff9b::/96 -iface ${epair_server1}a
756
757	pft_set_rules router \
758		"set state-policy if-bound" \
759		"table <tbl_src_match> { ${net_tester_6_host_tester} }" \
760		"table <tbl_dst_match> { 64:ff9b::${net_server1_4_host_server} }" \
761		"table <tbl_src_pass>  { ${net_tester_6_host_tester} }" \
762		"table <tbl_dst_pass>  { 64:ff9b::${net_server1_4_host_server} }" \
763		"block log " \
764		"pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
765		"pass  in  on ${epair_tester}b inet6 proto tcp keep state" \
766		"match out on ${epair_server1}a inet6 proto tcp from <tbl_src_match> to <tbl_dst_match> scrub (random-id)" \
767		"pass  out on ${epair_server1}a inet6 proto tcp from <tbl_src_pass>  to <tbl_dst_pass> \
768			af-to inet from (${epair_server1}a) \
769			keep state"
770
771	# Use a real TCP connection so that it will be properly closed, guaranteeing the amount of packets.
772	atf_check -s exit:0 -o match:"This is a test" -x \
773		"echo 'This is a test' | nc -w3 64:ff9b::${net_server1_4_host_server} echo"
774	sleep 1
775	get_counters
776
777	for rule_regexp in \
778		"@4 match out on ${epair_server1}a .* Packets: 10 Bytes: 686 States: 1 " \
779		"@5 pass out on ${epair_server1}a .* Packets: 10 Bytes: 686 States: 1 " \
780	; do
781		grep -qE "${rule_regexp}" $rules || atf_fail "Rule regexp not found for '${rule_regexp}'"
782	done
783
784	# All tables have counters increased for In/Pass and Out/Pass, not XPass nor Block.
785	table_counters="Evaluations: NoMatch: 0 Match: 1 In/Block: Packets: 0 Bytes: 0 In/Pass: Packets: 4 Bytes: 231 In/XPass: Packets: 0 Bytes: 0 Out/Block: Packets: 0 Bytes: 0 Out/Pass: Packets: 6 Bytes: 455 Out/XPass: Packets: 0 Bytes: 0"
786	for table_test in \
787		"tbl_src_match___${table_counters}" \
788		"tbl_dst_match___${table_counters}" \
789		"tbl_src_pass___${table_counters}" \
790		"tbl_dst_pass___${table_counters}" \
791	; do
792		table_name=${table_test%%___*}
793		table_regexp=${table_test##*___}
794		table=$(mktemp) || exit 1
795		cat $tables | grep -A10 $table_name | tr '\n' ' ' | awk '{gsub("[\\[\\]]", " ", $0); gsub("[[:blank:]]+"," ",$0); print $0}' > ${table}
796		grep -qE "${table_regexp}" ${table} || atf_fail "Bad counters for table ${table_name}"
797	done;
798
799	for state_regexp in \
800		"${epair_server1}a tcp 198.51.100.17:[0-9]+ \(64:ff9b::c633:6412\[7\]\) -> 198.51.100.18:7 \(2001:db8:4200::2\[[0-9]+\]\) .* 6:4 pkts, 455:231 bytes, rule 5," \
801	; do
802		grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
803	done
804
805	echo " === interfaces === "
806	echo " === tester === "
807	jexec router pfctl -qvvsI -i ${epair_tester}b
808	echo " === server === "
809	jexec router pfctl -qvvsI -i ${epair_server1}a
810	echo " === "
811}
812
813nat64_out_cleanup()
814{
815	pft_cleanup
816}
817
818atf_init_test_cases()
819{
820	atf_add_test_case "match_pass_state"
821	atf_add_test_case "match_pass_no_state"
822	atf_add_test_case "match_block"
823	atf_add_test_case "match_fail"
824	atf_add_test_case "nat_natonly"
825	atf_add_test_case "nat_nat"
826	atf_add_test_case "nat_match"
827	atf_add_test_case "nat_pass"
828	atf_add_test_case "rdr_match"
829	atf_add_test_case "nat64_in"
830	atf_add_test_case "nat64_out"
831}
832