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..0a1ea8a05 --- /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 -p /run/netd.pid -- 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/share/udhcpc/default.script b/board/common/rootfs/usr/share/udhcpc/default.script index 84221f813..1d6e1278b 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" @@ -102,7 +102,8 @@ set_dhcp_routes() cmp -s "$NAME" "$NEXT" && return mv "$NEXT" "$NAME" - initctl -nbq restart staticd + killall -SIGHUP netd + killall staticd } clr_dhcp_routes() @@ -111,7 +112,8 @@ clr_dhcp_routes() [ -f "$NAME" ] || return rm "$NAME" - initctl -nbq restart staticd + killall -SIGHUP netd + killall staticd } 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 710dfee88..1402584ec 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 7bc7ffca7..6000f6145 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 101da83a4..0198dc901 100644 --- a/configs/arm_defconfig +++ b/configs/arm_defconfig @@ -144,6 +144,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 a94f38553..8d0a9620a 100644 --- a/configs/arm_minimal_defconfig +++ b/configs/arm_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/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 2408c74d8..c72379b77 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 2355c116f..a7d4fc504 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/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..44d8d0524 --- /dev/null +++ b/package/netd/Config.in @@ -0,0 +1,30 @@ +config BR2_PACKAGE_NETD + bool "netd" + select BR2_PACKAGE_LIBITE + 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..4444ddbfa --- /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 +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..bbc6789bb 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,4 @@ # 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..f862ca96c 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\ @@ -36,7 +35,7 @@ int parse_rip_redistribute(sr_session_ctx_t *session, struct lyd_node *redistrib LY_LIST_FOR(lyd_child(redistributes), tmp) { const char *protocol = lydx_get_cattr(tmp, "protocol"); - fprintf(fp, " redistribute %s\n", protocol); + fprintf(fp, "redistribute %s\n", protocol); } return 0; @@ -47,7 +46,7 @@ int parse_rip_interfaces(sr_session_ctx_t *session, struct lyd_node *interfaces, struct lyd_node *interface, *neighbors_node, *neighbor; int num_interfaces = 0; - /* First pass: network and passive-interface statements (inside router rip block) */ + /* Output network and passive-interface statements in netd format */ LY_LIST_FOR(lyd_child(interfaces), interface) { const char *name; int passive; @@ -58,82 +57,30 @@ int parse_rip_interfaces(sr_session_ctx_t *session, struct lyd_node *interfaces, passive = lydx_get_bool(interface, "passive"); - /* Enable RIP on the interface by adding it to the network statement */ - fprintf(fp, " network %s\n", name); + /* netd format: simple "network IFNAME" and "passive IFNAME" */ + fprintf(fp, "network %s\n", name); if (passive) - fprintf(fp, " passive-interface %s\n", name); + fprintf(fp, "passive %s\n", name); num_interfaces++; } - /* Handle explicit neighbors (inside router rip block) */ + /* Handle explicit neighbors */ 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); + 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 */ - } - } - } + /* Note: Per-interface settings like split-horizon, send/recv version + * are not yet supported in the netd simple format. These would need + * to be added to the netd RIP parser if required. */ return num_interfaces; } @@ -150,14 +97,10 @@ int parse_rip_timers(sr_session_ctx_t *session, struct lyd_node *timers, FILE *f 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 - */ + /* netd format: "timers update N invalid N flush N" */ DEBUG("Ignoring 'holddown interval %s' for now", holddown); if (update || invalid || flush) { - fprintf(fp, " timers basic %s %s %s\n", + fprintf(fp, "timers update %s invalid %s flush %s\n", update ? update : "30", invalid ? invalid : "180", flush ? flush : "240"); @@ -166,54 +109,23 @@ int parse_rip_timers(sr_session_ctx_t *session, struct lyd_node *timers, FILE *f return 0; } -int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip) +int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp) { - struct lyd_node *interfaces, *timers, *default_route, *debug; + struct lyd_node *interfaces, *timers, *default_route; 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); + /* Write [rip] section header for netd */ + fputs("\n[rip]\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, "default-metric %s\n", default_metric); distance = lydx_get_cattr(rip, "distance"); if (distance) - fprintf(fp, " distance %s\n", distance); + fprintf(fp, "distance %s\n", distance); /* Timers */ timers = lydx_get_child(rip, "timers"); @@ -222,24 +134,28 @@ int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip) /* 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("default-route enabled\n", 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("system vtysh -c 'debug rip events'\n", fp); + if (lydx_get_bool(debug, "packet")) + fputs("system vtysh -c 'debug rip packet'\n", fp); + if (lydx_get_bool(debug, "kernel")) + fputs("system vtysh -c 'debug rip zebra'\n", fp); + } /* Redistribution */ parse_rip_redistribute(session, lydx_get_child(rip, "redistribute"), fp); - /* Interfaces - must be done after router rip block and before interface blocks */ + /* Interfaces */ interfaces = lydx_get_child(rip, "interfaces"); if (interfaces) num_interfaces = parse_rip_interfaces(session, interfaces, fp); - fclose(fp); - - if (!num_interfaces) { - (void)remove(RIPD_CONF_NEXT); - return 0; - } - - return 0; + return num_interfaces; } int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp) @@ -361,8 +277,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 +333,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; } @@ -469,6 +381,9 @@ static int parse_static_routes(sr_session_ctx_t *session, struct lyd_node *paren struct lyd_node *ipv4, *v4routes, *ipv6, *v6routes, *route; int num_routes = 0; + /* Write [routes] section header for netd */ + fputs("[routes]\n", fp); + ipv4 = lydx_get_child(parent, "ipv4"); ipv6 = lydx_get_child(parent, "ipv6"); @@ -490,9 +405,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 +417,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 +484,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 +497,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..bea74513d --- /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 \ + src/route.c src/route.h src/rip.c src/rip.h +netd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -I$(srcdir)/src +netd_CFLAGS = -W -Wall -Wextra +netd_CFLAGS += $(libite_CFLAGS) +netd_LDADD = $(libite_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 \ + 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..ab9cf5733 --- /dev/null +++ b/src/netd/README.md @@ -0,0 +1,231 @@ +# 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 + +Configuration files are placed in `/etc/netd/conf.d/` with `.conf` extension. + +### Quick Start + +```ini +[routes] +ip route 10.0.0.0/24 192.168.1.1 1 +ipv6 route 2001:db8::/32 fe80::1 1 + +[rip] +network eth0 +redistribute connected +``` + +See [CONFIGURATION.md](CONFIGURATION.md) for complete documentation. + +### Sample Configuration + +Copy and customize the sample: + +```bash +sudo mkdir -p /etc/netd/conf.d +sudo cp netd.conf /etc/netd/conf.d/10-static.conf +sudo vi /etc/netd/conf.d/10-static.conf +``` + +### Reload + +Signal netd to reload configuration: + +```bash +sudo killall -HUP netd +# or +sudo initctl touch netd # with Finit +``` + +## Running + +### Foreground (Debug) + +```bash +netd -d +``` + +### Background + +With Finit init system: + +```bash +sudo initctl start netd +``` + +With systemd: + +```bash +sudo systemctl start netd +``` + +## 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 - Main daemon +│ ├── config.c - Config parser +│ ├── route.c/h - Route data structures +│ ├── rip.c/h - RIP data structures +│ ├── 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 +├── CONFIGURATION.md - Config format documentation +└── README.md - This file +``` + +## API + +### Configuration Format + +- Section-based INI format +- `[routes]` - Static routes +- `[rip]` - RIP configuration +- `[ospf]` - Reserved for future use + +### 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 +# View logs +sudo journalctl -u netd -f + +# 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 + +- [CONFIGURATION.md](CONFIGURATION.md) - Configuration format details +- FRR Documentation - https://docs.frrouting.org/ +- rtnetlink(7) - Linux routing socket API 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..eaace1ddd --- /dev/null +++ b/src/netd/configure.ac @@ -0,0 +1,86 @@ +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]) + +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" +#include "rip.h" + +enum config_section { + SECTION_NONE, + SECTION_ROUTES, + SECTION_RIP, + SECTION_OSPF, /* Reserved for future use */ +}; + +/* + * Parse one route line: "ip route PREFIX NEXTHOP [DISTANCE] [tag TAG]" + * or "ipv6 route PREFIX NEXTHOP [DISTANCE] [tag TAG]" + */ +static int parse_route_line(const char *line, struct route_head *head) +{ + char buf[512]; + char *tok, *saveptr, *prefix_str, *nh_str, *dist_str; + struct route *r; + char *slash; + int family; + + snprintf(buf, sizeof(buf), "%s", line); + + /* First token: "ip" or "ipv6" */ + tok = strtok_r(buf, " \t", &saveptr); + if (!tok) + return 0; + + if (!strcmp(tok, "ip")) + family = AF_INET; + else if (!strcmp(tok, "ipv6")) + family = AF_INET6; + else + return 0; /* skip unknown lines */ + + /* Second token: "route" */ + tok = strtok_r(NULL, " \t", &saveptr); + if (!tok || strcmp(tok, "route")) + return 0; + + /* Third token: PREFIX/LEN */ + prefix_str = strtok_r(NULL, " \t", &saveptr); + if (!prefix_str) + return -1; + + /* Fourth token: NEXTHOP */ + nh_str = strtok_r(NULL, " \t", &saveptr); + if (!nh_str) + return -1; + + /* Optional fifth token: DISTANCE */ + dist_str = strtok_r(NULL, " \t\n\r", &saveptr); + + /* Check for optional "tag TAG" */ + tok = strtok_r(NULL, " \t\n\r", &saveptr); + if (tok && !strcmp(tok, "tag")) { + /* Skip the tag value - we don't use it yet */ + strtok_r(NULL, " \t\n\r", &saveptr); + } + + r = calloc(1, sizeof(*r)); + if (!r) + return -1; + + r->family = family; + r->distance = dist_str ? (uint8_t)atoi(dist_str) : 1; + + /* Parse prefix */ + slash = strchr(prefix_str, '/'); + if (!slash) { + free(r); + return -1; + } + *slash = '\0'; + r->prefixlen = (uint8_t)atoi(slash + 1); + + if (family == AF_INET) { + if (inet_pton(AF_INET, prefix_str, &r->prefix.ip4) != 1) { + free(r); + return -1; + } + } else { + if (inet_pton(AF_INET6, prefix_str, &r->prefix.ip6) != 1) { + free(r); + return -1; + } + } + + /* Parse nexthop */ + if (!strcmp(nh_str, "blackhole")) { + r->nh_type = NH_BLACKHOLE; + r->bh_type = BH_DROP; + } else if (!strcmp(nh_str, "reject")) { + r->nh_type = NH_BLACKHOLE; + r->bh_type = BH_REJECT; + } else if (!strcmp(nh_str, "Null0")) { + r->nh_type = NH_BLACKHOLE; + r->bh_type = BH_NULL; + } else { + /* Try as IP address first */ + struct in_addr a4; + struct in6_addr a6; + + if (family == AF_INET && inet_pton(AF_INET, nh_str, &a4) == 1) { + r->nh_type = NH_ADDR; + r->gateway.gw4 = a4; + } else if (family == AF_INET6 && inet_pton(AF_INET6, nh_str, &a6) == 1) { + 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", nh_str); + } + } + + TAILQ_INSERT_TAIL(head, r, entries); + return 0; +} + +/* + * Parse one RIP config line: "network IFNAME" + * or "passive IFNAME" + * or "neighbor ADDRESS" + * or "redistribute TYPE" + * or "timers update N invalid N flush N" + * or "default-metric N" + * or "distance N" + * or "default-route enabled" + */ +static int parse_rip_line(const char *line, struct rip_config *cfg) +{ + char buf[512]; + char *tok, *saveptr; + + snprintf(buf, sizeof(buf), "%s", line); + + tok = strtok_r(buf, " \t", &saveptr); + if (!tok) + return 0; + + if (!strcmp(tok, "network")) { + char *ifname = strtok_r(NULL, " \t\n\r", &saveptr); + if (!ifname) + return -1; + + struct rip_network *net = calloc(1, sizeof(*net)); + if (!net) + return -1; + + snprintf(net->ifname, sizeof(net->ifname), "%s", ifname); + TAILQ_INSERT_TAIL(&cfg->networks, net, entries); + cfg->enabled = 1; + + } else if (!strcmp(tok, "passive")) { + char *ifname = strtok_r(NULL, " \t\n\r", &saveptr); + if (!ifname) + return -1; + + /* Find the network and mark it passive */ + struct rip_network *net; + TAILQ_FOREACH(net, &cfg->networks, entries) { + if (!strcmp(net->ifname, ifname)) { + net->passive = 1; + break; + } + } + /* If not found, create it */ + if (!net) { + net = calloc(1, sizeof(*net)); + if (!net) + return -1; + snprintf(net->ifname, sizeof(net->ifname), "%s", ifname); + net->passive = 1; + TAILQ_INSERT_TAIL(&cfg->networks, net, entries); + } + + } else if (!strcmp(tok, "neighbor")) { + char *addr_str = strtok_r(NULL, " \t\n\r", &saveptr); + if (!addr_str) + return -1; + + struct rip_neighbor *nbr = calloc(1, sizeof(*nbr)); + if (!nbr) + return -1; + + if (inet_pton(AF_INET, addr_str, &nbr->addr) != 1) { + free(nbr); + return -1; + } + + TAILQ_INSERT_TAIL(&cfg->neighbors, nbr, entries); + cfg->enabled = 1; + + } else if (!strcmp(tok, "redistribute")) { + char *type_str = strtok_r(NULL, " \t\n\r", &saveptr); + if (!type_str) + return -1; + + struct rip_redistribute *redist = calloc(1, sizeof(*redist)); + if (!redist) + return -1; + + 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 { + free(redist); + return -1; + } + + TAILQ_INSERT_TAIL(&cfg->redistributes, redist, entries); + cfg->enabled = 1; + + } else if (!strcmp(tok, "timers")) { + char *update_tok, *invalid_tok, *flush_tok; + + /* Expect: "timers update N invalid N flush N" */ + tok = strtok_r(NULL, " \t", &saveptr); /* "update" */ + if (!tok || strcmp(tok, "update")) + return -1; + update_tok = strtok_r(NULL, " \t", &saveptr); + if (!update_tok) + return -1; + + tok = strtok_r(NULL, " \t", &saveptr); /* "invalid" */ + if (!tok || strcmp(tok, "invalid")) + return -1; + invalid_tok = strtok_r(NULL, " \t", &saveptr); + if (!invalid_tok) + return -1; + + tok = strtok_r(NULL, " \t", &saveptr); /* "flush" */ + if (!tok || strcmp(tok, "flush")) + return -1; + flush_tok = strtok_r(NULL, " \t\n\r", &saveptr); + if (!flush_tok) + return -1; + + cfg->timers.update = (uint32_t)atoi(update_tok); + cfg->timers.invalid = (uint32_t)atoi(invalid_tok); + cfg->timers.flush = (uint32_t)atoi(flush_tok); + + } else if (!strcmp(tok, "default-metric")) { + char *metric_str = strtok_r(NULL, " \t\n\r", &saveptr); + if (!metric_str) + return -1; + cfg->default_metric = (uint8_t)atoi(metric_str); + + } else if (!strcmp(tok, "distance")) { + char *dist_str = strtok_r(NULL, " \t\n\r", &saveptr); + if (!dist_str) + return -1; + cfg->distance = (uint8_t)atoi(dist_str); + + } else if (!strcmp(tok, "default-route")) { + char *enabled_str = strtok_r(NULL, " \t\n\r", &saveptr); + if (enabled_str && !strcmp(enabled_str, "enabled")) + cfg->default_route = 1; + + } else if (!strcmp(tok, "debug")) { + char *debug_type = strtok_r(NULL, " \t\n\r", &saveptr); + if (!debug_type) + return -1; + + if (!strcmp(debug_type, "events")) + cfg->debug_events = 1; + else if (!strcmp(debug_type, "packet")) + cfg->debug_packet = 1; + else if (!strcmp(debug_type, "kernel")) + cfg->debug_kernel = 1; + else + ERROR("Unknown RIP debug type: %s", debug_type); + + } else if (!strcmp(tok, "system")) { + /* System command to execute after config is applied */ + char *cmd_str = saveptr; /* Rest of the line */ + if (!cmd_str || !*cmd_str) { + ERROR("Empty system command"); + return -1; + } + + struct rip_system_cmd *cmd = calloc(1, sizeof(*cmd)); + if (!cmd) + return -1; + + snprintf(cmd->command, sizeof(cmd->command), "%s", cmd_str); + TAILQ_INSERT_TAIL(&cfg->system_cmds, cmd, entries); + cfg->enabled = 1; + + } else { + /* Unknown RIP command, skip */ + ERROR("Unknown RIP command: %s", tok); + return 0; + } + + return 0; +} + +static int config_parse_file(const char *path, struct route_head *routes, + struct rip_config *rip_cfg) +{ + char line[512]; + enum config_section section = SECTION_NONE; + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) { + ERROR("Failed opening %s: %s", path, strerror(errno)); + return -1; + } + + while (fgets(line, sizeof(line), fp)) { + char *p = line; + + /* Strip trailing newline/whitespace */ + line[strcspn(line, "\n\r")] = '\0'; + + /* Skip leading whitespace */ + while (isspace((unsigned char)*p)) + p++; + + /* Skip empty lines and comments */ + if (*p == '\0' || *p == '!' || *p == '#') + continue; + + /* Check for section markers */ + if (*p == '[') { + char *end = strchr(p, ']'); + if (end) { + *end = '\0'; + p++; /* skip '[' */ + + if (!strcmp(p, "routes")) { + section = SECTION_ROUTES; + DEBUG("Entered [routes] section"); + } else if (!strcmp(p, "rip")) { + section = SECTION_RIP; + DEBUG("Entered [rip] section"); + } else if (!strcmp(p, "ospf")) { + section = SECTION_OSPF; + DEBUG("Entered [ospf] section"); + } else { + section = SECTION_NONE; + DEBUG("Unknown section: [%s]", p); + } + + continue; + } + } + + /* Parse line based on current section */ + switch (section) { + case SECTION_ROUTES: + if (parse_route_line(p, routes)) + ERROR("Failed parsing route in %s: %s", path, line); + break; + + case SECTION_RIP: + DEBUG("Parsing RIP line: %s", p); + if (parse_rip_line(p, rip_cfg)) + ERROR("Failed parsing RIP config in %s: %s", path, line); + else + DEBUG("RIP line parsed OK, enabled=%d", rip_cfg->enabled); + break; + + case SECTION_OSPF: + /* Reserved for future OSPF support */ + DEBUG("OSPF section not yet implemented"); + break; + + case SECTION_NONE: + /* For backward compatibility, try parsing as route */ + if (strstr(p, "route")) + parse_route_line(p, routes); + break; + } + } + + fclose(fp); + return 0; +} + +int config_load(struct route_head *routes, struct rip_config *rip_cfg) +{ + struct dirent **namelist; + 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++) { + const char *name = namelist[i]->d_name; + const char *ext; + char path[PATH_MAX]; + + 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..b5bdad064 --- /dev/null +++ b/src/netd/src/config.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_CONFIG_H_ +#define NETD_CONFIG_H_ + +#include "route.h" +#include "rip.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..7b505d133 --- /dev/null +++ b/src/netd/src/grpc_backend.cc @@ -0,0 +1,607 @@ +/* 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 +#include + +#include "grpc/frr-northbound.grpc.pb.h" +#include "grpc/frr-northbound.pb.h" + +extern "C" { +#include "netd.h" +#include "route.h" +#include "rip.h" +#include "grpc_backend.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" + +/* + * 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). + */ +static std::string build_staticd_json(struct route_head *routes) +{ + struct route *r, *next; + char buf[INET6_ADDRSTRLEN]; + std::ostringstream json; + bool first_route = true; + + json << "{" + << "\"frr-routing:routing\":{" + << "\"control-plane-protocols\":{" + << "\"control-plane-protocol\":[{" + << "\"type\":\"frr-staticd:staticd\"," + << "\"name\":\"staticd\"," + << "\"vrf\":\"default\"," + << "\"frr-staticd:staticd\":{" + << "\"route-list\":["; + + r = TAILQ_FIRST(routes); + while (r != NULL) { + const char *afi; + + if (r->family == AF_INET) { + inet_ntop(AF_INET, &r->prefix.ip4, buf, sizeof(buf)); + afi = "frr-routing:ipv4-unicast"; + } else { + inet_ntop(AF_INET6, &r->prefix.ip6, buf, sizeof(buf)); + afi = "frr-routing:ipv6-unicast"; + } + + if (!first_route) + json << ","; + first_route = false; + + /* Start route-list entry */ + json << "{" + << "\"prefix\":\"" << buf << "/" << (int)r->prefixlen << "\"," + << "\"src-prefix\":\"::/0\"," /* FRR expects IPv6 format */ + << "\"afi-safi\":\"" << afi << "\"," + << "\"path-list\":["; + + /* Add all routes with the same prefix as path-list entries */ + bool first_path = true; + struct route *curr = r; + while (curr != NULL && same_prefix(r, curr)) { + if (!first_path) + json << ","; + first_path = false; + + /* Start path-list entry */ + json << "{" + << "\"table-id\":0," + << "\"distance\":" << (int)curr->distance << "," + << "\"frr-nexthops\":{\"nexthop\":[{"; + + /* Next-hop */ + switch (curr->nh_type) { + case NH_ADDR: + if (curr->family == AF_INET) + inet_ntop(AF_INET, &curr->gateway.gw4, buf, sizeof(buf)); + else + inet_ntop(AF_INET6, &curr->gateway.gw6, buf, sizeof(buf)); + + json << "\"nh-type\":\"" << (curr->family == AF_INET ? "ip4" : "ip6") << "\"," + << "\"vrf\":\"default\"," + << "\"gateway\":\"" << buf << "\"," + << "\"interface\":\"\""; + break; + + case NH_IFNAME: + json << "\"nh-type\":\"ifindex\"," + << "\"vrf\":\"default\"," + << "\"gateway\":\"\"," + << "\"interface\":\"" << curr->ifname << "\""; + break; + + case NH_BLACKHOLE: + json << "\"nh-type\":\"blackhole\"," + << "\"vrf\":\"default\"," + << "\"gateway\":\"\"," + << "\"interface\":\"\","; + + switch (curr->bh_type) { + case BH_DROP: + json << "\"bh-type\":\"null\""; + break; + case BH_REJECT: + json << "\"bh-type\":\"reject\""; + break; + case BH_NULL: + json << "\"bh-type\":\"unspec\""; + break; + } + break; + } + + /* Close nexthop object, nexthop array, frr-nexthops, and path-list entry */ + json << "}]}}"; + + /* Move to next route */ + curr = TAILQ_NEXT(curr, entries); + } + + /* Close path-list array and route-list entry */ + json << "]}"; + + /* Move r to the next different prefix */ + r = curr; + } + + /* Close all JSON structures */ + json << "]}}]}}}"; + + return json.str(); +} + +/* + * Build JSON configuration for RIP following FRR's YANG model + * RIP uses /frr-ripd:ripd/instance path, not control-plane-protocols + */ +static std::string build_rip_json(struct rip_config *rip_cfg) +{ + std::ostringstream json; + char buf[INET_ADDRSTRLEN]; + bool first; + + if (!rip_cfg || !rip_cfg->enabled) + return ""; + + json << "{" + << "\"frr-ripd:ripd\":{" + << "\"instance\":[{" + << "\"vrf\":\"default\","; + + /* Default metric */ + if (rip_cfg->default_metric != 1) + json << "\"default-metric\":" << (int)rip_cfg->default_metric << ","; + + /* Distance */ + if (rip_cfg->distance != 120) { + json << "\"distance\":{" + << "\"default\":" << (int)rip_cfg->distance + << "},"; + } + + /* Timers */ + if (rip_cfg->timers.update != 30 || rip_cfg->timers.invalid != 180 || + rip_cfg->timers.flush != 240) { + json << "\"timers\":{" + << "\"update\":" << rip_cfg->timers.update << "," + << "\"timeout\":" << rip_cfg->timers.invalid << "," + << "\"garbage-collection\":" << rip_cfg->timers.flush + << "},"; + } + + /* Default route origination */ + if (rip_cfg->default_route) { + json << "\"default-information-originate\":true,"; + } + + /* Note: Debug settings are handled via system commands in netd, + * not via northbound API, since FRR doesn't support debug config */ + + /* Interfaces (network statements) */ + struct rip_network *net; + first = true; + TAILQ_FOREACH(net, &rip_cfg->networks, entries) { + if (first) { + json << "\"interface\":["; + first = false; + } else { + json << ","; + } + json << "\"" << net->ifname << "\""; + } + if (!first) + json << "],"; + + /* Explicit neighbors */ + struct rip_neighbor *nbr; + first = true; + TAILQ_FOREACH(nbr, &rip_cfg->neighbors, entries) { + if (first) { + json << "\"explicit-neighbor\":["; + first = false; + } else { + json << ","; + } + inet_ntop(AF_INET, &nbr->addr, buf, sizeof(buf)); + json << "\"" << buf << "\""; + } + if (!first) + json << "],"; + + /* Redistribution */ + struct rip_redistribute *redist; + first = true; + TAILQ_FOREACH(redist, &rip_cfg->redistributes, entries) { + if (first) { + json << "\"redistribute\":["; + first = false; + } else { + json << ","; + } + + const char *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 << "{" + << "\"protocol\":\"" << proto << "\"" + << "}"; + } + } + if (!first) + json << "],"; + /* Passive interfaces - use /frr-interface augmentation */ + first = true; + TAILQ_FOREACH(net, &rip_cfg->networks, entries) { + if (!net->passive) + continue; + if (first) { + json << "\"passive-interface\":["; + first = false; + } else { + json << ","; + } + json << "\"" << net->ifname << "\""; + } + if (!first) + json << "],"; + + /* Remove trailing comma if present */ + std::string result = json.str(); + if (!result.empty() && result.back() == ',') + result.pop_back(); + + return result + "}]}}"; + +} + +/* + * 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) + */ +static std::string build_routing_json(struct route_head *routes, struct rip_config *rip_cfg) +{ + std::ostringstream json; + bool has_static = routes && !TAILQ_EMPTY(routes); + bool has_rip = rip_cfg && rip_cfg->enabled; + + json << "{"; + + /* Add static routes */ + if (has_static) { + json << "\"frr-routing:routing\":{" + << "\"control-plane-protocols\":{" + << "\"control-plane-protocol\":["; + /* Build staticd part (extract from build_staticd_json) */ + struct route *r, *next; + char buf[INET6_ADDRSTRLEN]; + bool first_route = true; + + json << "{" + << "\"type\":\"frr-staticd:staticd\"," + << "\"name\":\"staticd\"," + << "\"vrf\":\"default\"," + << "\"frr-staticd:staticd\":{" + << "\"route-list\":["; + + r = TAILQ_FIRST(routes); + while (r != NULL) { + const char *afi; + + if (r->family == AF_INET) { + inet_ntop(AF_INET, &r->prefix.ip4, buf, sizeof(buf)); + afi = "frr-routing:ipv4-unicast"; + } else { + inet_ntop(AF_INET6, &r->prefix.ip6, buf, sizeof(buf)); + afi = "frr-routing:ipv6-unicast"; + } + + if (!first_route) + json << ","; + first_route = false; + + json << "{" + << "\"prefix\":\"" << buf << "/" << (int)r->prefixlen << "\"," + << "\"src-prefix\":\"::/0\"," + << "\"afi-safi\":\"" << afi << "\"," + << "\"path-list\":["; + + bool first_path = true; + struct route *curr = r; + while (curr != NULL && same_prefix(r, curr)) { + if (!first_path) + json << ","; + first_path = false; + + json << "{" + << "\"table-id\":0," + << "\"distance\":" << (int)curr->distance << "," + << "\"frr-nexthops\":{\"nexthop\":[{"; + + switch (curr->nh_type) { + case NH_ADDR: + if (curr->family == AF_INET) + inet_ntop(AF_INET, &curr->gateway.gw4, buf, sizeof(buf)); + else + inet_ntop(AF_INET6, &curr->gateway.gw6, buf, sizeof(buf)); + + json << "\"nh-type\":\"" << (curr->family == AF_INET ? "ip4" : "ip6") << "\"," + << "\"vrf\":\"default\"," + << "\"gateway\":\"" << buf << "\"," + << "\"interface\":\"\""; + break; + + case NH_IFNAME: + json << "\"nh-type\":\"ifindex\"," + << "\"vrf\":\"default\"," + << "\"gateway\":\"\"," + << "\"interface\":\"" << curr->ifname << "\""; + break; + + case NH_BLACKHOLE: + json << "\"nh-type\":\"blackhole\"," + << "\"vrf\":\"default\"," + << "\"gateway\":\"\"," + << "\"interface\":\"\","; + + switch (curr->bh_type) { + case BH_DROP: + json << "\"bh-type\":\"null\""; + break; + case BH_REJECT: + json << "\"bh-type\":\"reject\""; + break; + case BH_NULL: + json << "\"bh-type\":\"unspec\""; + break; + } + break; + } + + json << "}]}}"; + curr = TAILQ_NEXT(curr, entries); + } + + json << "]}"; + r = curr; + } + + json << "]}}]}}"; + } + + /* Add RIP config (separate top-level container) */ + if (has_rip) { + if (has_static) + json << ","; + + std::string rip_json = build_rip_json(rip_cfg); + /* Remove the outer braces from rip_json since we're merging */ + if (rip_json.length() > 2) { + json << rip_json.substr(1, rip_json.length() - 2); + } + } + + json << "}"; + + return json.str(); +} + +extern "C" int grpc_backend_init(void) +{ + grpc::ChannelArguments args; + + /* 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 */ + frr::GetCapabilitiesRequest cap_req; + frr::GetCapabilitiesResponse cap_resp; + grpc::ClientContext cap_ctx; + + grpc::Status 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_fini(void) +{ + /* Clean up any pending candidates */ + if (g_candidate_id != 0 && g_stub) { + frr::DeleteCandidateRequest del_req; + frr::DeleteCandidateResponse del_resp; + grpc::ClientContext del_ctx; + + 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) +{ + if (!g_stub || !g_channel) { + ERROR("grpc: not initialized"); + return -1; + } + + /* Build JSON configuration for both static routes and RIP */ + std::string json_config = build_routing_json(routes, rip); + if (json_config.empty()) { + ERROR("grpc: failed to build JSON config"); + return -1; + } + + DEBUG("grpc: JSON config: %s", json_config.c_str()); + + /* Step 1: CreateCandidate */ + frr::CreateCandidateRequest create_req; + frr::CreateCandidateResponse create_resp; + grpc::ClientContext create_ctx; + + grpc::Status 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) */ + frr::LoadToCandidateRequest load_req; + frr::LoadToCandidateResponse load_resp; + grpc::ClientContext load_ctx; + + 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 */ + frr::DeleteCandidateRequest del_req; + frr::DeleteCandidateResponse del_resp; + grpc::ClientContext del_ctx; + 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 */ + frr::CommitRequest commit_req; + frr::CommitResponse commit_resp; + grpc::ClientContext commit_ctx; + + 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 */ + frr::DeleteCandidateRequest del_req; + frr::DeleteCandidateResponse del_resp; + grpc::ClientContext del_ctx; + 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 */ + frr::DeleteCandidateRequest del_req; + frr::DeleteCandidateResponse del_resp; + grpc::ClientContext del_ctx; + 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..fde8246d6 --- /dev/null +++ b/src/netd/src/grpc_backend.h @@ -0,0 +1,19 @@ +#ifndef NETD_GRPC_BACKEND_H_ +#define NETD_GRPC_BACKEND_H_ + +#include "route.h" +#include "rip.h" + +#ifdef __cplusplus +extern "C" { +#endif + +int grpc_backend_init(void); +void grpc_backend_fini(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/linux_backend.c b/src/netd/src/linux_backend.c new file mode 100644 index 000000000..3fb287f3a --- /dev/null +++ b/src/netd/src/linux_backend.c @@ -0,0 +1,446 @@ +/* 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) +{ + struct rtattr *rta; + size_t rtalen = RTA_LENGTH(len); + + 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 sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len }; + struct msghdr msg = { + .msg_name = &sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + 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 */ + struct nlmsghdr *h = (struct nlmsghdr *)buf; + if (h->nlmsg_type == NLMSG_ERROR) { + struct nlmsgerr *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 buf[4096]; + struct nlmsghdr *nlh; + struct rtmsg *rtm; + uint32_t ifindex; + char addrstr[INET6_ADDRSTRLEN]; + + 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_fini(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) +{ + char buf[8192]; + struct nlmsghdr *nlh; + struct rtmsg *rtm; + struct sockaddr_nl sa = { .nl_family = AF_NETLINK }; + struct iovec iov; + struct msghdr msg = { + .msg_name = &sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + int ret; + + /* 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 */ + struct route *r = calloc(1, sizeof(*r)); + if (!r) + continue; + + r->family = rtm->rtm_family; + r->prefixlen = rtm->rtm_dst_len; + + /* Parse attributes */ + struct rtattr *rta = RTM_RTA(rtm); + int 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 added = 0, removed = 0, errors = 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..0af907f55 --- /dev/null +++ b/src/netd/src/linux_backend.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_LINUX_BACKEND_H_ +#define NETD_LINUX_BACKEND_H_ + +#include "route.h" +#include "rip.h" + +int linux_backend_init(void); +void linux_backend_fini(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..2a28f2d0c --- /dev/null +++ b/src/netd/src/netd.c @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include + +#include "netd.h" +#include "config.h" +#include "route.h" +#include "rip.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_fini(void) { grpc_backend_fini(); } +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_fini(void) { linux_backend_fini(); } +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 reload(void) +{ + struct route_head new_routes = TAILQ_HEAD_INITIALIZER(new_routes); + struct rip_config new_rip; + 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 */ + struct rip_network *net; + 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 */ + struct rip_neighbor *nbr; + 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 */ + struct rip_redistribute *redist; + 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 */ + struct rip_system_cmd *cmd; + 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); + } + } + + INFO("Configuration reloaded"); +} + +static int usage(int rc) +{ + fprintf(stderr, + "Usage: netd [-dh] [-p pidfile]\n" + " -d Enable debug (log to stderr)\n" + " -h Show this help text\n" + " -p Set pidfile path (default: from program name)\n"); + return rc; +} + +int main(int argc, char *argv[]) +{ + int log_opts = LOG_PID | LOG_NDELAY; + const char *pidfn = NULL; + 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); + case 'p': + pidfn = optarg; + break; + 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; + + /* Signal readiness to Finit */ + pidfile(pidfn); + + 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_fini(); + + closelog(); + return 0; +} diff --git a/src/netd/src/netd.h b/src/netd/src/netd.h new file mode 100644 index 000000000..36e48e8a6 --- /dev/null +++ b/src/netd/src/netd.h @@ -0,0 +1,26 @@ +/* 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" + +#endif /* NETD_H_ */ diff --git a/src/netd/src/rip.c b/src/netd/src/rip.c new file mode 100644 index 000000000..d6ed90fa8 --- /dev/null +++ b/src/netd/src/rip.c @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include "rip.h" + +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); +} + +void rip_config_free(struct rip_config *cfg) +{ + struct rip_network *net, *net_tmp; + struct rip_neighbor *nbr, *nbr_tmp; + struct rip_redistribute *redist, *redist_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); + } + + struct rip_system_cmd *cmd, *cmd_tmp; + TAILQ_FOREACH_SAFE(cmd, &cfg->system_cmds, entries, cmd_tmp) { + TAILQ_REMOVE(&cfg->system_cmds, cmd, entries); + free(cmd); + } +} diff --git a/src/netd/src/rip.h b/src/netd/src/rip.h new file mode 100644 index 000000000..cce164bb1 --- /dev/null +++ b/src/netd/src/rip.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_RIP_H_ +#define NETD_RIP_H_ + +#include "netd.h" + +/* 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; +}; + +void rip_config_init(struct rip_config *cfg); +void rip_config_free(struct rip_config *cfg); + +#endif /* NETD_RIP_H_ */ diff --git a/src/netd/src/route.c b/src/netd/src/route.c new file mode 100644 index 000000000..071680730 --- /dev/null +++ b/src/netd/src/route.c @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include "route.h" + +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); + } +} diff --git a/src/netd/src/route.h b/src/netd/src/route.h new file mode 100644 index 000000000..b61607856 --- /dev/null +++ b/src/netd/src/route.h @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#ifndef NETD_ROUTE_H_ +#define NETD_ROUTE_H_ + +#include "netd.h" + +/* 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); + +void route_list_free(struct route_head *head); + +#endif /* NETD_ROUTE_H_ */