Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions src/services/vyos-netlinkd
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/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 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 sys import exit

running = True

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 main():
syslog.openlog(ident="vyos-netlinkd", logoption=syslog.LOG_PID)
syslog.syslog(syslog.LOG_INFO, "Netlink listener daemon started.")

ipr = IPRoute()
ipr.bind()
fd = ipr.fileno()

global running
while running:
try:
# Wait for up to 1 second for a netlink message
rlist, _, _ = select.select([fd], [], [], 1.0)
if not rlist:
# timeout - retry
continue

# Receive and process any messages
for message in ipr.get():
event_type = message['event']
match event_type:
case 'RTM_NEWLINK':
attrs = dict(message.get('attrs', []))
iface = attrs.get('IFLA_IFNAME', '<unknown>')
operstate = attrs.get('IFLA_OPERSTATE', '<unknown>')
mac = attrs.get('IFLA_ADDRESS', '<unknown>')
syslog.syslog(syslog.LOG_INFO,
f'RTM_NEWLINK -> {iface}, state={operstate}, mac={mac}')
Comment on lines +59 to +60
Copy link
Member

@sever-sever sever-sever Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will PPPoE interfaces also log?
For example, for BRAS. There could be thousands of interfaces.


case 'RTM_DELLINK':
attrs = dict(message.get('attrs', []))
iface = attrs.get('IFLA_IFNAME', '<unknown>')
syslog.syslog(syslog.LOG_INFO, f'RTM_DELLINK -> {iface}')

case _:
syslog.syslog(syslog.LOG_INFO, f'Received event {event_type} - unhandled!')

except NetlinkError as e:
syslog.syslog(syslog.LOG_ERR, f'Netlink error: {e}')
except KeyboardInterrupt:
break
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f'Unhandled exception: {e}')

ipr.close()
syslog.syslog(syslog.LOG_INFO, 'Netlink listener daemon stopped.')
exit(0)
Comment on lines +37 to +79
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IPRoute socket is closed outside the main loop, but if an exception occurs during initialization (lines 37-39) or within the loop before setting running=False, the socket may not be properly closed. Consider using a try-finally block or context manager to ensure cleanup happens reliably.

Suggested change
ipr = IPRoute()
ipr.bind()
fd = ipr.fileno()
global running
while running:
try:
# Wait for up to 1 second for a netlink message
rlist, _, _ = select.select([fd], [], [], 1.0)
if not rlist:
# timeout - retry
continue
# Receive and process any messages
for message in ipr.get():
event_type = message['event']
match event_type:
case 'RTM_NEWLINK':
attrs = dict(message.get('attrs', []))
iface = attrs.get('IFLA_IFNAME', '<unknown>')
operstate = attrs.get('IFLA_OPERSTATE', '<unknown>')
mac = attrs.get('IFLA_ADDRESS', '<unknown>')
syslog.syslog(syslog.LOG_INFO,
f'RTM_NEWLINK -> {iface}, state={operstate}, mac={mac}')
case 'RTM_DELLINK':
attrs = dict(message.get('attrs', []))
iface = attrs.get('IFLA_IFNAME', '<unknown>')
syslog.syslog(syslog.LOG_INFO, f'RTM_DELLINK -> {iface}')
case _:
syslog.syslog(syslog.LOG_INFO, f'Received event {event_type} - unhandled!')
except NetlinkError as e:
syslog.syslog(syslog.LOG_ERR, f'Netlink error: {e}')
except KeyboardInterrupt:
break
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f'Unhandled exception: {e}')
ipr.close()
syslog.syslog(syslog.LOG_INFO, 'Netlink listener daemon stopped.')
exit(0)
ipr = None
try:
ipr = IPRoute()
ipr.bind()
fd = ipr.fileno()
global running
while running:
try:
# Wait for up to 1 second for a netlink message
rlist, _, _ = select.select([fd], [], [], 1.0)
if not rlist:
# timeout - retry
continue
# Receive and process any messages
for message in ipr.get():
event_type = message['event']
match event_type:
case 'RTM_NEWLINK':
attrs = dict(message.get('attrs', []))
iface = attrs.get('IFLA_IFNAME', '<unknown>')
operstate = attrs.get('IFLA_OPERSTATE', '<unknown>')
mac = attrs.get('IFLA_ADDRESS', '<unknown>')
syslog.syslog(syslog.LOG_INFO,
f'RTM_NEWLINK -> {iface}, state={operstate}, mac={mac}')
case 'RTM_DELLINK':
attrs = dict(message.get('attrs', []))
iface = attrs.get('IFLA_IFNAME', '<unknown>')
syslog.syslog(syslog.LOG_INFO, f'RTM_DELLINK -> {iface}')
case _:
syslog.syslog(syslog.LOG_INFO, f'Received event {event_type} - unhandled!')
except NetlinkError as e:
syslog.syslog(syslog.LOG_ERR, f'Netlink error: {e}')
except KeyboardInterrupt:
break
except Exception as e:
syslog.syslog(syslog.LOG_ERR, f'Unhandled exception: {e}')
finally:
if ipr is not None:
ipr.close()
syslog.syslog(syslog.LOG_INFO, 'Netlink listener daemon stopped.')
exit(0)

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exit(0) from the sys module is unnecessary here. The function can simply return, or use sys.exit(0) explicitly if needed. Since exit was imported from sys, this should be sys.exit(0) for clarity, or preferably just return from main().

Copilot uses AI. Check for mistakes.

if __name__ == "__main__":
signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGINT, sigterm_handler)
main()
26 changes: 26 additions & 0 deletions src/systemd/vyos-netlinkd.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[Unit]
Description=VyOS netlink daemon

# Without this option, lots of default dependencies are added,
# among them network.target, which creates a dependency cycle
DefaultDependencies=no

# Seemingly sensible way to say "as early as the system is ready"
# All vyos-configd needs is read/write mounted root
After=systemd-remount-fs.service
Before=vyos-router.service

[Service]
ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-netlinkd
Type=idle
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type=idle is typically used for services that should run after all other jobs are dispatched. For a long-running daemon that listens for netlink events, Type=simple (the default) or Type=notify would be more appropriate. Type=idle may cause unnecessary delays in service startup.

Suggested change
Type=idle
Type=simple

Copilot uses AI. Check for mistakes.

SyslogIdentifier=vyos-netlinkd
SyslogFacility=daemon

Restart=on-failure

User=root
Group=vyattacfg

[Install]
WantedBy=vyos.target
Loading