1# 2# SPDX-License-Identifier: BSD-2-Clause 3# 4# Copyright (c) 2026 Rubicon Communications, LLC (Netgate) 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 29atf_test_case "state_basic" "cleanup" 30state_basic_head() 31{ 32 atf_set descr 'Basic state limiter test' 33 atf_set require.user root 34} 35 36state_basic_body() 37{ 38 pft_init 39 40 epair=$(vnet_mkepair) 41 42 ifconfig ${epair}a 192.0.2.2/24 up 43 44 vnet_mkjail alcatraz ${epair}b 45 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 46 47 # Sanity check 48 atf_check -s exit:0 -o ignore \ 49 ping -c 1 192.0.2.1 50 51 jexec alcatraz pfctl -e 52 # Allow up to one ICMP state. 53 pft_set_rules alcatraz \ 54 "set timeout icmp.error 120" \ 55 "state limiter \"server\" id 1 limit 1" \ 56 "block in proto icmp" \ 57 "pass in proto icmp state limiter \"server\" (no-match)" 58 59 atf_check -s exit:0 -o ignore \ 60 ping -c 2 192.0.2.1 61 62 # This should now fail 63 atf_check -s exit:2 -o ignore \ 64 ping -c 2 192.0.2.1 65 66 jexec alcatraz pfctl -sLimiterStates 67 hardlim=$(jexec alcatraz pfctl -sLimiterStates | awk 'NR>1 { print $5; }') 68 if [ $hardlim -eq 0 ]; then 69 atf_fail "Hard limit not incremented" 70 fi 71} 72 73state_basic_cleanup() 74{ 75 pft_cleanup 76} 77 78atf_test_case "state_rate" "cleanup" 79state_rate_head() 80{ 81 atf_set descr 'State rate limiting test' 82 atf_set require.user root 83} 84 85state_rate_body() 86{ 87 pft_init 88 89 epair=$(vnet_mkepair) 90 91 ifconfig ${epair}a 192.0.2.2/24 up 92 93 vnet_mkjail alcatraz ${epair}b 94 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 95 96 # Sanity check 97 atf_check -s exit:0 -o ignore \ 98 ping -c 1 192.0.2.1 99 100 jexec alcatraz pfctl -e 101 # Allow one ICMP state per 5 seconds 102 pft_set_rules alcatraz \ 103 "set timeout icmp.error 120" \ 104 "state limiter \"server\" id 1 limit 1000 rate 1/5" \ 105 "block in proto icmp" \ 106 "pass in proto icmp state limiter \"server\" (no-match)" 107 108 atf_check -s exit:0 -o ignore \ 109 ping -c 2 192.0.2.1 110 111 # This should now fail 112 atf_check -s exit:2 -o ignore \ 113 ping -c 2 192.0.2.1 114 115 jexec alcatraz pfctl -sLimiterStates 116 ratelim=$(jexec alcatraz pfctl -sLimiterStates | awk 'NR>1 { print $6; }') 117 if [ $ratelim -eq 0 ]; then 118 atf_fail "Rate limit not incremented" 119 fi 120 121 sleep 6 122 123 # We can now create another state 124 atf_check -s exit:0 -o ignore \ 125 ping -c 2 192.0.2.1 126} 127 128state_rate_cleanup() 129{ 130 pft_cleanup 131} 132 133atf_test_case "state_block" "cleanup" 134state_block_head() 135{ 136 atf_set descr 'Test block mode state limiter' 137 atf_set require.user root 138} 139 140state_block_body() 141{ 142 pft_init 143 144 epair=$(vnet_mkepair) 145 146 ifconfig ${epair}a 192.0.2.2/24 up 147 148 vnet_mkjail alcatraz ${epair}b 149 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 150 151 # Sanity check 152 atf_check -s exit:0 -o ignore \ 153 ping -c 1 192.0.2.1 154 155 jexec alcatraz pfctl -e 156 # Allow one ICMP state per 5 seconds 157 pft_set_rules alcatraz \ 158 "set timeout icmp.error 120" \ 159 "state limiter \"server\" id 1 limit 1000 rate 1/5" \ 160 "pass" \ 161 "pass in proto icmp state limiter \"server\" (block)" 162 163 atf_check -s exit:0 -o ignore \ 164 ping -c 2 192.0.2.1 165 166 # This should now fail 167 atf_check -s exit:2 -o ignore \ 168 ping -c 2 192.0.2.1 169 170 # However, if we set no-match and exceed the limit we just pass 171 pft_set_rules alcatraz \ 172 "set timeout icmp.error 120" \ 173 "state limiter \"server\" id 1 limit 1000 rate 1/5" \ 174 "pass" \ 175 "pass in proto icmp state limiter \"server\" (no-match)" 176 177 atf_check -s exit:0 -o ignore \ 178 ping -c 2 192.0.2.1 179 atf_check -s exit:0 -o ignore \ 180 ping -c 2 192.0.2.1 181} 182 183state_block_cleanup() 184{ 185 pft_cleanup 186} 187 188atf_test_case "state_multiple" "cleanup" 189state_multiple_head() 190{ 191 atf_set descr 'Create multiple state limiters' 192 atf_set require.user root 193} 194 195state_multiple_body() 196{ 197 pft_init 198 199 epair=$(vnet_mkepair) 200 201 ifconfig ${epair}a 192.0.2.2/24 up 202 203 vnet_mkjail alcatraz ${epair}b 204 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 205 206 # Sanity check 207 atf_check -s exit:0 -o ignore \ 208 ping -c 1 192.0.2.1 209 210 jexec alcatraz pfctl -e 211 # Allow up to one ICMP state. 212 pft_set_rules alcatraz \ 213 "set timeout icmp.error 120" \ 214 "state limiter \"server\" id 1 limit 1" \ 215 "state limiter \"client\" id 2 limit 1" \ 216 "block in proto icmp" \ 217 "pass in proto icmp state limiter \"server\" (no-match)" 218 219 atf_check -s exit:0 -o ignore \ 220 ping -c 2 192.0.2.1 221 222 # This should now fail 223 atf_check -s exit:2 -o ignore \ 224 ping -c 2 192.0.2.1 225 226 jexec alcatraz pfctl -sLimiterStates 227 hardlim=$(jexec alcatraz pfctl -sLimiterStates | awk 'NR>1 { print $5; }') 228 if [ $hardlim -eq 0 ]; then 229 atf_fail "Hard limit not incremented" 230 fi 231} 232 233state_multiple_cleanup() 234{ 235 pft_cleanup 236} 237 238atf_test_case "source_basic" "cleanup" 239source_basic_head() 240{ 241 atf_set descr 'Basic source limiter test' 242 atf_set require.user root 243} 244 245source_basic_body() 246{ 247 pft_init 248 249 epair=$(vnet_mkepair) 250 251 ifconfig ${epair}a 192.0.2.2/24 up 252 ifconfig ${epair}a inet alias 192.0.2.3/24 up 253 254 vnet_mkjail alcatraz ${epair}b 255 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 256 257 # Sanity check 258 atf_check -s exit:0 -o ignore \ 259 ping -S 192.0.2.2 -c 1 192.0.2.1 260 atf_check -s exit:0 -o ignore \ 261 ping -S 192.0.2.3 -c 1 192.0.2.1 262 263 jexec alcatraz pfctl -e 264 265 # Allow up to one source for ICMP. 266 pft_set_rules alcatraz \ 267 "set timeout icmp.error 120" \ 268 "source limiter \"server\" id 1 entries 128 limit 1" \ 269 "block in proto icmp" \ 270 "pass in proto icmp source limiter \"server\" (no-match)" 271 272 atf_check -s exit:0 -o ignore \ 273 ping -S 192.0.2.2 -c 2 192.0.2.1 274 275 # This should now fail 276 atf_check -s exit:2 -o ignore \ 277 ping -S 192.0.2.2 -c 2 192.0.2.1 278 279 jexec alcatraz pfctl -sLimiterSrcs 280 hardlim=$(jexec alcatraz pfctl -sLimiterSrcs | awk 'NR>1 { print $5; }') 281 if [ $hardlim -eq 0 ]; then 282 atf_fail "Hard limit not incremented" 283 fi 284 285 # However, a different source will succeed 286 atf_check -s exit:0 -o ignore \ 287 ping -S 192.0.2.3 -c 2 192.0.2.1 288 289 atf_check -o match:"192.0.2.2/32 .*hardlim 2 ratelim 0" \ 290 -e ignore \ 291 jexec alcatraz pfctl -sLimiterSrcs -v 292 atf_check -o match:"192.0.2.3/32 .*hardlim 0 ratelim 0" \ 293 -e ignore \ 294 jexec alcatraz pfctl -sLimiterSrcs -v 295 296 # Kill the source entry 297 atf_check -s exit:0 -e ignore \ 298 jexec alcatraz pfctl -I 1 -k source -k 192.0.2.2 299 # Now we can ping again from it 300 atf_check -s exit:0 -o ignore \ 301 ping -S 192.0.2.2 -c 2 192.0.2.1 302} 303 304source_basic_cleanup() 305{ 306 pft_cleanup 307} 308 309atf_test_case "source_table" "cleanup" 310source_table_head() 311{ 312 atf_set descr 'Test the table of a source limiter' 313 atf_set require.user root 314} 315 316source_table_body() 317{ 318 pft_init 319 320 epair=$(vnet_mkepair) 321 322 ifconfig ${epair}a 192.0.2.2/24 up 323 324 vnet_mkjail alcatraz ${epair}b 325 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 326 327 # Sanity check 328 atf_check -s exit:0 -o ignore \ 329 ping -c 1 192.0.2.1 330 331 jexec alcatraz pfctl -e 332 333 # This used to panic a kernel with INVARIANTS: the table was changed 334 # while the packet was handled, with the rules lock held for reading. 335 pft_set_rules alcatraz \ 336 "set timeout icmp.error 120" \ 337 "table <many> persist" \ 338 "source limiter \"server\" id 1 entries 128 limit 100 table <many> above 2 below 2" \ 339 "pass in proto icmp source limiter \"server\"" 340 341 # Each ping creates a state. Two states do not exceed the mark. 342 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 343 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 344 sleep 1 345 atf_check -s exit:0 -o empty \ 346 jexec alcatraz pfctl -t many -T show 347 348 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 349 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 350 # The address is added to the table by a task. 351 sleep 1 352 atf_check -s exit:0 -o "match:192.0.2.2" \ 353 jexec alcatraz pfctl -t many -T show 354 355 # Once the states are gone the source is below the mark and is removed. 356 atf_check -s exit:0 -e ignore \ 357 jexec alcatraz pfctl -F states 358 sleep 1 359 atf_check -s exit:0 -o empty \ 360 jexec alcatraz pfctl -t many -T show 361} 362 363source_table_cleanup() 364{ 365 pft_cleanup 366} 367 368atf_test_case "source_table_marked" "cleanup" 369source_table_marked_head() 370{ 371 atf_set descr 'Test the removal of a source from a table after a replace' 372 atf_set require.user root 373} 374 375source_table_marked_body() 376{ 377 pft_init 378 379 epair=$(vnet_mkepair) 380 381 ifconfig ${epair}a 192.0.2.2/24 up 382 383 vnet_mkjail alcatraz ${epair}b 384 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 385 386 # Sanity check 387 atf_check -s exit:0 -o ignore \ 388 ping -c 1 192.0.2.1 389 390 jexec alcatraz pfctl -e 391 392 pft_set_rules alcatraz \ 393 "set timeout icmp.error 120" \ 394 "table <many> persist" \ 395 "source limiter \"server\" id 1 entries 128 limit 100 table <many> above 2 below 2" \ 396 "pass in proto icmp source limiter \"server\"" 397 398 # Three states put the source in the table. 399 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 400 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 401 atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 402 sleep 1 403 atf_check -s exit:0 -o "match:192.0.2.2" \ 404 jexec alcatraz pfctl -t many -T show 405 406 # A replace that keeps the address leaves its entry marked. 407 atf_check -s exit:0 -e ignore \ 408 jexec alcatraz pfctl -t many -T replace 192.0.2.2 192.0.2.99 409 410 # The marked entry used to be skipped when the source was removed. 411 atf_check -s exit:0 -e ignore \ 412 jexec alcatraz pfctl -F states 413 sleep 1 414 atf_check -s exit:0 -o "not-match:192.0.2.2" -o "match:192.0.2.99" \ 415 jexec alcatraz pfctl -t many -T show 416} 417 418source_table_marked_cleanup() 419{ 420 pft_cleanup 421} 422 423atf_test_case "out" "cleanup" 424out_head() 425{ 426 atf_set descr 'Test limiters on outbound rules' 427 atf_set require.user root 428} 429 430out_body() 431{ 432 pft_init 433 434 epair=$(vnet_mkepair) 435 436 ifconfig ${epair}a 192.0.2.2/24 up 437 ifconfig ${epair}a inet alias 192.0.2.3/24 up 438 439 vnet_mkjail alcatraz ${epair}b 440 jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up 441 442 # Sanity check 443 atf_check -s exit:0 -o ignore \ 444 ping -S 192.0.2.2 -c 1 192.0.2.1 445 atf_check -s exit:0 -o ignore \ 446 ping -S 192.0.2.3 -c 1 192.0.2.1 447 448 jexec alcatraz pfctl -e 449 450 # Allow up to one source for ICMP. 451 pft_set_rules alcatraz \ 452 "set timeout icmp.error 120" \ 453 "source limiter \"crash\" id 1 entries 10000 limit 1000" \ 454 "block out proto icmp" \ 455 "pass out from any to any source limiter \"crash\" keep state" 456 457 atf_check -s exit:0 -o ignore \ 458 ping -S 192.0.2.2 -c 2 192.0.2.1 459 460 jexec alcatraz pfctl -Fs 461} 462 463out_cleanup() 464{ 465 pft_cleanup 466} 467 468atf_init_test_cases() 469{ 470 atf_add_test_case "state_basic" 471 atf_add_test_case "state_rate" 472 atf_add_test_case "state_block" 473 atf_add_test_case "state_multiple" 474 atf_add_test_case "source_basic" 475 atf_add_test_case "source_table" 476 atf_add_test_case "source_table_marked" 477 atf_add_test_case "out" 478} 479