feat: enhance Proxmox Firewall functionality by increasing log limit, adding DHCP management during firewall state changes, and implementing IP geolocation features for improved user experience

This commit is contained in:
2025-12-02 07:17:20 -05:00
parent d8db472915
commit b100625a2b
2 changed files with 503 additions and 5 deletions

View File

@@ -21,7 +21,7 @@ class Firewall extends Component
public array $networkAdapters = [];
public array $adapterFirewallStates = [];
public array $firewallLogs = [];
public int $logLimit = 100;
public int $logLimit = 5000;
public array $availableIps = [];
public bool $showDeleteModal = false;
public ?int $deletingRulePos = null;
@@ -737,6 +737,7 @@ class Firewall extends Component
$segments = array_filter(array_map('trim', explode(',', $currentConfig)));
$newSegments = [];
$firewallFound = false;
$dhcpFound = false;
$currentFirewallState = $this->adapterFirewallStates[$adapterName] ?? false;
$newFirewallState = !$currentFirewallState;
@@ -754,6 +755,15 @@ class Firewall extends Component
$newSegments[] = 'firewall=1';
}
// If disabling, we skip adding firewall=1, effectively removing it
} elseif (strtolower($segmentKey) === 'dhcp') {
$dhcpFound = true;
// If enabling firewall, disable DHCP
if ($newFirewallState) {
$newSegments[] = 'dhcp=0';
} else {
// Keep existing DHCP setting when disabling firewall
$newSegments[] = $segment;
}
} else {
$newSegments[] = $segment;
}
@@ -764,6 +774,11 @@ class Firewall extends Component
$newSegments[] = 'firewall=1';
}
// If DHCP wasn't in the config and we're enabling firewall, explicitly disable DHCP
if (!$dhcpFound && $newFirewallState) {
$newSegments[] = 'dhcp=0';
}
$newConfig = implode(',', $newSegments);
$proxmox->request(
@@ -802,7 +817,10 @@ class Firewall extends Component
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
'get',
['limit' => $this->logLimit]
[
'limit' => $this->logLimit,
'start' => 0
]
);
if (! $response->successful()) {
@@ -1001,6 +1019,101 @@ class Firewall extends Component
return $result;
}
/**
* Check if an IP address is public (callable from frontend)
*/
public function checkIsPublicIP(string $ip): bool
{
return $this->isPublicIP($ip);
}
/**
* Check if an IP address is public (not private/localhost)
*/
protected function isPublicIP(string $ip): bool
{
$ip = $this->extractIPFromString($ip);
if (empty($ip)) {
return false;
}
// Check for localhost
if (in_array(strtolower($ip), ['localhost', '127.0.0.1', '::1'])) {
return false;
}
// Validate IP format
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
// Check if it's a private IP
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return true;
}
return false;
}
/**
* Extract IP address from a string (removes port if present)
*/
protected function extractIPFromString(string $input): string
{
$input = trim($input);
// Handle IPv6 with port: [2001:db8::1]:8080
if (preg_match('/^\[([^\]]+)\](?::\d+)?$/', $input, $matches)) {
return $matches[1];
}
// Handle IPv4 with port: 192.168.1.1:8080
if (preg_match('/^([^:]+):\d+$/', $input, $matches)) {
// Make sure it's not IPv6 without brackets
if (!str_contains($matches[1], ':')) {
return $matches[1];
}
}
// Return as-is if no port detected
return $input;
}
/**
* Fetch IP geolocation data
*/
public function fetchIPLocation($ip): array
{
if (empty($ip)) {
return ['status' => 'fail', 'message' => 'IP address is required'];
}
// Extract and validate IP address
$cleanIP = $this->extractIPFromString((string)$ip);
if (!$this->isPublicIP($cleanIP)) {
return ['status' => 'fail', 'message' => 'Not a valid public IP address'];
}
try {
// Use HTTP protocol to fetch geolocation data
$response = \Illuminate\Support\Facades\Http::timeout(5)->get("http://ip-api.com/json/{$cleanIP}");
if ($response->successful()) {
return $response->json();
}
return ['status' => 'fail', 'message' => 'Failed to fetch location data'];
} catch (\Exception $e) {
\Log::error('Error fetching IP location', [
'ip' => $cleanIP,
'error' => $e->getMessage(),
]);
return ['status' => 'fail', 'message' => 'Error: ' . $e->getMessage()];
}
}
public function render()
{
return view('proxmox::firewall')->layoutData([

View File

@@ -460,10 +460,238 @@
<td class="py-3 px-4 text-sm">
<span class="px-2 py-1 rounded text-xs font-semibold" :class="log.action_badge_class" x-text="log.action || '-' "></span>
</td>
<td class="py-3 px-4 text-sm" x-text="log.protocol || '-' "></td>
<td class="py-3 px-4 text-sm font-mono" x-text="log.source || '-' "></td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span x-text="log.protocol || '-'"></span>
<template x-if="log.protocol && log.protocol !== '-'">
<div class="relative" x-data="{ open: false, protocolDescription: window.getProtocolDescription(log.protocol) }" x-cloak>
<button type="button" @click="open = true" class="text-base/50 hover:text-base/70 transition-colors">
<x-ri-information-line class="size-3" />
</button>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" 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 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-information-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">Protocol: <span class="font-mono break-all" x-text="log.protocol"></span></h3>
</div>
<div class="text-sm text-base/80 prose prose-sm max-w-none" x-html="protocolDescription"></div>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono" x-text="log.source || '-'"></span>
<template x-if="log.source && log.source !== '-' && window.isPublicIP(log.source)">
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.source }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(ip).then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load location data' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" 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 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all" x-text="log.source"></span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm text-base/60">Loading location data...</p>
</div>
<div x-show="ipData && !loading">
<template x-if="ipData && (ipData.status === 'success')">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
<iframe
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
style="width: 100%; height: 100%; border: 0;"
frameborder="0"
loading="lazy"
allowfullscreen
title="Location Map"></iframe>
</div>
</div>
</div>
</template>
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
<div class="text-center py-4">
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm" x-text="log.source_port || '-' "></td>
<td class="py-3 px-4 text-sm font-mono" x-text="log.destination || '-' "></td>
<td class="py-3 px-4 text-sm">
<div class="flex items-center gap-1">
<span class="font-mono" x-text="log.destination || '-'"></span>
<template x-if="log.destination && log.destination !== '-' && window.isPublicIP(log.destination)">
<div class="relative" x-data="{ open: false, loading: false, ipData: null, ip: log.destination }" x-cloak>
<button type="button" @click="
if (!ipData && !loading) {
loading = true;
open = true;
window.fetchIPLocation(ip).then(data => {
ipData = data;
loading = false;
}).catch(err => {
ipData = { status: 'error', message: 'Failed to load location data' };
loading = false;
});
} else {
open = true;
}
" class="text-base/50 hover:text-base/70 transition-colors" title="View IP Location">
<x-ri-map-pin-line class="size-3" />
</button>
<!-- Modal -->
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
@click="open = false"
@keydown.escape.window="open = false"
class="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4"
style="display: none;">
<div @click.stop class="relative w-full max-w-4xl bg-background-secondary rounded-xl border border-neutral/20 shadow-2xl max-h-[90vh] overflow-y-auto overflow-x-hidden">
<button @click="open = false" 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 break-words">
<div class="flex items-center gap-2 mb-4">
<x-ri-map-pin-line class="size-5 text-primary flex-shrink-0" />
<h3 class="text-lg font-semibold break-words">IP Address: <span class="font-mono break-all" x-text="log.destination"></span></h3>
</div>
<div x-show="loading" class="text-center py-8">
<svg fill="currentColor" stroke="currentColor" viewBox="0 0 128 128" class="size-6 mx-auto mb-2">
<g>
<path stroke-width="5" d="M31.6 8.98a4.02 4.02 0 0 0 1.7 5.3l6.12 3.22a52.56 52.56 0 1 0 63.13 10.73L96 33.97a43.88 43.88 0 1 1-53-8.5l-3.65 7.4a4.02 4.02 0 0 0 1.72 5.3 3.73 3.73 0 0 0 5.13-1.67l7.66-15.63a4.18 4.18 0 0 0-1.75-5.4L36.74 7.32a3.73 3.73 0 0 0-5.13 1.66z" fill="currentColor" stroke="currentColor"/>
<animateTransform attributeName="transform" type="rotate" from="0 64 64" to="360 64 64" dur="1000ms" repeatCount="indefinite"></animateTransform>
</g>
</svg>
<p class="text-sm text-base/60">Loading location data...</p>
</div>
<div x-show="ipData && !loading">
<template x-if="ipData && (ipData.status === 'success')">
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">Country:</span> <span class="font-semibold text-sm break-words" x-text="ipData.country"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Region:</span> <span class="font-semibold text-sm break-words" x-text="ipData.regionName + ' (' + ipData.region + ')'"></span></div>
<div class="break-words"><span class="text-xs text-base/60">City:</span> <span class="font-semibold text-sm break-words" x-text="ipData.city"></span></div>
<div class="break-words"><span class="text-xs text-base/60">ZIP Code:</span> <span class="font-semibold text-sm break-words" x-text="ipData.zip"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Timezone:</span> <span class="font-semibold text-sm break-words" x-text="ipData.timezone"></span></div>
</div>
<div class="space-y-2 break-words">
<div class="break-words"><span class="text-xs text-base/60">ISP:</span> <span class="font-semibold text-sm break-words" x-text="ipData.isp"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Organization:</span> <span class="font-semibold text-sm break-words" x-text="ipData.org"></span></div>
<div class="break-words"><span class="text-xs text-base/60">AS:</span> <span class="font-semibold text-sm break-all" x-text="ipData.as"></span></div>
<div class="break-words"><span class="text-xs text-base/60">Coordinates:</span> <span class="font-semibold text-sm font-mono break-all" x-text="ipData.lat + ', ' + ipData.lon"></span></div>
</div>
</div>
<div x-show="ipData.lat && ipData.lon" class="mt-4 w-full">
<p class="text-xs text-base/60 mb-2">Location Map:</p>
<div class="w-full rounded-lg border border-neutral/20 overflow-hidden bg-background/50" style="position: relative; width: 100%; max-width: 100%; height: 256px; min-height: 256px;">
<iframe
x-bind:src="'https://www.openstreetmap.org/export/embed.html?bbox=' + encodeURIComponent((parseFloat(ipData.lon) - 0.01) + ',' + (parseFloat(ipData.lat) - 0.01) + ',' + (parseFloat(ipData.lon) + 0.01) + ',' + (parseFloat(ipData.lat) + 0.01)) + '&layer=mapnik&marker=' + encodeURIComponent(ipData.lat + ',' + ipData.lon)"
style="width: 100%; height: 100%; border: 0;"
frameborder="0"
loading="lazy"
allowfullscreen
title="Location Map"></iframe>
</div>
</div>
</div>
</template>
<template x-if="ipData && (ipData.status === 'fail' || ipData.status === 'error')">
<div class="text-center py-4">
<p class="text-sm text-warning" x-text="ipData.message || 'Failed to load location data'"></p>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</td>
<td class="py-3 px-4 text-sm" x-text="log.destination_port || '-' "></td>
</tr>
</template>
@@ -734,5 +962,162 @@
@endif
</div>
<script>
// Helper function to extract IP from string (removes port)
window.extractIP = function(input) {
if (!input) return '';
input = String(input).trim();
// Handle IPv6 with port: [2001:db8::1]:8080
const ipv6WithPortMatch = input.match(/^\[([^\]]+)\](?::\d+)?$/);
if (ipv6WithPortMatch) {
return ipv6WithPortMatch[1];
}
// Handle IPv4 with port: 192.168.1.1:8080
const ipv4WithPortMatch = input.match(/^([^:]+):\d+$/);
if (ipv4WithPortMatch && !ipv4WithPortMatch[1].includes(':')) {
return ipv4WithPortMatch[1];
}
return input;
};
// Check if IP is public
window.isPublicIP = function(ip) {
if (!ip || typeof ip !== 'string') {
return false;
}
ip = window.extractIP(ip);
// Check for localhost
if (['localhost', '127.0.0.1', '::1'].includes(ip.toLowerCase())) {
return false;
}
// Check if it's IPv4
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) {
const parts = ip.split('.').map(Number);
// Check for valid IPv4 range
if (parts.some(part => part > 255)) {
return false;
}
// Check for private ranges
if (parts[0] === 10) return false;
if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false;
if (parts[0] === 192 && parts[1] === 168) return false;
if (parts[0] === 127) return false;
if (parts[0] === 169 && parts[1] === 254) return false;
return true;
}
// Check if it's IPv6
if (ip.includes(':')) {
// 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 via server-side proxy
window.fetchIPLocation = async function(ip) {
if (!window.isPublicIP(ip)) {
return { status: 'fail', message: 'Not a public IP address' };
}
ip = window.extractIP(ip);
try {
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 formatStateDescription = (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 formatStateDescription('Unknown protocol', '', '');
}
const protocolUpper = protocol.toUpperCase();
const protocolDescriptions = {
'TCP': formatStateDescription(
'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': formatStateDescription(
'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': formatStateDescription(
'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': formatStateDescription(
'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] || formatStateDescription(
`${protocol} is a network protocol used for communication between devices.`,
'This protocol handles specific types of network traffic and follows particular rules for data transmission.',
''
);
};
</script>
</x-proxmox::layout>