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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox