Page MenuHomeVyOS Platform

vyos-netlinkd
No OneTemporary

Size
6 KB
Referenced Files
None
Subscribers
None

vyos-netlinkd

#!/usr/bin/env python3
#
# Copyright VyOS maintainers and contributors <maintainers@vyos.io>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import sys
import syslog
import signal
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 time import sleep
from typing import Optional
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_progress
from vyos.utils.dict import dict_search
from vyos.utils.process import cmd
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)")
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 sigterm_handler(signo, frame):
global running
running = False
sig = signal.Signals(signo)
syslog.syslog(syslog.LOG_INFO, f'Received signal {sig.name} - shutting down...')
def _handle_dhcp_events(operstate: Optional[str], ifname: str) -> None:
systemdV4_service = f'dhclient@{ifname}.service'
systemdV6_service = f'dhcp6c@{ifname}.service'
# Only handle explicit UP/DOWN state transitions; ignore other kernel states.
if operstate not in ['UP', 'DOWN']:
return None
if operstate == 'DOWN':
# Interface moved state to down
if is_systemd_service_active(systemdV4_service):
syslog.syslog(syslog.LOG_DEBUG, f'Stopping {systemdV4_service}...')
stop_systemd_unit(systemdV4_service, raise_on_failure=False)
if is_systemd_service_active(systemdV6_service):
syslog.syslog(syslog.LOG_DEBUG, f'Stopping {systemdV6_service}...')
stop_systemd_unit(systemdV6_service, raise_on_failure=False)
elif operstate == 'UP':
v6_restart = False
interface_path = Section.get_config_path(ifname, delimiter='.')
config_dict = op_mode_config_dict(
['interfaces'], key_mangling=('-', '_'), get_first_key=True
)
if tmp := dict_search(f'{interface_path}.address', config_dict):
# Always (re-)start the DHCP(v6) client service. If the DHCP(v6) client
# is already running - which could happen if the interface is re-
# configured in operational down state, it will have an exponential backoff
# time increasing while not receiving a DHCP(v6) reply.
#
# To make the interface instantly available, and as for a DHCP(v6) lease
# we will re-start the service and thus cancel the backoff time.
if 'dhcp' in tmp:
syslog.syslog(syslog.LOG_DEBUG, f'Restarting {systemdV4_service}...')
cmd(f'systemctl restart {systemdV4_service}')
if 'dhcpv6' in tmp:
v6_restart = True
if dict_search(f'{interface_path}.dhcpv6_options.pd', config_dict):
v6_restart = True
if v6_restart:
syslog.syslog(syslog.LOG_DEBUG, f'Restarting {systemdV6_service}...')
cmd(f'systemctl restart {systemdV6_service}')
return None
def main():
syslog.openlog(ident="vyos-netlinkd",
logoption=syslog.LOG_PID,
facility=syslog.LOG_DAEMON)
syslog.syslog(syslog.LOG_INFO, "VyOS Netlink listener daemon started.")
# Subscribe to link notifications only (not routes/rules/neigh/addr/...).
ipr = IPRoute(groups=RTMGRP_LINK)
ipr.bind()
fd = ipr.fileno()
global running
while running:
if not boot_configuration_complete():
syslog.syslog(syslog.LOG_INFO, 'System bootup not yet finished...')
sleep(5)
continue
try:
# Wait for up to 1 second for a netlink message
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
# commit_in_progress() calls.
if commit_in_progress():
syslog.syslog(syslog.LOG_DEBUG,
'Config commit in progress, skipping netlink events')
sleep(1)
continue
# Receive and process any messages
for message in ipr.get():
# Parse NETLINK message
match message['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_dhcp_events(operstate, ifname)
# Deletion of a network link which has been previously added to the kernel
case 'RTM_DELLINK':
pass
case _:
pass
except NetlinkError as e:
syslog.syslog(syslog.LOG_ERR, f'Netlink error: {e}')
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f'Unhandled exception: {e}')
except KeyboardInterrupt:
break
ipr.close()
syslog.syslog(syslog.LOG_INFO, 'Netlink listener daemon stopped.')
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigterm_handler)
main()

File Metadata

Mime Type
text/x-script.python
Expires
Sat, Sep 26, 1:05 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4285214
Default Alt Text
vyos-netlinkd (6 KB)

Event Timeline