summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xsrc/services/vyos-netlinkd95
1 files changed, 84 insertions, 11 deletions
diff --git a/src/services/vyos-netlinkd b/src/services/vyos-netlinkd
index fda946050..9627f9b28 100755
--- a/src/services/vyos-netlinkd
+++ b/src/services/vyos-netlinkd
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import importlib.util
+import os
import re
import sys
import syslog
@@ -33,6 +34,7 @@ from vyos.configquery import op_mode_config_dict
from vyos.ifconfig import Section
from vyos.utils.boot import boot_configuration_complete
from vyos.utils.commit import commit_in_progress2
+from vyos.utils.file import read_file
from vyos.utils.dict import dict_search
from vyos.utils.process import cmdl
from vyos.utils.process import is_systemd_service_active
@@ -54,15 +56,31 @@ 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).
+# UP-to-UP re-notifications (e.g. post-migration gratuitous-ARP events), and
+# to detect edges missed while netlink events were drained during a commit.
_iface_prev_operstate: dict[str, str] = {}
+# True while the main loop is draining netlink events because a config
+# commit holds the lock. When it clears we reconcile DHCP clients against
+# live operstate so a transition that only happened inside the drain window
+# is not lost (T9086).
+_in_commit_skip = False
+
def match_iface(ifname: str) -> bool:
""" Helper function returning true if interface name is a match for further
processing (e.g. restart of DHCP(v6) client)
"""
return IFACE_RE.match(ifname) is not None
+def _read_operstate(ifname: str) -> Optional[str]:
+ """Read live kernel operstate from sysfs (uppercase), or None if unavailable.
+
+ Values match IFLA_OPERSTATE names used by RTM_NEWLINK (UP, DOWN, ...).
+ See Documentation/ABI/testing/sysfs-class-net.
+ """
+ fname = f'/sys/class/net/{ifname}/operstate'
+ return read_file(fname, defaultonfailure='').strip().upper() or None
+
def _is_dynamic_qos_iface(ifname: str) -> bool:
""" Helper function returning true if interface requires QoS re-apply
after first address assignment.
@@ -162,7 +180,45 @@ def _handle_dhcp_events(operstate: Optional[str], ifname: str) -> None:
return None
+def _reconcile_dhcp_operstate() -> None:
+ """After a commit drain window, sync DHCP clients to live operstate (T9086).
+
+ Events discarded while commit_in_progress2() was true may have included a
+ real UP/DOWN edge. Compare each interface's live sysfs operstate against
+ the last one we acted on and run the normal DHCP handler only where they
+ disagree.
+
+ Interfaces never seen before are seeded into the tracker without acting -
+ conf_mode owns DHCP bring-up across commits for those.
+ """
+ try:
+ ifnames = os.listdir('/sys/class/net')
+ except OSError as e:
+ syslog.syslog(syslog.LOG_ERR, f'Failed to list interfaces for reconcile: {e}')
+ return
+
+ for ifname in ifnames:
+ if not match_iface(ifname):
+ continue
+
+ operstate = _read_operstate(ifname)
+ if operstate not in ['UP', 'DOWN']:
+ continue
+
+ prev = _iface_prev_operstate.get(ifname)
+ if prev is None:
+ _iface_prev_operstate[ifname] = operstate
+ continue
+ if prev == operstate:
+ continue
+
+ syslog.syslog(syslog.LOG_DEBUG,
+ f'Reconcile {ifname}: prev={prev} current={operstate}')
+ _handle_dhcp_events(operstate, ifname)
+
def main():
+ global _in_commit_skip
+
syslog.openlog(ident="vyos-netlinkd",
logoption=syslog.LOG_PID,
facility=syslog.LOG_DAEMON)
@@ -200,19 +256,36 @@ def main():
continue
try:
- # Wait for up to 1 second for a netlink message
+ # Wait for up to 1 second for a netlink message. The timeout also
+ # lets us notice a commit ending even with no further netlink
+ # traffic, so we can reconcile (T9086).
rlist, _, _ = select.select([fd], [], [], 1.0)
- if not rlist:
- # timeout - retry
- continue
- # Check if a config commit is in progress before processing any
- # messages. This avoids blocking per-message and reduces unnecessary
- # calls to commit_in_progress2()
if commit_in_progress2():
- syslog.syslog(syslog.LOG_DEBUG,
- 'Config commit in progress, skipping netlink events')
- sleep(1)
+ # Drain the socket instead of leaving messages queued, so a
+ # stale event (e.g. a DOWN from a disable that was already
+ # re-enabled) can't be processed as fresh once the commit
+ # ends (T9086).
+ if not _in_commit_skip:
+ syslog.syslog(syslog.LOG_DEBUG,
+ 'Config commit in progress, draining netlink events without acting')
+ _in_commit_skip = True
+ if rlist:
+ try:
+ ipr.get()
+ except NetlinkError as e:
+ syslog.syslog(syslog.LOG_ERR,
+ f'Netlink error while draining during commit: {e}')
+ continue
+
+ if _in_commit_skip:
+ _in_commit_skip = False
+ syslog.syslog(syslog.LOG_INFO,
+ 'Config commit finished, reconciling DHCP client state with operstate')
+ _reconcile_dhcp_operstate()
+
+ if not rlist:
+ # timeout - retry
continue
# Receive and process any messages