diff --git a/app/Actions/Firewall/AddUfwDenyRuleAction.php b/app/Actions/Firewall/AddUfwDenyRuleAction.php new file mode 100644 index 0000000..2d71caa --- /dev/null +++ b/app/Actions/Firewall/AddUfwDenyRuleAction.php @@ -0,0 +1,22 @@ +failed()) { + throw new RuntimeException('UFW deny failed: ' . $proc->errorOutput()); + } + } +} diff --git a/app/Actions/Firewall/AddUfwRuleAction.php b/app/Actions/Firewall/AddUfwRuleAction.php new file mode 100644 index 0000000..972eb71 --- /dev/null +++ b/app/Actions/Firewall/AddUfwRuleAction.php @@ -0,0 +1,22 @@ +failed()) { + throw new RuntimeException('UFW allow failed: ' . $proc->errorOutput()); + } + } +} diff --git a/app/Actions/Firewall/DeleteUfwRuleAction.php b/app/Actions/Firewall/DeleteUfwRuleAction.php new file mode 100644 index 0000000..2dfae29 --- /dev/null +++ b/app/Actions/Firewall/DeleteUfwRuleAction.php @@ -0,0 +1,22 @@ +failed()) { + throw new RuntimeException('UFW delete failed: ' . $proc->errorOutput()); + } + } +} diff --git a/app/Actions/Firewall/GetUfwRulesAction.php b/app/Actions/Firewall/GetUfwRulesAction.php new file mode 100644 index 0000000..1ba7b17 --- /dev/null +++ b/app/Actions/Firewall/GetUfwRulesAction.php @@ -0,0 +1,39 @@ +failed()) { + return []; + } + + $lines = preg_split("/\r?\n/", trim($proc->output())); + $rules = []; + foreach ($lines as $line) { + if (!preg_match('/^\s*\[(\s*\d+)\]\s+(.+?)\s+(ALLOW|DENY)\s+(IN|OUT)\s+(.+)$/i', $line, $m)) { + continue; + } + $number = (int) trim($m[1]); + $service = trim($m[2]); + $action = strtoupper(trim($m[3])); + $direction = strtoupper(trim($m[4])); + $from = trim($m[5]); + + $rules[] = [ + 'number' => $number, + 'service' => $service, + 'action' => $action, + 'direction' => $direction, + 'from' => $from, + ]; + } + return $rules; + } +} diff --git a/app/Actions/Firewall/GetUfwStatusAction.php b/app/Actions/Firewall/GetUfwStatusAction.php new file mode 100644 index 0000000..91cd6fe --- /dev/null +++ b/app/Actions/Firewall/GetUfwStatusAction.php @@ -0,0 +1,18 @@ +failed()) { + return 'unknown'; + } + return trim($proc->output()); + } +} diff --git a/app/Actions/Firewall/ToggleUfwAction.php b/app/Actions/Firewall/ToggleUfwAction.php new file mode 100644 index 0000000..4931a08 --- /dev/null +++ b/app/Actions/Firewall/ToggleUfwAction.php @@ -0,0 +1,20 @@ +failed()) { + throw new RuntimeException('UFW toggle failed: ' . $proc->errorOutput()); + } + return trim($proc->output()); + } +} diff --git a/app/Http/Controllers/FirewallController.php b/app/Http/Controllers/FirewallController.php new file mode 100644 index 0000000..b397da0 --- /dev/null +++ b/app/Http/Controllers/FirewallController.php @@ -0,0 +1,66 @@ +middleware(['auth', AdminMiddleware::class]); + } + + public function index(): \Inertia\Response + { + $status = (new GetUfwStatusAction())->execute(); + $rules = (new GetUfwRulesAction())->execute(); + return Inertia::render('Firewall/Index', compact('status', 'rules')); + } + + public function toggle(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'enabled' => 'required|boolean', + ]); + + $enable = (bool) $validated['enabled']; + (new ToggleUfwAction())->execute($enable); + + session()->flash('success', 'Firewall ' . ($enable ? 'enabled' : 'disabled') . ' successfully.'); + return redirect()->route('firewall.index'); + } + + public function store(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'rule' => 'required|string|min:2', + 'type' => 'required|string|in:allow,deny', + ]); + + if ($validated['type'] === 'allow') { + (new AddUfwRuleAction())->execute($validated['rule']); + } else { + (new \App\Actions\Firewall\AddUfwDenyRuleAction())->execute($validated['rule']); + } + + session()->flash('success', 'Rule ' . $validated['type'] . 'ed successfully.'); + return redirect()->route('firewall.index'); + } + + public function destroy(string $id): RedirectResponse + { + (new DeleteUfwRuleAction())->execute($id); + + session()->flash('success', 'Rule deleted successfully.'); + return redirect()->route('firewall.index'); + } +} diff --git a/laranode-scripts/bin/laranode-ufw.sh b/laranode-scripts/bin/laranode-ufw.sh new file mode 100644 index 0000000..6ee9f3b --- /dev/null +++ b/laranode-scripts/bin/laranode-ufw.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +set -euo pipefail + +cmd=${1:-} +arg=${2:-} + +run() { + if command -v ufw >/dev/null 2>&1; then + ufw "$@" + else + echo "ufw command not found" >&2 + exit 127 + fi +} + +case "$cmd" in + status) + run status | head -n1 + ;; + enable) + yes | run enable >/dev/null + echo "enabled" + ;; + disable) + yes | run disable >/dev/null + echo "disabled" + ;; + list) + run status numbered + ;; + allow) + if [ -z "${arg:-}" ]; then + echo "rule spec required" >&2 + exit 2 + fi + run allow $arg >/dev/null + echo "allowed: $arg" + ;; + deny) + if [ -z "${arg:-}" ]; then + echo "rule spec required" >&2 + exit 2 + fi + run deny $arg >/dev/null + echo "denied: $arg" + ;; + delete) + if [ -z "${arg:-}" ]; then + echo "rule id/spec required" >&2 + exit 2 + fi + if [[ "$arg" =~ ^[0-9]+$ ]]; then + yes | run delete "$arg" >/dev/null + else + yes | run delete $arg >/dev/null + fi + echo "deleted: $arg" + ;; + *) + echo "unknown command" >&2 + exit 2 + ;; +} diff --git a/resources/js/Layouts/Partials/SidebarNavi.jsx b/resources/js/Layouts/Partials/SidebarNavi.jsx index 788af07..4e0934a 100644 --- a/resources/js/Layouts/Partials/SidebarNavi.jsx +++ b/resources/js/Layouts/Partials/SidebarNavi.jsx @@ -72,7 +72,7 @@ const SidebarNavi = () => { {auth.user.role == 'admin' && (
| # | +Service/Port | +Action | +Direction | +From | +Actions | +
|---|---|---|---|---|---|
| {r.number} | +{r.service} | +{r.action} | +{r.direction} | +{r.from} | +
+
+
+ |
+