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..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 StringField $target; + public SpecialNetworkField $target; public PortField $local_port; public BooleanField $disabled; public BooleanField $nordr; @@ -65,7 +66,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( @@ -96,9 +97,17 @@ 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 SpecialNetworkField( 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_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..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 @@ -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: ['RESTAPI\Responses\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(); + }, + ); + } + } }