public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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)




  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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal