-
Notifications
You must be signed in to change notification settings - Fork 394
vyos-netlinkd: T8047: add proof-of-concept for a netlink listener daemon #4872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: current
Are you sure you want to change the base?
Conversation
|
👍 |
Implementing a daemon that listens for netlink messages in Python was discussed
for many years. This is a proof-of-concept implementation how we can listen for
netlink messages and process them in Python.
Python 3.10 minimum is required due to the use of case statements which mimics
C-style switch/case instructions.
Add example:
set interfaces ethernet eth1 vif 21
commit
vyos-configd[4690]: Received message: {"type": "node", "last": true, "data":
"VYOS_TAGNODE_VALUE=eth1/usr/libexec/vyos/conf_mode/interfaces_ethernet.py"}
vyos-netlinkd[5955]: RTM_NEWLINK -> eth1, state=UP, mac=00:50:56:b3:38:c5
vyos-netlinkd[5955]: RTM_NEWLINK -> eth1.21, state=DOWN, mac=00:50:56:b3:38:c5
Remove example:
delete interfaces ethernet eth1 vif
commit
vyos-configd[4690]: Received message: {"type": "node", "last": true, "data":
"VYOS_TAGNODE_VALUE=eth1/usr/libexec/vyos/conf_mode/interfaces_ethernet.py"}
vyos-netlinkd[6803]: Received event RTM_DELADDR - unhandled!
vyos-netlinkd[6803]: RTM_NEWLINK -> eth1, state=UP, mac=00:50:56:b3:38:c5
vyos-netlinkd[6803]: RTM_NEWLINK -> eth1.21, state=DOWN, mac=00:50:56:b3:38:c5
Message parsing of course needs to be improved for real-world use!
|
CI integration ❌ failed! Details
|
| syslog.syslog(syslog.LOG_INFO, | ||
| f'RTM_NEWLINK -> {iface}, state={operstate}, mac={mac}') |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR introduces a proof-of-concept netlink listener daemon (vyos-netlinkd) that monitors and logs network interface changes via netlink messages in Python, leveraging Python 3.10+ match/case statements.
- Adds a new systemd service unit for the netlink daemon
- Implements basic netlink event handling for RTM_NEWLINK and RTM_DELLINK messages
- Provides a foundation for future netlink-based monitoring capabilities
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/systemd/vyos-netlinkd.service | Defines systemd service configuration for the netlink daemon with early startup ordering |
| src/services/vyos-netlinkd | Implements the main daemon logic with netlink message processing and signal handling |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get 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}') | ||
|
|
||
| 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) |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
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.
| 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) |
|
|
||
| ipr.close() | ||
| syslog.syslog(syslog.LOG_INFO, 'Netlink listener daemon stopped.') | ||
| exit(0) |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
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().
|
|
||
| [Service] | ||
| ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-netlinkd | ||
| Type=idle |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
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.
| Type=idle | |
| Type=simple |
Change summary
Implementing a daemon that listens for netlink messages in Python was discussed for many years. This is a proof-of-concept implementation how we can listen for netlink messages and process them in Python.
NOTE Python 3.10 minimum is required due to the use of case statements which mimics C-style switch/case instructions.
Add example
Remove example
Message parsing of course needs to be improved for real-world use!
Types of changes
Related Task(s)
Related PR(s)
Checklist: