feat: enhance Proxmox firewall view with improved rule management features, including drag-and-drop reordering and dynamic log updates

This commit is contained in:
2025-12-04 11:47:40 -05:00
parent b7bf8deaea
commit d2805fd995

View File

@@ -503,34 +503,18 @@
@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>
<!-- Rules Table -->
@if(count($rules) > 0)
<div class="overflow-x-auto"
x-data="{
rules: @js($rules),
draggedIndex: null,
dragOverIndex: null,
<div class="bg-background border border-neutral rounded-lg p-6"
x-data="{
editingPositions: false,
rules: @js($rules),
draggedIndex: null,
dragOverIndex: null,
startDrag(event, index) {
if (!this.editingPositions) {
event.preventDefault();
return;
}
this.draggedIndex = index;
event.target.closest('tr').classList.add('dragging');
event.dataTransfer.effectAllowed = 'move';
@@ -593,6 +577,37 @@
return '';
}
}">
<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>
<div class="flex gap-2">
<button
@click="editingPositions = !editingPositions"
class="px-4 py-2 rounded-lg transition-colors text-sm font-medium border"
:class="editingPositions ? 'bg-primary text-white border-primary hover:bg-primary/90' : 'bg-background-secondary text-base border-neutral/20 hover:bg-background-secondary/80'"
:title="editingPositions ? 'Done editing positions' : 'Edit rule positions'">
<x-ri-draggable class="size-4 inline mr-1" />
<span x-text="editingPositions ? 'Done' : 'Edit Positions'"></span>
</button>
<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>
</div>
<!-- Rules Table -->
@if(count($rules) > 0)
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="border-b border-neutral">
@@ -610,7 +625,7 @@
<tbody>
<template x-for="(rule, index) in rules" :key="rule.pos">
<tr class="rule-row border-b border-neutral/20 hover:bg-background-secondary/50"
draggable="true"
:draggable="editingPositions"
@dragstart="startDrag($event, index)"
@dragend="endDrag($event)"
@dragover="dragOver($event, index)"
@@ -618,7 +633,7 @@
@drop="drop($event, index)"
:class="{ 'drag-over': dragOverIndex === index }">
<td class="py-3 px-2 text-sm">
<div class="drag-handle" title="Drag to reorder">
<div x-show="editingPositions" class="drag-handle" title="Drag to reorder">
<x-ri-menu-line class="size-4" />
</div>
</td>
@@ -683,7 +698,17 @@
</div>
<div>
<h2 class="text-xl font-semibold">Firewall Logs</h2>
<p class="text-sm text-base/60 mt-1">Recent firewall activity and blocked/allowed connections ({{ count($firewallLogs) }} entries loaded)</p>
<p class="text-sm text-base/60 mt-1">
Recent firewall activity and blocked/allowed connections
<span
class="inline-block transition-all duration-300"
wire:key="log-count-{{ count($firewallLogs) }}"
x-data="{ updated: false }"
x-init="$watch('$wire.firewallLogs', () => { updated = true; setTimeout(() => updated = false, 1000) })"
:class="{ 'text-success font-semibold scale-110': updated }">
({{ count($firewallLogs) }} entries loaded)
</span>
</p>
</div>
</div>
<button
@@ -941,9 +966,26 @@
if (this.currentPage > 1) {
this.currentPage--;
}
},
init() {
// Listen for Livewire updates to refresh logs with animation
this.$watch('$wire.firewallLogs', (newLogs) => {
if (newLogs && Array.isArray(newLogs)) {
this.isRefreshing = true;
// Small delay for animation effect
setTimeout(() => {
this.initialize(newLogs);
this.currentPage = 1;
this.isRefreshing = false;
}, 300);
}
});
}
}"
x-effect="if (currentPage > totalPages && totalPages > 0) currentPage = totalPages"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
class="space-y-4"
>
<!-- Filter Tabs -->
@@ -1194,7 +1236,11 @@
</thead>
<tbody>
<template x-for="(log, index) in paginatedLogs" :key="log.index || index">
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
<tr
class="border-b border-neutral/20 hover:bg-background-secondary/50 transition-all duration-200"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0">
<!-- Timestamp -->
<td class="py-3 px-4 text-sm" x-text="log.timestamp_display || '-' "></td>
@@ -2099,13 +2145,146 @@
<p class="text-base/60">Enable the firewall above to manage firewall rules.</p>
</div>
@endif
</div>
<!-- Add/Edit Rule Modal -->
@if($showRuleForm)
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black/50 backdrop-blur-sm"
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()">
<div class="bg-background border border-neutral rounded-lg p-6 shadow-lg max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto" wire:click.stop>
<div class="flex items-center justify-between mb-4">
@script
<script>
// Check if an IP address is public (not private/localhost)
window.isPublicIP = function(ip) {
if (!ip || ip === '—' || ip === '-' || typeof ip !== 'string') return false;
ip = ip.trim();
// Check for localhost
if (ip === 'localhost' || ip === '127.0.0.1' || ip === '::1') return false;
// Check for IPv4 private ranges
const ipv4PrivateRanges = [
/^10\./, // 10.0.0.0/8
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
/^192\.168\./, // 192.168.0.0/16
/^169\.254\./, // 169.254.0.0/16 (link-local)
/^127\./, // 127.0.0.0/8
/^0\./ // 0.0.0.0/8
];
for (const range of ipv4PrivateRanges) {
if (range.test(ip)) return false;
}
// Check for IPv6 private ranges (very simplified)
const ipv6PrivatePatterns = [
/^fe80:/i, // Link-local
/^fc00:/i, // Unique local
/^fd00:/i, // Unique local
/^::1$/, // Loopback
/^::/ // Unspecified
];
for (const pattern of ipv6PrivatePatterns) {
if (pattern.test(ip)) return false;
}
return true;
};
// Fetch IP location data
window.fetchIPLocation = async function(ip) {
try {
const response = await fetch(`https://ipapi.co/${ip}/json/`);
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch IP location:', error);
return null;
}
};
// Get protocol description
window.getProtocolDescription = function(protocol) {
if (!protocol) return 'Unknown Protocol';
const descriptions = {
'TCP': '<p class="mb-2"><strong>Transmission Control Protocol (TCP)</strong> is a connection-oriented protocol that provides reliable, ordered delivery of data between applications.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Connection-oriented with three-way handshake</li><li>Guaranteed delivery with error checking</li><li>Used by HTTP, HTTPS, FTP, SSH, and many other services</li></ul>',
'UDP': '<p class="mb-2"><strong>User Datagram Protocol (UDP)</strong> is a connectionless protocol that sends messages without establishing a connection.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Connectionless and lightweight</li><li>No guaranteed delivery or ordering</li><li>Lower latency than TCP</li><li>Used by DNS, DHCP, streaming, and gaming</li></ul>',
'ICMP': '<p class="mb-2"><strong>Internet Control Message Protocol (ICMP)</strong> is used for diagnostic and control purposes in IP networks.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Used by ping and traceroute tools</li><li>Reports errors and network conditions</li><li>Not used for data transfer</li></ul>',
'GRE': '<p class="mb-2"><strong>Generic Routing Encapsulation (GRE)</strong> is a tunneling protocol that encapsulates network layer protocols.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Used for VPNs and tunneling</li><li>Can encapsulate various protocols</li></ul>',
'ESP': '<p class="mb-2"><strong>Encapsulating Security Payload (ESP)</strong> provides confidentiality, authentication, and integrity in IPsec.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Part of IPsec protocol suite</li><li>Encrypts packet payload</li><li>Used in VPNs</li></ul>',
'AH': '<p class="mb-2"><strong>Authentication Header (AH)</strong> provides connectionless integrity and data origin authentication for IP datagrams.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Part of IPsec protocol suite</li><li>Provides authentication but not encryption</li></ul>',
'SCTP': '<p class="mb-2"><strong>Stream Control Transmission Protocol (SCTP)</strong> is a transport layer protocol similar to TCP and UDP.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Message-oriented like UDP</li><li>Reliable like TCP</li><li>Supports multi-homing and multi-streaming</li></ul>',
'IGMP': '<p class="mb-2"><strong>Internet Group Management Protocol (IGMP)</strong> manages IP multicast group membership.</p><ul class="list-disc ml-5 space-y-1 text-sm"><li>Used for multicast groups</li><li>Allows hosts to join/leave multicast groups</li></ul>'
};
return descriptions[protocol.toUpperCase()] || `<p>${protocol} protocol</p>`;
};
// Get port description
window.getPortDescription = function(port) {
if (!port || port === '-') return 'No specific port';
const portDescriptions = {
'20': 'FTP Data Transfer',
'21': 'FTP Control',
'22': 'SSH (Secure Shell)',
'23': 'Telnet',
'25': 'SMTP (Email)',
'53': 'DNS',
'80': 'HTTP (Web)',
'110': 'POP3 (Email)',
'143': 'IMAP (Email)',
'443': 'HTTPS (Secure Web)',
'465': 'SMTPS (Secure Email)',
'587': 'SMTP Submission',
'993': 'IMAPS (Secure IMAP)',
'995': 'POP3S (Secure POP3)',
'3306': 'MySQL Database',
'3389': 'RDP (Remote Desktop)',
'5432': 'PostgreSQL Database',
'5900': 'VNC',
'8080': 'HTTP Alternate',
'8443': 'HTTPS Alternate'
};
return portDescriptions[port] || 'Unknown Port - No standard service associated with this port number';
};
</script>
@endscript
</div>
</div>
</div>
<!-- Modals (outside main container to escape overflow-hidden) -->
<!-- Error Message Modal -->
@if($error && $showErrorModal)
<div class="fixed inset-0 flex items-center justify-center z-[9999] bg-black/50 backdrop-blur-sm"
wire:click="$set('error', ''); $set('showErrorModal', false)">
<div class="bg-error/20 border-2 border-error rounded-lg p-6 shadow-lg max-w-3xl w-full mx-4 max-h-[80vh] overflow-y-auto" wire:click.stop>
<div class="flex items-start gap-3">
<x-ri-error-warning-fill class="size-6 text-error flex-shrink-0 mt-0.5" />
<div class="flex-1">
<h3 class="text-lg font-semibold text-error mb-2 text-center">Firewall Error</h3>
<div class="bg-background/50 rounded p-3 border border-error/30">
<p class="text-sm text-error/90 leading-relaxed whitespace-pre-wrap break-words">{{ $error }}</p>
</div>
</div>
<button
wire:click="$set('error', ''); $set('showErrorModal', false)"
class="text-error/70 hover:text-error transition-colors flex-shrink-0">
<x-ri-close-line class="size-5" />
</button>
</div>
</div>
</div>
@endif
<!-- Add/Edit Rule Modal -->
@if($showRuleForm)
<div class="fixed inset-0 flex items-center justify-center z-[9999] bg-black/50 backdrop-blur-sm"
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()">
<div class="bg-background border border-neutral rounded-lg p-6 shadow-lg max-w-4xl w-full mx-4 max-h-[90vh] overflow-y-auto" wire:click.stop>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">{{ $editingRulePos !== null ? 'Edit Rule' : 'Create New Rule' }}</h3>
<button
wire:click="$set('showRuleForm', false); $set('editingRulePos', null); resetRuleForm()"
@@ -2247,12 +2426,12 @@
</div>
@endif
<!-- Delete Rule Confirmation Modal -->
@if($showDeleteModal && $deletingRulePos !== null)
@php
$ruleToDelete = collect($rules)->firstWhere('pos', $deletingRulePos);
@endphp
<div class="fixed inset-0 flex items-center justify-center z-50 bg-black/60 backdrop-blur-sm transition-opacity"
<!-- Delete Rule Confirmation Modal -->
@if($showDeleteModal && $deletingRulePos !== null)
@php
$ruleToDelete = collect($rules)->firstWhere('pos', $deletingRulePos);
@endphp
<div class="fixed inset-0 flex items-center justify-center z-[9999] bg-black/60 backdrop-blur-sm transition-opacity"
wire:click="cancelDeleteRule">
<div class="bg-background border border-neutral/30 rounded-xl p-6 shadow-2xl max-w-lg w-full mx-4 transform transition-all" wire:click.stop>
<div class="flex items-start gap-4 mb-6">
@@ -2340,204 +2519,5 @@
</div>
</div>
@endif
</div>
@script
<script>
// Check if an IP address is public (not private/localhost)
window.isPublicIP = function(ip) {
if (!ip || ip === '—' || ip === '-' || typeof ip !== 'string') return false;
ip = ip.trim();
// Check for localhost
if (ip === 'localhost' || ip === '127.0.0.1' || ip === '::1') return false;
// Check for IPv4 private ranges
const ipv4PrivateRanges = [
/^10\./, // 10.0.0.0/8
/^172\.(1[6-9]|2[0-9]|3[0-1])\./, // 172.16.0.0/12
/^192\.168\./, // 192.168.0.0/16
/^169\.254\./, // 169.254.0.0/16 (link-local)
/^127\./, // 127.0.0.0/8
/^0\./ // 0.0.0.0/8
];
// Check if it's IPv4
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (ipv4Regex.test(ip)) {
// Check against private ranges
if (ipv4PrivateRanges.some(range => range.test(ip))) {
return false;
}
return true;
}
// Check if it's IPv6
if (ip.includes(':')) {
// Check for IPv6 private ranges
// Link-local (fe80::/10)
if (/^fe[89ab][0-9a-fA-F]:/i.test(ip)) {
return false;
}
// Unique Local Address - ULA (fc00::/7)
if (/^f[cd][0-9a-fA-F]/i.test(ip)) {
return false;
}
// IPv4-mapped IPv6 (::ffff:x.x.x.x)
if (ip.startsWith('::ffff:')) {
const ipv4Part = ip.substring(7);
return window.isPublicIP(ipv4Part);
}
// If it's a valid-looking IPv6 and not in private ranges, consider it public
const ipv6Regex = /^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/;
return ipv6Regex.test(ip);
}
return false;
};
// Fetch IP geolocation data
window.fetchIPLocation = async function(ip) {
if (!window.isPublicIP(ip)) {
return { status: 'fail', message: 'Not a public IP address' };
}
ip = ip.trim();
try {
// Use Livewire to call server-side method
const data = await @this.call('fetchIPLocation', ip);
return data || { status: 'fail', message: 'No data returned' };
} catch (error) {
console.error('Error fetching IP location:', error);
return { status: 'fail', message: 'Failed to fetch location data: ' + (error.message || 'Unknown error') };
}
};
// Helper function to format descriptions with sections
const formatDescription = (overview, meaning, scenarios, notes = null) => {
let html = '<div class="space-y-4">';
if (overview) {
html += `<div><h4 class="font-semibold text-base mb-2">Overview</h4><p>${overview}</p></div>`;
}
if (meaning) {
html += `<div><h4 class="font-semibold text-base mb-2">What This Means</h4><p>${meaning}</p></div>`;
}
if (scenarios) {
html += `<div><h4 class="font-semibold text-base mb-2">Common Scenarios</h4>${scenarios}</div>`;
}
if (notes) {
html += `<div><h4 class="font-semibold text-base mb-2">Additional Notes</h4><p>${notes}</p></div>`;
}
html += '</div>';
return html;
};
// Protocol descriptions mapping
window.getProtocolDescription = function(protocol) {
if (!protocol || typeof protocol !== 'string') {
return formatDescription('Unknown protocol', '', '');
}
const protocolUpper = protocol.toUpperCase();
const protocolDescriptions = {
'TCP': formatDescription(
'TCP (Transmission Control Protocol) is a connection-oriented protocol that provides reliable, ordered, and error-checked delivery of data between applications.',
'TCP establishes a connection before data transfer and ensures data integrity through acknowledgments and retransmissions. It guarantees that data is delivered in order and without errors.',
'<p>Common uses include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>Web browsing (HTTP/HTTPS)</li><li>Email (SMTP, IMAP, POP3)</li><li>File transfers (FTP)</li><li>Remote access (SSH, RDP)</li><li>Database connections</li><li>API communications</li></ul>',
'TCP is the most commonly used transport protocol on the Internet and provides reliable communication at the cost of slightly higher overhead compared to UDP.'
),
'UDP': formatDescription(
'UDP (User Datagram Protocol) is a connectionless protocol that provides fast, low-overhead data transmission without guarantees of delivery, ordering, or duplicate protection.',
'UDP does not establish connections before sending data and does not provide reliability features like acknowledgments or retransmissions. It prioritizes speed over reliability.',
'<p>Common uses include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>DNS queries</li><li>Video streaming</li><li>Online gaming</li><li>VoIP (Voice over IP)</li><li>Real-time applications</li><li>DHCP (Dynamic Host Configuration Protocol)</li></ul>',
'UDP is ideal for applications where speed is more important than reliability, or where occasional packet loss is acceptable.'
),
'ICMP': formatDescription(
'ICMP (Internet Control Message Protocol) is a network layer protocol used for diagnostic and control purposes.',
'ICMP is used by network devices to send error messages and operational information. It is essential for network troubleshooting and does not carry application data.',
'<p>Common uses include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>Ping (echo request/reply) for connectivity testing</li><li>Traceroute for path discovery</li><li>Error reporting (destination unreachable, time exceeded)</li><li>Network diagnostics</li></ul>',
'ICMP is a stateless protocol and does not use connection states like TCP or UDP. It operates at the network layer alongside IP.'
),
'ICMPV6': formatDescription(
'ICMPv6 (Internet Control Message Protocol version 6) is the IPv6 equivalent of ICMP, used for error reporting and diagnostic functions in IPv6 networks.',
'ICMPv6 includes additional features beyond what ICMP provides for IPv4, including integrated support for address configuration and network discovery.',
'<p>Key features include:</p><ul class="list-disc list-inside space-y-1 ml-2 mt-2"><li>Neighbor Discovery Protocol (NDP) for IPv6</li><li>Path MTU Discovery for IPv6</li><li>Router discovery and configuration</li><li>Error reporting and diagnostics</li></ul>',
'ICMPv6 is essential for IPv6 network operation and provides functions that in IPv4 are handled by multiple protocols (ARP, ICMP, etc.).'
),
};
return protocolDescriptions[protocolUpper] || formatDescription(
`The protocol "${protocol}" is a network protocol identifier that defines the rules and format for data communication between network devices.`,
'Network protocols specify how data is formatted, transmitted, and received over a network.',
''
);
};
// Port descriptions mapping
window.getPortDescription = function(port) {
const portDescriptions = {
20: 'FTP Data Transfer - File Transfer Protocol data connection',
21: 'FTP Control - File Transfer Protocol control connection',
22: 'SSH - Secure Shell, used for secure remote login and command execution',
23: 'Telnet - Unencrypted remote terminal access (insecure)',
25: 'SMTP - Simple Mail Transfer Protocol for sending email',
53: 'DNS - Domain Name System for resolving domain names to IP addresses',
67: 'DHCP Server - Dynamic Host Configuration Protocol server',
68: 'DHCP Client - Dynamic Host Configuration Protocol client',
69: 'TFTP - Trivial File Transfer Protocol',
80: 'HTTP - Hypertext Transfer Protocol for web traffic',
88: 'Kerberos - Network authentication protocol',
110: 'POP3 - Post Office Protocol version 3 for receiving email',
119: 'NNTP - Network News Transfer Protocol',
123: 'NTP - Network Time Protocol for time synchronization',
135: 'MSRPC - Microsoft Remote Procedure Call',
139: 'NetBIOS - Network Basic Input/Output System',
143: 'IMAP - Internet Message Access Protocol for email',
161: 'SNMP - Simple Network Management Protocol',
162: 'SNMP Trap - Simple Network Management Protocol trap',
389: 'LDAP - Lightweight Directory Access Protocol',
443: 'HTTPS - Hypertext Transfer Protocol Secure (encrypted web traffic)',
445: 'SMB - Server Message Block for file sharing (Windows)',
465: 'SMTPS - SMTP over SSL/TLS',
514: 'Syslog - System logging protocol',
515: 'LPD/LPR - Line Printer Daemon/Line Printer Remote',
587: 'SMTP Submission - Mail submission port',
636: 'LDAPS - LDAP over SSL/TLS',
993: 'IMAPS - IMAP over SSL/TLS',
995: 'POP3S - POP3 over SSL/TLS',
1433: 'MSSQL - Microsoft SQL Server database',
1521: 'Oracle Database - Oracle database listener',
3306: 'MySQL - MySQL database server',
3389: 'RDP - Remote Desktop Protocol (Windows)',
5432: 'PostgreSQL - PostgreSQL database server',
5900: 'VNC - Virtual Network Computing remote desktop',
6379: 'Redis - Redis in-memory data store',
8080: 'HTTP Alternate - Alternative HTTP port, often used for web proxies',
8443: 'HTTPS Alternate - Alternative HTTPS port',
27017: 'MongoDB - MongoDB database server',
27018: 'MongoDB Shard - MongoDB sharding port',
27019: 'MongoDB Config - MongoDB configuration server',
};
// Check for common port ranges
if (port >= 1024 && port <= 49151) {
return 'Registered/User Port - Port in the registered range, commonly used by applications';
} else if (port >= 49152 && port <= 65535) {
return 'Dynamic/Private Port - Ephemeral port range used for temporary connections';
}
return portDescriptions[port] || 'Unknown Port - No standard service associated with this port number';
};
</script>
@endscript
</div>
</div>
</div>
</x-proxmox::layout>