From eb4da01f919469c47ff9076dcf716121b8fafca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Tue, 3 Feb 2026 20:03:08 +0100 Subject: [PATCH 1/3] utils: bump-gns3: Fix syntax after qcow image was renamed --- utils/bump-gns3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/bump-gns3.py b/utils/bump-gns3.py index 5a9d41212..f0a6f9c32 100755 --- a/utils/bump-gns3.py +++ b/utils/bump-gns3.py @@ -82,7 +82,7 @@ def main(): return # Build qcow2 URL - filename = f"infix-x86_64-disk-{version}.qcow2" + filename = f"infix-x86_64-v{version}.qcow2" url = f"{REPO}/releases/download/v{version}/{filename}" print(f"Downloading {url} to compute MD5 and size...") From a5aec697c4adb3119cab19f62adfe93b8bb06160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Wed, 28 Jan 2026 08:47:55 +0100 Subject: [PATCH 2/3] Upgrade FRR to 10.5.1 This includes adding a netd-daemon since FRR has gone towards all daemons should be configured from mgmtd using YANG, netd uses gRPC to reconfigure in mgmtd --- board/common/rootfs/etc/default/mgmtd | 1 + .../rootfs/etc/finit.d/available/netd.conf | 3 + .../rootfs/etc/finit.d/enabled/netd.conf | 1 + board/common/rootfs/etc/netd/conf.d/.empty | 0 board/common/rootfs/usr/sbin/staticd-helper | 15 - .../rootfs/usr/share/udhcpc/default.script | 31 +- buildroot | 2 +- configs/aarch64_defconfig | 1 + configs/aarch64_minimal_defconfig | 1 + configs/arm_defconfig | 1 + configs/arm_minimal_defconfig | 1 + configs/riscv64_defconfig | 1 + configs/x86_64_defconfig | 1 + configs/x86_64_minimal_defconfig | 1 + doc/ChangeLog.md | 4 +- package/Config.in | 1 + package/netd/Config.in | 31 + package/netd/netd.conf | 3 + package/netd/netd.mk | 32 + .../skeleton-init-finit.mk | 5 +- .../skeleton/etc/default/ripd | 2 +- .../etc/finit.d/available/frr/mgmtd.conf | 2 + .../etc/finit.d/available/frr/staticd.conf | 5 +- .../etc/finit.d/available/frr/zebra.conf | 2 +- patches/frr/10.5.1/0001-Libyang4-compat.patch | 128 ++++ ...t-c-23-this-adds-compatibility-layer.patch | 127 ++++ patches/frr/9.1.3/0001-libyang-compat.patch | 149 ----- ...aticd-Re-enable-split-config-support.patch | 30 - src/confd/src/routing.c | 476 ++++++--------- src/netd/LICENSE | 27 + src/netd/Makefile.am | 34 ++ src/netd/README.md | 361 +++++++++++ src/netd/autogen.sh | 3 + src/netd/configure.ac | 88 +++ src/netd/grpc/frr-northbound.proto | 423 +++++++++++++ src/netd/netd.conf | 52 ++ src/netd/src/config.c | 384 ++++++++++++ src/netd/src/config.h | 10 + src/netd/src/grpc_backend.cc | 194 ++++++ src/netd/src/grpc_backend.h | 18 + src/netd/src/json_builder.c | 563 ++++++++++++++++++ src/netd/src/json_builder.h | 18 + src/netd/src/linux_backend.c | 452 ++++++++++++++ src/netd/src/linux_backend.h | 16 + src/netd/src/netd.c | 273 +++++++++ src/netd/src/netd.h | 126 ++++ 46 files changed, 3596 insertions(+), 503 deletions(-) create mode 100644 board/common/rootfs/etc/default/mgmtd create mode 100644 board/common/rootfs/etc/finit.d/available/netd.conf create mode 120000 board/common/rootfs/etc/finit.d/enabled/netd.conf create mode 100644 board/common/rootfs/etc/netd/conf.d/.empty delete mode 100755 board/common/rootfs/usr/sbin/staticd-helper create mode 100644 package/netd/Config.in create mode 100644 package/netd/netd.conf create mode 100644 package/netd/netd.mk create mode 100644 package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf create mode 100644 patches/frr/10.5.1/0001-Libyang4-compat.patch create mode 100644 patches/frr/10.5.1/0002-Failed-without-c-23-this-adds-compatibility-layer.patch delete mode 100644 patches/frr/9.1.3/0001-libyang-compat.patch delete mode 100644 patches/frr/9.1.3/0002-staticd-Re-enable-split-config-support.patch create mode 100644 src/netd/LICENSE create mode 100644 src/netd/Makefile.am create mode 100644 src/netd/README.md create mode 100755 src/netd/autogen.sh create mode 100644 src/netd/configure.ac create mode 100644 src/netd/grpc/frr-northbound.proto create mode 100644 src/netd/netd.conf create mode 100644 src/netd/src/config.c create mode 100644 src/netd/src/config.h create mode 100644 src/netd/src/grpc_backend.cc create mode 100644 src/netd/src/grpc_backend.h create mode 100644 src/netd/src/json_builder.c create mode 100644 src/netd/src/json_builder.h create mode 100644 src/netd/src/linux_backend.c create mode 100644 src/netd/src/linux_backend.h create mode 100644 src/netd/src/netd.c create mode 100644 src/netd/src/netd.h diff --git a/board/common/rootfs/etc/default/mgmtd b/board/common/rootfs/etc/default/mgmtd new file mode 100644 index 000000000..3b1b4e1d1 --- /dev/null +++ b/board/common/rootfs/etc/default/mgmtd @@ -0,0 +1 @@ +MGMTD_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog --log-level err -M grpc" diff --git a/board/common/rootfs/etc/finit.d/available/netd.conf b/board/common/rootfs/etc/finit.d/available/netd.conf new file mode 100644 index 000000000..e61019c77 --- /dev/null +++ b/board/common/rootfs/etc/finit.d/available/netd.conf @@ -0,0 +1,3 @@ +#set DEBUG=1 + +service name:netd log [S12345] netd -- Network route daemon diff --git a/board/common/rootfs/etc/finit.d/enabled/netd.conf b/board/common/rootfs/etc/finit.d/enabled/netd.conf new file mode 120000 index 000000000..7afbfcdef --- /dev/null +++ b/board/common/rootfs/etc/finit.d/enabled/netd.conf @@ -0,0 +1 @@ +../available/netd.conf \ No newline at end of file diff --git a/board/common/rootfs/etc/netd/conf.d/.empty b/board/common/rootfs/etc/netd/conf.d/.empty new file mode 100644 index 000000000..e69de29bb diff --git a/board/common/rootfs/usr/sbin/staticd-helper b/board/common/rootfs/usr/sbin/staticd-helper deleted file mode 100755 index 744b2cd3f..000000000 --- a/board/common/rootfs/usr/sbin/staticd-helper +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/sh -# Sort and collate all /etc/frr/static.d/*.conf files managed by confd, -# udhcpc, and avahi-autoipd before starting staticd. - -DIRD=/etc/frr/static.d -NAME=/etc/frr/staticd.conf -NEXT=${NAME}+ - -rm -f "$NEXT" -find "$DIRD" -type f -name '*.conf' ! -name '*~' | sort | while read -r file; do - cat "$file" >> "$NEXT" -done - -cmp -s "$NAME" "$NEXT" && exit 0 -mv "$NEXT" "$NAME" diff --git a/board/common/rootfs/usr/share/udhcpc/default.script b/board/common/rootfs/usr/share/udhcpc/default.script index 84221f813..ecf48f0ed 100755 --- a/board/common/rootfs/usr/share/udhcpc/default.script +++ b/board/common/rootfs/usr/share/udhcpc/default.script @@ -7,7 +7,7 @@ ACTION="$1" IP_CACHE="/var/lib/misc/${interface}.cache" RESOLV_CONF="/run/resolvconf/interfaces/${interface}.conf" NTPFILE="/run/chrony/dhcp-sources.d/${interface}.sources" -NAME="/etc/frr/static.d/${interface}-dhcp.conf" +NAME="/etc/netd/conf.d/${interface}-dhcp.conf" NEXT="${NAME}+" [ -n "$broadcast" ] && BROADCAST="broadcast $broadcast" @@ -75,14 +75,23 @@ was_option_requested() # client MUST ignore the Router option. set_dhcp_routes() { - echo "! Generated by udhcpc" > "$NEXT" + echo "# Generated by udhcpc" > "$NEXT" + echo "" >> "$NEXT" if [ -n "$staticroutes" ]; then if was_option_requested 121; then # format: dest1/mask gw1 ... destn/mask gwn set -- $staticroutes while [ -n "$1" -a -n "$2" ]; do dbg "adding route $1 via $2 metric $metric tag 100" - echo "ip route $1 $2 $metric tag 100" >> "$NEXT" + cat >> "$NEXT" <<-EOF + route { + prefix = "$1" + nexthop = "$2" + distance = $metric + tag = 100 + } + + EOF shift 2 done else @@ -91,7 +100,15 @@ set_dhcp_routes() elif [ -n "$router" ] ; then if was_option_requested 3; then for i in $router ; do - echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT" + cat >> "$NEXT" <<-EOF + route { + prefix = "0.0.0.0/0" + nexthop = "$i" + distance = $metric + tag = 100 + } + + EOF done else log "ignoring unrequested router (option 3)" @@ -102,7 +119,8 @@ set_dhcp_routes() cmp -s "$NAME" "$NEXT" && return mv "$NEXT" "$NAME" - initctl -nbq restart staticd + rm /run/netd.pid + initctl reload netd } clr_dhcp_routes() @@ -111,7 +129,8 @@ clr_dhcp_routes() [ -f "$NAME" ] || return rm "$NAME" - initctl -nbq restart staticd + rm /run/netd.pid + initctl reload netd } clr_dhcp_addresses() diff --git a/buildroot b/buildroot index c3390cbf0..9cdb4ab54 160000 --- a/buildroot +++ b/buildroot @@ -1 +1 @@ -Subproject commit c3390cbf09f6f596ce9d3171365bd6e0aac09066 +Subproject commit 9cdb4ab54bb1c7f37c976c93328a74baa7163a5f diff --git a/configs/aarch64_defconfig b/configs/aarch64_defconfig index ccc59f03d..7bd4ca88c 100644 --- a/configs/aarch64_defconfig +++ b/configs/aarch64_defconfig @@ -149,6 +149,7 @@ INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_CURIOS_HTTPD=y BR2_PACKAGE_CURIOS_NFTABLES=y diff --git a/configs/aarch64_minimal_defconfig b/configs/aarch64_minimal_defconfig index c8f5f3211..19d2bd52d 100644 --- a/configs/aarch64_minimal_defconfig +++ b/configs/aarch64_minimal_defconfig @@ -125,6 +125,7 @@ INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y diff --git a/configs/arm_defconfig b/configs/arm_defconfig index 20e286d07..b0f2ef892 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -145,6 +145,7 @@ INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y diff --git a/configs/arm_minimal_defconfig b/configs/arm_minimal_defconfig index 98a211e98..bb25eaf6d 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_minimal_defconfig @@ -125,6 +125,7 @@ INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y diff --git a/configs/riscv64_defconfig b/configs/riscv64_defconfig index f496f4b46..6fbcb8168 100644 --- a/configs/riscv64_defconfig +++ b/configs/riscv64_defconfig @@ -177,6 +177,7 @@ BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y BR2_PACKAGE_FACTORY=y diff --git a/configs/x86_64_defconfig b/configs/x86_64_defconfig index 431b28397..b381d4ab1 100644 --- a/configs/x86_64_defconfig +++ b/configs/x86_64_defconfig @@ -149,6 +149,7 @@ BR2_PACKAGE_FEATURE_WIFI=y BR2_PACKAGE_FEATURE_WIFI_MEDIATEK=y BR2_PACKAGE_FEATURE_WIFI_REALTEK=y BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_CURIOS_HTTPD=y BR2_PACKAGE_CURIOS_NFTABLES=y diff --git a/configs/x86_64_minimal_defconfig b/configs/x86_64_minimal_defconfig index 3398654c3..763c8ff24 100644 --- a/configs/x86_64_minimal_defconfig +++ b/configs/x86_64_minimal_defconfig @@ -124,6 +124,7 @@ INFIX_HOME="https://github.com/kernelkit/infix/" INFIX_DOC="https://kernelkit.org/infix/" INFIX_SUPPORT="mailto:kernelkit@googlegroups.com" BR2_PACKAGE_CONFD=y +BR2_PACKAGE_NETD=y BR2_PACKAGE_CONFD_TEST_MODE=y BR2_PACKAGE_GENCERT=y BR2_PACKAGE_STATD=y diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index bdfbab2f9..619890073 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -3,18 +3,20 @@ Change Log All notable changes to the project are documented in this file. -[v26.01.0][UNRELEASED] +[v26.02.0][UNRELEASED] - ------------------------- ### Changes - Upgrade Linux kernel to 6.18.9 (LTS) +- Upgrade FRR to 10.5.1 - Add support for Microchip SAMA7G54-EK Evaluation Kit, Arm Cortex-A7 ### Fixes N/A + [v26.01.0][] - 2026-02-03 ------------------------- diff --git a/package/Config.in b/package/Config.in index 71cf73cc7..ed4c43c3f 100644 --- a/package/Config.in +++ b/package/Config.in @@ -6,6 +6,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/feature-wifi/Config.in" comment "Software Packages" source "$BR2_EXTERNAL_INFIX_PATH/package/bin/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/confd/Config.in" +source "$BR2_EXTERNAL_INFIX_PATH/package/netd/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/confd-test-mode/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/curios-httpd/Config.in" source "$BR2_EXTERNAL_INFIX_PATH/package/curios-nftables/Config.in" diff --git a/package/netd/Config.in b/package/netd/Config.in new file mode 100644 index 000000000..a1af11fe0 --- /dev/null +++ b/package/netd/Config.in @@ -0,0 +1,31 @@ +config BR2_PACKAGE_NETD + bool "netd" + select BR2_PACKAGE_LIBITE + select BR2_PACKAGE_LIBCONFUSE + help + Network route daemon. Manages static routes and RIP routing. + Reads configuration from /etc/netd/conf.d/*.conf. + + With FRR: Full routing via gRPC (static routes, RIP, OSPF). + Without FRR: Standalone Linux backend via rtnetlink. + + https://github.com/kernelkit/infix + +if BR2_PACKAGE_NETD + +config BR2_PACKAGE_NETD_FRR + bool "FRR integration" + default y if BR2_PACKAGE_FRR + depends on BR2_PACKAGE_FRR + select BR2_PACKAGE_PROTOBUF + select BR2_PACKAGE_GRPC + select BR2_PACKAGE_HOST_PROTOBUF + select BR2_PACKAGE_HOST_GRPC + help + Enable FRR integration via gRPC northbound API. + Provides full routing support (static routes, RIP, OSPF). + + If disabled, netd uses Linux kernel backend (rtnetlink) + with static routes only. + +endif diff --git a/package/netd/netd.conf b/package/netd/netd.conf new file mode 100644 index 000000000..377c96a58 --- /dev/null +++ b/package/netd/netd.conf @@ -0,0 +1,3 @@ +#set DEBUG=1 + +service name:netd log [S12345] netd -p /run/netd.pid -- Network route daemon diff --git a/package/netd/netd.mk b/package/netd/netd.mk new file mode 100644 index 000000000..3f7f76efb --- /dev/null +++ b/package/netd/netd.mk @@ -0,0 +1,32 @@ +################################################################################ +# +# netd +# +################################################################################ + +NETD_VERSION = 1.0 +NETD_SITE_METHOD = local +NETD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/netd +NETD_LICENSE = BSD-3-Clause +NETD_LICENSE_FILES = LICENSE +NETD_REDISTRIBUTE = NO +NETD_DEPENDENCIES = libite libconfuse jansson +NETD_AUTORECONF = YES + +NETD_CONF_ENV = CFLAGS="$(INFIX_CFLAGS)" + +NETD_CONF_OPTS = --prefix= --disable-silent-rules + +# FRR integration (gRPC backend) or standalone Linux backend +ifeq ($(BR2_PACKAGE_NETD_FRR),y) +NETD_DEPENDENCIES += frr grpc host-grpc protobuf +NETD_CONF_ENV += \ + PROTOC="$(HOST_DIR)/bin/protoc" \ + GRPC_CPP_PLUGIN="$(HOST_DIR)/bin/grpc_cpp_plugin" +else +NETD_CONF_OPTS += --without-frr +endif + +NETD_TARGET_FINALIZE_HOOKS += NETD_INSTALL_EXTRA + +$(eval $(autotools-package)) diff --git a/package/skeleton-init-finit/skeleton-init-finit.mk b/package/skeleton-init-finit/skeleton-init-finit.mk index 8c1f8da47..7e9e55586 100644 --- a/package/skeleton-init-finit/skeleton-init-finit.mk +++ b/package/skeleton-init-finit/skeleton-init-finit.mk @@ -86,11 +86,12 @@ endif ifeq ($(BR2_PACKAGE_FRR),y) define SKELETON_INIT_FINIT_SET_FRR - for svc in babeld bfdd bgpd eigrpd isisd ldpd ospfd ospf6d pathd ripd ripng staticd vrrpd zebra; do \ + for svc in babeld bfdd bgpd mgmtd eigrpd isisd ldpd ospfd ospf6d pathd ripd ripng staticd vrrpd zebra; do \ cp $(SKELETON_INIT_FINIT_AVAILABLE)/frr/$$svc.conf $(FINIT_D)/available/$$svc.conf; \ done - ln -sf ../available/staticd.conf $(FINIT_D)/enabled/staticd.conf ln -sf ../available/zebra.conf $(FINIT_D)/enabled/zebra.conf + ln -sf ../available/staticd.conf $(FINIT_D)/enabled/staticd.conf + ln -sf ../available/mgmtd.conf $(FINIT_D)/enabled/mgmtd.conf endef SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_FRR endif # BR2_PACKAGE_FRR diff --git a/package/skeleton-init-finit/skeleton/etc/default/ripd b/package/skeleton-init-finit/skeleton/etc/default/ripd index c8d2b4f4f..48b63f915 100644 --- a/package/skeleton-init-finit/skeleton/etc/default/ripd +++ b/package/skeleton-init-finit/skeleton/etc/default/ripd @@ -1,2 +1,2 @@ # --log-level debug -RIPD_ARGS="-A 127.0.0.1 -u frr -g frr -f /etc/frr/ripd.conf --log syslog" +RIPD_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog" diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf new file mode 100644 index 000000000..825aff9cc --- /dev/null +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/mgmtd.conf @@ -0,0 +1,2 @@ +service pid:!/run/frr/mgmtd.pid env:-/etc/default/mgmtd \ + [2345] mgmtd $MGMTD_ARGS -- FRR MGMT daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf index e3af6b5f1..0016ef901 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/staticd.conf @@ -1,4 +1,3 @@ -# Run staticd-helper to collate /etc/static.d/*.conf before starting staticd -service log:null pre:/usr/sbin/staticd-helper \ - [2345] staticd -A 127.0.0.1 -u frr -g frr -f /etc/frr/staticd.conf \ +service log:null \ + [2345] staticd -A 127.0.0.1 -u frr -g frr \ -- Static routing daemon diff --git a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf index 45d394848..8699e09a2 100644 --- a/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf +++ b/package/skeleton-init-finit/skeleton/etc/finit.d/available/frr/zebra.conf @@ -1,2 +1,2 @@ -service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \ +service pid:!/run/frr/zebra.pid env:-/etc/default/zebra \ [2345] zebra $ZEBRA_ARGS -- Zebra routing daemon diff --git a/patches/frr/10.5.1/0001-Libyang4-compat.patch b/patches/frr/10.5.1/0001-Libyang4-compat.patch new file mode 100644 index 000000000..a4c985381 --- /dev/null +++ b/patches/frr/10.5.1/0001-Libyang4-compat.patch @@ -0,0 +1,128 @@ +From 597cf064f6076e3859f72b0da4dc0ab98ca2e1d2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Tue, 27 Jan 2026 22:54:59 +0100 +Subject: [PATCH 1/2] Libyang4 compat +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +libyang4 had breaking changes needs to be adapded for it. + +Signed-off-by: Mattias Walström +--- + lib/northbound.c | 7 ++++++- + lib/yang.c | 24 +++++++++++++++++++++--- + lib/yang.h | 6 ++++++ + 3 files changed, 33 insertions(+), 4 deletions(-) + +diff --git a/lib/northbound.c b/lib/northbound.c +index c21436804f..4de63f9b2d 100644 +--- a/lib/northbound.c ++++ b/lib/northbound.c +@@ -2299,7 +2299,12 @@ bool nb_cb_operation_is_valid(enum nb_cb_operation operation, + if (sleaf->when) + return true; + if (CHECK_FLAG(sleaf->flags, LYS_MAND_TRUE) +- || sleaf->dflt) ++#if (LY_VERSION_MAJOR < 4) ++ || sleaf->dflt ++#else ++ || sleaf->dflt.str ++#endif ++ ) + return false; + break; + case LYS_CONTAINER: +diff --git a/lib/yang.c b/lib/yang.c +index a8f66dce6e..aef0468a68 100644 +--- a/lib/yang.c ++++ b/lib/yang.c +@@ -401,9 +401,13 @@ const char *yang_snode_get_default(const struct lysc_node *snode) + switch (snode->nodetype) { + case LYS_LEAF: + sleaf = (const struct lysc_node_leaf *)snode; +- return sleaf->dflt ? lyd_value_get_canonical(sleaf->module->ctx, +- sleaf->dflt) ++#if (LY_VERSION_MAJOR < 4) ++ return sleaf->dflt ? lyd_value_get_canonical(sleaf->module->ctx, sleaf->dflt) + : NULL; ++#else ++ /* NOTE: this is value in the schema, not necessarily the canonical form */ ++ return sleaf->dflt.str; ++#endif + case LYS_LEAFLIST: + /* TODO: check leaf-list default values */ + return NULL; +@@ -954,6 +958,9 @@ LY_ERR yang_parse_notification(const char *xpath, LYD_FORMAT format, + } + + err = lyd_parse_op(ly_native_ctx, NULL, in, format, LYD_TYPE_NOTIF_YANG, ++#if (LY_VERSION_MAJOR >= 4) ++ LYD_PARSE_LYB_SKIP_CTX_CHECK /* parse_options */, ++#endif + &tree, NULL); + ly_in_free(in, 0); + if (err) { +@@ -1025,6 +1032,9 @@ LY_ERR yang_parse_rpc(const char *xpath, LYD_FORMAT format, const char *data, + + err = lyd_parse_op(ly_native_ctx, parent, in, format, + reply ? LYD_TYPE_REPLY_YANG : LYD_TYPE_RPC_YANG, ++#if (LY_VERSION_MAJOR >= 4) ++ LYD_PARSE_LYB_SKIP_CTX_CHECK /* parse_options */, ++#endif + NULL, rpc); + ly_in_free(in, 0); + if (err) { +@@ -1072,6 +1082,7 @@ char *yang_convert_lyd_format(const char *data, size_t data_len, + bool shrink) + { + struct lyd_node *tree = NULL; ++ uint32_t parse_options = LYD_PARSE_ONLY; + uint32_t options = LYD_PRINT_WD_EXPLICIT | LYD_PRINT_WITHSIBLINGS; + uint8_t *result = NULL; + LY_ERR err; +@@ -1086,8 +1097,12 @@ char *yang_convert_lyd_format(const char *data, size_t data_len, + if (in_format == out_format) + return darr_strdup((const char *)data); + ++#ifdef LYD_PARSE_LYB_SKIP_CTX_CHECK ++ if (in_format == LYD_LYB) ++ parse_options |= LYD_PARSE_LYB_SKIP_CTX_CHECK; ++#endif + err = lyd_parse_data_mem(ly_native_ctx, (const char *)data, in_format, +- LYD_PARSE_ONLY, 0, &tree); ++ parse_options, 0, &tree); + + if (err) { + flog_err_sys(EC_LIB_LIBYANG, +@@ -1171,6 +1186,9 @@ struct ly_ctx *yang_ctx_new_setup(bool embedded_modules, bool explicit_compile, + } + + options = LY_CTX_DISABLE_SEARCHDIR_CWD; ++#if (LY_VERSION_MAJOR >= 4) ++ options |= LY_CTX_LYB_HASHES; ++#endif + if (!load_library) + options |= LY_CTX_NO_YANGLIBRARY; + if (explicit_compile) +diff --git a/lib/yang.h b/lib/yang.h +index 3877a421c5..7942573158 100644 +--- a/lib/yang.h ++++ b/lib/yang.h +@@ -16,6 +16,12 @@ + + #include "yang_wrappers.h" + ++/* libyang4 renamed LYD_PRINT_WITHSIBLINGS to LYD_PRINT_SIBLINGS */ ++#include ++#if (LY_VERSION_MAJOR >= 4) ++#define LYD_PRINT_WITHSIBLINGS LYD_PRINT_SIBLINGS ++#endif ++ + #ifdef __cplusplus + extern "C" { + #endif +-- +2.43.0 + diff --git a/patches/frr/10.5.1/0002-Failed-without-c-23-this-adds-compatibility-layer.patch b/patches/frr/10.5.1/0002-Failed-without-c-23-this-adds-compatibility-layer.patch new file mode 100644 index 000000000..352dae043 --- /dev/null +++ b/patches/frr/10.5.1/0002-Failed-without-c-23-this-adds-compatibility-layer.patch @@ -0,0 +1,127 @@ +From 859bc23f318cfa019b104e83591f185f5bcc3bd4 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= +Date: Fri, 30 Jan 2026 13:00:12 +0100 +Subject: [PATCH 2/2] Failed without c++ 23, this adds compatibility layer +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Organization: Wires + +Signed-off-by: Mattias Walström +--- + lib/assert/assert.h | 29 +++++++++++++++++++++++++++++ + lib/zlog.c | 45 +++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 74 insertions(+) + +diff --git a/lib/assert/assert.h b/lib/assert/assert.h +index 97c7460079..8fe8b10c05 100644 +--- a/lib/assert/assert.h ++++ b/lib/assert/assert.h +@@ -41,12 +41,25 @@ extern void _zlog_assert_failed(const struct xref_assert *xref, + const char *extra, ...) PRINTFRR(2, 3) + __attribute__((noreturn)); + ++#ifdef __cplusplus ++/* C++ helper functions for assert without xref (to work in constexpr) */ ++extern void _zlog_assert_failed_cpp(const char *file, int line, ++ const char *func, const char *expr) ++ __attribute__((noreturn)); ++extern void _zlog_assert_failed_cpp_fmt(const char *file, int line, ++ const char *func, const char *expr, ++ const char *extra, ...) ++ PRINTFRR(5, 6) __attribute__((noreturn)); ++#endif ++ + /* the "do { } while (expr_)" is there to get a warning for assignments inside + * the assert expression aka "assert(x = 1)". The (necessary) braces around + * expr_ in the if () statement would suppress these warnings. Since + * _zlog_assert_failed() is noreturn, the while condition will never be + * checked. + */ ++#ifndef __cplusplus ++/* C version with full xref tracking */ + #define assert(expr_) \ + ({ \ + static const struct xref_assert _xref __attribute__( \ +@@ -77,6 +90,22 @@ extern void _zlog_assert_failed(const struct xref_assert *xref, + ##__VA_ARGS__); \ + } while (expr_); \ + }) ++#else ++/* C++ version without xref tracking to allow use in constexpr contexts. ++ * Static variables in constexpr functions are only allowed in C++23, but ++ * we need to support earlier standards for compatibility with libraries ++ * like Abseil that use assert() in constexpr functions. ++ */ ++#define assert(expr_) \ ++ ((expr_) ? (void)0 \ ++ : _zlog_assert_failed_cpp(__FILE__, __LINE__, __func__, \ ++ #expr_)) ++ ++#define assertf(expr_, extra_, ...) \ ++ ((expr_) ? (void)0 \ ++ : _zlog_assert_failed_cpp_fmt(__FILE__, __LINE__, __func__, \ ++ #expr_, extra_, ##__VA_ARGS__)) ++#endif + + #define zassert assert + +diff --git a/lib/zlog.c b/lib/zlog.c +index 157f3323cb..7e7b6f0c25 100644 +--- a/lib/zlog.c ++++ b/lib/zlog.c +@@ -789,6 +789,51 @@ void _zlog_assert_failed(const struct xref_assert *xref, const char *extra, ...) + abort(); + } + ++/* C++ versions without xref struct - used to avoid static variables in ++ * constexpr contexts (C++23 requirement) ++ */ ++void _zlog_assert_failed_cpp(const char *file, int line, const char *func, ++ const char *expr) ++{ ++ static bool assert_in_assert; /* "global-ish" variable, init to 0 */ ++ ++ if (assert_in_assert) ++ abort(); ++ assert_in_assert = true; ++ ++ zlog(LOG_CRIT, "%s:%d: %s(): assertion (%s) failed", file, line, func, ++ expr); ++ ++ /* abort() prints backtrace & memstats in SIGABRT handler */ ++ abort(); ++} ++ ++void _zlog_assert_failed_cpp_fmt(const char *file, int line, const char *func, ++ const char *expr, const char *extra, ...) ++{ ++ va_list ap; ++ static bool assert_in_assert; /* "global-ish" variable, init to 0 */ ++ ++ if (assert_in_assert) ++ abort(); ++ assert_in_assert = true; ++ ++ struct va_format vaf; ++ ++ va_start(ap, extra); ++ vaf.fmt = extra; ++ vaf.va = ≈ ++ ++ zlog(LOG_CRIT, ++ "%s:%d: %s(): assertion (%s) failed, extra info: %pVA", file, ++ line, func, expr, &vaf); ++ ++ va_end(ap); ++ ++ /* abort() prints backtrace & memstats in SIGABRT handler */ ++ abort(); ++} ++ + int zlog_msg_prio(struct zlog_msg *msg) + { + return msg->prio; +-- +2.43.0 + diff --git a/patches/frr/9.1.3/0001-libyang-compat.patch b/patches/frr/9.1.3/0001-libyang-compat.patch deleted file mode 100644 index ebb5b950d..000000000 --- a/patches/frr/9.1.3/0001-libyang-compat.patch +++ /dev/null @@ -1,149 +0,0 @@ -diff --git a/lib/northbound.c b/lib/northbound.c -index 6ff5c24bd1..c6cca523b2 100644 ---- a/lib/northbound.c -+++ b/lib/northbound.c -@@ -581,7 +581,7 @@ void nb_config_diff(const struct nb_config *config1, - char *s; - - if (!lyd_print_mem(&s, diff, LYD_JSON, -- LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL)) { -+ LYD_PRINT_SIBLINGS | LYD_PRINT_WD_ALL)) { - zlog_debug("%s: %s", __func__, s); - free(s); - } -@@ -2283,7 +2283,7 @@ bool nb_operation_is_valid(enum nb_operation operation, - if (sleaf->when) - return true; - if (CHECK_FLAG(sleaf->flags, LYS_MAND_TRUE) -- || sleaf->dflt) -+ || sleaf->dflt.str) - return false; - break; - case LYS_CONTAINER: -diff --git a/lib/northbound_cli.c b/lib/northbound_cli.c -index 8003679ed5..0997b5f8c5 100644 ---- a/lib/northbound_cli.c -+++ b/lib/northbound_cli.c -@@ -634,7 +634,7 @@ static int nb_cli_show_config_libyang(struct vty *vty, LYD_FORMAT format, - return CMD_WARNING; - } - -- SET_FLAG(options, LYD_PRINT_WITHSIBLINGS); -+ SET_FLAG(options, LYD_PRINT_SIBLINGS); - if (with_defaults) - SET_FLAG(options, LYD_PRINT_WD_ALL); - else -@@ -1443,7 +1443,7 @@ DEFPY (show_yang_operational_data, - struct ly_ctx *ly_ctx; - struct lyd_node *dnode; - char *strp; -- uint32_t print_options = LYD_PRINT_WITHSIBLINGS; -+ uint32_t print_options = LYD_PRINT_SIBLINGS; - int ret; - - if (xml) -diff --git a/lib/northbound_db.c b/lib/northbound_db.c -index 74abcde955..7d51f39291 100644 ---- a/lib/northbound_db.c -+++ b/lib/northbound_db.c -@@ -79,7 +79,7 @@ int nb_db_transaction_save(const struct nb_transaction *transaction, - * values too, as this covers the case where defaults may change. - */ - if (lyd_print_mem(&config_str, transaction->config->dnode, LYD_XML, -- LYD_PRINT_WITHSIBLINGS | LYD_PRINT_WD_ALL) -+ LYD_PRINT_SIBLINGS | LYD_PRINT_WD_ALL) - != 0) - goto exit; - -diff --git a/lib/northbound_grpc.cpp b/lib/northbound_grpc.cpp -index 6c33351cef..b3e14828f3 100644 ---- a/lib/northbound_grpc.cpp -+++ b/lib/northbound_grpc.cpp -@@ -370,7 +370,7 @@ static LY_ERR data_tree_from_dnode(frr::DataTree *dt, - char *strp; - int options = 0; - -- SET_FLAG(options, LYD_PRINT_WITHSIBLINGS); -+ SET_FLAG(options, LYD_PRINT_SIBLINGS); - if (with_defaults) - SET_FLAG(options, LYD_PRINT_WD_ALL); - else -diff --git a/lib/yang.c b/lib/yang.c -index 4dd8654217..44563d4922 100644 ---- a/lib/yang.c -+++ b/lib/yang.c -@@ -10,11 +10,23 @@ - #include "lib_errors.h" - #include "yang.h" - #include "yang_translator.h" -+#include - #include "northbound.h" - - DEFINE_MTYPE_STATIC(LIB, YANG_MODULE, "YANG module"); - DEFINE_MTYPE_STATIC(LIB, YANG_DATA, "YANG data structure"); - -+/* Safe to remove after libyang 2.2.8 */ -+#if (LY_VERSION_MAJOR < 3) -+#define yang_lyd_find_xpath3(ctx_node, tree, xpath, format, prefix_data, vars, \ -+ set) \ -+ lyd_find_xpath3(ctx_node, tree, xpath, vars, set) -+#else -+#define yang_lyd_find_xpath3(ctx_node, tree, xpath, format, prefix_data, vars, \ -+ set) \ -+ lyd_find_xpath3(ctx_node, tree, xpath, LY_VALUE_JSON, NULL, vars, set) -+#endif -+ - /* libyang container. */ - struct ly_ctx *ly_native_ctx; - -@@ -329,9 +341,7 @@ const char *yang_snode_get_default(const struct lysc_node *snode) - switch (snode->nodetype) { - case LYS_LEAF: - sleaf = (const struct lysc_node_leaf *)snode; -- return sleaf->dflt ? lyd_value_get_canonical(sleaf->module->ctx, -- sleaf->dflt) -- : NULL; -+ return sleaf->dflt.str; - case LYS_LEAFLIST: - /* TODO: check leaf-list default values */ - return NULL; -@@ -657,7 +667,12 @@ struct yang_data *yang_data_list_find(const struct list *list, - } - - /* Make libyang log its errors using FRR logging infrastructure. */ --static void ly_log_cb(LY_LOG_LEVEL level, const char *msg, const char *path) -+static void ly_zlog_cb(LY_LOG_LEVEL level, const char *msg, const char *data_path -+#if !(LY_VERSION_MAJOR < 3) -+ , -+ const char *schema_path, uint64_t line -+#endif -+) - { - int priority = LOG_ERR; - -@@ -674,8 +689,14 @@ static void ly_log_cb(LY_LOG_LEVEL level, const char *msg, const char *path) - break; - } - -- if (path) -- zlog(priority, "libyang: %s (%s)", msg, path); -+ if (data_path) -+ zlog(priority, "libyang: %s (%s)", msg, data_path); -+#if !(LY_VERSION_MAJOR < 3) -+ else if (schema_path) -+ zlog(priority, "libyang %s (%s)\n", msg, schema_path); -+ else if (line) -+ zlog(priority, "libyang %s (line %" PRIu64 ")\n", msg, line); -+#endif - else - zlog(priority, "libyang: %s", msg); - } -@@ -752,7 +773,7 @@ struct ly_ctx *yang_ctx_new_setup(bool embedded_modules, bool explicit_compile) - void yang_init(bool embedded_modules, bool defer_compile) - { - /* Initialize libyang global parameters that affect all containers. */ -- ly_set_log_clb(ly_log_cb, 1); -+ ly_set_log_clb(ly_zlog_cb); - ly_log_options(LY_LOLOG | LY_LOSTORE); - - /* Initialize libyang container for native models. */ diff --git a/patches/frr/9.1.3/0002-staticd-Re-enable-split-config-support.patch b/patches/frr/9.1.3/0002-staticd-Re-enable-split-config-support.patch deleted file mode 100644 index 923016233..000000000 --- a/patches/frr/9.1.3/0002-staticd-Re-enable-split-config-support.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 5f37809521acda432d77aa4028b74c5713c2d988 Mon Sep 17 00:00:00 2001 -From: Tobias Waldekranz -Date: Wed, 20 Nov 2024 15:53:21 +0100 -Subject: [PATCH 2/2] staticd: Re-enable split config support -Organization: Addiva Elektronik - -Because we can. - -Signed-off-by: Tobias Waldekranz ---- - staticd/static_main.c | 3 +-- - 1 file changed, 1 insertion(+), 2 deletions(-) - -diff --git a/staticd/static_main.c b/staticd/static_main.c -index 165fb4d65..59e924c83 100644 ---- a/staticd/static_main.c -+++ b/staticd/static_main.c -@@ -128,8 +128,7 @@ FRR_DAEMON_INFO(staticd, STATIC, .vty_port = STATIC_VTY_PORT, - - .privs = &static_privs, .yang_modules = staticd_yang_modules, - .n_yang_modules = array_size(staticd_yang_modules), -- -- .flags = FRR_NO_SPLIT_CONFIG); -+ ); - - int main(int argc, char **argv, char **envp) - { --- -2.43.0 - diff --git a/src/confd/src/routing.c b/src/confd/src/routing.c index e9110e42a..3a881b677 100644 --- a/src/confd/src/routing.c +++ b/src/confd/src/routing.c @@ -8,17 +8,16 @@ #define XPATH_BASE_ "/ietf-routing:routing/control-plane-protocols/control-plane-protocol" #define XPATH_OSPF_ XPATH_BASE_ "/ietf-ospf:ospf" -#define STATICD_CONF "/etc/frr/static.d/confd.conf" -#define STATICD_CONF_NEXT STATICD_CONF "+" -#define STATICD_CONF_PREV STATICD_CONF "-" +#define NETD_CONF "/etc/netd/conf.d/confd.conf" +#define NETD_CONF_NEXT NETD_CONF "+" +#define NETD_CONF_PREV NETD_CONF "-" #define OSPFD_CONF "/etc/frr/ospfd.conf" #define OSPFD_CONF_NEXT OSPFD_CONF "+" #define OSPFD_CONF_PREV OSPFD_CONF "-" -#define RIPD_CONF "/etc/frr/ripd.conf" -#define RIPD_CONF_NEXT RIPD_CONF "+" -#define RIPD_CONF_PREV RIPD_CONF "-" -#define BFDD_CONF "/etc/frr/bfd_enabled" /* Just signal that bfd should be enabled*/ -#define BFDD_CONF_NEXT BFDD_CONF "+" +#define RIPD_SIGNAL "/run/ripd_enabled" +#define RIPD_SIGNAL_NEXT RIPD_SIGNAL "+" +#define BFDD_SIGNAL "/run/bfd_enabled" /* Just signal that bfd should be enabled*/ +#define BFDD_SIGNAL_NEXT BFDD_SIGNAL "+" #define FRR_STATIC_CONFIG "! Generated by Infix confd\n\ frr defaults traditional\n\ @@ -29,217 +28,129 @@ no log unique-id\n\ log syslog warnings\n\ log facility local2\n" -int parse_rip_redistribute(sr_session_ctx_t *session, struct lyd_node *redistributes, FILE *fp) +int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp) { - struct lyd_node *tmp; - - LY_LIST_FOR(lyd_child(redistributes), tmp) { - const char *protocol = lydx_get_cattr(tmp, "protocol"); - - fprintf(fp, " redistribute %s\n", protocol); - } - - return 0; -} - -int parse_rip_interfaces(sr_session_ctx_t *session, struct lyd_node *interfaces, FILE *fp) -{ - struct lyd_node *interface, *neighbors_node, *neighbor; - int num_interfaces = 0; - - /* First pass: network and passive-interface statements (inside router rip block) */ - LY_LIST_FOR(lyd_child(interfaces), interface) { - const char *name; - int passive; - - name = lydx_get_cattr(interface, "interface"); - if (!name) - continue; - - passive = lydx_get_bool(interface, "passive"); - - /* Enable RIP on the interface by adding it to the network statement */ - fprintf(fp, " network %s\n", name); - - if (passive) - fprintf(fp, " passive-interface %s\n", name); - - num_interfaces++; - } - - /* Handle explicit neighbors (inside router rip block) */ - LY_LIST_FOR(lyd_child(interfaces), interface) { - neighbors_node = lydx_get_child(interface, "neighbors"); - if (neighbors_node) { - LY_LIST_FOR(lyd_child(neighbors_node), neighbor) { - const char *address = lydx_get_cattr(neighbor, "address"); - if (address) - fprintf(fp, " neighbor %s\n", address); - } - } - } - - /* Second pass: interface-specific settings (outside router rip block) */ - LY_LIST_FOR(lyd_child(interfaces), interface) { - const char *name, *split_horizon, *cost, *send_version, *recv_version; - - name = lydx_get_cattr(interface, "interface"); - if (!name) - continue; - - split_horizon = lydx_get_cattr(interface, "split-horizon"); - cost = lydx_get_cattr(interface, "cost"); - send_version = lydx_get_cattr(interface, "send-version"); - recv_version = lydx_get_cattr(interface, "receive-version"); - - /* Only create interface block if there are per-interface settings */ - if (split_horizon || cost || send_version || recv_version) { - fprintf(fp, "interface %s\n", name); - - if (split_horizon) { - if (!strcmp(split_horizon, "poison-reverse")) - fputs(" ip rip split-horizon poisoned-reverse\n", fp); - else if (!strcmp(split_horizon, "disabled")) - fputs(" no ip rip split-horizon\n", fp); - /* "simple" is default, no need to configure */ - } - - /* Configure send version */ - if (send_version) { - if (!strcmp(send_version, "1")) - fputs(" ip rip send version 1\n", fp); - else if (!strcmp(send_version, "1-2")) - fputs(" ip rip send version 1 2\n", fp); - /* "2" is default in augmentation, explicit config if needed */ - else if (!strcmp(send_version, "2")) - fputs(" ip rip send version 2\n", fp); - } - - /* Configure receive version */ - if (recv_version) { - if (!strcmp(recv_version, "1")) - fputs(" ip rip receive version 1\n", fp); - else if (!strcmp(recv_version, "1-2")) - fputs(" ip rip receive version 1 2\n", fp); - /* "2" is default in augmentation, explicit config if needed */ - else if (!strcmp(recv_version, "2")) - fputs(" ip rip receive version 2\n", fp); - } - - if (cost) { - /* FRR uses offset-list for per-interface cost adjustment */ - /* Note: offset-list is configured globally, not per-interface */ - /* This is just a placeholder - actual implementation would need - access lists and global offset-list configuration */ - } - } - } - - return num_interfaces; -} - -int parse_rip_timers(sr_session_ctx_t *session, struct lyd_node *timers, FILE *fp) -{ - const char *update, *invalid, *holddown, *flush; - - if (!timers) - return 0; - - update = lydx_get_cattr(timers, "update-interval"); - invalid = lydx_get_cattr(timers, "invalid-interval"); - holddown = lydx_get_cattr(timers, "holddown-interval"); - flush = lydx_get_cattr(timers, "flush-interval"); - - /* FRR timers basic: UPDATE TIMEOUT GARBAGE - * TIMEOUT = invalid-interval (when route becomes invalid) - * GARBAGE = flush-interval (when route is flushed) - * Note: holddown-interval is used between invalid and flush - */ - DEBUG("Ignoring 'holddown interval %s' for now", holddown); - if (update || invalid || flush) { - fprintf(fp, " timers basic %s %s %s\n", - update ? update : "30", - invalid ? invalid : "180", - flush ? flush : "240"); - } - - return 0; -} - -int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip) -{ - struct lyd_node *interfaces, *timers, *default_route, *debug; + struct lyd_node *interfaces, *timers, *default_route, *interface, *tmp; const char *default_metric, *distance; int num_interfaces = 0; - FILE *fp; - - fp = fopen(RIPD_CONF_NEXT, "w"); - if (!fp) { - ERROR("Failed to open %s", RIPD_CONF_NEXT); - return SR_ERR_INTERNAL; - } - - fputs(FRR_STATIC_CONFIG, fp); - - /* Handle RIP debug configuration */ - debug = lydx_get_child(rip, "debug"); - if (debug) { - int any_debug = 0; - - if (lydx_get_bool(debug, "events")) { - fputs("debug rip events\n", fp); - any_debug = 1; - } - if (lydx_get_bool(debug, "packet")) { - fputs("debug rip packet\n", fp); - any_debug = 1; - } - if (lydx_get_bool(debug, "kernel")) { - fputs("debug rip zebra\n", fp); - any_debug = 1; - } - - if (any_debug) - fputs("!\n", fp); - } - fputs("router rip\n", fp); - fputs(" version 2\n", fp); + /* Generate libconfuse format for RIP */ + fputs("\nrip {\n", fp); + fputs("\tenabled = true\n", fp); /* Global RIP parameters */ default_metric = lydx_get_cattr(rip, "default-metric"); if (default_metric) - fprintf(fp, " default-metric %s\n", default_metric); + fprintf(fp, "\tdefault-metric = %s\n", default_metric); distance = lydx_get_cattr(rip, "distance"); if (distance) - fprintf(fp, " distance %s\n", distance); + fprintf(fp, "\tdistance = %s\n", distance); /* Timers */ timers = lydx_get_child(rip, "timers"); - parse_rip_timers(session, timers, fp); + if (timers) { + const char *update, *invalid, *holddown, *flush; + + update = lydx_get_cattr(timers, "update-interval"); + invalid = lydx_get_cattr(timers, "invalid-interval"); + holddown = lydx_get_cattr(timers, "holddown-interval"); + flush = lydx_get_cattr(timers, "flush-interval"); + + DEBUG("Ignoring 'holddown interval %s' for now", holddown); + if (update || invalid || flush) { + fputs("\ttimers {\n", fp); + fprintf(fp, "\t\tupdate = %s\n", update ? update : "30"); + fprintf(fp, "\t\tinvalid = %s\n", invalid ? invalid : "180"); + fprintf(fp, "\t\tflush = %s\n", flush ? flush : "240"); + fputs("\t}\n", fp); + } + } /* Default route origination */ default_route = lydx_get_child(rip, "originate-default-route"); if (default_route && lydx_get_bool(default_route, "enabled")) - fputs(" default-information originate\n", fp); + fputs("\tdefault-route = true\n", fp); - /* Redistribution */ - parse_rip_redistribute(session, lydx_get_child(rip, "redistribute"), fp); + /* Debug options - use system commands since FRR doesn't support via northbound */ + struct lyd_node *debug = lydx_get_child(rip, "debug"); + if (debug) { + if (lydx_get_bool(debug, "events")) + fputs("\tsystem = \"vtysh -c 'debug rip events'\"\n", fp); + if (lydx_get_bool(debug, "packet")) + fputs("\tsystem = \"vtysh -c 'debug rip packet'\"\n", fp); + if (lydx_get_bool(debug, "kernel")) + fputs("\tsystem = \"vtysh -c 'debug rip zebra'\"\n", fp); + } - /* Interfaces - must be done after router rip block and before interface blocks */ + /* Networks (interfaces) - output as list */ interfaces = lydx_get_child(rip, "interfaces"); - if (interfaces) - num_interfaces = parse_rip_interfaces(session, interfaces, fp); + if (interfaces) { + int first = 1; + fputs("\tnetwork = { ", fp); + LY_LIST_FOR(lyd_child(interfaces), interface) { + const char *name = lydx_get_cattr(interface, "interface"); + if (name) { + if (!first) + fputs(", ", fp); + fprintf(fp, "\"%s\"", name); + first = 0; + num_interfaces++; + } + } + fputs(" }\n", fp); - fclose(fp); + /* Passive interfaces - output as list */ + first = 1; + int has_passive = 0; + LY_LIST_FOR(lyd_child(interfaces), interface) { + if (lydx_get_bool(interface, "passive")) { + if (!has_passive) { + fputs("\tpassive = { ", fp); + has_passive = 1; + } + if (!first) + fputs(", ", fp); + fprintf(fp, "\"%s\"", lydx_get_cattr(interface, "interface")); + first = 0; + } + } + if (has_passive) + fputs(" }\n", fp); - if (!num_interfaces) { - (void)remove(RIPD_CONF_NEXT); - return 0; + /* Neighbors */ + LY_LIST_FOR(lyd_child(interfaces), interface) { + struct lyd_node *neighbors_node = lydx_get_child(interface, "neighbors"); + if (neighbors_node) { + LY_LIST_FOR(lyd_child(neighbors_node), tmp) { + const char *address = lydx_get_cattr(tmp, "address"); + if (address) + fprintf(fp, "\tneighbor = \"%s\"\n", address); + } + } + } } - return 0; + /* Redistribution - output as list */ + tmp = lydx_get_child(rip, "redistribute"); + if (tmp && lyd_child(tmp)) { + int first = 1; + fputs("\tredistribute = { ", fp); + LY_LIST_FOR(lyd_child(tmp), tmp) { + const char *protocol = lydx_get_cattr(tmp, "protocol"); + if (protocol) { + if (!first) + fputs(", ", fp); + fprintf(fp, "\"%s\"", protocol); + first = 0; + } + } + fputs(" }\n", fp); + } + + fputs("}\n", fp); + + return num_interfaces; } int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp) @@ -361,8 +272,6 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf) return SR_ERR_INTERNAL; } - fputs(FRR_STATIC_CONFIG, fp); - /* Handle OSPF debug configuration */ debug = lydx_get_child(ospf, "debug"); if (debug) { @@ -419,17 +328,15 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf) fprintf(fp, " ospf router-id %s\n", router_id); fclose(fp); - if (!bfd_enabled) - (void)remove(BFDD_CONF); - if (!num_areas) { (void)remove(OSPFD_CONF_NEXT); return 0; } if (bfd_enabled) - (void)touch(BFDD_CONF_NEXT); - + (void)touch(BFDD_SIGNAL_NEXT); + else + (void)remove(BFDD_SIGNAL_NEXT); return 0; } @@ -446,20 +353,26 @@ static int parse_route(struct lyd_node *parent, FILE *fp, const char *ip) next_hop_address = lydx_get_cattr(next_hop, "next-hop-address"); special_next_hop = lydx_get_cattr(next_hop, "special-next-hop"); - fprintf(fp, "%s route %s ", ip, destination_prefix); + /* Generate libconfuse format: route { prefix = "..." nexthop = "..." distance = ... } */ + fputs("route {\n", fp); + fprintf(fp, "\tprefix = \"%s\"\n", destination_prefix); - /* There can only be one */ + /* Nexthop - there can only be one */ if (outgoing_interface) - fputs(outgoing_interface, fp); + fprintf(fp, "\tnexthop = \"%s\"\n", outgoing_interface); else if (next_hop_address) - fputs(next_hop_address, fp); + fprintf(fp, "\tnexthop = \"%s\"\n", next_hop_address); else if (strcmp(special_next_hop, "blackhole") == 0) - fputs("blackhole", fp); + fputs("\tnexthop = \"blackhole\"\n", fp); else if (strcmp(special_next_hop, "unreachable") == 0) - fputs("reject", fp); + fputs("\tnexthop = \"reject\"\n", fp); else if (strcmp(special_next_hop, "receive") == 0) - fputs("Null0", fp); - fprintf(fp, " %s\n", route_preference); + fputs("\tnexthop = \"Null0\"\n", fp); + + if (route_preference) + fprintf(fp, "\tdistance = %s\n", route_preference); + + fputs("}\n", fp); return SR_ERR_OK; } @@ -490,9 +403,8 @@ static int parse_static_routes(sr_session_ctx_t *session, struct lyd_node *paren int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd) { - int staticd_enabled = 0, ospfd_enabled = 0, ripd_enabled = 0, bfdd_enabled = 0; + int netd_enabled = 0, ospfd_enabled = 0, bfdd_enabled = 0, ripd_enabled = 0; struct lyd_node *cplane, *cplanes; - bool ospfd_running, ripd_running, bfdd_running; bool restart_zebra = false; int rc = SR_ERR_OK; FILE *fp; @@ -503,107 +415,66 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly switch (event) { case SR_EV_ENABLED: /* first time, on register. */ case SR_EV_CHANGE: /* regular change (copy cand running) */ - fp = fopen(STATICD_CONF_NEXT, "w"); - if (!fp) { - ERROR("Failed to open %s", STATICD_CONF_NEXT); - return SR_ERR_INTERNAL; - } - fputs("! Generated by Infix confd\n", fp); break; case SR_EV_ABORT: /* User abort, or other plugin failed */ - (void)remove(STATICD_CONF_NEXT); + (void)remove(NETD_CONF_NEXT); return SR_ERR_OK; case SR_EV_DONE: /* Check if passed validation in previous event */ - staticd_enabled = fexist(STATICD_CONF_NEXT); + netd_enabled = fexist(NETD_CONF_NEXT); ospfd_enabled = fexist(OSPFD_CONF_NEXT); - ripd_enabled = fexist(RIPD_CONF_NEXT); - bfdd_enabled = fexist(BFDD_CONF_NEXT); - ospfd_running = !systemf("initctl -bfq status ospfd"); - ripd_running = !systemf("initctl -bfq status ripd"); - bfdd_running = !systemf("initctl -bfq status bfdd"); - - if (bfdd_running && !bfdd_enabled) { - if (systemf("initctl -bfq disable bfdd")) { - ERROR("Failed to disable BFD routing daemon"); - rc = SR_ERR_INTERNAL; - goto err_abandon; - } - /* Remove all generated files */ - (void)remove(BFDD_CONF); - } + bfdd_enabled = fexist(BFDD_SIGNAL_NEXT); + ripd_enabled = fexist(RIPD_SIGNAL_NEXT); - if (ospfd_running && !ospfd_enabled) { - if (systemf("initctl -bfq disable ospfd")) { - ERROR("Failed to disable OSPF routing daemon"); - rc = SR_ERR_INTERNAL; - goto err_abandon; - } - /* Remove all generated files */ - (void)remove(OSPFD_CONF); - } - - if (ripd_running && !ripd_enabled) { - if (systemf("initctl -bfq disable ripd")) { - ERROR("Failed to disable RIP routing daemon"); - rc = SR_ERR_INTERNAL; - goto err_abandon; - } - /* Remove all generated files */ - (void)remove(RIPD_CONF); - } if (bfdd_enabled) { - (void)rename(BFDD_CONF_NEXT, BFDD_CONF); - if (!bfdd_running) { - if (systemf("initctl -bfq enable bfdd")) { - ERROR("Failed to enable OSPF routing daemon"); - rc = SR_ERR_INTERNAL; - goto err_abandon; - } - } - } + (void)rename(BFDD_SIGNAL_NEXT, BFDD_SIGNAL); + systemf("initctl -bfq enable bfdd"); + systemf("initctl -bfq touch bfdd"); + restart_zebra = true; + } else { + (void)remove(BFDD_SIGNAL); + systemf("initctl -bfq disable bfdd"); + } if (ospfd_enabled) { (void)remove(OSPFD_CONF_PREV); (void)rename(OSPFD_CONF, OSPFD_CONF_PREV); (void)rename(OSPFD_CONF_NEXT, OSPFD_CONF); - if (!ospfd_running) { - if (systemf("initctl -bnq enable ospfd")) { - ERROR("Failed to enable OSPF routing daemon"); - rc = SR_ERR_INTERNAL; - goto err_abandon; - } - } else { - restart_zebra = true; - } + systemf("initctl enable ospfd"); + systemf("initctl touch ospfd"); + + restart_zebra = true; + } else { + systemf("initctl -bfq disable ospfd"); + (void)remove(OSPFD_CONF); } + /* Start/stop ripd daemon based on whether RIP config is present */ if (ripd_enabled) { - (void)remove(RIPD_CONF_PREV); - (void)rename(RIPD_CONF, RIPD_CONF_PREV); - (void)rename(RIPD_CONF_NEXT, RIPD_CONF); - if (!ripd_running) { - if (systemf("initctl -bnq enable ripd")) { - ERROR("Failed to enable RIP routing daemon"); - rc = SR_ERR_INTERNAL; - goto err_abandon; - } - } else { - restart_zebra = true; - } + systemf("initctl enable ripd"); + systemf("initctl touch ripd"); + restart_zebra = true; + } else { + (void)remove(RIPD_SIGNAL); + systemf("initctl -bfq disable ripd"); } - if (staticd_enabled) { - (void)remove(STATICD_CONF_PREV); - (void)rename(STATICD_CONF, STATICD_CONF_PREV); - (void)rename(STATICD_CONF_NEXT, STATICD_CONF); + /* netd handles both static routes and RIP */ + if (netd_enabled) { restart_zebra = true; + (void)remove(NETD_CONF_PREV); + (void)rename(NETD_CONF, NETD_CONF_PREV); + (void)rename(NETD_CONF_NEXT, NETD_CONF); + if (systemf("initctl -bfq touch netd")) + ERROR("Failed to signal netd for reload"); } else { - if (!remove(STATICD_CONF)) - restart_zebra = true; + if (!remove(NETD_CONF)) { + if (systemf("initctl -bfq touch netd")) + ERROR("Failed to signal netd for reload"); + } } if (restart_zebra) { @@ -611,7 +482,7 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly if (systemf("runlevel >/dev/null 2>&1")) return SR_ERR_OK; - if (systemf("initctl -bfq restart zebra")) { + if (systemf("initctl -bfq touch zebra")) { ERROR("Failed to restart zebra routing daemon"); rc = SR_ERR_INTERNAL; goto err_abandon; @@ -624,22 +495,39 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly } cplanes = lydx_get_descendant(config, "routing", "control-plane-protocols", "control-plane-protocol", NULL); + + /* Open netd config file for both static routes and RIP */ + fp = fopen(NETD_CONF_NEXT, "w"); + if (!fp) { + ERROR("Failed to open %s", NETD_CONF_NEXT); + return SR_ERR_INTERNAL; + } + fputs("# Generated by Infix confd\n", fp); + LYX_LIST_FOR_EACH(cplanes, cplane, "control-plane-protocol") { const char *type; + int num; type = lydx_get_cattr(cplane, "type"); if (!strcmp(type, "infix-routing:static")) { - staticd_enabled = parse_static_routes(session, lydx_get_child(cplane, "static-routes"), fp); + num = parse_static_routes(session, lydx_get_child(cplane, "static-routes"), fp); + if (num > 0) + netd_enabled = 1; } else if (!strcmp(type, "infix-routing:ospfv2")) { parse_ospf(session, lydx_get_child(cplane, "ospf")); } else if (!strcmp(type, "infix-routing:ripv2")) { - parse_rip(session, lydx_get_child(cplane, "rip")); + num = parse_rip(session, lydx_get_child(cplane, "rip"), fp); + if (num > 0) { + touch(RIPD_SIGNAL_NEXT); + ripd_enabled = 1; + netd_enabled = 1; + } } } fclose(fp); - if (!staticd_enabled) - (void)remove(STATICD_CONF_NEXT); + if (!netd_enabled) + (void)remove(NETD_CONF_NEXT); err_abandon: return rc; diff --git a/src/netd/LICENSE b/src/netd/LICENSE new file mode 100644 index 000000000..6e2ecd3e0 --- /dev/null +++ b/src/netd/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2026 The KernelKit Authors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of copyright holders nor the names of + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/netd/Makefile.am b/src/netd/Makefile.am new file mode 100644 index 000000000..0c600979e --- /dev/null +++ b/src/netd/Makefile.am @@ -0,0 +1,34 @@ +DISTCLEANFILES = *~ *.d +ACLOCAL_AMFLAGS = -I m4 + +sbin_PROGRAMS = netd +netd_SOURCES = src/netd.c src/netd.h src/config.c src/config.h +netd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -I$(srcdir)/src +netd_CFLAGS = -W -Wall -Wextra +netd_CFLAGS += $(libite_CFLAGS) $(libconfuse_CFLAGS) $(jansson_CFLAGS) +netd_LDADD = $(libite_LIBS) $(libconfuse_LIBS) $(jansson_LIBS) + +# Backend selection: FRR gRPC or Linux kernel +if HAVE_FRR_GRPC +BUILT_SOURCES = grpc/frr-northbound.pb.cc grpc/frr-northbound.pb.h \ + grpc/frr-northbound.grpc.pb.cc grpc/frr-northbound.grpc.pb.h + +grpc/frr-northbound.pb.cc grpc/frr-northbound.pb.h: grpc/frr-northbound.proto + $(AM_V_GEN)$(PROTOC) --cpp_out=grpc --proto_path=grpc $< + +grpc/frr-northbound.grpc.pb.cc grpc/frr-northbound.grpc.pb.h: grpc/frr-northbound.proto + $(AM_V_GEN)$(PROTOC) --grpc_out=grpc \ + --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN) \ + --proto_path=grpc $< + +netd_SOURCES += src/grpc_backend.cc src/grpc_backend.h \ + src/json_builder.c src/json_builder.h \ + grpc/frr-northbound.pb.cc grpc/frr-northbound.grpc.pb.cc +netd_CPPFLAGS += $(grpc_CFLAGS) $(protobuf_CFLAGS) +netd_LDADD += $(grpc_LIBS) $(protobuf_LIBS) -lstdc++ + +CLEANFILES = grpc/*.pb.cc grpc/*.pb.h +else +# Linux kernel backend (no FRR) +netd_SOURCES += src/linux_backend.c src/linux_backend.h +endif diff --git a/src/netd/README.md b/src/netd/README.md new file mode 100644 index 000000000..5ebcfc804 --- /dev/null +++ b/src/netd/README.md @@ -0,0 +1,361 @@ +# netd - Network Daemon for Static Routes and RIP + +A lightweight routing daemon that manages static routes and RIP routing protocol. + +## Features + +- **Static Routes** - IPv4 and IPv6 route management +- **RIP** - Routing Information Protocol (RIPv2) support +- **Dual Backend** - FRR integration or standalone Linux kernel routing +- **Simple Config** - INI-style section-based configuration format +- **Hot Reload** - SIGHUP support for configuration updates + +## Building + +### With FRR Integration (Default) + +Full routing support with FRR gRPC northbound API: + +```bash +./configure +make +make install +``` + +**Requirements:** +- FRR with gRPC northbound support +- protobuf >= 3.0.0 +- grpc++ >= 1.16.0 + +**Note:** The FRR gRPC protocol definition (`grpc/frr-northbound.proto`) is included in the netd source tree. + +**Features:** +- Static routes via FRR staticd +- RIP routing protocol +- OSPF (via separate FRR config) +- System command execution + +### Standalone Linux Backend + +Direct kernel routing without FRR: + +```bash +./configure --without-frr +make +make install +``` + +**Requirements:** +- Linux kernel with rtnetlink support + +**Features:** +- Static routes via rtnetlink +- No external dependencies + +**Limitations:** +- No RIP/OSPF support +- No system commands + +## Configuration + +netd uses [libconfuse](https://github.com/martinh/libconfuse) for configuration parsing, providing a clean and structured format. + +Configuration files are placed in `/etc/netd/conf.d/` with the `.conf` extension. Files are processed in alphabetical order. + +### Configuration Format + +The configuration uses libconfuse syntax with sections and key-value pairs: + +``` +route { + prefix = "10.0.0.0/24" + nexthop = "192.168.1.1" + distance = 1 +} + +rip { + enabled = true + network = ["eth0", "eth1"] +} +``` + +### Static Routes + +Define static routes using `route` sections. Multiple route sections can be specified. + +**Route Section:** + +``` +route { + prefix = "PREFIX/LEN" + nexthop = "NEXTHOP" + distance = DISTANCE + tag = TAG +} +``` + +**Parameters:** +- `prefix` (required) - Network prefix with CIDR notation + - IPv4: `"10.0.0.0/24"` + - IPv6: `"2001:db8::/32"` +- `nexthop` (required) - Next hop specification + - IP address: `"192.168.1.1"` or `"fe80::1"` + - Interface name: `"eth0"` + - Blackhole: `"blackhole"`, `"reject"`, or `"Null0"` +- `distance` (optional, default: 1) - Administrative distance (1-255) +- `tag` (optional, default: 0) - Route tag (0-4294967295), used for route filtering/redistribution + +**Examples:** + +IPv4 route via gateway: +``` +route { + prefix = "10.0.0.0/24" + nexthop = "192.168.1.1" + distance = 10 +} +``` + +IPv6 route via interface: +``` +route { + prefix = "2001:db8::/32" + nexthop = "eth0" + distance = 1 +} +``` + +Blackhole route: +``` +route { + prefix = "192.0.2.0/24" + nexthop = "blackhole" +} +``` + +### RIP Configuration + +Configure RIP routing protocol (requires FRR backend). + +**RIP Section:** + +``` +rip { + enabled = BOOL + default-metric = VALUE + distance = VALUE + default-route = BOOL + + network = [LIST] + passive = [LIST] + neighbor = [LIST] + redistribute = [LIST] + + timers { + update = SECONDS + invalid = SECONDS + flush = SECONDS + } + + debug-events = BOOL + debug-packet = BOOL + debug-kernel = BOOL + + system = [LIST] +} +``` + +**Parameters:** +- `enabled` (optional, default: false) - Enable RIP routing +- `default-metric` (optional, default: 1) - Default route metric (1-16) +- `distance` (optional, default: 120) - Administrative distance (1-255) +- `default-route` (optional, default: false) - Originate default route +- `network` (optional) - List of interfaces to enable RIP on +- `passive` (optional) - List of passive interfaces (receive only) +- `neighbor` (optional) - List of static RIP neighbor addresses +- `redistribute` (optional) - List of route types to redistribute + - Valid types: `"connected"`, `"static"`, `"kernel"`, `"ospf"` +- `timers` (optional) - RIP timer configuration subsection + - `update` (default: 30) - Update timer in seconds + - `invalid` (default: 180) - Invalid timer in seconds + - `flush` (default: 240) - Flush timer in seconds +- `debug-events` (optional, default: false) - Enable RIP event debugging +- `debug-packet` (optional, default: false) - Enable RIP packet debugging +- `debug-kernel` (optional, default: false) - Enable RIP kernel debugging +- `system` (optional) - List of system commands to execute after config application + +**Examples:** + +Basic RIP configuration: +``` +rip { + enabled = true + network = ["eth0", "eth1"] + redistribute = ["connected"] +} +``` + +RIP with passive interface: +``` +rip { + enabled = true + network = ["eth0", "eth1"] + passive = ["eth1"] +} +``` + +RIP with custom timers: +``` +rip { + enabled = true + network = ["eth0"] + timers { + update = 15 + invalid = 90 + flush = 120 + } +} +``` + +### Configuration Files + +Configuration files must be placed in `/etc/netd/conf.d/` with the `.conf` extension: + +```bash +/etc/netd/conf.d/ +├── 10-static.conf # Static routes +├── 20-rip.conf # RIP configuration +└── 99-local.conf # Local overrides +``` + +Files are processed in alphabetical order. Use numeric prefixes to control processing order. + +Lines starting with `#` are comments: + +``` +# This is a comment +route { + # This is also a comment + prefix = "10.0.0.0/24" # Inline comment + nexthop = "192.168.1.1" +} +``` + +### Reloading Configuration + +Signal netd to reload configuration: + +```bash +# Using killall +sudo killall -HUP netd +``` + +netd validates configuration on reload. Check syslog for errors. + +## Architecture + +``` +┌─────────┐ +│ confd │ Writes /etc/netd/conf.d/confd.conf +└────┬────┘ + │ SIGHUP + ▼ +┌─────────┐ +│ netd │ Parses config files +└────┬────┘ + │ + ├──► FRR Backend (gRPC) + │ ├─► mgmtd + │ ├─► staticd (static routes) + │ └─► ripd (RIP protocol) + │ + └──► Linux Backend (rtnetlink) + └─► Kernel routing table +``` + +### FRR Backend Flow + +1. netd parses config files +2. Builds JSON config for FRR +3. Sends via gRPC to mgmtd +4. mgmtd distributes to backend daemons +5. Executes system commands (if any) + +### Linux Backend Flow + +1. netd parses config files +2. Opens rtnetlink socket +3. Sends RTM_NEWROUTE/RTM_DELROUTE +4. Kernel updates routing table + +## Files + +``` +src/netd/ +├── src/ +│ ├── netd.c/h - Main daemon and data structures +│ ├── config.c/h - Config parser +│ ├── json_builder.c/h - FRR JSON config builder +│ ├── grpc_backend.cc/h - FRR gRPC backend +│ └── linux_backend.c/h - Linux rtnetlink backend +├── grpc/ +│ └── frr-northbound.proto - gRPC protocol definition (copied from FRR) +├── configure.ac - Build configuration +├── Makefile.am - Build rules +├── netd.conf - Sample configuration +└── README.md - This file +``` + +## API + +### Configuration Format + +- libconfuse syntax with sections +- `route { }` - Static route entries (multiple allowed) +- `rip { }` - RIP configuration (single section) + +### Supported Routes + +- IPv4 and IPv6 +- Gateway, interface, blackhole nexthops +- Administrative distance +- Route tags + +### RIP Features + +- Network interfaces +- Passive interfaces +- Static neighbors +- Route redistribution +- Timer configuration +- Default route origination +- Debug commands + +## Logging + +netd logs to syslog facility `daemon`: + +```bash +# Debug mode (stderr) +netd -d +``` + +Log levels: +- `INFO` - Configuration changes, route operations +- `ERROR` - Failures, errors +- `DEBUG` - Detailed operation info (with `-d`) + +## Signal Handling + +- `SIGHUP` - Reload configuration +- `SIGTERM` / `SIGINT` - Graceful shutdown + +## License + +BSD-3-Clause + +## See Also + +- FRR Documentation - https://docs.frrouting.org/ +- rtnetlink(7) - Linux routing socket API +- libconfuse Documentation - https://github.com/martinh/libconfuse diff --git a/src/netd/autogen.sh b/src/netd/autogen.sh new file mode 100755 index 000000000..69ad0e189 --- /dev/null +++ b/src/netd/autogen.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +autoreconf -W portability -vifm diff --git a/src/netd/configure.ac b/src/netd/configure.ac new file mode 100644 index 000000000..34e2f825d --- /dev/null +++ b/src/netd/configure.ac @@ -0,0 +1,88 @@ +AC_PREREQ(2.61) +AC_INIT([netd], [1.0.0], [https://github.com/kernelkit/infix/issues]) +AM_INIT_AUTOMAKE(1.11 foreign subdir-objects) +AM_SILENT_RULES(yes) + +AC_CONFIG_FILES([ + Makefile +]) + +AC_PROG_CC +AC_PROG_CXX +AC_PROG_INSTALL + +# +# FRR gRPC northbound API (optional, enabled by default) +# +AC_ARG_WITH(frr, + AS_HELP_STRING([--without-frr], [Build without FRR integration (use Linux kernel backend)]), + [with_frr=$withval], + [with_frr=yes]) + +AS_IF([test "x$with_frr" = "xyes"], [ + # Check for protoc and grpc_cpp_plugin + AC_PATH_PROG([PROTOC], [protoc], [no]) + AS_IF([test "x$PROTOC" = "xno"], [ + AC_MSG_ERROR([protoc not found, required for FRR support (use --without-frr to disable)]) + ]) + + AC_PATH_PROG([GRPC_CPP_PLUGIN], [grpc_cpp_plugin], [no]) + AS_IF([test "x$GRPC_CPP_PLUGIN" = "xno"], [ + AC_MSG_ERROR([grpc_cpp_plugin not found, required for FRR support (use --without-frr to disable)]) + ]) + + # Check for grpc++ and protobuf libraries + PKG_CHECK_MODULES([grpc], [grpc++ >= 1.16.0]) + PKG_CHECK_MODULES([protobuf], [protobuf >= 3.0.0]) + + AC_DEFINE(HAVE_FRR_GRPC, 1, [Built with FRR gRPC northbound API support]) + + AC_SUBST(PROTOC) + AC_SUBST(GRPC_CPP_PLUGIN) +]) + +AM_CONDITIONAL(HAVE_FRR_GRPC, [test "x$with_frr" = "xyes"]) + +# Check for pkg-config first +PKG_PROG_PKG_CONFIG + +PKG_CHECK_MODULES([libite], [libite >= 2.6.1]) +PKG_CHECK_MODULES([libconfuse], [libconfuse >= 3.0]) +PKG_CHECK_MODULES([jansson], [jansson >= 2.0]) + +test "x$prefix" = xNONE && prefix=$ac_default_prefix +test "x$exec_prefix" = xNONE && exec_prefix='${prefix}' + +DATAROOTDIR=`eval echo $datarootdir` +DATAROOTDIR=`eval echo $DATAROOTDIR` +AC_SUBST(DATAROOTDIR) + +SYSCONFDIR=`eval echo $sysconfdir` +SYSCONFDIR=`eval echo $SYSCONFDIR` +AC_SUBST(SYSCONFDIR) + +RUNSTATEDIR=`eval echo $runstatedir` +RUNSTATEDIR=`eval echo $RUNSTATEDIR` +AC_SUBST(RUNSTATEDIR) + +AC_OUTPUT + +cat < +#include +#include + +#include "config.h" + +/* + * Parse a single route section from libconfuse + */ +static int parse_route_section(cfg_t *cfg_route, struct route_head *head) +{ + const char *prefix_str, *nexthop_str; + char prefix_copy[128]; + struct in6_addr a6; + struct in_addr a4; + struct route *r; + long distance; + char *slash; + + prefix_str = cfg_getstr(cfg_route, "prefix"); + nexthop_str = cfg_getstr(cfg_route, "nexthop"); + distance = cfg_getint(cfg_route, "distance"); + + if (!prefix_str || !nexthop_str) { + ERROR("Route missing prefix or nexthop"); + return -1; + } + + r = calloc(1, sizeof(*r)); + if (!r) { + ERROR("Failed to allocate route"); + return -1; + } + + r->distance = (uint8_t)distance; + r->tag = (uint32_t)cfg_getint(cfg_route, "tag"); + + /* Parse prefix - determine address family from presence of ':' */ + snprintf(prefix_copy, sizeof(prefix_copy), "%s", prefix_str); + slash = strchr(prefix_copy, '/'); + if (!slash) { + ERROR("Route prefix missing netmask: %s", prefix_str); + free(r); + return -1; + } + *slash = '\0'; + r->prefixlen = (uint8_t)atoi(slash + 1); + + /* Try IPv6 first (has ':'), then IPv4 */ + if (strchr(prefix_copy, ':')) { + r->family = AF_INET6; + if (inet_pton(AF_INET6, prefix_copy, &r->prefix.ip6) != 1) { + ERROR("Invalid IPv6 prefix: %s", prefix_copy); + free(r); + return -1; + } + } else { + r->family = AF_INET; + if (inet_pton(AF_INET, prefix_copy, &r->prefix.ip4) != 1) { + ERROR("Invalid IPv4 prefix: %s", prefix_copy); + free(r); + return -1; + } + } + + /* Parse nexthop - check for special keywords */ + if (!strcmp(nexthop_str, "blackhole")) { + r->nh_type = NH_BLACKHOLE; + r->bh_type = BH_DROP; + } else if (!strcmp(nexthop_str, "reject")) { + r->nh_type = NH_BLACKHOLE; + r->bh_type = BH_REJECT; + } else if (!strcmp(nexthop_str, "Null0")) { + r->nh_type = NH_BLACKHOLE; + r->bh_type = BH_NULL; + } else if (r->family == AF_INET && inet_pton(AF_INET, nexthop_str, &a4) == 1) { + /* IPv4 address */ + r->nh_type = NH_ADDR; + r->gateway.gw4 = a4; + } else if (r->family == AF_INET6 && inet_pton(AF_INET6, nexthop_str, &a6) == 1) { + /* IPv6 address */ + r->nh_type = NH_ADDR; + r->gateway.gw6 = a6; + } else { + /* Treat as interface name */ + r->nh_type = NH_IFNAME; + snprintf(r->ifname, sizeof(r->ifname), "%s", nexthop_str); + } + + TAILQ_INSERT_TAIL(head, r, entries); + return 0; +} + +/* + * Parse RIP configuration section from libconfuse + */ +static int parse_rip_section(cfg_t *cfg_rip, struct rip_config *rip_cfg) +{ + struct rip_redistribute *redist; + struct rip_system_cmd *cmd; + struct rip_neighbor *nbr; + struct rip_network *net; + const char *addr_str; + const char *type_str; + const char *cmd_str; + const char *ifname; + unsigned int n, i; + cfg_t *cfg_timers; + + /* Check if RIP is enabled */ + if (!cfg_getbool(cfg_rip, "enabled")) { + DEBUG("RIP is disabled"); + return 0; + } + + rip_cfg->enabled = 1; + + /* Basic settings */ + rip_cfg->default_metric = (uint8_t)cfg_getint(cfg_rip, "default-metric"); + rip_cfg->distance = (uint8_t)cfg_getint(cfg_rip, "distance"); + rip_cfg->default_route = cfg_getbool(cfg_rip, "default-route"); + + /* Debug flags */ + rip_cfg->debug_events = cfg_getbool(cfg_rip, "debug-events"); + rip_cfg->debug_packet = cfg_getbool(cfg_rip, "debug-packet"); + rip_cfg->debug_kernel = cfg_getbool(cfg_rip, "debug-kernel"); + + /* Timers subsection */ + cfg_timers = cfg_getsec(cfg_rip, "timers"); + if (cfg_timers) { + rip_cfg->timers.update = (uint32_t)cfg_getint(cfg_timers, "update"); + rip_cfg->timers.invalid = (uint32_t)cfg_getint(cfg_timers, "invalid"); + rip_cfg->timers.flush = (uint32_t)cfg_getint(cfg_timers, "flush"); + } + + /* Network interfaces */ + n = cfg_size(cfg_rip, "network"); + for (i = 0; i < n; i++) { + ifname = cfg_getnstr(cfg_rip, "network", i); + if (!ifname) + continue; + + net = calloc(1, sizeof(*net)); + if (!net) { + ERROR("Failed to allocate network"); + continue; + } + + snprintf(net->ifname, sizeof(net->ifname), "%s", ifname); + net->passive = 0; + TAILQ_INSERT_TAIL(&rip_cfg->networks, net, entries); + } + + /* Passive interfaces - mark existing networks as passive */ + n = cfg_size(cfg_rip, "passive"); + for (i = 0; i < n; i++) { + int found = 0; + + ifname = cfg_getnstr(cfg_rip, "passive", i); + if (!ifname) + continue; + + /* Find the network and mark it passive */ + TAILQ_FOREACH(net, &rip_cfg->networks, entries) { + if (!strcmp(net->ifname, ifname)) { + net->passive = 1; + found = 1; + break; + } + } + + /* If not found, create it as passive */ + if (!found) { + net = calloc(1, sizeof(*net)); + if (!net) { + ERROR("Failed to allocate passive network"); + continue; + } + snprintf(net->ifname, sizeof(net->ifname), "%s", ifname); + net->passive = 1; + TAILQ_INSERT_TAIL(&rip_cfg->networks, net, entries); + } + } + + /* Neighbors */ + n = cfg_size(cfg_rip, "neighbor"); + for (i = 0; i < n; i++) { + addr_str = cfg_getnstr(cfg_rip, "neighbor", i); + if (!addr_str) + continue; + + nbr = calloc(1, sizeof(*nbr)); + if (!nbr) { + ERROR("Failed to allocate neighbor"); + continue; + } + + if (inet_pton(AF_INET, addr_str, &nbr->addr) != 1) { + ERROR("Invalid neighbor address: %s", addr_str); + free(nbr); + continue; + } + + TAILQ_INSERT_TAIL(&rip_cfg->neighbors, nbr, entries); + } + + /* Redistribute routes */ + n = cfg_size(cfg_rip, "redistribute"); + for (i = 0; i < n; i++) { + type_str = cfg_getnstr(cfg_rip, "redistribute", i); + if (!type_str) + continue; + + redist = calloc(1, sizeof(*redist)); + if (!redist) { + ERROR("Failed to allocate redistribute"); + continue; + } + + if (!strcmp(type_str, "connected")) + redist->type = RIP_REDIST_CONNECTED; + else if (!strcmp(type_str, "static")) + redist->type = RIP_REDIST_STATIC; + else if (!strcmp(type_str, "kernel")) + redist->type = RIP_REDIST_KERNEL; + else if (!strcmp(type_str, "ospf")) + redist->type = RIP_REDIST_OSPF; + else { + ERROR("Unknown redistribute type: %s", type_str); + free(redist); + continue; + } + + TAILQ_INSERT_TAIL(&rip_cfg->redistributes, redist, entries); + } + + /* System commands */ + n = cfg_size(cfg_rip, "system"); + for (i = 0; i < n; i++) { + cmd_str = cfg_getnstr(cfg_rip, "system", i); + if (!cmd_str || !*cmd_str) { + ERROR("Empty system command"); + continue; + } + + cmd = calloc(1, sizeof(*cmd)); + if (!cmd) { + ERROR("Failed to allocate system command"); + continue; + } + + snprintf(cmd->command, sizeof(cmd->command), "%s", cmd_str); + TAILQ_INSERT_TAIL(&rip_cfg->system_cmds, cmd, entries); + } + + return 0; +} + +/* + * Parse a single config file using libconfuse + */ +static int config_parse_file(const char *path, struct route_head *routes, + struct rip_config *rip_cfg) +{ + cfg_opt_t timers_opts[] = { + CFG_INT("update", 30, CFGF_NONE), + CFG_INT("invalid", 180, CFGF_NONE), + CFG_INT("flush", 240, CFGF_NONE), + CFG_END() + }; + + cfg_opt_t rip_opts[] = { + CFG_BOOL("enabled", cfg_false, CFGF_NONE), + CFG_INT("default-metric", 1, CFGF_NONE), + CFG_INT("distance", 120, CFGF_NONE), + CFG_BOOL("default-route", cfg_false, CFGF_NONE), + CFG_BOOL("debug-events", cfg_false, CFGF_NONE), + CFG_BOOL("debug-packet", cfg_false, CFGF_NONE), + CFG_BOOL("debug-kernel", cfg_false, CFGF_NONE), + CFG_STR_LIST("network", NULL, CFGF_NONE), + CFG_STR_LIST("passive", NULL, CFGF_NONE), + CFG_STR_LIST("neighbor", NULL, CFGF_NONE), + CFG_STR_LIST("redistribute", NULL, CFGF_NONE), + CFG_STR_LIST("system", NULL, CFGF_NONE), + CFG_SEC("timers", timers_opts, CFGF_NONE), + CFG_END() + }; + + cfg_opt_t route_opts[] = { + CFG_STR("prefix", NULL, CFGF_NONE), + CFG_STR("nexthop", NULL, CFGF_NONE), + CFG_INT("distance", 1, CFGF_NONE), + CFG_INT("tag", 0, CFGF_NONE), + CFG_END() + }; + + cfg_opt_t opts[] = { + CFG_SEC("route", route_opts, CFGF_MULTI), + CFG_SEC("rip", rip_opts, CFGF_NONE), + CFG_END() + }; + + unsigned int i, n; + cfg_t *cfg_route; + cfg_t *cfg_rip; + cfg_t *cfg; + int ret; + + cfg = cfg_init(opts, CFGF_NONE); + if (!cfg) { + ERROR("Failed to initialize libconfuse"); + return -1; + } + + ret = cfg_parse(cfg, path); + if (ret == CFG_FILE_ERROR) { + ERROR("Failed to open config file: %s", path); + cfg_free(cfg); + return -1; + } else if (ret == CFG_PARSE_ERROR) { + ERROR("Parse error in config file: %s", path); + cfg_free(cfg); + return -1; + } + + /* Parse all route sections */ + n = cfg_size(cfg, "route"); + for (i = 0; i < n; i++) { + cfg_route = cfg_getnsec(cfg, "route", i); + if (cfg_route) { + if (parse_route_section(cfg_route, routes) < 0) + ERROR("Failed to parse route section %u in %s", i, path); + } + } + + /* Parse RIP section if present */ + cfg_rip = cfg_getsec(cfg, "rip"); + if (cfg_rip) { + if (parse_rip_section(cfg_rip, rip_cfg) < 0) + ERROR("Failed to parse RIP section in %s", path); + } + + cfg_free(cfg); + return 0; +} + +int config_load(struct route_head *routes, struct rip_config *rip_cfg) +{ + struct dirent **namelist; + char path[PATH_MAX]; + const char *name; + const char *ext; + int n, i; + + n = scandir(CONF_DIR, &namelist, NULL, alphasort); + if (n < 0) { + if (errno == ENOENT) { + DEBUG("No config directory %s", CONF_DIR); + return 0; + } + ERROR("scandir %s: %s", CONF_DIR, strerror(errno)); + return -1; + } + + for (i = 0; i < n; i++) { + name = namelist[i]->d_name; + + ext = strrchr(name, '.'); + if (!ext || strcmp(ext, ".conf")) { + free(namelist[i]); + continue; + } + + snprintf(path, sizeof(path), "%s/%s", CONF_DIR, name); + DEBUG("Loading config %s", path); + config_parse_file(path, routes, rip_cfg); + free(namelist[i]); + } + + free(namelist); + return 0; +} diff --git a/src/netd/src/config.h b/src/netd/src/config.h new file mode 100644 index 000000000..3aac29971 --- /dev/null +++ b/src/netd/src/config.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_CONFIG_H_ +#define NETD_CONFIG_H_ + +#include "netd.h" + +int config_load(struct route_head *routes, struct rip_config *rip_cfg); + +#endif /* NETD_CONFIG_H_ */ diff --git a/src/netd/src/grpc_backend.cc b/src/netd/src/grpc_backend.cc new file mode 100644 index 000000000..dd3e6730a --- /dev/null +++ b/src/netd/src/grpc_backend.cc @@ -0,0 +1,194 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +/* + * gRPC backend for netd - communicates with FRR's management daemon + * using the gRPC northbound API. This provides a standard protocol + * interface for configuration management. + */ + +#include + +#include +#include +#include + +#include "grpc/frr-northbound.grpc.pb.h" +#include "grpc/frr-northbound.pb.h" + +extern "C" { +#include "netd.h" +#include "grpc_backend.h" +#include "json_builder.h" +} + +/* Global gRPC state */ +static std::shared_ptr g_channel; +static std::unique_ptr g_stub; +static uint64_t g_candidate_id = 0; + +/* gRPC server address */ +#define GRPC_SERVER "127.0.0.1:50051" + +extern "C" int grpc_backend_init(void) +{ + frr::GetCapabilitiesResponse cap_resp; + frr::GetCapabilitiesRequest cap_req; + grpc::ClientContext cap_ctx; + grpc::ChannelArguments args; + grpc::Status status; + + /* Create insecure channel to local mgmtd */ + g_channel = grpc::CreateChannel(GRPC_SERVER, grpc::InsecureChannelCredentials()); + if (!g_channel) { + ERROR("grpc: failed to create channel"); + return -1; + } + + /* Create Northbound stub */ + g_stub = frr::Northbound::NewStub(g_channel); + if (!g_stub) { + ERROR("grpc: failed to create stub"); + g_channel.reset(); + return -1; + } + + /* Test connection with GetCapabilities */ + status = g_stub->GetCapabilities(&cap_ctx, cap_req, &cap_resp); + if (!status.ok()) { + ERROR("grpc: GetCapabilities failed: %s (code=%d)", + status.error_message().c_str(), status.error_code()); + g_stub.reset(); + g_channel.reset(); + return -1; + } + + INFO("grpc: connected to FRR mgmtd (version=%s, supported encodings=%d)", + cap_resp.frr_version().c_str(), cap_resp.supported_encodings_size()); + + return 0; +} + +extern "C" void grpc_backend_cleanup(void) +{ + frr::DeleteCandidateResponse del_resp; + frr::DeleteCandidateRequest del_req; + grpc::ClientContext del_ctx; + + /* Clean up any pending candidates */ + if (g_candidate_id != 0 && g_stub) { + del_req.set_candidate_id(g_candidate_id); + g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp); + g_candidate_id = 0; + } + + g_stub.reset(); + g_channel.reset(); + + DEBUG("grpc: finalized"); +} + +extern "C" int grpc_backend_apply(struct route_head *routes, struct rip_config *rip) +{ + frr::CreateCandidateResponse create_resp; + frr::LoadToCandidateResponse load_resp; + frr::CreateCandidateRequest create_req; + frr::DeleteCandidateResponse del_resp; + frr::LoadToCandidateRequest load_req; + frr::DeleteCandidateRequest del_req; + frr::CommitResponse commit_resp; + grpc::ClientContext create_ctx; + grpc::ClientContext commit_ctx; + frr::CommitRequest commit_req; + grpc::ClientContext load_ctx; + grpc::ClientContext del_ctx; + const char *json_config; + grpc::Status status; + + if (!g_stub || !g_channel) { + ERROR("grpc: not initialized"); + return -1; + } + + /* Build JSON configuration for both static routes and RIP */ + json_config = build_routing_json(routes, rip); + if (!json_config) { + ERROR("grpc: failed to build JSON config"); + return -1; + } + + DEBUG("grpc: JSON config: %s", json_config); + + /* Step 1: CreateCandidate */ + status = g_stub->CreateCandidate(&create_ctx, create_req, &create_resp); + if (!status.ok()) { + ERROR("grpc: CreateCandidate failed: %s (code=%d)", + status.error_message().c_str(), status.error_code()); + return -1; + } + + g_candidate_id = create_resp.candidate_id(); + DEBUG("grpc: created candidate %lu", (unsigned long)g_candidate_id); + + /* Step 2: LoadToCandidate (REPLACE entire routing config) */ + load_req.set_candidate_id(g_candidate_id); + load_req.set_type(frr::LoadToCandidateRequest_LoadType_REPLACE); + + { + auto* config = load_req.mutable_config(); + config->set_encoding(frr::JSON); + config->set_data(json_config); + } + + status = g_stub->LoadToCandidate(&load_ctx, load_req, &load_resp); + if (!status.ok()) { + ERROR("grpc: LoadToCandidate failed: %s (code=%d)", + status.error_message().c_str(), status.error_code()); + + /* Clean up candidate */ + del_req.set_candidate_id(g_candidate_id); + g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp); + g_candidate_id = 0; + + return -1; + } + + DEBUG("grpc: config loaded to candidate"); + + /* Step 3: Commit */ + commit_req.set_candidate_id(g_candidate_id); + commit_req.set_phase(frr::CommitRequest_Phase_ALL); + commit_req.set_comment("netd configuration (routes + rip)"); + + status = g_stub->Commit(&commit_ctx, commit_req, &commit_resp); + if (!status.ok()) { + /* ABORTED with "No changes to apply" is a success case */ + if (status.error_code() == grpc::StatusCode::ABORTED && + status.error_message().find("No changes to apply") != std::string::npos) { + DEBUG("grpc: No changes to apply (config already up-to-date)"); + + /* Clean up candidate - it won't be auto-deleted on ABORTED */ + del_req.set_candidate_id(g_candidate_id); + g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp); + g_candidate_id = 0; + + INFO("grpc: configuration already up-to-date"); + return 0; + } + + ERROR("grpc: Commit failed: %s (code=%d)", + status.error_message().c_str(), status.error_code()); + + /* Clean up candidate */ + del_req.set_candidate_id(g_candidate_id); + g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp); + g_candidate_id = 0; + + return -1; + } + + /* Candidate is auto-deleted after successful commit */ + g_candidate_id = 0; + + INFO("grpc: configuration committed successfully"); + return 0; +} diff --git a/src/netd/src/grpc_backend.h b/src/netd/src/grpc_backend.h new file mode 100644 index 000000000..77db096f4 --- /dev/null +++ b/src/netd/src/grpc_backend.h @@ -0,0 +1,18 @@ +#ifndef NETD_GRPC_BACKEND_H_ +#define NETD_GRPC_BACKEND_H_ + +#include "netd.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int grpc_backend_init(void); +void grpc_backend_cleanup(void); +int grpc_backend_apply(struct route_head *routes, struct rip_config *rip); + +#ifdef __cplusplus +} +#endif + +#endif /* NETD_GRPC_BACKEND_H_ */ diff --git a/src/netd/src/json_builder.c b/src/netd/src/json_builder.c new file mode 100644 index 000000000..87ea31f70 --- /dev/null +++ b/src/netd/src/json_builder.c @@ -0,0 +1,563 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +/* + * JSON builder for FRR routing configuration using libjansson. + * Converts internal routing and RIP structures to JSON format + * following FRR's YANG model for northbound API. + */ + +#include +#include +#include +#include + +#include + +#include "netd.h" +#include "json_builder.h" + +/* Static buffer for JSON output (64KB should be sufficient) */ +#define JSON_BUFFER_SIZE (64 * 1024) +static char json_buffer[JSON_BUFFER_SIZE]; + +/* + * Helper to check if two routes have the same prefix. + * Routes with the same prefix should be grouped into one route-list + * entry with multiple path-list entries. + */ +static bool same_prefix(const struct route *a, const struct route *b) +{ + if (a->family != b->family || a->prefixlen != b->prefixlen) + return false; + + if (a->family == AF_INET) + return memcmp(&a->prefix.ip4, &b->prefix.ip4, sizeof(a->prefix.ip4)) == 0; + else + return memcmp(&a->prefix.ip6, &b->prefix.ip6, sizeof(a->prefix.ip6)) == 0; +} + +/* + * Build JSON configuration for staticd following FRR's YANG model. + * Groups routes with the same prefix into a single route-list entry + * with multiple path-list entries (for ECMP/multipath support). + */ +const char *build_staticd_json(struct route_head *routes) +{ + char prefix_str[INET6_ADDRSTRLEN + 4]; + char addr_buf[INET6_ADDRSTRLEN]; + json_t *control_plane_protocols; + json_t *control_plane_protocol; + struct route *r, *curr; + json_t *frr_staticd; + json_t *route_entry; + json_t *route_list; + json_t *frr_routing; + const char *afi; + json_t *nexthop; + json_t *root; + char *result; + + root = json_object(); + if (!root) + return NULL; + + frr_routing = json_object(); + control_plane_protocols = json_object(); + control_plane_protocol = json_array(); + + json_t *protocol = json_object(); + json_object_set_new(protocol, "type", json_string("frr-staticd:staticd")); + json_object_set_new(protocol, "name", json_string("staticd")); + json_object_set_new(protocol, "vrf", json_string("default")); + + frr_staticd = json_object(); + route_list = json_array(); + + r = TAILQ_FIRST(routes); + while (r != NULL) { + if (r->family == AF_INET) { + inet_ntop(AF_INET, &r->prefix.ip4, addr_buf, sizeof(addr_buf)); + afi = "frr-routing:ipv4-unicast"; + } else { + inet_ntop(AF_INET6, &r->prefix.ip6, addr_buf, sizeof(addr_buf)); + afi = "frr-routing:ipv6-unicast"; + } + snprintf(prefix_str, sizeof(prefix_str), "%s/%d", addr_buf, (int)r->prefixlen); + + route_entry = json_object(); + json_object_set_new(route_entry, "prefix", json_string(prefix_str)); + json_object_set_new(route_entry, "src-prefix", json_string("::/0")); + json_object_set_new(route_entry, "afi-safi", json_string(afi)); + + json_t *path_list = json_array(); + + /* Add all routes with the same prefix as path-list entries */ + curr = r; + while (curr != NULL && same_prefix(r, curr)) { + json_t *path = json_object(); + json_object_set_new(path, "table-id", json_integer(0)); + json_object_set_new(path, "distance", json_integer((int)curr->distance)); + + json_t *frr_nexthops = json_object(); + json_t *nexthop_array = json_array(); + nexthop = json_object(); + + /* Next-hop */ + switch (curr->nh_type) { + case NH_ADDR: + if (curr->family == AF_INET) + inet_ntop(AF_INET, &curr->gateway.gw4, addr_buf, sizeof(addr_buf)); + else + inet_ntop(AF_INET6, &curr->gateway.gw6, addr_buf, sizeof(addr_buf)); + + json_object_set_new(nexthop, "nh-type", json_string(curr->family == AF_INET ? "ip4" : "ip6")); + json_object_set_new(nexthop, "vrf", json_string("default")); + json_object_set_new(nexthop, "gateway", json_string(addr_buf)); + json_object_set_new(nexthop, "interface", json_string("")); + break; + + case NH_IFNAME: + json_object_set_new(nexthop, "nh-type", json_string("ifindex")); + json_object_set_new(nexthop, "vrf", json_string("default")); + json_object_set_new(nexthop, "gateway", json_string("")); + json_object_set_new(nexthop, "interface", json_string(curr->ifname)); + break; + + case NH_BLACKHOLE: + json_object_set_new(nexthop, "nh-type", json_string("blackhole")); + json_object_set_new(nexthop, "vrf", json_string("default")); + json_object_set_new(nexthop, "gateway", json_string("")); + json_object_set_new(nexthop, "interface", json_string("")); + + switch (curr->bh_type) { + case BH_DROP: + json_object_set_new(nexthop, "bh-type", json_string("null")); + break; + case BH_REJECT: + json_object_set_new(nexthop, "bh-type", json_string("reject")); + break; + case BH_NULL: + json_object_set_new(nexthop, "bh-type", json_string("unspec")); + break; + } + break; + } + + json_array_append_new(nexthop_array, nexthop); + json_object_set_new(frr_nexthops, "nexthop", nexthop_array); + json_object_set_new(path, "frr-nexthops", frr_nexthops); + json_array_append_new(path_list, path); + + curr = TAILQ_NEXT(curr, entries); + } + + json_object_set_new(route_entry, "path-list", path_list); + json_array_append_new(route_list, route_entry); + + r = curr; + } + + json_object_set_new(frr_staticd, "route-list", route_list); + json_object_set_new(protocol, "frr-staticd:staticd", frr_staticd); + json_array_append_new(control_plane_protocol, protocol); + json_object_set_new(control_plane_protocols, "control-plane-protocol", control_plane_protocol); + json_object_set_new(frr_routing, "control-plane-protocols", control_plane_protocols); + json_object_set_new(root, "frr-routing:routing", frr_routing); + + result = json_dumps(root, JSON_COMPACT); + json_decref(root); + + if (!result) + return NULL; + + if (strlen(result) >= JSON_BUFFER_SIZE) { + ERROR("json_builder: buffer overflow (needed %zu, have %d)", strlen(result), JSON_BUFFER_SIZE); + free(result); + return NULL; + } + + strncpy(json_buffer, result, JSON_BUFFER_SIZE - 1); + json_buffer[JSON_BUFFER_SIZE - 1] = '\0'; + free(result); + + return json_buffer; +} + +/* + * Build JSON configuration for RIP following FRR's YANG model. + * RIP uses /frr-ripd:ripd/instance path, not control-plane-protocols. + */ +const char *build_rip_json(struct rip_config *rip_cfg) +{ + struct rip_redistribute *redist; + char addr_buf[INET_ADDRSTRLEN]; + struct rip_neighbor *nbr; + struct rip_network *net; + json_t *instance_array; + const char *proto; + json_t *instance; + json_t *ripd_obj; + json_t *array; + json_t *root; + char *result; + + if (!rip_cfg || !rip_cfg->enabled) + return ""; + + root = json_object(); + if (!root) + return NULL; + + ripd_obj = json_object(); + instance_array = json_array(); + instance = json_object(); + + json_object_set_new(instance, "vrf", json_string("default")); + + /* Default metric */ + if (rip_cfg->default_metric != 1) + json_object_set_new(instance, "default-metric", json_integer((int)rip_cfg->default_metric)); + + /* Distance */ + if (rip_cfg->distance != 120) { + json_t *distance_obj = json_object(); + json_object_set_new(distance_obj, "default", json_integer((int)rip_cfg->distance)); + json_object_set_new(instance, "distance", distance_obj); + } + + /* Timers */ + if (rip_cfg->timers.update != 30 || rip_cfg->timers.invalid != 180 || + rip_cfg->timers.flush != 240) { + json_t *timers = json_object(); + json_object_set_new(timers, "update", json_integer(rip_cfg->timers.update)); + json_object_set_new(timers, "timeout", json_integer(rip_cfg->timers.invalid)); + json_object_set_new(timers, "garbage-collection", json_integer(rip_cfg->timers.flush)); + json_object_set_new(instance, "timers", timers); + } + + /* Default route origination */ + if (rip_cfg->default_route) + json_object_set_new(instance, "default-information-originate", json_true()); + + /* Interfaces (network statements) */ + array = json_array(); + TAILQ_FOREACH(net, &rip_cfg->networks, entries) + json_array_append_new(array, json_string(net->ifname)); + if (json_array_size(array) > 0) + json_object_set_new(instance, "interface", array); + else + json_decref(array); + + /* Explicit neighbors */ + array = json_array(); + TAILQ_FOREACH(nbr, &rip_cfg->neighbors, entries) { + inet_ntop(AF_INET, &nbr->addr, addr_buf, sizeof(addr_buf)); + json_array_append_new(array, json_string(addr_buf)); + } + if (json_array_size(array) > 0) + json_object_set_new(instance, "explicit-neighbor", array); + else + json_decref(array); + + /* Redistribution */ + array = json_array(); + TAILQ_FOREACH(redist, &rip_cfg->redistributes, entries) { + proto = NULL; + switch (redist->type) { + case RIP_REDIST_CONNECTED: + proto = "connected"; + break; + case RIP_REDIST_STATIC: + proto = "static"; + break; + case RIP_REDIST_KERNEL: + proto = "kernel"; + break; + case RIP_REDIST_OSPF: + proto = "ospf"; + break; + } + + if (proto) { + json_t *redist_obj = json_object(); + json_object_set_new(redist_obj, "protocol", json_string(proto)); + json_array_append_new(array, redist_obj); + } + } + if (json_array_size(array) > 0) + json_object_set_new(instance, "redistribute", array); + else + json_decref(array); + + /* Passive interfaces */ + array = json_array(); + TAILQ_FOREACH(net, &rip_cfg->networks, entries) { + if (net->passive) + json_array_append_new(array, json_string(net->ifname)); + } + if (json_array_size(array) > 0) + json_object_set_new(instance, "passive-interface", array); + else + json_decref(array); + + json_array_append_new(instance_array, instance); + json_object_set_new(ripd_obj, "instance", instance_array); + json_object_set_new(root, "frr-ripd:ripd", ripd_obj); + + result = json_dumps(root, JSON_COMPACT); + json_decref(root); + + if (!result) + return NULL; + + if (strlen(result) >= JSON_BUFFER_SIZE) { + ERROR("json_builder: buffer overflow (needed %zu, have %d)", strlen(result), JSON_BUFFER_SIZE); + free(result); + return NULL; + } + + strncpy(json_buffer, result, JSON_BUFFER_SIZE - 1); + json_buffer[JSON_BUFFER_SIZE - 1] = '\0'; + free(result); + + return json_buffer; +} + +/* + * Build complete routing configuration JSON with both static routes and RIP. + * Static routes go in /frr-routing:routing/control-plane-protocols + * RIP goes in /frr-ripd:ripd/instance (top level, separate container) + */ +const char *build_routing_json(struct route_head *routes, struct rip_config *rip_cfg) +{ + char prefix_str[INET6_ADDRSTRLEN + 4]; + char addr_buf[INET6_ADDRSTRLEN]; + struct route *r, *curr; + json_t *root; + char *result; + + root = json_object(); + if (!root) + return NULL; + + /* Add static routes */ + if (routes && !TAILQ_EMPTY(routes)) { + json_t *control_plane_protocols = json_object(); + json_t *control_plane_protocol = json_array(); + json_t *frr_routing = json_object(); + + json_t *protocol = json_object(); + json_object_set_new(protocol, "type", json_string("frr-staticd:staticd")); + json_object_set_new(protocol, "name", json_string("staticd")); + json_object_set_new(protocol, "vrf", json_string("default")); + + json_t *frr_staticd = json_object(); + json_t *route_list = json_array(); + + r = TAILQ_FIRST(routes); + while (r != NULL) { + const char *afi; + + if (r->family == AF_INET) { + inet_ntop(AF_INET, &r->prefix.ip4, addr_buf, sizeof(addr_buf)); + afi = "frr-routing:ipv4-unicast"; + } else { + inet_ntop(AF_INET6, &r->prefix.ip6, addr_buf, sizeof(addr_buf)); + afi = "frr-routing:ipv6-unicast"; + } + snprintf(prefix_str, sizeof(prefix_str), "%s/%d", addr_buf, (int)r->prefixlen); + + json_t *route_entry = json_object(); + json_object_set_new(route_entry, "prefix", json_string(prefix_str)); + json_object_set_new(route_entry, "src-prefix", json_string("::/0")); + json_object_set_new(route_entry, "afi-safi", json_string(afi)); + + json_t *path_list = json_array(); + + curr = r; + while (curr != NULL && same_prefix(r, curr)) { + json_t *path = json_object(); + json_object_set_new(path, "table-id", json_integer(0)); + json_object_set_new(path, "distance", json_integer((int)curr->distance)); + + json_t *frr_nexthops = json_object(); + json_t *nexthop_array = json_array(); + json_t *nexthop = json_object(); + + switch (curr->nh_type) { + case NH_ADDR: + if (curr->family == AF_INET) + inet_ntop(AF_INET, &curr->gateway.gw4, addr_buf, sizeof(addr_buf)); + else + inet_ntop(AF_INET6, &curr->gateway.gw6, addr_buf, sizeof(addr_buf)); + + json_object_set_new(nexthop, "nh-type", json_string(curr->family == AF_INET ? "ip4" : "ip6")); + json_object_set_new(nexthop, "vrf", json_string("default")); + json_object_set_new(nexthop, "gateway", json_string(addr_buf)); + json_object_set_new(nexthop, "interface", json_string("")); + break; + + case NH_IFNAME: + json_object_set_new(nexthop, "nh-type", json_string("ifindex")); + json_object_set_new(nexthop, "vrf", json_string("default")); + json_object_set_new(nexthop, "gateway", json_string("")); + json_object_set_new(nexthop, "interface", json_string(curr->ifname)); + break; + + case NH_BLACKHOLE: + json_object_set_new(nexthop, "nh-type", json_string("blackhole")); + json_object_set_new(nexthop, "vrf", json_string("default")); + json_object_set_new(nexthop, "gateway", json_string("")); + json_object_set_new(nexthop, "interface", json_string("")); + + switch (curr->bh_type) { + case BH_DROP: + json_object_set_new(nexthop, "bh-type", json_string("null")); + break; + case BH_REJECT: + json_object_set_new(nexthop, "bh-type", json_string("reject")); + break; + case BH_NULL: + json_object_set_new(nexthop, "bh-type", json_string("unspec")); + break; + } + break; + } + + json_array_append_new(nexthop_array, nexthop); + json_object_set_new(frr_nexthops, "nexthop", nexthop_array); + json_object_set_new(path, "frr-nexthops", frr_nexthops); + json_array_append_new(path_list, path); + + curr = TAILQ_NEXT(curr, entries); + } + + json_object_set_new(route_entry, "path-list", path_list); + json_array_append_new(route_list, route_entry); + + r = curr; + } + + json_object_set_new(frr_staticd, "route-list", route_list); + json_object_set_new(protocol, "frr-staticd:staticd", frr_staticd); + json_array_append_new(control_plane_protocol, protocol); + json_object_set_new(control_plane_protocols, "control-plane-protocol", control_plane_protocol); + json_object_set_new(frr_routing, "control-plane-protocols", control_plane_protocols); + json_object_set_new(root, "frr-routing:routing", frr_routing); + } + + /* Add RIP config (separate top-level container) */ + if (rip_cfg && rip_cfg->enabled) { + struct rip_redistribute *redist; + struct rip_neighbor *nbr; + struct rip_network *net; + const char *proto; + json_t *array; + + json_t *ripd_obj = json_object(); + json_t *instance_array = json_array(); + json_t *instance = json_object(); + + json_object_set_new(instance, "vrf", json_string("default")); + + if (rip_cfg->default_metric != 1) + json_object_set_new(instance, "default-metric", json_integer((int)rip_cfg->default_metric)); + + if (rip_cfg->distance != 120) { + json_t *distance_obj = json_object(); + json_object_set_new(distance_obj, "default", json_integer((int)rip_cfg->distance)); + json_object_set_new(instance, "distance", distance_obj); + } + + if (rip_cfg->timers.update != 30 || rip_cfg->timers.invalid != 180 || + rip_cfg->timers.flush != 240) { + json_t *timers = json_object(); + json_object_set_new(timers, "update", json_integer(rip_cfg->timers.update)); + json_object_set_new(timers, "timeout", json_integer(rip_cfg->timers.invalid)); + json_object_set_new(timers, "garbage-collection", json_integer(rip_cfg->timers.flush)); + json_object_set_new(instance, "timers", timers); + } + + if (rip_cfg->default_route) + json_object_set_new(instance, "default-information-originate", json_true()); + + array = json_array(); + TAILQ_FOREACH(net, &rip_cfg->networks, entries) + json_array_append_new(array, json_string(net->ifname)); + if (json_array_size(array) > 0) + json_object_set_new(instance, "interface", array); + else + json_decref(array); + + array = json_array(); + TAILQ_FOREACH(nbr, &rip_cfg->neighbors, entries) { + inet_ntop(AF_INET, &nbr->addr, addr_buf, sizeof(addr_buf)); + json_array_append_new(array, json_string(addr_buf)); + } + if (json_array_size(array) > 0) + json_object_set_new(instance, "explicit-neighbor", array); + else + json_decref(array); + + array = json_array(); + TAILQ_FOREACH(redist, &rip_cfg->redistributes, entries) { + proto = NULL; + switch (redist->type) { + case RIP_REDIST_CONNECTED: + proto = "connected"; + break; + case RIP_REDIST_STATIC: + proto = "static"; + break; + case RIP_REDIST_KERNEL: + proto = "kernel"; + break; + case RIP_REDIST_OSPF: + proto = "ospf"; + break; + } + + if (proto) { + json_t *redist_obj = json_object(); + json_object_set_new(redist_obj, "protocol", json_string(proto)); + json_array_append_new(array, redist_obj); + } + } + if (json_array_size(array) > 0) + json_object_set_new(instance, "redistribute", array); + else + json_decref(array); + + array = json_array(); + TAILQ_FOREACH(net, &rip_cfg->networks, entries) { + if (net->passive) + json_array_append_new(array, json_string(net->ifname)); + } + if (json_array_size(array) > 0) + json_object_set_new(instance, "passive-interface", array); + else + json_decref(array); + + json_array_append_new(instance_array, instance); + json_object_set_new(ripd_obj, "instance", instance_array); + json_object_set_new(root, "frr-ripd:ripd", ripd_obj); + } + + result = json_dumps(root, JSON_COMPACT); + json_decref(root); + + if (!result) + return NULL; + + if (strlen(result) >= JSON_BUFFER_SIZE) { + ERROR("json_builder: buffer overflow (needed %zu, have %d)", strlen(result), JSON_BUFFER_SIZE); + free(result); + return NULL; + } + + strncpy(json_buffer, result, JSON_BUFFER_SIZE - 1); + json_buffer[JSON_BUFFER_SIZE - 1] = '\0'; + free(result); + + return json_buffer; +} diff --git a/src/netd/src/json_builder.h b/src/netd/src/json_builder.h new file mode 100644 index 000000000..21517a6fc --- /dev/null +++ b/src/netd/src/json_builder.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_JSON_BUILDER_H_ +#define NETD_JSON_BUILDER_H_ + +#include "netd.h" + +/* + * Build JSON configuration for FRR routing daemons. + * Returns pointer to static buffer containing JSON string. + * Buffer is valid until next call to any build_*_json function. + */ + +const char *build_staticd_json(struct route_head *routes); +const char *build_rip_json(struct rip_config *rip_cfg); +const char *build_routing_json(struct route_head *routes, struct rip_config *rip_cfg); + +#endif /* NETD_JSON_BUILDER_H_ */ diff --git a/src/netd/src/linux_backend.c b/src/netd/src/linux_backend.c new file mode 100644 index 000000000..9e69ad2c3 --- /dev/null +++ b/src/netd/src/linux_backend.c @@ -0,0 +1,452 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +/* + * Linux kernel backend for netd - sets routes directly in kernel + * via rtnetlink. Does not use FRR. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "netd.h" +#include "linux_backend.h" + +static int nl_sock = -1; +static uint32_t nl_seq = 0; + +/* Helper: add rtnetlink attribute */ +static void rta_add(struct nlmsghdr *nlh, size_t maxlen, int type, const void *data, int len) +{ + size_t rtalen = RTA_LENGTH(len); + struct rtattr *rta; + + if (NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rtalen) > maxlen) { + ERROR("rtnetlink: attribute overflow"); + return; + } + + rta = (struct rtattr *)(((char *)nlh) + NLMSG_ALIGN(nlh->nlmsg_len)); + rta->rta_type = type; + rta->rta_len = rtalen; + if (len) + memcpy(RTA_DATA(rta), data, len); + nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rtalen); +} + +/* Send netlink message and wait for ACK */ +static int nl_talk(struct nlmsghdr *nlh) +{ + struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len }; + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct msghdr msg = { + .msg_name = &sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + struct nlmsgerr *err; + struct nlmsghdr *h; + char buf[4096]; + int ret; + + /* Send request */ + ret = sendmsg(nl_sock, &msg, 0); + if (ret < 0) { + ERROR("netlink sendmsg: %s", strerror(errno)); + return -1; + } + + /* Receive ACK */ + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + ret = recvmsg(nl_sock, &msg, 0); + if (ret < 0) { + ERROR("netlink recvmsg: %s", strerror(errno)); + return -1; + } + + /* Parse ACK */ + h = (struct nlmsghdr *)buf; + if (h->nlmsg_type == NLMSG_ERROR) { + err = (struct nlmsgerr *)NLMSG_DATA(h); + if (err->error) { + errno = -err->error; + return -1; + } + return 0; /* Success */ + } + + ERROR("netlink: unexpected response type %d", h->nlmsg_type); + return -1; +} + +/* Add or delete a route */ +static int netlink_route_op(const struct route *r, int cmd) +{ + char addrstr[INET6_ADDRSTRLEN]; + struct nlmsghdr *nlh; + struct rtmsg *rtm; + uint32_t ifindex; + char buf[4096]; + + memset(buf, 0, sizeof(buf)); + nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlh->nlmsg_type = cmd; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; + if (cmd == RTM_NEWROUTE) + nlh->nlmsg_flags |= NLM_F_CREATE | NLM_F_REPLACE; + nlh->nlmsg_seq = ++nl_seq; + + rtm = (struct rtmsg *)NLMSG_DATA(nlh); + rtm->rtm_family = r->family; + rtm->rtm_dst_len = r->prefixlen; + rtm->rtm_table = RT_TABLE_MAIN; + rtm->rtm_protocol = RTPROT_STATIC; + rtm->rtm_scope = RT_SCOPE_UNIVERSE; + rtm->rtm_type = RTN_UNICAST; + + /* Destination prefix */ + if (r->family == AF_INET) + rta_add(nlh, sizeof(buf), RTA_DST, &r->prefix.ip4, sizeof(r->prefix.ip4)); + else + rta_add(nlh, sizeof(buf), RTA_DST, &r->prefix.ip6, sizeof(r->prefix.ip6)); + + /* Nexthop */ + switch (r->nh_type) { + case NH_ADDR: + /* Gateway address */ + if (r->family == AF_INET) { + rta_add(nlh, sizeof(buf), RTA_GATEWAY, &r->gateway.gw4, sizeof(r->gateway.gw4)); + inet_ntop(AF_INET, &r->gateway.gw4, addrstr, sizeof(addrstr)); + } else { + rta_add(nlh, sizeof(buf), RTA_GATEWAY, &r->gateway.gw6, sizeof(r->gateway.gw6)); + inet_ntop(AF_INET6, &r->gateway.gw6, addrstr, sizeof(addrstr)); + } + DEBUG("netlink: %s route via %s", + cmd == RTM_NEWROUTE ? "add" : "del", addrstr); + break; + + case NH_IFNAME: + /* Output interface */ + ifindex = if_nametoindex(r->ifname); + if (!ifindex) { + ERROR("netlink: interface %s not found", r->ifname); + return -1; + } + rta_add(nlh, sizeof(buf), RTA_OIF, &ifindex, sizeof(ifindex)); + DEBUG("netlink: %s route dev %s", + cmd == RTM_NEWROUTE ? "add" : "del", r->ifname); + break; + + case NH_BLACKHOLE: + /* Blackhole route */ + switch (r->bh_type) { + case BH_DROP: + rtm->rtm_type = RTN_BLACKHOLE; + break; + case BH_REJECT: + rtm->rtm_type = RTN_UNREACHABLE; + break; + case BH_NULL: + rtm->rtm_type = RTN_BLACKHOLE; + break; + } + DEBUG("netlink: %s blackhole route", + cmd == RTM_NEWROUTE ? "add" : "del"); + break; + } + + /* Priority (metric/distance) - kernel expects 32-bit value */ + if (r->distance) { + uint32_t priority = r->distance; + + rta_add(nlh, sizeof(buf), RTA_PRIORITY, &priority, sizeof(priority)); + } + + /* Send and wait for ACK */ + if (nl_talk(nlh)) { + if (errno == EEXIST && cmd == RTM_NEWROUTE) { + DEBUG("netlink: route already exists"); + return 0; + } + if (errno == ESRCH && cmd == RTM_DELROUTE) { + DEBUG("netlink: route doesn't exist"); + return 0; + } + ERROR("netlink: %s route failed: %s", + cmd == RTM_NEWROUTE ? "add" : "del", strerror(errno)); + return -1; + } + + return 0; +} + +int netlink_route_add(const struct route *r) +{ + return netlink_route_op(r, RTM_NEWROUTE); +} + +int netlink_route_del(const struct route *r) +{ + return netlink_route_op(r, RTM_DELROUTE); +} + +int linux_backend_init(void) +{ + struct sockaddr_nl sa = { + .nl_family = AF_NETLINK, + }; + + INFO("Using Linux kernel backend (direct rtnetlink, no FRR)"); + + nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (nl_sock < 0) { + ERROR("Failed to create netlink socket: %s", strerror(errno)); + return -1; + } + + if (bind(nl_sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { + ERROR("Failed to bind netlink socket: %s", strerror(errno)); + close(nl_sock); + nl_sock = -1; + return -1; + } + + DEBUG("Linux backend initialized, netlink socket fd=%d", nl_sock); + return 0; +} + +void linux_backend_cleanup(void) +{ + if (nl_sock >= 0) { + close(nl_sock); + nl_sock = -1; + DEBUG("Linux backend shutdown"); + } +} + +/* Check if route exists in list */ +static int route_exists(struct route_head *list, const struct route *needle) +{ + struct route *r; + + TAILQ_FOREACH(r, list, entries) { + if (r->family != needle->family) + continue; + if (r->prefixlen != needle->prefixlen) + continue; + + /* Compare prefix */ + if (r->family == AF_INET) { + if (memcmp(&r->prefix.ip4, &needle->prefix.ip4, sizeof(r->prefix.ip4))) + continue; + } else { + if (memcmp(&r->prefix.ip6, &needle->prefix.ip6, sizeof(r->prefix.ip6))) + continue; + } + + /* Compare nexthop type */ + if (r->nh_type != needle->nh_type) + continue; + + /* Compare nexthop details */ + switch (r->nh_type) { + case NH_ADDR: + if (r->family == AF_INET) { + if (memcmp(&r->gateway.gw4, &needle->gateway.gw4, sizeof(r->gateway.gw4))) + continue; + } else { + if (memcmp(&r->gateway.gw6, &needle->gateway.gw6, sizeof(r->gateway.gw6))) + continue; + } + break; + case NH_IFNAME: + if (strcmp(r->ifname, needle->ifname)) + continue; + break; + case NH_BLACKHOLE: + if (r->bh_type != needle->bh_type) + continue; + break; + } + + /* Match found */ + return 1; + } + + return 0; +} + +/* Read installed routes from kernel (proto=RTPROT_STATIC only) */ +static int kernel_read_routes(struct route_head *routes, int family) +{ + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct nlmsghdr *nlh; + struct rtattr *rta; + struct msghdr msg; + struct rtmsg *rtm; + struct iovec iov; + struct route *r; + char buf[8192]; + int rta_len; + int ret; + + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + /* Request route dump */ + memset(buf, 0, sizeof(buf)); + nlh = (struct nlmsghdr *)buf; + nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nlh->nlmsg_type = RTM_GETROUTE; + nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + nlh->nlmsg_seq = ++nl_seq; + + rtm = (struct rtmsg *)NLMSG_DATA(nlh); + rtm->rtm_family = family; + + iov.iov_base = nlh; + iov.iov_len = nlh->nlmsg_len; + + if (sendmsg(nl_sock, &msg, 0) < 0) { + ERROR("netlink: failed to request route dump: %s", strerror(errno)); + return -1; + } + + /* Read response */ + while (1) { + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + + ret = recvmsg(nl_sock, &msg, 0); + if (ret < 0) { + ERROR("netlink: route dump recvmsg: %s", strerror(errno)); + return -1; + } + + for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, ret); nlh = NLMSG_NEXT(nlh, ret)) { + if (nlh->nlmsg_type == NLMSG_DONE) + return 0; + + if (nlh->nlmsg_type == NLMSG_ERROR) { + ERROR("netlink: route dump error"); + return -1; + } + + if (nlh->nlmsg_type != RTM_NEWROUTE) + continue; + + rtm = (struct rtmsg *)NLMSG_DATA(nlh); + + /* Only handle routes we manage (proto=RTPROT_STATIC) */ + if (rtm->rtm_protocol != RTPROT_STATIC) + continue; + + /* Parse route attributes */ + r = calloc(1, sizeof(*r)); + if (!r) + continue; + + r->family = rtm->rtm_family; + r->prefixlen = rtm->rtm_dst_len; + + /* Parse attributes */ + rta = RTM_RTA(rtm); + rta_len = RTM_PAYLOAD(nlh); + + for (; RTA_OK(rta, rta_len); rta = RTA_NEXT(rta, rta_len)) { + switch (rta->rta_type) { + case RTA_DST: + if (r->family == AF_INET) + memcpy(&r->prefix.ip4, RTA_DATA(rta), sizeof(r->prefix.ip4)); + else + memcpy(&r->prefix.ip6, RTA_DATA(rta), sizeof(r->prefix.ip6)); + break; + + case RTA_GATEWAY: + r->nh_type = NH_ADDR; + if (r->family == AF_INET) + memcpy(&r->gateway.gw4, RTA_DATA(rta), sizeof(r->gateway.gw4)); + else + memcpy(&r->gateway.gw6, RTA_DATA(rta), sizeof(r->gateway.gw6)); + break; + + case RTA_OIF: + r->nh_type = NH_IFNAME; + if_indextoname(*(uint32_t *)RTA_DATA(rta), r->ifname); + break; + + case RTA_PRIORITY: + r->distance = *(uint32_t *)RTA_DATA(rta); + break; + } + } + + /* Detect blackhole routes */ + if (rtm->rtm_type == RTN_BLACKHOLE || rtm->rtm_type == RTN_UNREACHABLE) { + r->nh_type = NH_BLACKHOLE; + if (rtm->rtm_type == RTN_UNREACHABLE) + r->bh_type = BH_REJECT; + else + r->bh_type = BH_DROP; + } + + TAILQ_INSERT_TAIL(routes, r, entries); + } + } + + return 0; +} + +int linux_backend_apply(struct route_head *routes, struct rip_config *rip) +{ + struct route_head kernel_routes = TAILQ_HEAD_INITIALIZER(kernel_routes); + struct route *r, *tmp; + int removed = 0; + int errors = 0; + int added = 0; + + if (rip->enabled) + DEBUG("Linux backend: RIP not supported without FRR"); + + /* Read current static routes from kernel (both IPv4 and IPv6) */ + kernel_read_routes(&kernel_routes, AF_INET); + kernel_read_routes(&kernel_routes, AF_INET6); + + /* Remove routes no longer in config */ + TAILQ_FOREACH_SAFE(r, &kernel_routes, entries, tmp) { + if (!route_exists(routes, r)) { + DEBUG("Removing old route"); + if (netlink_route_del(r) == 0) + removed++; + } + } + + /* Add new routes from config (kernel_routes still has old state) */ + TAILQ_FOREACH(r, routes, entries) { + if (!route_exists(&kernel_routes, r)) { + DEBUG("Adding new route"); + if (netlink_route_add(r) == 0) + added++; + else + errors++; + } + } + + /* Free kernel routes list */ + while ((tmp = TAILQ_FIRST(&kernel_routes)) != NULL) { + TAILQ_REMOVE(&kernel_routes, tmp, entries); + free(tmp); + } + + INFO("Linux backend: +%d -%d routes (%d errors)", added, removed, errors); + return errors ? -1 : 0; +} diff --git a/src/netd/src/linux_backend.h b/src/netd/src/linux_backend.h new file mode 100644 index 000000000..97e066b4d --- /dev/null +++ b/src/netd/src/linux_backend.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_LINUX_BACKEND_H_ +#define NETD_LINUX_BACKEND_H_ + +#include "netd.h" + +int linux_backend_init(void); +void linux_backend_cleanup(void); +int linux_backend_apply(struct route_head *routes, struct rip_config *rip); + +/* Internal netlink operations */ +int netlink_route_add(const struct route *r); +int netlink_route_del(const struct route *r); + +#endif /* NETD_LINUX_BACKEND_H_ */ diff --git a/src/netd/src/netd.c b/src/netd/src/netd.c new file mode 100644 index 000000000..cab863728 --- /dev/null +++ b/src/netd/src/netd.c @@ -0,0 +1,273 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include + +#include "netd.h" +#include "config.h" + +/* Backend selection at compile time */ +#ifdef HAVE_FRR_GRPC +#include "grpc_backend.h" +static int backend_init(void) { return grpc_backend_init(); } +static void backend_cleanup(void) { grpc_backend_cleanup(); } +static int backend_apply(struct route_head *routes, struct rip_config *rip) { + return grpc_backend_apply(routes, rip); +} +#else +#include "linux_backend.h" +static int backend_init(void) { return linux_backend_init(); } +static void backend_cleanup(void) { linux_backend_cleanup(); } +static int backend_apply(struct route_head *routes, struct rip_config *rip) { + return linux_backend_apply(routes, rip); +} +#endif + +int debug; + +static volatile sig_atomic_t do_reload; +static volatile sig_atomic_t do_shutdown; + +static struct route_head active_routes = TAILQ_HEAD_INITIALIZER(active_routes); +static struct rip_config active_rip; + +static void sighup_handler(int sig) +{ + (void)sig; + do_reload = 1; +} + +static void sigterm_handler(int sig) +{ + (void)sig; + do_shutdown = 1; +} + +static void route_list_free(struct route_head *head) +{ + struct route *r, *tmp; + + TAILQ_FOREACH_SAFE(r, head, entries, tmp) { + TAILQ_REMOVE(head, r, entries); + free(r); + } +} + +static void rip_config_init(struct rip_config *cfg) +{ + memset(cfg, 0, sizeof(*cfg)); + + /* Set defaults */ + cfg->default_metric = 1; + cfg->distance = 120; + cfg->timers.update = 30; + cfg->timers.invalid = 180; + cfg->timers.flush = 240; + + TAILQ_INIT(&cfg->networks); + TAILQ_INIT(&cfg->neighbors); + TAILQ_INIT(&cfg->redistributes); + TAILQ_INIT(&cfg->system_cmds); +} + +static void rip_config_free(struct rip_config *cfg) +{ + struct rip_redistribute *redist, *redist_tmp; + struct rip_system_cmd *cmd, *cmd_tmp; + struct rip_neighbor *nbr, *nbr_tmp; + struct rip_network *net, *net_tmp; + + if (!cfg) + return; + + TAILQ_FOREACH_SAFE(net, &cfg->networks, entries, net_tmp) { + TAILQ_REMOVE(&cfg->networks, net, entries); + free(net); + } + + TAILQ_FOREACH_SAFE(nbr, &cfg->neighbors, entries, nbr_tmp) { + TAILQ_REMOVE(&cfg->neighbors, nbr, entries); + free(nbr); + } + + TAILQ_FOREACH_SAFE(redist, &cfg->redistributes, entries, redist_tmp) { + TAILQ_REMOVE(&cfg->redistributes, redist, entries); + free(redist); + } + + TAILQ_FOREACH_SAFE(cmd, &cfg->system_cmds, entries, cmd_tmp) { + TAILQ_REMOVE(&cfg->system_cmds, cmd, entries); + free(cmd); + } +} + +static void reload(void) +{ + struct route_head new_routes = TAILQ_HEAD_INITIALIZER(new_routes); + struct rip_redistribute *redist; + struct rip_system_cmd *cmd; + struct rip_config new_rip; + struct rip_neighbor *nbr; + struct rip_network *net; + struct route *r; + int count = 0; + + INFO("Reloading configuration"); + + rip_config_init(&new_rip); + + if (config_load(&new_routes, &new_rip)) { + ERROR("Failed loading config, keeping current routes"); + route_list_free(&new_routes); + rip_config_free(&new_rip); + return; + } + + TAILQ_FOREACH(r, &new_routes, entries) + count++; + DEBUG("Loaded %d routes from config", count); + if (new_rip.enabled) + DEBUG("RIP configuration loaded"); + + /* Apply config via backend */ + if (backend_apply(&new_routes, &new_rip)) { + ERROR("Failed applying config via backend"); + route_list_free(&new_routes); + rip_config_free(&new_rip); + return; + } + + route_list_free(&active_routes); + TAILQ_INIT(&active_routes); + rip_config_free(&active_rip); + rip_config_init(&active_rip); + + /* Move new_routes to active_routes */ + while ((r = TAILQ_FIRST(&new_routes)) != NULL) { + TAILQ_REMOVE(&new_routes, r, entries); + TAILQ_INSERT_TAIL(&active_routes, r, entries); + } + + /* Move new_rip to active_rip - copy scalars and move lists */ + active_rip.enabled = new_rip.enabled; + active_rip.default_metric = new_rip.default_metric; + active_rip.distance = new_rip.distance; + active_rip.default_route = new_rip.default_route; + active_rip.debug_events = new_rip.debug_events; + active_rip.debug_packet = new_rip.debug_packet; + active_rip.debug_kernel = new_rip.debug_kernel; + active_rip.timers = new_rip.timers; + + /* Move network list */ + while ((net = TAILQ_FIRST(&new_rip.networks)) != NULL) { + TAILQ_REMOVE(&new_rip.networks, net, entries); + TAILQ_INSERT_TAIL(&active_rip.networks, net, entries); + } + + /* Move neighbor list */ + while ((nbr = TAILQ_FIRST(&new_rip.neighbors)) != NULL) { + TAILQ_REMOVE(&new_rip.neighbors, nbr, entries); + TAILQ_INSERT_TAIL(&active_rip.neighbors, nbr, entries); + } + + /* Move redistribute list */ + while ((redist = TAILQ_FIRST(&new_rip.redistributes)) != NULL) { + TAILQ_REMOVE(&new_rip.redistributes, redist, entries); + TAILQ_INSERT_TAIL(&active_rip.redistributes, redist, entries); + } + + /* Move system commands list */ + while ((cmd = TAILQ_FIRST(&new_rip.system_cmds)) != NULL) { + TAILQ_REMOVE(&new_rip.system_cmds, cmd, entries); + TAILQ_INSERT_TAIL(&active_rip.system_cmds, cmd, entries); + } + + /* Execute system commands after config is applied. + * Run in background with retry since daemons may not be ready yet. */ + if (!TAILQ_EMPTY(&active_rip.system_cmds)) { + TAILQ_FOREACH(cmd, &active_rip.system_cmds, entries) { + char retry_cmd[512]; + + snprintf(retry_cmd, sizeof(retry_cmd), + "(for i in 1 2 3 4 5; do %s && break || sleep 1; done) &", + cmd->command); + DEBUG("Executing system command with retry: %s", cmd->command); + if (system(retry_cmd) != 0) + ERROR("Failed to launch system command: %s", cmd->command); + } + } + pidfile(NULL); + INFO("Configuration reloaded"); +} + +static int usage(int rc) +{ + fprintf(stderr, + "Usage: netd [-dh]\n" + " -d Enable debug (log to stderr)\n" + " -h Show this help text\n"); + return rc; +} + +int main(int argc, char *argv[]) +{ + int log_opts = LOG_PID | LOG_NDELAY; + struct sigaction sa; + int c; + + while ((c = getopt(argc, argv, "dhp:")) != -1) { + switch (c) { + case 'd': + log_opts |= LOG_PERROR; + debug = 1; + break; + case 'h': + return usage(0); + default: + return usage(1); + } + } + + openlog("netd", log_opts, LOG_DAEMON); + INFO("netd starting"); + + /* Set up signal handlers */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sighup_handler; + sigaction(SIGHUP, &sa, NULL); + + sa.sa_handler = sigterm_handler; + sigaction(SIGTERM, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + if (backend_init()) { + ERROR("Failed to initialize backend"); + closelog(); + return 1; + } + + TAILQ_INIT(&active_routes); + rip_config_init(&active_rip); + + /* Initial load */ + do_reload = 1; + + pidfile(NULL); + while (!do_shutdown) { + if (do_reload) { + do_reload = 0; + reload(); + } + pause(); + } + + INFO("netd shutting down"); + + route_list_free(&active_routes); + rip_config_free(&active_rip); + backend_cleanup(); + + closelog(); + return 0; +} diff --git a/src/netd/src/netd.h b/src/netd/src/netd.h new file mode 100644 index 000000000..99fb4ab21 --- /dev/null +++ b/src/netd/src/netd.h @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_H_ +#define NETD_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern int debug; + +#define LOG(level, fmt, args...) syslog(level, fmt, ##args) +#define ERROR(fmt, args...) LOG(LOG_ERR, fmt, ##args) +#define INFO(fmt, args...) LOG(LOG_INFO, fmt, ##args) +#define DEBUG(fmt, args...) do { if (debug) LOG(LOG_DEBUG, fmt, ##args); } while (0) + +#define CONF_DIR "/etc/netd/conf.d" + +/* Nexthop types */ +enum nh_type { + NH_IFNAME, /* Nexthop is interface name */ + NH_ADDR, /* Nexthop is IP address */ + NH_BLACKHOLE, /* Blackhole route */ +}; + +/* Blackhole subtypes */ +enum bh_type { + BH_NULL, /* Null0 interface */ + BH_REJECT, /* ICMP unreachable */ + BH_DROP, /* Silent drop */ +}; + +/* Static route entry */ +struct route { + int family; /* AF_INET or AF_INET6 */ + uint8_t prefixlen; /* Prefix length */ + uint8_t distance; /* Administrative distance */ + uint32_t tag; /* Route tag */ + + union { + struct in_addr ip4; + struct in6_addr ip6; + } prefix; + + enum nh_type nh_type; + enum bh_type bh_type; /* For NH_BLACKHOLE */ + + union { + struct in_addr gw4; + struct in6_addr gw6; + } gateway; /* For NH_ADDR */ + + char ifname[IFNAMSIZ]; /* For NH_IFNAME */ + + TAILQ_ENTRY(route) entries; +}; + +/* Route list head */ +TAILQ_HEAD(route_head, route); + +/* RIP network/interface configuration */ +struct rip_network { + char ifname[IFNAMSIZ]; + int passive; /* Is this interface passive? */ + TAILQ_ENTRY(rip_network) entries; +}; + +/* RIP neighbor configuration */ +struct rip_neighbor { + struct in_addr addr; + TAILQ_ENTRY(rip_neighbor) entries; +}; + +/* RIP redistribution configuration */ +enum rip_redist_type { + RIP_REDIST_CONNECTED, + RIP_REDIST_STATIC, + RIP_REDIST_KERNEL, + RIP_REDIST_OSPF, +}; + +struct rip_redistribute { + enum rip_redist_type type; + TAILQ_ENTRY(rip_redistribute) entries; +}; + +/* System commands to execute after applying config */ +#define RIP_SYSTEM_CMD_MAX 256 +struct rip_system_cmd { + char command[RIP_SYSTEM_CMD_MAX]; + TAILQ_ENTRY(rip_system_cmd) entries; +}; + +/* RIP timers */ +struct rip_timers { + uint32_t update; /* Update interval (default 30) */ + uint32_t invalid; /* Invalid interval (default 180) */ + uint32_t flush; /* Flush interval (default 240) */ +}; + +/* Main RIP configuration */ +struct rip_config { + int enabled; /* Is RIP enabled? */ + uint8_t default_metric; /* Default metric (default 1) */ + uint8_t distance; /* Administrative distance (default 120) */ + int default_route; /* Originate default route? */ + int debug_events; /* Enable RIP events debug? */ + int debug_packet; /* Enable RIP packet debug? */ + int debug_kernel; /* Enable kernel routing debug? */ + + struct rip_timers timers; + + TAILQ_HEAD(, rip_network) networks; + TAILQ_HEAD(, rip_neighbor) neighbors; + TAILQ_HEAD(, rip_redistribute) redistributes; + TAILQ_HEAD(, rip_system_cmd) system_cmds; +}; + +#endif /* NETD_H_ */ From 4f5a946250a9391c0b56f55d1632925538bb6698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= Date: Sat, 7 Feb 2026 23:16:54 +0100 Subject: [PATCH 3/3] yanger: Wi-Fi: Add missing radio component in operational --- board/common/rootfs/usr/libexec/infix/iw.py | 8 ++++++++ src/statd/python/yanger/ietf_interfaces/wifi.py | 11 +++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/board/common/rootfs/usr/libexec/infix/iw.py b/board/common/rootfs/usr/libexec/infix/iw.py index 035dd53f0..8dfac86e3 100755 --- a/board/common/rootfs/usr/libexec/infix/iw.py +++ b/board/common/rootfs/usr/libexec/infix/iw.py @@ -290,6 +290,14 @@ def parse_interface_info(ifname): if power_match: result['txpower'] = float(power_match.group(1)) + # wiphy index -> phy/radio name + elif stripped.startswith('wiphy '): + try: + wiphy_idx = int(stripped.split()[1]) + result['phy'] = normalize_phy_name(f'phy{wiphy_idx}') + except (ValueError, IndexError): + pass + return result diff --git a/src/statd/python/yanger/ietf_interfaces/wifi.py b/src/statd/python/yanger/ietf_interfaces/wifi.py index ccf092d14..517b23b9a 100644 --- a/src/statd/python/yanger/ietf_interfaces/wifi.py +++ b/src/statd/python/yanger/ietf_interfaces/wifi.py @@ -93,10 +93,17 @@ def wifi(ifname): info = get_iw_info(ifname) mode = info.get('iftype', '').lower() + result = {} + + if info.get('phy'): + result['radio'] = info['phy'] + if mode == 'ap': - return wifi_ap(ifname) + result.update(wifi_ap(ifname)) else: - return wifi_station(ifname) + result.update(wifi_station(ifname)) + + return result def parse_wpa_scan_result(scan_output):