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' && (
  • diff --git a/resources/js/Pages/Firewall/Index.jsx b/resources/js/Pages/Firewall/Index.jsx new file mode 100644 index 0000000..8d7b343 --- /dev/null +++ b/resources/js/Pages/Firewall/Index.jsx @@ -0,0 +1,122 @@ +import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; +import { Head, usePage } from '@inertiajs/react'; +import { router } from '@inertiajs/react'; +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import ConfirmationButton from '@/Components/ConfirmationButton'; +import { MdSecurity } from 'react-icons/md'; +import { FaToggleOn, FaToggleOff } from 'react-icons/fa'; +import { TiDelete } from 'react-icons/ti'; + +export default function FirewallIndex({ status, rules }) { + const { auth } = usePage().props; + const [newRule, setNewRule] = useState(''); + const [ruleType, setRuleType] = useState('allow'); + + const isEnabled = (status || '').toLowerCase().includes('active') || (status || '').toLowerCase().includes('enabled'); + + const toggleFirewall = () => { + router.post(route('firewall.toggle'), { enabled: !isEnabled }, { + onBefore: () => toast(`${!isEnabled ? 'Enabling' : 'Disabling'} firewall...`), + onSuccess: () => router.reload({ only: ['status'] }), + onError: () => toast('Failed to toggle firewall') + }); + }; + + const addRule = (e) => { + e.preventDefault(); + if (!newRule.trim()) return; + router.post(route('firewall.store'), { rule: newRule.trim(), type: ruleType }, { + onBefore: () => toast('Adding rule...'), + onSuccess: () => { setNewRule(''); setRuleType('allow'); router.reload({ only: ['rules'] }); }, + onError: () => toast('Failed to add rule') + }); + }; + + const deleteRule = (idOrSpec) => { + router.delete(route('firewall.destroy', { id: idOrSpec }), { + onBefore: () => toast('Deleting rule...'), + onSuccess: () => router.reload({ only: ['rules'] }), + onError: () => toast('Failed to delete rule') + }); + }; + + return ( + +

    + + Firewall +

    +
    +
    + {isEnabled ? 'Active' : 'Disabled'} +
    + + + +
    +
    + } + > + + +
    +
    + setNewRule(e.target.value)} + /> + + +
    + +
    + + + + + + + + + + + + + {rules?.map((r, idx) => ( + + + + + + + + + ))} + +
    #Service/PortActionDirectionFromActions
    {r.number}{r.service}{r.action}{r.direction}{r.from} +
    + deleteRule(r.number)}> + + +
    +
    +
    +
    + + ); +} diff --git a/routes/web.php b/routes/web.php index 6667c23..7c640d8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use App\Http\Controllers\AccountsController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\FilemanagerController; +use App\Http\Controllers\FirewallController; use App\Http\Controllers\PHPManagerController; use App\Http\Controllers\ProfileController; use App\Http\Controllers\StatsHistoryController; @@ -43,6 +44,14 @@ Route::post('/mysql', [MysqlController::class, 'store'])->middleware(['auth'])-> Route::patch('/mysql', [MysqlController::class, 'update'])->middleware(['auth'])->name('mysql.update'); Route::delete('/mysql', [MysqlController::class, 'destroy'])->middleware(['auth'])->name('mysql.destroy'); +// Firewall [Admin] +Route::middleware(['auth', AdminMiddleware::class])->group(function () { + Route::get('/admin/firewall', [FirewallController::class, 'index'])->name('firewall.index'); + Route::post('/admin/firewall/toggle', [FirewallController::class, 'toggle'])->name('firewall.toggle'); + Route::post('/admin/firewall/rules', [FirewallController::class, 'store'])->name('firewall.store'); + Route::delete('/admin/firewall/rules/{id}', [FirewallController::class, 'destroy'])->name('firewall.destroy'); +}); + // Filemanager [Admin | User] Route::get('/filemanager', [FilemanagerController::class, 'index'])->middleware(['auth'])->name('filemanager'); Route::get('/filemanager/get-directory-contents', [FilemanagerController::class, 'getDirectoryContents'])->middleware(['auth'])->name('filemanager.getDirectorContents');