Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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();
},
);
}
}
}