From d054b7c82f239ff09a905b506943c03094d2d26b Mon Sep 17 00:00:00 2001 From: Nataliia Solomko Date: Fri, 4 Sep 2026 16:32:01 +0300 Subject: vpp: T9161: Enforce NIC MTU limit and apply MTU to VLAN sub-interfaces On a VPP-bound interface the MTU limit is the NIC's, which only VPP knows (the kernel side is an LCP tap reporting a meaningless larger max). Reject at commit an MTU larger than that limit, read from VPP's 'show hardware-interfaces' (max mtu + driver frame overhead), so the kernel and dataplane cannot silently end up with different MTUs. Also apply a VLAN sub-interface's MTU to its VPP dataplane sub-interface and its linux-cp tap (created with MTU 0), not just the parent; a sub-interface without its own MTU inherits the parent's. Covers ethernet (vif/vif-s) and VPP bond/loopback (vif). --- python/vyos/ifconfig/vpp/bond.py | 9 ++++--- python/vyos/ifconfig/vpp/interface.py | 48 +++++++++++++++++++++++++---------- python/vyos/ifconfig/vpp/loopback.py | 15 ++++++++--- python/vyos/vpp/control_vpp.py | 34 +++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 19 deletions(-) (limited to 'python') diff --git a/python/vyos/ifconfig/vpp/bond.py b/python/vyos/ifconfig/vpp/bond.py index 240657383..095cfc538 100644 --- a/python/vyos/ifconfig/vpp/bond.py +++ b/python/vyos/ifconfig/vpp/bond.py @@ -160,8 +160,11 @@ class VPPBondInterface(Interface, VPPInterface): if member not in members: self.add_member(interface=member) + # Apply all settings to the lcp pair (kernel) interface first: creating + # the kernel VLANs is what makes linux-cp create the matching VPP + # sub-interfaces and their taps, which the VPP-specific settings below + # (MTU sync to those taps) then depend on. + super().update(config) + # Apply VPP-specific interface settings VPPInterface.update(self, config) - - # Apply all settings to the lcp pair (kernel) interface - super().update(config) diff --git a/python/vyos/ifconfig/vpp/interface.py b/python/vyos/ifconfig/vpp/interface.py index 17be83814..99ade51d2 100644 --- a/python/vyos/ifconfig/vpp/interface.py +++ b/python/vyos/ifconfig/vpp/interface.py @@ -55,22 +55,44 @@ class VPPInterface: if lcp_name: self.vpp.iface_rxmode(lcp_name, rx_mode) - def set_mtu_vpp(self, mtu): - # Set MTU for the VPP interface - self.vpp.set_iface_mtu(self.vpp_ifname, mtu) + def set_mtu_vpp(self, config): + """Set the MTU on this interface and its VLAN sub-interfaces. - # Set MTU for the LCP pair interface - lcp_pair = self.vpp.lcp_pair_find(vpp_name_hw=self.vpp_ifname) - if lcp_pair: - lcp_name = lcp_pair.get('vpp_name_kernel') - if lcp_name: - self.vpp.set_iface_mtu(lcp_name, mtu) + The MTU is set on both the dataplane interface and its LCP tap. The + tap is created with MTU 0, so it always needs setting. A VLAN + sub-interface uses its own MTU if set, otherwise it inherits the + parent MTU. + """ + parent_mtu = config.get('mtu') + if not parent_mtu: + return + + # Fetch all LCP pairs + lcp_tap_map = { + pair.get('vpp_name_hw'): pair.get('vpp_name_kernel') + for pair in self.vpp.lcp_pairs_list() + } + + # Parent: always set the dataplane interface, plus its tap if present. + self.vpp.set_iface_mtu(self.vpp_ifname, int(parent_mtu)) + parent_tap = lcp_tap_map.get(self.vpp_ifname) + if parent_tap: + self.vpp.set_iface_mtu(parent_tap, int(parent_mtu)) + + # VLAN sub-interfaces: set the sub-interface and its tap once linux-cp + # has created the tap. + for vlan_id, vlan_config in config.get('vif', {}).items(): + vlan_ifname = f'{self.vpp_ifname}.{vlan_id}' + lcp_name = lcp_tap_map.get(vlan_ifname) + if not lcp_name: + continue + mtu = int(vlan_config.get('mtu') or parent_mtu) + self.vpp.set_iface_mtu(vlan_ifname, mtu) + self.vpp.set_iface_mtu(lcp_name, mtu) def update(self, config): - # Set MTU - if 'mtu' in config: - mtu = int(config['mtu']) - self.set_mtu_vpp(mtu) + # Set MTU on the interface, its VLAN sub-interfaces, and their LCP taps + self.set_mtu_vpp(config) # Set rx-mode rx_mode = config.get('vpp_settings', {}).get('interface_rx_mode') diff --git a/python/vyos/ifconfig/vpp/loopback.py b/python/vyos/ifconfig/vpp/loopback.py index 9f87781c0..ee34a5348 100644 --- a/python/vyos/ifconfig/vpp/loopback.py +++ b/python/vyos/ifconfig/vpp/loopback.py @@ -106,7 +106,16 @@ class VPPLoopbackInterface(Interface, VPPInterface): # Add loopback interface self.add() - VPPInterface.update(self, config) - - # Apply all settings to the lcp pair (kernel) interface + # Apply all settings to the lcp pair (kernel) interface first: creating + # the kernel VLANs is what makes linux-cp create the matching VPP + # sub-interfaces and their taps, which the VPP-specific settings below + # (MTU sync to those taps) then depend on. super().update(config) + + # Without a configured MTU the loopback keeps VPP's default; take it + # from the kernel so it still reaches the LCP taps and VLAN + # sub-interfaces. An explicit MTU is applied by VPPInterface. + if 'mtu' not in config: + config['mtu'] = self.get_mtu() + + VPPInterface.update(self, config) diff --git a/python/vyos/vpp/control_vpp.py b/python/vyos/vpp/control_vpp.py index 5cad2a646..49c55b56e 100644 --- a/python/vyos/vpp/control_vpp.py +++ b/python/vyos/vpp/control_vpp.py @@ -534,6 +534,40 @@ class VPPControl: } return self.__vpp_api_client.api.sw_interface_set_mtu(**api_call_args) + @_Decorators.api_call + def get_iface_max_mtu(self, iface_name_vpp: str) -> int | None: + """Return the maximum L3 MTU an interface's NIC can accept. + + Derived from the driver limits in 'show hardware-interfaces': 'max mtu' + (rte_eth_dev_info.max_mtu) plus 'driver frame overhead'. The NIC is owned + by the VPP dataplane, so only VPP knows its real limit - the kernel side + is an LCP tap that reports its own, much larger max MTU and does not + reflect the physical NIC. + + Args: + iface_name_vpp (str): Name of an interface in VPP + + Returns: + int | None: maximum L3 MTU or None + """ + hw_info = self.cli_cmd(f'show hardware-interfaces {iface_name_vpp}').reply or '' + max_mtu = re.search(r'max mtu:\s*(\d+)', hw_info) + if not max_mtu: + return None + + max_mtu = int(max_mtu.group(1)) + overhead = re.search(r'driver frame overhead:\s*(\d+)', hw_info) + if not overhead: + # Without the driver frame overhead the L3 limit is unknown. + return None + + # VPP programs the NIC with (L3 MTU + hi->frame_overhead - + # driver_frame_overhead); hi->frame_overhead for a DPDK Ethernet + # interface is Ethernet header (14) + two QinQ VLAN tags (8) = 22. So the + # largest settable L3 MTU is (max_mtu + driver_frame_overhead - 22). + l2_frame_overhead = 14 + 8 + return max_mtu + int(overhead.group(1)) - l2_frame_overhead + @_Decorators.api_call def get_sw_if_dev_type(self, ifname: str) -> int | None: """Find interface device type by interface name in VPP -- cgit v1.2.3