Skip to content

Commit

Permalink
feat: tcpflags, quick, state type in rules (#272)
Browse files Browse the repository at this point in the history
* feat: support statetype is fw rules

* fix: support quick apply rules

* feat: support tcpflags

* test: fixed synproxy gateway test

* feat: support openvpn and multi-if rules

* fix: corrected rule interface validation

* test: correct return id for rule gw test

* docs: added docs to detail new fields
  • Loading branch information
jaredhendrickson13 authored Sep 3, 2022
1 parent a38dc68 commit 5578989
Show file tree
Hide file tree
Showing 6 changed files with 720 additions and 32 deletions.
36 changes: 36 additions & 0 deletions pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4107,6 +4107,42 @@ function get($id, $data=[], $all=false) {
"return" => $id,
"message" => "Alias resolve interval must be numeric"
],
4243 => [
"status" => "bad request",
"code" => 400,
"return" => $id,
"message" => "Unknown firewall rule state type specified"
],
4244 => [
"status" => "bad request",
"code" => 400,
"return" => $id,
"message" => "Firewall rule protocol must be 'tcp' if state type is set to synproxy"
],
4245 => [
"status" => "bad request",
"code" => 400,
"return" => $id,
"message" => "Firewall rule gateway must be default if state type is set to synproxy"
],
4246 => [
"status" => "bad request",
"code" => 400,
"return" => $id,
"message" => "Unknown firewall rule TCP flag type specified"
],
4247 => [
"status" => "bad request",
"code" => 400,
"return" => $id,
"message" => "Firewall rule tcpflags1 (set) items must also be present in tcpflags2 (out of)"
],
4248 => [
"status" => "bad request",
"code" => 400,
"return" => $id,
"message" => "Firewall rules can only be applied to one interface unless it is a floating rule"
],

//5000-5999 reserved for /api/v1/user API calls
5000 => [
Expand Down
7 changes: 6 additions & 1 deletion pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ function sort_nat_rules($top=false, $data=null, $field=null) {
}

# Input a physical interface ID, or a descriptive interface name, and return the pfSense interface ID (lan,wan,optx)
function get_pfsense_if_id($interface, $include_carp=false, $include_ifgroup=false, $include_ipsec=false) {
function get_pfsense_if_id($interface, $include_carp=false, $include_ifgroup=false, $include_ipsec=false, $include_openvpn=false) {
# Variables
global $config;
# Loop through our config and check each interface for a physical ID match
Expand Down Expand Up @@ -402,6 +402,11 @@ function get_pfsense_if_id($interface, $include_carp=false, $include_ifgroup=fal
if ($include_ipsec and $interface === "enc0") {
return $interface;
}

# Only include the OpenVPN interface if explicitly requested and OpenVPN is configured
if ($include_openvpn and $interface === "openvpn" and $config["openvpn"]) {
return $interface;
}
}

# Check if a string is a virtual IPs unique ID
Expand Down
159 changes: 152 additions & 7 deletions pfSense-pkg-API/files/etc/inc/api/models/APIFirewallRuleCreate.inc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ require_once("api/framework/APIModel.inc");
require_once("api/framework/APIResponse.inc");

class APIFirewallRuleCreate extends APIModel {
public $tcpflag_options = ["fin", "syn", "rst", "psh", "ack", "urg", "ece", "cwr"];

# Create our method constructor
public function __construct() {
parent::__construct();
Expand Down Expand Up @@ -56,13 +58,37 @@ class APIFirewallRuleCreate extends APIModel {
private function __validate_interface() {
# Check for our required 'interface' payload value
if (isset($this->initial_data['interface'])) {
$this->initial_data['interface'] = APITools\get_pfsense_if_id($this->initial_data['interface'], false, true, true);
# Check that we found the request pfSense interface ID
if (is_string($this->initial_data["interface"])) {
$this->validated_data['interface'] = $this->initial_data['interface'];
} else {
$this->errors[] = APIResponse\get(4034);
# Convert value to array if it is not already and support comma separated strings.
if (!is_array($this->initial_data["interface"])) {
$this->initial_data["interface"] = explode(",", (string)$this->initial_data["interface"]);
}

# Only allow 1 interface to be specified unless this is a floating rule
if (!isset($this->validated_data["floating"]) and count($this->initial_data["interface"]) > 1) {
$this->errors[] = APIResponse\get(4248);
return;
}

# Initialize an array to store validated interfaces
$this->validated_data["interface"] = [];

# Loop through each specified interface and ensure it exists
foreach ($this->initial_data["interface"] as $if) {
# Find this interface's pfSense interface ID
$if = APITools\get_pfsense_if_id($if, false, true, true, true);

# Check that we found the request pfSense interface ID
if ($if) {
$this->validated_data["interface"][] = $if;
} else {
$this->errors[] = APIResponse\get(4040);
return;
}
}

# Remove duplicates and convert back to pfSense expected format
$this->validated_data["interface"] = array_unique($this->validated_data["interface"]);
$this->validated_data["interface"] = implode(",", $this->validated_data["interface"]);
} else {
$this->errors[] = APIResponse\get(4034);
}
Expand Down Expand Up @@ -339,6 +365,111 @@ class APIFirewallRuleCreate extends APIModel {
}
}

private function __validate_tcpflags_any() {
# Check for our optional 'tcpflags_any' payload value
if ($this->initial_data['tcpflags_any'] === true) {
$this->validated_data["tcpflags_any"] = true;
}
}

private function __validate_tcpflags2() {
# Only validate this field if 'tcpflags_any' is not set
if (!$this->validated_data["tcpflags_any"]) {
# Validate the optional 'tcpflags2' field
if (isset($this->initial_data["tcpflags2"])) {
# Initialize validated flags as empty array
$this->validated_data["tcpflags2"] = [];

# Ensure value is an array and support comma separated strings
if (!is_array($this->initial_data["tcpflags2"])) {
$this->initial_data["tcpflags2"] = explode(",", (string)$this->initial_data["tcpflags2"]);
}

# Loop through the specified TCP flags
foreach ($this->initial_data["tcpflags2"] as $tcpflag) {
# Ensure value is a valid tcp flag option
if (in_array($tcpflag, $this->tcpflag_options)) {
$this->validated_data["tcpflags2"][] = $tcpflag;
}
else {
$this->errors[] = APIResponse\get(4246);
return;
}
}

# Remove duplicate items if present and convert to csv as expected by pfSense
$this->validated_data["tcpflags2"] = array_unique($this->validated_data["tcpflags2"]);
$this->validated_data["tcpflags2"] = implode(",", $this->validated_data["tcpflags2"]);
}
}
}

private function __validate_tcpflags1() {
# Only validate this field if 'tcpflags_any' is not set
if (!$this->validated_data["tcpflags_any"]) {
# Validate the optional 'tcpflags1' field
if (isset($this->initial_data["tcpflags1"])) {
# Initialize validated flags as empty array
$this->validated_data["tcpflags1"] = [];

# Ensure value is an array and support comma separated strings
if (!is_array($this->initial_data["tcpflags1"])) {
$this->initial_data["tcpflags1"] = explode(",", (string)$this->initial_data["tcpflags1"]);
}

# Loop through the specified TCP flags
foreach ($this->initial_data["tcpflags1"] as $tcpflag) {
# Ensure value is a valid tcp flag option
if (in_array($tcpflag, $this->tcpflag_options)) {
# Ensure this value is also present in tcpflags2 (out of)
if (in_array($tcpflag, explode(",", $this->validated_data["tcpflags2"]))) {
$this->validated_data["tcpflags1"][] = $tcpflag;
}
else {
$this->errors[] = APIResponse\get(4247);
return;
}
}
else {
$this->errors[] = APIResponse\get(4246);
return;
}
}

# Remove duplicate items if present and convert to csv as expected by pfSense
$this->validated_data["tcpflags1"] = array_unique($this->validated_data["tcpflags1"]);
$this->validated_data["tcpflags1"] = implode(",", $this->validated_data["tcpflags1"]);
}
}
}

private function __validate_statetype() {
# Variables
$protocol = $this->validated_data["protocol"];
$gateway = $this->validated_data["gateway"];
$statetype_options = ["keep state", "sloppy state", "synproxy state"];

# Check for the optional 'statetype' field
if (isset($this->initial_data["statetype"])) {
# Require value to be supported state type
if (!in_array($this->initial_data["statetype"], $statetype_options)) {
$this->errors[] = APIResponse\get(4243);
}
# Require protocol to be 'tcp' if synproxy state is specified
elseif ($this->initial_data["statetype"] === "synproxy state" and $protocol !== "tcp") {
$this->errors[] = APIResponse\get(4244);
}
# Require the default gateway to be used if synproxy state is specified
elseif ($this->initial_data["statetype"] === "synproxy state" and $gateway) {
$this->errors[] = APIResponse\get(4245);
}
# Otherwise this value is valid.
else {
$this->validated_data["statetype"] = $this->initial_data["statetype"];
}
}
}

private function __validate_top() {
# Check for our optional 'top' payload value
if ($this->initial_data['top'] === true) {
Expand Down Expand Up @@ -368,7 +499,17 @@ class APIFirewallRuleCreate extends APIModel {
}
}

private function __validate_quick() {
# Only validate this field if floating is enabled
if ($this->validated_data["floating"]) {
if ($this->initial_data["quick"] === true) {
$this->validated_data["quick"] = "yes";
}
}
}

public function validate_payload($delay_tracker=true) {
$this->__validate_floating();
$this->__validate_type();
$this->__validate_interface();
$this->__validate_ipprotocol();
Expand All @@ -387,9 +528,13 @@ class APIFirewallRuleCreate extends APIModel {
$this->__validate_disabled();
$this->__validate_descr();
$this->__validate_log();
$this->__validate_tcpflags_any();
$this->__validate_tcpflags2();
$this->__validate_tcpflags1();
$this->__validate_statetype();
$this->__validate_top();
$this->__validate_floating();
$this->__validate_direction();
$this->__validate_quick();

# Delay generating the tracker. Reduces the likelihood of two rules getting the same tracker in looped calls.
# todo: this is a quick fix and still does not guarantee uniqueness, a better solution is needed
Expand Down
Loading

0 comments on commit 5578989

Please sign in to comment.