367 lines
22 KiB
PHP
367 lines
22 KiB
PHP
<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
|
|
|
<div class="space-y-6">
|
|
<!-- Firewall Status Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-shield-line class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Firewall Status</h2>
|
|
<p class="text-sm text-base/60 mt-1">Enable or disable the firewall for this virtual machine</p>
|
|
</div>
|
|
</div>
|
|
<label class="inline-flex items-center cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
wire:model.live="firewallEnabled"
|
|
wire:click="toggleFirewall"
|
|
:disabled="loading"
|
|
class="sr-only peer">
|
|
<div class="relative w-11 h-6 bg-gray-200 dark:bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
</label>
|
|
</div>
|
|
@if($error)
|
|
<div class="bg-error/10 border border-error/30 rounded-lg p-3 mt-4">
|
|
<p class="text-sm text-error">{{ $error }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if($firewallEnabled)
|
|
<!-- Firewall Rules Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-list-check class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Firewall Rules</h2>
|
|
<p class="text-sm text-base/60 mt-1">Manage allow and block rules for network traffic</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
wire:click="startCreateRule"
|
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
<x-ri-add-line class="size-4 inline mr-1" />
|
|
Add Rule
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Rule Form Modal -->
|
|
@if($editingRulePos !== null || (empty($rules) && !$editingRulePos))
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4 mb-4" x-data="{ showForm: true }" x-show="showForm">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">{{ $editingRulePos !== null ? 'Edit Rule' : 'Create New Rule' }}</h3>
|
|
<button @click="showForm = false; $wire.resetRuleForm(); $wire.editingRulePos = null" class="text-base/50 hover:text-base">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Action</label>
|
|
<select wire:model="ruleAction" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Protocol</label>
|
|
<select wire:model="ruleProto" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
<option value="">Any</option>
|
|
<option value="tcp">TCP</option>
|
|
<option value="udp">UDP</option>
|
|
<option value="icmp">ICMP</option>
|
|
<option value="icmpv6">ICMPv6</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Source</label>
|
|
<input type="text" wire:model="ruleSource" placeholder="e.g., 192.168.1.0/24" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Destination</label>
|
|
<input type="text" wire:model="ruleDest" placeholder="e.g., 10.0.0.0/8" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Destination Port</label>
|
|
<input type="text" wire:model="ruleDport" placeholder="e.g., 80,443 or 1000:2000" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Source Port</label>
|
|
<input type="text" wire:model="ruleSport" placeholder="e.g., 8080" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Interface</label>
|
|
<input type="text" wire:model="ruleIface" placeholder="e.g., net0" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Log Level</label>
|
|
<select wire:model="ruleLog" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
<option value="">No Logging</option>
|
|
<option value="nolog">No Log</option>
|
|
<option value="info">Info</option>
|
|
<option value="warning">Warning</option>
|
|
<option value="err">Error</option>
|
|
<option value="crit">Critical</option>
|
|
<option value="alert">Alert</option>
|
|
<option value="emerg">Emergency</option>
|
|
</select>
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-medium mb-2">Comment</label>
|
|
<input type="text" wire:model="ruleComment" placeholder="Optional description" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div class="md:col-span-2 flex items-center gap-2">
|
|
<input type="checkbox" wire:model="ruleEnable" id="ruleEnable" class="rounded border-neutral">
|
|
<label for="ruleEnable" class="text-sm">Enable this rule</label>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 mt-4">
|
|
<button
|
|
wire:click="saveRule"
|
|
wire:loading.attr="disabled"
|
|
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
Save Rule
|
|
</button>
|
|
<button
|
|
@click="showForm = false; $wire.resetRuleForm(); $wire.editingRulePos = null"
|
|
class="px-4 py-2 bg-background-secondary border border-neutral rounded-lg hover:bg-background transition-colors text-sm font-medium">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Rules Table -->
|
|
@if(count($rules) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-neutral">
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Position</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Destination</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Protocol</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Port</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Status</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($rules as $rule)
|
|
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
|
<td class="py-3 px-4 text-sm">{{ $rule['pos'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="px-2 py-1 rounded text-xs font-medium
|
|
{{ ($rule['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
|
|
{{ ($rule['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
|
|
{{ ($rule['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
|
|
{{ $rule['action'] ?? '-' }}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['source'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['dest'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ strtoupper($rule['proto'] ?? 'any') }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['dport'] ?? ($rule['sport'] ?? '-') }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
@if(isset($rule['enable']) && $rule['enable'] == 0)
|
|
<span class="px-2 py-1 rounded text-xs font-medium bg-base/20 text-base/60">Disabled</span>
|
|
@else
|
|
<span class="px-2 py-1 rounded text-xs font-medium bg-success/20 text-success">Enabled</span>
|
|
@endif
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<div class="flex gap-2">
|
|
<button
|
|
wire:click="startEditRule({{ $rule['pos'] ?? 0 }})"
|
|
class="text-primary hover:text-primary/80 transition-colors">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button
|
|
wire:click="deleteRule({{ $rule['pos'] ?? 0 }})"
|
|
wire:confirm="Are you sure you want to delete this rule?"
|
|
class="text-error hover:text-error/80 transition-colors">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 text-base/60">
|
|
<x-ri-list-check class="size-12 mx-auto mb-3 opacity-50" />
|
|
<p>No firewall rules configured</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Aliases Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-bookmark-line class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">Aliases</h2>
|
|
<p class="text-sm text-base/60 mt-1">Create named aliases for IP addresses and networks</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Alias Form -->
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4 mb-4">
|
|
<h3 class="text-lg font-semibold mb-4">Create New Alias</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Name</label>
|
|
<input type="text" wire:model="aliasName" placeholder="e.g., trusted-network" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">CIDR</label>
|
|
<input type="text" wire:model="aliasCidr" placeholder="e.g., 192.168.1.0/24" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Comment</label>
|
|
<input type="text" wire:model="aliasComment" placeholder="Optional description" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
</div>
|
|
<button
|
|
wire:click="createAlias"
|
|
wire:loading.attr="disabled"
|
|
class="mt-4 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
Create Alias
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Aliases Table -->
|
|
@if(count($aliases) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-neutral">
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Name</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">CIDR</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Comment</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($aliases as $alias)
|
|
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
|
<td class="py-3 px-4 text-sm font-medium">{{ $alias['name'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm"><code>{{ $alias['cidr'] ?? '-' }}</code></td>
|
|
<td class="py-3 px-4 text-sm">{{ $alias['comment'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<button
|
|
wire:click="deleteAlias('{{ $alias['name'] ?? '' }}')"
|
|
wire:confirm="Are you sure you want to delete this alias?"
|
|
class="text-error hover:text-error/80 transition-colors">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 text-base/60">
|
|
<x-ri-bookmark-line class="size-12 mx-auto mb-3 opacity-50" />
|
|
<p>No aliases configured</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<!-- IP Sets Card -->
|
|
<div class="bg-background border border-neutral rounded-lg p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
|
<x-ri-stack-line class="size-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">IP Sets</h2>
|
|
<p class="text-sm text-base/60 mt-1">Manage sets of IP addresses for use in firewall rules</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create IP Set Form -->
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4 mb-4">
|
|
<h3 class="text-lg font-semibold mb-4">Create New IP Set</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Name</label>
|
|
<input type="text" wire:model="ipsetName" placeholder="e.g., blocked-ips" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Comment</label>
|
|
<input type="text" wire:model="ipsetComment" placeholder="Optional description" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
|
</div>
|
|
</div>
|
|
<button
|
|
wire:click="createIpset"
|
|
wire:loading.attr="disabled"
|
|
class="mt-4 px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
Create IP Set
|
|
</button>
|
|
</div>
|
|
|
|
<!-- IP Sets Table -->
|
|
@if(count($ipsets) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<thead>
|
|
<tr class="border-b border-neutral">
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Name</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Comment</th>
|
|
<th class="text-left py-3 px-4 font-semibold text-sm">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($ipsets as $ipset)
|
|
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
|
<td class="py-3 px-4 text-sm font-medium">{{ $ipset['name'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $ipset['comment'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<button
|
|
wire:click="deleteIpset('{{ $ipset['name'] ?? '' }}')"
|
|
wire:confirm="Are you sure you want to delete this IP set?"
|
|
class="text-error hover:text-error/80 transition-colors">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<div class="text-center py-8 text-base/60">
|
|
<x-ri-stack-line class="size-12 mx-auto mb-3 opacity-50" />
|
|
<p>No IP sets configured</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@else
|
|
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-8 text-center">
|
|
<x-ri-shield-line class="size-16 mx-auto mb-4 opacity-50" />
|
|
<h3 class="text-lg font-semibold mb-2">Firewall is Disabled</h3>
|
|
<p class="text-base/60">Enable the firewall above to manage rules, aliases, and IP sets.</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
</x-proxmox::layout>
|
|
|