Files
Virtnet.bond/extensions/Servers/Proxmox/Livewire/Firewall.php

965 lines
38 KiB
PHP

<?php
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
class Firewall extends Component
{
public bool $firewallEnabled = false;
public array $rules = [];
public array $aliases = [];
public array $ipsets = [];
public bool $loading = false;
public string $error = '';
public bool $showErrorModal = false;
public bool $showRuleForm = false;
public array $networkAdapters = [];
public array $adapterFirewallStates = [];
public array $firewallLogs = [];
public int $logLimit = 100;
public array $availableIps = [];
public bool $showDeleteModal = false;
public ?int $deletingRulePos = null;
public string $logError = '';
// Rule creation/editing
public ?int $editingRulePos = null;
public string $ruleAction = 'ACCEPT';
public string $ruleSource = '';
public string $ruleDest = '';
public string $ruleProto = 'tcp';
public string $ruleDport = '';
public string $ruleSport = '';
public string $ruleComment = '';
public bool $ruleEnable = true;
public string $ruleMacro = '';
public string $ruleIface = '';
public string $ruleLog = 'nolog';
// Alias management
public string $aliasName = '';
public string $aliasCidr = '';
public string $aliasComment = '';
// IPSet management
public string $ipsetName = '';
public string $ipsetComment = '';
public function mount(Server $server): void
{
parent::mount($server);
$this->server->loadMissing('ipAddresses');
$this->loadAvailableIps();
$this->loadFirewallOptions();
$this->loadFirewallRules();
$this->loadNetworkAdapters();
// Don't load logs on mount - only load when user clicks refresh
}
protected function loadAvailableIps(): void
{
$allIps = collect();
if ($this->server->primaryIpv4) {
$allIps->push(['ip' => $this->server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']);
}
if ($this->server->primaryIpv6) {
$allIps->push(['ip' => $this->server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']);
}
foreach ($this->server->ipAddresses as $ipAddress) {
$allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']);
}
$this->availableIps = $allIps->unique('ip')->values()->toArray();
}
/**
* Make a firewall API request using the dedicated firewall credentials
*/
protected function firewallRequest(string $url, string $method = 'get', array $data = []): \Illuminate\Http\Client\Response
{
$baseUrl = 'https://pve.virtnet.bond';
$user = 'root@pam';
$tokenId = 'firewall';
$secret = '0a439967-0a44-4b94-8906-c36b3a40cf68';
$fullUrl = rtrim($baseUrl, '/') . '/api2/json' . $url;
// Proxmox API token format: PVEAPIToken=USER@REALM!TOKENID=SECRET
$authHeader = 'PVEAPIToken=' . $user . '!' . $tokenId . '=' . $secret;
// For firewall log endpoint, accept text/plain as well
$acceptHeader = str_contains($url, '/firewall/log')
? 'application/json, text/plain, */*'
: 'application/json';
$request = Http::withHeaders([
'Authorization' => $authHeader,
'Accept' => $acceptHeader,
])->withOptions([
'verify' => true,
]);
// For PUT/POST requests, Proxmox API expects form data
if (in_array(strtolower($method), ['put', 'post']) && !empty($data)) {
return $request->asForm()->$method($fullUrl, $data)->throw();
}
return $request->$method($fullUrl, $data)->throw();
}
public function loadFirewallOptions(): void
{
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options'
);
$data = $response->json()['data'] ?? [];
$this->firewallEnabled = isset($data['enable']) && $data['enable'] == 1;
$this->error = '';
$this->showErrorModal = false;
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
// Try to extract more detailed error from response if available
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to load firewall options: ' . $errorMessage;
$this->showErrorModal = true;
$this->firewallEnabled = false;
}
}
public function toggleFirewall(): void
{
$this->loading = true;
try {
// Store the current state before toggling
$currentState = $this->firewallEnabled;
// Calculate the new desired state (opposite of current)
$newState = $currentState ? 0 : 1;
$payload = ['enable' => $newState];
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options',
'put',
$payload
);
if ($response->successful()) {
// Reload from API to get the actual state (don't assume it worked)
$this->loadFirewallOptions();
$this->notify('Firewall ' . ($this->firewallEnabled ? 'enabled' : 'disabled') . ' successfully', 'success');
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->notify('Failed to toggle firewall: ' . $message, 'error');
// Reload to ensure UI matches actual state
$this->loadFirewallOptions();
}
} catch (\Illuminate\Http\Client\RequestException $e) {
$errorMessage = $e->getMessage();
// Try to extract more detailed error from response
if ($e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to toggle firewall: ' . $errorMessage;
$this->showErrorModal = true;
// Reload to ensure UI matches actual state
$this->loadFirewallOptions();
} catch (\Exception $e) {
$this->error = 'Failed to toggle firewall: ' . $e->getMessage();
$this->showErrorModal = true;
// Reload to ensure UI matches actual state
$this->loadFirewallOptions();
} finally {
$this->loading = false;
}
}
public function loadFirewallRules(): void
{
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules'
);
$data = $response->json()['data'] ?? [];
$this->rules = is_array($data) ? $data : [];
$this->error = '';
$this->showErrorModal = false;
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
// Try to extract more detailed error from response if available
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to load firewall rules: ' . $errorMessage;
$this->showErrorModal = true;
$this->rules = [];
}
}
public function startCreateRule(): void
{
$this->resetRuleForm();
// Set default destination to first IP address
if (!empty($this->availableIps)) {
$this->ruleDest = $this->availableIps[0]['ip'];
}
// Set default interface to first network adapter
if (!empty($this->networkAdapters) && isset($this->networkAdapters[0]) && isset($this->networkAdapters[0]['name'])) {
$this->ruleIface = $this->networkAdapters[0]['name'];
}
$this->editingRulePos = null;
$this->showRuleForm = true;
}
public function startEditRule(int $pos): void
{
$rule = collect($this->rules)->firstWhere('pos', $pos);
if ($rule) {
$this->editingRulePos = $pos;
$this->showRuleForm = true;
$this->ruleAction = $rule['action'] ?? 'ACCEPT';
$this->ruleSource = $rule['source'] ?? '';
$this->ruleDest = $rule['dest'] ?? '';
$this->ruleProto = $rule['proto'] ?? '';
$this->ruleDport = $rule['dport'] ?? '';
$this->ruleSport = $rule['sport'] ?? '';
$this->ruleComment = $rule['comment'] ?? '';
$this->ruleEnable = !isset($rule['enable']) || $rule['enable'] == 1;
$this->ruleMacro = $rule['macro'] ?? '';
$this->ruleIface = $rule['iface'] ?? '';
$this->ruleLog = $rule['log'] ?? 'nolog';
}
}
public function resetRuleForm(): void
{
$this->ruleAction = 'ACCEPT';
$this->ruleSource = '';
$this->ruleDest = '';
$this->ruleProto = 'tcp';
$this->ruleDport = '';
$this->ruleSport = '';
$this->ruleComment = '';
$this->ruleEnable = true;
$this->ruleMacro = '';
$this->ruleIface = '';
$this->ruleLog = 'nolog';
}
public function saveRule(): void
{
$this->loading = true;
try {
// Build payload - always include action, enable, and type
// Type defaults to 'in' for incoming rules (can be 'in' or 'out')
$payload = [
'action' => $this->ruleAction,
'enable' => $this->ruleEnable ? 1 : 0,
'type' => 'in', // Default to incoming rule
];
// Add optional parameters only if they have values
// Note: Proxmox allows rules with just action, but some parameters may be required in certain contexts
if (!empty(trim($this->ruleSource))) {
$payload['source'] = trim($this->ruleSource);
}
// Destination is required, always include it
if (!empty(trim($this->ruleDest))) {
$payload['dest'] = trim($this->ruleDest);
}
if (!empty(trim($this->ruleProto))) {
$payload['proto'] = trim($this->ruleProto);
}
if (!empty(trim($this->ruleDport))) {
$payload['dport'] = trim($this->ruleDport);
}
if (!empty(trim($this->ruleSport))) {
$payload['sport'] = trim($this->ruleSport);
}
if (!empty(trim($this->ruleComment))) {
$payload['comment'] = trim($this->ruleComment);
}
if (!empty(trim($this->ruleMacro))) {
$payload['macro'] = trim($this->ruleMacro);
}
// Interface is required, always include it
if (!empty(trim($this->ruleIface))) {
$payload['iface'] = trim($this->ruleIface);
}
if (!empty(trim($this->ruleLog))) {
$payload['log'] = trim($this->ruleLog);
}
if ($this->editingRulePos !== null) {
// Update existing rule
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $this->editingRulePos,
'put',
$payload
);
} else {
// Create new rule
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules',
'post',
$payload
);
}
if ($response->successful()) {
$this->notify('Rule ' . ($this->editingRulePos !== null ? 'updated' : 'created') . ' successfully', 'success');
$this->resetRuleForm();
$this->editingRulePos = null;
$this->showRuleForm = false;
$this->loadFirewallRules();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to save rule: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
$fullErrorDetails = '';
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors'])) {
// Try to get the full error details
$errorMessages = [];
foreach ($errorData['errors'] as $error) {
if (is_array($error)) {
$errorMessages[] = json_encode($error);
} else {
$errorMessages[] = $error;
}
}
$fullErrorDetails = implode(' ', $errorMessages);
$errorMessage = !empty($fullErrorDetails) ? $fullErrorDetails : ($errorData['errors'][0]['message'] ?? $errorMessage);
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
if (isset($errorData['errors'])) {
$errorMessage .= ' ' . json_encode($errorData['errors']);
}
}
}
$this->error = 'Failed to save rule: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function toggleRule(int $pos): void
{
$this->loading = true;
try {
// Find the rule to get its current enable state
$rule = collect($this->rules)->firstWhere('pos', $pos);
if (!$rule) {
$this->error = 'Rule not found';
$this->showErrorModal = true;
$this->loading = false;
return;
}
// Toggle the enable state (0 = disabled, 1 = enabled)
$currentEnable = isset($rule['enable']) ? (int)$rule['enable'] : 1;
$newEnable = $currentEnable == 0 ? 1 : 0;
// Update only the enable field
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos,
'put',
['enable' => $newEnable]
);
if ($response->successful()) {
$this->notify('Rule ' . ($newEnable == 1 ? 'enabled' : 'disabled') . ' successfully', 'success');
$this->loadFirewallRules();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to toggle rule: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to toggle rule: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function confirmDeleteRule(int $pos): void
{
$this->deletingRulePos = $pos;
$this->showDeleteModal = true;
}
public function cancelDeleteRule(): void
{
$this->deletingRulePos = null;
$this->showDeleteModal = false;
}
public function deleteRule(): void
{
if ($this->deletingRulePos === null) {
return;
}
$pos = $this->deletingRulePos;
$this->loading = true;
$this->showDeleteModal = false;
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos,
'delete'
);
if ($response->successful()) {
$this->notify('Rule deleted successfully', 'success');
$this->loadFirewallRules();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to delete rule: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to delete rule: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
$this->deletingRulePos = null;
}
}
public function loadAliases(): void
{
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases'
);
$data = $response->json()['data'] ?? [];
$this->aliases = is_array($data) ? $data : [];
} catch (\Exception $e) {
$this->aliases = [];
}
}
public function createAlias(): void
{
if (empty($this->aliasName) || empty($this->aliasCidr)) {
$this->error = 'Name and CIDR are required';
$this->showErrorModal = true;
return;
}
$this->loading = true;
try {
$payload = [
'name' => $this->aliasName,
'cidr' => $this->aliasCidr,
];
if ($this->aliasComment) {
$payload['comment'] = $this->aliasComment;
}
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases',
'post',
$payload
);
if ($response->successful()) {
$this->notify('Alias created successfully', 'success');
$this->aliasName = '';
$this->aliasCidr = '';
$this->aliasComment = '';
$this->loadAliases();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to create alias: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to create alias: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function deleteAlias(string $name): void
{
$this->loading = true;
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $name,
'delete'
);
if ($response->successful()) {
$this->notify('Alias deleted successfully', 'success');
$this->loadAliases();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to delete alias: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to delete alias: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function loadIpsets(): void
{
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset'
);
$data = $response->json()['data'] ?? [];
$this->ipsets = is_array($data) ? $data : [];
} catch (\Exception $e) {
$this->ipsets = [];
}
}
public function createIpset(): void
{
if (empty($this->ipsetName)) {
$this->error = 'Name is required';
$this->showErrorModal = true;
return;
}
$this->loading = true;
try {
$payload = [
'name' => $this->ipsetName,
];
if ($this->ipsetComment) {
$payload['comment'] = $this->ipsetComment;
}
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset',
'post',
$payload
);
if ($response->successful()) {
$this->notify('IP set created successfully', 'success');
$this->ipsetName = '';
$this->ipsetComment = '';
$this->loadIpsets();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to create IP set: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to create IP set: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function deleteIpset(string $name): void
{
$this->loading = true;
try {
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $name,
'delete'
);
if ($response->successful()) {
$this->notify('IP set deleted successfully', 'success');
$this->loadIpsets();
} else {
$message = $response->json()['errors'][0]['message'] ?? 'Unknown error';
$this->error = 'Failed to delete IP set: ' . $message;
$this->showErrorModal = true;
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to delete IP set: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function loadNetworkAdapters(): void
{
try {
$proxmox = new Proxmox;
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
$this->networkAdapters = [];
$this->adapterFirewallStates = [];
foreach ($config as $key => $value) {
if (!is_string($key) || !str_starts_with($key, 'net')) {
continue;
}
$configString = (string) $value;
$segments = array_filter(array_map('trim', explode(',', $configString)));
$model = 'virtio';
$mac = null;
$firewallEnabled = false;
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
continue;
}
[$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2));
$segmentKeyLower = strtolower($segmentKey);
if (in_array($segmentKeyLower, ['virtio', 'e1000', 'e1000e', 'rtl8139', 'vmxnet3', 'pcnet'], true)) {
$model = $segmentKeyLower;
$mac = $segmentValue;
} elseif ($segmentKeyLower === 'firewall') {
$firewallEnabled = ($segmentValue == '1' || $segmentValue === '1');
}
}
$this->networkAdapters[] = [
'name' => $key,
'model' => $model,
'mac' => $mac,
];
$this->adapterFirewallStates[$key] = $firewallEnabled;
}
} catch (\Exception $e) {
$this->networkAdapters = [];
$this->adapterFirewallStates = [];
}
}
public function toggleAdapterFirewall(string $adapterName): void
{
$this->loading = true;
try {
$proxmox = new Proxmox;
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
if (!isset($config[$adapterName])) {
$this->error = 'Network adapter not found: ' . $adapterName;
$this->showErrorModal = true;
$this->loading = false;
return;
}
$currentConfig = (string) $config[$adapterName];
$segments = array_filter(array_map('trim', explode(',', $currentConfig)));
$newSegments = [];
$firewallFound = false;
$currentFirewallState = $this->adapterFirewallStates[$adapterName] ?? false;
$newFirewallState = !$currentFirewallState;
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
$newSegments[] = $segment;
continue;
}
[$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2));
if (strtolower($segmentKey) === 'firewall') {
$firewallFound = true;
if ($newFirewallState) {
$newSegments[] = 'firewall=1';
}
// If disabling, we skip adding firewall=1, effectively removing it
} else {
$newSegments[] = $segment;
}
}
// If firewall wasn't in the config and we're enabling it, add it
if (!$firewallFound && $newFirewallState) {
$newSegments[] = 'firewall=1';
}
$newConfig = implode(',', $newSegments);
$proxmox->request(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config',
'put',
[$adapterName => $newConfig]
);
$this->adapterFirewallStates[$adapterName] = $newFirewallState;
$this->notify('Firewall ' . ($newFirewallState ? 'enabled' : 'disabled') . ' for ' . $adapterName, 'success');
$this->loadNetworkAdapters(); // Reload to ensure consistency
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
$errorData = $e->response()->json();
if (isset($errorData['errors']) && is_array($errorData['errors']) && !empty($errorData['errors'])) {
$errorMessage = $errorData['errors'][0]['message'] ?? $errorMessage;
} elseif (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
}
$this->error = 'Failed to toggle firewall for adapter: ' . $errorMessage;
$this->showErrorModal = true;
} finally {
$this->loading = false;
}
}
public function loadFirewallLogs(): void
{
$this->logError = '';
try {
// Proxmox API: GET /api2/json/nodes/{node}/qemu/{vmid}/firewall/log
// Documentation: https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/firewall/log
// Returns: text/plain format with log entries, one per line
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
'get',
['limit' => $this->logLimit]
);
if (!$response->successful()) {
$errorData = [];
try {
$errorData = $response->json();
} catch (\Exception $e) {
// Response might not be JSON
}
$errorMsg = $errorData['message'] ?? 'HTTP ' . $response->status();
$this->logError = 'Failed to fetch logs: ' . $errorMsg;
\Log::warning('Firewall log API request failed', [
'status' => $response->status(),
'body' => substr($response->body(), 0, 500),
'server_id' => $this->server->id,
'vm_id' => $this->server->vm_id
]);
$this->firewallLogs = [];
return;
}
// Proxmox firewall log endpoint returns text/plain format
// Each log entry is on a new line
$body = $response->body();
$rawLogs = [];
if (!empty($body)) {
// Split by newlines and filter empty lines
$lines = explode("\n", trim($body));
$rawLogs = array_filter($lines, function($line) {
return !empty(trim($line));
});
}
\Log::debug('Firewall logs fetched from Proxmox API', [
'raw_count' => count($rawLogs),
'body_length' => strlen($body),
'content_type' => $response->header('Content-Type'),
'sample' => !empty($rawLogs) ? substr($rawLogs[0], 0, 200) : null
]);
$this->firewallLogs = [];
// Store raw log entries exactly as received from Proxmox (no parsing/sanitization)
foreach ($rawLogs as $logEntry) {
if (is_string($logEntry)) {
$logText = trim($logEntry);
if (!empty($logText)) {
$this->firewallLogs[] = ['raw' => $logText];
}
}
}
if (empty($this->firewallLogs) && !empty($rawLogs)) {
$this->logError = 'Logs received but could not be processed.';
} elseif (empty($this->firewallLogs)) {
$this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).';
}
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
try {
$errorData = $e->response()->json();
if (isset($errorData['message'])) {
$errorMessage = $errorData['message'];
}
} catch (\Exception $jsonError) {
// Response might not be JSON
}
}
$this->logError = 'Error loading logs: ' . $errorMessage;
\Log::error('Failed to load firewall logs', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'server_id' => $this->server->id,
'vm_id' => $this->server->vm_id
]);
$this->firewallLogs = [];
}
}
protected function parseFirewallLog(string $logText): ?array
{
// Example log: 202500009 2 tap202500009i0-IN 01/Dec/2025:09:00:39 -1000 REJECT: IN=fwbr202500009i0 OUT=fwbr202500009i0 PHYSIN=fwln202500009i0 PHYSOUT=tap202500009i0 MAC=8e:ef:c1:86:d4:20:58:9c:fc:10:ff:d3:08:00 SRC=24.210.66.222 DST=45.154.99.21 LEN=52 TOS=0x00 PREC=0x00 TTL=115 ID=58510 DF PROTO=TCP SPT=50692 DPT=22 SEQ=2584199599 ACK=0 WINDOW=65535 SYN
// Extract SRC, DST, TTL, PROTO, SPT, DPT, ACK
$result = [
'source' => null,
'destination' => null,
'ttl' => null,
'proto' => null,
'spt' => null,
'dpt' => null,
'ack' => null,
'action' => null,
'timestamp' => null,
];
// Extract action (REJECT, ACCEPT, DROP) - usually before the colon
if (preg_match('/(REJECT|ACCEPT|DROP):/i', $logText, $matches)) {
$result['action'] = strtoupper($matches[1]);
}
// Extract timestamp - look for date pattern like "01/Dec/2025:09:00:39"
if (preg_match('/(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2})/', $logText, $matches)) {
$result['timestamp'] = $matches[1];
}
// Extract SRC
if (preg_match('/SRC=([^\s]+)/', $logText, $matches)) {
$result['source'] = $matches[1];
}
// Extract DST
if (preg_match('/DST=([^\s]+)/', $logText, $matches)) {
$result['destination'] = $matches[1];
}
// Extract TTL
if (preg_match('/TTL=(\d+)/', $logText, $matches)) {
$result['ttl'] = (int) $matches[1];
}
// Extract PROTO
if (preg_match('/PROTO=(\w+)/', $logText, $matches)) {
$result['proto'] = strtoupper($matches[1]);
}
// Extract SPT (Source Port)
if (preg_match('/SPT=(\d+)/', $logText, $matches)) {
$result['spt'] = (int) $matches[1];
}
// Extract DPT (Destination Port)
if (preg_match('/DPT=(\d+)/', $logText, $matches)) {
$result['dpt'] = (int) $matches[1];
}
// Extract ACK
if (preg_match('/ACK=(\d+)/', $logText, $matches)) {
$result['ack'] = (int) $matches[1];
}
// Only return if we have at least some data
if ($result['source'] || $result['destination'] || $result['proto']) {
return $result;
}
return null;
}
public function render()
{
return view('proxmox::firewall')->layoutData([
'server' => $this->server,
'status' => $this->status,
]);
}
}