175 lines
7.6 KiB
PHP
175 lines
7.6 KiB
PHP
<?php
|
|
/*
|
|
* pfsense-gwc.php — pfSense gateway-control helper for the unifi-wifi skill (SSH backend).
|
|
* Shipped to the box at /tmp by pfsense-ssh.sh and run with `php`, which bootstraps the full
|
|
* pfSense PHP env via config.inc (gives us $config + write_config()/filter_configure()).
|
|
*
|
|
* It is argv-driven — NO operator data is interpolated into PHP source (pfsense-ssh.sh passes
|
|
* everything as positional args), so there is no shell/PHP injection surface.
|
|
*
|
|
* php pfsense-gwc.php <action> <apply:0|1> [target] [val1] [val2]
|
|
*
|
|
* actions (reads ignore apply; writes are no-ops unless apply=1):
|
|
* pf-list list NAT port-forwards
|
|
* fw-list list filter (firewall) rules
|
|
* pf-disable <tracker|descr> 1 disable a port-forward (+ its associated filter rule)
|
|
* pf-enable <tracker|descr> 1 re-enable a port-forward
|
|
* pf-delete <tracker|descr> 1 delete a port-forward (+ associated filter rule)
|
|
* pf-set-ports <tracker|descr> 1 <dst> [<localport>] change destination port (+ local-port)
|
|
* pf-set-src <tracker|descr> 1 <cidr|any> restrict the source of a port-forward
|
|
* fw-disable <tracker|descr> 1 disable a filter rule
|
|
* fw-enable <tracker|descr> 1 re-enable a filter rule
|
|
*
|
|
* Filter rules are keyed on `tracker` (the `id` field is empty on pf25.07); match also accepts an
|
|
* exact case-insensitive `descr`. Enabled/disabled is the PRESENCE of a `disabled` key (= "" means off).
|
|
* Writes: backup config.xml to /tmp first, mutate, write_config(<note>), then filter_configure().
|
|
*/
|
|
|
|
// config.inc bootstraps the full pfSense env (incl. write_config() + filter_configure()).
|
|
// Do NOT re-require util/functions/filter — that triggers a "cannot redeclare" fatal.
|
|
ini_set("display_errors", "1");
|
|
error_reporting(E_ALL & ~E_DEPRECATED & ~E_NOTICE & ~E_WARNING);
|
|
require_once("config.inc");
|
|
|
|
global $config;
|
|
|
|
$ACTION = $argv[1] ?? "";
|
|
$APPLY = (($argv[2] ?? "0") === "1");
|
|
$TARGET = $argv[3] ?? "";
|
|
$VAL1 = $argv[4] ?? "";
|
|
$VAL2 = $argv[5] ?? "";
|
|
|
|
function gwc_out($s){ fwrite(STDOUT, $s . "\n"); }
|
|
function gwc_fail($s){ fwrite(STDOUT, "[FAIL] $s\n"); exit(1); }
|
|
|
|
/* on/off label for a config entry (disabled key present => off) */
|
|
function gwc_on_off($r){ return array_key_exists("disabled", $r) ? "off" : "on "; }
|
|
|
|
/* compact rendering of a source/destination object */
|
|
function gwc_addr($o){
|
|
if (!is_array($o)) return (string)$o;
|
|
if (array_key_exists("any", $o)) return "any";
|
|
$net = $o["network"] ?? ($o["address"] ?? "");
|
|
$port = isset($o["port"]) ? (":" . $o["port"]) : "";
|
|
return ($net !== "" ? $net : "?") . $port;
|
|
}
|
|
|
|
/* find a rule in $list by exact tracker, then exact case-insensitive descr; returns index or -1 */
|
|
function gwc_find_idx($list, $key){
|
|
foreach ($list as $i => $r) {
|
|
if ((string)($r["tracker"] ?? "") === (string)$key) return $i;
|
|
}
|
|
foreach ($list as $i => $r) {
|
|
if (strcasecmp((string)($r["descr"] ?? ""), (string)$key) === 0) return $i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* timestamped config backup to /tmp; returns the path (best-effort) */
|
|
function gwc_backup_config(){
|
|
$src = "/cf/conf/config.xml";
|
|
$dst = "/tmp/gwc-config-backup-" . date("Ymd-His") . ".xml";
|
|
if (@copy($src, $dst)) { gwc_out("[backup] $dst"); return $dst; }
|
|
gwc_out("[backup] WARN could not copy $src");
|
|
return "";
|
|
}
|
|
|
|
/* commit + reload */
|
|
function gwc_commit($note){
|
|
write_config($note);
|
|
gwc_out("[ok] config written: $note");
|
|
gwc_out("[reload] applying filter ...");
|
|
filter_configure();
|
|
gwc_out("[ok] filter reloaded");
|
|
}
|
|
|
|
$nat = &$config["nat"]["rule"];
|
|
$flt = &$config["filter"]["rule"];
|
|
if (!is_array($nat)) $nat = array();
|
|
if (!is_array($flt)) $flt = array();
|
|
|
|
switch ($ACTION) {
|
|
|
|
case "pf-list":
|
|
if (count($nat) === 0) { gwc_out(" (no NAT port-forwards configured)"); break; }
|
|
foreach ($nat as $i => $p) {
|
|
gwc_out(sprintf(" [%s] #%d tracker=%s '%s' %s if=%s dst=%s -> %s:%s src=%s",
|
|
gwc_on_off($p), $i, $p["tracker"] ?? "", $p["descr"] ?? "",
|
|
$p["protocol"] ?? "-", $p["interface"] ?? "?",
|
|
gwc_addr($p["destination"] ?? array()), $p["target"] ?? "?",
|
|
$p["local-port"] ?? "?", gwc_addr($p["source"] ?? array("any"=>""))));
|
|
}
|
|
break;
|
|
|
|
case "fw-list":
|
|
if (count($flt) === 0) { gwc_out(" (no filter rules)"); break; }
|
|
foreach ($flt as $i => $r) {
|
|
gwc_out(sprintf(" [%s] #%d tracker=%s %s if=%s proto=%s '%s' src=%s dst=%s",
|
|
gwc_on_off($r), $i, $r["tracker"] ?? "", $r["type"] ?? "?",
|
|
$r["interface"] ?? "?", $r["protocol"] ?? "-", $r["descr"] ?? "",
|
|
gwc_addr($r["source"] ?? array()), gwc_addr($r["destination"] ?? array())));
|
|
}
|
|
break;
|
|
|
|
case "pf-disable": case "pf-enable": case "pf-delete":
|
|
case "pf-set-ports": case "pf-set-src":
|
|
if ($TARGET === "") gwc_fail("$ACTION needs <tracker|descr>");
|
|
$i = gwc_find_idx($nat, $TARGET);
|
|
if ($i < 0) gwc_fail("port-forward '$TARGET' not found (try pf-list)");
|
|
$p = $nat[$i];
|
|
gwc_out(sprintf(" target: #%d tracker=%s '%s' [%s] dst=%s -> %s:%s src=%s",
|
|
$i, $p["tracker"] ?? "", $p["descr"] ?? "", gwc_on_off($p),
|
|
gwc_addr($p["destination"] ?? array()), $p["target"] ?? "?",
|
|
$p["local-port"] ?? "?", gwc_addr($p["source"] ?? array("any"=>""))));
|
|
$assoc = $p["associated-rule-id"] ?? "";
|
|
if (!$APPLY) { gwc_out(" [dry-run] add --apply to write."); break; }
|
|
gwc_backup_config();
|
|
if ($ACTION === "pf-delete") {
|
|
array_splice($nat, $i, 1);
|
|
if ($assoc !== "") {
|
|
foreach ($flt as $j => $r) {
|
|
if (($r["associated-rule-id"] ?? "") === $assoc) { array_splice($flt, $j, 1); break; }
|
|
}
|
|
}
|
|
gwc_commit("gwc: pf-delete '" . ($p["descr"] ?? $TARGET) . "'");
|
|
break;
|
|
}
|
|
if ($ACTION === "pf-disable") {
|
|
$nat[$i]["disabled"] = "";
|
|
foreach ($flt as $j => $r) if (($r["associated-rule-id"] ?? "") === $assoc && $assoc !== "") $flt[$j]["disabled"] = "";
|
|
} elseif ($ACTION === "pf-enable") {
|
|
unset($nat[$i]["disabled"]);
|
|
foreach ($flt as $j => $r) if (($r["associated-rule-id"] ?? "") === $assoc && $assoc !== "") unset($flt[$j]["disabled"]);
|
|
} elseif ($ACTION === "pf-set-ports") {
|
|
if ($VAL1 === "") gwc_fail("pf-set-ports needs <dstport> [<localport>]");
|
|
if (!is_array($nat[$i]["destination"])) $nat[$i]["destination"] = array();
|
|
$nat[$i]["destination"]["port"] = $VAL1;
|
|
$nat[$i]["local-port"] = ($VAL2 !== "" ? $VAL2 : $VAL1);
|
|
} elseif ($ACTION === "pf-set-src") {
|
|
if ($VAL1 === "" || strcasecmp($VAL1, "any") === 0) {
|
|
$nat[$i]["source"] = array("any" => "");
|
|
} else {
|
|
$nat[$i]["source"] = array("network" => $VAL1);
|
|
}
|
|
}
|
|
gwc_commit("gwc: $ACTION '" . ($p["descr"] ?? $TARGET) . "'");
|
|
break;
|
|
|
|
case "fw-disable": case "fw-enable":
|
|
if ($TARGET === "") gwc_fail("$ACTION needs <tracker|descr>");
|
|
$i = gwc_find_idx($flt, $TARGET);
|
|
if ($i < 0) gwc_fail("filter rule '$TARGET' not found (try fw-list)");
|
|
$r = $flt[$i];
|
|
gwc_out(sprintf(" target: #%d tracker=%s %s if=%s '%s' [%s]",
|
|
$i, $r["tracker"] ?? "", $r["type"] ?? "?", $r["interface"] ?? "?",
|
|
$r["descr"] ?? "", gwc_on_off($r)));
|
|
if (!$APPLY) { gwc_out(" [dry-run] add --apply to write."); break; }
|
|
gwc_backup_config();
|
|
if ($ACTION === "fw-disable") $flt[$i]["disabled"] = ""; else unset($flt[$i]["disabled"]);
|
|
gwc_commit("gwc: $ACTION '" . ($r["descr"] !== "" ? $r["descr"] : ($r["tracker"] ?? $TARGET)) . "'");
|
|
break;
|
|
|
|
default:
|
|
gwc_fail("unknown action '$ACTION'");
|
|
}
|