'VirtIO (Paravirtualized)', 'e1000' => 'Intel E1000', 'e1000e' => 'Intel E1000e', 'rtl8139' => 'Realtek RTL8139', 'vmxnet3' => 'VMware vmxnet3', 'pcnet' => 'AMD PCnet', ]; public function mount(Server $server): void { parent::mount($server); $this->loadGuestAgentNetworks(); $result = $this->loadVmInterfaces(); $this->interfaces = $result['interfaces']; $this->interfaceConfigs = $result['configs']; $this->interfaceSelections = collect($this->interfaces) ->map(fn ($meta) => $meta['model'] ?? 'virtio') ->toArray(); $this->interfaceMultiqueues = collect($this->interfaces) ->map(fn ($meta) => $meta['queues'] ?? 1) ->toArray(); $this->selectedInterface = array_key_first($this->interfaces) ?? 'net0'; $this->interfaceType = $this->interfaceSelections[$this->selectedInterface] ?? 'virtio'; $this->canRemoveInterfaces = count($this->interfaces) > 1; // Store original values for synced comparison if (empty($this->original)) { $this->original = [ 'interfaceSelections' => json_decode(json_encode($this->interfaceSelections), true), 'interfaceMultiqueues' => json_decode(json_encode($this->interfaceMultiqueues), true), ]; } } public function getCanUpdateInterfaceProperty(): bool { return isset($this->interfaces[$this->selectedInterface]['model'], $this->interfaceSelections[$this->selectedInterface]) && strtolower($this->interfaces[$this->selectedInterface]['model']) !== strtolower($this->interfaceSelections[$this->selectedInterface]); } protected function loadVmInterfaces(): array { try { $proxmox = new Proxmox; $config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $interfaces = []; $configs = []; foreach ($config as $key => $value) { if (!is_string($key) || !str_starts_with($key, 'net')) { continue; } $configs[$key] = (string) $value; $segments = array_filter(array_map('trim', explode(',', (string) $value))); $model = 'virtio'; $mac = null; $mtu = null; $linkdown = false; $queues = 1; foreach ($segments as $segment) { if (!str_contains($segment, '=')) { continue; } [$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2)); if (in_array(strtolower($segmentKey), array_keys($this->interfaceTypeOptions), true)) { $model = strtolower($segmentKey); $mac = $segmentValue; } elseif (strtolower($segmentKey) === 'bridge') { continue; } elseif (strtolower($segmentKey) === 'mtu') { $mtu = $segmentValue; } elseif (strtolower($segmentKey) === 'link_down' || strtolower($segmentKey) === 'linkdown') { $linkdown = (bool) $segmentValue; } elseif (strtolower($segmentKey) === 'queues') { $queues = max(1, (int) $segmentValue); } } $interfaces[$key] = [ 'model' => $model, 'mac' => $mac, 'mtu' => $mtu, 'linkdown' => $linkdown, 'queues' => $queues, ]; } // Add disconnected devices from stored configs foreach ($this->disconnectedConfigs as $key => $disconnectedConfig) { if (!isset($interfaces[$key]) && !empty($disconnectedConfig)) { $configs[$key] = $disconnectedConfig; $segments = array_filter(array_map('trim', explode(',', $disconnectedConfig))); $model = 'virtio'; $mac = null; $mtu = null; $queues = 1; foreach ($segments as $segment) { if (!str_contains($segment, '=')) { continue; } [$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2)); if (in_array(strtolower($segmentKey), array_keys($this->interfaceTypeOptions), true)) { $model = strtolower($segmentKey); $mac = $segmentValue; } elseif (strtolower($segmentKey) === 'bridge') { continue; } elseif (strtolower($segmentKey) === 'mtu') { $mtu = $segmentValue; } elseif (strtolower($segmentKey) === 'queues') { $queues = max(1, (int) $segmentValue); } } $interfaces[$key] = [ 'model' => $model, 'mac' => $mac, 'mtu' => $mtu, 'linkdown' => true, 'queues' => $queues, ]; } } if (empty($interfaces)) { $interfaces = [ 'net0' => [ 'model' => 'virtio', 'mac' => null, 'mtu' => null, 'linkdown' => false, 'queues' => 1, ], ]; $configs = ['net0' => '']; } [$interfaces, $configs] = $this->sortInterfaceArrays($interfaces, $configs); return [ 'interfaces' => $interfaces, 'configs' => $configs, ]; } catch (\Exception $e) { $interfaces = [ 'net0' => [ 'model' => 'virtio', 'mac' => null, 'mtu' => null, 'linkdown' => false, 'queues' => 1, ], ]; $configs = ['net0' => '']; [$interfaces, $configs] = $this->sortInterfaceArrays($interfaces, $configs); return [ 'interfaces' => $interfaces, 'configs' => $configs, ]; } } protected function loadGuestAgentNetworks() { try { $proxmox = new Proxmox; $response = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/agent/network-get-interfaces'); $json = $response->json(); // Check if the response indicates an error (VM not running, guest agent not available, etc.) if (isset($json['data']) && $json['data'] === null) { $this->guestAgentNetworks = []; return; } $this->guestAgentNetworks = $json['data']['result'] ?? []; } catch (RequestException $e) { // HTTP error (500, 400, etc.) - VM not running or guest agent unavailable $this->guestAgentNetworks = []; } catch (\Exception $e) { // Any other error $this->guestAgentNetworks = []; } } public function getNetworkInterfaceForIp($ipAddress) { foreach ($this->guestAgentNetworks as $interface) { if (isset($interface['ip-addresses'])) { foreach ($interface['ip-addresses'] as $ip) { if (isset($ip['ip-address']) && $ip['ip-address'] === $ipAddress->ip) { return $interface; } } } } return null; } public function getAdditionalGuestAgentAddresses() { $knownIps = $this->server->ipAddresses->pluck('ip')->toArray(); $additionalAddresses = []; foreach ($this->guestAgentNetworks as $interface) { if (isset($interface['ip-addresses'])) { foreach ($interface['ip-addresses'] as $ip) { if (isset($ip['ip-address']) && !in_array($ip['ip-address'], $knownIps)) { $additionalAddresses[] = [ 'ip' => $ip['ip-address'], 'type' => $ip['ip-address-type'] ?? 'ipv4', 'prefix' => $ip['prefix'] ?? null, 'interface' => $interface, ]; } } } } return $additionalAddresses; } public function getCidrSuffix($ipAddress): ?string { $netmask = $ipAddress->netmask ?? null; if (empty($netmask)) { return null; } if (is_numeric($netmask)) { return '/' . (int) $netmask; } $packed = @inet_pton($netmask); if ($packed === false) { return null; } $bits = 0; foreach (str_split($packed) as $char) { $bits += substr_count(sprintf('%08b', ord($char)), '1'); } return '/' . $bits; } public function getCpuCores(): int { try { $proxmox = new Proxmox; $config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; // CPU cores can be specified with 'cores' or 'cpus' in Proxmox config $cores = (int) ($config['cores'] ?? $config['cpus'] ?? 1); return max(1, $cores); } catch (\Exception $e) { // Fallback to 1 if unable to retrieve return 1; } } public function isPublicIp($ip, $type = 'ipv4') { if ($type === 'ipv6' || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { // Check for IPv6 private/local addresses if ($ip === '::1' || strpos($ip, '::1/') === 0) { return false; // localhost } // Link-local (fe80::/10) if (preg_match('/^fe[89ab][0-9a-f]:/i', $ip)) { return false; } // Unique local (fc00::/7) - fc00::/8 and fd00::/8 if (preg_match('/^fc[0-9a-f]{1,2}:/i', $ip) || preg_match('/^fd[0-9a-f]{1,2}:/i', $ip)) { return false; } // IPv4-mapped IPv6 addresses if (strpos($ip, '::ffff:') === 0 || strpos($ip, '::ffff:') !== false) { return false; } return true; } else { // Check for IPv4 private/local addresses $parts = explode('.', $ip); if (count($parts) !== 4) { return false; } $first = (int)$parts[0]; $second = (int)$parts[1]; // 10.0.0.0/8 if ($first === 10) { return false; } // 172.16.0.0/12 if ($first === 172 && $second >= 16 && $second <= 31) { return false; } // 192.168.0.0/16 if ($first === 192 && $second === 168) { return false; } // 127.0.0.0/8 (localhost) if ($first === 127) { return false; } // 169.254.0.0/16 (link-local) if ($first === 169 && $second === 254) { return false; } return true; } } /** * Check if Network Devices settings are synced with the original values. */ public function getNetworkDevicesSyncedProperty(): bool { // If original hasn't been loaded yet, consider synced if (!isset($this->original['interfaceSelections'])) { return true; } $originalInterfaceSelections = isset($this->original['interfaceSelections']) && is_array($this->original['interfaceSelections']) ? $this->original['interfaceSelections'] : []; $currentInterfaceSelections = is_array($this->interfaceSelections) ? $this->interfaceSelections : []; // Normalize arrays: sort by keys and compare ksort($originalInterfaceSelections); ksort($currentInterfaceSelections); if (count($originalInterfaceSelections) !== count($currentInterfaceSelections)) { return false; } foreach ($originalInterfaceSelections as $key => $value) { if (!isset($currentInterfaceSelections[$key]) || $currentInterfaceSelections[$key] !== $value) { return false; } } // Also check interfaceMultiqueues $originalInterfaceMultiqueues = isset($this->original['interfaceMultiqueues']) && is_array($this->original['interfaceMultiqueues']) ? $this->original['interfaceMultiqueues'] : []; $currentInterfaceMultiqueues = is_array($this->interfaceMultiqueues) ? $this->interfaceMultiqueues : []; ksort($originalInterfaceMultiqueues); ksort($currentInterfaceMultiqueues); if (count($originalInterfaceMultiqueues) !== count($currentInterfaceMultiqueues)) { return false; } foreach ($originalInterfaceMultiqueues as $key => $value) { if (!isset($currentInterfaceMultiqueues[$key]) || $currentInterfaceMultiqueues[$key] !== $value) { return false; } } return true; } public function render() { return view('proxmox::network')->layoutData([ 'sidebar' => true, ]); } public function updateInterfaceType(): void { if (!isset($this->interfaces[$this->selectedInterface], $this->interfaceSelections[$this->selectedInterface])) { $this->notify('Select a valid network device.', 'error'); return; } $currentInterface = $this->interfaces[$this->selectedInterface]; $currentModel = strtolower($currentInterface['model'] ?? 'virtio'); try { $targetType = strtolower($this->interfaceSelections[$this->selectedInterface] ?? $currentModel); if (!array_key_exists($targetType, $this->interfaceTypeOptions)) { $this->notify('Invalid interface type selected.', 'error'); return; } $proxmox = new Proxmox; $currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $currentValue = (string) ($currentConfig[$this->selectedInterface] ?? ''); $segments = array_filter(array_map('trim', explode(',', $currentValue))); $macValue = $currentInterface['mac'] ?? null; $otherSegments = []; foreach ($segments as $segment) { if (!str_contains($segment, '=')) { $otherSegments[] = $segment; continue; } [$key, $value] = array_map('trim', explode('=', $segment, 2)); $lowerKey = strtolower($key); if (array_key_exists($lowerKey, $this->interfaceTypeOptions)) { // This is the existing model definition, capture MAC and skip so it can be replaced. $macValue = $value; continue; } $otherSegments[] = $key . '=' . $value; } if ($macValue === null || $macValue === '') { $macValue = $this->generateMacAddress(); } $newSegments = [ $targetType . '=' . $macValue, ]; foreach ($otherSegments as $segment) { $newSegments[] = $segment; } $newValue = implode(',', $newSegments); try { $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$this->selectedInterface => $newValue] ); } catch (\Exception $e) { // Check if this is a hotplug error (occurs when server is powered on) // These errors don't break functionality - the config is still saved $errorMessage = strtolower($e->getMessage()); if (str_contains($errorMessage, 'hotplug') || str_contains($errorMessage, 'hot-unplugging')) { // Suppress the error notification but continue to update the interface state // The configuration is still applied, just with a hotplug warning from Proxmox } else { // Re-throw non-hotplug errors to be handled below throw $e; } } $this->interfaces[$this->selectedInterface]['model'] = $targetType; $this->interfaces[$this->selectedInterface]['mac'] = $macValue; $this->interfaceSelections[$this->selectedInterface] = $targetType; // Update original values after successful save $this->original = [ 'interfaceSelections' => json_decode(json_encode($this->interfaceSelections), true), 'interfaceMultiqueues' => json_decode(json_encode($this->interfaceMultiqueues), true), ]; $this->notify(strtoupper($this->selectedInterface) . ' updated to ' . strtoupper($targetType) . '.', 'success'); } catch (\Exception $e) { $this->notify('Failed to update interface type. You must power off the VPS before making this change. Error: ' . $e->getMessage(), 'error'); } } protected function generateMacAddress(): string { $bytes = random_bytes(6); $bytes[0] = chr((ord($bytes[0]) | 0x02) & 0xfe); // locally administered, unicast return implode(':', array_map(fn ($char) => sprintf('%02x', ord($char)), str_split($bytes))); } public function removeInterface(?string $name = null): void { $target = $name ?? $this->selectedInterface; if (!$this->canRemoveInterfaces || !isset($this->interfaces[$target])) { $this->notify('At least one network device is required.', 'error'); return; } try { $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', ['delete' => $target] ); // Refresh interfaces to get updated state $this->refreshInterfaces(); $this->notify('Network device removed successfully.', 'success'); } catch (\Exception $e) { $this->notify('Failed to remove network device: ' . $e->getMessage(), 'error'); } } public function refreshInterfaces(): void { $result = $this->loadVmInterfaces(); $this->interfaces = $result['interfaces']; $this->interfaceConfigs = $result['configs']; $this->interfaceSelections = collect($this->interfaces) ->map(fn ($meta) => $meta['model'] ?? 'virtio') ->toArray(); $this->interfaceMultiqueues = collect($this->interfaces) ->map(fn ($meta) => $meta['queues'] ?? 1) ->toArray(); $this->selectedInterface = array_key_first($this->interfaces) ?? 'net0'; $this->interfaceType = $this->interfaceSelections[$this->selectedInterface] ?? 'virtio'; $this->canRemoveInterfaces = count($this->interfaces) > 1; // Update original values after refresh $this->original = [ 'interfaceSelections' => json_decode(json_encode($this->interfaceSelections), true), 'interfaceMultiqueues' => json_decode(json_encode($this->interfaceMultiqueues), true), ]; $this->notify('Network interfaces refreshed.', 'success'); } public function queueInterfaceChange(string $name): void { $this->selectedInterface = $name; $this->interfaceSelections[$name] = $this->interfaceSelections[$name] ?? ($this->interfaces[$name]['model'] ?? 'virtio'); $this->interfaceType = $this->interfaceSelections[$name]; } public function updateMultiqueue(string $name, int $queues): void { if (!isset($this->interfaces[$name])) { $this->notify('Network device not found.', 'error'); return; } $cpuCores = $this->getCpuCores(); $queues = max(1, min($queues, $cpuCores)); $this->interfaceMultiqueues[$name] = $queues; try { $proxmox = new Proxmox; $currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $currentValue = (string) ($currentConfig[$name] ?? ''); if (empty($currentValue)) { $this->notify('Network device configuration not found.', 'error'); return; } $segments = array_filter(array_map('trim', explode(',', $currentValue))); $newSegments = []; $queuesFound = false; foreach ($segments as $segment) { if (!str_contains($segment, '=')) { $newSegments[] = $segment; continue; } [$key, $value] = array_map('trim', explode('=', $segment, 2)); $lowerKey = strtolower($key); if ($lowerKey === 'queues') { $queuesFound = true; $newSegments[] = 'queues=' . $queues; } else { $newSegments[] = $key . '=' . $value; } } // If queues parameter wasn't found, add it if (!$queuesFound) { $newSegments[] = 'queues=' . $queues; } $newValue = implode(',', $newSegments); $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$name => $newValue] ); $this->interfaces[$name]['queues'] = $queues; // Update original state to reflect the saved change if (isset($this->original['interfaceMultiqueues'])) { $this->original['interfaceMultiqueues'][$name] = $queues; } $this->notify('Multiqueue updated for ' . strtoupper($name) . ' to ' . $queues . ' queues.', 'success'); } catch (\Exception $e) { $this->notify('Failed to update multiqueue: ' . $e->getMessage(), 'error'); } } public function getCanAddInterfaceProperty(): bool { return count($this->interfaces) < 5; } protected function getNextInterfaceName(): string { $existing = array_keys($this->interfaces); for ($i = 0; $i < 5; $i++) { $name = 'net' . $i; if (!in_array($name, $existing, true)) { return $name; } } return 'net' . count($existing); } public function addInterface(): void { if (!$this->canAddInterface) { $this->notify('Maximum of 5 network devices allowed.', 'error'); return; } try { // Get bridge and MTU from the lowest numbered network device $interfaceKeys = array_keys($this->interfaceConfigs); if (empty($interfaceKeys)) { $this->notify('No existing network devices found.', 'error'); return; } // Sort interface keys to find the lowest numbered one (net0, net1, etc.) usort($interfaceKeys, function ($a, $b) { $numA = (int) preg_replace('/[^0-9]/', '', $a); $numB = (int) preg_replace('/[^0-9]/', '', $b); return $numA <=> $numB; }); $lowestInterface = $interfaceKeys[0]; $sourceConfig = $this->interfaceConfigs[$lowestInterface] ?? ''; $bridge = null; $mtu = null; if ($sourceConfig) { $segments = array_filter(array_map('trim', explode(',', $sourceConfig))); foreach ($segments as $segment) { if (str_contains($segment, '=')) { [$key, $value] = array_map('trim', explode('=', $segment, 2)); if (strtolower($key) === 'bridge') { $bridge = $value; } elseif (strtolower($key) === 'mtu') { $mtu = $value; } } } } $newInterfaceName = $this->getNextInterfaceName(); $macAddress = $this->generateMacAddress(); $newConfig = 'virtio=' . $macAddress; if ($bridge) { $newConfig .= ',bridge=' . $bridge; } if ($mtu) { $newConfig .= ',mtu=' . $mtu; } $proxmox = new Proxmox; $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$newInterfaceName => $newConfig] ); // Refresh interfaces to get the new device $this->refreshInterfaces(); $this->selectedInterface = $newInterfaceName; $this->notify('Network device ' . strtoupper($newInterfaceName) . ' added successfully.', 'success'); } catch (\Exception $e) { $this->notify('Failed to add network device: ' . $e->getMessage(), 'error'); } } public function toggleInterfaceLink(string $name): void { if (!isset($this->interfaces[$name])) { $this->notify('Network device not found.', 'error'); return; } try { $proxmox = new Proxmox; $currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? []; $currentValue = (string) ($currentConfig[$name] ?? ''); if (empty($currentValue)) { $this->notify('Network device configuration not found.', 'error'); return; } $currentLinkdown = $this->interfaces[$name]['linkdown'] ?? false; $segments = array_filter(array_map('trim', explode(',', $currentValue))); $newSegments = []; $linkDownFound = false; foreach ($segments as $segment) { if (!str_contains($segment, '=')) { $newSegments[] = $segment; continue; } [$key, $value] = array_map('trim', explode('=', $segment, 2)); $lowerKey = strtolower($key); if ($lowerKey === 'link_down' || $lowerKey === 'linkdown') { $linkDownFound = true; // Remove link_down when connecting if ($currentLinkdown) { continue; // Skip it to remove } // Keep it when disconnecting (already has it) $newSegments[] = 'link_down=1'; } elseif ($lowerKey === 'bridge') { $newSegments[] = $key . '=' . $value; // If we need to add link_down (disconnecting) and haven't found it yet, add it after bridge if (!$currentLinkdown && !$linkDownFound) { $newSegments[] = 'link_down=1'; $linkDownFound = true; } } elseif ($lowerKey === 'mtu') { // If we need to add link_down and haven't yet, add it before mtu if (!$currentLinkdown && !$linkDownFound) { $newSegments[] = 'link_down=1'; $linkDownFound = true; } $newSegments[] = $key . '=' . $value; } else { $newSegments[] = $key . '=' . $value; } } // If we need to add link_down and it still wasn't added (no bridge or mtu found) if (!$currentLinkdown && !$linkDownFound) { $newSegments[] = 'link_down=1'; } $newValue = implode(',', $newSegments); $proxmox->request( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$name => $newValue] ); // Refresh interfaces to get updated state $this->refreshInterfaces(); $status = $currentLinkdown ? 'connected' : 'disconnected'; $this->notify('Network device ' . strtoupper($name) . ' ' . $status . ' successfully.', 'success'); } catch (\Exception $e) { $this->notify('Failed to toggle network device: ' . $e->getMessage(), 'error'); } } protected function sortInterfaceArrays(array $interfaces, array $configs): array { $keys = array_keys($interfaces); usort($keys, function ($a, $b) { return $this->interfaceIndexFromKey($a) <=> $this->interfaceIndexFromKey($b); }); $sortedInterfaces = []; $sortedConfigs = []; foreach ($keys as $key) { $sortedInterfaces[$key] = $interfaces[$key]; $sortedConfigs[$key] = $configs[$key] ?? ''; } return [$sortedInterfaces, $sortedConfigs]; } protected function interfaceIndexFromKey(string $key): int { if (preg_match('/(\d+)/', $key, $matches)) { return (int) $matches[1]; } return PHP_INT_MAX; } }