@@ -11,8 +11,44 @@ use RESTAPI\Models\RoutingGateway;
1111use RESTAPI \Models \TrafficShaper ;
1212use RESTAPI \Models \TrafficShaperLimiter ;
1313use RESTAPI \Models \TrafficShaperLimiterQueue ;
14+ use RESTAPI \Responses \ServerError ;
1415
1516class APIModelsFirewallRuleTestCase extends TestCase {
17+ /**
18+ * Reads the active ruleset directly from pfctl.
19+ */
20+ public function read_pfctl_rules (): Command {
21+ # Keywords that indicate pf is not ready yet
22+ $ not_ready_keywords = ['pfctl: DIOCGETRULE: Device busy ' ];
23+
24+ # Check the pf ruleset until it appears to be fully loaded, or until we've tried 5 times
25+ $ attempt = 0 ;
26+ $ max_attempts = 5 ;
27+ while ($ attempt < $ max_attempts ) {
28+ $ cmd = new Command ('/sbin/pfctl -sr ' );
29+ $ ready = true ;
30+
31+ foreach ($ not_ready_keywords as $ keyword ) {
32+ # pf is not ready if any of the keywords are found in the output
33+ if (str_contains ($ cmd ->output , $ keyword )) {
34+ $ ready = false ;
35+ $ attempt ++;
36+ sleep (1 );
37+ break ;
38+ }
39+ }
40+
41+ if ($ ready ) {
42+ return $ cmd ;
43+ }
44+ }
45+
46+ throw new ServerError (
47+ message: "pfctl ruleset was not ready after $ max_attempts attempts. " ,
48+ response_id: 'API_MODELS_FIREWALL_RULE_TEST_CASE_PFCTL_NOT_READY ' ,
49+ );
50+ }
51+
1652 /**
1753 * Checks that multiple interfaces cannot be assigned to a FirewallRule unless `floating` is enabled.
1854 */
@@ -508,20 +544,20 @@ class APIModelsFirewallRuleTestCase extends TestCase {
508544 $ rule ->create (apply: true );
509545
510546 # Ensure the rule with the queue is seen in pfctl
511- $ pfctl = new Command ( ' pfctl -sr ' );
547+ $ pfctl = $ this -> read_pfctl_rules ( );
512548 $ this ->assert_str_contains ($ pfctl ->output , 'ridentifier ' . $ rule ->tracker ->value . ' queue TestQueue1 ' );
513549
514550 # Update the rule to use a different queue and ensure it is seen in pfctl
515551 $ rule ->defaultqueue ->value = 'TestQueue2 ' ;
516552 $ rule ->update (apply: true );
517- $ pfctl = new Command ( ' pfctl -sr ' );
553+ $ pfctl = $ this -> read_pfctl_rules ( );
518554 $ this ->assert_str_contains ($ pfctl ->output , 'ridentifier ' . $ rule ->tracker ->value . ' queue TestQueue2 ' );
519555
520556 # Delete the rule and ensure the rule referencing the queue no longer exists
521557 $ rule ->delete ();
522558 $ shaper1 ->delete ();
523559 $ rule ->apply ();
524- $ pfctl = new Command ( ' pfctl -sr ' );
560+ $ pfctl = $ this -> read_pfctl_rules ( );
525561 $ this ->assert_str_does_not_contain (
526562 $ pfctl ->output ,
527563 'ridentifier ' . $ rule ->tracker ->value . ' queue TestQueue1 ' ,
@@ -577,7 +613,7 @@ class APIModelsFirewallRuleTestCase extends TestCase {
577613 $ rule ->create (apply: true );
578614
579615 # Ensure the rule with the queue is seen in pfctl
580- $ pfctl = new Command ( ' pfctl -sr ' );
616+ $ pfctl = $ this -> read_pfctl_rules ( );
581617 $ this ->assert_str_contains (
582618 $ pfctl ->output ,
583619 'ridentifier ' . $ rule ->tracker ->value . ' queue(TestQueue1, TestQueue2) ' ,
@@ -587,7 +623,7 @@ class APIModelsFirewallRuleTestCase extends TestCase {
587623 $ rule ->defaultqueue ->value = 'TestQueue2 ' ;
588624 $ rule ->ackqueue ->value = 'TestQueue1 ' ;
589625 $ rule ->update (apply: true );
590- $ pfctl = new Command ( ' pfctl -sr ' );
626+ $ pfctl = $ this -> read_pfctl_rules ( );
591627 $ this ->assert_str_contains (
592628 $ pfctl ->output ,
593629 'ridentifier ' . $ rule ->tracker ->value . ' queue(TestQueue2, TestQueue1) ' ,
@@ -597,7 +633,7 @@ class APIModelsFirewallRuleTestCase extends TestCase {
597633 $ rule ->delete ();
598634 $ shaper1 ->delete ();
599635 $ rule ->apply ();
600- $ pfctl = new Command ( ' pfctl -sr ' );
636+ $ pfctl = $ this -> read_pfctl_rules ( );
601637 $ this ->assert_str_does_not_contain (
602638 $ pfctl ->output ,
603639 'ridentifier ' . $ rule ->tracker ->value . ' queue(TestQueue1, TestQueue2) ' ,
@@ -795,10 +831,9 @@ class APIModelsFirewallRuleTestCase extends TestCase {
795831 async: false ,
796832 );
797833 $ rule ->create (apply: true );
798- sleep (3 ); // Wait a bit to ensure device is not busy
799834
800835 # Ensure the dnpipe is correctly represented
801- $ pfctl = new Command ( ' pfctl -sr ' );
836+ $ pfctl = $ this -> read_pfctl_rules ( );
802837 $ this ->assert_str_contains (
803838 $ pfctl ->output ,
804839 "ridentifier {$ rule ->tracker ->value } dnpipe {$ limiter ->number ->value }" ,
@@ -843,10 +878,9 @@ class APIModelsFirewallRuleTestCase extends TestCase {
843878 async: false ,
844879 );
845880 $ rule ->create (apply: true );
846- sleep (3 ); // Wait a bit to ensure device is not busy
847881
848882 # Check pfctl rules and ensure the dnpipe is correctly represented
849- $ pfctl = new Command ( ' pfctl -sr ' );
883+ $ pfctl = $ this -> read_pfctl_rules ( );
850884 $ this ->assert_str_contains (
851885 $ pfctl ->output ,
852886 "ridentifier {$ rule ->tracker ->value } dnqueue {$ queue ->number ->value }" ,
@@ -894,10 +928,9 @@ class APIModelsFirewallRuleTestCase extends TestCase {
894928 async: false ,
895929 );
896930 $ rule ->create (apply: true );
897- sleep (3 ); // Wait a bit to ensure device is not busy
898931
899932 # Check pfctl rules and ensure the dnpipe is correctly represented
900- $ pfctl = new Command ( ' pfctl -sr ' );
933+ $ pfctl = $ this -> read_pfctl_rules ( );
901934 $ this ->assert_str_contains (
902935 $ pfctl ->output ,
903936 "ridentifier {$ rule ->tracker ->value } dnpipe( {$ limiter1 ->number ->value }, {$ limiter2 ->number ->value }) " ,
0 commit comments