mirror of
https://github.com/crivion/laranode.git
synced 2026-09-03 06:24:09 +08:00
firewall: add/manage rules
This commit is contained in:
22
app/Actions/Firewall/AddUfwDenyRuleAction.php
Normal file
22
app/Actions/Firewall/AddUfwDenyRuleAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Firewall;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class AddUfwDenyRuleAction
|
||||
{
|
||||
public function execute(string $ruleSpec): void
|
||||
{
|
||||
$ruleSpec = trim($ruleSpec);
|
||||
if ($ruleSpec === '') {
|
||||
throw new RuntimeException('Empty rule spec');
|
||||
}
|
||||
$bin = config('laranode.laranode_bin_path') . '/laranode-ufw.sh';
|
||||
$proc = Process::run(['sudo', $bin, 'deny', $ruleSpec]);
|
||||
if ($proc->failed()) {
|
||||
throw new RuntimeException('UFW deny failed: ' . $proc->errorOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Actions/Firewall/AddUfwRuleAction.php
Normal file
22
app/Actions/Firewall/AddUfwRuleAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Firewall;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class AddUfwRuleAction
|
||||
{
|
||||
public function execute(string $ruleSpec): void
|
||||
{
|
||||
$ruleSpec = trim($ruleSpec);
|
||||
if ($ruleSpec === '') {
|
||||
throw new RuntimeException('Empty rule spec');
|
||||
}
|
||||
$bin = config('laranode.laranode_bin_path') . '/laranode-ufw.sh';
|
||||
$proc = Process::run(['sudo', $bin, 'allow', $ruleSpec]);
|
||||
if ($proc->failed()) {
|
||||
throw new RuntimeException('UFW allow failed: ' . $proc->errorOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
22
app/Actions/Firewall/DeleteUfwRuleAction.php
Normal file
22
app/Actions/Firewall/DeleteUfwRuleAction.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Firewall;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class DeleteUfwRuleAction
|
||||
{
|
||||
public function execute(string $idOrSpec): void
|
||||
{
|
||||
$idOrSpec = trim($idOrSpec);
|
||||
if ($idOrSpec === '') {
|
||||
throw new RuntimeException('Empty rule id/spec');
|
||||
}
|
||||
$bin = config('laranode.laranode_bin_path') . '/laranode-ufw.sh';
|
||||
$proc = Process::run(['sudo', $bin, 'delete', $idOrSpec]);
|
||||
if ($proc->failed()) {
|
||||
throw new RuntimeException('UFW delete failed: ' . $proc->errorOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
39
app/Actions/Firewall/GetUfwRulesAction.php
Normal file
39
app/Actions/Firewall/GetUfwRulesAction.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Firewall;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class GetUfwRulesAction
|
||||
{
|
||||
public function execute(): array
|
||||
{
|
||||
$bin = config('laranode.laranode_bin_path') . '/laranode-ufw.sh';
|
||||
$proc = Process::run(['sudo', $bin, 'list']);
|
||||
if ($proc->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;
|
||||
}
|
||||
}
|
||||
18
app/Actions/Firewall/GetUfwStatusAction.php
Normal file
18
app/Actions/Firewall/GetUfwStatusAction.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Firewall;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class GetUfwStatusAction
|
||||
{
|
||||
public function execute(): string
|
||||
{
|
||||
$bin = config('laranode.laranode_bin_path') . '/laranode-ufw.sh';
|
||||
$proc = Process::run(['sudo', $bin, 'status']);
|
||||
if ($proc->failed()) {
|
||||
return 'unknown';
|
||||
}
|
||||
return trim($proc->output());
|
||||
}
|
||||
}
|
||||
20
app/Actions/Firewall/ToggleUfwAction.php
Normal file
20
app/Actions/Firewall/ToggleUfwAction.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Firewall;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
class ToggleUfwAction
|
||||
{
|
||||
public function execute(bool $enable): string
|
||||
{
|
||||
$bin = config('laranode.laranode_bin_path') . '/laranode-ufw.sh';
|
||||
$cmd = $enable ? 'enable' : 'disable';
|
||||
$proc = Process::run(['sudo', $bin, $cmd]);
|
||||
if ($proc->failed()) {
|
||||
throw new RuntimeException('UFW toggle failed: ' . $proc->errorOutput());
|
||||
}
|
||||
return trim($proc->output());
|
||||
}
|
||||
}
|
||||
66
app/Http/Controllers/FirewallController.php
Normal file
66
app/Http/Controllers/FirewallController.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Actions\Firewall\AddUfwRuleAction;
|
||||
use App\Actions\Firewall\DeleteUfwRuleAction;
|
||||
use App\Actions\Firewall\GetUfwRulesAction;
|
||||
use App\Actions\Firewall\GetUfwStatusAction;
|
||||
use App\Actions\Firewall\ToggleUfwAction;
|
||||
use App\Http\Middleware\AdminMiddleware;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class FirewallController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
64
laranode-scripts/bin/laranode-ufw.sh
Normal file
64
laranode-scripts/bin/laranode-ufw.sh
Normal file
@@ -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
|
||||
;;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ const SidebarNavi = () => {
|
||||
{auth.user.role == 'admin' && (
|
||||
<li>
|
||||
<Link
|
||||
href="/admin/firewall"
|
||||
href={route('firewall.index')}
|
||||
className="relative flex flex-row items-center h-11 focus:outline-none hover:bg-gray-900 text-gray-300 border-l-4 border-transparent hover:border-indigo-900 pr-6"
|
||||
>
|
||||
<div>
|
||||
|
||||
122
resources/js/Pages/Firewall/Index.jsx
Normal file
122
resources/js/Pages/Firewall/Index.jsx
Normal file
@@ -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 (
|
||||
<AuthenticatedLayout
|
||||
header={
|
||||
<div className="flex flex-col xl:flex-row xl:justify-between max-w-7xl pr-5">
|
||||
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight flex items-center">
|
||||
<MdSecurity className='mr-2' />
|
||||
Firewall
|
||||
</h2>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`inline-flex items-center px-3 py-1 rounded-md text-sm font-medium ${isEnabled ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'}`}>
|
||||
{isEnabled ? 'Active' : 'Disabled'}
|
||||
</div>
|
||||
<ConfirmationButton doAction={toggleFirewall}>
|
||||
<button className={`p-2 rounded-lg transition-colors ${isEnabled ? 'bg-green-100 hover:bg-green-200 text-green-600 dark:bg-green-900 dark:hover:bg-green-800 dark:text-green-300' : 'bg-gray-100 hover:bg-gray-200 text-gray-600 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-400'}`}>
|
||||
{isEnabled ? <FaToggleOn className='w-5 h-5' /> : <FaToggleOff className='w-5 h-5' />}
|
||||
</button>
|
||||
</ConfirmationButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Head title="Firewall" />
|
||||
|
||||
<div className="max-w-7xl px-4 my-8">
|
||||
<form onSubmit={addRule} className="bg-white dark:bg-gray-850 p-4 rounded-md flex items-center space-x-3">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
placeholder="e.g. 22/tcp or proto tcp from 1.2.3.4 to any port 22"
|
||||
value={newRule}
|
||||
onChange={(e) => setNewRule(e.target.value)}
|
||||
/>
|
||||
<select
|
||||
className="bg-gray-100 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white"
|
||||
value={ruleType}
|
||||
onChange={(e) => setRuleType(e.target.value)}
|
||||
>
|
||||
<option value="allow">Allow</option>
|
||||
<option value="deny">Deny</option>
|
||||
</select>
|
||||
<button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">Add Rule</button>
|
||||
</form>
|
||||
|
||||
<div className="relative overflow-x-auto bg-white dark:bg-gray-850 mt-3">
|
||||
<table className="w-full text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
||||
<thead className="text-gray-700 uppercase bg-gray-200 dark:bg-gray-700 dark:text-gray-300 text-sm">
|
||||
<tr>
|
||||
<th className="px-6 py-3">#</th>
|
||||
<th className="px-6 py-3">Service/Port</th>
|
||||
<th className="px-6 py-3">Action</th>
|
||||
<th className="px-6 py-3">Direction</th>
|
||||
<th className="px-6 py-3">From</th>
|
||||
<th className="px-6 py-3">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-sm">
|
||||
{rules?.map((r, idx) => (
|
||||
<tr key={`rule-${idx}`} className="bg-white border-b text-gray-700 dark:text-gray-200 dark:bg-gray-850 dark:border-gray-700 border-gray-200">
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{r.number}</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{r.service}</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{r.action}</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{r.direction}</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">{r.from}</td>
|
||||
<td className="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<div className='flex items-center space-x-2'>
|
||||
<ConfirmationButton doAction={() => deleteRule(r.number)}>
|
||||
<TiDelete className='w-6 h-6 text-red-500' />
|
||||
</ConfirmationButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
);
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user