From: Gabriel Goller <g.goller@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH ifupdown2] fix #8299: only recreate vxlan and vlan devices on reload if required
Date: Fri, 06 Nov 2026 14:42:26 +0100 [thread overview]
Message-ID: <20261106-fix_8299_only-v1-1-a7d760fff147@proxmox.com> (raw)
Every config change on a vxlan or vlan interface caused ifreload to
delete and recreate the device. Adding a vxlan-remoteip thus removed
the vxlan from its bridge and flushed all fdb entries, causing an
outage on the VNI instead of just appending the new fdb entry.
Replace the patch forcing the recreation with one that only skips it if
all changed attributes are known to be applied in place by ifup,
including being reset when removed. Any other change still recreates
the device.
Fixes: https://bugzilla.proxmox.com/show_bug.cgi?id=8299
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
---
Wasn't sure if I should leave the old patch and make a new one
overriding it or if I should delete it. Went for the second version, let
me know if I should do it differently.
---
...wn-up-vxlan-vlan-interfaces-when-ifreload.patch | 39 ---
...ly-recreate-vxlan-and-vlan-devices-if-req.patch | 280 +++++++++++++++++++++
debian/patches/series | 2 +-
3 files changed, 281 insertions(+), 40 deletions(-)
diff --git a/debian/patches/pve/0004-ifreload-down-up-vxlan-vlan-interfaces-when-ifreload.patch b/debian/patches/pve/0004-ifreload-down-up-vxlan-vlan-interfaces-when-ifreload.patch
deleted file mode 100644
index a8ca532ad4ee..000000000000
--- a/debian/patches/pve/0004-ifreload-down-up-vxlan-vlan-interfaces-when-ifreload.patch
+++ /dev/null
@@ -1,39 +0,0 @@
-From 1926699b02882bc5ed659d4debce2a0127d2edf0 Mon Sep 17 00:00:00 2001
-From: Alexandre Derumier <aderumier@odiso.com>
-Date: Wed, 5 Jun 2019 14:47:05 +0200
-Subject: [PATCH 04/12] ifreload: down/up vxlan && vlan interfaces when
- ifreload_down_changed=0
-
-almost all attributes of vxlan interfaces can't be updated
-(including vxlan-id).
-
-Same for vlan interfaces (vlan-protocol, vlan-id)
-
-so when ifreload_down_changed=0, ifreload can't update vxlan or vlan
- attributes.
-
-fix: https://github.com/CumulusNetworks/ifupdown2/issues/50
-Signed-off-by: Alexandre Derumier <aderumier@odiso.com>
-Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
----
- ifupdown2/ifupdown/ifupdownmain.py | 4 +++-
- 1 file changed, 3 insertions(+), 1 deletion(-)
-
-diff --git a/ifupdown2/ifupdown/ifupdownmain.py b/ifupdown2/ifupdown/ifupdownmain.py
-index b73c742..65fd67e 100644
---- a/ifupdown2/ifupdown/ifupdownmain.py
-+++ b/ifupdown2/ifupdown/ifupdownmain.py
-@@ -2355,7 +2355,9 @@ class ifupdownMain:
- % (newifaceobjlist[objidx].name,
- ifaceLinkKind.to_str(lastifaceobjlist[0].link_kind)))
- ifacedownlist.append(newifaceobjlist[objidx].name)
-- if not down_changed:
-+
-+ ifacelink_kind = ifaceLinkKind.to_str(lastifaceobjlist[0].link_kind)
-+ if not down_changed and ifacelink_kind != 'vxlan' and ifacelink_kind != 'vlan':
- continue
- if len(newifaceobjlist) != len(lastifaceobjlist):
- ifacedownlist.append(ifname)
---
-2.30.2
-
diff --git a/debian/patches/pve/0004-ifreload-only-recreate-vxlan-and-vlan-devices-if-req.patch b/debian/patches/pve/0004-ifreload-only-recreate-vxlan-and-vlan-devices-if-req.patch
new file mode 100644
index 000000000000..93d4c540e110
--- /dev/null
+++ b/debian/patches/pve/0004-ifreload-only-recreate-vxlan-and-vlan-devices-if-req.patch
@@ -0,0 +1,280 @@
+From 1cdc3a3b46aa7969b2d3ea8eb898b7d5352a6762 Mon Sep 17 00:00:00 2001
+From: Gabriel Goller <g.goller@proxmox.com>
+Date: Fri, 6 Nov 2026 11:21:23 +0100
+Subject: [PATCH] ifreload: only recreate vxlan and vlan devices if required
+
+Previously every config change on a vxlan or vlan interface caused
+ifreload to delete and recreate the device, even with
+ifreload_down_changed=0. E.g. adding a vxlan-remoteip removed the vxlan
+from its bridge and flushed all fdb entries, causing an outage on the
+VNI instead of just appending the new fdb entry.
+
+Keep recreating by default, as most vxlan and vlan attributes can't be
+changed on an existing device, but skip it if only attributes changed
+that ifup applies in place, including resetting them when removed.
+
+Some resets previously only happened as a side effect of the recreation:
+removing all vxlan-remoteip lines now reconciles the fdb entries, taking
+remote ips of all stanzas of the interface into account, and removing
+vxlan-ttl on single vxlan and l3vxi devices now sends ttl 0. With
+adjust_logical_dev_mtu=0 removing mtu from a vlan isn't reset, so keep
+recreating in that case. The same applies to address and gateway on
+interfaces with multiple stanzas or address-purge no, as stale entries
+aren't purged there.
+
+This replaces the patch "ifreload: down/up vxlan && vlan interfaces when
+ifreload_down_changed=0" by Alexandre Derumier.
+
+Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
+---
+ ifupdown2/addons/vxlan.py | 25 ++++++++++-----
+ ifupdown2/ifupdown/iface.py | 16 +++++-----
+ ifupdown2/ifupdown/ifupdownmain.py | 37 ++++++++++++++++++++--
+ ifupdown2/lib/iproute2.py | 4 +--
+ tests/unit/test_reload_recreate.py | 51 ++++++++++++++++++++++++++++++
+ 5 files changed, 113 insertions(+), 20 deletions(-)
+ create mode 100644 tests/unit/test_reload_recreate.py
+
+diff --git a/ifupdown2/addons/vxlan.py b/ifupdown2/addons/vxlan.py
+index cc8d3b3b7d45..b9befdaebaad 100644
+--- a/ifupdown2/addons/vxlan.py
++++ b/ifupdown2/addons/vxlan.py
+@@ -1067,7 +1067,7 @@ class vxlan(Vxlan, moduleBase):
+
+ return None
+
+- def _up(self, ifaceobj):
++ def _up(self, ifaceobj, ifaceobj_getfunc=None):
+ self.check_and_raise_svd_tvd_errors(ifaceobj)
+
+ vxlan_id_str = ifaceobj.get_attr_value_first("vxlan-id")
+@@ -1231,7 +1231,10 @@ class vxlan(Vxlan, moduleBase):
+
+ vxlan_purge_remotes = self.__get_vlxan_purge_remotes(ifaceobj)
+
+- remoteips = ifaceobj.get_attr_value('vxlan-remoteip')
++ # with ifaceobj squashing disabled, vxlan-remoteip may be spread
++ # over multiple stanzas of the same interface
++ ifaceobjs = ifaceobj_getfunc(ifaceobj.name) if ifaceobj_getfunc else None
++ remoteips = self.get_remote_ips(ifaceobjs or [ifaceobj])
+ if remoteips:
+ try:
+ for remoteip in remoteips:
+@@ -1243,7 +1246,7 @@ class vxlan(Vxlan, moduleBase):
+ # purge any removed remote ip
+ old_remoteips = self.get_old_remote_ips(ifaceobj.name)
+
+- if vxlan_purge_remotes or (isinstance(remoteips,list) and remoteips != old_remoteips):
++ if vxlan_purge_remotes or remoteips != old_remoteips:
+ # figure out the diff for remotes and do the bridge fdb updates
+ # only if provisioned by user and not by an vxlan external
+ # controller.
+@@ -1285,13 +1288,17 @@ class vxlan(Vxlan, moduleBase):
+
+ self.vxlan_remote_ip_map(ifaceobj, vxlan_mcast_grp_map)
+
++ @staticmethod
++ def get_remote_ips(ifaceobjs):
++ remoteips = []
++ for ifaceobj in ifaceobjs or []:
++ for remote in ifaceobj.get_attr_value("vxlan-remoteip") or []:
++ remoteips.append(remote)
++ return remoteips
++
+ @staticmethod
+ def get_old_remote_ips(ifname):
+- old_remoteips = []
+- for old_ifaceobj in statemanager.get_ifaceobjs(ifname) or []:
+- for remote in old_ifaceobj.get_attr_value("vxlan-remoteip") or []:
+- old_remoteips.append(remote)
+- return old_remoteips
++ return vxlan.get_remote_ips(statemanager.get_ifaceobjs(ifname))
+
+ def vxlan_remote_ip_map(self, ifaceobj, vxlan_mcast_grp_map):
+ # get user configured remote ip map
+@@ -1754,5 +1761,7 @@ class vxlan(Vxlan, moduleBase):
+
+ if operation == 'query-checkcurr':
+ op_handler(self, ifaceobj, query_ifaceobj)
++ elif operation == 'pre-up':
++ op_handler(self, ifaceobj, extra_args.get('ifaceobj_getfunc'))
+ else:
+ op_handler(self, ifaceobj)
+diff --git a/ifupdown2/ifupdown/iface.py b/ifupdown2/ifupdown/iface.py
+index fe99b6d168b9..de2ab588e884 100644
+--- a/ifupdown2/ifupdown/iface.py
++++ b/ifupdown2/ifupdown/iface.py
+@@ -620,10 +620,11 @@ class iface():
+ Looks at the iface _config_status dict"""
+ return self._config_status.get(attr_name, [])[idx]
+
+- def compare(self, dstiface):
++ def compare(self, dstiface, ignore_attrs=()):
+ """ compares iface object with iface object passed as argument
+
+- Returns True if object self is same as dstiface and False otherwise """
++ Returns True if object self is same as dstiface and False otherwise.
++ Attributes in ignore_attrs are not compared. """
+
+ if self.name != dstiface.name: return False
+ if self.type != dstiface.type: return False
+@@ -631,12 +632,11 @@ class iface():
+ if self.addr_method != dstiface.addr_method: return False
+ if self.auto != dstiface.auto: return False
+ if self.classes != dstiface.classes: return False
+- if len(self.config) != len(dstiface.config):
+- return False
+- if any(True for k in self.config if k not in dstiface.config):
+- return False
+- if any(True for k,v in list(self.config.items())
+- if v != dstiface.config.get(k)): return False
++ for k in set(self.config) | set(dstiface.config):
++ if k in ignore_attrs:
++ continue
++ if self.config.get(k) != dstiface.config.get(k):
++ return False
+ return True
+
+ def squash(self, newifaceobj):
+diff --git a/ifupdown2/ifupdown/ifupdownmain.py b/ifupdown2/ifupdown/ifupdownmain.py
+index b73c742137b2..15da490f9c0c 100644
+--- a/ifupdown2/ifupdown/ifupdownmain.py
++++ b/ifupdown2/ifupdown/ifupdownmain.py
+@@ -93,6 +93,20 @@ class ifupdownMain:
+ addon_modules_dir = ADDON_MODULES_DIR
+ addon_modules_configfile = ADDONS_CONF_PATH
+
++ # vxlan and vlan attributes which ifup applies to an existing device,
++ # including resetting them when removed from the config
++ RELOAD_LIVE_ATTRS = frozenset([
++ 'vxlan-remoteip',
++ 'vxlan-remoteip-map',
++ 'vxlan-ttl',
++ 'vxlan-ageing',
++ 'vlan-bridge-binding',
++ 'mtu',
++ 'alias',
++ 'address',
++ 'gateway',
++ ])
++
+ # Handlers for ops that ifupdown2 owns
+ def run_up(self, ifaceobj):
+ # Skip link sets on ifaceobjs of type 'vlan' (used for l2 attrs).
+@@ -2355,8 +2369,27 @@ class ifupdownMain:
+ % (newifaceobjlist[objidx].name,
+ ifaceLinkKind.to_str(lastifaceobjlist[0].link_kind)))
+ ifacedownlist.append(newifaceobjlist[objidx].name)
++
++ # most vxlan and vlan attributes can't be changed on an
++ # existing device, recreate it unless only attributes in
++ # RELOAD_LIVE_ATTRS changed
++ ignore_attrs = ()
+ if not down_changed:
+- continue
++ if not lastifaceobjlist[0].link_kind & (ifaceLinkKind.VXLAN | ifaceLinkKind.VLAN):
++ continue
++ ignore_attrs = self.RELOAD_LIVE_ATTRS
++ # without adjust_logical_dev_mtu removing mtu from a
++ # vlan doesn't reset it to the lower device mtu
++ if (lastifaceobjlist[0].link_kind & ifaceLinkKind.VLAN and
++ self.config.get('adjust_logical_dev_mtu', '1') == '0'):
++ ignore_attrs = ignore_attrs.difference(['mtu'])
++ # stale addresses and gateways are only removed for
++ # single stanza interfaces with address purging enabled
++ address_purge = utils.get_boolean_from_string(
++ newifaceobjlist[0].get_attr_value_first('address-purge'),
++ default=True)
++ if len(newifaceobjlist) > 1 or not address_purge:
++ ignore_attrs = ignore_attrs.difference(['address', 'gateway'])
+ if len(newifaceobjlist) != len(lastifaceobjlist):
+ ifacedownlist.append(ifname)
+ continue
+@@ -2367,7 +2400,7 @@ class ifupdownMain:
+ for objidx in range(0, len(lastifaceobjlist)):
+ oldobj = lastifaceobjlist[objidx]
+ newobj = newifaceobjlist[objidx]
+- if not newobj.compare(oldobj):
++ if not newobj.compare(oldobj, ignore_attrs):
+ ifacedownlist.append(ifname)
+ continue
+
+diff --git a/ifupdown2/lib/iproute2.py b/ifupdown2/lib/iproute2.py
+index 543082e34f62..3e927137bc98 100644
+--- a/ifupdown2/lib/iproute2.py
++++ b/ifupdown2/lib/iproute2.py
+@@ -310,7 +310,7 @@ class IPRoute2(Cache, Requirements):
+ if port:
+ cmd.append("dstport %s" % port)
+
+- if ttl:
++ if ttl is not None:
+ cmd.append("ttl %s" % ttl)
+
+ self.__execute_or_batch(utils.ip_cmd, " ".join(cmd))
+@@ -343,7 +343,7 @@ class IPRoute2(Cache, Requirements):
+ if port:
+ cmd.append("dstport %s" % port)
+
+- if ttl:
++ if ttl is not None:
+ cmd.append("ttl %s" % ttl)
+
+ self.__execute_or_batch(utils.ip_cmd, " ".join(cmd))
+diff --git a/tests/unit/test_reload_recreate.py b/tests/unit/test_reload_recreate.py
+new file mode 100644
+index 000000000000..5ba7912a94a3
+--- /dev/null
++++ b/tests/unit/test_reload_recreate.py
+@@ -0,0 +1,51 @@
++"""ifreload must only recreate vxlan/vlan devices for non-live attributes."""
++
++import unittest
++
++from ifupdown2.addons.vxlan import vxlan
++from ifupdown2.ifupdown.iface import iface
++from ifupdown2.ifupdown.ifupdownmain import ifupdownMain
++
++
++def make_iface(**attrs):
++ ifaceobj = iface({"name": "vxlan100"})
++ for attr, value in attrs.items():
++ ifaceobj.config[attr.replace("_", "-")] = value
++ return ifaceobj
++
++
++class ReloadRecreateTests(unittest.TestCase):
++ def changed(self, old, new):
++ return not make_iface(**new).compare(make_iface(**old), ifupdownMain.RELOAD_LIVE_ATTRS)
++
++ def test_live_attrs(self):
++ base = {"vxlan_id": ["100"], "vxlan_remoteip": ["10.0.0.2"]}
++ self.assertFalse(self.changed(base, dict(base, vxlan_remoteip=["10.0.0.2", "10.0.0.3"])))
++ self.assertFalse(self.changed(base, {"vxlan_id": ["100"]}))
++ self.assertFalse(self.changed(base, dict(base, mtu=["1450"])))
++
++ def test_other_attrs(self):
++ base = {"vxlan_id": ["100"]}
++ self.assertTrue(self.changed(base, {"vxlan_id": ["200"]}))
++ self.assertTrue(self.changed(base, dict(base, vxlan_port=["4790"])))
++ self.assertTrue(self.changed(dict(base, vxlan_tos=["1"]), base))
++ self.assertTrue(self.changed(base, dict(base, vxlan_learning=["no"])))
++
++ def test_vlan(self):
++ base = {"vlan_raw_device": ["eth0"], "vlan_id": ["10"]}
++ self.assertFalse(self.changed(base, dict(base, vlan_bridge_binding=["on"], mtu=["1400"])))
++ self.assertTrue(self.changed(base, dict(base, vlan_id=["11"])))
++ self.assertTrue(self.changed(base, dict(base, vlan_protocol=["802.1ad"])))
++
++
++class VxlanRemoteIpsTests(unittest.TestCase):
++ def test_remote_ips_merged_over_stanzas(self):
++ first = make_iface(vxlan_id=["100"], vxlan_remoteip=["10.0.0.2", "10.0.0.3"])
++ second = make_iface(mtu=["1450"])
++ self.assertEqual(vxlan.get_remote_ips([first, second]), ["10.0.0.2", "10.0.0.3"])
++ self.assertEqual(vxlan.get_remote_ips([second]), [])
++ self.assertEqual(vxlan.get_remote_ips(None), [])
++
++
++if __name__ == "__main__":
++ unittest.main()
diff --git a/debian/patches/series b/debian/patches/series
index 6de618c7844c..023e8e36374d 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,7 +1,7 @@
pve/0001-don-t-remove-tap-veth-fwpr-interfaces-from-bridge-on.patch
pve/0002-add-dummy-mtu-bridgevlanport-modules.patch
pve/0003-don-t-remove-bridge-is-tap-veth-are-still-plugged.patch
-pve/0004-ifreload-down-up-vxlan-vlan-interfaces-when-ifreload.patch
+pve/0004-ifreload-only-recreate-vxlan-and-vlan-devices-if-req.patch
pve/0005-config-tuning.patch
pve/0006-openvswitch-ovs-ports-condone-regex-exclude-tap-veth.patch
pve/0007-allow-vlan-tag-inside-vxlan-tunnel.patch
---
base-commit: 705d9a203ae3a8a896b4fb03217701c65b10ef21
change-id: 20261106-fix_8299_only-86419dd42acd
Best regards,
--
Gabriel Goller <g.goller@proxmox.com>
next reply other threads:[~2026-11-06 13:42 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-06 13:42 Gabriel Goller [this message]
2026-11-06 17:17 ` [PATCH ifupdown2] fix #8299: only recreate vxlan and vlan devices on reload if required Gabriel Goller
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=20261106-fix_8299_only-v1-1-a7d760fff147@proxmox.com \
--to=g.goller@proxmox.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