From: Ryosuke Nakayama <ryosuke.nakayama@ryskn.com>
To: pve-devel@lists.proxmox.com
Subject: [RFC PATCH manager 1/2] api: network: add VPP (fd.io) dataplane bridge support
Date: Tue, 17 Mar 2026 07:28:15 +0900 [thread overview]
Message-ID: <20260316222816.42944-2-ryosuke.nakayama@ryskn.com> (raw)
In-Reply-To: <20260316222816.42944-1-ryosuke.nakayama@ryskn.com>
From: ryskn <ryosuke.nakayama@ryskn.com>
Integrate VPP (fd.io) as an alternative network dataplane alongside
OVS. This adds VPP bridge domain management via the Proxmox WebUI
and REST API.
Backend (PVE/API2/Network.pm):
- Detect VPP bridges via 'vppctl show bridge-domain' and expose them
as type=VPPBridge in the network interface list
- Create/delete VPP bridge domains via vppctl
- Persist bridge domains to /etc/vpp/pve-bridges.conf (exec'd at VPP
startup) so they survive reboots
- Support vpp_vlan_aware flag: maps to 'set bridge-domain property N
learn enable/disable' in VPP
- Add VPP VLAN subinterface create/delete/list via vppctl, persisted
to /etc/vpp/pve-vlans.conf
- Validate parent interface exists before creating a VLAN subinterface
- Exclude VPP bridges from the SDN-only access guard so they appear
in the WebUI NIC selector
- Use $VPP_SOCKET constant consistently (no hardcoded paths)
- Log warning on bridge-removal failure instead of silently swallowing
- Rely on get_vpp_vlans() for VPP VLAN detection in update/delete to
avoid false-positives on Linux dot-notation VLANs (e.g. eth0.100)
- Fetch VPP data once per request; filter path reuses $ifaces instead
of making redundant vppctl calls
- VPP conf writes are serialised by the existing $iflockfn lock
Vhost-user socket path convention: /var/run/vpp/qemu-<vmid>-<net>.sock
Signed-off-by: ryskn <ryosuke.nakayama@ryskn.com>
---
PVE/API2/Network.pm | 413 +++++++++++++++++++++++++++-
PVE/API2/Nodes.pm | 19 ++
PVE/CLI/pve8to9.pm | 48 ++++
www/manager6/form/BridgeSelector.js | 5 +
www/manager6/lxc/Network.js | 34 +++
www/manager6/node/Config.js | 1 +
www/manager6/qemu/NetworkEdit.js | 27 ++
www/manager6/window/Migrate.js | 48 ++++
8 files changed, 590 insertions(+), 5 deletions(-)
diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm
index fc053fec..f87a8f79 100644
--- a/PVE/API2/Network.pm
+++ b/PVE/API2/Network.pm
@@ -49,6 +49,8 @@ my $network_type_enum = [
'OVSBond',
'OVSPort',
'OVSIntPort',
+ 'VPPBridge',
+ 'VPPVlan',
'vnet',
];
@@ -117,6 +119,17 @@ my $confdesc = {
type => 'string',
format => 'pve-iface',
},
+ vpp_bridge => {
+ description => "The VPP bridge domain to add this VLAN interface to (e.g. vppbr1).",
+ optional => 1,
+ type => 'string',
+ format => 'pve-iface',
+ },
+ vpp_vlan_aware => {
+ description => "Enable VLAN-aware mode for VPP bridge domain.",
+ optional => 1,
+ type => 'boolean',
+ },
slaves => {
description => "Specify the interfaces used by the bonding device.",
optional => 1,
@@ -259,6 +272,170 @@ sub extract_altnames {
return undef;
}
+my $VPP_BRIDGES_CONF = '/etc/vpp/pve-bridges.conf';
+my $VPP_VLANS_CONF = '/etc/vpp/pve-vlans.conf';
+my $VPP_SOCKET = '/run/vpp/cli.sock';
+
+sub vpp_save_bridges_conf {
+ my ($bridges) = @_;
+
+ my $content = "# Auto-generated by PVE - do not edit manually\n";
+ for my $id (sort { $a <=> $b } keys %$bridges) {
+ next if $id == 0; # skip default bridge-domain
+ $content .= "create bridge-domain $id learn 1 forward 1 uu-flood 1 arp-term 0\n";
+ if ($bridges->{$id}{vlan_aware}) {
+ # 'vlan_aware' maps to VPP's per-port tag-rewrite workflow.
+ # We use 'set bridge-domain property learn enable' as a marker
+ # so the flag survives VPP restarts via pve-bridges.conf exec.
+ $content .= "set bridge-domain property $id learn enable\n";
+ }
+ }
+
+ PVE::Tools::file_set_contents($VPP_BRIDGES_CONF, $content);
+}
+
+sub vpp_load_bridges_conf {
+ my $bridges = {};
+ return $bridges if !-f $VPP_BRIDGES_CONF;
+
+ my $content = PVE::Tools::file_get_contents($VPP_BRIDGES_CONF);
+ for my $line (split(/\n/, $content)) {
+ next if $line =~ /^#/;
+ if ($line =~ /^create bridge-domain\s+(\d+)/ && $1 != 0) {
+ $bridges->{$1} //= {};
+ } elsif ($line =~ /^set bridge-domain property\s+(\d+)\s+learn\s+enable/) {
+ $bridges->{$1}{vlan_aware} = 1 if $bridges->{$1};
+ }
+ }
+ return $bridges;
+}
+
+sub vpp_save_vlan_conf {
+ my ($iface, $parent, $sub_id, $vlan_id, $bridge) = @_;
+
+ my $vlans = vpp_load_vlan_conf();
+ $vlans->{$iface} = {
+ parent => $parent,
+ sub_id => $sub_id,
+ vlan_id => $vlan_id,
+ bridge => $bridge // '',
+ };
+
+ my $content = "# Auto-generated by PVE - do not edit manually\n";
+ for my $name (sort keys %$vlans) {
+ my $v = $vlans->{$name};
+ $content .= "create sub-interfaces $v->{parent} $v->{sub_id} dot1q $v->{vlan_id}\n";
+ $content .= "set interface state $name up\n";
+ if ($v->{bridge} && $v->{bridge} =~ /^vppbr(\d+)$/) {
+ $content .= "set interface l2 bridge $name $1\n";
+ }
+ }
+ PVE::Tools::file_set_contents($VPP_VLANS_CONF, $content);
+}
+
+sub vpp_load_vlan_conf {
+ my $vlans = {};
+ return $vlans if !-f $VPP_VLANS_CONF;
+
+ my $content = PVE::Tools::file_get_contents($VPP_VLANS_CONF);
+ my %pending;
+ for my $line (split(/\n/, $content)) {
+ next if $line =~ /^#/;
+ if ($line =~ /^create sub-interfaces\s+(\S+)\s+(\d+)\s+dot1q\s+(\d+)/) {
+ my ($parent, $sub_id, $vlan_id) = ($1, $2, $3);
+ my $name = "$parent.$sub_id";
+ $pending{$name} = { parent => $parent, sub_id => $sub_id, vlan_id => $vlan_id, bridge => '' };
+ } elsif ($line =~ /^set interface l2 bridge\s+(\S+)\s+(\d+)/) {
+ my ($name, $bd_id) = ($1, $2);
+ $pending{$name}{bridge} = "vppbr$bd_id" if $pending{$name};
+ }
+ }
+ $vlans->{$_} = $pending{$_} for keys %pending;
+ return $vlans;
+}
+
+sub vpp_delete_vlan_conf {
+ my ($iface) = @_;
+ my $vlans = vpp_load_vlan_conf();
+ return if !$vlans->{$iface};
+ delete $vlans->{$iface};
+
+ my $content = "# Auto-generated by PVE - do not edit manually\n";
+ for my $name (sort keys %$vlans) {
+ my $v = $vlans->{$name};
+ $content .= "create sub-interfaces $v->{parent} $v->{sub_id} dot1q $v->{vlan_id}\n";
+ $content .= "set interface state $name up\n";
+ if ($v->{bridge} && $v->{bridge} =~ /^vppbr(\d+)$/) {
+ $content .= "set interface l2 bridge $name $1\n";
+ }
+ }
+ PVE::Tools::file_set_contents($VPP_VLANS_CONF, $content);
+}
+
+sub get_vpp_vlans {
+ return {} if !-x '/usr/bin/vppctl';
+
+ my $vlans = {};
+ eval {
+ my $output = '';
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET, 'show', 'interface'],
+ outfunc => sub { $output .= $_[0] . "\n"; },
+ timeout => 5,
+ );
+ my $saved = vpp_load_vlan_conf();
+ while ($output =~ /^(\S+)\.(\d+)\s+\d+\s+(\S+)/mg) {
+ my ($parent, $sub_id, $state) = ($1, $2, $3);
+ my $name = "$parent.$sub_id";
+ my $vlan_id = $saved->{$name} ? $saved->{$name}{vlan_id} : $sub_id;
+ $vlans->{$name} = {
+ type => 'VPPVlan',
+ active => ($state eq 'up') ? 1 : 0,
+ iface => $name,
+ 'vlan-raw-device' => $parent,
+ 'vlan-id' => $vlan_id,
+ vpp_bridge => $saved->{$name} ? $saved->{$name}{bridge} : '',
+ };
+ }
+ };
+ warn "VPP VLAN detection failed: $@" if $@;
+ return $vlans;
+}
+
+sub get_vpp_bridges {
+ return {} if !-x '/usr/bin/vppctl';
+
+ my $bridges = {};
+ eval {
+ my $output = '';
+ my $errout = '';
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET, 'show', 'bridge-domain'],
+ outfunc => sub { $output .= $_[0] . "\n"; },
+ errfunc => sub { $errout .= $_[0] . "\n"; },
+ timeout => 5,
+ );
+ warn "VPP bridge detection stderr: $errout" if $errout;
+ my $saved = vpp_load_bridges_conf();
+ for my $line (split(/\n/, $output)) {
+ next if $line !~ /^\s*(\d+)\s+/;
+ my $id = $1;
+ next if $id == 0; # skip default bridge-domain
+ my $name = "vppbr$id";
+ $bridges->{$name} = {
+ type => 'VPPBridge',
+ active => 1,
+ iface => $name,
+ priority => $id,
+ vpp_vlan_aware => $saved->{$id} ? ($saved->{$id}{vlan_aware} ? 1 : 0) : 0,
+ };
+ }
+ };
+ warn "VPP bridge detection failed: $@" if $@;
+
+ return $bridges;
+}
+
__PACKAGE__->register_method({
name => 'index',
path => '',
@@ -422,6 +599,16 @@ __PACKAGE__->register_method({
delete $ifaces->{lo}; # do not list the loopback device
+ # always include VPP bridges and VLANs if VPP is available.
+ # These are fetched once here; the filter path below reuses $ifaces
+ # rather than calling get_vpp_bridges/get_vpp_vlans a second time.
+ # Note: VPP conf writes (create/update/delete) are serialised by
+ # $iflockfn, so no separate lock is needed for the conf files.
+ my $vpp_bridges_all = get_vpp_bridges();
+ $ifaces->{$_} = $vpp_bridges_all->{$_} for keys $vpp_bridges_all->%*;
+ my $vpp_vlans_all = get_vpp_vlans();
+ $ifaces->{$_} = $vpp_vlans_all->{$_} for keys $vpp_vlans_all->%*;
+
if (my $tfilter = $param->{type}) {
my $vnets;
my $fabrics;
@@ -440,7 +627,7 @@ __PACKAGE__->register_method({
if ($tfilter ne 'include_sdn') {
for my $k (sort keys $ifaces->%*) {
my $type = $ifaces->{$k}->{type};
- my $is_bridge = $type eq 'bridge' || $type eq 'OVSBridge';
+ my $is_bridge = $type eq 'bridge' || $type eq 'OVSBridge' || $type eq 'VPPBridge';
my $bridge_match = $is_bridge && $tfilter =~ /^any(_local)?_bridge$/;
my $match = $tfilter eq $type || $bridge_match;
delete $ifaces->{$k} if !$match;
@@ -675,6 +862,89 @@ __PACKAGE__->register_method({
|| die "Open VSwitch is not installed (need package 'openvswitch-switch')\n";
}
+ if ($param->{type} eq 'VPPVlan') {
+ -x '/usr/bin/vppctl'
+ || die "VPP is not installed (need package 'vpp')\n";
+
+ $iface =~ /^(.+)\.(\d+)$/
+ || die "VPP VLAN name must be <parent>.<vlan-id>, e.g. tap0.100\n";
+ my ($parent, $sub_id) = ($1, $2);
+ my $vlan_id = $sub_id;
+
+ # check VLAN doesn't already exist and parent interface exists in VPP
+ my $existing_vlans = get_vpp_vlans();
+ die "VPP VLAN '$iface' already exists\n" if $existing_vlans->{$iface};
+
+ my $iface_out = '';
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET, 'show', 'interface'],
+ outfunc => sub { $iface_out .= $_[0] . "\n"; },
+ timeout => 5,
+ );
+ die "VPP interface '$parent' does not exist\n"
+ if $iface_out !~ /^\Q$parent\E\s/m;
+
+ # create sub-interface
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'create', 'sub-interfaces', $parent, $sub_id, 'dot1q', $vlan_id],
+ timeout => 10,
+ );
+
+ # bring up
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'set', 'interface', 'state', $iface, 'up'],
+ timeout => 10,
+ );
+
+ # optionally add to VPP bridge domain
+ if (my $bridge = $param->{vpp_bridge}) {
+ $bridge =~ /^vppbr(\d+)$/
+ || die "Invalid VPP bridge name '$bridge'\n";
+ my $bd_id = $1;
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'set', 'interface', 'l2', 'bridge', $iface, $bd_id],
+ timeout => 10,
+ );
+ }
+
+ vpp_save_vlan_conf($iface, $parent, $sub_id, $vlan_id, $param->{vpp_bridge});
+ return undef;
+ }
+
+ if ($param->{type} eq 'VPPBridge') {
+ -x '/usr/bin/vppctl'
+ || die "VPP is not installed (need package 'vpp')\n";
+
+ $iface =~ /^vppbr(\d+)$/
+ || die "VPP bridge name must match 'vppbrN' (e.g. vppbr1)\n";
+ my $bd_id = $1;
+
+ die "bridge-domain 0 is reserved by VPP, use vppbr1 or higher\n"
+ if $bd_id == 0;
+
+ # check for duplicate bridge-domain ID
+ my $existing = get_vpp_bridges();
+ die "VPP bridge-domain $bd_id already exists\n" if $existing->{"vppbr$bd_id"};
+
+ # create bridge-domain in running VPP
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'create', 'bridge-domain', $bd_id,
+ 'learn', '1', 'forward', '1', 'uu-flood', '1', 'arp-term', '0'],
+ timeout => 10,
+ );
+
+ # persist for VPP restarts
+ my $saved = vpp_load_bridges_conf();
+ $saved->{$bd_id} = { vlan_aware => $param->{vpp_vlan_aware} ? 1 : 0 };
+ vpp_save_bridges_conf($saved);
+
+ return undef; # VPP bridges are not stored in /etc/network/interfaces
+ }
+
if ($param->{type} eq 'OVSIntPort' || $param->{type} eq 'OVSBond') {
my $brname = $param->{ovs_bridge};
raise_param_exc({ ovs_bridge => "parameter is required" }) if !$brname;
@@ -743,6 +1013,67 @@ __PACKAGE__->register_method({
my $delete = extract_param($param, 'delete');
my $code = sub {
+ # VPP bridges and VLANs are not stored in /etc/network/interfaces
+ if ($iface =~ /^vppbr(\d+)$/) {
+ my $bd_id = $1;
+ my $existing = get_vpp_bridges();
+ raise_param_exc({ iface => "VPP bridge '$iface' does not exist" })
+ if !$existing->{$iface};
+
+ my $vlan_aware = $param->{vpp_vlan_aware} ? 1 : 0;
+
+ # apply to running VPP
+ my $vlan_cmd = $vlan_aware ? 'enable' : 'disable';
+ eval {
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'set', 'bridge-domain', 'property', $bd_id, 'learn', $vlan_cmd],
+ timeout => 10,
+ );
+ };
+ warn "Failed to set VPP bridge-domain $bd_id vlan_aware: $@" if $@;
+
+ # persist
+ my $saved = vpp_load_bridges_conf();
+ $saved->{$bd_id} //= {};
+ $saved->{$bd_id}{vlan_aware} = $vlan_aware;
+ vpp_save_bridges_conf($saved);
+ return undef;
+ }
+
+ if (get_vpp_vlans()->{$iface}) {
+ # VPP VLAN: update bridge assignment
+ my $saved = vpp_load_vlan_conf();
+ my $entry = $saved->{$iface};
+ raise_param_exc({ iface => "VPP VLAN '$iface' not found in config" })
+ if !$entry;
+ my $new_bridge = $param->{vpp_bridge} // '';
+
+ # move bridge assignment if changed
+ if (($entry->{bridge} // '') ne $new_bridge) {
+ if ($entry->{bridge} && $entry->{bridge} =~ /^vppbr(\d+)$/) {
+ eval {
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'set', 'interface', 'l2', 'bridge', $iface, $1, 'del'],
+ timeout => 10,
+ );
+ };
+ warn "Failed to remove '$iface' from bridge '$entry->{bridge}': $@" if $@;
+ }
+ if ($new_bridge && $new_bridge =~ /^vppbr(\d+)$/) {
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'set', 'interface', 'l2', 'bridge', $iface, $1],
+ timeout => 10,
+ );
+ }
+ $entry->{bridge} = $new_bridge;
+ }
+ vpp_save_vlan_conf($iface, $entry->{parent}, $entry->{sub_id}, $entry->{vlan_id}, $new_bridge);
+ return undef;
+ }
+
my $config = PVE::INotify::read_file('interfaces');
my $ifaces = $config->{ifaces};
@@ -848,6 +1179,21 @@ __PACKAGE__->register_method({
my $iface = $param->{iface};
+ # check VPP interfaces if not found in /etc/network/interfaces
+ if (!$ifaces->{$iface}) {
+ if ($iface =~ /^vppbr\d+$/) {
+ my $vpp_bridges = get_vpp_bridges();
+ raise_param_exc({ iface => "interface does not exist" })
+ if !$vpp_bridges->{$iface};
+ return $vpp_bridges->{$iface};
+ } elsif ($iface =~ /^.+\.\d+$/) {
+ my $vpp_vlans = get_vpp_vlans();
+ raise_param_exc({ iface => "interface does not exist" })
+ if !$vpp_vlans->{$iface};
+ return $vpp_vlans->{$iface};
+ }
+ }
+
raise_param_exc({ iface => "interface does not exist" })
if !$ifaces->{$iface};
@@ -969,26 +1315,83 @@ __PACKAGE__->register_method({
my ($param) = @_;
my $code = sub {
+ my $iface = $param->{iface};
+
+ # Handle VPP VLAN deletion
+ if (get_vpp_vlans()->{$iface}) {
+ my $saved = vpp_load_vlan_conf();
+ my $entry = $saved->{$iface};
+
+ # remove from bridge domain if assigned
+ if ($entry && $entry->{bridge} && $entry->{bridge} =~ /^vppbr(\d+)$/) {
+ eval {
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'set', 'interface', 'l2', 'bridge', $iface, $1, 'del'],
+ timeout => 10,
+ );
+ };
+ }
+
+ # delete sub-interface
+ eval {
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'delete', 'sub-interface', $iface],
+ timeout => 10,
+ );
+ };
+ warn "Failed to delete VPP VLAN '$iface': $@" if $@;
+
+ vpp_delete_vlan_conf($iface);
+ return undef;
+ }
+
+ # Handle VPP bridge deletion separately (not in /etc/network/interfaces)
+ if ($iface =~ /^vppbr(\d+)$/) {
+ my $bd_id = $1;
+ my $existing = get_vpp_bridges();
+ raise_param_exc({ iface => "VPP bridge '$iface' does not exist" })
+ if !$existing->{$iface};
+
+ # delete bridge-domain in running VPP
+ eval {
+ PVE::Tools::run_command(
+ ['/usr/bin/vppctl', '-s', $VPP_SOCKET,
+ 'create', 'bridge-domain', $bd_id, 'del'],
+ timeout => 10,
+ );
+ };
+ warn "Failed to delete VPP bridge-domain $bd_id: $@" if $@;
+
+ # remove from persistence config
+ my $saved = vpp_load_bridges_conf();
+ delete $saved->{$bd_id};
+ vpp_save_bridges_conf($saved);
+
+ return undef;
+ }
+
my $config = PVE::INotify::read_file('interfaces');
my $ifaces = $config->{ifaces};
raise_param_exc({ iface => "interface does not exist" })
- if !$ifaces->{ $param->{iface} };
+ if !$ifaces->{$iface};
- my $d = $ifaces->{ $param->{iface} };
+ my $d = $ifaces->{$iface};
if ($d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') {
if (my $brname = $d->{ovs_bridge}) {
if (my $br = $ifaces->{$brname}) {
if ($br->{ovs_ports}) {
my @ports = split(/\s+/, $br->{ovs_ports});
- my @new = grep { $_ ne $param->{iface} } @ports;
+ my @new = grep { $_ ne $iface } @ports;
$br->{ovs_ports} = join(' ', @new);
}
}
}
}
- delete $ifaces->{ $param->{iface} };
+ delete $ifaces->{$iface};
PVE::INotify::write_file('interfaces', $config);
};
diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index 5bd6fe49..c4dcd9e6 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -2496,6 +2496,25 @@ my $create_migrate_worker = sub {
my $preconditions = PVE::API2::Qemu->migrate_vm_precondition(
{ node => $nodename, vmid => $vmid, target => $target });
my $invalidConditions = '';
+
+ if ($online) {
+ my $vpp_bridges = PVE::API2::Network::get_vpp_bridges();
+ if (keys %$vpp_bridges) {
+ my $conf = PVE::QemuConfig->load_config($vmid);
+ my @vpp_nics;
+ for my $opt (sort keys %$conf) {
+ next if $opt !~ m/^net\d+$/;
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$opt});
+ next if !$net || !$net->{bridge};
+ push @vpp_nics, $opt if $vpp_bridges->{$net->{bridge}};
+ }
+ if (@vpp_nics) {
+ $invalidConditions .= "\n Has VPP vhost-user NICs: ";
+ $invalidConditions .= join(', ', @vpp_nics);
+ }
+ }
+ }
+
if ($online && !$with_local_disks && scalar @{ $preconditions->{local_disks} }) {
$invalidConditions .= "\n Has local disks: ";
$invalidConditions .=
diff --git a/PVE/CLI/pve8to9.pm b/PVE/CLI/pve8to9.pm
index 0c4b2343..dd31c8e5 100644
--- a/PVE/CLI/pve8to9.pm
+++ b/PVE/CLI/pve8to9.pm
@@ -11,6 +11,7 @@ use PVE::API2::LXC;
use PVE::API2::Qemu;
use PVE::API2::Certificates;
use PVE::API2::Cluster::Ceph;
+use PVE::API2::Network;
use PVE::AccessControl;
use PVE::Ceph::Tools;
@@ -1909,6 +1910,52 @@ sub check_bridge_mtu {
}
}
+sub check_vpp_firewall_conflicts {
+ log_info("Checking for VMs with firewall enabled on VPP bridges...");
+
+ my $vpp_bridges = eval { PVE::API2::Network::get_vpp_bridges() } // {};
+ if (!keys %$vpp_bridges) {
+ log_skip("No VPP bridges detected.");
+ return;
+ }
+
+ my $affected = [];
+ my $vms = PVE::QemuServer::config_list();
+ for my $vmid (sort { $a <=> $b } keys %$vms) {
+ my $config = PVE::QemuConfig->load_config($vmid);
+ for my $opt (sort keys %$config) {
+ next if $opt !~ m/^net\d+$/;
+ my $net = PVE::QemuServer::Network::parse_net($config->{$opt});
+ next if !$net || !$net->{bridge};
+ if ($vpp_bridges->{$net->{bridge}} && $net->{firewall}) {
+ push @$affected, "VM $vmid ($opt on $net->{bridge})";
+ }
+ }
+ }
+
+ my $cts = PVE::LXC::config_list();
+ for my $vmid (sort { $a <=> $b } keys %$cts) {
+ my $conf = PVE::LXC::Config->load_config($vmid);
+ for my $opt (sort keys %$conf) {
+ next if $opt !~ m/^net\d+$/;
+ my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt});
+ next if !$net || !$net->{bridge};
+ if ($vpp_bridges->{$net->{bridge}} && $net->{firewall}) {
+ push @$affected, "CT $vmid ($opt on $net->{bridge})";
+ }
+ }
+ }
+
+ if (@$affected) {
+ log_warn(
+ "The following guests have firewall enabled on VPP bridges (kernel firewall not available):\n"
+ . " "
+ . join(", ", @$affected));
+ } else {
+ log_pass("No firewall conflicts with VPP bridges found.");
+ }
+}
+
sub check_rrd_migration {
if (-e "/var/lib/rrdcached/db/pve-node-9.0") {
log_info("Check post RRD metrics data format update situation...");
@@ -2016,6 +2063,7 @@ sub check_virtual_guests {
check_lxcfs_fuse_version();
check_bridge_mtu();
+ check_vpp_firewall_conflicts();
my $affected_guests_long_desc = [];
my $affected_cts_cgroup_keys = [];
diff --git a/www/manager6/form/BridgeSelector.js b/www/manager6/form/BridgeSelector.js
index b5949018..297a3e19 100644
--- a/www/manager6/form/BridgeSelector.js
+++ b/www/manager6/form/BridgeSelector.js
@@ -30,6 +30,11 @@ Ext.define('PVE.form.BridgeSelector', {
dataIndex: 'active',
renderer: Proxmox.Utils.format_boolean,
},
+ {
+ header: gettext('Type'),
+ width: 80,
+ dataIndex: 'type',
+ },
{
header: gettext('Comment'),
dataIndex: 'comments',
diff --git a/www/manager6/lxc/Network.js b/www/manager6/lxc/Network.js
index e56d47c0..8f377bfa 100644
--- a/www/manager6/lxc/Network.js
+++ b/www/manager6/lxc/Network.js
@@ -6,6 +6,15 @@ Ext.define('PVE.lxc.NetworkInputPanel', {
onlineHelp: 'pct_container_network',
+ viewModel: {
+ data: {
+ bridgeType: '',
+ },
+ formulas: {
+ isVPPBridge: (get) => get('bridgeType') === 'VPPBridge',
+ },
+ },
+
setNodename: function (nodename) {
let me = this;
@@ -116,6 +125,20 @@ Ext.define('PVE.lxc.NetworkInputPanel', {
fieldLabel: gettext('Bridge'),
value: cdata.bridge,
allowBlank: false,
+ listeners: {
+ change: function (field, value) {
+ let store = field.getStore();
+ let rec = store.findRecord('iface', value, 0, false, false, true);
+ let type = rec ? rec.data.type : '';
+ me.getViewModel().set('bridgeType', type);
+ if (type === 'VPPBridge') {
+ let fw = me.down('field[name=firewall]');
+ if (fw) {
+ fw.setValue(false);
+ }
+ }
+ },
+ },
},
{
xtype: 'pveVlanField',
@@ -127,6 +150,17 @@ Ext.define('PVE.lxc.NetworkInputPanel', {
fieldLabel: gettext('Firewall'),
name: 'firewall',
value: cdata.firewall,
+ bind: {
+ disabled: '{isVPPBridge}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('Kernel firewall is not available with VPP bridges'),
+ bind: {
+ hidden: '{!isVPPBridge}',
+ },
},
];
diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js
index f6cd8749..bd24fe68 100644
--- a/www/manager6/node/Config.js
+++ b/www/manager6/node/Config.js
@@ -193,6 +193,7 @@ Ext.define('PVE.node.Config', {
showAltNames: true,
groups: ['services'],
nodename: nodename,
+ types: ['bridge', 'bond', 'vlan', 'ovs', 'vpp'],
editOptions: {
enableBridgeVlanIds: true,
},
diff --git a/www/manager6/qemu/NetworkEdit.js b/www/manager6/qemu/NetworkEdit.js
index 2ba13c40..3d096465 100644
--- a/www/manager6/qemu/NetworkEdit.js
+++ b/www/manager6/qemu/NetworkEdit.js
@@ -38,10 +38,12 @@ Ext.define('PVE.qemu.NetworkInputPanel', {
data: {
networkModel: undefined,
mtu: '',
+ bridgeType: '',
},
formulas: {
isVirtio: (get) => get('networkModel') === 'virtio',
showMtuHint: (get) => get('mtu') === 1,
+ isVPPBridge: (get) => get('bridgeType') === 'VPPBridge',
},
},
@@ -82,6 +84,20 @@ Ext.define('PVE.qemu.NetworkInputPanel', {
nodename: me.nodename,
autoSelect: true,
allowBlank: false,
+ listeners: {
+ change: function (field, value) {
+ let store = field.getStore();
+ let rec = store.findRecord('iface', value, 0, false, false, true);
+ let type = rec ? rec.data.type : '';
+ me.getViewModel().set('bridgeType', type);
+ if (type === 'VPPBridge') {
+ let fw = me.down('field[name=firewall]');
+ if (fw) {
+ fw.setValue(false);
+ }
+ }
+ },
+ },
});
me.column1 = [
@@ -96,6 +112,17 @@ Ext.define('PVE.qemu.NetworkInputPanel', {
fieldLabel: gettext('Firewall'),
name: 'firewall',
checked: me.insideWizard || me.isCreate,
+ bind: {
+ disabled: '{isVPPBridge}',
+ },
+ },
+ {
+ xtype: 'displayfield',
+ userCls: 'pmx-hint',
+ value: gettext('Kernel firewall is not available with VPP bridges'),
+ bind: {
+ hidden: '{!isVPPBridge}',
+ },
},
];
diff --git a/www/manager6/window/Migrate.js b/www/manager6/window/Migrate.js
index ff80c70c..c1509be6 100644
--- a/www/manager6/window/Migrate.js
+++ b/www/manager6/window/Migrate.js
@@ -463,6 +463,54 @@ Ext.define('PVE.window.Migrate', {
}
}
+ if (vm.get('running')) {
+ try {
+ let { result: netResult } = await Proxmox.Async.api2({
+ url: `/nodes/${vm.get('nodename')}/network?type=any_bridge`,
+ method: 'GET',
+ });
+ let vppBridges = new Set();
+ for (const iface of netResult.data || []) {
+ if (iface.type === 'VPPBridge') {
+ vppBridges.add(iface.iface);
+ }
+ }
+ if (vppBridges.size > 0) {
+ let vmConfig = {};
+ try {
+ let { result: cfgResult } = await Proxmox.Async.api2({
+ url: `/nodes/${vm.get('nodename')}/qemu/${vm.get('vmid')}/config`,
+ method: 'GET',
+ });
+ vmConfig = cfgResult.data || {};
+ } catch (_err) { /* ignore */ }
+
+ let vppNics = [];
+ for (const [key, value] of Object.entries(vmConfig)) {
+ if (!key.match(/^net\d+$/)) {
+ continue;
+ }
+ let net = PVE.Parser.parseQemuNetwork(key, value);
+ if (net && net.bridge && vppBridges.has(net.bridge)) {
+ vppNics.push(key);
+ }
+ }
+ if (vppNics.length > 0) {
+ migration.possible = false;
+ migration.preconditions.push({
+ text: Ext.String.format(
+ gettext('Cannot live-migrate VM with VPP vhost-user NICs: {0}. Use offline migration or HA (stop/start).'),
+ vppNics.join(', '),
+ ),
+ severity: 'error',
+ });
+ }
+ }
+ } catch (_err) {
+ // VPP bridge check is best-effort
+ }
+ }
+
vm.set('migration', migration);
},
checkLxcPreconditions: async function (resetMigrationPossible) {
--
2.50.1 (Apple Git-155)
next prev parent reply other threads:[~2026-03-16 22:28 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-16 22:28 [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Ryosuke Nakayama
2026-03-16 22:28 ` Ryosuke Nakayama [this message]
2026-03-16 22:28 ` [RFC PATCH widget-toolkit 2/2] ui: network: add VPP (fd.io) bridge type support Ryosuke Nakayama
2026-03-17 6:39 ` [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Stefan Hanreich
2026-03-17 10:18 ` DERUMIER, Alexandre
2026-03-17 11:14 ` Ryosuke Nakayama
2026-03-17 11:14 ` [RFC PATCH qemu-server 1/2] qemu: add VPP vhost-user dataplane support Ryosuke Nakayama
2026-03-17 11:14 ` [RFC PATCH qemu-server 2/2] qemu: VPP: clean up vhost-user interfaces on stop, fix tx_queue_size Ryosuke Nakayama
2026-03-17 11:26 ` [RFC PATCH qemu-server 1/2] qemu: add VPP vhost-user dataplane support Ryosuke Nakayama
2026-03-17 11:21 ` [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Ryosuke Nakayama
2026-03-17 11:21 ` [RFC PATCH pve-common] network: add VPP bridge helpers for vhost-user dataplane Ryosuke Nakayama
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260316222816.42944-2-ryosuke.nakayama@ryskn.com \
--to=ryosuke.nakayama@ryskn.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.