diff --git a/interface-definitions/include/netlink/log-level.xml.i b/interface-definitions/include/netlink/log-level.xml.i
new file mode 100644
index 0000000000..bbaf9412c2
--- /dev/null
+++ b/interface-definitions/include/netlink/log-level.xml.i
@@ -0,0 +1,21 @@
+
+
+
+ Set log-level
+
+ info debug
+
+
+ info
+ Info log level
+
+
+ debug
+ Debug log level
+
+
+ (info|debug)
+
+
+
+
diff --git a/interface-definitions/include/netlink/queue-size.xml.i b/interface-definitions/include/netlink/queue-size.xml.i
new file mode 100644
index 0000000000..d284838cfa
--- /dev/null
+++ b/interface-definitions/include/netlink/queue-size.xml.i
@@ -0,0 +1,15 @@
+
+
+
+ Internal message queue size
+
+ u32:100-2147483647
+ Queue size
+
+
+
+
+ Queue size must be between 100 and 2147483647
+
+
+
diff --git a/interface-definitions/service_monitoring_network_event.xml.in b/interface-definitions/service_monitoring_network_event.xml.in
new file mode 100644
index 0000000000..edf23a06af
--- /dev/null
+++ b/interface-definitions/service_monitoring_network_event.xml.in
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+ Monitoring services
+
+
+
+
+ Network event logger
+ 1280
+
+
+
+
+ Network event type
+
+
+
+
+ Log routing table update events
+
+
+
+
+
+ Log link status change events
+
+
+
+
+
+ Log address assignment and removal events
+
+
+
+
+
+ Log neighbor (ARP/ND) table update events
+
+
+
+
+
+ Log policy routing rule change events
+
+
+
+
+
+ #include
+ #include
+
+
+
+
+
+
+
diff --git a/interface-definitions/system_conntrack.xml.in b/interface-definitions/system_conntrack.xml.in
index cd59d13081..54610b6259 100644
--- a/interface-definitions/system_conntrack.xml.in
+++ b/interface-definitions/system_conntrack.xml.in
@@ -263,38 +263,8 @@
-
-
- Internal message queue size
-
- u32:100-999999
- Queue size
-
-
-
-
- Queue size must be between 100 and 999999
-
-
-
-
- Set log-level. Log must be enable.
-
- info debug
-
-
- info
- Info log level
-
-
- debug
- Debug log level
-
-
- (info|debug)
-
-
-
+ #include
+ #include
diff --git a/op-mode-definitions/include/log/network-event-type-interface.xml.i b/op-mode-definitions/include/log/network-event-type-interface.xml.i
new file mode 100644
index 0000000000..2d781223cd
--- /dev/null
+++ b/op-mode-definitions/include/log/network-event-type-interface.xml.i
@@ -0,0 +1,11 @@
+
+
+
+ Show log for specific interface
+
+
+
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service | grep "$(echo "\[$4\]" | tr '[:lower:]' '[:upper:]')" | grep "\b$6\b"
+
+
diff --git a/op-mode-definitions/include/show-interface-type-event-log.xml.i b/op-mode-definitions/include/show-interface-type-event-log.xml.i
new file mode 100644
index 0000000000..c69073fdaa
--- /dev/null
+++ b/op-mode-definitions/include/show-interface-type-event-log.xml.i
@@ -0,0 +1,40 @@
+
+
+
+ Show network interface change event log
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b"
+
+
+
+ Show log for route events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"
+
+
+
+ Show log for network link events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"
+
+
+
+ Show log for network address events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"
+
+
+
+ Show log for neighbor table events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"
+
+
+
+ Show log for PBR rule change events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\b$4\b" | grep -i "\[$6\]"
+
+
+
+
diff --git a/op-mode-definitions/show-interfaces-bonding.xml.in b/op-mode-definitions/show-interfaces-bonding.xml.in
index e2950331bf..0abb7cd5a2 100644
--- a/op-mode-definitions/show-interfaces-bonding.xml.in
+++ b/op-mode-definitions/show-interfaces-bonding.xml.in
@@ -67,6 +67,7 @@
+ #include
diff --git a/op-mode-definitions/show-interfaces-bridge.xml.in b/op-mode-definitions/show-interfaces-bridge.xml.in
index dc813682de..998dacd385 100644
--- a/op-mode-definitions/show-interfaces-bridge.xml.in
+++ b/op-mode-definitions/show-interfaces-bridge.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=bridge
+ #include
diff --git a/op-mode-definitions/show-interfaces-dummy.xml.in b/op-mode-definitions/show-interfaces-dummy.xml.in
index b8ec7da917..18f21e97e0 100644
--- a/op-mode-definitions/show-interfaces-dummy.xml.in
+++ b/op-mode-definitions/show-interfaces-dummy.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=dummy
+ #include
diff --git a/op-mode-definitions/show-interfaces-ethernet.xml.in b/op-mode-definitions/show-interfaces-ethernet.xml.in
index 09f0b3933e..8a23455bf1 100644
--- a/op-mode-definitions/show-interfaces-ethernet.xml.in
+++ b/op-mode-definitions/show-interfaces-ethernet.xml.in
@@ -68,6 +68,7 @@
+ #include
diff --git a/op-mode-definitions/show-interfaces-geneve.xml.in b/op-mode-definitions/show-interfaces-geneve.xml.in
index d3d1880319..b5fe84ca70 100644
--- a/op-mode-definitions/show-interfaces-geneve.xml.in
+++ b/op-mode-definitions/show-interfaces-geneve.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=geneve
+ #include
diff --git a/op-mode-definitions/show-interfaces-input.xml.in b/op-mode-definitions/show-interfaces-input.xml.in
index e5d420056a..c9856f77f0 100644
--- a/op-mode-definitions/show-interfaces-input.xml.in
+++ b/op-mode-definitions/show-interfaces-input.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=input
+ #include
diff --git a/op-mode-definitions/show-interfaces-l2tpv3.xml.in b/op-mode-definitions/show-interfaces-l2tpv3.xml.in
index 2d165171c5..88b73d7d7a 100644
--- a/op-mode-definitions/show-interfaces-l2tpv3.xml.in
+++ b/op-mode-definitions/show-interfaces-l2tpv3.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=l2tpv3
+ #include
diff --git a/op-mode-definitions/show-interfaces-loopback.xml.in b/op-mode-definitions/show-interfaces-loopback.xml.in
index d341a63592..467e1a13db 100644
--- a/op-mode-definitions/show-interfaces-loopback.xml.in
+++ b/op-mode-definitions/show-interfaces-loopback.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=loopback
+ #include
diff --git a/op-mode-definitions/show-interfaces-macsec.xml.in b/op-mode-definitions/show-interfaces-macsec.xml.in
index 28264d252c..640031b772 100644
--- a/op-mode-definitions/show-interfaces-macsec.xml.in
+++ b/op-mode-definitions/show-interfaces-macsec.xml.in
@@ -29,6 +29,9 @@
ip macsec show $4
+
+ #include
+
diff --git a/op-mode-definitions/show-interfaces-pppoe.xml.in b/op-mode-definitions/show-interfaces-pppoe.xml.in
index 1c6e0b83e1..c1f502cb3d 100644
--- a/op-mode-definitions/show-interfaces-pppoe.xml.in
+++ b/op-mode-definitions/show-interfaces-pppoe.xml.in
@@ -28,6 +28,7 @@
if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi
+ #include
diff --git a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in
index 4ab2a5fbb4..a9e4257cec 100644
--- a/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in
+++ b/op-mode-definitions/show-interfaces-pseudo-ethernet.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=pseudo-ethernet
+ #include
diff --git a/op-mode-definitions/show-interfaces-sstpc.xml.in b/op-mode-definitions/show-interfaces-sstpc.xml.in
index 307276f72a..3bd7a82478 100644
--- a/op-mode-definitions/show-interfaces-sstpc.xml.in
+++ b/op-mode-definitions/show-interfaces-sstpc.xml.in
@@ -28,6 +28,7 @@
if [ -d "/sys/class/net/$4" ]; then /usr/sbin/pppstats "$4"; fi
+ #include
diff --git a/op-mode-definitions/show-interfaces-tunnel.xml.in b/op-mode-definitions/show-interfaces-tunnel.xml.in
index b99b0cbb22..579b173cb5 100644
--- a/op-mode-definitions/show-interfaces-tunnel.xml.in
+++ b/op-mode-definitions/show-interfaces-tunnel.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=tunnel
+ #include
diff --git a/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in
index 18ae806b71..4112a17af3 100644
--- a/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in
+++ b/op-mode-definitions/show-interfaces-virtual-ethernet.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=virtual-ethernet
+ #include
diff --git a/op-mode-definitions/show-interfaces-vti.xml.in b/op-mode-definitions/show-interfaces-vti.xml.in
index ae5cfeb9c9..d13b3e7cc5 100644
--- a/op-mode-definitions/show-interfaces-vti.xml.in
+++ b/op-mode-definitions/show-interfaces-vti.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=vti
+ #include
diff --git a/op-mode-definitions/show-interfaces-vxlan.xml.in b/op-mode-definitions/show-interfaces-vxlan.xml.in
index fd729b9861..89c8d075b6 100644
--- a/op-mode-definitions/show-interfaces-vxlan.xml.in
+++ b/op-mode-definitions/show-interfaces-vxlan.xml.in
@@ -19,6 +19,7 @@
${vyos_op_scripts_dir}/interfaces.py show_summary --intf-name="$4" --intf-type=vxlan
+ #include
diff --git a/op-mode-definitions/show-interfaces-wireguard.xml.in b/op-mode-definitions/show-interfaces-wireguard.xml.in
index 0e61ccd747..d86152a217 100644
--- a/op-mode-definitions/show-interfaces-wireguard.xml.in
+++ b/op-mode-definitions/show-interfaces-wireguard.xml.in
@@ -43,6 +43,7 @@
sudo ${vyos_op_scripts_dir}/interfaces_wireguard.py show_summary --intf-name="$4"
+ #include
diff --git a/op-mode-definitions/show-interfaces-wireless.xml.in b/op-mode-definitions/show-interfaces-wireless.xml.in
index 09c9a78950..b0a1502de9 100644
--- a/op-mode-definitions/show-interfaces-wireless.xml.in
+++ b/op-mode-definitions/show-interfaces-wireless.xml.in
@@ -73,6 +73,7 @@
+ #include
diff --git a/op-mode-definitions/show-interfaces-wwan.xml.in b/op-mode-definitions/show-interfaces-wwan.xml.in
index 3682282a37..2301b32d0c 100644
--- a/op-mode-definitions/show-interfaces-wwan.xml.in
+++ b/op-mode-definitions/show-interfaces-wwan.xml.in
@@ -80,6 +80,7 @@
echo not implemented
+ #include
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 7ace50cc9d..5ee7c973fc 100755
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -958,6 +958,68 @@
journalctl --no-hostname --boot --unit squid.service
+
+
+ Show log for network events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service
+
+
+
+ Show log for specific interface
+
+
+
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep $5
+
+
+
+ Show log for route events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"
+
+ #include
+
+
+
+
+ Show log for network link events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"
+
+ #include
+
+
+
+
+ Show log for network address events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"
+
+ #include
+
+
+
+
+ Show log for neighbor table events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"
+
+ #include
+
+
+
+
+ Show log for PBR rule change events
+
+ journalctl --no-hostname --boot --unit vyos-network-event-logger.service --grep "\[$4\]"
+
+ #include
+
+
+
+
diff --git a/python/vyos/include/__init__.py b/python/vyos/include/__init__.py
new file mode 100644
index 0000000000..22e8365316
--- /dev/null
+++ b/python/vyos/include/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 VyOS maintainers and contributors
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see .
+
diff --git a/python/vyos/include/uapi/__init__.py b/python/vyos/include/uapi/__init__.py
new file mode 100644
index 0000000000..22e8365316
--- /dev/null
+++ b/python/vyos/include/uapi/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 VyOS maintainers and contributors
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see .
+
diff --git a/python/vyos/include/uapi/linux/__init__.py b/python/vyos/include/uapi/linux/__init__.py
new file mode 100644
index 0000000000..22e8365316
--- /dev/null
+++ b/python/vyos/include/uapi/linux/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2025 VyOS maintainers and contributors
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see .
+
diff --git a/python/vyos/include/uapi/linux/fib_rules.py b/python/vyos/include/uapi/linux/fib_rules.py
new file mode 100644
index 0000000000..72f0b18cb4
--- /dev/null
+++ b/python/vyos/include/uapi/linux/fib_rules.py
@@ -0,0 +1,20 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+FIB_RULE_PERMANENT = 0x00000001
+FIB_RULE_INVERT = 0x00000002
+FIB_RULE_UNRESOLVED = 0x00000004
+FIB_RULE_IIF_DETACHED = 0x00000008
+FIB_RULE_DEV_DETACHED = FIB_RULE_IIF_DETACHED
+FIB_RULE_OIF_DETACHED = 0x00000010
diff --git a/python/vyos/include/uapi/linux/icmpv6.py b/python/vyos/include/uapi/linux/icmpv6.py
new file mode 100644
index 0000000000..47e0c723c5
--- /dev/null
+++ b/python/vyos/include/uapi/linux/icmpv6.py
@@ -0,0 +1,18 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+ICMPV6_ROUTER_PREF_LOW = 3
+ICMPV6_ROUTER_PREF_MEDIUM = 0
+ICMPV6_ROUTER_PREF_HIGH = 1
+ICMPV6_ROUTER_PREF_INVALID = 2
diff --git a/python/vyos/include/uapi/linux/if_arp.py b/python/vyos/include/uapi/linux/if_arp.py
new file mode 100644
index 0000000000..90cb66ebd3
--- /dev/null
+++ b/python/vyos/include/uapi/linux/if_arp.py
@@ -0,0 +1,176 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+# ARP protocol HARDWARE identifiers
+ARPHRD_NETROM = 0 # from KA9Q: NET/ROM pseudo
+ARPHRD_ETHER = 1 # Ethernet 10Mbps
+ARPHRD_EETHER = 2 # Experimental Ethernet
+ARPHRD_AX25 = 3 # AX.25 Level 2
+ARPHRD_PRONET = 4 # PROnet token ring
+ARPHRD_CHAOS = 5 # Chaosnet
+ARPHRD_IEEE802 = 6 # IEEE 802.2 Ethernet/TR/TB
+ARPHRD_ARCNET = 7 # ARCnet
+ARPHRD_APPLETLK = 8 # APPLEtalk
+ARPHRD_DLCI = 15 # Frame Relay DLCI
+ARPHRD_ATM = 19 # ATM
+ARPHRD_METRICOM = 23 # Metricom STRIP (new IANA id)
+ARPHRD_IEEE1394 = 24 # IEEE 1394 IPv4 - RFC 2734
+ARPHRD_EUI64 = 27 # EUI-64
+ARPHRD_INFINIBAND = 32 # InfiniBand
+
+# Dummy types for non-ARP hardware
+ARPHRD_SLIP = 256
+ARPHRD_CSLIP = 257
+ARPHRD_SLIP6 = 258
+ARPHRD_CSLIP6 = 259
+ARPHRD_RSRVD = 260 # Notional KISS type
+ARPHRD_ADAPT = 264
+ARPHRD_ROSE = 270
+ARPHRD_X25 = 271 # CCITT X.25
+ARPHRD_HWX25 = 272 # Boards with X.25 in firmware
+ARPHRD_CAN = 280 # Controller Area Network
+ARPHRD_MCTP = 290
+ARPHRD_PPP = 512
+ARPHRD_CISCO = 513 # Cisco HDLC
+ARPHRD_HDLC = ARPHRD_CISCO # Alias for CISCO
+ARPHRD_LAPB = 516 # LAPB
+ARPHRD_DDCMP = 517 # Digital's DDCMP protocol
+ARPHRD_RAWHDLC = 518 # Raw HDLC
+ARPHRD_RAWIP = 519 # Raw IP
+
+ARPHRD_TUNNEL = 768 # IPIP tunnel
+ARPHRD_TUNNEL6 = 769 # IP6IP6 tunnel
+ARPHRD_FRAD = 770 # Frame Relay Access Device
+ARPHRD_SKIP = 771 # SKIP vif
+ARPHRD_LOOPBACK = 772 # Loopback device
+ARPHRD_LOCALTLK = 773 # Localtalk device
+ARPHRD_FDDI = 774 # Fiber Distributed Data Interface
+ARPHRD_BIF = 775 # AP1000 BIF
+ARPHRD_SIT = 776 # sit0 device - IPv6-in-IPv4
+ARPHRD_IPDDP = 777 # IP over DDP tunneller
+ARPHRD_IPGRE = 778 # GRE over IP
+ARPHRD_PIMREG = 779 # PIMSM register interface
+ARPHRD_HIPPI = 780 # High Performance Parallel Interface
+ARPHRD_ASH = 781 # Nexus 64Mbps Ash
+ARPHRD_ECONET = 782 # Acorn Econet
+ARPHRD_IRDA = 783 # Linux-IrDA
+ARPHRD_FCPP = 784 # Point to point fibrechannel
+ARPHRD_FCAL = 785 # Fibrechannel arbitrated loop
+ARPHRD_FCPL = 786 # Fibrechannel public loop
+ARPHRD_FCFABRIC = 787 # Fibrechannel fabric
+
+ARPHRD_IEEE802_TR = 800 # Magic type ident for TR
+ARPHRD_IEEE80211 = 801 # IEEE 802.11
+ARPHRD_IEEE80211_PRISM = 802 # IEEE 802.11 + Prism2 header
+ARPHRD_IEEE80211_RADIOTAP = 803 # IEEE 802.11 + radiotap header
+ARPHRD_IEEE802154 = 804
+ARPHRD_IEEE802154_MONITOR = 805 # IEEE 802.15.4 network monitor
+
+ARPHRD_PHONET = 820 # PhoNet media type
+ARPHRD_PHONET_PIPE = 821 # PhoNet pipe header
+ARPHRD_CAIF = 822 # CAIF media type
+ARPHRD_IP6GRE = 823 # GRE over IPv6
+ARPHRD_NETLINK = 824 # Netlink header
+ARPHRD_6LOWPAN = 825 # IPv6 over LoWPAN
+ARPHRD_VSOCKMON = 826 # Vsock monitor header
+
+ARPHRD_VOID = 0xFFFF # Void type, nothing is known
+ARPHRD_NONE = 0xFFFE # Zero header length
+
+# ARP protocol opcodes
+ARPOP_REQUEST = 1 # ARP request
+ARPOP_REPLY = 2 # ARP reply
+ARPOP_RREQUEST = 3 # RARP request
+ARPOP_RREPLY = 4 # RARP reply
+ARPOP_InREQUEST = 8 # InARP request
+ARPOP_InREPLY = 9 # InARP reply
+ARPOP_NAK = 10 # (ATM)ARP NAK
+
+ARPHRD_TO_NAME = {
+ ARPHRD_NETROM: "netrom",
+ ARPHRD_ETHER: "ether",
+ ARPHRD_EETHER: "eether",
+ ARPHRD_AX25: "ax25",
+ ARPHRD_PRONET: "pronet",
+ ARPHRD_CHAOS: "chaos",
+ ARPHRD_IEEE802: "ieee802",
+ ARPHRD_ARCNET: "arcnet",
+ ARPHRD_APPLETLK: "atalk",
+ ARPHRD_DLCI: "dlci",
+ ARPHRD_ATM: "atm",
+ ARPHRD_METRICOM: "metricom",
+ ARPHRD_IEEE1394: "ieee1394",
+ ARPHRD_INFINIBAND: "infiniband",
+ ARPHRD_SLIP: "slip",
+ ARPHRD_CSLIP: "cslip",
+ ARPHRD_SLIP6: "slip6",
+ ARPHRD_CSLIP6: "cslip6",
+ ARPHRD_RSRVD: "rsrvd",
+ ARPHRD_ADAPT: "adapt",
+ ARPHRD_ROSE: "rose",
+ ARPHRD_X25: "x25",
+ ARPHRD_HWX25: "hwx25",
+ ARPHRD_CAN: "can",
+ ARPHRD_PPP: "ppp",
+ ARPHRD_HDLC: "hdlc",
+ ARPHRD_LAPB: "lapb",
+ ARPHRD_DDCMP: "ddcmp",
+ ARPHRD_RAWHDLC: "rawhdlc",
+ ARPHRD_TUNNEL: "ipip",
+ ARPHRD_TUNNEL6: "tunnel6",
+ ARPHRD_FRAD: "frad",
+ ARPHRD_SKIP: "skip",
+ ARPHRD_LOOPBACK: "loopback",
+ ARPHRD_LOCALTLK: "ltalk",
+ ARPHRD_FDDI: "fddi",
+ ARPHRD_BIF: "bif",
+ ARPHRD_SIT: "sit",
+ ARPHRD_IPDDP: "ip/ddp",
+ ARPHRD_IPGRE: "gre",
+ ARPHRD_PIMREG: "pimreg",
+ ARPHRD_HIPPI: "hippi",
+ ARPHRD_ASH: "ash",
+ ARPHRD_ECONET: "econet",
+ ARPHRD_IRDA: "irda",
+ ARPHRD_FCPP: "fcpp",
+ ARPHRD_FCAL: "fcal",
+ ARPHRD_FCPL: "fcpl",
+ ARPHRD_FCFABRIC: "fcfb0",
+ ARPHRD_FCFABRIC+1: "fcfb1",
+ ARPHRD_FCFABRIC+2: "fcfb2",
+ ARPHRD_FCFABRIC+3: "fcfb3",
+ ARPHRD_FCFABRIC+4: "fcfb4",
+ ARPHRD_FCFABRIC+5: "fcfb5",
+ ARPHRD_FCFABRIC+6: "fcfb6",
+ ARPHRD_FCFABRIC+7: "fcfb7",
+ ARPHRD_FCFABRIC+8: "fcfb8",
+ ARPHRD_FCFABRIC+9: "fcfb9",
+ ARPHRD_FCFABRIC+10: "fcfb10",
+ ARPHRD_FCFABRIC+11: "fcfb11",
+ ARPHRD_FCFABRIC+12: "fcfb12",
+ ARPHRD_IEEE802_TR: "tr",
+ ARPHRD_IEEE80211: "ieee802.11",
+ ARPHRD_IEEE80211_PRISM: "ieee802.11/prism",
+ ARPHRD_IEEE80211_RADIOTAP: "ieee802.11/radiotap",
+ ARPHRD_IEEE802154: "ieee802.15.4",
+ ARPHRD_IEEE802154_MONITOR: "ieee802.15.4/monitor",
+ ARPHRD_PHONET: "phonet",
+ ARPHRD_PHONET_PIPE: "phonet_pipe",
+ ARPHRD_CAIF: "caif",
+ ARPHRD_IP6GRE: "gre6",
+ ARPHRD_NETLINK: "netlink",
+ ARPHRD_6LOWPAN: "6lowpan",
+ ARPHRD_NONE: "none",
+ ARPHRD_VOID: "void",
+}
\ No newline at end of file
diff --git a/python/vyos/include/uapi/linux/lwtunnel.py b/python/vyos/include/uapi/linux/lwtunnel.py
new file mode 100644
index 0000000000..6797a762ba
--- /dev/null
+++ b/python/vyos/include/uapi/linux/lwtunnel.py
@@ -0,0 +1,38 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+LWTUNNEL_ENCAP_NONE = 0
+LWTUNNEL_ENCAP_MPLS = 1
+LWTUNNEL_ENCAP_IP = 2
+LWTUNNEL_ENCAP_ILA = 3
+LWTUNNEL_ENCAP_IP6 = 4
+LWTUNNEL_ENCAP_SEG6 = 5
+LWTUNNEL_ENCAP_BPF = 6
+LWTUNNEL_ENCAP_SEG6_LOCAL = 7
+LWTUNNEL_ENCAP_RPL = 8
+LWTUNNEL_ENCAP_IOAM6 = 9
+LWTUNNEL_ENCAP_XFRM = 10
+
+ENCAP_TO_NAME = {
+ LWTUNNEL_ENCAP_MPLS: 'mpls',
+ LWTUNNEL_ENCAP_IP: 'ip',
+ LWTUNNEL_ENCAP_IP6: 'ip6',
+ LWTUNNEL_ENCAP_ILA: 'ila',
+ LWTUNNEL_ENCAP_BPF: 'bpf',
+ LWTUNNEL_ENCAP_SEG6: 'seg6',
+ LWTUNNEL_ENCAP_SEG6_LOCAL: 'seg6local',
+ LWTUNNEL_ENCAP_RPL: 'rpl',
+ LWTUNNEL_ENCAP_IOAM6: 'ioam6',
+ LWTUNNEL_ENCAP_XFRM: 'xfrm',
+}
diff --git a/python/vyos/include/uapi/linux/neighbour.py b/python/vyos/include/uapi/linux/neighbour.py
new file mode 100644
index 0000000000..d5caf44b99
--- /dev/null
+++ b/python/vyos/include/uapi/linux/neighbour.py
@@ -0,0 +1,34 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+NTF_USE = (1 << 0)
+NTF_SELF = (1 << 1)
+NTF_MASTER = (1 << 2)
+NTF_PROXY = (1 << 3)
+NTF_EXT_LEARNED = (1 << 4)
+NTF_OFFLOADED = (1 << 5)
+NTF_STICKY = (1 << 6)
+NTF_ROUTER = (1 << 7)
+NTF_EXT_MANAGED = (1 << 0)
+NTF_EXT_LOCKED = (1 << 1)
+
+NTF_FlAGS = {
+ 'self': NTF_SELF,
+ 'router': NTF_ROUTER,
+ 'extern_learn': NTF_EXT_LEARNED,
+ 'offload': NTF_OFFLOADED,
+ 'master': NTF_MASTER,
+ 'sticky': NTF_STICKY,
+ 'locked': NTF_EXT_LOCKED,
+}
diff --git a/python/vyos/include/uapi/linux/rtnetlink.py b/python/vyos/include/uapi/linux/rtnetlink.py
new file mode 100644
index 0000000000..e312724604
--- /dev/null
+++ b/python/vyos/include/uapi/linux/rtnetlink.py
@@ -0,0 +1,63 @@
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+RTM_F_NOTIFY = 0x100
+RTM_F_CLONED = 0x200
+RTM_F_EQUALIZE = 0x400
+RTM_F_PREFIX = 0x800
+RTM_F_LOOKUP_TABLE = 0x1000
+RTM_F_FIB_MATCH = 0x2000
+RTM_F_OFFLOAD = 0x4000
+RTM_F_TRAP = 0x8000
+RTM_F_OFFLOAD_FAILED = 0x20000000
+
+RTNH_F_DEAD = 1
+RTNH_F_PERVASIVE = 2
+RTNH_F_ONLINK = 4
+RTNH_F_OFFLOAD = 8
+RTNH_F_LINKDOWN = 16
+RTNH_F_UNRESOLVED = 32
+RTNH_F_TRAP = 64
+
+RT_TABLE_COMPAT = 252
+RT_TABLE_DEFAULT = 253
+RT_TABLE_MAIN = 254
+RT_TABLE_LOCAL = 255
+
+RTAX_FEATURE_ECN = (1 << 0)
+RTAX_FEATURE_SACK = (1 << 1)
+RTAX_FEATURE_TIMESTAMP = (1 << 2)
+RTAX_FEATURE_ALLFRAG = (1 << 3)
+RTAX_FEATURE_TCP_USEC_TS = (1 << 4)
+
+RT_FlAGS = {
+ 'dead': RTNH_F_DEAD,
+ 'onlink': RTNH_F_ONLINK,
+ 'pervasive': RTNH_F_PERVASIVE,
+ 'offload': RTNH_F_OFFLOAD,
+ 'trap': RTNH_F_TRAP,
+ 'notify': RTM_F_NOTIFY,
+ 'linkdown': RTNH_F_LINKDOWN,
+ 'unresolved': RTNH_F_UNRESOLVED,
+ 'rt_offload': RTM_F_OFFLOAD,
+ 'rt_trap': RTM_F_TRAP,
+ 'rt_offload_failed': RTM_F_OFFLOAD_FAILED,
+}
+
+RT_TABLE_TO_NAME = {
+ RT_TABLE_COMPAT: 'compat',
+ RT_TABLE_DEFAULT: 'default',
+ RT_TABLE_MAIN: 'main',
+ RT_TABLE_LOCAL: 'local',
+}
diff --git a/smoketest/scripts/cli/test_service_monitoring_network_event.py b/smoketest/scripts/cli/test_service_monitoring_network_event.py
new file mode 100644
index 0000000000..3c9b4bf7f4
--- /dev/null
+++ b/smoketest/scripts/cli/test_service_monitoring_network_event.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# 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 .
+
+import unittest
+from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import read_json
+
+
+base_path = ['service', 'monitoring', 'network-event']
+
+
+def get_logger_config():
+ return read_json('/run/vyos-network-event-logger.conf')
+
+
+class TestMonitoringNetworkEvent(VyOSUnitTestSHIM.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(TestMonitoringNetworkEvent, cls).setUpClass()
+
+ # ensure we can also run this test on a live system - so lets clean
+ # out the current configuration :)
+ cls.cli_delete(cls, base_path)
+
+ def tearDown(self):
+ self.cli_delete(base_path)
+ self.cli_commit()
+
+ def test_network_event_log(self):
+ expected_config = {
+ 'event': {
+ 'route': {},
+ 'link': {},
+ 'addr': {},
+ 'neigh': {},
+ 'rule': {},
+ },
+ 'queue_size': '10000'
+ }
+
+ self.cli_set(base_path + ['event', 'route'])
+ self.cli_set(base_path + ['event', 'link'])
+ self.cli_set(base_path + ['event', 'addr'])
+ self.cli_set(base_path + ['event', 'neigh'])
+ self.cli_set(base_path + ['event', 'rule'])
+ self.cli_set(base_path + ['queue-size', '10000'])
+ self.cli_commit()
+ self.assertEqual(expected_config, get_logger_config())
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/conf_mode/service_monitoring_network_event.py b/src/conf_mode/service_monitoring_network_event.py
new file mode 100644
index 0000000000..104e6ce232
--- /dev/null
+++ b/src/conf_mode/service_monitoring_network_event.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# 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 .
+
+import os
+import json
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.utils.file import write_file
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+vyos_network_event_logger_config = r'/run/vyos-network-event-logger.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'monitoring', 'network-event']
+ if not conf.exists(base):
+ return None
+
+ monitoring = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ monitoring = conf.merge_defaults(monitoring, recursive=True)
+
+ return monitoring
+
+
+def verify(monitoring):
+ if not monitoring:
+ return None
+
+ return None
+
+
+def generate(monitoring):
+ if not monitoring:
+ # Delete config
+ if os.path.exists(vyos_network_event_logger_config):
+ os.unlink(vyos_network_event_logger_config)
+
+ return None
+
+ # Create config
+ log_conf_json = json.dumps(monitoring, indent=4)
+ write_file(vyos_network_event_logger_config, log_conf_json)
+
+ return None
+
+
+def apply(monitoring):
+ # Reload systemd manager configuration
+ systemd_service = 'vyos-network-event-logger.service'
+
+ if not monitoring:
+ call(f'systemctl stop {systemd_service}')
+ return
+
+ call(f'systemctl restart {systemd_service}')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/services/vyos-network-event-logger b/src/services/vyos-network-event-logger
new file mode 100644
index 0000000000..840ff3cda1
--- /dev/null
+++ b/src/services/vyos-network-event-logger
@@ -0,0 +1,1218 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 VyOS maintainers and contributors
+#
+# 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 .
+
+import argparse
+import logging
+import multiprocessing
+import queue
+import signal
+import socket
+import threading
+from pathlib import Path
+from time import sleep
+from typing import Dict, AnyStr, List, Union
+
+from pyroute2.common import AF_MPLS
+from pyroute2.iproute import IPRoute
+from pyroute2.netlink import rtnl, nlmsg
+from pyroute2.netlink.nfnetlink.nfctsocket import nfct_msg
+from pyroute2.netlink.rtnl import (rt_proto as RT_PROTO, rt_type as RT_TYPES,
+ rtypes as RTYPES
+ )
+from pyroute2.netlink.rtnl.fibmsg import FR_ACT_GOTO, FR_ACT_NOP, FR_ACT_TO_TBL, \
+ fibmsg
+from pyroute2.netlink.rtnl import ifaddrmsg
+from pyroute2.netlink.rtnl import ifinfmsg
+from pyroute2.netlink.rtnl import ndmsg
+from pyroute2.netlink.rtnl import rtmsg
+from pyroute2.netlink.rtnl.rtmsg import nh, rtmsg_base
+
+from vyos.include.uapi.linux.fib_rules import *
+from vyos.include.uapi.linux.icmpv6 import *
+from vyos.include.uapi.linux.if_arp import *
+from vyos.include.uapi.linux.lwtunnel import *
+from vyos.include.uapi.linux.neighbour import *
+from vyos.include.uapi.linux.rtnetlink import *
+
+from vyos.utils.file import read_json
+
+
+manager = multiprocessing.Manager()
+cache = manager.dict()
+
+
+class UnsupportedMessageType(Exception):
+ pass
+
+shutdown_event = multiprocessing.Event()
+
+logging.basicConfig(level=logging.INFO, format='%(message)s')
+logger = logging.getLogger(__name__)
+
+
+class DebugFormatter(logging.Formatter):
+ def format(self, record):
+ self._style._fmt = '[%(asctime)s] %(levelname)s: %(message)s'
+ return super().format(record)
+
+
+def set_log_level(level: str) -> None:
+ if level == 'debug':
+ logger.setLevel(logging.DEBUG)
+ logger.parent.handlers[0].setFormatter(DebugFormatter())
+ else:
+ logger.setLevel(logging.INFO)
+
+IFF_FLAGS = {
+ 'RUNNING': ifinfmsg.IFF_RUNNING,
+ 'LOOPBACK': ifinfmsg.IFF_LOOPBACK,
+ 'BROADCAST': ifinfmsg.IFF_BROADCAST,
+ 'POINTOPOINT': ifinfmsg.IFF_POINTOPOINT,
+ 'MULTICAST': ifinfmsg.IFF_MULTICAST,
+ 'NOARP': ifinfmsg.IFF_NOARP,
+ 'ALLMULTI': ifinfmsg.IFF_ALLMULTI,
+ 'PROMISC': ifinfmsg.IFF_PROMISC,
+ 'MASTER': ifinfmsg.IFF_MASTER,
+ 'SLAVE': ifinfmsg.IFF_SLAVE,
+ 'DEBUG': ifinfmsg.IFF_DEBUG,
+ 'DYNAMIC': ifinfmsg.IFF_DYNAMIC,
+ 'AUTOMEDIA': ifinfmsg.IFF_AUTOMEDIA,
+ 'PORTSEL': ifinfmsg.IFF_PORTSEL,
+ 'NOTRAILERS': ifinfmsg.IFF_NOTRAILERS,
+ 'UP': ifinfmsg.IFF_UP,
+ 'LOWER_UP': ifinfmsg.IFF_LOWER_UP,
+ 'DORMANT': ifinfmsg.IFF_DORMANT,
+ 'ECHO': ifinfmsg.IFF_ECHO,
+}
+
+NEIGH_STATE_FLAGS = {
+ 'INCOMPLETE': ndmsg.NUD_INCOMPLETE,
+ 'REACHABLE': ndmsg.NUD_REACHABLE,
+ 'STALE': ndmsg.NUD_STALE,
+ 'DELAY': ndmsg.NUD_DELAY,
+ 'PROBE': ndmsg.NUD_PROBE,
+ 'FAILED': ndmsg.NUD_FAILED,
+ 'NOARP': ndmsg.NUD_NOARP,
+ 'PERMANENT': ndmsg.NUD_PERMANENT,
+}
+
+IFA_FLAGS = {
+ 'secondary': ifaddrmsg.IFA_F_SECONDARY,
+ 'temporary': ifaddrmsg.IFA_F_SECONDARY,
+ 'nodad': ifaddrmsg.IFA_F_NODAD,
+ 'optimistic': ifaddrmsg.IFA_F_OPTIMISTIC,
+ 'dadfailed': ifaddrmsg.IFA_F_DADFAILED,
+ 'home': ifaddrmsg.IFA_F_HOMEADDRESS,
+ 'deprecated': ifaddrmsg.IFA_F_DEPRECATED,
+ 'tentative': ifaddrmsg.IFA_F_TENTATIVE,
+ 'permanent': ifaddrmsg.IFA_F_PERMANENT,
+ 'mngtmpaddr': ifaddrmsg.IFA_F_MANAGETEMPADDR,
+ 'noprefixroute': ifaddrmsg.IFA_F_NOPREFIXROUTE,
+ 'autojoin': ifaddrmsg.IFA_F_MCAUTOJOIN,
+ 'stable-privacy': ifaddrmsg.IFA_F_STABLE_PRIVACY,
+}
+
+RT_SCOPE_TO_NAME = {
+ rtmsg.RT_SCOPE_UNIVERSE: 'global',
+ rtmsg.RT_SCOPE_SITE: 'site',
+ rtmsg.RT_SCOPE_LINK: 'link',
+ rtmsg.RT_SCOPE_HOST: 'host',
+ rtmsg.RT_SCOPE_NOWHERE: 'nowhere',
+}
+
+FAMILY_TO_NAME = {
+ socket.AF_INET: 'inet',
+ socket.AF_INET6: 'inet6',
+ socket.AF_PACKET: 'link',
+ AF_MPLS: 'mpls',
+ socket.AF_BRIDGE: 'bridge',
+}
+
+_INFINITY = 4294967295
+
+
+def _get_iif_name(idx: int) -> str:
+ """
+ Retrieves the interface name associated with a given index.
+ """
+ try:
+ if_info = IPRoute().link("get", index=idx)
+ if if_info:
+ return if_info[0].get_attr('IFLA_IFNAME')
+ except Exception as e:
+ pass
+
+ return ''
+
+
+def remember_if_index(idx: int, event_type: int) -> None:
+ """
+ Manages the caching of network interface names based on their index and event type.
+
+ - For RTM_DELLINK event, the interface name is removed from the cache if exists.
+ - For RTM_NEWLINK event, the interface name is retrieved and updated in the cache.
+ """
+ name = cache.get(idx)
+ if name:
+ if event_type == rtnl.RTM_DELLINK:
+ del cache[idx]
+ else:
+ name = _get_iif_name(idx)
+ if name:
+ cache[idx] = name
+ else:
+ cache[idx] = _get_iif_name(idx)
+
+
+class BaseFormatter:
+ """
+ A base class providing utility methods for formatting network message data.
+ """
+ def _get_if_name_by_index(self, idx: int) -> str:
+ """
+ Retrieves the name of a network interface based on its index.
+
+ Uses a cached lookup for efficiency. If the name is not found in the cache,
+ it queries the system and updates the cache.
+ """
+ if_name = cache.get(idx)
+ if not if_name:
+ if_name = _get_iif_name(idx)
+ cache[idx] = if_name
+
+ return if_name
+
+ def _format_rttable(self, idx: int) -> str:
+ """
+ Formats a route table identifier into a readable name.
+ """
+ return f'{RT_TABLE_TO_NAME.get(idx, idx)}'
+
+ def _parse_flag(self, data: int, flags: dict) -> list:
+ """
+ Extracts and returns flag names equal the bits set in a numeric value.
+ """
+ result = list()
+ if data:
+ for key, val in flags.items():
+ if data & val:
+ result.append(key)
+ data &= ~val
+
+ if data:
+ result.append(f"{data:#x}")
+
+ return result
+
+ def af_bit_len(self, af: int) -> int:
+ """
+ Gets the bit length of a given address family.
+ Supports common address families like IPv4, IPv6, and MPLS.
+ """
+ _map = {
+ socket.AF_INET6: 128,
+ socket.AF_INET: 32,
+ AF_MPLS: 20,
+ }
+
+ return _map.get(af)
+
+ def _format_simple_field(self, data: str, prefix: str='') -> str:
+ """
+ Formats a simple field with an optional prefix.
+
+ A simple field represents a value that does not require additional
+ parsing and is used as is.
+ """
+ return self._output(f'{prefix} {data}') if data is not None else ''
+
+ def _output(self, data: str) -> str:
+ """
+ Standardizes the output format.
+
+ Ensures that the output is enclosed with single spaces and has no leading
+ or trailing whitespace.
+ """
+ return f' {data.strip()} ' if data else ''
+
+
+class BaseMSGFormatter(BaseFormatter):
+ """
+ A base formatter class for network messages.
+ This class provides common methods for formatting network-related messages,
+ """
+
+ def _prepare_start_message(self, event: str) -> str:
+ """
+ Prepares a starting message string based on the event type.
+ """
+ if event in ['RTM_DELROUTE', 'RTM_DELLINK', 'RTM_DELNEIGH',
+ 'RTM_DELADDR', 'RTM_DELADDRLABEL', 'RTM_DELRULE',
+ 'RTM_DELNETCONF']:
+ return 'Deleted '
+ if event == 'RTM_GETNEIGH':
+ return 'Miss '
+ return ''
+
+ def _format_flow_field(self, data: int) -> str:
+ """
+ Formats a flow field to represent traffic realms.
+ """
+ to = data & 0xFFFF
+ from_ = data >> 16
+ result = f"realm{'s' if from_ else ''} "
+ if from_:
+ result += f'{from_}/'
+ result += f'{to}'
+
+ return self._output(result)
+
+ def format(self, msg: nlmsg) -> str:
+ """
+ Abstract method to format a complete message.
+
+ This method must be implemented by subclasses to provide specific formatting
+ logic for different types of messages.
+ """
+ raise NotImplementedError(f'{msg.get("event")}: {msg}')
+
+
+class LinkFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling link-related network messages
+ `RTM_NEWLINK` and `RTM_DELLINK`.
+ """
+ def _format_iff_flags(self, flags: int) -> str:
+ """
+ Formats interface flags into a human-readable string.
+ """
+ result = list()
+ if flags:
+ if flags & IFF_FLAGS['UP'] and not flags & IFF_FLAGS['RUNNING']:
+ result.append('NO-CARRIER')
+
+ flags &= ~IFF_FLAGS['RUNNING']
+
+ result.extend(self._parse_flag(flags, IFF_FLAGS))
+
+ return self._output(f'<{(",").join(result)}>')
+
+ def _format_if_props(self, data: ifinfmsg.ifinfbase.proplist) -> str:
+ """
+ Formats interface alternative name properties.
+ """
+ result = ''
+ for rec in data.altnames():
+ result += f'[altname {rec}] '
+ return self._output(result)
+
+ def _format_link(self, msg: ifinfmsg.ifinfmsg) -> str:
+ """
+ Formats the link attribute of a network interface message.
+ """
+ if msg.get_attr("IFLA_LINK") is not None:
+ iflink = msg.get_attr("IFLA_LINK")
+ if iflink:
+ if msg.get_attr("IFLA_LINK_NETNSID"):
+ return f'if{iflink}'
+ else:
+ return self._get_if_name_by_index(iflink)
+ return 'NONE'
+
+ def _format_link_info(self, msg: ifinfmsg.ifinfmsg) -> str:
+ """
+ Formats detailed information about the link, including type, address,
+ broadcast address, and permanent address.
+ """
+ result = f'link/{ARPHRD_TO_NAME.get(msg.get("ifi_type"), msg.get("ifi_type"))}'
+ result += self._format_simple_field(msg.get_attr('IFLA_ADDRESS'))
+
+ if msg.get_attr("IFLA_BROADCAST"):
+ if msg.get('flags') & ifinfmsg.IFF_POINTOPOINT:
+ result += f' peer'
+ else:
+ result += f' brd'
+ result += f' {msg.get_attr("IFLA_BROADCAST")}'
+
+ if msg.get_attr("IFLA_PERM_ADDRESS"):
+ if not msg.get_attr("IFLA_ADDRESS") or \
+ msg.get_attr("IFLA_ADDRESS") != msg.get_attr("IFLA_PERM_ADDRESS"):
+ result += f' permaddr {msg.get_attr("IFLA_PERM_ADDRESS")}'
+
+ return self._output(result)
+
+ def format(self, msg: ifinfmsg.ifinfmsg):
+ """
+ Formats a network link message into a structured output string.
+ """
+ if msg.get("family") not in [socket.AF_UNSPEC, socket.AF_BRIDGE]:
+ return None
+
+ message = self._prepare_start_message(msg.get('event'))
+
+ link = self._format_link(msg)
+
+ message += f'{msg.get("index")}: {msg.get_attr("IFLA_IFNAME")}'
+ message += f'@{link}' if link else ''
+ message += f': {self._format_iff_flags(msg.get("flags"))}'
+
+ message += self._format_simple_field(msg.get_attr('IFLA_MTU'), prefix='mtu')
+ message += self._format_simple_field(msg.get_attr('IFLA_QDISC'), prefix='qdisc')
+ message += self._format_simple_field(msg.get_attr('IFLA_OPERSTATE'), prefix='state')
+ message += self._format_simple_field(msg.get_attr('IFLA_GROUP'), prefix='group')
+ message += self._format_simple_field(msg.get_attr('IFLA_MASTER'), prefix='master')
+
+ message += self._format_link_info(msg)
+
+ if msg.get_attr('IFLA_PROP_LIST'):
+ message += self._format_if_props(msg.get_attr('IFLA_PROP_LIST'))
+
+ return self._output(message)
+
+
+class EncapFormatter(BaseFormatter):
+ """
+ A formatter class for handling encapsulation attributes in routing messages.
+ """
+ # TODO: implement other lwtunnel decoder in pyroute2
+ # https://github.com/svinota/pyroute2/blob/78cfe838bec8d96324811a3962bda15fb028e0ce/pyroute2/netlink/rtnl/rtmsg.py#L657
+ def __init__(self):
+ """
+ Initializes the EncapFormatter with supported encapsulation types.
+ """
+ self.formatters = {
+ rtmsg.LWTUNNEL_ENCAP_MPLS: self.mpls_format,
+ rtmsg.LWTUNNEL_ENCAP_SEG6: self.seg6_format,
+ rtmsg.LWTUNNEL_ENCAP_BPF: self.bpf_format,
+ rtmsg.LWTUNNEL_ENCAP_SEG6_LOCAL: self.seg6local_format,
+ }
+
+ def _format_srh(self, data: rtmsg_base.seg6_encap_info.ipv6_sr_hdr):
+ """
+ Formats Segment Routing Header (SRH) attributes.
+ """
+ result = ''
+ # pyroute2 decode mode only as inline or encap (encap, l2encap, encap.red, l2encap.red")
+ # https://github.com/svinota/pyroute2/blob/78cfe838bec8d96324811a3962bda15fb028e0ce/pyroute2/netlink/rtnl/rtmsg.py#L220
+ for key in ['mode', 'segs']:
+
+ val = data.get(key)
+
+ if val:
+ if key == 'segs':
+ result += f'{key} {len(val)} {val} '
+ else:
+ result += f'{key} {val} '
+
+ return self._output(result)
+
+ def _format_bpf_object(self, data: rtmsg_base.bpf_encap_info, attr_name: str, attr_key: str):
+ """
+ Formats eBPF program attributes.
+ """
+ attr = data.get_attr(attr_name)
+ if not attr:
+ return ''
+ result = ''
+ if attr.get_attr("LWT_BPF_PROG_NAME"):
+ result += f'{attr.get_attr("LWT_BPF_PROG_NAME")} '
+ if attr.get_attr("LWT_BPF_PROG_FD"):
+ result += f'{attr.get_attr("LWT_BPF_PROG_FD")} '
+
+ return self._output(f'{attr_key} {result.strip()}')
+
+ def mpls_format(self, data: rtmsg_base.mpls_encap_info):
+ """
+ Formats MPLS encapsulation attributes.
+ """
+ result = ''
+ if data.get_attr("MPLS_IPTUNNEL_DST"):
+ for rec in data.get_attr("MPLS_IPTUNNEL_DST"):
+ for key, val in rec.items():
+ if val:
+ result += f'{key} {val} '
+
+ if data.get_attr("MPLS_IPTUNNEL_TTL"):
+ result += f' ttl {data.get_attr("MPLS_IPTUNNEL_TTL")}'
+
+ return self._output(result)
+
+ def bpf_format(self, data: rtmsg_base.bpf_encap_info):
+ """
+ Formats eBPF encapsulation attributes.
+ """
+ result = ''
+ result += self._format_bpf_object(data, 'LWT_BPF_IN', 'in')
+ result += self._format_bpf_object(data, 'LWT_BPF_OUT', 'out')
+ result += self._format_bpf_object(data, 'LWT_BPF_XMIT', 'xmit')
+
+ if data.get_attr('LWT_BPF_XMIT_HEADROOM'):
+ result += f'headroom {data.get_attr("LWT_BPF_XMIT_HEADROOM")} '
+
+ return self._output(result)
+
+ def seg6_format(self, data: rtmsg_base.seg6_encap_info):
+ """
+ Formats Segment Routing (SEG6) encapsulation attributes.
+ """
+ result = ''
+ if data.get_attr("SEG6_IPTUNNEL_SRH"):
+ result += self._format_srh(data.get_attr("SEG6_IPTUNNEL_SRH"))
+
+ return self._output(result)
+
+ def seg6local_format(self, data: rtmsg_base.seg6local_encap_info):
+ """
+ Formats SEG6 local encapsulation attributes.
+ """
+ result = ''
+ formatters = {
+ 'SEG6_LOCAL_ACTION': lambda val: f' action {next((k for k, v in data.action.actions.items() if v == val), "unknown")}',
+ 'SEG6_LOCAL_SRH': lambda val: f' {self._format_srh(val)}',
+ 'SEG6_LOCAL_TABLE': lambda val: f' table {self._format_rttable(val)}',
+ 'SEG6_LOCAL_NH4': lambda val: f' nh4 {val}',
+ 'SEG6_LOCAL_NH6': lambda val: f' nh6 {val}',
+ 'SEG6_LOCAL_IIF': lambda val: f' iif {self._get_if_name_by_index(val)}',
+ 'SEG6_LOCAL_OIF': lambda val: f' oif {self._get_if_name_by_index(val)}',
+ 'SEG6_LOCAL_BPF': lambda val: f' endpoint {val.get("LWT_BPF_PROG_NAME")}',
+ 'SEG6_LOCAL_VRFTABLE': lambda val: f' vrftable {self._format_rttable(val)}',
+ }
+
+ for rec in data.get('attrs'):
+ if rec[0] in formatters:
+ result += formatters[rec[0]](rec[1])
+
+ return self._output(result)
+
+ def format(self, type: int, data: Union[rtmsg_base.mpls_encap_info,
+ rtmsg_base.bpf_encap_info,
+ rtmsg_base.seg6_encap_info,
+ rtmsg_base.seg6local_encap_info]):
+ """
+ Formats encapsulation attributes based on their type.
+ """
+ result = ''
+ formatter = self.formatters.get(type)
+
+ result += f'encap {ENCAP_TO_NAME.get(type, "unknown")}'
+
+ if formatter:
+ result += f' {formatter(data)}'
+
+ return self._output(result)
+
+
+class RouteFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling network routing messages
+ `RTM_NEWROUTE` and `RTM_DELROUTE`.
+ """
+
+ def _format_rt_flags(self, flags: int) -> str:
+ """
+ Formats route flags into a comma-separated string.
+ """
+ result = list()
+ result.extend(self._parse_flag(flags, RT_FlAGS))
+
+ return self._output(",".join(result))
+
+ def _format_rta_encap(self, type: int, data: Union[rtmsg_base.mpls_encap_info,
+ rtmsg_base.bpf_encap_info,
+ rtmsg_base.seg6_encap_info,
+ rtmsg_base.seg6local_encap_info]) -> str:
+ """
+ Formats encapsulation attributes.
+ """
+ return EncapFormatter().format(type, data)
+
+ def _format_rta_newdest(self, data: str) -> str:
+ """
+ Formats a new destination attribute.
+ """
+ return self._output(f'as to {data}')
+
+ def _format_rta_gateway(self, data: str) -> str:
+ """
+ Formats a gateway attribute.
+ """
+ return self._output(f'via {data}')
+
+ def _format_rta_via(self, data: str) -> str:
+ """
+ Formats a 'via' route attribute.
+ """
+ return self._output(f'{data}')
+
+ def _format_rta_metrics(self, data: rtmsg_base.metrics):
+ """
+ Formats routing metrics.
+ """
+ result = ''
+
+ def __format_metric_time(_val: int) -> str:
+ """Formats metric time values into seconds or milliseconds."""
+ return f"{_val / 1000}s" if _val >= 1000 else f"{_val}ms"
+
+ def __format_reatures(_val: int) -> str:
+ """Parse and formats routing feature flags."""
+ result = self._parse_flag(_val, {'ecn': RTAX_FEATURE_ECN,
+ 'tcp_usec_ts': RTAX_FEATURE_TCP_USEC_TS})
+ return ",".join(result)
+
+ formatters = {
+ 'RTAX_MTU': lambda val: f' mtu {val}',
+ 'RTAX_WINDOW': lambda val: f' window {val}',
+ 'RTAX_RTT': lambda val: f' rtt {__format_metric_time(val / 8)}',
+ 'RTAX_RTTVAR': lambda val: f' rttvar {__format_metric_time(val / 4)}',
+ 'RTAX_SSTHRESH': lambda val: f' ssthresh {val}',
+ 'RTAX_CWND': lambda val: f' cwnd {val}',
+ 'RTAX_ADVMSS': lambda val: f' advmss {val}',
+ 'RTAX_REORDERING': lambda val: f' reordering {val}',
+ 'RTAX_HOPLIMIT': lambda val: f' hoplimit {val}',
+ 'RTAX_INITCWND': lambda val: f' initcwnd {val}',
+ 'RTAX_FEATURES': lambda val: f' features {__format_reatures(val)}',
+ 'RTAX_RTO_MIN': lambda val: f' rto_min {__format_metric_time(val)}',
+ 'RTAX_INITRWND': lambda val: f' initrwnd {val}',
+ 'RTAX_QUICKACK': lambda val: f' quickack {val}',
+ }
+
+ for rec in data.get('attrs'):
+ if rec[0] in formatters:
+ result += formatters[rec[0]](rec[1])
+
+ return self._output(result)
+
+ def _format_rta_pref(self, data: int) -> str:
+ """
+ Formats a pref attribute.
+ """
+ pref = {
+ ICMPV6_ROUTER_PREF_LOW: "low",
+ ICMPV6_ROUTER_PREF_MEDIUM: "medium",
+ ICMPV6_ROUTER_PREF_HIGH: "high",
+ }
+
+ return self._output(f' pref {pref.get(data, data)}')
+
+ def _format_rta_multipath(self, mcast_cloned: bool, family: int, data: List[nh]) -> str:
+ """
+ Formats multipath route attributes.
+ """
+ result = ''
+ first = True
+ for rec in data:
+ if mcast_cloned:
+ if first:
+ result += ' Oifs: '
+ first = False
+ else:
+ result += ' '
+ else:
+ result += ' nexthop '
+
+ if rec.get_attr('RTA_ENCAP'):
+ result += self._format_rta_encap(rec.get_attr('RTA_ENCAP_TYPE'),
+ rec.get_attr('RTA_ENCAP'))
+
+ if rec.get_attr('RTA_NEWDST'):
+ result += self._format_rta_newdest(rec.get_attr('RTA_NEWDST'))
+
+ if rec.get_attr('RTA_GATEWAY'):
+ result += self._format_rta_gateway(rec.get_attr('RTA_GATEWAY'))
+
+ if rec.get_attr('RTA_VIA'):
+ result += self._format_rta_via(rec.get_attr('RTA_VIA'))
+
+ if rec.get_attr('RTA_FLOW'):
+ result += self._format_flow_field(rec.get_attr('RTA_FLOW'))
+
+ result += f' dev {self._get_if_name_by_index(rec.get("oif"))}'
+ if mcast_cloned:
+ if rec.get("hops") != 1:
+ result += f' (ttl>{rec.get("hops")})'
+ else:
+ if family != AF_MPLS:
+ result += f' weight {rec.get("hops") + 1}'
+
+ result += self._format_rt_flags(rec.get("flags"))
+
+ return self._output(result)
+
+ def format(self, msg: rtmsg.rtmsg) -> str:
+ """
+ Formats a network route message into a human-readable string representation.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+
+ message += RT_TYPES.get(msg.get('type'))
+
+ if msg.get_attr('RTA_DST'):
+ host_len = self.af_bit_len(msg.get('family'))
+ if msg.get('dst_len') != host_len:
+ message += f' {msg.get_attr("RTA_DST")}/{msg.get("dst_len")}'
+ else:
+ message += f' {msg.get_attr("RTA_DST")}'
+ elif msg.get('dst_len'):
+ message += f' 0/{msg.get("dst_len")}'
+ else:
+ message += ' default'
+
+ if msg.get_attr('RTA_SRC'):
+ message += f' from {msg.get_attr("RTA_SRC")}'
+ elif msg.get('src_len'):
+ message += f' from 0/{msg.get("src_len")}'
+
+ message += self._format_simple_field(msg.get_attr('RTA_NH_ID'), prefix='nhid')
+
+ if msg.get_attr('RTA_NEWDST'):
+ message += self._format_rta_newdest(msg.get_attr('RTA_NEWDST'))
+
+ if msg.get_attr('RTA_ENCAP'):
+ message += self._format_rta_encap(msg.get_attr('RTA_ENCAP_TYPE'),
+ msg.get_attr('RTA_ENCAP'))
+
+ message += self._format_simple_field(msg.get('tos'), prefix='tos')
+
+ if msg.get_attr('RTA_GATEWAY'):
+ message += self._format_rta_gateway(msg.get_attr('RTA_GATEWAY'))
+
+ if msg.get_attr('RTA_VIA'):
+ message += self._format_rta_via(msg.get_attr('RTA_VIA'))
+
+ if msg.get_attr('RTA_OIF') is not None:
+ message += f' dev {self._get_if_name_by_index(msg.get_attr("RTA_OIF"))}'
+
+ if msg.get_attr("RTA_TABLE"):
+ message += f' table {self._format_rttable(msg.get_attr("RTA_TABLE"))}'
+
+ if not msg.get('flags') & RTM_F_CLONED:
+ message += f' proto {RT_PROTO.get(msg.get("proto"))}'
+
+ if not msg.get('scope') == rtmsg.RT_SCOPE_UNIVERSE:
+ message += f' scope {RT_SCOPE_TO_NAME.get(msg.get("scope"))}'
+
+ message += self._format_simple_field(msg.get_attr('RTA_PREFSRC'), prefix='src')
+ message += self._format_simple_field(msg.get_attr('RTA_PRIORITY'), prefix='metric')
+
+ message += self._format_rt_flags(msg.get("flags"))
+
+ if msg.get_attr('RTA_MARK'):
+ mark = msg.get_attr("RTA_MARK")
+ if mark >= 16:
+ message += f' mark 0x{mark:x}'
+ else:
+ message += f' mark {mark}'
+
+ if msg.get_attr('RTA_FLOW'):
+ message += self._format_flow_field(msg.get_attr('RTA_FLOW'))
+
+ message += self._format_simple_field(msg.get_attr('RTA_UID'), prefix='uid')
+
+ if msg.get_attr('RTA_METRICS'):
+ message += self._format_rta_metrics(msg.get_attr("RTA_METRICS"))
+
+ if msg.get_attr('RTA_IIF') is not None:
+ message += f' iif {self._get_if_name_by_index(msg.get_attr("RTA_IIF"))}'
+
+ if msg.get_attr('RTA_PREF') is not None:
+ message += self._format_rta_pref(msg.get_attr("RTA_PREF"))
+
+ if msg.get_attr('RTA_TTL_PROPAGATE') is not None:
+ message += f' ttl-propogate {"enabled" if msg.get_attr("RTA_TTL_PROPAGATE") else "disabled"}'
+
+ if msg.get_attr('RTA_MULTIPATH') is not None:
+ _tmp = self._format_rta_multipath(
+ mcast_cloned=msg.get('flags') & RTM_F_CLONED and msg.get('type') == RTYPES['RTN_MULTICAST'],
+ family=msg.get('family'),
+ data=msg.get_attr("RTA_MULTIPATH"))
+ message += f' {_tmp}'
+
+ return self._output(message)
+
+
+class AddrFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling address-related network messages
+ `RTM_NEWADDR` and `RTM_DELADDR`.
+ """
+ INFINITY_LIFE_TIME = _INFINITY
+
+ def _format_ifa_flags(self, flags: int, family: int) -> str:
+ """
+ Formats address flags into a human-readable string.
+ """
+ result = list()
+ if flags:
+ if not flags & IFA_FLAGS['permanent']:
+ result.append('dynamic')
+ flags &= ~IFA_FLAGS['permanent']
+
+ if flags & IFA_FLAGS['temporary'] and family == socket.AF_INET6:
+ result.append('temporary')
+ flags &= ~IFA_FLAGS['temporary']
+
+ result.extend(self._parse_flag(flags, IFA_FLAGS))
+
+ return self._output(",".join(result))
+
+ def _format_ifa_addr(self, local: str, addr: str, preflen: int, priority: int) -> str:
+ """
+ Formats address information into a shuman-readable string.
+ """
+ result = ''
+ local = local or addr
+ addr = addr or local
+
+ if local:
+ result += f'{local}'
+ if addr and addr != local:
+ result += f' peer {addr}'
+ result += f'/{preflen}'
+
+ if priority:
+ result += f' {priority}'
+
+ return self._output(result)
+
+ def _format_ifa_cacheinfo(self, data: ifaddrmsg.ifaddrmsg.cacheinfo) -> str:
+ """
+ Formats cache information for an address.
+ """
+ result = ''
+ _map = {
+ 'ifa_valid': 'valid_lft',
+ 'ifa_preferred': 'preferred_lft',
+ }
+
+ for key in ['ifa_valid', 'ifa_preferred']:
+ val = data.get(key)
+ if val == self.INFINITY_LIFE_TIME:
+ result += f'{_map.get(key)} forever '
+ else:
+ result += f'{_map.get(key)} {val}sec '
+
+ return self._output(result)
+
+ def format(self, msg: ifaddrmsg.ifaddrmsg) -> str:
+ """
+ Formats a full network address message.
+ Combine attributes such as index, family, address, flags, and cache
+ information into a structured output string.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+
+ message += f'{msg.get("index")}: {self._get_if_name_by_index(msg.get("index"))} '
+ message += f'{FAMILY_TO_NAME.get(msg.get("family"), msg.get("family"))} '
+
+ message += self._format_ifa_addr(
+ msg.get_attr('IFA_LOCAL'),
+ msg.get_attr('IFA_ADDRESS'),
+ msg.get('prefixlen'),
+ msg.get_attr('IFA_RT_PRIORITY')
+ )
+ message += self._format_simple_field(msg.get_attr('IFA_BROADCAST'), prefix='brd')
+ message += self._format_simple_field(msg.get_attr('IFA_ANYCAST'), prefix='any')
+
+ if msg.get('scope') is not None:
+ message += f' scope {RT_SCOPE_TO_NAME.get(msg.get("scope"))}'
+
+ message += self._format_ifa_flags(msg.get_attr("IFA_FLAGS"), msg.get("family"))
+ message += self._format_simple_field(msg.get_attr('IFA_LABEL'), prefix='label:')
+
+ if msg.get_attr('IFA_CACHEINFO'):
+ message += self._format_ifa_cacheinfo(msg.get_attr('IFA_CACHEINFO'))
+
+ return self._output(message)
+
+
+class NeighFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling neighbor-related network messages
+ `RTM_NEWNEIGH`, `RTM_DELNEIGH` and `RTM_GETNEIGH`
+ """
+ def _format_ntf_flags(self, flags: int) -> str:
+ """
+ Formats neighbor table entry flags into a human-readable string.
+ """
+ result = list()
+ result.extend(self._parse_flag(flags, NTF_FlAGS))
+
+ return self._output(",".join(result))
+
+ def _format_neigh_state(self, data: int) -> str:
+ """
+ Formats the state of a neighbor entry.
+ """
+ result = list()
+ result.extend(self._parse_flag(data, NEIGH_STATE_FLAGS))
+
+ return self._output(",".join(result))
+
+ def format(self, msg: ndmsg.ndmsg) -> str:
+ """
+ Formats a full neighbor-related network message.
+ Combine attributes such as destination, device, link-layer address,
+ flags, state, and protocol into a structured output string.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+ message += self._format_simple_field(msg.get_attr('NDA_DST'), prefix='')
+
+ if msg.get("ifindex") is not None:
+ message += f' dev {self._get_if_name_by_index(msg.get("ifindex"))}'
+
+ message += self._format_simple_field(msg.get_attr('NDA_LLADDR'), prefix='lladdr')
+ message += f' {self._format_ntf_flags(msg.get("flags"))}'
+ message += f' {self._format_neigh_state(msg.get("state"))}'
+
+ if msg.get_attr('NDA_PROTOCOL'):
+ message += f' proto {RT_PROTO.get(msg.get_attr("NDA_PROTOCOL"), msg.get_attr("NDA_PROTOCOL"))}'
+
+ return self._output(message)
+
+
+class RuleFormatter(BaseMSGFormatter):
+ """
+ A formatter class for handling ruting tule network messages
+ `RTM_NEWRULE` and `RTM_DELRULE`
+ """
+ def _format_direction(self, data: str, length: int, host_len: int):
+ """
+ Formats the direction of traffic based on source or destination and prefix length.
+ """
+ result = ''
+ if data:
+ result += f' {data}'
+ if length != host_len:
+ result += f'/{length}'
+ elif length:
+ result += f' 0/{length}'
+
+ return self._output(result)
+
+ def _format_fra_interface(self, data: str, flags: int, prefix: str):
+ """
+ Formats interface-related attributes.
+ """
+ result = f'{prefix} {data}'
+ if flags & FIB_RULE_IIF_DETACHED:
+ result += '[detached]'
+
+ return self._output(result)
+
+ def _format_fra_range(self, data: [str, dict], prefix: str):
+ """
+ Formats a range of values (e.g., UID, sport, or dport).
+ """
+ result = ''
+ if data:
+ if isinstance(data, str):
+ result += f' {prefix} {data}'
+ else:
+ result += f' {prefix} {data.get("start")}:{data.get("end")}'
+ return self._output(result)
+
+ def _format_fra_table(self, msg: fibmsg):
+ """
+ Formats the lookup table and associated attributes in the message.
+ """
+ def __format_field(data: int, prefix: str):
+ if data and data not in [-1, _INFINITY]:
+ return f' {prefix} {data}'
+ return ''
+
+ result = ''
+ table = msg.get_attr('FRA_TABLE') or msg.get('table')
+ if table:
+ result += f' lookup {self._format_rttable(table)}'
+ result += __format_field(msg.get_attr('FRA_SUPPRESS_PREFIXLEN'), 'suppress_prefixlength')
+ result += __format_field(msg.get_attr('FRA_SUPPRESS_IFGROUP'), 'suppress_ifgroup')
+
+ return self._output(result)
+
+ def _format_fra_action(self, msg: fibmsg):
+ """
+ Formats the action associated with the rule.
+ """
+ result = ''
+ if msg.get('action') == RTYPES.get('RTN_NAT'):
+ if msg.get_attr('RTA_GATEWAY'): # looks like deprecated but still use in iproute2
+ result += f' map-to {msg.get_attr("RTA_GATEWAY")}'
+ else:
+ result += ' masquerade'
+
+ elif msg.get('action') == FR_ACT_GOTO:
+ result += f' goto {msg.get_attr("FRA_GOTO") or "none"}'
+ if msg.get('flags') & FIB_RULE_UNRESOLVED:
+ result += ' [unresolved]'
+
+ elif msg.get('action') == FR_ACT_NOP:
+ result += ' nop'
+
+ elif msg.get('action') != FR_ACT_TO_TBL:
+ result += f' {RTYPES.get(msg.get("action"))}'
+
+ return self._output(result)
+
+ def format(self, msg: fibmsg):
+ """
+ Formats a complete routing rule message.
+ Combines information about source, destination, interfaces, actions,
+ and other attributes into a single formatted string.
+ """
+ message = self._prepare_start_message(msg.get('event'))
+ host_len = self.af_bit_len(msg.get('family'))
+ message += self._format_simple_field(msg.get_attr('FRA_PRIORITY'), prefix='')
+
+ if msg.get('flags') & FIB_RULE_INVERT:
+ message += ' not'
+
+ tmp = self._format_direction(msg.get_attr('FRA_SRC'), msg.get('src_len'), host_len)
+ message += ' from' + (tmp if tmp else ' all ')
+
+ if msg.get_attr('FRA_DST'):
+ tmp = self._format_direction(msg.get_attr('FRA_DST'), msg.get('dst_len'), host_len)
+ message += ' to' + tmp
+
+ if msg.get('tos'):
+ message += f' tos {hex(msg.get("tos"))}'
+
+ if msg.get_attr('FRA_FWMARK') or msg.get_attr('FRA_FWMASK'):
+ mark = msg.get_attr('FRA_FWMARK') or 0
+ mask = msg.get_attr('FRA_FWMASK') or 0
+ if mask != 0xFFFFFFFF:
+ message += f' fwmark {mark}/{mask}'
+ else:
+ message += f' fwmark {mark}'
+
+ if msg.get_attr('FRA_IIFNAME'):
+ message += self._format_fra_interface(
+ msg.get_attr('FRA_IIFNAME'),
+ msg.get('flags'),
+ 'iif'
+ )
+
+ if msg.get_attr('FRA_OIFNAME'):
+ message += self._format_fra_interface(
+ msg.get_attr('FRA_OIFNAME'),
+ msg.get('flags'),
+ 'oif'
+ )
+
+ if msg.get_attr('FRA_L3MDEV'):
+ message += f' lookup [l3mdev-table]'
+
+ if msg.get_attr('FRA_UID_RANGE'):
+ message += self._format_fra_range(msg.get_attr('FRA_UID_RANGE'), 'uidrange')
+
+ message += self._format_simple_field(msg.get_attr('FRA_IP_PROTO'), prefix='ipproto')
+
+ if msg.get_attr('FRA_SPORT_RANGE'):
+ message += self._format_fra_range(msg.get_attr('FRA_SPORT_RANGE'), 'sport')
+
+ if msg.get_attr('FRA_DPORT_RANGE'):
+ message += self._format_fra_range(msg.get_attr('FRA_DPORT_RANGE'), 'dport')
+
+ message += self._format_simple_field(msg.get_attr('FRA_TUN_ID'), prefix='tun_id')
+
+ message += self._format_fra_table(msg)
+
+ if msg.get_attr('FRA_FLOW'):
+ message += self._format_flow_field(msg.get_attr('FRA_FLOW'))
+
+ message += self._format_fra_action(msg)
+
+ if msg.get_attr('FRA_PROTOCOL'):
+ message += f' proto {RT_PROTO.get(msg.get_attr("FRA_PROTOCOL"), msg.get_attr("FRA_PROTOCOL"))}'
+
+ return self._output(message)
+
+
+class AddrlabelFormatter(BaseMSGFormatter):
+ # Not implemented decoder on pytroute2 but ip monitor use it message
+ pass
+
+
+class PrefixFormatter(BaseMSGFormatter):
+ # Not implemented decoder on pytroute2 but ip monitor use it message
+ pass
+
+
+class NetconfFormatter(BaseMSGFormatter):
+ # Not implemented decoder on pytroute2 but ip monitor use it message
+ pass
+
+
+EVENT_MAP = {
+ rtnl.RTM_NEWROUTE: {'parser': RouteFormatter, 'event': 'route'},
+ rtnl.RTM_DELROUTE: {'parser': RouteFormatter, 'event': 'route'},
+ rtnl.RTM_NEWLINK: {'parser': LinkFormatter, 'event': 'link'},
+ rtnl.RTM_DELLINK: {'parser': LinkFormatter, 'event': 'link'},
+ rtnl.RTM_NEWADDR: {'parser': AddrFormatter, 'event': 'addr'},
+ rtnl.RTM_DELADDR: {'parser': AddrFormatter, 'event': 'addr'},
+ # rtnl.RTM_NEWADDRLABEL: {'parser': AddrlabelFormatter, 'event': 'addrlabel'},
+ # rtnl.RTM_DELADDRLABEL: {'parser': AddrlabelFormatter, 'event': 'addrlabel'},
+ rtnl.RTM_NEWNEIGH: {'parser': NeighFormatter, 'event': 'neigh'},
+ rtnl.RTM_DELNEIGH: {'parser': NeighFormatter, 'event': 'neigh'},
+ rtnl.RTM_GETNEIGH: {'parser': NeighFormatter, 'event': 'neigh'},
+ # rtnl.RTM_NEWPREFIX: {'parser': PrefixFormatter, 'event': 'prefix'},
+ rtnl.RTM_NEWRULE: {'parser': RuleFormatter, 'event': 'rule'},
+ rtnl.RTM_DELRULE: {'parser': RuleFormatter, 'event': 'rule'},
+ # rtnl.RTM_NEWNETCONF: {'parser': NetconfFormatter, 'event': 'netconf'},
+ # rtnl.RTM_DELNETCONF: {'parser': NetconfFormatter, 'event': 'netconf'},
+}
+
+
+def sig_handler(signum, frame):
+ process_name = multiprocessing.current_process().name
+ logger.debug(
+ f'[{process_name}]: {"Shutdown" if signum == signal.SIGTERM else "Reload"} signal received...'
+ )
+ shutdown_event.set()
+
+
+def parse_event_type(header: Dict) -> tuple:
+ """
+ Extract event type and parser.
+ """
+ event_type = EVENT_MAP.get(header['type'], {}).get('event', 'unknown')
+ _parser = EVENT_MAP.get(header['type'], {}).get('parser')
+
+ if _parser is None:
+ raise UnsupportedMessageType(f'Unsupported message type: {header["type"]}')
+
+ return event_type, _parser
+
+
+def is_need_to_log(event_type: AnyStr, conf_event: Dict):
+ """
+ Filter message by event type and protocols
+ """
+ conf = conf_event.get(event_type)
+ if conf == {}:
+ return True
+ return False
+
+
+def parse_event(msg: nfct_msg, conf_event: Dict) -> str:
+ """
+ Convert nfct_msg to internal data dict.
+ """
+ data = ''
+ event_type, parser = parse_event_type(msg['header'])
+ if event_type == 'link':
+ remember_if_index(idx=msg.get('index'), event_type=msg['header'].get('type'))
+
+ if not is_need_to_log(event_type, conf_event):
+ return data
+
+ message = parser().format(msg)
+ if message:
+ data = f'{f"[{event_type}]".upper():<{7}} {message}'
+
+ return data
+
+
+def worker(ct: IPRoute, shutdown_event: multiprocessing.Event, conf_event: Dict) -> None:
+ """
+ Main function of parser worker process
+ """
+ process_name = multiprocessing.current_process().name
+ logger.debug(f'[{process_name}] started')
+ timeout = 0.1
+ while not shutdown_event.is_set():
+ if not ct.buffer_queue.empty():
+ msg = None
+ try:
+ for msg in ct.get():
+ message = parse_event(msg, conf_event)
+ if message:
+ if logger.level == logging.DEBUG:
+ logger.debug(f'[{process_name}]: {message} raw: {msg}')
+ else:
+ logger.info(message)
+ except queue.Full:
+ logger.error('IPRoute message queue if full.')
+ except UnsupportedMessageType as e:
+ logger.debug(f'{e} =====> raw msg: {msg}')
+ except Exception as e:
+ logger.error(f'Unexpected error: {e.__class__} {e} [{msg}]')
+ else:
+ sleep(timeout)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '-c',
+ '--config',
+ action='store',
+ help='Path to vyos-network-event-logger configuration',
+ required=True,
+ type=Path,
+ )
+
+ args = parser.parse_args()
+ try:
+ config = read_json(args.config)
+ except Exception as err:
+ logger.error(f'Configuration file "{args.config}" does not exist or malformed: {err}')
+ exit(1)
+
+ set_log_level(config.get('log_level', 'info'))
+
+ signal.signal(signal.SIGHUP, sig_handler)
+ signal.signal(signal.SIGTERM, sig_handler)
+
+ if 'event' in config:
+ event_groups = list(config.get('event').keys())
+ else:
+ logger.error(f'Configuration is wrong. Event filter is empty.')
+ exit(1)
+
+ conf_event = config['event']
+ qsize = config.get('queue_size')
+ ct = IPRoute(async_qsize=int(qsize) if qsize else None)
+ ct.buffer_queue = multiprocessing.Queue(ct.async_qsize)
+ ct.bind(async_cache=True)
+
+ processes = list()
+ try:
+ for _ in range(multiprocessing.cpu_count()):
+ p = multiprocessing.Process(target=worker, args=(ct, shutdown_event, conf_event))
+ processes.append(p)
+ p.start()
+ logger.info('IPRoute socket bound and listening for messages.')
+
+ while not shutdown_event.is_set():
+ if not ct.pthread.is_alive():
+ if ct.buffer_queue.qsize() / ct.async_qsize < 0.9:
+ if not shutdown_event.is_set():
+ logger.debug('Restart listener thread')
+ # restart listener thread after queue overloaded when queue size low than 90%
+ ct.pthread = threading.Thread(name='Netlink async cache', target=ct.async_recv)
+ ct.pthread.daemon = True
+ ct.pthread.start()
+ else:
+ sleep(0.1)
+ finally:
+ for p in processes:
+ p.join()
+ if not p.is_alive():
+ logger.debug(f'[{p.name}]: finished')
+ ct.close()
+ logging.info('IPRoute socket closed.')
+ exit()
diff --git a/src/systemd/vyos-network-event-logger.service b/src/systemd/vyos-network-event-logger.service
new file mode 100644
index 0000000000..990dc43ba2
--- /dev/null
+++ b/src/systemd/vyos-network-event-logger.service
@@ -0,0 +1,21 @@
+[Unit]
+Description=VyOS network-event logger daemon
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=vyos.target
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-network-event-logger -c /run/vyos-network-event-logger.conf
+Type=idle
+
+SyslogIdentifier=vyos-network-event-logger
+SyslogFacility=daemon
+
+Restart=on-failure
+
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=multi-user.target