feat: implement network adapter management in Proxmox Firewall component, allowing users to enable or disable firewalls for individual adapters and enhancing overall functionality
This commit is contained in:
@@ -17,13 +17,15 @@ class Firewall extends Component
|
||||
public string $error = '';
|
||||
public bool $showErrorModal = false;
|
||||
public bool $showRuleForm = false;
|
||||
public array $networkAdapters = [];
|
||||
public array $adapterFirewallStates = [];
|
||||
|
||||
// Rule creation/editing
|
||||
public ?int $editingRulePos = null;
|
||||
public string $ruleAction = 'ACCEPT';
|
||||
public string $ruleSource = '';
|
||||
public string $ruleDest = '';
|
||||
public string $ruleProto = '';
|
||||
public string $ruleProto = 'tcp';
|
||||
public string $ruleDport = '';
|
||||
public string $ruleSport = '';
|
||||
public string $ruleComment = '';
|
||||
@@ -44,10 +46,12 @@ class Firewall extends Component
|
||||
public function mount(Server $server): void
|
||||
{
|
||||
parent::mount($server);
|
||||
$this->server->loadMissing('ipAddresses');
|
||||
$this->loadFirewallOptions();
|
||||
$this->loadFirewallRules();
|
||||
$this->loadAliases();
|
||||
$this->loadIpsets();
|
||||
$this->loadNetworkAdapters();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +221,7 @@ class Firewall extends Component
|
||||
$this->ruleAction = 'ACCEPT';
|
||||
$this->ruleSource = '';
|
||||
$this->ruleDest = '';
|
||||
$this->ruleProto = '';
|
||||
$this->ruleProto = 'tcp';
|
||||
$this->ruleDport = '';
|
||||
$this->ruleSport = '';
|
||||
$this->ruleComment = '';
|
||||
@@ -557,6 +561,132 @@ class Firewall extends Component
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
$bridge = 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 === 'bridge') {
|
||||
$bridge = $segmentValue;
|
||||
} elseif ($segmentKeyLower === 'firewall') {
|
||||
$firewallEnabled = ($segmentValue == '1' || $segmentValue === '1');
|
||||
}
|
||||
}
|
||||
|
||||
$this->networkAdapters[$key] = [
|
||||
'name' => $key,
|
||||
'model' => $model,
|
||||
'mac' => $mac,
|
||||
'bridge' => $bridge,
|
||||
];
|
||||
$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 render()
|
||||
{
|
||||
return view('proxmox::firewall')->layoutData([
|
||||
|
||||
@@ -101,6 +101,63 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Adapters Card -->
|
||||
<div class="bg-background border border-neutral rounded-lg p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
||||
<x-ri-wifi-line class="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Network Adapters</h2>
|
||||
<p class="text-sm text-base/60 mt-1">Enable or disable firewall for each network adapter</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(count($networkAdapters) > 0)
|
||||
<div class="space-y-4">
|
||||
@foreach($networkAdapters as $adapter)
|
||||
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="text-base font-semibold">{{ $adapter['name'] }}</h3>
|
||||
<span class="px-2 py-1 rounded text-xs font-medium bg-primary/20 text-primary">
|
||||
{{ ucfirst($adapter['model'] ?? 'Unknown') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-base/60 space-y-1">
|
||||
@if($adapter['mac'])
|
||||
<div>MAC: <code class="text-xs">{{ $adapter['mac'] }}</code></div>
|
||||
@endif
|
||||
@if($adapter['bridge'])
|
||||
<div>Bridge: <code class="text-xs">{{ $adapter['bridge'] }}</code></div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 ml-4">
|
||||
<span class="text-sm font-medium text-base/70">Disabled</span>
|
||||
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
|
||||
<input
|
||||
type="checkbox"
|
||||
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
|
||||
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
||||
wire:loading.attr="disabled">
|
||||
<span class="firewall-toggle-slider"></span>
|
||||
</label>
|
||||
<span class="text-sm font-medium text-base/70">Enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-base/60">
|
||||
<x-ri-wifi-line class="size-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No network adapters found</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($firewallEnabled)
|
||||
<!-- Firewall Rules Card -->
|
||||
<div class="bg-background border border-neutral rounded-lg p-6">
|
||||
@@ -143,11 +200,57 @@
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Protocol</label>
|
||||
<select wire:model="ruleProto" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<option value="">Any</option>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="icmp">ICMP</option>
|
||||
<option value="icmpv6">ICMPv6</option>
|
||||
<option value="igmp">IGMP</option>
|
||||
<option value="ggp">GGP</option>
|
||||
<option value="ipencap">IPENCAP</option>
|
||||
<option value="st">ST</option>
|
||||
<option value="egp">EGP</option>
|
||||
<option value="igp">IGP</option>
|
||||
<option value="pip">PIP</option>
|
||||
<option value="hmp">HMP</option>
|
||||
<option value="xns-idp">XNS-IDP</option>
|
||||
<option value="rdp">RDP</option>
|
||||
<option value="iso-tp4">ISO-TP4</option>
|
||||
<option value="dccp">DCCP</option>
|
||||
<option value="xtp">XTP</option>
|
||||
<option value="ddp">DDP</option>
|
||||
<option value="idpr-cmtp">IDPR-CMTP</option>
|
||||
<option value="ipv6">IPv6</option>
|
||||
<option value="ipv6-route">IPv6-Route</option>
|
||||
<option value="ipv6-frag">IPv6-Frag</option>
|
||||
<option value="idrp">IDRP</option>
|
||||
<option value="rsvp">RSVP</option>
|
||||
<option value="gre">GRE</option>
|
||||
<option value="esp">ESP</option>
|
||||
<option value="ah">AH</option>
|
||||
<option value="skip">SKIP</option>
|
||||
<option value="ipv6-icmp">IPv6-ICMP</option>
|
||||
<option value="ipv6-nonxt">IPv6-Nonxt</option>
|
||||
<option value="ipv6-opts">IPv6-Opts</option>
|
||||
<option value="vmtp">VMTP</option>
|
||||
<option value="eigrp">EIGRP</option>
|
||||
<option value="ospf">OSPF</option>
|
||||
<option value="ax.25">AX.25</option>
|
||||
<option value="ipip">IPIP</option>
|
||||
<option value="etherip">EtherIP</option>
|
||||
<option value="encap">ENCAP</option>
|
||||
<option value="pim">PIM</option>
|
||||
<option value="ipcomp">IPComp</option>
|
||||
<option value="vrrp">VRRP</option>
|
||||
<option value="l2tp">L2TP</option>
|
||||
<option value="isis">ISIS</option>
|
||||
<option value="sctp">SCTP</option>
|
||||
<option value="fc">FC</option>
|
||||
<option value="mobility-header">Mobility-Header</option>
|
||||
<option value="udplite">UDPLite</option>
|
||||
<option value="mpls-in-ip">MPLS-in-IP</option>
|
||||
<option value="hip">HIP</option>
|
||||
<option value="shim6">Shim6</option>
|
||||
<option value="wesp">WESP</option>
|
||||
<option value="rohc">ROHC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -156,7 +259,25 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Destination</label>
|
||||
<input type="text" wire:model="ruleDest" placeholder="e.g., 10.0.0.0/8" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<option value="">Any</option>
|
||||
@php
|
||||
$allIps = collect();
|
||||
if ($server->primaryIpv4) {
|
||||
$allIps->push(['ip' => $server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary']);
|
||||
}
|
||||
if ($server->primaryIpv6) {
|
||||
$allIps->push(['ip' => $server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary']);
|
||||
}
|
||||
foreach ($server->ipAddresses as $ipAddress) {
|
||||
$allIps->push(['ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Assigned']);
|
||||
}
|
||||
$uniqueIps = $allIps->unique('ip');
|
||||
@endphp
|
||||
@foreach($uniqueIps as $ipData)
|
||||
<option value="{{ $ipData['ip'] }}">{{ $ipData['ip'] }} ({{ $ipData['type'] }} - {{ $ipData['label'] }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Destination Port</label>
|
||||
@@ -168,7 +289,12 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Interface</label>
|
||||
<input type="text" wire:model="ruleIface" placeholder="e.g., net0" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<select wire:model="ruleIface" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<option value="">Any</option>
|
||||
@foreach($networkAdapters as $adapter)
|
||||
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Log Level</label>
|
||||
|
||||
Reference in New Issue
Block a user