public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane
@ 2026-03-16 22:28 Ryosuke Nakayama
  2026-03-16 22:28 ` [RFC PATCH manager 1/2] api: network: add VPP (fd.io) dataplane bridge support Ryosuke Nakayama
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Ryosuke Nakayama @ 2026-03-16 22:28 UTC (permalink / raw)
  To: pve-devel

From: ryskn <ryosuke.nakayama@ryskn.com>

This RFC series integrates VPP (Vector Packet Processor, fd.io) as an
optional userspace dataplane alongside OVS in Proxmox VE.

VPP is a DPDK-based, userspace packet processing framework that
provides VM networking via vhost-user sockets. It is already used in
production by several cloud/telecom stacks. The motivation here is to
expose VPP bridge domains natively in the PVE WebUI and REST API,
following the same pattern as OVS integration.

Background and prior discussion:
  https://forum.proxmox.com/threads/interest-in-vpp-vector-packet-processing-as-a-dataplane-option-for-proxmox.181530/

Note: the benchmark figures quoted in that forum thread are slightly
off due to test configuration differences. Please use the numbers in
this cover letter instead.

--- What the patches do ---

Patch 1 (pve-manager):
  - 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 bridge-domain learn flag)
  - VPP VLAN subinterface create/delete/list, persisted to
    /etc/vpp/pve-vlans.conf
  - Exclude VPP bridges from the SDN-only access guard so they appear
    in the WebUI NIC selector
  - Vhost-user socket convention:
    /var/run/vpp/qemu-<vmid>-<net>.sock
  - pve8to9: add upgrade checker for VPP dependencies

Patch 2 (proxmox-widget-toolkit):
  - Add VPPBridge/VPPVlan to network_iface_types (Utils.js)
  - NetworkView: VPPBridge and VPPVlan entries in the Create menu;
    render vlan-raw-device in Ports/Slaves column for VPPVlan;
    vpp_vlan_aware support in VLAN aware column
  - NetworkEdit: vppbrN name validator; vpp_bridge field for VPPVlan;
    hide MTU/Autostart/IP fields for VPP types; use VlanName vtype
    for VPPVlan (allows dot notation, e.g. tap0.100)

--- Testing ---

Due to the absence of physical NICs in my test environment, all
benchmarks were performed as VM-to-VM communication over the
hypervisor's virtual switch (vmbr1 or VPP bridge domain). These
results reflect the virtual switching overhead, not physical NIC
performance, where VPP's DPDK polling would show a larger advantage.

Host: Proxmox VE 8.x (Intel Xeon), VMs: Debian 12 (virtio-net q=1)
VPP: 24.06, coalescing: frames=32 time=0.5ms, polling mode

iperf3 / netperf (single queue, VM-to-VM):

  Metric             vmbr1          VPP (vhost-user)
  iperf3             31.0 Gbits/s   13.2 Gbits/s
  netperf TCP_STREAM 32,243 Mbps    13,181 Mbps
  netperf TCP_RR     15,734 tx/s    989 tx/s

VPP's raw throughput is lower than vmbr1 in this VM-to-VM setup due
to vhost-user coalescing latency. Physical NIC testing (DPDK PMD) is
expected to close or reverse this gap.

gRPC (unary, grpc-flow-bench, single queue, VM-to-VM):

  Flows  Metric    vmbr1     VPP
  100    RPS       32,847    39,742
  100    p99 lat   7.28 ms   6.16 ms
  1000   RPS       40,315    41,139
  1000   p99 lat   48.96 ms  31.96 ms

VPP's userspace polling removes kernel scheduler jitter, which is
visible in the gRPC latency results even in the VM-to-VM scenario.

--- Known limitations / TODO ---

- No ifupdown2 integration yet; VPP config is managed separately via
  /etc/vpp/pve-bridges.conf and pve-vlans.conf
- No live migration path for vhost-user sockets (sockets must be
  pre-created on the target host)
- OVS and VPP cannot share the same physical NIC in this
  implementation
- VPP must be installed and running independently (not managed by PVE)

--- CLA ---

Individual CLA has been submitted to office@proxmox.com.

---

ryskn (2):
  api: network: add VPP (fd.io) dataplane bridge support
  ui: network: add VPP (fd.io) bridge type support

 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 ++++
 src/Utils.js                        |   2 +
 src/node/NetworkEdit.js             |  64 ++++-
 src/node/NetworkView.js             |  35 +++
 11 files changed, 675 insertions(+), 21 deletions(-)

-- 
2.50.1 (Apple Git-155)




^ permalink raw reply	[flat|nested] 4+ messages in thread

* [RFC PATCH manager 1/2] api: network: add VPP (fd.io) dataplane bridge support
  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
  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
  2 siblings, 0 replies; 4+ messages in thread
From: Ryosuke Nakayama @ 2026-03-16 22:28 UTC (permalink / raw)
  To: pve-devel

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)




^ permalink raw reply	[flat|nested] 4+ messages in thread

* [RFC PATCH widget-toolkit 2/2] ui: network: add VPP (fd.io) bridge type support
  2026-03-16 22:28 [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Ryosuke Nakayama
  2026-03-16 22:28 ` [RFC PATCH manager 1/2] api: network: add VPP (fd.io) dataplane bridge support Ryosuke Nakayama
@ 2026-03-16 22:28 ` Ryosuke Nakayama
  2026-03-17  6:39 ` [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Stefan Hanreich
  2 siblings, 0 replies; 4+ messages in thread
From: Ryosuke Nakayama @ 2026-03-16 22:28 UTC (permalink / raw)
  To: pve-devel

From: ryskn <ryosuke.nakayama@ryskn.com>

Add VPP bridge domain as a creatable/editable network type in the
Proxmox node network configuration UI.

- Utils.js: add VPPBridge/VPPVlan to network_iface_types with gettext()
- NetworkView.js: add 'vpp' to default types list; add VPPBridge and
  VPPVlan entries to the Create menu; VPPVlan uses a dedicated menu
  entry (no auto-generated default name); render vlan-raw-device in
  Ports/Slaves column for VPPVlan; fix VLAN aware column to render
  vpp_vlan_aware for VPP bridges; declare vpp_bridge/vpp_vlan_aware
  in the proxmox-networks model
- NetworkEdit.js: introduce vppTypes Set as single source of truth for
  VPP type checks; add vppbrN validator for VPPBridge name field; add
  vppbrN validator for vpp_bridge field in VPPVlan; increase maxLength
  to 40 for VPP interface names; hide MTU field for VPP types; exclude
  Autostart and IP/GW fields for VPP types via vppTypes; use VlanName
  vtype for VPPVlan to allow dot notation (e.g. tap0.100)

Signed-off-by: ryskn <ryosuke.nakayama@ryskn.com>
---
 src/Utils.js            |  2 ++
 src/node/NetworkEdit.js | 64 ++++++++++++++++++++++++++++++++++-------
 src/node/NetworkView.js | 35 ++++++++++++++++++----
 3 files changed, 85 insertions(+), 16 deletions(-)

diff --git a/src/Utils.js b/src/Utils.js
index 5457ffa..fa88fb1 100644
--- a/src/Utils.js
+++ b/src/Utils.js
@@ -707,6 +707,8 @@ Ext.define('Proxmox.Utils', {
             OVSBond: 'OVS Bond',
             OVSPort: 'OVS Port',
             OVSIntPort: 'OVS IntPort',
+            VPPBridge: gettext('VPP Bridge'),
+            VPPVlan: gettext('VPP VLAN'),
         },
 
         render_network_iface_type: function (value) {
diff --git a/src/node/NetworkEdit.js b/src/node/NetworkEdit.js
index c945139..c53cd90 100644
--- a/src/node/NetworkEdit.js
+++ b/src/node/NetworkEdit.js
@@ -21,7 +21,12 @@ Ext.define('Proxmox.node.NetworkEdit', {
 
         me.isCreate = !me.iface;
 
+        // Canonical set of VPP interface types — used to gate autostart,
+        // IP config, MTU, and other kernel-only fields.
+        const vppTypes = new Set(['VPPBridge', 'VPPVlan']);
+
         let iface_vtype;
+        let iface_validator; // optional extra validator for the Name field
 
         if (me.iftype === 'bridge') {
             iface_vtype = 'BridgeName';
@@ -39,6 +44,12 @@ Ext.define('Proxmox.node.NetworkEdit', {
             iface_vtype = 'InterfaceName';
         } else if (me.iftype === 'OVSPort') {
             iface_vtype = 'InterfaceName';
+        } else if (me.iftype === 'VPPBridge') {
+            iface_vtype = 'InterfaceName';
+            iface_validator = (v) =>
+                /^vppbr\d+$/.test(v) || gettext('Name must match vppbrN format (e.g. vppbr1)');
+        } else if (me.iftype === 'VPPVlan') {
+            iface_vtype = 'VlanName';
         } else {
             console.log(me.iftype);
             throw 'unknown network device type specified';
@@ -52,7 +63,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
             advancedColumn1 = [],
             advancedColumn2 = [];
 
-        if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || me.iftype === 'OVSBond')) {
+        if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || me.iftype === 'OVSBond' || vppTypes.has(me.iftype))) {
             column2.push({
                 xtype: 'proxmoxcheckbox',
                 fieldLabel: gettext('Autostart'),
@@ -295,6 +306,32 @@ Ext.define('Proxmox.node.NetworkEdit', {
                 fieldLabel: gettext('OVS options'),
                 name: 'ovs_options',
             });
+        } else if (me.iftype === 'VPPBridge') {
+            column2.push({
+                xtype: 'proxmoxcheckbox',
+                fieldLabel: gettext('VLAN aware'),
+                name: 'vpp_vlan_aware',
+                deleteEmpty: !me.isCreate,
+            });
+        } else if (me.iftype === 'VPPVlan') {
+            column2.push({
+                xtype: 'displayfield',
+                userCls: 'pmx-hint',
+                value: gettext('Name format: <parent>.<vlan-id>, e.g. tap0.100'),
+            });
+            column2.push({
+                xtype: me.isCreate ? 'textfield' : 'displayfield',
+                fieldLabel: gettext('Bridge domain'),
+                name: 'vpp_bridge',
+                emptyText: gettext('none'),
+                allowBlank: true,
+                validator: (v) =>
+                    !v || /^vppbr\d+$/.test(v) || gettext('Must match vppbrN format (e.g. vppbr1)'),
+                autoEl: {
+                    tag: 'div',
+                    'data-qtip': gettext('VPP bridge domain to attach this VLAN interface to, e.g. vppbr1'),
+                },
+            });
         }
 
         column2.push({
@@ -328,8 +365,9 @@ Ext.define('Proxmox.node.NetworkEdit', {
                 name: 'iface',
                 value: me.iface,
                 vtype: iface_vtype,
+                validator: iface_validator,
                 allowBlank: false,
-                maxLength: iface_vtype === 'BridgeName' ? 10 : 15,
+                maxLength: iface_vtype === 'BridgeName' ? 10 : (vppTypes.has(me.iftype) ? 40 : 15),
                 autoEl: {
                     tag: 'div',
                     'data-qtip': gettext('For example, vmbr0.100, vmbr0, vlan0.100, vlan0'),
@@ -391,6 +429,8 @@ Ext.define('Proxmox.node.NetworkEdit', {
                     name: 'ovs_bonds',
                 },
             );
+        } else if (vppTypes.has(me.iftype)) {
+            // VPP interfaces do not use kernel IP configuration
         } else {
             column1.push(
                 {
@@ -423,15 +463,17 @@ Ext.define('Proxmox.node.NetworkEdit', {
                 },
             );
         }
-        advancedColumn1.push({
-            xtype: 'proxmoxintegerfield',
-            minValue: 1280,
-            maxValue: 65520,
-            deleteEmpty: !me.isCreate,
-            emptyText: 1500,
-            fieldLabel: 'MTU',
-            name: 'mtu',
-        });
+        if (!vppTypes.has(me.iftype)) {
+            advancedColumn1.push({
+                xtype: 'proxmoxintegerfield',
+                minValue: 1280,
+                maxValue: 65520,
+                deleteEmpty: !me.isCreate,
+                emptyText: 1500,
+                fieldLabel: 'MTU',
+                name: 'mtu',
+            });
+        }
 
         Ext.applyIf(me, {
             url: url,
diff --git a/src/node/NetworkView.js b/src/node/NetworkView.js
index 0ff9649..164b349 100644
--- a/src/node/NetworkView.js
+++ b/src/node/NetworkView.js
@@ -19,6 +19,8 @@ Ext.define('proxmox-networks', {
         'type',
         'vlan-id',
         'vlan-raw-device',
+        'vpp_bridge',
+        'vpp_vlan_aware',
     ],
     idProperty: 'iface',
 });
@@ -30,7 +32,7 @@ Ext.define('Proxmox.node.NetworkView', {
 
     // defines what types of network devices we want to create
     // order is always the same
-    types: ['bridge', 'bond', 'vlan', 'ovs'],
+    types: ['bridge', 'bond', 'vlan', 'ovs', 'vpp'],
 
     showApplyBtn: false,
 
@@ -223,6 +225,27 @@ Ext.define('Proxmox.node.NetworkView', {
             });
         }
 
+        if (me.types.indexOf('vpp') !== -1) {
+            if (menu_items.length > 0) {
+                menu_items.push({ xtype: 'menuseparator' });
+            }
+
+            addEditWindowToMenu('VPPBridge', 'vppbr');
+            menu_items.push({
+                text: Proxmox.Utils.render_network_iface_type('VPPVlan'),
+                handler: () =>
+                    Ext.create('Proxmox.node.NetworkEdit', {
+                        autoShow: true,
+                        nodename: me.nodename,
+                        iftype: 'VPPVlan',
+                        ...me.editOptions,
+                        listeners: {
+                            destroy: () => reload(),
+                        },
+                    }),
+            });
+        }
+
         let renderer_generator = function (fieldname) {
             return function (val, metaData, rec) {
                 let tmp = [];
@@ -326,14 +349,14 @@ Ext.define('Proxmox.node.NetworkView', {
                             undefinedText: Proxmox.Utils.noText,
                         },
                         {
-                            xtype: 'booleancolumn',
                             header: gettext('VLAN aware'),
                             width: 80,
                             sortable: true,
                             dataIndex: 'bridge_vlan_aware',
-                            trueText: Proxmox.Utils.yesText,
-                            falseText: Proxmox.Utils.noText,
-                            undefinedText: Proxmox.Utils.noText,
+                            renderer: (value, metaData, { data }) => {
+                                const v = data.bridge_vlan_aware || data.vpp_vlan_aware;
+                                return v ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+                            },
                         },
                         {
                             header: gettext('Ports/Slaves'),
@@ -347,6 +370,8 @@ Ext.define('Proxmox.node.NetworkView', {
                                     return data.ovs_ports;
                                 } else if (value === 'OVSBond') {
                                     return data.ovs_bonds;
+                                } else if (value === 'VPPVlan') {
+                                    return data['vlan-raw-device'];
                                 }
                                 return '';
                             },
-- 
2.50.1 (Apple Git-155)




^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane
  2026-03-16 22:28 [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Ryosuke Nakayama
  2026-03-16 22:28 ` [RFC PATCH manager 1/2] api: network: add VPP (fd.io) dataplane bridge support Ryosuke Nakayama
  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 ` Stefan Hanreich
  2 siblings, 0 replies; 4+ messages in thread
From: Stefan Hanreich @ 2026-03-17  6:39 UTC (permalink / raw)
  To: pve-devel

Hi!

Thanks for your contribution!

I was already following the discussion in the linked forum thread and
shortly discussed this proposal with a colleague - but I wasn't able to
find the time yet to take a closer look at VPP itself in order to form
an opinion. I'll take a closer look in the coming days and give the
patches a spin on my machine.


On 3/16/26 11:27 PM, Ryosuke Nakayama wrote:
> From: ryskn <ryosuke.nakayama@ryskn.com>
> 
> This RFC series integrates VPP (Vector Packet Processor, fd.io) as an
> optional userspace dataplane alongside OVS in Proxmox VE.
> 
> VPP is a DPDK-based, userspace packet processing framework that
> provides VM networking via vhost-user sockets. It is already used in
> production by several cloud/telecom stacks. The motivation here is to
> expose VPP bridge domains natively in the PVE WebUI and REST API,
> following the same pattern as OVS integration.
> 
> Background and prior discussion:
>   https://forum.proxmox.com/threads/interest-in-vpp-vector-packet-processing-as-a-dataplane-option-for-proxmox.181530/
> 
> Note: the benchmark figures quoted in that forum thread are slightly
> off due to test configuration differences. Please use the numbers in
> this cover letter instead.
> 
> --- What the patches do ---
> 
> Patch 1 (pve-manager):
>   - 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 bridge-domain learn flag)
>   - VPP VLAN subinterface create/delete/list, persisted to
>     /etc/vpp/pve-vlans.conf
>   - Exclude VPP bridges from the SDN-only access guard so they appear
>     in the WebUI NIC selector
>   - Vhost-user socket convention:
>     /var/run/vpp/qemu-<vmid>-<net>.sock
>   - pve8to9: add upgrade checker for VPP dependencies
> 
> Patch 2 (proxmox-widget-toolkit):
>   - Add VPPBridge/VPPVlan to network_iface_types (Utils.js)
>   - NetworkView: VPPBridge and VPPVlan entries in the Create menu;
>     render vlan-raw-device in Ports/Slaves column for VPPVlan;
>     vpp_vlan_aware support in VLAN aware column
>   - NetworkEdit: vppbrN name validator; vpp_bridge field for VPPVlan;
>     hide MTU/Autostart/IP fields for VPP types; use VlanName vtype
>     for VPPVlan (allows dot notation, e.g. tap0.100)
> 
> --- Testing ---
> 
> Due to the absence of physical NICs in my test environment, all
> benchmarks were performed as VM-to-VM communication over the
> hypervisor's virtual switch (vmbr1 or VPP bridge domain). These
> results reflect the virtual switching overhead, not physical NIC
> performance, where VPP's DPDK polling would show a larger advantage.
> 
> Host: Proxmox VE 8.x (Intel Xeon), VMs: Debian 12 (virtio-net q=1)
> VPP: 24.06, coalescing: frames=32 time=0.5ms, polling mode
> 
> iperf3 / netperf (single queue, VM-to-VM):
> 
>   Metric             vmbr1          VPP (vhost-user)
>   iperf3             31.0 Gbits/s   13.2 Gbits/s
>   netperf TCP_STREAM 32,243 Mbps    13,181 Mbps
>   netperf TCP_RR     15,734 tx/s    989 tx/s
> 
> VPP's raw throughput is lower than vmbr1 in this VM-to-VM setup due
> to vhost-user coalescing latency. Physical NIC testing (DPDK PMD) is
> expected to close or reverse this gap.
> 
> gRPC (unary, grpc-flow-bench, single queue, VM-to-VM):
> 
>   Flows  Metric    vmbr1     VPP
>   100    RPS       32,847    39,742
>   100    p99 lat   7.28 ms   6.16 ms
>   1000   RPS       40,315    41,139
>   1000   p99 lat   48.96 ms  31.96 ms
> 
> VPP's userspace polling removes kernel scheduler jitter, which is
> visible in the gRPC latency results even in the VM-to-VM scenario.
> 
> --- Known limitations / TODO ---
> 
> - No ifupdown2 integration yet; VPP config is managed separately via
>   /etc/vpp/pve-bridges.conf and pve-vlans.conf
> - No live migration path for vhost-user sockets (sockets must be
>   pre-created on the target host)
> - OVS and VPP cannot share the same physical NIC in this
>   implementation
> - VPP must be installed and running independently (not managed by PVE)
> 
> --- CLA ---
> 
> Individual CLA has been submitted to office@proxmox.com.
> 
> ---
> 
> ryskn (2):
>   api: network: add VPP (fd.io) dataplane bridge support
>   ui: network: add VPP (fd.io) bridge type support
> 
>  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 ++++
>  src/Utils.js                        |   2 +
>  src/node/NetworkEdit.js             |  64 ++++-
>  src/node/NetworkView.js             |  35 +++
>  11 files changed, 675 insertions(+), 21 deletions(-)
> 





^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-03-17  6:39 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-03-16 22:28 [RFC PATCH 0/2] network: add VPP (fd.io) as alternative dataplane Ryosuke Nakayama
2026-03-16 22:28 ` [RFC PATCH manager 1/2] api: network: add VPP (fd.io) dataplane bridge support Ryosuke Nakayama
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

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