879 lines
57 KiB
PHP
879 lines
57 KiB
PHP
<x-proxmox::layout :server="$server" view="firewall" :status="$status" :status-polling="true" :status-polling-interval="'2s'">
|
|
@if(!$status)
|
|
<x-proxmox::ui.alert variant="danger" icon="ri-error-warning-fill" class="mt-8">
|
|
<x-slot name="title">Unable to Load Server Status</x-slot>
|
|
<p class="text-gray-600 dark:text-gray-400 mb-3">Could not retrieve status from hypervisor:</p>
|
|
<ul class="list-disc list-inside text-gray-600 dark:text-gray-400 mb-3 space-y-1 text-sm">
|
|
<li>Temporary connectivity issues with the Proxmox cluster</li>
|
|
<li>The VM may be in an intermediate state</li>
|
|
<li>Network timeout while fetching data</li>
|
|
</ul>
|
|
<x-proxmox::ui.button wire:click="loadVmStatus" icon="ri-refresh-line">
|
|
Retry
|
|
</x-proxmox::ui.button>
|
|
</x-proxmox::ui.alert>
|
|
@else
|
|
|
|
{{-- Global Loader Styles --}}
|
|
<style>
|
|
.fw-loader {
|
|
width: 24px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 3px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 2px)/10px 5px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 3px), currentColor calc(100% - 2px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 3px), currentColor calc(100% - 2px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
.fw-loader-sm {
|
|
width: 16px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 2px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 1.5px)/6px 3px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 2px), currentColor calc(100% - 1.5px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
.fw-loader-lg {
|
|
width: 32px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 4px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 3px)/12px 6px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 4px), currentColor calc(100% - 3px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 4px), currentColor calc(100% - 3px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
.fw-loader-xl {
|
|
width: 50px;
|
|
aspect-ratio: 1;
|
|
border-radius: 50%;
|
|
padding: 6px;
|
|
background:
|
|
conic-gradient(from 135deg at top, currentColor 90deg, #0000 0) 0 calc(50% - 4px)/17px 8.5px,
|
|
radial-gradient(farthest-side at bottom left, #0000 calc(100% - 6px), currentColor calc(100% - 5px) 99%, #0000) top right/50% 50% content-box content-box,
|
|
radial-gradient(farthest-side at top, #0000 calc(100% - 6px), currentColor calc(100% - 5px) 99%, #0000) bottom/100% 50% content-box content-box;
|
|
background-repeat: no-repeat;
|
|
animation: fw-loader-spin 1s infinite linear;
|
|
}
|
|
@keyframes fw-loader-spin {
|
|
100% { transform: rotate(1turn); }
|
|
}
|
|
|
|
/* Firewall Toggle Switch */
|
|
.fw-toggle {
|
|
position: relative;
|
|
width: 64px;
|
|
height: 32px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.fw-toggle input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.fw-toggle-slider {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
|
border-radius: 32px;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(220, 38, 38, 0.3);
|
|
}
|
|
.fw-toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 24px;
|
|
height: 24px;
|
|
left: 4px;
|
|
bottom: 4px;
|
|
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
|
|
border-radius: 50%;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
|
}
|
|
.fw-toggle input:checked + .fw-toggle-slider {
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(34, 197, 94, 0.3);
|
|
}
|
|
.fw-toggle input:checked + .fw-toggle-slider::before {
|
|
transform: translateX(32px);
|
|
}
|
|
.fw-toggle:hover .fw-toggle-slider {
|
|
filter: brightness(1.1);
|
|
}
|
|
.fw-toggle input:disabled + .fw-toggle-slider {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
.fw-toggle-label {
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
transition: color 0.3s ease;
|
|
}
|
|
.fw-toggle-label-enabled {
|
|
color: #22c55e;
|
|
}
|
|
.fw-toggle-label-disabled {
|
|
color: #dc2626;
|
|
}
|
|
@keyframes fw-toggle-pulse {
|
|
0%, 100% { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 8px rgba(34, 197, 94, 0.3); }
|
|
50% { box-shadow: inset 0 2px 4px rgba(0,0,0,0.2), 0 2px 16px rgba(34, 197, 94, 0.5); }
|
|
}
|
|
.fw-toggle input:checked + .fw-toggle-slider {
|
|
animation: fw-toggle-pulse 2s infinite ease-in-out;
|
|
}
|
|
|
|
/* Small Toggle for Network Adapters */
|
|
.fw-toggle-sm {
|
|
position: relative;
|
|
width: 44px;
|
|
height: 24px;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
.fw-toggle-sm input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.fw-toggle-slider-sm {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
|
|
border-radius: 24px;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2), 0 1px 6px rgba(220, 38, 38, 0.3);
|
|
}
|
|
.fw-toggle-slider-sm::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 18px;
|
|
height: 18px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background: linear-gradient(135deg, #ffffff 0%, #f3f4f6 100%);
|
|
border-radius: 50%;
|
|
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
box-shadow: 0 1px 6px rgba(0,0,0,0.2);
|
|
}
|
|
.fw-toggle-sm input:checked + .fw-toggle-slider-sm {
|
|
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2), 0 1px 6px rgba(34, 197, 94, 0.3);
|
|
}
|
|
.fw-toggle-sm input:checked + .fw-toggle-slider-sm::before {
|
|
transform: translateX(20px);
|
|
}
|
|
.fw-toggle-sm:hover .fw-toggle-slider-sm {
|
|
filter: brightness(1.1);
|
|
}
|
|
.fw-toggle-sm input:disabled + .fw-toggle-slider-sm {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|
|
|
|
<div class="space-y-6" x-data="{ pageLoaded: false, activeTab: 'rules' }" x-init="setTimeout(() => pageLoaded = true, 50)">
|
|
|
|
{{-- Page Header --}}
|
|
<div :class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-2'" style="transition: all 0.4s ease-out;">
|
|
<x-proxmox::ui.section-header
|
|
title="Firewall Management"
|
|
icon="ri-shield-line"
|
|
subtitle="Configure firewall rules, policies, aliases, and IPSets for your virtual machine."
|
|
class="mb-0">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.help-modal title="Firewall Management" icon="ri-question-line">
|
|
<p class="text-sm text-base/90 mb-4">
|
|
The Firewall Management interface provides comprehensive control over your virtual machine's network security, including rule-based filtering, IP aliases, and security policies.
|
|
</p>
|
|
<div class="pt-4 border-t border-neutral/30 space-y-2">
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">Firewall Rules:</span> Create granular rules to allow or block specific traffic patterns.</p>
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">IP Aliases:</span> Define named groups of IP addresses for easier rule management.</p>
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">IP Sets:</span> Manage collections of IP addresses for advanced filtering.</p>
|
|
<p class="text-xs text-base/75"><span class="font-semibold text-base/90">Security Logs:</span> Monitor firewall activity and blocked attempts.</p>
|
|
</div>
|
|
</x-proxmox::ui.help-modal>
|
|
</x-slot>
|
|
</x-proxmox::ui.section-header>
|
|
</div>
|
|
|
|
{{-- Error Modal --}}
|
|
@if($error && $showErrorModal)
|
|
<x-proxmox::ui.modal title="Error" icon="ri-error-warning-line" icon-class="text-error" :show="true" wire:key="error-modal">
|
|
<x-slot name="header">
|
|
<button wire:click="$set('showErrorModal', false)" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
</x-slot>
|
|
<p class="text-sm text-base/70">{{ $error }}</p>
|
|
<x-slot name="footer">
|
|
<x-proxmox::ui.button wire:click="$set('showErrorModal', false)" variant="secondary">
|
|
Close
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.modal>
|
|
@endif
|
|
|
|
{{-- Master Firewall Control --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-sm transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.1s;">
|
|
<div class="flex items-center justify-between mb-5">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-shield-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Master Firewall Control</h2>
|
|
</div>
|
|
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-full {{ $status->status === 'running' ? 'bg-success/15 text-success' : 'bg-warning/15 text-warning' }}">
|
|
@if($status->status === 'running')
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full animate-pulse"></span>
|
|
Running
|
|
@else
|
|
<span class="w-1.5 h-1.5 bg-warning rounded-full"></span>
|
|
Stopped
|
|
@endif
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 rounded-xl bg-primary flex items-center justify-center">
|
|
<x-ri-shield-line class="size-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-bold">Firewall Status</h2>
|
|
<p class="text-sm mt-1">
|
|
@if($firewallEnabled)
|
|
<span class="inline-flex items-center gap-1.5 text-success font-medium">
|
|
<span class="relative flex h-2 w-2">
|
|
<span class="absolute inline-flex h-full w-full rounded-full bg-success opacity-75 animate-ping"></span>
|
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-success"></span>
|
|
</span>
|
|
Active - Your VM is protected
|
|
</span>
|
|
@else
|
|
<span class="inline-flex items-center gap-1.5 text-warning font-medium">
|
|
<x-ri-error-warning-line class="size-4" />
|
|
Disabled - No firewall protection
|
|
</span>
|
|
@endif
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<span class="fw-toggle-label {{ $firewallEnabled ? 'fw-toggle-label-enabled' : 'fw-toggle-label-disabled' }}">
|
|
{{ $firewallEnabled ? 'Enabled' : 'Disabled' }}
|
|
</span>
|
|
<label class="fw-toggle" wire:loading.class="pointer-events-none">
|
|
<input
|
|
type="checkbox"
|
|
wire:change="toggleFirewall"
|
|
wire:loading.attr="disabled"
|
|
@checked($firewallEnabled)
|
|
>
|
|
<span class="fw-toggle-slider"></span>
|
|
</label>
|
|
</div>
|
|
<span wire:loading wire:target="toggleFirewall" class="text-xs text-base/50 flex items-center gap-1.5">
|
|
<span class="fw-loader-sm text-primary"></span>
|
|
{{ $firewallEnabled ? 'Disabling...' : 'Enabling...' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
@if($firewallEnabled)
|
|
{{-- Firewall Configuration --}}
|
|
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
|
|
|
|
{{-- Default Policies --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.15s;">
|
|
<div class="flex items-center gap-2 px-5 py-4 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-filter-3-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Default Policies</h2>
|
|
</div>
|
|
|
|
<div class="p-5 space-y-4">
|
|
<x-proxmox::ui.select
|
|
wire:model="policyIn"
|
|
label="Incoming Traffic (Policy In)"
|
|
help="Default action for incoming traffic not matching any rule">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</x-proxmox::ui.select>
|
|
|
|
<x-proxmox::ui.select
|
|
wire:model="policyOut"
|
|
label="Outgoing Traffic (Policy Out)"
|
|
help="Default action for outgoing traffic not matching any rule">
|
|
<option value="ACCEPT">Accept</option>
|
|
<option value="REJECT">Reject</option>
|
|
<option value="DROP">Drop</option>
|
|
</x-proxmox::ui.select>
|
|
|
|
<x-proxmox::ui.button
|
|
wire:click="updatePolicies"
|
|
wire:loading.attr="disabled"
|
|
class="w-full mt-4">
|
|
<span wire:loading.remove wire:target="updatePolicies">Update Policies</span>
|
|
<span wire:loading wire:target="updatePolicies">Updating...</span>
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</section>
|
|
|
|
{{-- Network Adapters --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.2s;">
|
|
<div class="flex items-center gap-2 px-5 py-4 border-b border-gray-100 dark:border-gray-800">
|
|
<x-ri-router-line class="size-5 text-primary" />
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Network Adapters</h2>
|
|
</div>
|
|
|
|
<div class="p-5">
|
|
@if(count($networkAdapters) > 0)
|
|
<div class="space-y-3">
|
|
@foreach($networkAdapters as $adapter)
|
|
<div class="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class="font-semibold text-sm">{{ $adapter['name'] }}</span>
|
|
<x-proxmox::ui.badge :variant="($adapterFirewallStates[$adapter['name']] ?? false) ? 'success' : 'secondary'">
|
|
{{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'Active' : 'Off' }}
|
|
</x-proxmox::ui.badge>
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ ucfirst($adapter['model'] ?? 'Unknown') }}
|
|
@if($adapter['mac'])
|
|
<span class="ml-2">{{ $adapter['mac'] }}</span>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<span class="fw-toggle-label {{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'fw-toggle-label-enabled' : 'fw-toggle-label-disabled' }}">
|
|
{{ ($adapterFirewallStates[$adapter['name']] ?? false) ? 'On' : 'Off' }}
|
|
</span>
|
|
<label class="fw-toggle-sm" wire:loading.class="pointer-events-none">
|
|
<input
|
|
type="checkbox"
|
|
wire:change="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
|
wire:loading.attr="disabled"
|
|
wire:key="adapter-{{ $adapter['name'] }}"
|
|
@checked($adapterFirewallStates[$adapter['name']] ?? false)
|
|
>
|
|
<span class="fw-toggle-slider-sm"></span>
|
|
</label>
|
|
<span wire:loading wire:target="toggleAdapterFirewall('{{ $adapter['name'] }}')" class="text-xs text-base/50">
|
|
<span class="fw-loader-sm text-primary"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-router-line"
|
|
title="No Network Adapters"
|
|
description="No network adapters found on this VM" />
|
|
@endif
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{{-- Firewall Management Tabs --}}
|
|
<section class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm overflow-hidden transition-all duration-300 hover:shadow-md"
|
|
:class="pageLoaded ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-4'" style="transition: all 0.4s ease-out 0.25s;">
|
|
<div class="flex border-b border-gray-100 dark:border-gray-800 overflow-x-auto">
|
|
<button @click="activeTab = 'rules'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'rules' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-list-check class="size-4" />
|
|
<span>Firewall Rules</span>
|
|
@if(count($rules) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($rules) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'rules'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'aliases'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'aliases' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-bookmark-line class="size-4" />
|
|
<span>IP Aliases</span>
|
|
@if(count($aliases) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($aliases) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'aliases'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'ipsets'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'ipsets' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-folder-3-line class="size-4" />
|
|
<span>IP Sets</span>
|
|
@if(count($ipsets) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($ipsets) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'ipsets'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'logs'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'logs' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-file-list-3-line class="size-4" />
|
|
<span>Firewall Logs</span>
|
|
@if(count($firewallLogs) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($firewallLogs) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'logs'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
<button @click="activeTab = 'saved'"
|
|
class="px-6 py-4 text-sm font-semibold transition-all relative whitespace-nowrap"
|
|
:class="activeTab === 'saved' ? 'text-primary' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'">
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-save-3-line class="size-4" />
|
|
<span>Saved Configurations</span>
|
|
@if(count($savedConfigurations) > 0)
|
|
<span class="px-2 py-0.5 bg-primary text-white rounded-full text-xs font-bold">{{ count($savedConfigurations) }}</span>
|
|
@endif
|
|
</div>
|
|
<div x-show="activeTab === 'saved'" class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary"></div>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
{{-- RULES TAB --}}
|
|
<div x-show="activeTab === 'rules'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Firewall Rules</h3>
|
|
<button type="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 flex items-center gap-2">
|
|
<x-ri-add-line class="size-4" />
|
|
Add Rule
|
|
</button>
|
|
</div>
|
|
|
|
@if(count($rules) > 0)
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full">
|
|
<thead>
|
|
<tr class="border-b border-gray-100 dark:border-gray-800">
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Order</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Pos</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Action</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Direction</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Source</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Destination</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Protocol</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Ports</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Status</th>
|
|
<th class="text-left py-3 px-4 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($rules as $index => $rule)
|
|
<tr class="border-b border-gray-50 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
|
|
<td class="py-3 px-4 text-sm">
|
|
<div class="flex flex-col gap-1">
|
|
@if($index > 0)
|
|
<button type="button"
|
|
wire:click="reorderRules({{ $rule['pos'] }}, {{ $rules[$index - 1]['pos'] }})"
|
|
wire:key="move-up-{{ $index }}"
|
|
class="text-gray-400 hover:text-primary transition-colors p-0.5 hover:bg-primary/10 rounded"
|
|
title="Move up">
|
|
<x-ri-arrow-up-s-line class="size-4" />
|
|
</button>
|
|
@else
|
|
<div class="size-4 p-0.5"></div>
|
|
@endif
|
|
@if($index < count($rules) - 1)
|
|
<button type="button"
|
|
wire:click="reorderRules({{ $rule['pos'] }}, {{ $rules[$index + 1]['pos'] }})"
|
|
wire:key="move-down-{{ $index }}"
|
|
class="text-gray-400 hover:text-primary transition-colors p-0.5 hover:bg-primary/10 rounded"
|
|
title="Move down">
|
|
<x-ri-arrow-down-s-line class="size-4" />
|
|
</button>
|
|
@else
|
|
<div class="size-4 p-0.5"></div>
|
|
@endif
|
|
</div>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm font-mono">{{ $rule['pos'] }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-full {{ $rule['action'] === 'ACCEPT' ? 'bg-success/15 text-success' : ($rule['action'] === 'REJECT' ? 'bg-warning/15 text-warning' : 'bg-error/15 text-error') }}">
|
|
{{ $rule['action'] }}
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['direction'] ?? 'IN' }}</td>
|
|
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $rule['source'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $rule['dest'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">{{ $rule['proto'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm font-mono text-xs">{{ $rule['dport'] ?? $rule['sport'] ?? '-' }}</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-full {{ $rule['enable'] ? 'bg-success/15 text-success' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400' }}">
|
|
@if($rule['enable'])
|
|
<span class="w-1.5 h-1.5 bg-success rounded-full"></span>
|
|
Enabled
|
|
@else
|
|
Disabled
|
|
@endif
|
|
</span>
|
|
</td>
|
|
<td class="py-3 px-4 text-sm">
|
|
<div class="flex items-center gap-1">
|
|
<button wire:click="toggleRule({{ $rule['pos'] }})" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="{{ $rule['enable'] ? 'Disable' : 'Enable' }}">
|
|
@if($rule['enable'])
|
|
<x-ri-pause-circle-line class="size-4" />
|
|
@else
|
|
<x-ri-play-circle-line class="size-4" />
|
|
@endif
|
|
</button>
|
|
<button wire:click="startEditRule({{ $rule['pos'] }})" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="Edit">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteRule({{ $rule['pos'] }})" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-list-check"
|
|
title="No Firewall Rules"
|
|
description="Create your first firewall rule to control network traffic">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="startCreateRule" icon="ri-add-line">
|
|
Add Rule
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- ALIASES TAB --}}
|
|
<div x-show="activeTab === 'aliases'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">IP Aliases</h3>
|
|
<button type="button" wire:click="startCreateAlias" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
|
<x-ri-add-line class="size-4" />
|
|
Add Alias
|
|
</button>
|
|
</div>
|
|
|
|
@if(count($aliases) > 0)
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
@foreach($aliases as $alias)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 transition-colors">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h4 class="font-semibold text-sm truncate">{{ $alias['name'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ $alias['comment'] ?? 'No description' }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-2">
|
|
<button wire:click="startEditAlias('{{ $alias['name'] }}')" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="Edit">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteAlias('{{ $alias['name'] }}')" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
@if($alias['cidr'])
|
|
@foreach(explode(',', $alias['cidr']) as $cidr)
|
|
<div class="flex items-center gap-2">
|
|
<x-ri-global-line class="size-3 text-gray-400" />
|
|
<code class="text-xs font-mono bg-white dark:bg-gray-900 px-2 py-1 rounded">{{ trim($cidr) }}</code>
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-bookmark-line"
|
|
title="No IP Aliases"
|
|
description="Create IP aliases to group addresses for easier rule management">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="startCreateAlias" icon="ri-add-line">
|
|
Add Alias
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- IPSETS TAB --}}
|
|
<div x-show="activeTab === 'ipsets'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">IP Sets</h3>
|
|
<button type="button" wire:click="startCreateIpset" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium flex items-center gap-2">
|
|
<x-ri-add-line class="size-4" />
|
|
Add IP Set
|
|
</button>
|
|
</div>
|
|
|
|
@if(count($ipsets) > 0)
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
@foreach($ipsets as $ipset)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 transition-colors">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h4 class="font-semibold text-sm">{{ $ipset['name'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ $ipset['comment'] ?? 'No description' }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-2">
|
|
<button wire:click="startEditIpset('{{ $ipset['name'] }}')" class="text-gray-400 hover:text-primary transition-colors p-1 hover:bg-primary/10 rounded" title="Edit">
|
|
<x-ri-edit-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteIpset('{{ $ipset['name'] }}')" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between text-xs">
|
|
<span class="text-gray-500 dark:text-gray-400">Addresses:</span>
|
|
<span class="font-mono">{{ count($ipset['cidr'] ?? []) }}</span>
|
|
</div>
|
|
@if(isset($ipset['cidr']) && count($ipset['cidr']) > 0)
|
|
<div class="max-h-32 overflow-y-auto space-y-1">
|
|
@foreach(array_slice($ipset['cidr'], 0, 5) as $cidr)
|
|
<code class="text-xs font-mono bg-white dark:bg-gray-900 px-2 py-1 rounded block">{{ $cidr }}</code>
|
|
@endforeach
|
|
@if(count($ipset['cidr']) > 5)
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">... and {{ count($ipset['cidr']) - 5 }} more</p>
|
|
@endif
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-folder-3-line"
|
|
title="No IP Sets"
|
|
description="Create IP sets to manage collections of addresses for advanced filtering">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="startCreateIpset" icon="ri-add-line">
|
|
Add IP Set
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
|
|
{{-- LOGS TAB --}}
|
|
<div x-show="activeTab === 'logs'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Firewall Logs</h3>
|
|
<div class="flex items-center gap-2">
|
|
<x-proxmox::ui.button wire:click="refreshLogs" variant="secondary" size="sm" icon="ri-refresh-line">
|
|
Refresh
|
|
</x-proxmox::ui.button>
|
|
<x-proxmox::ui.button wire:click="clearLogs" variant="danger" size="sm" icon="ri-delete-bin-line">
|
|
Clear Logs
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
</div>
|
|
|
|
@if(count($firewallLogs) > 0)
|
|
<div class="space-y-3">
|
|
@foreach($firewallLogs as $log)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700">
|
|
<div class="flex items-start justify-between mb-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="inline-flex items-center gap-1.5 px-2 py-1 text-xs font-semibold rounded-full {{ $log['action'] === 'ACCEPT' ? 'bg-success/15 text-success' : 'bg-error/15 text-error' }}">
|
|
{{ $log['action'] }}
|
|
</span>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">{{ $log['time'] }}</span>
|
|
</div>
|
|
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">{{ $log['interface'] ?? 'N/A' }}</span>
|
|
</div>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Source</span>
|
|
<div class="font-mono">{{ $log['src'] ?? 'N/A' }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Destination</span>
|
|
<div class="font-mono">{{ $log['dst'] ?? 'N/A' }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Protocol</span>
|
|
<div>{{ $log['proto'] ?? 'N/A' }}</div>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400 uppercase">Port</span>
|
|
<div class="font-mono">{{ $log['dport'] ?? $log['sport'] ?? 'N/A' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-file-list-3-line"
|
|
title="No Firewall Logs"
|
|
description="Firewall activity will appear here when rules are triggered" />
|
|
@endif
|
|
</div>
|
|
|
|
{{-- SAVED CONFIGURATIONS TAB --}}
|
|
<div x-show="activeTab === 'saved'" x-transition>
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Saved Configurations</h3>
|
|
<x-proxmox::ui.button wire:click="saveCurrentConfiguration" icon="ri-save-3-line">
|
|
Save Current Config
|
|
</x-proxmox::ui.button>
|
|
</div>
|
|
|
|
@if(count($savedConfigurations) > 0)
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
@foreach($savedConfigurations as $config)
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-100 dark:border-gray-700 hover:border-gray-200 dark:hover:border-gray-600 transition-colors">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div class="min-w-0 flex-1">
|
|
<h4 class="font-semibold text-sm truncate">{{ $config['name'] ?? 'Configuration ' . $config['id'] }}</h4>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ $config['description'] ?? 'No description' }}</p>
|
|
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">{{ $config['created_at'] }}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 ml-2">
|
|
<button wire:click="loadConfiguration('{{ $config['id'] }}')" class="text-gray-400 hover:text-success transition-colors p-1 hover:bg-success/10 rounded" title="Load">
|
|
<x-ri-download-line class="size-4" />
|
|
</button>
|
|
<button wire:click="deleteConfiguration('{{ $config['id'] }}')" class="text-gray-400 hover:text-error transition-colors p-1 hover:bg-error/10 rounded" title="Delete">
|
|
<x-ri-delete-bin-line class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1 text-xs text-gray-500 dark:text-gray-400">
|
|
<div>Rules: {{ $config['rules_count'] ?? 0 }}</div>
|
|
<div>Aliases: {{ $config['aliases_count'] ?? 0 }}</div>
|
|
<div>IP Sets: {{ $config['ipsets_count'] ?? 0 }}</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<x-proxmox::ui.empty-state
|
|
icon="ri-save-3-line"
|
|
title="No Saved Configurations"
|
|
description="Save your current firewall configuration for backup or reuse">
|
|
<x-slot name="actions">
|
|
<x-proxmox::ui.button wire:click="saveCurrentConfiguration" icon="ri-save-3-line">
|
|
Save Current Config
|
|
</x-proxmox::ui.button>
|
|
</x-slot>
|
|
</x-proxmox::ui.empty-state>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{{-- Policy Info Modal --}}
|
|
@if($showPolicyInfoModal)
|
|
<div class="fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
|
<div class="relative w-full max-w-3xl bg-white dark:bg-gray-900 rounded-2xl border border-gray-200 dark:border-gray-800 shadow-2xl max-h-[85vh] overflow-y-auto">
|
|
<button wire:click="closePolicyInfoModal" class="absolute top-4 right-4 text-base/50 hover:text-base transition-colors z-10">
|
|
<x-ri-close-line class="size-5" />
|
|
</button>
|
|
<div class="p-6">
|
|
<div class="flex items-center gap-3 mb-6">
|
|
<div class="w-12 h-12 rounded-xl bg-primary flex items-center justify-center">
|
|
<x-ri-information-line class="size-6 text-white" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-bold">Understanding Firewall Policies</h3>
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">How default policies control your network traffic</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-6 prose prose-sm max-w-none">
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
|
|
<x-ri-arrow-down-line class="size-4 text-warning" />
|
|
Policy In (Incoming Traffic)
|
|
</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">Controls what happens to traffic coming INTO your VM when it doesn't match any explicit firewall rule.</p>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><strong class="text-success">ACCEPT:</strong> Allow all incoming traffic by default (permissive, requires explicit DENY rules)</li>
|
|
<li><strong class="text-warning">REJECT:</strong> Block incoming traffic and send rejection notification to sender</li>
|
|
<li><strong class="text-error">DROP:</strong> Silently discard incoming traffic without notifying sender (recommended)</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="p-4 bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
|
|
<x-ri-arrow-up-line class="size-4 text-info" />
|
|
Policy Out (Outgoing Traffic)
|
|
</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">Controls what happens to traffic going OUT of your VM when it doesn't match any explicit firewall rule.</p>
|
|
<ul class="space-y-2 text-sm">
|
|
<li><strong class="text-success">ACCEPT:</strong> Allow all outgoing traffic by default (recommended for most VMs)</li>
|
|
<li><strong class="text-warning">REJECT:</strong> Block outgoing traffic and notify your VM</li>
|
|
<li><strong class="text-error">DROP:</strong> Silently discard outgoing traffic</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="p-4 bg-warning border border-warning/30 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2 flex items-center gap-2">
|
|
<x-ri-lock-line class="size-4 text-warning" />
|
|
Creating a Whitelist Configuration
|
|
</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">To create a strict whitelist (only allow specific traffic):</p>
|
|
<ol class="list-decimal list-inside space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
|
<li>Set <strong>Policy In</strong> to <strong>DROP</strong></li>
|
|
<li>Set <strong>Policy Out</strong> to <strong>DROP</strong> (if restricting outbound)</li>
|
|
<li>Create explicit <strong>ACCEPT</strong> rules for allowed traffic</li>
|
|
<li>Everything not explicitly allowed will be blocked</li>
|
|
</ol>
|
|
</div>
|
|
|
|
<div class="p-4 bg-info border border-info/30 rounded-lg">
|
|
<h4 class="font-semibold text-base mb-2">Additional Filters</h4>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2"><strong>IP Filter:</strong> Validates that source/destination IP addresses in packets match the VM's assigned IPs. Helps prevent IP spoofing.</p>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300"><strong>MAC Filter:</strong> Validates that MAC addresses match the VM's network adapters. Provides additional layer of security.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end mt-6">
|
|
<button wire:click="closePolicyInfoModal" class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
|
Got it
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@endif
|
|
|
|
</div>
|
|
|
|
@endif
|
|
</x-proxmox::layout></content> |