Originally reported in the community forum [0].

The sysfs-based methods do not handle altnames at all. Instead, get/set
interface MTUs through netlink directly, which also has transparent
altname support, and is a more robust way in general.

At the same time, this simplifies some things as it means that the MTU
will always be present in the Netlink cache.

[0] https://forum.proxmox.com/threads/wrong-mtu-after-upgrade-to-9.169887/

Signed-off-by: Christoph Heiss <c.he...@proxmox.com>
---
 ...et-interface-mtu-through-netlink-ins.patch | 318 ++++++++++++++++++
 debian/patches/series                         |   1 +
 2 files changed, 319 insertions(+)
 create mode 100644 
debian/patches/pve/0012-addons-nlcache-set-interface-mtu-through-netlink-ins.patch

diff --git 
a/debian/patches/pve/0012-addons-nlcache-set-interface-mtu-through-netlink-ins.patch
 
b/debian/patches/pve/0012-addons-nlcache-set-interface-mtu-through-netlink-ins.patch
new file mode 100644
index 0000000..e40e666
--- /dev/null
+++ 
b/debian/patches/pve/0012-addons-nlcache-set-interface-mtu-through-netlink-ins.patch
@@ -0,0 +1,318 @@
+From f7d6aa4769a86980ce0e93cc55f2ef0c6b4682a9 Mon Sep 17 00:00:00 2001
+From: Christoph Heiss <c.he...@proxmox.com>
+Date: Wed, 20 Aug 2025 14:02:22 +0200
+Subject: [PATCH] addons, nlcache: set interface mtu through netlink instead of
+ sysfs
+
+The sysfs-based methods do not handle altnames at all. Instead, get/set
+interface MTUs through netlink directly, which also has transparent
+altname support, and is a more robust way in general.
+
+At the same time, this simplifies some things as it means that the MTU
+will always be present in the netlink cache.
+
+Along the way also clean up some weird mtu_{int,str}, where the MTU is
+passed around in the `address` as both string and integer.
+
+Signed-off-by: Christoph Heiss <c.he...@proxmox.com>
+---
+ ifupdown2/addons/address.py        | 65 +++++++++++++++++++-----------
+ ifupdown2/addons/addressvirtual.py |  2 +-
+ ifupdown2/addons/bridge.py         |  2 +-
+ ifupdown2/lib/iproute2.py          |  5 +--
+ ifupdown2/lib/nlcache.py           | 33 +++++++++++++++
+ ifupdown2/lib/sysfs.py             | 16 --------
+ 6 files changed, 78 insertions(+), 45 deletions(-)
+
+diff --git a/ifupdown2/addons/address.py b/ifupdown2/addons/address.py
+index 3196a5e..25270c3 100644
+--- a/ifupdown2/addons/address.py
++++ b/ifupdown2/addons/address.py
+@@ -430,7 +430,7 @@ class address(AddonWithIpBlackList, moduleBase):
+             except ValueError as e:
+                 self.logger.warning("%s: invalid mtu %s: %s" % 
(ifaceobj.name, mtu_str, str(e)))
+                 return False
+-            return self._check_mtu_config(ifaceobj, mtu_str, mtu_int, 
ifaceobj_getfunc, syntaxcheck=True)
++            return self._check_mtu_config(ifaceobj, mtu_int, 
ifaceobj_getfunc, syntaxcheck=True)
+         return True
+ 
+     def syntax_check_addr_allowed_on(self, ifaceobj, syntax_check=False):
+@@ -792,7 +792,14 @@ class address(AddonWithIpBlackList, moduleBase):
+             return ipv
+         return prev_gateways
+ 
+-    def _check_mtu_config(self, ifaceobj, mtu_str, mtu_int, ifaceobj_getfunc, 
syntaxcheck=False):
++    def _check_mtu_config(self, ifaceobj, mtu, ifaceobj_getfunc, 
syntaxcheck=False):
++        """
++        :param ifaceobj:
++        :param mtu: integer
++        :param ifaceobj_getfunc:
++        :param syntaxcheck: boolean
++        """
++
+         retval = True
+         if (ifaceobj.link_kind & ifaceLinkKind.BRIDGE):
+             if syntaxcheck:
+@@ -806,10 +813,10 @@ class address(AddonWithIpBlackList, moduleBase):
+                 masterobj = ifaceobj_getfunc(ifaceobj.upperifaces[0])
+                 if masterobj:
+                     master_mtu = masterobj[0].get_attr_value_first('mtu')
+-                    if master_mtu and master_mtu != mtu_str:
++                    if master_mtu and master_mtu != str(mtu):
+                         log_msg = ("%s: bond slave mtu %s is different from 
bond master %s mtu %s. "
+                                   "There is no need to configure mtu on a 
bond slave." %
+-                                   (ifaceobj.name, mtu_str, 
masterobj[0].name, master_mtu))
++                                   (ifaceobj.name, mtu, masterobj[0].name, 
master_mtu))
+                         if syntaxcheck:
+                             self.logger.warning(log_msg)
+                             retval = False
+@@ -823,24 +830,30 @@ class address(AddonWithIpBlackList, moduleBase):
+                         lowerdev_mtu = 
int(lowerobj[0].get_attr_value_first('mtu') or 0)
+                     else:
+                         lowerdev_mtu = 
self.cache.get_link_mtu(lowerobj[0].name)  # return type: int
+-                    if lowerdev_mtu and mtu_int > lowerdev_mtu:
++                    if lowerdev_mtu and mtu > lowerdev_mtu:
+                         self.logger.warning('%s: vlan dev mtu %s is greater 
than lower realdev %s mtu %s'
+-                                         %(ifaceobj.name, mtu_str, 
lowerobj[0].name, lowerdev_mtu))
++                                         %(ifaceobj.name, mtu, 
lowerobj[0].name, lowerdev_mtu))
+                         retval = False
+                     elif (not lowerobj[0].link_kind and
+                           not (lowerobj[0].link_privflags & 
ifaceLinkPrivFlags.LOOPBACK) and
+-                          not lowerdev_mtu and self.default_mtu and (mtu_int 
> self.default_mtu_int)):
++                          not lowerdev_mtu and self.default_mtu and (mtu > 
self.default_mtu_int)):
+                         # only check default mtu on lower device which is a 
physical interface
+                         self.logger.warning('%s: vlan dev mtu %s is greater 
than lower realdev %s mtu %s'
+-                                         %(ifaceobj.name, mtu_str, 
lowerobj[0].name, self.default_mtu))
++                                         %(ifaceobj.name, mtu, 
lowerobj[0].name, self.default_mtu))
+                         retval = False
+-            if self.max_mtu and mtu_int > self.max_mtu:
++            if self.max_mtu and mtu > self.max_mtu:
+                 self.logger.warning('%s: specified mtu %s is greater than max 
mtu %s'
+-                                 %(ifaceobj.name, mtu_str, self.max_mtu))
++                                 %(ifaceobj.name, mtu, self.max_mtu))
+                 retval = False
+         return retval
+ 
+-    def _propagate_mtu_to_upper_devs(self, ifaceobj, mtu_str, mtu_int, 
ifaceobj_getfunc):
++    def _propagate_mtu_to_upper_devs(self, ifaceobj, mtu, ifaceobj_getfunc):
++        """
++        :param ifaceobj:
++        :param mtu: integer
++        :param ifaceobj_getfunc:
++        """
++
+         if (not ifaceobj.upperifaces or
+             (ifaceobj.link_privflags & ifaceLinkPrivFlags.BOND_SLAVE) or
+             (ifaceobj.link_privflags & ifaceLinkPrivFlags.VRF_SLAVE) or
+@@ -855,15 +868,21 @@ class address(AddonWithIpBlackList, moduleBase):
+             umtu = upperobjs[0].get_attr_value_first('mtu')
+             if not umtu:
+                 running_mtu = self.cache.get_link_mtu(upperobjs[0].name)
+-                if not running_mtu or running_mtu != mtu_int:
+-                    self.sysfs.link_set_mtu(u, mtu_str=mtu_str, 
mtu_int=mtu_int)
++                if not running_mtu or running_mtu != mtu:
++                    self.netlink.link_set_mtu(u, mtu)
+ 
+-    def _process_mtu_config_mtu_valid(self, ifaceobj, ifaceobj_getfunc, 
mtu_str, mtu_int):
+-        if not self._check_mtu_config(ifaceobj, mtu_str, mtu_int, 
ifaceobj_getfunc):
++    def _process_mtu_config_mtu_valid(self, ifaceobj, ifaceobj_getfunc, mtu):
++        """
++        :param ifaceobj:
++        :param ifaceobj_getfunc:
++        :param mtu: integer
++        """
++
++        if not self._check_mtu_config(ifaceobj, mtu, ifaceobj_getfunc):
+             return
+ 
+-        if mtu_int != self.cache.get_link_mtu(ifaceobj.name):
+-            self.sysfs.link_set_mtu(ifaceobj.name, mtu_str=mtu_str, 
mtu_int=mtu_int)
++        if mtu != self.cache.get_link_mtu(ifaceobj.name):
++            self.netlink.link_set_mtu(ifaceobj.name, mtu)
+ 
+             if (not ifupdownflags.flags.ALL and
+                     not ifaceobj.link_kind and
+@@ -871,7 +890,7 @@ class address(AddonWithIpBlackList, moduleBase):
+                 # This is additional cost to us, so do it only when
+                 # ifupdown2 is called on a particular interface and
+                 # it is a physical interface
+-                self._propagate_mtu_to_upper_devs(ifaceobj, mtu_str, mtu_int, 
ifaceobj_getfunc)
++                self._propagate_mtu_to_upper_devs(ifaceobj, mtu, 
ifaceobj_getfunc)
+         return
+ 
+     def _process_mtu_config_mtu_none(self, ifaceobj):
+@@ -889,7 +908,7 @@ class address(AddonWithIpBlackList, moduleBase):
+                     or ifaceobj.link_kind & ifaceLinkKind.BRIDGE \
+                     or ifaceobj.link_kind & ifaceLinkKind.OTHER:
+                 if cached_link_mtu != self.default_mtu_int:
+-                    self.sysfs.link_set_mtu(ifaceobj.name, 
mtu_str=self.default_mtu, mtu_int=self.default_mtu_int)
++                    self.netlink.link_set_mtu(ifaceobj.name, 
self.default_mtu_int)
+                 return
+ 
+             # set vlan interface mtu to lower device mtu
+@@ -902,7 +921,7 @@ class address(AddonWithIpBlackList, moduleBase):
+                 lower_iface_mtu_int = self.cache.get_link_mtu(lower_iface)
+ 
+                 if lower_iface_mtu_int != cached_link_mtu:
+-                    self.sysfs.link_set_mtu(ifaceobj.name, 
mtu_str=str(lower_iface_mtu_int), mtu_int=lower_iface_mtu_int)
++                    self.netlink.link_set_mtu(ifaceobj.name, 
lower_iface_mtu_int)
+ 
+         elif (
+             ifaceobj.name != 'lo'
+@@ -919,7 +938,7 @@ class address(AddonWithIpBlackList, moduleBase):
+             # config by the kernel in play, we try to be cautious here
+             # on which devices we want to reset mtu to default.
+             # essentially only physical interfaces which are not bond slaves
+-            self.sysfs.link_set_mtu(ifaceobj.name, mtu_str=self.default_mtu, 
mtu_int=self.default_mtu_int)
++            self.netlink.link_set_mtu(ifaceobj.name, self.default_mtu_int)
+ 
+     def _set_bridge_forwarding(self, ifaceobj):
+         """ set ip forwarding to 0 if bridge interface does not have a
+@@ -1105,7 +1124,7 @@ class address(AddonWithIpBlackList, moduleBase):
+                     self.logger.warning("%s: invalid MTU value: %s" % 
(ifaceobj.name, str(e)))
+                 return
+ 
+-            self._process_mtu_config_mtu_valid(ifaceobj, ifaceobj_getfunc, 
mtu_str, mtu_int)
++            self._process_mtu_config_mtu_valid(ifaceobj, ifaceobj_getfunc, 
mtu_int)
+         else:
+             self._process_mtu_config_mtu_none(ifaceobj)
+ 
+@@ -1373,7 +1392,7 @@ class address(AddonWithIpBlackList, moduleBase):
+             # ifupdown2. If this MTU is different from our default mtu,
+             # if so we need to reset it back to default.
+             if not ifaceobj.link_kind and self.default_mtu and 
ifaceobj.get_attr_value_first('mtu') and self.cache.get_link_mtu(ifaceobj.name) 
!= self.default_mtu_int:
+-                self.sysfs.link_set_mtu(ifaceobj.name, 
mtu_str=self.default_mtu, mtu_int=self.default_mtu_int)
++                self.netlink.link_set_mtu(ifaceobj.name, self.default_mtu_int)
+ 
+             #
+             # alias
+diff --git a/ifupdown2/addons/addressvirtual.py 
b/ifupdown2/addons/addressvirtual.py
+index 587f39f..80cd820 100644
+--- a/ifupdown2/addons/addressvirtual.py
++++ b/ifupdown2/addons/addressvirtual.py
+@@ -540,7 +540,7 @@ class addressvirtual(AddonWithIpBlackList, moduleBase):
+                     update_mtu = False
+ 
+                     try:
+-                        self.sysfs.link_set_mtu(macvlan_ifname, 
mtu_str=lower_iface_mtu_str, mtu_int=lower_iface_mtu)
++                        self.netlink.link_set_mtu(macvlan_ifname, 
lower_iface_mtu)
+                     except Exception as e:
+                         self.logger.info('%s: failed to set mtu %s: %s' % 
(macvlan_ifname, lower_iface_mtu, e))
+ 
+diff --git a/ifupdown2/addons/bridge.py b/ifupdown2/addons/bridge.py
+index 1da8de7..dee6f7b 100644
+--- a/ifupdown2/addons/bridge.py
++++ b/ifupdown2/addons/bridge.py
+@@ -2765,7 +2765,7 @@ class bridge(Bridge, moduleBase):
+ 
+             bridge_mtu = self.get_bridge_mtu(ifaceobj)
+             if bridge_mtu:
+-                self.sysfs.link_set_mtu(ifname, bridge_mtu, int(bridge_mtu))
++                self.netlink.link_set_mtu(ifname, int(bridge_mtu))
+         else:
+             link_just_created = False
+             self.logger.info('%s: bridge already exists' % ifname)
+diff --git a/ifupdown2/lib/iproute2.py b/ifupdown2/lib/iproute2.py
+index 7e7366c..3760963 100644
+--- a/ifupdown2/lib/iproute2.py
++++ b/ifupdown2/lib/iproute2.py
+@@ -518,10 +518,7 @@ class IPRoute2(Cache, Requirements):
+             self.logger.info("%s: cannot set addrgen: ipv6 is disabled on 
this device" % ifname)
+             return False
+ 
+-        if link_created:
+-            link_mtu = self.sysfs.link_get_mtu(ifname)
+-        else:
+-            link_mtu = self.cache.get_link_mtu(ifname)
++        link_mtu = self.cache.get_link_mtu(ifname)
+ 
+         if link_mtu < 1280:
+             self.logger.info("%s: ipv6 addrgen is disabled on device with MTU 
"
+diff --git a/ifupdown2/lib/nlcache.py b/ifupdown2/lib/nlcache.py
+index e5a42ef..a36e610 100644
+--- a/ifupdown2/lib/nlcache.py
++++ b/ifupdown2/lib/nlcache.py
+@@ -60,6 +60,7 @@ try:
+         NLM_F_REQUEST, \
+         NLM_F_CREATE, \
+         NLM_F_ACK, \
++        NLM_F_REPLACE, \
+         RT_SCOPES, \
+         INFINITY_LIFE_TIME
+ 
+@@ -91,6 +92,7 @@ except (ImportError, ModuleNotFoundError):
+         NLM_F_REQUEST, \
+         NLM_F_CREATE, \
+         NLM_F_ACK, \
++        NLM_F_REPLACE, \
+         RT_SCOPES, \
+         INFINITY_LIFE_TIME
+ 
+@@ -3302,6 +3304,37 @@ class 
NetlinkListenerWithCache(nllistener.NetlinkManagerWithListener, BaseObject
+     def link_translate_altnames(self, ifnames):
+         return self.cache.link_translate_altnames(ifnames)
+ 
++    ###
++
++    """
++    Sets the MTU of the given link, updating the cache on success.
++
++    :param ifname: str - Name of the interface to update
++    :param mtu: int - New MTU to set for the interface.
++    :return: True if the operation was successful
++    """
++    def link_set_mtu(self, ifname, mtu):
++        if self.cache.get_link_mtu(ifname) == mtu:
++            # no need to update
++            return
++
++        self.logger.info(f'{ifname}: netlink: ip link set dev {ifname} mtu 
{mtu}')
++
++        debug = RTM_SETLINK in self.debug
++        try:
++            link = Link(RTM_SETLINK, debug, use_color=self.use_color)
++            link.flags = NLM_F_REPLACE | NLM_F_REQUEST | NLM_F_ACK
++            link.body = struct.pack('Bxxxiii', socket.AF_UNSPEC, 0, 0, 0)
++            link.add_attribute(Link.IFLA_IFNAME, ifname)
++            link.add_attribute(Link.IFLA_MTU, mtu)
++            link.build_message(next(self.sequence), self.pid)
++            result = self.tx_nlpacket_get_response_with_error(link)
++            self.cache.override_link_mtu(ifname, mtu)
++
++            return result
++        except Exception as e:
++            raise Exception(f'{ifname}: netlink: failed to set mtu to {mtu}: 
{str(e)}')
++
+     
############################################################################
+     # ADDRESS
+     
############################################################################
+diff --git a/ifupdown2/lib/sysfs.py b/ifupdown2/lib/sysfs.py
+index dd9f361..3ac678d 100644
+--- a/ifupdown2/lib/sysfs.py
++++ b/ifupdown2/lib/sysfs.py
+@@ -106,22 +106,6 @@ class __Sysfs(IO, Requirements):
+         """
+         return self.read_file_oneline("/sys/class/net/%s/address" % ifname)
+ 
+-    #
+-    # MTU
+-    #
+-
+-    def link_get_mtu(self, ifname):
+-        return int(self.read_file_oneline("/sys/class/net/%s/mtu" % ifname) 
or 0)
+-
+-    def link_set_mtu(self, ifname, mtu_str, mtu_int):
+-        if self.cache.get_link_mtu(ifname) != mtu_int:
+-            if self.write_to_file('/sys/class/net/%s/mtu' % ifname, mtu_str):
+-                self.cache.override_link_mtu(ifname, mtu_int)
+-
+-    def link_set_mtu_dry_run(self, ifname, mtu_str, mtu_int):
+-        # we can remove the cache check in DRYRUN mode
+-        self.write_to_file('/sys/class/net/%s/mtu' % ifname, mtu_str)
+-
+     #
+     # ALIAS
+     #
+-- 
+2.50.1
+
diff --git a/debian/patches/series b/debian/patches/series
index 6955322..e8aa870 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -9,5 +9,6 @@ pve/0008-lacp-bond-remove-bond-min-links-0-warning.patch
 pve/0009-gvgeb-fix-python-interpreter-shebang.patch
 pve/0010-main-ignore-dpkg-files-when-running-hook-scripts.patch
 pve/0011-nlmanager-addons-add-transparent-support-interface-a.patch
+pve/0012-addons-nlcache-set-interface-mtu-through-netlink-ins.patch
 upstream/0001-add-ipv6-slaac-support-inet6-auto-accept_ra.patch
 upstream/0001-use-raw-strings-for-regex-to-fix-backslash-interpret.patch
-- 
2.50.1



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to