diff options
| author | opswill <will@nixops.org> | 2026-05-27 17:19:17 +0800 |
|---|---|---|
| committer | Christian Breunig <christian@breunig.cc> | 2026-06-20 22:56:18 +0200 |
| commit | 2c6c98060ec2af764d4802ec58969399854eb140 (patch) | |
| tree | 52787d58eb07571b335f6e934e8a23a50f0e0a98 /src | |
| parent | 6c0fd99099a6251a12bc2287d5ba2ab115cd8015 (diff) | |
| download | vyos-1x-2c6c98060ec2af764d4802ec58969399854eb140.tar.gz vyos-1x-2c6c98060ec2af764d4802ec58969399854eb140.zip | |
vyos-netlink: T7965: re-apply QoS configuration on dynamic interfaces
Re-apply QoS after dynamic interfaces get addresses after connect/disconnect.
When PPPoE interfaces re-connect we need to re-do QoS settings.
Diffstat (limited to 'src')
| -rwxr-xr-x | src/op_mode/connect_disconnect.py | 13 | ||||
| -rwxr-xr-x | src/services/vyos-netlinkd | 86 |
2 files changed, 72 insertions, 27 deletions
diff --git a/src/op_mode/connect_disconnect.py b/src/op_mode/connect_disconnect.py index d5db85d25..17cf9d7e0 100755 --- a/src/op_mode/connect_disconnect.py +++ b/src/op_mode/connect_disconnect.py @@ -18,9 +18,7 @@ import os import argparse from psutil import process_iter -from time import sleep -from vyos.configquery import ConfigTreeQuery from vyos.utils.process import call from vyos.utils.commit import commit_in_progress from vyos.utils.network import is_wwan_connected @@ -61,17 +59,6 @@ def connect(interface): else: print(f'Unknown interface {interface}, cannot connect. Aborting!') - # Reaply QoS configuration - config = ConfigTreeQuery() - if config.exists(f'qos interface {interface}'): - count = 1 - while commit_in_progress(): - if ( count % 60 == 0 ): - print(f'Commit still in progress after {count}s - waiting') - count += 1 - sleep(1) - call('/usr/libexec/vyos/conf_mode/qos.py') - def disconnect(interface): """ Disconnect dialer interface """ diff --git a/src/services/vyos-netlinkd b/src/services/vyos-netlinkd index 0e7da7a59..ce54f7f8f 100755 --- a/src/services/vyos-netlinkd +++ b/src/services/vyos-netlinkd @@ -23,6 +23,8 @@ import select from pyroute2 import IPRoute # pylint: disable = no-name-in-module from pyroute2 import NetlinkError # pylint: disable = no-name-in-module from pyroute2.netlink.rtnl import RTMGRP_LINK +from pyroute2.netlink.rtnl import RTMGRP_IPV4_IFADDR +from pyroute2.netlink.rtnl import RTMGRP_IPV6_IFADDR from time import sleep from typing import Optional @@ -32,13 +34,15 @@ from vyos.utils.boot import boot_configuration_complete from vyos.utils.commit import commit_in_progress2 from vyos.utils.dict import dict_search from vyos.utils.process import cmd +from vyos.utils.process import call from vyos.utils.process import is_systemd_service_active from vyos.utils.process import stop_systemd_unit running = True # compile regex once during startup for fast match -IFACE_RE = re.compile(r"^(?:eth|br|bond|wlan)") +IFACE_RE = re.compile(r"^(?:eth|br|bond|wlan|pppoe|sstpc|wwan)") +dynamic_qos_interfaces: set[str] = set() # Per-interface previous operstate, used to suppress DHCP restarts on # UP-to-UP re-notifications (e.g. post-migration gratuitous-ARP events). @@ -50,6 +54,45 @@ def match_iface(ifname: str) -> bool: """ return IFACE_RE.match(ifname) is not None +def _is_dynamic_qos_iface(ifname: str) -> bool: + """ Helper function returning true if interface requires QoS re-apply + after first address assignment. + """ + return ifname.startswith(('pppoe', 'sstpc', 'wwan')) + +def _handle_dynamic_qos_events(event: str, ifname: str, + operstate: Optional[str] = None) -> None: + """ Re-apply QoS for dynamic interfaces after they receive an address. """ + if not _is_dynamic_qos_iface(ifname): + return None + + if event in ['RTM_NEWLINK', 'RTM_DELLINK']: + if event == 'RTM_NEWLINK' and operstate != 'DOWN': + return None + + if ifname in dynamic_qos_interfaces: + syslog.syslog(syslog.LOG_DEBUG, + f'Clearing QoS re-apply state on dynamic interface {ifname}...') + dynamic_qos_interfaces.discard(ifname) + + return None + + if event != 'RTM_NEWADDR': + return None + + if ifname in dynamic_qos_interfaces: + syslog.syslog(syslog.LOG_DEBUG, + f'QoS already re-applied on dynamic interface {ifname}, skipping...') + return None + + syslog.syslog(syslog.LOG_INFO, + f'Re-applying QoS on dynamic interface {ifname}...') + call(f'/usr/libexec/vyos/conf_mode/qos.py') + + dynamic_qos_interfaces.add(ifname) + + return None + def sigterm_handler(signo, frame): global running running = False @@ -118,13 +161,13 @@ def main(): facility=syslog.LOG_DAEMON) syslog.syslog(syslog.LOG_INFO, "VyOS Netlink listener daemon started.") - # Subscribe to link notifications only (not routes/rules/neigh/addr/...). + # Subscribe to link and address notifications only (not routes/rules/neigh/...). ipr = IPRoute() try: # newer pyroute2 versions support bind group in IPRoute() constructor - ipr.bind(groups=RTMGRP_LINK) + ipr.bind(groups=RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR) syslog.syslog(syslog.LOG_INFO, - 'IPRoute.bind() using groups=RTMGRP_LINK RTNL subscription') + 'IPRoute.bind() using link and address RTNL subscriptions') except TypeError: syslog.syslog(syslog.LOG_WARNING, 'IPRoute.bind() has no groups= support; using default RTNL subscriptions', @@ -158,32 +201,47 @@ def main(): # Receive and process any messages for message in ipr.get(): # Parse NETLINK message - match message['event']: + event = message['event'] + attrs = dict(message.get('attrs', [])) + ifname = attrs.get('IFLA_IFNAME', None) or attrs.get('IFA_LABEL', None) + + if not ifname and 'index' in message: + links = ipr.get_links(message['index']) + if links: + ifname = dict(links[0].get('attrs', [])).get('IFLA_IFNAME', None) + + # Bail out early - no interface name in the message + if not ifname: + continue + # Bail out early - not interested in interface type + if not match_iface(ifname): + continue + + match event: # Message received during interface creation or modification # e.g. link up/down. case 'RTM_NEWLINK': - attrs = dict(message.get('attrs', [])) - ifname = attrs.get('IFLA_IFNAME', None) mac = attrs.get('IFLA_ADDRESS', '<unknown>') operstate = attrs.get('IFLA_OPERSTATE', None) syslog.syslog(syslog.LOG_DEBUG, f'RTM_NEWLINK -> {ifname}, state={operstate}, mac={mac}') - # Bail out early - no interface name in the message - if not ifname: - continue - # Bail out early - not interested in interface type - if not match_iface(ifname): - continue - + _handle_dynamic_qos_events(event, ifname, operstate) _handle_dhcp_events(operstate, ifname) + # Address added to an interface. For dynamic interfaces, this + # means the connection completed and QoS can be re-applied. + case 'RTM_NEWADDR': + syslog.syslog(syslog.LOG_DEBUG, f'RTM_NEWADDR -> {ifname}') + _handle_dynamic_qos_events(event, ifname) + # Deletion of a network link which has been previously added to the kernel case 'RTM_DELLINK': attrs = dict(message.get('attrs', [])) ifname = attrs.get('IFLA_IFNAME', None) if ifname: _iface_prev_operstate.pop(ifname, None) + _handle_dynamic_qos_events(event, ifname) case _: pass |
