summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authoropswill <will@nixops.org>2026-05-27 17:19:17 +0800
committerChristian Breunig <christian@breunig.cc>2026-06-20 22:56:18 +0200
commit2c6c98060ec2af764d4802ec58969399854eb140 (patch)
tree52787d58eb07571b335f6e934e8a23a50f0e0a98 /src
parent6c0fd99099a6251a12bc2287d5ba2ab115cd8015 (diff)
downloadvyos-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-xsrc/op_mode/connect_disconnect.py13
-rwxr-xr-xsrc/services/vyos-netlinkd86
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