From f67eaed7c4cc8c98b085469afc35f10b38150a86 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 12 Jul 2025 08:06:24 -0600 Subject: [PATCH 1/5] feat(PortForward): add 'any' to protocol choices #721 --- .../files/usr/local/pkg/RESTAPI/Models/PortForward.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc index a43ec867..8ac0953a 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc @@ -65,7 +65,7 @@ class PortForward extends Model { ); $this->protocol = new StringField( required: true, - choices: ['tcp', 'udp', 'tcp/udp', 'icmp', 'esp', 'ah', 'gre', 'ipv6', 'igmp', 'pim', 'ospf'], + choices: ['any', 'tcp', 'udp', 'tcp/udp', 'icmp', 'esp', 'ah', 'gre', 'ipv6', 'igmp', 'pim', 'ospf'], help_text: 'The IP/transport protocol this port forward rule should match.', ); $this->source = new FilterAddressField( From 09dba926554e0e2e636dd06847a0b5ab3a566d9f Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 12 Jul 2025 08:23:10 -0600 Subject: [PATCH 2/5] feat(PortForward): allow 'target' to use interface IP #721 --- .../local/pkg/RESTAPI/Models/PortForward.inc | 16 +++++- .../Tests/APIModelsPortForwardTestCase.inc | 56 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc index 8ac0953a..3532817d 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc @@ -27,7 +27,7 @@ class PortForward extends Model { public PortField $source_port; public FilterAddressField $destination; public PortField $destination_port; - public StringField $target; + public FilterAddressField $target; public PortField $local_port; public BooleanField $disabled; public BooleanField $nordr; @@ -96,9 +96,19 @@ class PortForward extends Model { conditions: ['protocol' => ['tcp', 'udp', 'tcp/udp']], help_text: 'The destination port this port forward rule applies to. Set to `null` to allow any destination port.', ); - $this->target = new StringField( + $this->target = new FilterAddressField( required: true, - validators: [new IPAddressValidator(allow_ipv4: true, allow_ipv6: true, allow_alias: true)], + allow_ipaddr: true, + allow_subnet: false, + allow_alias: true, + allow_interface: false, + allow_interface_ip: true, + allow_interface_groups: false, + allow_any: false, + allow_invert: false, + allow_self: false, + allow_l2tp: false, + allow_pppoe: false, help_text: 'The IP address or alias of the internal host to forward matching traffic to.', ); $this->local_port = new PortField( diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc index 6e9862a0..b7acd585 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc @@ -3,8 +3,10 @@ namespace RESTAPI\Tests; use RESTAPI\Core\TestCase; +use RESTAPI\Models\FirewallAlias; use RESTAPI\Models\FirewallRule; use RESTAPI\Models\PortForward; +use RESTAPI\Responses\ValidationError; class APIModelsPortForwardTestCase extends TestCase { /** @@ -227,4 +229,58 @@ class APIModelsPortForwardTestCase extends TestCase { $rule_q = FirewallRule::query(associated_rule_id: $port_forward->associated_rule_id->value); $this->assert_is_false($rule_q->exists()); } + + /** + * Ensures the target field accepts IP addresses, aliases and interface IPs + */ + public function test_target_validation(): void { + # Create an alias to test with + $alias = new FirewallAlias(name: "testalias", type:"host"); + $alias->create(); + + # Set values we expect to be allowed vs disallowed + $allowed_values = ["1.2.3.4", "wan:ip", "testalias"]; + $disallowed_values = ["example.com", "wan", "self", "l2tp", "1.2.3.4/24"]; + + # Check each allowed value and ensure it does not throw an exception during validation + foreach ($allowed_values as $value) { + $this->assert_does_not_throw( + callable: function () use ($value) { + $port_forward = new PortForward( + data: [ + 'interface' => 'wan', + 'protocol' => 'tcp', + 'source' => 'any', + 'destination' => 'wan:ip', + 'destination_port' => '8443', + 'target' => $value, + 'local_port' => '4443', + ], + ); + $port_forward->validate(); + }, + ); + } + + # Check each disallowed value and ensure it throws an exception during validation + foreach ($disallowed_values as $value) { + $this->assert_throws( + exceptions: ["ValidationError"], + callable: function () use ($value) { + $port_forward = new PortForward( + data: [ + 'interface' => 'wan', + 'protocol' => 'tcp', + 'source' => 'any', + 'destination' => 'wan:ip', + 'destination_port' => '8443', + 'target' => $value, + 'local_port' => '4443', + ], + ); + $port_forward->validate(); + } + ); + } + } } From 397980a1270e396028d228e9bd22e5d1625295ba Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 12 Jul 2025 08:23:44 -0600 Subject: [PATCH 3/5] style: run prettier on changed files --- .../pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc index b7acd585..fbffd593 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc @@ -235,12 +235,12 @@ class APIModelsPortForwardTestCase extends TestCase { */ public function test_target_validation(): void { # Create an alias to test with - $alias = new FirewallAlias(name: "testalias", type:"host"); + $alias = new FirewallAlias(name: 'testalias', type: 'host'); $alias->create(); # Set values we expect to be allowed vs disallowed - $allowed_values = ["1.2.3.4", "wan:ip", "testalias"]; - $disallowed_values = ["example.com", "wan", "self", "l2tp", "1.2.3.4/24"]; + $allowed_values = ['1.2.3.4', 'wan:ip', 'testalias']; + $disallowed_values = ['example.com', 'wan', 'self', 'l2tp', '1.2.3.4/24']; # Check each allowed value and ensure it does not throw an exception during validation foreach ($allowed_values as $value) { @@ -265,7 +265,7 @@ class APIModelsPortForwardTestCase extends TestCase { # Check each disallowed value and ensure it throws an exception during validation foreach ($disallowed_values as $value) { $this->assert_throws( - exceptions: ["ValidationError"], + exceptions: ['ValidationError'], callable: function () use ($value) { $port_forward = new PortForward( data: [ @@ -279,7 +279,7 @@ class APIModelsPortForwardTestCase extends TestCase { ], ); $port_forward->validate(); - } + }, ); } } From a4109388cbc81b09b76ab3e238cab4f1a191f694 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 19 Jul 2025 12:33:35 -0600 Subject: [PATCH 4/5] fix(PortForward): use SpecialNetworkField instead of FilterAddressfield --- .../files/usr/local/pkg/RESTAPI/Models/PortForward.inc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc index 3532817d..eb87f2f2 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Models/PortForward.inc @@ -11,6 +11,7 @@ use RESTAPI\Fields\FilterAddressField; use RESTAPI\Fields\ForeignModelField; use RESTAPI\Fields\InterfaceField; use RESTAPI\Fields\PortField; +use RESTAPI\Fields\SpecialNetworkField; use RESTAPI\Fields\StringField; use RESTAPI\Fields\UnixTimeField; use RESTAPI\Responses\ServerError; @@ -27,7 +28,7 @@ class PortForward extends Model { public PortField $source_port; public FilterAddressField $destination; public PortField $destination_port; - public FilterAddressField $target; + public SpecialNetworkField $target; public PortField $local_port; public BooleanField $disabled; public BooleanField $nordr; @@ -96,7 +97,7 @@ class PortForward extends Model { conditions: ['protocol' => ['tcp', 'udp', 'tcp/udp']], help_text: 'The destination port this port forward rule applies to. Set to `null` to allow any destination port.', ); - $this->target = new FilterAddressField( + $this->target = new SpecialNetworkField( required: true, allow_ipaddr: true, allow_subnet: false, @@ -104,8 +105,6 @@ class PortForward extends Model { allow_interface: false, allow_interface_ip: true, allow_interface_groups: false, - allow_any: false, - allow_invert: false, allow_self: false, allow_l2tp: false, allow_pppoe: false, From 9a517cafdeb97ca421f92b4d7e618ea669b3eca6 Mon Sep 17 00:00:00 2001 From: Jared Hendrickson Date: Sat, 19 Jul 2025 12:33:56 -0600 Subject: [PATCH 5/5] test(PortForward): check for full exception fqcn --- .../local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc index fbffd593..c2e6da77 100644 --- a/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc +++ b/pfSense-pkg-RESTAPI/files/usr/local/pkg/RESTAPI/Tests/APIModelsPortForwardTestCase.inc @@ -265,7 +265,7 @@ class APIModelsPortForwardTestCase extends TestCase { # Check each disallowed value and ensure it throws an exception during validation foreach ($disallowed_values as $value) { $this->assert_throws( - exceptions: ['ValidationError'], + exceptions: ['RESTAPI\Responses\ValidationError'], callable: function () use ($value) { $port_forward = new PortForward( data: [