Merge pull request #1384 from kernelkit/new-frr

This commit is contained in:
Mattias Walström
2026-02-25 18:09:01 +01:00
committed by GitHub
74 changed files with 4509 additions and 859 deletions
+1
View File
@@ -0,0 +1 @@
MGMTD_ARGS="-A 127.0.0.1 -u frr -g frr --log syslog --log-level err -M grpc"
@@ -0,0 +1,2 @@
#set DEBUG=1
service [2345] <pid/confd,pid/staticd> name:netd netd -- Network route daemon
+1
View File
@@ -0,0 +1 @@
../available/netd.conf
+28
View File
@@ -0,0 +1,28 @@
# Default FRR daemons file for Infix - confd overwrites on routing changes.
# watchfrr, zebra, mgmtd, and staticd are always started by frrinit.sh.
ospfd=no
ripd=no
bfdd=no
bgpd=no
ospf6d=no
ripngd=no
isisd=no
pimd=no
pim6d=no
ldpd=no
eigrpd=no
babeld=no
vrrpd=no
pathd=no
vtysh_enable=yes
watchfrr_options="-l 4"
zebra_options=" -A 127.0.0.1 -s 90000000"
mgmtd_options=" -A 127.0.0.1"
ospfd_options=" -A 127.0.0.1"
ripd_options=" -A 127.0.0.1"
staticd_options="-A 127.0.0.1"
bfdd_options=" -A 127.0.0.1"
frr_profile="traditional"
+2
View File
@@ -0,0 +1,2 @@
# Replaces default frr.conf file
log syslog warnings
@@ -1,9 +0,0 @@
! Default settings for staticd, used for both
! confd generated routes, udhcpc and zeroconf
frr defaults traditional
hostname Router
password zebra
enable password zebra
no log unique-id
log syslog informational
log facility local2
@@ -290,6 +290,14 @@ def parse_interface_info(ifname):
if power_match:
result['txpower'] = float(power_match.group(1))
# wiphy index -> phy/radio name
elif stripped.startswith('wiphy '):
try:
wiphy_idx = int(stripped.split()[1])
result['phy'] = normalize_phy_name(f'phy{wiphy_idx}')
except (ValueError, IndexError):
pass
return result
@@ -1,15 +0,0 @@
#!/bin/sh
# Sort and collate all /etc/frr/static.d/*.conf files managed by confd,
# udhcpc, and avahi-autoipd before starting staticd.
DIRD=/etc/frr/static.d
NAME=/etc/frr/staticd.conf
NEXT=${NAME}+
rm -f "$NEXT"
find "$DIRD" -type f -name '*.conf' ! -name '*~' | sort | while read -r file; do
cat "$file" >> "$NEXT"
done
cmp -s "$NAME" "$NEXT" && exit 0
mv "$NEXT" "$NAME"
@@ -7,7 +7,7 @@ ACTION="$1"
IP_CACHE="/var/lib/misc/${interface}.cache"
RESOLV_CONF="/run/resolvconf/interfaces/${interface}.conf"
NTPFILE="/run/chrony/dhcp-sources.d/${interface}.sources"
NAME="/etc/frr/static.d/${interface}-dhcp.conf"
NAME="/etc/netd/conf.d/${interface}-dhcp.conf"
NEXT="${NAME}+"
[ -n "$broadcast" ] && BROADCAST="broadcast $broadcast"
@@ -75,14 +75,23 @@ was_option_requested()
# client MUST ignore the Router option.
set_dhcp_routes()
{
echo "! Generated by udhcpc" > "$NEXT"
echo "# Generated by udhcpc" > "$NEXT"
echo "" >> "$NEXT"
if [ -n "$staticroutes" ]; then
if was_option_requested 121; then
# format: dest1/mask gw1 ... destn/mask gwn
set -- $staticroutes
while [ -n "$1" -a -n "$2" ]; do
dbg "adding route $1 via $2 metric $metric tag 100"
echo "ip route $1 $2 $metric tag 100" >> "$NEXT"
cat >> "$NEXT" <<-EOF
route {
prefix = "$1"
nexthop = "$2"
distance = $metric
tag = 100
}
EOF
shift 2
done
else
@@ -91,7 +100,15 @@ set_dhcp_routes()
elif [ -n "$router" ] ; then
if was_option_requested 3; then
for i in $router ; do
echo "ip route 0.0.0.0/0 $i $metric tag 100" >> "$NEXT"
cat >> "$NEXT" <<-EOF
route {
prefix = "0.0.0.0/0"
nexthop = "$i"
distance = $metric
tag = 100
}
EOF
done
else
log "ignoring unrequested router (option 3)"
@@ -102,7 +119,7 @@ set_dhcp_routes()
cmp -s "$NAME" "$NEXT" && return
mv "$NEXT" "$NAME"
initctl -nbq restart staticd
initctl reload netd
}
clr_dhcp_routes()
@@ -111,7 +128,7 @@ clr_dhcp_routes()
[ -f "$NAME" ] || return
rm "$NAME"
initctl -nbq restart staticd
initctl reload netd
}
clr_dhcp_addresses()
+1
View File
@@ -152,6 +152,7 @@ BR2_PACKAGE_FEATURE_GPS=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
+1
View File
@@ -128,6 +128,7 @@ INFIX_DOC="https://kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_NETD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
BR2_PACKAGE_GENCERT=y
BR2_PACKAGE_STATD=y
+1
View File
@@ -148,6 +148,7 @@ BR2_PACKAGE_FEATURE_GPS=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_GENCERT=y
BR2_PACKAGE_STATD=y
+1
View File
@@ -128,6 +128,7 @@ INFIX_DOC="https://kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_NETD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
BR2_PACKAGE_GENCERT=y
BR2_PACKAGE_STATD=y
+1
View File
@@ -180,6 +180,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
+1
View File
@@ -152,6 +152,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
+1
View File
@@ -127,6 +127,7 @@ INFIX_DOC="https://kernelkit.org/infix/"
INFIX_SUPPORT="mailto:kernelkit@googlegroups.com"
BR2_PACKAGE_FEATURE_GPS=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_NETD=y
BR2_PACKAGE_CONFD_TEST_MODE=y
BR2_PACKAGE_GENCERT=y
BR2_PACKAGE_STATD=y
+2 -1
View File
@@ -3,13 +3,14 @@ Change Log
All notable changes to the project are documented in this file.
[v26.01.0][UNRELEASED]
[v26.02.0][UNRELEASED] -
-------------------------
### Changes
- Upgrade Linux kernel to 6.18.13 (LTS)
- Upgrade Buildroot to 2025.02.11 (LTS)
- Upgrade FRR to 10.5.1
- Add support for Microchip SAMA7G54-EK Evaluation Kit, Arm Cortex-A7
- Add GPS/GNSS receiver support with NTP reference clock integration
- Add support for [Banana Pi R3 Mini][BPI-R3-MINI], a 2 port router with 2 WiFi chip,
+2
View File
@@ -7,6 +7,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"
@@ -32,6 +33,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/mdns-alias/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/netbrowse/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/onieprom/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/podman/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/python-spy/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/python-libyang/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/python-yangdoc/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/skeleton-init-finit/Config.in"
@@ -1,48 +0,0 @@
From abaad560f0bf1f7de832a55222e20e2a9a61986f Mon Sep 17 00:00:00 2001
From: Aaron Andersen <aaron@fosslib.net>
Date: Sat, 10 Jan 2026 18:57:52 -0500
Subject: [PATCH 1/2] Set USER and LOGNAME environment variables when dropping
privileges
Organization: Wires
When a service is configured to run as a non-root user (@user), finit
correctly drops privileges via setuid() and sets HOME and PATH, but
does not set the USER and LOGNAME environment variables. They remain
set to "root" from boot time.
This causes problems for software that determines its identity from
the environment rather than getuid(). For example, rootless Podman
checks os.Getenv("USER") first when looking up subordinate UID/GID
ranges in /etc/subuid and /etc/subgid.
With USER=root but UID=1000, Podman looks up root's subuid entry
instead of the actual user's, causing applications like newuidmap
to fail. Setting USER and LOGNAME to match the actual user identity
follows POSIX conventions and matches the behavior of su, sudo, and
login.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/service.c | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/service.c b/src/service.c
index 2c8fb6e..d45db40 100644
--- a/src/service.c
+++ b/src/service.c
@@ -627,8 +627,11 @@ static pid_t service_fork(svc_t *svc)
set_uid(uid, svc);
/* Set default path for regular users */
- if (uid > 0)
+ if (uid > 0) {
setenv("PATH", _PATH_DEFPATH, 1);
+ setenv("USER", svc->username, 1);
+ setenv("LOGNAME", svc->username, 1);
+ }
if (home) {
setenv("HOME", home, 1);
if (chdir(home)) {
--
2.43.0
@@ -1,86 +0,0 @@
From 34bf9a77765db4d963fc66dde29b415ecc8ab611 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Mon, 12 Jan 2026 19:31:39 +0100
Subject: [PATCH 2/2] Fix #467: TTY services stuck in restart state after
non-zero exit
Organization: Wires
When a TTY exited with non-zero code (e.g., user with shell=/sbin/false),
it would enter restart state but never recover, requiring manual restart.
The throttling logic from commit f0032ab had two issues:
1. Duplicate exit code check in service_retry() created infinite timer loop
2. TTYs lacked default restart_tmo, causing timer to never start
Fix by removing duplicate check and ensuring TTYs get a 2-second default
restart_tmo for proper throttling.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/service.c | 31 ++++++++++++++-----------------
1 file changed, 14 insertions(+), 17 deletions(-)
diff --git a/src/service.c b/src/service.c
index d45db40..ff8e180 100644
--- a/src/service.c
+++ b/src/service.c
@@ -2194,15 +2194,20 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
/* only set forking based on pidfile if user supplied pid: option */
if (pid && svc->pidfile[0] == '!')
svc->forking = 1;
+ }
- if (svc->restart_tmo == 0) {
- if (svc_is_forking(svc))
- svc->restart_tmo = 2000;
- else
- svc->restart_tmo = 1;
- }
+ /* Set default restart_tmo for services and TTYs that can restart */
+ if (svc_is_daemon(svc) && svc->restart_tmo == 0) {
+ if (svc_is_forking(svc))
+ svc->restart_tmo = 2000;
+ else
+ svc->restart_tmo = 1;
}
+ /* TTYs need a longer default to throttle errors (e.g., missing device) */
+ if (svc_is_tty(svc) && svc->restart_tmo == 0)
+ svc->restart_tmo = 2000;
+
/* Set configured limits */
memcpy(svc->rlimit, rlimit, sizeof(svc->rlimit));
@@ -2631,7 +2636,6 @@ static void service_cleanup_script(svc_t *svc)
static void service_retry(svc_t *svc)
{
char *restart_cnt = (char *)&svc->restart_cnt;
- int rc = WEXITSTATUS(svc->status);
int timeout;
service_timeout_cancel(svc);
@@ -2641,17 +2645,10 @@ static void service_retry(svc_t *svc)
if (svc->respawn) {
/*
- * Non-zero exit indicates an error that may not be resolved
- * by immediate retry. Add delay to prevent busy-loop and to
- * rate-limit retries, e.g. when TTY device doesn't exist.
+ * Respawn services (TTYs) that exited with non-zero status
+ * have already been delayed in the SVC_RUNNING_STATE handler.
+ * Just restart now.
*/
- if (WIFEXITED(svc->status) && rc != 0) {
- dbg("%s exited with error %d, delaying respawn ...",
- svc_ident(svc, NULL, 0), rc);
- service_timeout_after(svc, svc->restart_tmo, service_retry);
- return;
- }
-
dbg("%s crashed/exited, respawning ...", svc_ident(svc, NULL, 0));
svc_unblock(svc);
service_step(svc);
--
2.43.0
@@ -1,131 +0,0 @@
From c4463ec64f9732bd67b8cfd2b73adc10e50d1178 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Sun, 18 Jan 2026 23:26:37 +0100
Subject: [PATCH] initctl: escape special characters in JSON output
Organization: Wires
Strings like command, description, and environment may contain characters
that need escaping for valid JSON, e.g., embedded quotes in command line
arguments like -V "NanoPi R2S".
Add json_escape() helper to handle quotes, backslashes, and control chars.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
src/initctl.c | 78 +++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 69 insertions(+), 9 deletions(-)
diff --git a/src/initctl.c b/src/initctl.c
index 25c52cf..435e2da 100644
--- a/src/initctl.c
+++ b/src/initctl.c
@@ -1036,6 +1036,62 @@ static int svc_compare(svc_t *svc, char *arg)
return 0;
}
+/*
+ * Escape a string for safe JSON output. Handles quotes, backslashes,
+ * and control characters. Returns pointer to static buffer.
+ */
+static char *json_escape(const char *str)
+{
+ static char buf[1024];
+ char *ptr = buf;
+ size_t left = sizeof(buf) - 1;
+
+ if (!str)
+ return "";
+
+ while (*str && left > 1) {
+ char c = *str++;
+
+ switch (c) {
+ case '"':
+ case '\\':
+ *ptr++ = '\\';
+ *ptr++ = c;
+ left -= 2;
+ break;
+ case '\n':
+ *ptr++ = '\\';
+ *ptr++ = 'n';
+ left -= 2;
+ break;
+ case '\r':
+ *ptr++ = '\\';
+ *ptr++ = 'r';
+ left -= 2;
+ break;
+ case '\t':
+ *ptr++ = '\\';
+ *ptr++ = 't';
+ left -= 2;
+ break;
+ default:
+ if ((unsigned char)c < 0x20) {
+ /* Other control chars: use \uXXXX */
+ int n = snprintf(ptr, left, "\\u%04x", (unsigned char)c);
+ ptr += n;
+ left -= n;
+ } else {
+ *ptr++ = c;
+ left--;
+ }
+ break;
+ }
+ }
+ *ptr = '\0';
+
+ return buf;
+}
+
static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
{
long now = jiffies();
@@ -1051,14 +1107,16 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
fprintf(fp,
"%s"
"%s{\n"
- "%s \"identity\": \"%s\",\n"
- "%s \"description\": \"%s\",\n"
+ "%s \"identity\": \"%s\",\n",
+ prev ? ",\n" : indent, prev ? indent : "",
+ indent, svc_ident(svc, NULL, 0));
+ fprintf(fp,
+ "%s \"description\": \"%s\",\n",
+ indent, json_escape(svc->desc));
+ fprintf(fp,
"%s \"type\": \"%s\",\n"
"%s \"forking\": %s,\n"
"%s \"status\": \"%s\",\n",
- prev ? ",\n" : indent, prev ? indent : "",
- indent, svc_ident(svc, NULL, 0),
- indent, svc->desc,
indent, svc_typestr(svc),
indent, svc->forking ? "true" : "false",
indent, svc_status(svc));
@@ -1080,15 +1138,17 @@ static int json_status_one(FILE *fp, svc_t *svc, char *indent, int prev)
}
fprintf(fp,
- "%s \"origin\": \"%s\",\n"
+ "%s \"origin\": \"%s\",\n",
+ indent, svc->file[0] ? svc->file : "built-in");
+ svc_command(svc, buf, sizeof(buf), 0);
+ fprintf(fp,
"%s \"command\": \"%s\",\n",
- indent, svc->file[0] ? svc->file : "built-in",
- indent, svc_command(svc, buf, sizeof(buf), 0));
+ indent, json_escape(buf));
svc_environ(svc, buf, sizeof(buf), 0);
if (buf[0])
fprintf(fp,
- "%s \"environment\": \"%s\",\n", indent, buf);
+ "%s \"environment\": \"%s\",\n", indent, json_escape(buf));
svc_cond(svc, buf, sizeof(buf), 0);
if (buf[0])
--
2.43.0
+1 -1
View File
@@ -1,5 +1,5 @@
# From https://github.com/troglobit/finit/releases/
sha256 0e4774ccb8933ed92287e6c18d27cb463222dcc1f50a3607e27bbe5fd150ece0 finit-4.15.tar.gz
sha256 7f8b5a49149b17670d93e41a9b146c5633e035aa00087b8c945def8387cdecbd finit-4.16-rc1.tar.gz
# Locally calculated
sha256 868cb6c5414933a48db11186042cfe65c87480d326734bc6cf0e4b19b4a2e52a LICENSE
+1 -1
View File
@@ -4,7 +4,7 @@
#
################################################################################
FINIT_VERSION = 4.15
FINIT_VERSION = 4.16-rc1
FINIT_SITE = https://github.com/troglobit/finit/releases/download/$(FINIT_VERSION)
FINIT_LICENSE = MIT
FINIT_LICENSE_FILES = LICENSE
+61
View File
@@ -0,0 +1,61 @@
config BR2_PACKAGE_NETD
bool "netd"
select BR2_PACKAGE_LIBITE
select BR2_PACKAGE_LIBCONFUSE
help
Network route daemon. Manages static routes and RIP routing.
Reads configuration from /etc/netd/conf.d/*.conf.
With FRR: Full routing via gRPC, vtysh, or frr.conf.
Without FRR: Standalone Linux backend via rtnetlink.
https://github.com/kernelkit/infix
if BR2_PACKAGE_NETD
choice
prompt "FRR backend"
default BR2_PACKAGE_NETD_FRR_VTYSH if BR2_PACKAGE_FRR
default BR2_PACKAGE_NETD_LINUX
config BR2_PACKAGE_NETD_FRR_VTYSH
bool "FRR vtysh"
depends on BR2_PACKAGE_FRR
help
Enable FRR integration via vtysh -b.
netd generates incremental config diffs and applies
them to running FRR daemons via vtysh -b.
FRR daemons run as standalone finit services (no watchfrr).
Provides static routes and RIP support.
config BR2_PACKAGE_NETD_FRR_CONF
bool "FRR frr.conf"
depends on BR2_PACKAGE_FRR
help
Enable FRR integration via frr.conf file generation.
netd assembles a single /etc/frr/frr.conf from routing
config and signals FRR to reload via frrinit.sh/watchfrr.
Provides full routing support (static routes, RIP, OSPF).
config BR2_PACKAGE_NETD_FRR
bool "FRR gRPC"
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).
config BR2_PACKAGE_NETD_LINUX
bool "Linux kernel"
help
Use Linux kernel backend (rtnetlink) with static routes
only. No FRR dependency.
endchoice
endif
+3
View File
@@ -0,0 +1,3 @@
#set DEBUG=1
service <pid/confd> name:netd log \
[2345] netd -p /run/netd.pid -- Network route daemon
+39
View File
@@ -0,0 +1,39 @@
################################################################################
#
# netd
#
################################################################################
NETD_VERSION = 1.0
NETD_SITE_METHOD = local
NETD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/netd
NETD_LICENSE = BSD-3-Clause
NETD_LICENSE_FILES = LICENSE
NETD_REDISTRIBUTE = NO
NETD_DEPENDENCIES = libite libconfuse jansson
NETD_AUTORECONF = YES
NETD_CONF_ENV = CFLAGS="$(INFIX_CFLAGS)"
NETD_CONF_OPTS = --prefix= --disable-silent-rules
# Backend selection: FRR frr.conf, FRR gRPC, or Linux kernel
ifeq ($(BR2_PACKAGE_NETD_FRR_CONF),y)
NETD_DEPENDENCIES += frr
NETD_CONF_OPTS += --with-frr-conf
else ifeq ($(BR2_PACKAGE_NETD_FRR_VTYSH),y)
NETD_DEPENDENCIES += frr
NETD_CONF_OPTS += --with-frr-vtysh
else 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"
NETD_CONF_OPTS += --with-frr
else
NETD_CONF_OPTS += --without-frr
endif
NETD_TARGET_FINALIZE_HOOKS += NETD_INSTALL_EXTRA
$(eval $(autotools-package))
+8
View File
@@ -0,0 +1,8 @@
config BR2_PACKAGE_PYTHON_SPY
bool "python-spy"
depends on BR2_aarch64 || BR2_x86_64
select BR2_PACKAGE_PYTHON3
help
Sampling profiler for Python programs.
https://github.com/benfred/py-spy
+3
View File
@@ -0,0 +1,3 @@
# Locally calculated
sha256 ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084 py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
sha256 6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29 py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
+36
View File
@@ -0,0 +1,36 @@
################################################################################
#
# python-spy
#
################################################################################
PYTHON_SPY_VERSION = 0.4.1
ifeq ($(BR2_aarch64),y)
PYTHON_SPY_SITE = https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68
PYTHON_SPY_SOURCE = py_spy-$(PYTHON_SPY_VERSION)-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
else ifeq ($(BR2_x86_64),y)
PYTHON_SPY_SITE = https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4
PYTHON_SPY_SOURCE = py_spy-$(PYTHON_SPY_VERSION)-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl
endif
PYTHON_SPY_LICENSE = MIT
PYTHON_SPY_DEPENDENCIES = host-python3 host-python-installer
# Keep the wheel intact; we install from it directly.
define PYTHON_SPY_EXTRACT_CMDS
cp $(PYTHON_SPY_DL_DIR)/$(PYTHON_SPY_SOURCE) $(@D)/
endef
define PYTHON_SPY_INSTALL_TARGET_CMDS
$(HOST_DIR)/bin/python3 $(TOPDIR)/support/scripts/pyinstaller.py \
$(@D)/$(PYTHON_SPY_SOURCE) \
--interpreter=/usr/bin/python3 \
--script-kind=posix \
--purelib=$(TARGET_DIR)/usr/lib/python$(PYTHON3_VERSION_MAJOR)/site-packages \
--headers=$(TARGET_DIR)/usr/include/python$(PYTHON3_VERSION_MAJOR) \
--scripts=$(TARGET_DIR)/usr/bin \
--data=$(TARGET_DIR)
endef
$(eval $(generic-package))
@@ -85,13 +85,21 @@ SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_DROPBEA
endif
ifeq ($(BR2_PACKAGE_FRR),y)
ifeq ($(BR2_PACKAGE_NETD_FRR_CONF),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 \
cp $(SKELETON_INIT_FINIT_AVAILABLE)/frr/frr.conf $(FINIT_D)/available/frr.conf
ln -sf ../available/frr.conf $(FINIT_D)/enabled/frr.conf
endef
else
define SKELETON_INIT_FINIT_SET_FRR
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
endif
SKELETON_INIT_FINIT_POST_INSTALL_TARGET_HOOKS += SKELETON_INIT_FINIT_SET_FRR
endif # BR2_PACKAGE_FRR
@@ -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"
@@ -0,0 +1,3 @@
sysv <!~pid/netd> name:watchfrr pid:!/run/frr/watchfrr.pid \
reload:'frrinit.sh reload' log:null \
[2345] frrinit.sh -- FRR routing suite
@@ -0,0 +1,2 @@
service <!> pid:!/run/frr/mgmtd.pid env:-/etc/default/mgmtd \
[2345] mgmtd $MGMTD_ARGS -- FRR MGMT daemon
@@ -1,2 +1,3 @@
service <pid/zebra> env:-/etc/default/ripd \
[2345] ripd $RIPD_ARGS -- RIP daemon
[2345] ripd $RIPD_ARGS
-- RIP daemon
@@ -1,4 +1,3 @@
# Run staticd-helper to collate /etc/static.d/*.conf before starting staticd
service log:null <!pid/zebra> pre:/usr/sbin/staticd-helper \
[2345] staticd -A 127.0.0.1 -u frr -g frr -f /etc/frr/staticd.conf \
service <!pid/zebra> log:null \
[2345] staticd -A 127.0.0.1 -u frr -g frr \
-- Static routing daemon
@@ -1,2 +1,3 @@
service <!> pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
[2345] zebra $ZEBRA_ARGS -- Zebra routing daemon
service <!pid/mgmtd> pid:!/run/frr/zebra.pid env:-/etc/default/zebra \
[2345] zebra $ZEBRA_ARGS
-- Zebra routing daemon
@@ -0,0 +1,125 @@
From 597cf064f6076e3859f72b0da4dc0ab98ca2e1d2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
Date: Tue, 27 Jan 2026 22:54:59 +0100
Subject: [PATCH 1/3] Libyang4 compat
Organization: Wires
libyang4 had breaking changes needs to be adapded for it.
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
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 <libyang/version.h>
+#if (LY_VERSION_MAJOR >= 4)
+#define LYD_PRINT_WITHSIBLINGS LYD_PRINT_SIBLINGS
+#endif
+
#ifdef __cplusplus
extern "C" {
#endif
--
2.43.0
@@ -0,0 +1,124 @@
From 859bc23f318cfa019b104e83591f185f5bcc3bd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mattias=20Walstr=C3=B6m?= <lazzer@gmail.com>
Date: Fri, 30 Jan 2026 13:00:12 +0100
Subject: [PATCH 2/3] Failed without c++ 23, this adds compatibility layer
Organization: Wires
Signed-off-by: Mattias Walström <lazzer@gmail.com>
---
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 = &ap;
+
+ 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
@@ -0,0 +1,70 @@
From daf4378dbf9d48372e2d10e825d67ba652abda87 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Sun, 22 Feb 2026 10:22:06 +0100
Subject: [PATCH 3/3] zebra: don't resolve nexthop via inactive connected route
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Organization: Wires
When an interface is deleted, zebra clears NEXTHOP_FLAG_ACTIVE on the
nexthops of any NHG that depended on that interface (via
if_down_nhg_dependents -> zebra_nhg_check_valid -> zebra_nhg_set_valid).
However, there is a window — and in some cases a permanent stale state —
where the corresponding ZEBRA_ROUTE_LOCAL /32 route for the interface's
address is still present in the RIB as dest->selected_fib for that
prefix node, without having ROUTE_ENTRY_REMOVED set.
This happens when rib_delnode() is never called for the LOCAL route,
e.g. because connected_down() returned early (ZEBRA_IFC_DOWN already
set from a prior call) or because process_subq_early_route_delete()
silently dropped the deletion due to a nexthop ifindex mismatch. The
stale route entry then sits in the RIB with "unknown inactive" in
"show ip route" — its nexthop's NEXTHOP_FLAG_ACTIVE is clear, but its
ifindex still holds the original value of the now-deleted interface.
When nexthop_active() resolves a static route whose gateway address
matches the stale LOCAL /32 prefix, it finds the stale route as
dest->selected_fib, passes the RIB_CONNECTED_ROUTE check (ZEBRA_ROUTE_LOCAL
is included in that macro), and — crucially — copies the stale, invalid
ifindex into the resolving nexthop before returning success.
The resulting NHG is then installed into the kernel with an ifindex that
no longer exists. The kernel rejects the NHG install with EINVAL
("Invalid argument"), and the subsequent route install fails with ENOENT
("Nexthop id does not exist"), leaving the route stuck as "S>r" (selected
but rejected).
Fix this by checking NEXTHOP_FLAG_ACTIVE on the connected route's nexthop
before copying its ifindex. If the nexthop is inactive, fall through to
continue_up_tree so that the lookup can try parent prefixes rather than
using a stale interface index.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
zebra/zebra_nhg.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/zebra/zebra_nhg.c b/zebra/zebra_nhg.c
index f5f78050f1..39b61c02f8 100644
--- a/zebra/zebra_nhg.c
+++ b/zebra/zebra_nhg.c
@@ -2466,6 +2466,15 @@ static int nexthop_active(struct nexthop *nexthop, struct nhg_hash_entry *nhe,
nexthop->ifindex);
newhop = match->nhe->nhg.nexthop;
+ /*
+ * If the connected route's nexthop is inactive
+ * (e.g. the interface was deleted but the LOCAL route
+ * hasn't been cleaned up from the RIB yet), do not
+ * copy its stale ifindex. Treat it as unresolvable
+ * and continue up the trie instead.
+ */
+ if (!CHECK_FLAG(newhop->flags, NEXTHOP_FLAG_ACTIVE))
+ goto continue_up_tree;
if (nexthop->type == NEXTHOP_TYPE_IPV4) {
nexthop->ifindex = newhop->ifindex;
nexthop->type = NEXTHOP_TYPE_IPV4_IFINDEX;
--
2.43.0
-149
View File
@@ -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 <libyang/version.h>
#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. */
@@ -1,30 +0,0 @@
From 5f37809521acda432d77aa4028b74c5713c2d988 Mon Sep 17 00:00:00 2001
From: Tobias Waldekranz <tobias@waldekranz.com>
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 <tobias@waldekranz.com>
---
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
+271 -329
View File
@@ -8,17 +8,17 @@
#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_DAEMONS "/etc/frr/daemons"
#define FRR_STATIC_CONFIG "! Generated by Infix confd\n\
frr defaults traditional\n\
@@ -29,217 +29,129 @@ no log unique-id\n\
log syslog warnings\n\
log facility local2\n"
int parse_rip_redistribute(sr_session_ctx_t *session, struct lyd_node *redistributes, FILE *fp)
int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip, FILE *fp)
{
struct lyd_node *tmp;
LY_LIST_FOR(lyd_child(redistributes), tmp) {
const char *protocol = lydx_get_cattr(tmp, "protocol");
fprintf(fp, " redistribute %s\n", protocol);
}
return 0;
}
int parse_rip_interfaces(sr_session_ctx_t *session, struct lyd_node *interfaces, FILE *fp)
{
struct lyd_node *interface, *neighbors_node, *neighbor;
int num_interfaces = 0;
/* First pass: network and passive-interface statements (inside router rip block) */
LY_LIST_FOR(lyd_child(interfaces), interface) {
const char *name;
int passive;
name = lydx_get_cattr(interface, "interface");
if (!name)
continue;
passive = lydx_get_bool(interface, "passive");
/* Enable RIP on the interface by adding it to the network statement */
fprintf(fp, " network %s\n", name);
if (passive)
fprintf(fp, " passive-interface %s\n", name);
num_interfaces++;
}
/* Handle explicit neighbors (inside router rip block) */
LY_LIST_FOR(lyd_child(interfaces), interface) {
neighbors_node = lydx_get_child(interface, "neighbors");
if (neighbors_node) {
LY_LIST_FOR(lyd_child(neighbors_node), neighbor) {
const char *address = lydx_get_cattr(neighbor, "address");
if (address)
fprintf(fp, " neighbor %s\n", address);
}
}
}
/* Second pass: interface-specific settings (outside router rip block) */
LY_LIST_FOR(lyd_child(interfaces), interface) {
const char *name, *split_horizon, *cost, *send_version, *recv_version;
name = lydx_get_cattr(interface, "interface");
if (!name)
continue;
split_horizon = lydx_get_cattr(interface, "split-horizon");
cost = lydx_get_cattr(interface, "cost");
send_version = lydx_get_cattr(interface, "send-version");
recv_version = lydx_get_cattr(interface, "receive-version");
/* Only create interface block if there are per-interface settings */
if (split_horizon || cost || send_version || recv_version) {
fprintf(fp, "interface %s\n", name);
if (split_horizon) {
if (!strcmp(split_horizon, "poison-reverse"))
fputs(" ip rip split-horizon poisoned-reverse\n", fp);
else if (!strcmp(split_horizon, "disabled"))
fputs(" no ip rip split-horizon\n", fp);
/* "simple" is default, no need to configure */
}
/* Configure send version */
if (send_version) {
if (!strcmp(send_version, "1"))
fputs(" ip rip send version 1\n", fp);
else if (!strcmp(send_version, "1-2"))
fputs(" ip rip send version 1 2\n", fp);
/* "2" is default in augmentation, explicit config if needed */
else if (!strcmp(send_version, "2"))
fputs(" ip rip send version 2\n", fp);
}
/* Configure receive version */
if (recv_version) {
if (!strcmp(recv_version, "1"))
fputs(" ip rip receive version 1\n", fp);
else if (!strcmp(recv_version, "1-2"))
fputs(" ip rip receive version 1 2\n", fp);
/* "2" is default in augmentation, explicit config if needed */
else if (!strcmp(recv_version, "2"))
fputs(" ip rip receive version 2\n", fp);
}
if (cost) {
/* FRR uses offset-list for per-interface cost adjustment */
/* Note: offset-list is configured globally, not per-interface */
/* This is just a placeholder - actual implementation would need
access lists and global offset-list configuration */
}
}
}
return num_interfaces;
}
int parse_rip_timers(sr_session_ctx_t *session, struct lyd_node *timers, FILE *fp)
{
const char *update, *invalid, *holddown, *flush;
if (!timers)
return 0;
update = lydx_get_cattr(timers, "update-interval");
invalid = lydx_get_cattr(timers, "invalid-interval");
holddown = lydx_get_cattr(timers, "holddown-interval");
flush = lydx_get_cattr(timers, "flush-interval");
/* FRR timers basic: UPDATE TIMEOUT GARBAGE
* TIMEOUT = invalid-interval (when route becomes invalid)
* GARBAGE = flush-interval (when route is flushed)
* Note: holddown-interval is used between invalid and flush
*/
DEBUG("Ignoring 'holddown interval %s' for now", holddown);
if (update || invalid || flush) {
fprintf(fp, " timers basic %s %s %s\n",
update ? update : "30",
invalid ? invalid : "180",
flush ? flush : "240");
}
return 0;
}
int parse_rip(sr_session_ctx_t *session, struct lyd_node *rip)
{
struct lyd_node *interfaces, *timers, *default_route, *debug;
struct lyd_node *interfaces, *timers, *default_route, *interface, *tmp;
const char *default_metric, *distance;
int num_interfaces = 0;
FILE *fp;
fp = fopen(RIPD_CONF_NEXT, "w");
if (!fp) {
ERROR("Failed to open %s", RIPD_CONF_NEXT);
return SR_ERR_INTERNAL;
}
fputs(FRR_STATIC_CONFIG, fp);
/* Handle RIP debug configuration */
debug = lydx_get_child(rip, "debug");
if (debug) {
int any_debug = 0;
if (lydx_get_bool(debug, "events")) {
fputs("debug rip events\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "packet")) {
fputs("debug rip packet\n", fp);
any_debug = 1;
}
if (lydx_get_bool(debug, "kernel")) {
fputs("debug rip zebra\n", fp);
any_debug = 1;
}
if (any_debug)
fputs("!\n", fp);
}
fputs("router rip\n", fp);
fputs(" version 2\n", fp);
/* Generate libconfuse format for RIP */
fputs("\nrip {\n", fp);
fputs("\tenabled = true\n", fp);
/* Global RIP parameters */
default_metric = lydx_get_cattr(rip, "default-metric");
if (default_metric)
fprintf(fp, " default-metric %s\n", default_metric);
fprintf(fp, "\tdefault-metric = %s\n", default_metric);
distance = lydx_get_cattr(rip, "distance");
if (distance)
fprintf(fp, " distance %s\n", distance);
fprintf(fp, "\tdistance = %s\n", distance);
/* Timers */
timers = lydx_get_child(rip, "timers");
parse_rip_timers(session, timers, fp);
if (timers) {
const char *update, *invalid, *holddown, *flush;
update = lydx_get_cattr(timers, "update-interval");
invalid = lydx_get_cattr(timers, "invalid-interval");
holddown = lydx_get_cattr(timers, "holddown-interval");
flush = lydx_get_cattr(timers, "flush-interval");
DEBUG("Ignoring 'holddown interval %s' for now", holddown);
if (update || invalid || flush) {
fputs("\ttimers {\n", fp);
fprintf(fp, "\t\tupdate = %s\n", update ? update : "30");
fprintf(fp, "\t\tinvalid = %s\n", invalid ? invalid : "180");
fprintf(fp, "\t\tflush = %s\n", flush ? flush : "240");
fputs("\t}\n", fp);
}
}
/* Default route origination */
default_route = lydx_get_child(rip, "originate-default-route");
if (default_route && lydx_get_bool(default_route, "enabled"))
fputs(" default-information originate\n", fp);
fputs("\tdefault-route = true\n", fp);
/* Redistribution */
parse_rip_redistribute(session, lydx_get_child(rip, "redistribute"), fp);
/* Interfaces - must be done after router rip block and before interface blocks */
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;
/* Debug options - use system commands since FRR doesn't support via northbound */
struct lyd_node *debug = lydx_get_child(rip, "debug");
if (debug) {
if (lydx_get_bool(debug, "events"))
fputs("\tsystem = \"vtysh -c 'debug rip events'\"\n", fp);
if (lydx_get_bool(debug, "packet"))
fputs("\tsystem = \"vtysh -c 'debug rip packet'\"\n", fp);
if (lydx_get_bool(debug, "kernel"))
fputs("\tsystem = \"vtysh -c 'debug rip zebra'\"\n", fp);
}
return 0;
/* Networks (interfaces) - output as list */
interfaces = lydx_get_child(rip, "interfaces");
if (interfaces) {
int first = 1;
fputs("\tnetwork = { ", fp);
LY_LIST_FOR(lyd_child(interfaces), interface) {
const char *name = lydx_get_cattr(interface, "interface");
if (name) {
if (!first)
fputs(", ", fp);
fprintf(fp, "\"%s\"", name);
first = 0;
num_interfaces++;
}
}
fputs(" }\n", fp);
/* Passive interfaces - output as list */
first = 1;
int has_passive = 0;
LY_LIST_FOR(lyd_child(interfaces), interface) {
if (lydx_get_bool(interface, "passive")) {
if (!has_passive) {
fputs("\tpassive = { ", fp);
has_passive = 1;
}
if (!first)
fputs(", ", fp);
fprintf(fp, "\"%s\"", lydx_get_cattr(interface, "interface"));
first = 0;
}
}
if (has_passive)
fputs(" }\n", fp);
/* Neighbors */
LY_LIST_FOR(lyd_child(interfaces), interface) {
struct lyd_node *neighbors_node = lydx_get_child(interface, "neighbors");
if (neighbors_node) {
LY_LIST_FOR(lyd_child(neighbors_node), tmp) {
const char *address = lydx_get_cattr(tmp, "address");
if (address)
fprintf(fp, "\tneighbor = \"%s\"\n", address);
}
}
}
}
/* Redistribution - output as list */
tmp = lydx_get_child(rip, "redistribute");
if (tmp && lyd_child(tmp)) {
int first = 1;
fputs("\tredistribute = { ", fp);
LY_LIST_FOR(lyd_child(tmp), tmp) {
const char *protocol = lydx_get_cattr(tmp, "protocol");
if (protocol) {
if (!first)
fputs(", ", fp);
fprintf(fp, "\"%s\"", protocol);
first = 0;
}
}
fputs(" }\n", fp);
}
fputs("}\n", fp);
return num_interfaces;
}
int parse_ospf_interfaces(sr_session_ctx_t *session, struct lyd_node *areas, FILE *fp)
@@ -361,8 +273,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) {
@@ -393,8 +303,10 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
any_debug = 1;
}
if (any_debug)
if (any_debug) {
fputs("log syslog debugging\n", fp);
fputs("!\n", fp);
}
}
areas = lydx_get_child(ospf, "areas");
@@ -419,16 +331,15 @@ int parse_ospf(sr_session_ctx_t *session, struct lyd_node *ospf)
fprintf(fp, " ospf router-id %s\n", router_id);
fclose(fp);
if (!bfd_enabled)
(void)remove(BFDD_CONF);
if (!num_areas) {
(void)remove(OSPFD_CONF_NEXT);
return 0;
}
if (bfd_enabled)
(void)touch(BFDD_CONF_NEXT);
(void)touch(BFDD_SIGNAL_NEXT);
else
(void)remove(BFDD_SIGNAL_NEXT);
return 0;
}
@@ -446,20 +357,26 @@ static int parse_route(struct lyd_node *parent, FILE *fp, const char *ip)
next_hop_address = lydx_get_cattr(next_hop, "next-hop-address");
special_next_hop = lydx_get_cattr(next_hop, "special-next-hop");
fprintf(fp, "%s route %s ", ip, destination_prefix);
/* Generate libconfuse format: route { prefix = "..." nexthop = "..." distance = ... } */
fputs("route {\n", fp);
fprintf(fp, "\tprefix = \"%s\"\n", destination_prefix);
/* There can only be one */
/* Nexthop - there can only be one */
if (outgoing_interface)
fputs(outgoing_interface, fp);
fprintf(fp, "\tnexthop = \"%s\"\n", outgoing_interface);
else if (next_hop_address)
fputs(next_hop_address, fp);
fprintf(fp, "\tnexthop = \"%s\"\n", next_hop_address);
else if (strcmp(special_next_hop, "blackhole") == 0)
fputs("blackhole", fp);
fputs("\tnexthop = \"blackhole\"\n", fp);
else if (strcmp(special_next_hop, "unreachable") == 0)
fputs("reject", fp);
fputs("\tnexthop = \"reject\"\n", fp);
else if (strcmp(special_next_hop, "receive") == 0)
fputs("Null0", fp);
fprintf(fp, " %s\n", route_preference);
fputs("\tnexthop = \"Null0\"\n", fp);
if (route_preference)
fprintf(fp, "\tdistance = %s\n", route_preference);
fputs("}\n", fp);
return SR_ERR_OK;
}
@@ -488,12 +405,66 @@ static int parse_static_routes(sr_session_ctx_t *session, struct lyd_node *paren
return num_routes;
}
/*
* Generate the complete /etc/frr/daemons file. Written atomically as a
* single unit so the file is always consistent and easy to read.
*/
static void frr_daemons_write(int ospfd, int ripd, int bfdd)
{
const char *next = FRR_DAEMONS "+";
FILE *fp;
fp = fopen(next, "w");
if (!fp) {
ERROR("Failed to open %s", next);
return;
}
/* Daemon selection - watchfrr, zebra, mgmtd, and staticd are
* always started by frrinit.sh regardless of these settings. */
fprintf(fp,
"# Generated by Infix confd\n"
"ospfd=%s\n"
"ripd=%s\n"
"bfdd=%s\n"
"bgpd=no\n"
"ospf6d=no\n"
"ripngd=no\n"
"isisd=no\n"
"pimd=no\n"
"pim6d=no\n"
"ldpd=no\n"
"eigrpd=no\n"
"babeld=no\n"
"vrrpd=no\n"
"pathd=no\n"
"\n",
ospfd ? "yes" : "no",
ripd ? "yes" : "no",
bfdd ? "yes" : "no");
/* Global settings and per-daemon options */
fputs(
"vtysh_enable=yes\n"
"watchfrr_options=\"-l 4\"\n"
"zebra_options=\" -A 127.0.0.1 -s 90000000\"\n"
"mgmtd_options=\" -A 127.0.0.1\"\n"
"ospfd_options=\" -A 127.0.0.1\"\n"
"ripd_options=\" -A 127.0.0.1\"\n"
"staticd_options=\"-A 127.0.0.1\"\n"
"bfdd_options=\" -A 127.0.0.1\"\n"
"\n"
"frr_profile=\"traditional\"\n",
fp);
fclose(fp);
rename(next, FRR_DAEMONS);
}
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;
@@ -501,146 +472,117 @@ int routing_change(sr_session_ctx_t *session, struct lyd_node *config, struct ly
return SR_ERR_OK;
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_ENABLED: /* first time, on register - no SR_EV_DONE follows */
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");
bfdd_enabled = fexist(BFDD_SIGNAL_NEXT);
ripd_enabled = fexist(RIPD_SIGNAL_NEXT);
goto activate;
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);
}
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;
}
}
}
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;
}
}
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;
}
}
if (staticd_enabled) {
(void)remove(STATICD_CONF_PREV);
(void)rename(STATICD_CONF, STATICD_CONF_PREV);
(void)rename(STATICD_CONF_NEXT, STATICD_CONF);
restart_zebra = true;
} else {
if (!remove(STATICD_CONF))
restart_zebra = true;
}
if (restart_zebra) {
/* skip in runlevel S, no routing daemons run here anyway */
if (systemf("runlevel >/dev/null 2>&1"))
return SR_ERR_OK;
if (systemf("initctl -bfq restart zebra")) {
ERROR("Failed to restart zebra routing daemon");
rc = SR_ERR_INTERNAL;
goto err_abandon;
}
}
return SR_ERR_OK;
default:
return SR_ERR_OK;
}
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);
if (event == SR_EV_CHANGE)
return rc;
/* For SR_EV_ENABLED we activate immediately (no SR_EV_DONE follows) */
netd_enabled = fexist(NETD_CONF_NEXT);
ospfd_enabled = fexist(OSPFD_CONF_NEXT);
bfdd_enabled = fexist(BFDD_SIGNAL_NEXT);
ripd_enabled = fexist(RIPD_SIGNAL_NEXT);
activate:
/* Generate complete /etc/frr/daemons (for watchfrr/frrinit.sh) */
frr_daemons_write(ospfd_enabled, ripd_enabled, bfdd_enabled);
if (bfdd_enabled)
(void)rename(BFDD_SIGNAL_NEXT, BFDD_SIGNAL);
else
(void)remove(BFDD_SIGNAL);
if (ospfd_enabled) {
(void)remove(OSPFD_CONF_PREV);
(void)rename(OSPFD_CONF, OSPFD_CONF_PREV);
(void)rename(OSPFD_CONF_NEXT, OSPFD_CONF);
} else {
(void)remove(OSPFD_CONF);
}
if (ripd_enabled)
(void)rename(RIPD_SIGNAL_NEXT, RIPD_SIGNAL);
else
(void)remove(RIPD_SIGNAL);
/* netd handles both static routes and RIP, assembles frr.conf */
if (netd_enabled) {
(void)remove(NETD_CONF_PREV);
(void)rename(NETD_CONF, NETD_CONF_PREV);
(void)rename(NETD_CONF_NEXT, NETD_CONF);
} else {
(void)remove(NETD_CONF);
}
/* Enable/disable FRR daemons as standalone finit services.
* Harmless no-op when using watchfrr (services not installed). */
systemf("initctl -nbq %s ospfd", ospfd_enabled ? "enable" : "disable");
if (ospfd_enabled)
systemf("initctl -nbq touch ospfd");
systemf("initctl -nbq %s ripd", ripd_enabled ? "enable" : "disable");
systemf("initctl -nbq %s bfdd", bfdd_enabled ? "enable" : "disable");
/*
* Signal netd to reload - it assembles /etc/frr/frr.conf and
* Finit propagates the restart to the frr sysv service
*/
if (systemf("initctl -bfq touch netd"))
ERROR("Failed to signal netd for reload");
err_abandon:
return rc;
}
+27
View File
@@ -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.
+44
View File
@@ -0,0 +1,44 @@
DISTCLEANFILES = *~ *.d
ACLOCAL_AMFLAGS = -I m4
sbin_PROGRAMS = netd
netd_SOURCES = src/netd.c src/netd.h src/config.c src/config.h
netd_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -I$(srcdir)/src
netd_CFLAGS = -W -Wall -Wextra
netd_CFLAGS += $(libite_CFLAGS) $(libconfuse_CFLAGS) $(jansson_CFLAGS)
netd_LDADD = $(libite_LIBS) $(libconfuse_LIBS) $(jansson_LIBS)
# Backend selection: FRR gRPC, FRR frr.conf, or Linux kernel
if HAVE_FRR_GRPC
BUILT_SOURCES = grpc/frr-northbound.pb.cc grpc/frr-northbound.pb.h \
grpc/frr-northbound.grpc.pb.cc grpc/frr-northbound.grpc.pb.h
grpc/frr-northbound.pb.cc grpc/frr-northbound.pb.h: grpc/frr-northbound.proto
$(AM_V_GEN)$(PROTOC) --cpp_out=grpc --proto_path=grpc $<
grpc/frr-northbound.grpc.pb.cc grpc/frr-northbound.grpc.pb.h: grpc/frr-northbound.proto
$(AM_V_GEN)$(PROTOC) --grpc_out=grpc \
--plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN) \
--proto_path=grpc $<
netd_SOURCES += src/grpc_backend.cc src/grpc_backend.h \
src/json_builder.c src/json_builder.h \
grpc/frr-northbound.pb.cc grpc/frr-northbound.grpc.pb.cc
netd_CPPFLAGS += $(grpc_CFLAGS) $(protobuf_CFLAGS)
netd_LDADD += $(grpc_LIBS) $(protobuf_LIBS) -lstdc++
CLEANFILES = grpc/*.pb.cc grpc/*.pb.h
else
if HAVE_FRR_CONF
# FRR frr.conf file backend
netd_SOURCES += src/frrconf_backend.c src/frrconf_backend.h
else
if HAVE_FRR_VTYSH
# FRR vtysh -b backend
netd_SOURCES += src/vtysh_backend.c src/vtysh_backend.h
else
# Linux kernel backend (no FRR)
netd_SOURCES += src/linux_backend.c src/linux_backend.h
endif
endif
endif
+361
View File
@@ -0,0 +1,361 @@
# netd - Network Daemon for Static Routes and RIP
A lightweight routing daemon that manages static routes and RIP routing protocol.
## Features
- **Static Routes** - IPv4 and IPv6 route management
- **RIP** - Routing Information Protocol (RIPv2) support
- **Dual Backend** - FRR integration or standalone Linux kernel routing
- **Simple Config** - INI-style section-based configuration format
- **Hot Reload** - SIGHUP support for configuration updates
## Building
### With FRR Integration (Default)
Full routing support with FRR gRPC northbound API:
```bash
./configure
make
make install
```
**Requirements:**
- FRR with gRPC northbound support
- protobuf >= 3.0.0
- grpc++ >= 1.16.0
**Note:** The FRR gRPC protocol definition (`grpc/frr-northbound.proto`) is included in the netd source tree.
**Features:**
- Static routes via FRR staticd
- RIP routing protocol
- OSPF (via separate FRR config)
- System command execution
### Standalone Linux Backend
Direct kernel routing without FRR:
```bash
./configure --without-frr
make
make install
```
**Requirements:**
- Linux kernel with rtnetlink support
**Features:**
- Static routes via rtnetlink
- No external dependencies
**Limitations:**
- No RIP/OSPF support
- No system commands
## Configuration
netd uses [libconfuse](https://github.com/martinh/libconfuse) for configuration parsing, providing a clean and structured format.
Configuration files are placed in `/etc/netd/conf.d/` with the `.conf` extension. Files are processed in alphabetical order.
### Configuration Format
The configuration uses libconfuse syntax with sections and key-value pairs:
```
route {
prefix = "10.0.0.0/24"
nexthop = "192.168.1.1"
distance = 1
}
rip {
enabled = true
network = ["eth0", "eth1"]
}
```
### Static Routes
Define static routes using `route` sections. Multiple route sections can be specified.
**Route Section:**
```
route {
prefix = "PREFIX/LEN"
nexthop = "NEXTHOP"
distance = DISTANCE
tag = TAG
}
```
**Parameters:**
- `prefix` (required) - Network prefix with CIDR notation
- IPv4: `"10.0.0.0/24"`
- IPv6: `"2001:db8::/32"`
- `nexthop` (required) - Next hop specification
- IP address: `"192.168.1.1"` or `"fe80::1"`
- Interface name: `"eth0"`
- Blackhole: `"blackhole"`, `"reject"`, or `"Null0"`
- `distance` (optional, default: 1) - Administrative distance (1-255)
- `tag` (optional, default: 0) - Route tag (0-4294967295), used for route filtering/redistribution
**Examples:**
IPv4 route via gateway:
```
route {
prefix = "10.0.0.0/24"
nexthop = "192.168.1.1"
distance = 10
}
```
IPv6 route via interface:
```
route {
prefix = "2001:db8::/32"
nexthop = "eth0"
distance = 1
}
```
Blackhole route:
```
route {
prefix = "192.0.2.0/24"
nexthop = "blackhole"
}
```
### RIP Configuration
Configure RIP routing protocol (requires FRR backend).
**RIP Section:**
```
rip {
enabled = BOOL
default-metric = VALUE
distance = VALUE
default-route = BOOL
network = [LIST]
passive = [LIST]
neighbor = [LIST]
redistribute = [LIST]
timers {
update = SECONDS
invalid = SECONDS
flush = SECONDS
}
debug-events = BOOL
debug-packet = BOOL
debug-kernel = BOOL
system = [LIST]
}
```
**Parameters:**
- `enabled` (optional, default: false) - Enable RIP routing
- `default-metric` (optional, default: 1) - Default route metric (1-16)
- `distance` (optional, default: 120) - Administrative distance (1-255)
- `default-route` (optional, default: false) - Originate default route
- `network` (optional) - List of interfaces to enable RIP on
- `passive` (optional) - List of passive interfaces (receive only)
- `neighbor` (optional) - List of static RIP neighbor addresses
- `redistribute` (optional) - List of route types to redistribute
- Valid types: `"connected"`, `"static"`, `"kernel"`, `"ospf"`
- `timers` (optional) - RIP timer configuration subsection
- `update` (default: 30) - Update timer in seconds
- `invalid` (default: 180) - Invalid timer in seconds
- `flush` (default: 240) - Flush timer in seconds
- `debug-events` (optional, default: false) - Enable RIP event debugging
- `debug-packet` (optional, default: false) - Enable RIP packet debugging
- `debug-kernel` (optional, default: false) - Enable RIP kernel debugging
- `system` (optional) - List of system commands to execute after config application
**Examples:**
Basic RIP configuration:
```
rip {
enabled = true
network = ["eth0", "eth1"]
redistribute = ["connected"]
}
```
RIP with passive interface:
```
rip {
enabled = true
network = ["eth0", "eth1"]
passive = ["eth1"]
}
```
RIP with custom timers:
```
rip {
enabled = true
network = ["eth0"]
timers {
update = 15
invalid = 90
flush = 120
}
}
```
### Configuration Files
Configuration files must be placed in `/etc/netd/conf.d/` with the `.conf` extension:
```bash
/etc/netd/conf.d/
├── 10-static.conf # Static routes
├── 20-rip.conf # RIP configuration
└── 99-local.conf # Local overrides
```
Files are processed in alphabetical order. Use numeric prefixes to control processing order.
Lines starting with `#` are comments:
```
# This is a comment
route {
# This is also a comment
prefix = "10.0.0.0/24" # Inline comment
nexthop = "192.168.1.1"
}
```
### Reloading Configuration
Signal netd to reload configuration:
```bash
# Using killall
sudo killall -HUP netd
```
netd validates configuration on reload. Check syslog for errors.
## Architecture
```
┌─────────┐
│ confd │ Writes /etc/netd/conf.d/confd.conf
└────┬────┘
│ SIGHUP
┌─────────┐
│ netd │ Parses config files
└────┬────┘
├──► FRR Backend (gRPC)
│ ├─► mgmtd
│ ├─► staticd (static routes)
│ └─► ripd (RIP protocol)
└──► Linux Backend (rtnetlink)
└─► Kernel routing table
```
### FRR Backend Flow
1. netd parses config files
2. Builds JSON config for FRR
3. Sends via gRPC to mgmtd
4. mgmtd distributes to backend daemons
5. Executes system commands (if any)
### Linux Backend Flow
1. netd parses config files
2. Opens rtnetlink socket
3. Sends RTM_NEWROUTE/RTM_DELROUTE
4. Kernel updates routing table
## Files
```
src/netd/
├── src/
│ ├── netd.c/h - Main daemon and data structures
│ ├── config.c/h - Config parser
│ ├── json_builder.c/h - FRR JSON config builder
│ ├── grpc_backend.cc/h - FRR gRPC backend
│ └── linux_backend.c/h - Linux rtnetlink backend
├── grpc/
│ └── frr-northbound.proto - gRPC protocol definition (copied from FRR)
├── configure.ac - Build configuration
├── Makefile.am - Build rules
├── netd.conf - Sample configuration
└── README.md - This file
```
## API
### Configuration Format
- libconfuse syntax with sections
- `route { }` - Static route entries (multiple allowed)
- `rip { }` - RIP configuration (single section)
### Supported Routes
- IPv4 and IPv6
- Gateway, interface, blackhole nexthops
- Administrative distance
- Route tags
### RIP Features
- Network interfaces
- Passive interfaces
- Static neighbors
- Route redistribution
- Timer configuration
- Default route origination
- Debug commands
## Logging
netd logs to syslog facility `daemon`:
```bash
# Debug mode (stderr)
netd -d
```
Log levels:
- `INFO` - Configuration changes, route operations
- `ERROR` - Failures, errors
- `DEBUG` - Detailed operation info (with `-d`)
## Signal Handling
- `SIGHUP` - Reload configuration
- `SIGTERM` / `SIGINT` - Graceful shutdown
## License
BSD-3-Clause
## See Also
- FRR Documentation - https://docs.frrouting.org/
- rtnetlink(7) - Linux routing socket API
- libconfuse Documentation - https://github.com/martinh/libconfuse
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
autoreconf -W portability -vifm
+118
View File
@@ -0,0 +1,118 @@
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 backend selection
#
AC_ARG_WITH(frr,
AS_HELP_STRING([--with-frr], [Build with FRR gRPC backend]),
[with_frr=$withval],
[with_frr=no])
AC_ARG_WITH(frr-conf,
AS_HELP_STRING([--with-frr-conf], [Build with FRR frr.conf file backend (default when FRR available)]),
[with_frr_conf=$withval],
[with_frr_conf=no])
AC_ARG_WITH(frr-vtysh,
AS_HELP_STRING([--with-frr-vtysh], [Build with FRR vtysh -b backend (incremental config)]),
[with_frr_vtysh=$withval],
[with_frr_vtysh=no])
# Mutual exclusion check
AS_IF([test "x$with_frr" = "xyes" -a "x$with_frr_conf" = "xyes"], [
AC_MSG_ERROR([--with-frr and --with-frr-conf are mutually exclusive])
])
AS_IF([test "x$with_frr" = "xyes" -a "x$with_frr_vtysh" = "xyes"], [
AC_MSG_ERROR([--with-frr and --with-frr-vtysh are mutually exclusive])
])
AS_IF([test "x$with_frr_conf" = "xyes" -a "x$with_frr_vtysh" = "xyes"], [
AC_MSG_ERROR([--with-frr-conf and --with-frr-vtysh are mutually exclusive])
])
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 gRPC support])
])
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 gRPC support])
])
# 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)
])
AS_IF([test "x$with_frr_conf" = "xyes"], [
AC_DEFINE(HAVE_FRR_CONF, 1, [Built with FRR frr.conf file backend])
])
AS_IF([test "x$with_frr_vtysh" = "xyes"], [
AC_DEFINE(HAVE_FRR_VTYSH, 1, [Built with FRR vtysh -b backend])
])
AM_CONDITIONAL(HAVE_FRR_GRPC, [test "x$with_frr" = "xyes"])
AM_CONDITIONAL(HAVE_FRR_CONF, [test "x$with_frr_conf" = "xyes"])
AM_CONDITIONAL(HAVE_FRR_VTYSH, [test "x$with_frr_vtysh" = "xyes"])
# Check for pkg-config first
PKG_PROG_PKG_CONFIG
PKG_CHECK_MODULES([libite], [libite >= 2.6.1])
PKG_CHECK_MODULES([libconfuse], [libconfuse >= 3.0])
PKG_CHECK_MODULES([jansson], [jansson >= 2.0])
test "x$prefix" = xNONE && prefix=$ac_default_prefix
test "x$exec_prefix" = xNONE && exec_prefix='${prefix}'
DATAROOTDIR=`eval echo $datarootdir`
DATAROOTDIR=`eval echo $DATAROOTDIR`
AC_SUBST(DATAROOTDIR)
SYSCONFDIR=`eval echo $sysconfdir`
SYSCONFDIR=`eval echo $SYSCONFDIR`
AC_SUBST(SYSCONFDIR)
RUNSTATEDIR=`eval echo $runstatedir`
RUNSTATEDIR=`eval echo $RUNSTATEDIR`
AC_SUBST(RUNSTATEDIR)
AC_OUTPUT
cat <<EOF
------------------ Summary ------------------
$PACKAGE_NAME version $PACKAGE_VERSION
Prefix................: $prefix
Exec prefix...........: $eprefix
Sysconfdir............: `eval echo $sysconfdir`
Backend...............: $(test "x$with_frr" = "xyes" && echo "FRR gRPC" || (test "x$with_frr_conf" = "xyes" && echo "FRR frr.conf" || (test "x$with_frr_vtysh" = "xyes" && echo "FRR vtysh" || echo "Linux kernel")))
C Compiler............: $CC $CFLAGS $CPPFLAGS $LDFLAGS $LIBS
------------- Compiler version --------------
$($CC --version || true)
---------------------------------------------
Check the above options and compile with:
${MAKE-make}
EOF
+423
View File
@@ -0,0 +1,423 @@
// SPDX-License-Identifier: BSD-2-Clause
//
// Copyright 2019 FRRouting
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. 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.
//
// 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.
//
syntax = "proto3";
package frr;
// Service specification for the FRR northbound interface.
service Northbound {
// Retrieve the capabilities supported by the target.
rpc GetCapabilities(GetCapabilitiesRequest) returns (GetCapabilitiesResponse) {}
// Retrieve configuration data, state data or both from the target.
rpc Get(GetRequest) returns (stream GetResponse) {}
// Create a new candidate configuration and return a reference to it. The
// created candidate is a copy of the running configuration.
rpc CreateCandidate(CreateCandidateRequest) returns (CreateCandidateResponse) {}
// Delete a candidate configuration.
rpc DeleteCandidate(DeleteCandidateRequest) returns (DeleteCandidateResponse) {}
// Update a candidate configuration by rebasing the changes on top of the
// latest running configuration. Resolve conflicts automatically by giving
// preference to the changes done in the candidate configuration.
rpc UpdateCandidate(UpdateCandidateRequest) returns (UpdateCandidateResponse) {}
// Edit a candidate configuration. All changes are discarded if any error
// happens.
rpc EditCandidate(EditCandidateRequest) returns (EditCandidateResponse) {}
// Load configuration data into a candidate configuration. Both merge and
// replace semantics are supported.
rpc LoadToCandidate(LoadToCandidateRequest) returns (LoadToCandidateResponse) {}
// Create a new configuration transaction using a two-phase commit protocol.
rpc Commit(CommitRequest) returns (CommitResponse) {}
// List the metadata of all configuration transactions recorded in the
// transactions database.
rpc ListTransactions(ListTransactionsRequest) returns (stream ListTransactionsResponse) {}
// Fetch a configuration (identified by its transaction ID) from the
// transactions database.
rpc GetTransaction(GetTransactionRequest) returns (GetTransactionResponse) {}
// Lock the running configuration, preventing other users from changing it.
rpc LockConfig(LockConfigRequest) returns (LockConfigResponse) {}
// Unlock the running configuration.
rpc UnlockConfig(UnlockConfigRequest) returns (UnlockConfigResponse) {}
// Execute a YANG RPC.
rpc Execute(ExecuteRequest) returns (ExecuteResponse) {}
}
// ----------------------- Parameters and return types -------------------------
//
// RPC: GetCapabilities()
//
message GetCapabilitiesRequest {
// Empty.
}
message GetCapabilitiesResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// FRR version.
string frr_version = 1;
// Indicates whether FRR was compiled with support for configuration
// rollbacks or not (--enable-config-rollbacks).
bool rollback_support = 2;
// Supported schema modules.
repeated ModuleData supported_modules = 3;
// Supported encodings.
repeated Encoding supported_encodings = 4;
}
//
// RPC: Get()
//
message GetRequest {
// Type of elements within the data tree.
enum DataType {
// All data elements.
ALL = 0;
// Config elements.
CONFIG = 1;
// State elements.
STATE = 2;
}
// The type of data being requested.
DataType type = 1;
// Encoding to be used.
Encoding encoding = 2;
// Include implicit default nodes.
bool with_defaults = 3;
// Paths requested by the client.
repeated string path = 4;
}
message GetResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::INVALID_ARGUMENT: Invalid YANG data path.
// Timestamp in nanoseconds since Epoch.
int64 timestamp = 1;
// The requested data.
DataTree data = 2;
}
//
// RPC: CreateCandidate()
//
message CreateCandidateRequest {
// Empty.
}
message CreateCandidateResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::RESOURCE_EXHAUSTED: can't create candidate
// configuration.
// Handle to the new created candidate configuration.
uint32 candidate_id = 1;
}
//
// RPC: DeleteCandidate()
//
message DeleteCandidateRequest {
// Candidate configuration to delete.
uint32 candidate_id = 1;
}
message DeleteCandidateResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::NOT_FOUND: Candidate wasn't found.
}
//
// RPC: UpdateCandidate()
//
message UpdateCandidateRequest {
// Candidate configuration to update.
uint32 candidate_id = 1;
}
message UpdateCandidateResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::NOT_FOUND: Candidate wasn't found.
}
//
// RPC: EditCandidate()
//
message EditCandidateRequest {
// Candidate configuration that is going to be edited.
uint32 candidate_id = 1;
// Data elements to be created or updated.
repeated PathValue update = 2;
// Paths to be deleted from the data tree.
repeated PathValue delete = 3;
}
message EditCandidateResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::NOT_FOUND: Candidate wasn't found.
// - grpc::StatusCode::INVALID_ARGUMENT: An error occurred while editing the
// candidate configuration.
}
//
// RPC: LoadToCandidate()
//
message LoadToCandidateRequest {
enum LoadType {
// Merge the data tree into the candidate configuration.
MERGE = 0;
// Replace the candidate configuration by the provided data tree.
REPLACE = 1;
}
// Candidate configuration that is going to be edited.
uint32 candidate_id = 1;
// Load operation to apply.
LoadType type = 2;
// Configuration data.
DataTree config = 3;
}
message LoadToCandidateResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::INVALID_ARGUMENT: An error occurred while performing
// the load operation.
}
//
// RPC: Commit()
//
message CommitRequest {
enum Phase {
// Validate if the configuration changes are valid (phase 0).
VALIDATE = 0;
// Prepare resources to apply the configuration changes (phase 1).
PREPARE = 1;
// Release previously allocated resources (phase 2).
ABORT = 2;
// Apply the configuration changes (phase 2).
APPLY = 3;
// All of the above (VALIDATE + PREPARE + ABORT/APPLY).
//
// This option can't be used to implement network-wide transactions,
// since they require the manager entity to take into account the results
// of the preparation phase of multiple managed devices.
ALL = 4;
}
// Candidate configuration that is going to be committed.
uint32 candidate_id = 1;
// Transaction phase.
Phase phase = 2;
// Assign a comment to this commit.
string comment = 3;
}
message CommitResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::FAILED_PRECONDITION: misuse of the two-phase commit
// protocol.
// - grpc::StatusCode::INVALID_ARGUMENT: Validation error.
// - grpc::StatusCode::RESOURCE_EXHAUSTED: Failure to allocate resource.
// ID of the created configuration transaction (when the phase is APPLY
// or ALL).
uint32 transaction_id = 1;
// Human-readable error or warning message(s).
string error_message = 2;
}
//
// RPC: ListTransactions()
//
message ListTransactionsRequest {
// Empty.
}
message ListTransactionsResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// Transaction ID.
uint32 id = 1;
// Client that committed the transaction.
string client = 2;
// Date and time the transaction was committed.
string date = 3;
// Comment assigned to the transaction.
string comment = 4;
}
//
// RPC: GetTransaction()
//
message GetTransactionRequest {
// Transaction to retrieve.
uint32 transaction_id = 1;
// Encoding to be used.
Encoding encoding = 2;
// Include implicit default nodes.
bool with_defaults = 3;
}
message GetTransactionResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::NOT_FOUND: Transaction wasn't found in the transactions
// database.
DataTree config = 1;
}
//
// RPC: LockConfig()
//
message LockConfigRequest {
// Empty.
}
message LockConfigResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::FAILED_PRECONDITION: Running configuration is
// locked already.
}
//
// RPC: UnlockConfig()
//
message UnlockConfigRequest {
// Empty.
}
message UnlockConfigResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// - grpc::StatusCode::FAILED_PRECONDITION: Running configuration isn't
// locked.
}
//
// RPC: Execute()
//
message ExecuteRequest {
// Path of the YANG RPC or YANG Action.
string path = 1;
// Input parameters.
repeated PathValue input = 2;
}
message ExecuteResponse {
// Return values:
// - grpc::StatusCode::OK: Success.
// Output parameters.
repeated PathValue output = 1;
}
// -------------------------------- Definitions --------------------------------
// YANG module.
message ModuleData {
// Name of the YANG module;
string name = 1;
// Organization publishing the module.
string organization = 2;
// Latest revision of the module;
string revision = 3;
}
// Supported encodings for YANG instance data.
enum Encoding {
JSON = 0;
XML = 1;
}
// Path-value pair representing a data element.
message PathValue {
// YANG data path.
string path = 1;
// Data value.
string value = 2;
}
// YANG instance data.
message DataTree {
Encoding encoding = 1;
string data = 2;
}
+52
View File
@@ -0,0 +1,52 @@
# netd sample configuration
# See CONFIGURATION.md for detailed documentation
# Static routes
route {
prefix = "10.0.0.0/24"
nexthop = "192.168.1.1"
distance = 1
}
route {
prefix = "2001:db8::/32"
nexthop = "fe80::1"
distance = 1
}
# RIP configuration (requires FRR backend)
rip {
enabled = false
default-metric = 1
distance = 120
# Network interfaces (active by default)
network = ["eth0"]
# Passive interfaces (receive but don't send RIP updates)
# passive = ["eth1"]
# Static RIP neighbors
# neighbor = ["192.168.1.1"]
# Route redistribution
redistribute = ["connected", "static"]
# RIP timers
timers {
update = 30
invalid = 180
flush = 240
}
# Originate default route
default-route = false
# Debug options
debug-events = false
debug-packet = false
debug-kernel = false
# System commands to execute after config is applied
# system = ["vtysh -c 'configure' -c 'router rip' -c 'debug rip events'"]
}
+384
View File
@@ -0,0 +1,384 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <dirent.h>
#include <errno.h>
#include <confuse.h>
#include "config.h"
/*
* Parse a single route section from libconfuse
*/
static int parse_route_section(cfg_t *cfg_route, struct route_head *head)
{
const char *prefix_str, *nexthop_str;
char prefix_copy[128];
struct in6_addr a6;
struct in_addr a4;
struct route *r;
long distance;
char *slash;
prefix_str = cfg_getstr(cfg_route, "prefix");
nexthop_str = cfg_getstr(cfg_route, "nexthop");
distance = cfg_getint(cfg_route, "distance");
if (!prefix_str || !nexthop_str) {
ERROR("Route missing prefix or nexthop");
return -1;
}
r = calloc(1, sizeof(*r));
if (!r) {
ERROR("Failed to allocate route");
return -1;
}
r->distance = (uint8_t)distance;
r->tag = (uint32_t)cfg_getint(cfg_route, "tag");
/* Parse prefix - determine address family from presence of ':' */
snprintf(prefix_copy, sizeof(prefix_copy), "%s", prefix_str);
slash = strchr(prefix_copy, '/');
if (!slash) {
ERROR("Route prefix missing netmask: %s", prefix_str);
free(r);
return -1;
}
*slash = '\0';
r->prefixlen = (uint8_t)atoi(slash + 1);
/* Try IPv6 first (has ':'), then IPv4 */
if (strchr(prefix_copy, ':')) {
r->family = AF_INET6;
if (inet_pton(AF_INET6, prefix_copy, &r->prefix.ip6) != 1) {
ERROR("Invalid IPv6 prefix: %s", prefix_copy);
free(r);
return -1;
}
} else {
r->family = AF_INET;
if (inet_pton(AF_INET, prefix_copy, &r->prefix.ip4) != 1) {
ERROR("Invalid IPv4 prefix: %s", prefix_copy);
free(r);
return -1;
}
}
/* Parse nexthop - check for special keywords */
if (!strcmp(nexthop_str, "blackhole")) {
r->nh_type = NH_BLACKHOLE;
r->bh_type = BH_DROP;
} else if (!strcmp(nexthop_str, "reject")) {
r->nh_type = NH_BLACKHOLE;
r->bh_type = BH_REJECT;
} else if (!strcmp(nexthop_str, "Null0")) {
r->nh_type = NH_BLACKHOLE;
r->bh_type = BH_NULL;
} else if (r->family == AF_INET && inet_pton(AF_INET, nexthop_str, &a4) == 1) {
/* IPv4 address */
r->nh_type = NH_ADDR;
r->gateway.gw4 = a4;
} else if (r->family == AF_INET6 && inet_pton(AF_INET6, nexthop_str, &a6) == 1) {
/* IPv6 address */
r->nh_type = NH_ADDR;
r->gateway.gw6 = a6;
} else {
/* Treat as interface name */
r->nh_type = NH_IFNAME;
snprintf(r->ifname, sizeof(r->ifname), "%s", nexthop_str);
}
TAILQ_INSERT_TAIL(head, r, entries);
return 0;
}
/*
* Parse RIP configuration section from libconfuse
*/
static int parse_rip_section(cfg_t *cfg_rip, struct rip_config *rip_cfg)
{
struct rip_redistribute *redist;
struct rip_system_cmd *cmd;
struct rip_neighbor *nbr;
struct rip_network *net;
const char *addr_str;
const char *type_str;
const char *cmd_str;
const char *ifname;
unsigned int n, i;
cfg_t *cfg_timers;
/* Check if RIP is enabled */
if (!cfg_getbool(cfg_rip, "enabled")) {
DEBUG("RIP is disabled");
return 0;
}
rip_cfg->enabled = 1;
/* Basic settings */
rip_cfg->default_metric = (uint8_t)cfg_getint(cfg_rip, "default-metric");
rip_cfg->distance = (uint8_t)cfg_getint(cfg_rip, "distance");
rip_cfg->default_route = cfg_getbool(cfg_rip, "default-route");
/* Debug flags */
rip_cfg->debug_events = cfg_getbool(cfg_rip, "debug-events");
rip_cfg->debug_packet = cfg_getbool(cfg_rip, "debug-packet");
rip_cfg->debug_kernel = cfg_getbool(cfg_rip, "debug-kernel");
/* Timers subsection */
cfg_timers = cfg_getsec(cfg_rip, "timers");
if (cfg_timers) {
rip_cfg->timers.update = (uint32_t)cfg_getint(cfg_timers, "update");
rip_cfg->timers.invalid = (uint32_t)cfg_getint(cfg_timers, "invalid");
rip_cfg->timers.flush = (uint32_t)cfg_getint(cfg_timers, "flush");
}
/* Network interfaces */
n = cfg_size(cfg_rip, "network");
for (i = 0; i < n; i++) {
ifname = cfg_getnstr(cfg_rip, "network", i);
if (!ifname)
continue;
net = calloc(1, sizeof(*net));
if (!net) {
ERROR("Failed to allocate network");
continue;
}
snprintf(net->ifname, sizeof(net->ifname), "%s", ifname);
net->passive = 0;
TAILQ_INSERT_TAIL(&rip_cfg->networks, net, entries);
}
/* Passive interfaces - mark existing networks as passive */
n = cfg_size(cfg_rip, "passive");
for (i = 0; i < n; i++) {
int found = 0;
ifname = cfg_getnstr(cfg_rip, "passive", i);
if (!ifname)
continue;
/* Find the network and mark it passive */
TAILQ_FOREACH(net, &rip_cfg->networks, entries) {
if (!strcmp(net->ifname, ifname)) {
net->passive = 1;
found = 1;
break;
}
}
/* If not found, create it as passive */
if (!found) {
net = calloc(1, sizeof(*net));
if (!net) {
ERROR("Failed to allocate passive network");
continue;
}
snprintf(net->ifname, sizeof(net->ifname), "%s", ifname);
net->passive = 1;
TAILQ_INSERT_TAIL(&rip_cfg->networks, net, entries);
}
}
/* Neighbors */
n = cfg_size(cfg_rip, "neighbor");
for (i = 0; i < n; i++) {
addr_str = cfg_getnstr(cfg_rip, "neighbor", i);
if (!addr_str)
continue;
nbr = calloc(1, sizeof(*nbr));
if (!nbr) {
ERROR("Failed to allocate neighbor");
continue;
}
if (inet_pton(AF_INET, addr_str, &nbr->addr) != 1) {
ERROR("Invalid neighbor address: %s", addr_str);
free(nbr);
continue;
}
TAILQ_INSERT_TAIL(&rip_cfg->neighbors, nbr, entries);
}
/* Redistribute routes */
n = cfg_size(cfg_rip, "redistribute");
for (i = 0; i < n; i++) {
type_str = cfg_getnstr(cfg_rip, "redistribute", i);
if (!type_str)
continue;
redist = calloc(1, sizeof(*redist));
if (!redist) {
ERROR("Failed to allocate redistribute");
continue;
}
if (!strcmp(type_str, "connected"))
redist->type = RIP_REDIST_CONNECTED;
else if (!strcmp(type_str, "static"))
redist->type = RIP_REDIST_STATIC;
else if (!strcmp(type_str, "kernel"))
redist->type = RIP_REDIST_KERNEL;
else if (!strcmp(type_str, "ospf"))
redist->type = RIP_REDIST_OSPF;
else {
ERROR("Unknown redistribute type: %s", type_str);
free(redist);
continue;
}
TAILQ_INSERT_TAIL(&rip_cfg->redistributes, redist, entries);
}
/* System commands */
n = cfg_size(cfg_rip, "system");
for (i = 0; i < n; i++) {
cmd_str = cfg_getnstr(cfg_rip, "system", i);
if (!cmd_str || !*cmd_str) {
ERROR("Empty system command");
continue;
}
cmd = calloc(1, sizeof(*cmd));
if (!cmd) {
ERROR("Failed to allocate system command");
continue;
}
snprintf(cmd->command, sizeof(cmd->command), "%s", cmd_str);
TAILQ_INSERT_TAIL(&rip_cfg->system_cmds, cmd, entries);
}
return 0;
}
/*
* Parse a single config file using libconfuse
*/
static int config_parse_file(const char *path, struct route_head *routes,
struct rip_config *rip_cfg)
{
cfg_opt_t timers_opts[] = {
CFG_INT("update", 30, CFGF_NONE),
CFG_INT("invalid", 180, CFGF_NONE),
CFG_INT("flush", 240, CFGF_NONE),
CFG_END()
};
cfg_opt_t rip_opts[] = {
CFG_BOOL("enabled", cfg_false, CFGF_NONE),
CFG_INT("default-metric", 1, CFGF_NONE),
CFG_INT("distance", 120, CFGF_NONE),
CFG_BOOL("default-route", cfg_false, CFGF_NONE),
CFG_BOOL("debug-events", cfg_false, CFGF_NONE),
CFG_BOOL("debug-packet", cfg_false, CFGF_NONE),
CFG_BOOL("debug-kernel", cfg_false, CFGF_NONE),
CFG_STR_LIST("network", NULL, CFGF_NONE),
CFG_STR_LIST("passive", NULL, CFGF_NONE),
CFG_STR_LIST("neighbor", NULL, CFGF_NONE),
CFG_STR_LIST("redistribute", NULL, CFGF_NONE),
CFG_STR_LIST("system", NULL, CFGF_NONE),
CFG_SEC("timers", timers_opts, CFGF_NONE),
CFG_END()
};
cfg_opt_t route_opts[] = {
CFG_STR("prefix", NULL, CFGF_NONE),
CFG_STR("nexthop", NULL, CFGF_NONE),
CFG_INT("distance", 1, CFGF_NONE),
CFG_INT("tag", 0, CFGF_NONE),
CFG_END()
};
cfg_opt_t opts[] = {
CFG_SEC("route", route_opts, CFGF_MULTI),
CFG_SEC("rip", rip_opts, CFGF_NONE),
CFG_END()
};
unsigned int i, n;
cfg_t *cfg_route;
cfg_t *cfg_rip;
cfg_t *cfg;
int ret;
cfg = cfg_init(opts, CFGF_NONE);
if (!cfg) {
ERROR("Failed to initialize libconfuse");
return -1;
}
ret = cfg_parse(cfg, path);
if (ret == CFG_FILE_ERROR) {
ERROR("Failed to open config file: %s", path);
cfg_free(cfg);
return -1;
} else if (ret == CFG_PARSE_ERROR) {
ERROR("Parse error in config file: %s", path);
cfg_free(cfg);
return -1;
}
/* Parse all route sections */
n = cfg_size(cfg, "route");
for (i = 0; i < n; i++) {
cfg_route = cfg_getnsec(cfg, "route", i);
if (cfg_route) {
if (parse_route_section(cfg_route, routes) < 0)
ERROR("Failed to parse route section %u in %s", i, path);
}
}
/* Parse RIP section if present */
cfg_rip = cfg_getsec(cfg, "rip");
if (cfg_rip) {
if (parse_rip_section(cfg_rip, rip_cfg) < 0)
ERROR("Failed to parse RIP section in %s", path);
}
cfg_free(cfg);
return 0;
}
int config_load(struct route_head *routes, struct rip_config *rip_cfg)
{
struct dirent **namelist;
char path[PATH_MAX];
const char *name;
const char *ext;
int n, i;
n = scandir(CONF_DIR, &namelist, NULL, alphasort);
if (n < 0) {
if (errno == ENOENT) {
DEBUG("No config directory %s", CONF_DIR);
return 0;
}
ERROR("scandir %s: %s", CONF_DIR, strerror(errno));
return -1;
}
for (i = 0; i < n; i++) {
name = namelist[i]->d_name;
ext = strrchr(name, '.');
if (!ext || strcmp(ext, ".conf")) {
free(namelist[i]);
continue;
}
snprintf(path, sizeof(path), "%s/%s", CONF_DIR, name);
DEBUG("Loading config %s", path);
config_parse_file(path, routes, rip_cfg);
free(namelist[i]);
}
free(namelist);
return 0;
}
+10
View File
@@ -0,0 +1,10 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef NETD_CONFIG_H_
#define NETD_CONFIG_H_
#include "netd.h"
int config_load(struct route_head *routes, struct rip_config *rip_cfg);
#endif /* NETD_CONFIG_H_ */
+192
View File
@@ -0,0 +1,192 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* FRR frr.conf backend for netd - assembles a single /etc/frr/frr.conf
* from static routes, RIP config, and any existing OSPF config snippet,
* then signals FRR to reload via Finit.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "netd.h"
#include "frrconf_backend.h"
#define FRR_CONF "/etc/frr/frr.conf"
#define FRR_CONF_NEXT FRR_CONF "+"
#define OSPFD_CONF "/etc/frr/ospfd.conf"
static const char *frr_header =
"! Generated by netd\n"
"frr defaults traditional\n"
"hostname Router\n"
"password zebra\n"
"enable password zebra\n"
"no log unique-id\n"
"log syslog warnings\n"
"log facility local2\n"
"!\n";
int frrconf_backend_init(void)
{
INFO("Using FRR frr.conf backend");
return 0;
}
void frrconf_backend_cleanup(void)
{
/* Nothing to clean up */
}
static void write_static_routes(FILE *fp, struct route_head *routes)
{
char prefix_str[INET6_ADDRSTRLEN];
char gw_str[INET6_ADDRSTRLEN];
struct route *r;
int count = 0;
TAILQ_FOREACH(r, routes, entries) {
const char *cmd;
if (r->family == AF_INET) {
cmd = "ip";
inet_ntop(AF_INET, &r->prefix.ip4, prefix_str, sizeof(prefix_str));
} else {
cmd = "ipv6";
inet_ntop(AF_INET6, &r->prefix.ip6, prefix_str, sizeof(prefix_str));
}
fprintf(fp, "%s route %s/%u ", cmd, prefix_str, r->prefixlen);
switch (r->nh_type) {
case NH_ADDR:
if (r->family == AF_INET)
inet_ntop(AF_INET, &r->gateway.gw4, gw_str, sizeof(gw_str));
else
inet_ntop(AF_INET6, &r->gateway.gw6, gw_str, sizeof(gw_str));
fputs(gw_str, fp);
break;
case NH_IFNAME:
fputs(r->ifname, fp);
break;
case NH_BLACKHOLE:
switch (r->bh_type) {
case BH_NULL:
fputs("Null0", fp);
break;
case BH_REJECT:
fputs("reject", fp);
break;
case BH_DROP:
fputs("blackhole", fp);
break;
}
break;
}
if (r->distance)
fprintf(fp, " %u", r->distance);
fputc('\n', fp);
count++;
}
if (count)
fputs("!\n", fp);
DEBUG("frrconf: wrote %d static routes", count);
}
static const char *redist_name(enum rip_redist_type type)
{
switch (type) {
case RIP_REDIST_CONNECTED: return "connected";
case RIP_REDIST_STATIC: return "static";
case RIP_REDIST_KERNEL: return "kernel";
case RIP_REDIST_OSPF: return "ospf";
}
return "unknown";
}
static void write_rip_config(FILE *fp, struct rip_config *rip)
{
struct rip_redistribute *redist;
struct rip_neighbor *nbr;
struct rip_network *net;
if (!rip->enabled)
return;
fputs("router rip\n", fp);
fprintf(fp, " default-metric %u\n", rip->default_metric);
fprintf(fp, " distance %u\n", rip->distance);
fprintf(fp, " timers basic %u %u %u\n",
rip->timers.update, rip->timers.invalid, rip->timers.flush);
if (rip->default_route)
fputs(" default-information originate\n", fp);
TAILQ_FOREACH(net, &rip->networks, entries) {
fprintf(fp, " network %s\n", net->ifname);
if (net->passive)
fprintf(fp, " passive-interface %s\n", net->ifname);
}
TAILQ_FOREACH(nbr, &rip->neighbors, entries) {
char addr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &nbr->addr, addr, sizeof(addr));
fprintf(fp, " neighbor %s\n", addr);
}
TAILQ_FOREACH(redist, &rip->redistributes, entries)
fprintf(fp, " redistribute %s\n", redist_name(redist->type));
fputs("!\n", fp);
DEBUG("frrconf: wrote RIP configuration");
}
static void append_file(FILE *fp, const char *path)
{
char buf[1024];
FILE *src;
size_t n;
src = fopen(path, "r");
if (!src)
return;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
fwrite(buf, 1, n, fp);
fclose(src);
DEBUG("frrconf: appended %s", path);
}
int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip)
{
FILE *fp;
fp = fopen(FRR_CONF_NEXT, "w");
if (!fp) {
ERROR("frrconf: failed to open %s: %s", FRR_CONF_NEXT, strerror(errno));
return -1;
}
fputs(frr_header, fp);
write_static_routes(fp, routes);
write_rip_config(fp, rip);
append_file(fp, OSPFD_CONF);
fclose(fp);
if (rename(FRR_CONF_NEXT, FRR_CONF)) {
ERROR("frrconf: failed to rename %s to %s: %s",
FRR_CONF_NEXT, FRR_CONF, strerror(errno));
return -1;
}
INFO("frrconf: updated %s", FRR_CONF);
return 0;
}
+12
View File
@@ -0,0 +1,12 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef NETD_FRRCONF_BACKEND_H_
#define NETD_FRRCONF_BACKEND_H_
#include "netd.h"
int frrconf_backend_init(void);
void frrconf_backend_cleanup(void);
int frrconf_backend_apply(struct route_head *routes, struct rip_config *rip);
#endif /* NETD_FRRCONF_BACKEND_H_ */
+194
View File
@@ -0,0 +1,194 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* gRPC backend for netd - communicates with FRR's management daemon
* using the gRPC northbound API. This provides a standard protocol
* interface for configuration management.
*/
#include <memory>
#include <grpcpp/grpcpp.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/security/credentials.h>
#include "grpc/frr-northbound.grpc.pb.h"
#include "grpc/frr-northbound.pb.h"
extern "C" {
#include "netd.h"
#include "grpc_backend.h"
#include "json_builder.h"
}
/* Global gRPC state */
static std::shared_ptr<grpc::Channel> g_channel;
static std::unique_ptr<frr::Northbound::Stub> g_stub;
static uint64_t g_candidate_id = 0;
/* gRPC server address */
#define GRPC_SERVER "127.0.0.1:50051"
extern "C" int grpc_backend_init(void)
{
frr::GetCapabilitiesResponse cap_resp;
frr::GetCapabilitiesRequest cap_req;
grpc::ClientContext cap_ctx;
grpc::ChannelArguments args;
grpc::Status status;
/* Create insecure channel to local mgmtd */
g_channel = grpc::CreateChannel(GRPC_SERVER, grpc::InsecureChannelCredentials());
if (!g_channel) {
ERROR("grpc: failed to create channel");
return -1;
}
/* Create Northbound stub */
g_stub = frr::Northbound::NewStub(g_channel);
if (!g_stub) {
ERROR("grpc: failed to create stub");
g_channel.reset();
return -1;
}
/* Test connection with GetCapabilities */
status = g_stub->GetCapabilities(&cap_ctx, cap_req, &cap_resp);
if (!status.ok()) {
ERROR("grpc: GetCapabilities failed: %s (code=%d)",
status.error_message().c_str(), status.error_code());
g_stub.reset();
g_channel.reset();
return -1;
}
INFO("grpc: connected to FRR mgmtd (version=%s, supported encodings=%d)",
cap_resp.frr_version().c_str(), cap_resp.supported_encodings_size());
return 0;
}
extern "C" void grpc_backend_cleanup(void)
{
frr::DeleteCandidateResponse del_resp;
frr::DeleteCandidateRequest del_req;
grpc::ClientContext del_ctx;
/* Clean up any pending candidates */
if (g_candidate_id != 0 && g_stub) {
del_req.set_candidate_id(g_candidate_id);
g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp);
g_candidate_id = 0;
}
g_stub.reset();
g_channel.reset();
DEBUG("grpc: finalized");
}
extern "C" int grpc_backend_apply(struct route_head *routes, struct rip_config *rip)
{
frr::CreateCandidateResponse create_resp;
frr::LoadToCandidateResponse load_resp;
frr::CreateCandidateRequest create_req;
frr::DeleteCandidateResponse del_resp;
frr::LoadToCandidateRequest load_req;
frr::DeleteCandidateRequest del_req;
frr::CommitResponse commit_resp;
grpc::ClientContext create_ctx;
grpc::ClientContext commit_ctx;
frr::CommitRequest commit_req;
grpc::ClientContext load_ctx;
grpc::ClientContext del_ctx;
const char *json_config;
grpc::Status status;
if (!g_stub || !g_channel) {
ERROR("grpc: not initialized");
return -1;
}
/* Build JSON configuration for both static routes and RIP */
json_config = build_routing_json(routes, rip);
if (!json_config) {
ERROR("grpc: failed to build JSON config");
return -1;
}
DEBUG("grpc: JSON config: %s", json_config);
/* Step 1: CreateCandidate */
status = g_stub->CreateCandidate(&create_ctx, create_req, &create_resp);
if (!status.ok()) {
ERROR("grpc: CreateCandidate failed: %s (code=%d)",
status.error_message().c_str(), status.error_code());
return -1;
}
g_candidate_id = create_resp.candidate_id();
DEBUG("grpc: created candidate %lu", (unsigned long)g_candidate_id);
/* Step 2: LoadToCandidate (REPLACE entire routing config) */
load_req.set_candidate_id(g_candidate_id);
load_req.set_type(frr::LoadToCandidateRequest_LoadType_REPLACE);
{
auto* config = load_req.mutable_config();
config->set_encoding(frr::JSON);
config->set_data(json_config);
}
status = g_stub->LoadToCandidate(&load_ctx, load_req, &load_resp);
if (!status.ok()) {
ERROR("grpc: LoadToCandidate failed: %s (code=%d)",
status.error_message().c_str(), status.error_code());
/* Clean up candidate */
del_req.set_candidate_id(g_candidate_id);
g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp);
g_candidate_id = 0;
return -1;
}
DEBUG("grpc: config loaded to candidate");
/* Step 3: Commit */
commit_req.set_candidate_id(g_candidate_id);
commit_req.set_phase(frr::CommitRequest_Phase_ALL);
commit_req.set_comment("netd configuration (routes + rip)");
status = g_stub->Commit(&commit_ctx, commit_req, &commit_resp);
if (!status.ok()) {
/* ABORTED with "No changes to apply" is a success case */
if (status.error_code() == grpc::StatusCode::ABORTED &&
status.error_message().find("No changes to apply") != std::string::npos) {
DEBUG("grpc: No changes to apply (config already up-to-date)");
/* Clean up candidate - it won't be auto-deleted on ABORTED */
del_req.set_candidate_id(g_candidate_id);
g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp);
g_candidate_id = 0;
INFO("grpc: configuration already up-to-date");
return 0;
}
ERROR("grpc: Commit failed: %s (code=%d)",
status.error_message().c_str(), status.error_code());
/* Clean up candidate */
del_req.set_candidate_id(g_candidate_id);
g_stub->DeleteCandidate(&del_ctx, del_req, &del_resp);
g_candidate_id = 0;
return -1;
}
/* Candidate is auto-deleted after successful commit */
g_candidate_id = 0;
INFO("grpc: configuration committed successfully");
return 0;
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef NETD_GRPC_BACKEND_H_
#define NETD_GRPC_BACKEND_H_
#include "netd.h"
#ifdef __cplusplus
extern "C" {
#endif
int grpc_backend_init(void);
void grpc_backend_cleanup(void);
int grpc_backend_apply(struct route_head *routes, struct rip_config *rip);
#ifdef __cplusplus
}
#endif
#endif /* NETD_GRPC_BACKEND_H_ */
+563
View File
@@ -0,0 +1,563 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* JSON builder for FRR routing configuration using libjansson.
* Converts internal routing and RIP structures to JSON format
* following FRR's YANG model for northbound API.
*/
#include <arpa/inet.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <jansson.h>
#include "netd.h"
#include "json_builder.h"
/* Static buffer for JSON output (64KB should be sufficient) */
#define JSON_BUFFER_SIZE (64 * 1024)
static char json_buffer[JSON_BUFFER_SIZE];
/*
* Helper to check if two routes have the same prefix.
* Routes with the same prefix should be grouped into one route-list
* entry with multiple path-list entries.
*/
static bool same_prefix(const struct route *a, const struct route *b)
{
if (a->family != b->family || a->prefixlen != b->prefixlen)
return false;
if (a->family == AF_INET)
return memcmp(&a->prefix.ip4, &b->prefix.ip4, sizeof(a->prefix.ip4)) == 0;
else
return memcmp(&a->prefix.ip6, &b->prefix.ip6, sizeof(a->prefix.ip6)) == 0;
}
/*
* Build JSON configuration for staticd following FRR's YANG model.
* Groups routes with the same prefix into a single route-list entry
* with multiple path-list entries (for ECMP/multipath support).
*/
const char *build_staticd_json(struct route_head *routes)
{
char prefix_str[INET6_ADDRSTRLEN + 4];
char addr_buf[INET6_ADDRSTRLEN];
json_t *control_plane_protocols;
json_t *control_plane_protocol;
struct route *r, *curr;
json_t *frr_staticd;
json_t *route_entry;
json_t *route_list;
json_t *frr_routing;
const char *afi;
json_t *nexthop;
json_t *root;
char *result;
root = json_object();
if (!root)
return NULL;
frr_routing = json_object();
control_plane_protocols = json_object();
control_plane_protocol = json_array();
json_t *protocol = json_object();
json_object_set_new(protocol, "type", json_string("frr-staticd:staticd"));
json_object_set_new(protocol, "name", json_string("staticd"));
json_object_set_new(protocol, "vrf", json_string("default"));
frr_staticd = json_object();
route_list = json_array();
r = TAILQ_FIRST(routes);
while (r != NULL) {
if (r->family == AF_INET) {
inet_ntop(AF_INET, &r->prefix.ip4, addr_buf, sizeof(addr_buf));
afi = "frr-routing:ipv4-unicast";
} else {
inet_ntop(AF_INET6, &r->prefix.ip6, addr_buf, sizeof(addr_buf));
afi = "frr-routing:ipv6-unicast";
}
snprintf(prefix_str, sizeof(prefix_str), "%s/%d", addr_buf, (int)r->prefixlen);
route_entry = json_object();
json_object_set_new(route_entry, "prefix", json_string(prefix_str));
json_object_set_new(route_entry, "src-prefix", json_string("::/0"));
json_object_set_new(route_entry, "afi-safi", json_string(afi));
json_t *path_list = json_array();
/* Add all routes with the same prefix as path-list entries */
curr = r;
while (curr != NULL && same_prefix(r, curr)) {
json_t *path = json_object();
json_object_set_new(path, "table-id", json_integer(0));
json_object_set_new(path, "distance", json_integer((int)curr->distance));
json_t *frr_nexthops = json_object();
json_t *nexthop_array = json_array();
nexthop = json_object();
/* Next-hop */
switch (curr->nh_type) {
case NH_ADDR:
if (curr->family == AF_INET)
inet_ntop(AF_INET, &curr->gateway.gw4, addr_buf, sizeof(addr_buf));
else
inet_ntop(AF_INET6, &curr->gateway.gw6, addr_buf, sizeof(addr_buf));
json_object_set_new(nexthop, "nh-type", json_string(curr->family == AF_INET ? "ip4" : "ip6"));
json_object_set_new(nexthop, "vrf", json_string("default"));
json_object_set_new(nexthop, "gateway", json_string(addr_buf));
json_object_set_new(nexthop, "interface", json_string(""));
break;
case NH_IFNAME:
json_object_set_new(nexthop, "nh-type", json_string("ifindex"));
json_object_set_new(nexthop, "vrf", json_string("default"));
json_object_set_new(nexthop, "gateway", json_string(""));
json_object_set_new(nexthop, "interface", json_string(curr->ifname));
break;
case NH_BLACKHOLE:
json_object_set_new(nexthop, "nh-type", json_string("blackhole"));
json_object_set_new(nexthop, "vrf", json_string("default"));
json_object_set_new(nexthop, "gateway", json_string(""));
json_object_set_new(nexthop, "interface", json_string(""));
switch (curr->bh_type) {
case BH_DROP:
json_object_set_new(nexthop, "bh-type", json_string("null"));
break;
case BH_REJECT:
json_object_set_new(nexthop, "bh-type", json_string("reject"));
break;
case BH_NULL:
json_object_set_new(nexthop, "bh-type", json_string("unspec"));
break;
}
break;
}
json_array_append_new(nexthop_array, nexthop);
json_object_set_new(frr_nexthops, "nexthop", nexthop_array);
json_object_set_new(path, "frr-nexthops", frr_nexthops);
json_array_append_new(path_list, path);
curr = TAILQ_NEXT(curr, entries);
}
json_object_set_new(route_entry, "path-list", path_list);
json_array_append_new(route_list, route_entry);
r = curr;
}
json_object_set_new(frr_staticd, "route-list", route_list);
json_object_set_new(protocol, "frr-staticd:staticd", frr_staticd);
json_array_append_new(control_plane_protocol, protocol);
json_object_set_new(control_plane_protocols, "control-plane-protocol", control_plane_protocol);
json_object_set_new(frr_routing, "control-plane-protocols", control_plane_protocols);
json_object_set_new(root, "frr-routing:routing", frr_routing);
result = json_dumps(root, JSON_COMPACT);
json_decref(root);
if (!result)
return NULL;
if (strlen(result) >= JSON_BUFFER_SIZE) {
ERROR("json_builder: buffer overflow (needed %zu, have %d)", strlen(result), JSON_BUFFER_SIZE);
free(result);
return NULL;
}
strncpy(json_buffer, result, JSON_BUFFER_SIZE - 1);
json_buffer[JSON_BUFFER_SIZE - 1] = '\0';
free(result);
return json_buffer;
}
/*
* Build JSON configuration for RIP following FRR's YANG model.
* RIP uses /frr-ripd:ripd/instance path, not control-plane-protocols.
*/
const char *build_rip_json(struct rip_config *rip_cfg)
{
struct rip_redistribute *redist;
char addr_buf[INET_ADDRSTRLEN];
struct rip_neighbor *nbr;
struct rip_network *net;
json_t *instance_array;
const char *proto;
json_t *instance;
json_t *ripd_obj;
json_t *array;
json_t *root;
char *result;
if (!rip_cfg || !rip_cfg->enabled)
return "";
root = json_object();
if (!root)
return NULL;
ripd_obj = json_object();
instance_array = json_array();
instance = json_object();
json_object_set_new(instance, "vrf", json_string("default"));
/* Default metric */
if (rip_cfg->default_metric != 1)
json_object_set_new(instance, "default-metric", json_integer((int)rip_cfg->default_metric));
/* Distance */
if (rip_cfg->distance != 120) {
json_t *distance_obj = json_object();
json_object_set_new(distance_obj, "default", json_integer((int)rip_cfg->distance));
json_object_set_new(instance, "distance", distance_obj);
}
/* Timers */
if (rip_cfg->timers.update != 30 || rip_cfg->timers.invalid != 180 ||
rip_cfg->timers.flush != 240) {
json_t *timers = json_object();
json_object_set_new(timers, "update", json_integer(rip_cfg->timers.update));
json_object_set_new(timers, "timeout", json_integer(rip_cfg->timers.invalid));
json_object_set_new(timers, "garbage-collection", json_integer(rip_cfg->timers.flush));
json_object_set_new(instance, "timers", timers);
}
/* Default route origination */
if (rip_cfg->default_route)
json_object_set_new(instance, "default-information-originate", json_true());
/* Interfaces (network statements) */
array = json_array();
TAILQ_FOREACH(net, &rip_cfg->networks, entries)
json_array_append_new(array, json_string(net->ifname));
if (json_array_size(array) > 0)
json_object_set_new(instance, "interface", array);
else
json_decref(array);
/* Explicit neighbors */
array = json_array();
TAILQ_FOREACH(nbr, &rip_cfg->neighbors, entries) {
inet_ntop(AF_INET, &nbr->addr, addr_buf, sizeof(addr_buf));
json_array_append_new(array, json_string(addr_buf));
}
if (json_array_size(array) > 0)
json_object_set_new(instance, "explicit-neighbor", array);
else
json_decref(array);
/* Redistribution */
array = json_array();
TAILQ_FOREACH(redist, &rip_cfg->redistributes, entries) {
proto = NULL;
switch (redist->type) {
case RIP_REDIST_CONNECTED:
proto = "connected";
break;
case RIP_REDIST_STATIC:
proto = "static";
break;
case RIP_REDIST_KERNEL:
proto = "kernel";
break;
case RIP_REDIST_OSPF:
proto = "ospf";
break;
}
if (proto) {
json_t *redist_obj = json_object();
json_object_set_new(redist_obj, "protocol", json_string(proto));
json_array_append_new(array, redist_obj);
}
}
if (json_array_size(array) > 0)
json_object_set_new(instance, "redistribute", array);
else
json_decref(array);
/* Passive interfaces */
array = json_array();
TAILQ_FOREACH(net, &rip_cfg->networks, entries) {
if (net->passive)
json_array_append_new(array, json_string(net->ifname));
}
if (json_array_size(array) > 0)
json_object_set_new(instance, "passive-interface", array);
else
json_decref(array);
json_array_append_new(instance_array, instance);
json_object_set_new(ripd_obj, "instance", instance_array);
json_object_set_new(root, "frr-ripd:ripd", ripd_obj);
result = json_dumps(root, JSON_COMPACT);
json_decref(root);
if (!result)
return NULL;
if (strlen(result) >= JSON_BUFFER_SIZE) {
ERROR("json_builder: buffer overflow (needed %zu, have %d)", strlen(result), JSON_BUFFER_SIZE);
free(result);
return NULL;
}
strncpy(json_buffer, result, JSON_BUFFER_SIZE - 1);
json_buffer[JSON_BUFFER_SIZE - 1] = '\0';
free(result);
return json_buffer;
}
/*
* Build complete routing configuration JSON with both static routes and RIP.
* Static routes go in /frr-routing:routing/control-plane-protocols
* RIP goes in /frr-ripd:ripd/instance (top level, separate container)
*/
const char *build_routing_json(struct route_head *routes, struct rip_config *rip_cfg)
{
char prefix_str[INET6_ADDRSTRLEN + 4];
char addr_buf[INET6_ADDRSTRLEN];
struct route *r, *curr;
json_t *root;
char *result;
root = json_object();
if (!root)
return NULL;
/* Add static routes */
if (routes && !TAILQ_EMPTY(routes)) {
json_t *control_plane_protocols = json_object();
json_t *control_plane_protocol = json_array();
json_t *frr_routing = json_object();
json_t *protocol = json_object();
json_object_set_new(protocol, "type", json_string("frr-staticd:staticd"));
json_object_set_new(protocol, "name", json_string("staticd"));
json_object_set_new(protocol, "vrf", json_string("default"));
json_t *frr_staticd = json_object();
json_t *route_list = json_array();
r = TAILQ_FIRST(routes);
while (r != NULL) {
const char *afi;
if (r->family == AF_INET) {
inet_ntop(AF_INET, &r->prefix.ip4, addr_buf, sizeof(addr_buf));
afi = "frr-routing:ipv4-unicast";
} else {
inet_ntop(AF_INET6, &r->prefix.ip6, addr_buf, sizeof(addr_buf));
afi = "frr-routing:ipv6-unicast";
}
snprintf(prefix_str, sizeof(prefix_str), "%s/%d", addr_buf, (int)r->prefixlen);
json_t *route_entry = json_object();
json_object_set_new(route_entry, "prefix", json_string(prefix_str));
json_object_set_new(route_entry, "src-prefix", json_string("::/0"));
json_object_set_new(route_entry, "afi-safi", json_string(afi));
json_t *path_list = json_array();
curr = r;
while (curr != NULL && same_prefix(r, curr)) {
json_t *path = json_object();
json_object_set_new(path, "table-id", json_integer(0));
json_object_set_new(path, "distance", json_integer((int)curr->distance));
json_t *frr_nexthops = json_object();
json_t *nexthop_array = json_array();
json_t *nexthop = json_object();
switch (curr->nh_type) {
case NH_ADDR:
if (curr->family == AF_INET)
inet_ntop(AF_INET, &curr->gateway.gw4, addr_buf, sizeof(addr_buf));
else
inet_ntop(AF_INET6, &curr->gateway.gw6, addr_buf, sizeof(addr_buf));
json_object_set_new(nexthop, "nh-type", json_string(curr->family == AF_INET ? "ip4" : "ip6"));
json_object_set_new(nexthop, "vrf", json_string("default"));
json_object_set_new(nexthop, "gateway", json_string(addr_buf));
json_object_set_new(nexthop, "interface", json_string(""));
break;
case NH_IFNAME:
json_object_set_new(nexthop, "nh-type", json_string("ifindex"));
json_object_set_new(nexthop, "vrf", json_string("default"));
json_object_set_new(nexthop, "gateway", json_string(""));
json_object_set_new(nexthop, "interface", json_string(curr->ifname));
break;
case NH_BLACKHOLE:
json_object_set_new(nexthop, "nh-type", json_string("blackhole"));
json_object_set_new(nexthop, "vrf", json_string("default"));
json_object_set_new(nexthop, "gateway", json_string(""));
json_object_set_new(nexthop, "interface", json_string(""));
switch (curr->bh_type) {
case BH_DROP:
json_object_set_new(nexthop, "bh-type", json_string("null"));
break;
case BH_REJECT:
json_object_set_new(nexthop, "bh-type", json_string("reject"));
break;
case BH_NULL:
json_object_set_new(nexthop, "bh-type", json_string("unspec"));
break;
}
break;
}
json_array_append_new(nexthop_array, nexthop);
json_object_set_new(frr_nexthops, "nexthop", nexthop_array);
json_object_set_new(path, "frr-nexthops", frr_nexthops);
json_array_append_new(path_list, path);
curr = TAILQ_NEXT(curr, entries);
}
json_object_set_new(route_entry, "path-list", path_list);
json_array_append_new(route_list, route_entry);
r = curr;
}
json_object_set_new(frr_staticd, "route-list", route_list);
json_object_set_new(protocol, "frr-staticd:staticd", frr_staticd);
json_array_append_new(control_plane_protocol, protocol);
json_object_set_new(control_plane_protocols, "control-plane-protocol", control_plane_protocol);
json_object_set_new(frr_routing, "control-plane-protocols", control_plane_protocols);
json_object_set_new(root, "frr-routing:routing", frr_routing);
}
/* Add RIP config (separate top-level container) */
if (rip_cfg && rip_cfg->enabled) {
struct rip_redistribute *redist;
struct rip_neighbor *nbr;
struct rip_network *net;
const char *proto;
json_t *array;
json_t *ripd_obj = json_object();
json_t *instance_array = json_array();
json_t *instance = json_object();
json_object_set_new(instance, "vrf", json_string("default"));
if (rip_cfg->default_metric != 1)
json_object_set_new(instance, "default-metric", json_integer((int)rip_cfg->default_metric));
if (rip_cfg->distance != 120) {
json_t *distance_obj = json_object();
json_object_set_new(distance_obj, "default", json_integer((int)rip_cfg->distance));
json_object_set_new(instance, "distance", distance_obj);
}
if (rip_cfg->timers.update != 30 || rip_cfg->timers.invalid != 180 ||
rip_cfg->timers.flush != 240) {
json_t *timers = json_object();
json_object_set_new(timers, "update", json_integer(rip_cfg->timers.update));
json_object_set_new(timers, "timeout", json_integer(rip_cfg->timers.invalid));
json_object_set_new(timers, "garbage-collection", json_integer(rip_cfg->timers.flush));
json_object_set_new(instance, "timers", timers);
}
if (rip_cfg->default_route)
json_object_set_new(instance, "default-information-originate", json_true());
array = json_array();
TAILQ_FOREACH(net, &rip_cfg->networks, entries)
json_array_append_new(array, json_string(net->ifname));
if (json_array_size(array) > 0)
json_object_set_new(instance, "interface", array);
else
json_decref(array);
array = json_array();
TAILQ_FOREACH(nbr, &rip_cfg->neighbors, entries) {
inet_ntop(AF_INET, &nbr->addr, addr_buf, sizeof(addr_buf));
json_array_append_new(array, json_string(addr_buf));
}
if (json_array_size(array) > 0)
json_object_set_new(instance, "explicit-neighbor", array);
else
json_decref(array);
array = json_array();
TAILQ_FOREACH(redist, &rip_cfg->redistributes, entries) {
proto = NULL;
switch (redist->type) {
case RIP_REDIST_CONNECTED:
proto = "connected";
break;
case RIP_REDIST_STATIC:
proto = "static";
break;
case RIP_REDIST_KERNEL:
proto = "kernel";
break;
case RIP_REDIST_OSPF:
proto = "ospf";
break;
}
if (proto) {
json_t *redist_obj = json_object();
json_object_set_new(redist_obj, "protocol", json_string(proto));
json_array_append_new(array, redist_obj);
}
}
if (json_array_size(array) > 0)
json_object_set_new(instance, "redistribute", array);
else
json_decref(array);
array = json_array();
TAILQ_FOREACH(net, &rip_cfg->networks, entries) {
if (net->passive)
json_array_append_new(array, json_string(net->ifname));
}
if (json_array_size(array) > 0)
json_object_set_new(instance, "passive-interface", array);
else
json_decref(array);
json_array_append_new(instance_array, instance);
json_object_set_new(ripd_obj, "instance", instance_array);
json_object_set_new(root, "frr-ripd:ripd", ripd_obj);
}
result = json_dumps(root, JSON_COMPACT);
json_decref(root);
if (!result)
return NULL;
if (strlen(result) >= JSON_BUFFER_SIZE) {
ERROR("json_builder: buffer overflow (needed %zu, have %d)", strlen(result), JSON_BUFFER_SIZE);
free(result);
return NULL;
}
strncpy(json_buffer, result, JSON_BUFFER_SIZE - 1);
json_buffer[JSON_BUFFER_SIZE - 1] = '\0';
free(result);
return json_buffer;
}
+18
View File
@@ -0,0 +1,18 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef NETD_JSON_BUILDER_H_
#define NETD_JSON_BUILDER_H_
#include "netd.h"
/*
* Build JSON configuration for FRR routing daemons.
* Returns pointer to static buffer containing JSON string.
* Buffer is valid until next call to any build_*_json function.
*/
const char *build_staticd_json(struct route_head *routes);
const char *build_rip_json(struct rip_config *rip_cfg);
const char *build_routing_json(struct route_head *routes, struct rip_config *rip_cfg);
#endif /* NETD_JSON_BUILDER_H_ */
+452
View File
@@ -0,0 +1,452 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Linux kernel backend for netd - sets routes directly in kernel
* via rtnetlink. Does not use FRR.
*/
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <net/if.h>
#include "netd.h"
#include "linux_backend.h"
static int nl_sock = -1;
static uint32_t nl_seq = 0;
/* Helper: add rtnetlink attribute */
static void rta_add(struct nlmsghdr *nlh, size_t maxlen, int type, const void *data, int len)
{
size_t rtalen = RTA_LENGTH(len);
struct rtattr *rta;
if (NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rtalen) > maxlen) {
ERROR("rtnetlink: attribute overflow");
return;
}
rta = (struct rtattr *)(((char *)nlh) + NLMSG_ALIGN(nlh->nlmsg_len));
rta->rta_type = type;
rta->rta_len = rtalen;
if (len)
memcpy(RTA_DATA(rta), data, len);
nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rtalen);
}
/* Send netlink message and wait for ACK */
static int nl_talk(struct nlmsghdr *nlh)
{
struct iovec iov = { .iov_base = nlh, .iov_len = nlh->nlmsg_len };
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
struct msghdr msg = {
.msg_name = &sa,
.msg_namelen = sizeof(sa),
.msg_iov = &iov,
.msg_iovlen = 1,
};
struct nlmsgerr *err;
struct nlmsghdr *h;
char buf[4096];
int ret;
/* Send request */
ret = sendmsg(nl_sock, &msg, 0);
if (ret < 0) {
ERROR("netlink sendmsg: %s", strerror(errno));
return -1;
}
/* Receive ACK */
iov.iov_base = buf;
iov.iov_len = sizeof(buf);
ret = recvmsg(nl_sock, &msg, 0);
if (ret < 0) {
ERROR("netlink recvmsg: %s", strerror(errno));
return -1;
}
/* Parse ACK */
h = (struct nlmsghdr *)buf;
if (h->nlmsg_type == NLMSG_ERROR) {
err = (struct nlmsgerr *)NLMSG_DATA(h);
if (err->error) {
errno = -err->error;
return -1;
}
return 0; /* Success */
}
ERROR("netlink: unexpected response type %d", h->nlmsg_type);
return -1;
}
/* Add or delete a route */
static int netlink_route_op(const struct route *r, int cmd)
{
char addrstr[INET6_ADDRSTRLEN];
struct nlmsghdr *nlh;
struct rtmsg *rtm;
uint32_t ifindex;
char buf[4096];
memset(buf, 0, sizeof(buf));
nlh = (struct nlmsghdr *)buf;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlh->nlmsg_type = cmd;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
if (cmd == RTM_NEWROUTE)
nlh->nlmsg_flags |= NLM_F_CREATE | NLM_F_REPLACE;
nlh->nlmsg_seq = ++nl_seq;
rtm = (struct rtmsg *)NLMSG_DATA(nlh);
rtm->rtm_family = r->family;
rtm->rtm_dst_len = r->prefixlen;
rtm->rtm_table = RT_TABLE_MAIN;
rtm->rtm_protocol = RTPROT_STATIC;
rtm->rtm_scope = RT_SCOPE_UNIVERSE;
rtm->rtm_type = RTN_UNICAST;
/* Destination prefix */
if (r->family == AF_INET)
rta_add(nlh, sizeof(buf), RTA_DST, &r->prefix.ip4, sizeof(r->prefix.ip4));
else
rta_add(nlh, sizeof(buf), RTA_DST, &r->prefix.ip6, sizeof(r->prefix.ip6));
/* Nexthop */
switch (r->nh_type) {
case NH_ADDR:
/* Gateway address */
if (r->family == AF_INET) {
rta_add(nlh, sizeof(buf), RTA_GATEWAY, &r->gateway.gw4, sizeof(r->gateway.gw4));
inet_ntop(AF_INET, &r->gateway.gw4, addrstr, sizeof(addrstr));
} else {
rta_add(nlh, sizeof(buf), RTA_GATEWAY, &r->gateway.gw6, sizeof(r->gateway.gw6));
inet_ntop(AF_INET6, &r->gateway.gw6, addrstr, sizeof(addrstr));
}
DEBUG("netlink: %s route via %s",
cmd == RTM_NEWROUTE ? "add" : "del", addrstr);
break;
case NH_IFNAME:
/* Output interface */
ifindex = if_nametoindex(r->ifname);
if (!ifindex) {
ERROR("netlink: interface %s not found", r->ifname);
return -1;
}
rta_add(nlh, sizeof(buf), RTA_OIF, &ifindex, sizeof(ifindex));
DEBUG("netlink: %s route dev %s",
cmd == RTM_NEWROUTE ? "add" : "del", r->ifname);
break;
case NH_BLACKHOLE:
/* Blackhole route */
switch (r->bh_type) {
case BH_DROP:
rtm->rtm_type = RTN_BLACKHOLE;
break;
case BH_REJECT:
rtm->rtm_type = RTN_UNREACHABLE;
break;
case BH_NULL:
rtm->rtm_type = RTN_BLACKHOLE;
break;
}
DEBUG("netlink: %s blackhole route",
cmd == RTM_NEWROUTE ? "add" : "del");
break;
}
/* Priority (metric/distance) - kernel expects 32-bit value */
if (r->distance) {
uint32_t priority = r->distance;
rta_add(nlh, sizeof(buf), RTA_PRIORITY, &priority, sizeof(priority));
}
/* Send and wait for ACK */
if (nl_talk(nlh)) {
if (errno == EEXIST && cmd == RTM_NEWROUTE) {
DEBUG("netlink: route already exists");
return 0;
}
if (errno == ESRCH && cmd == RTM_DELROUTE) {
DEBUG("netlink: route doesn't exist");
return 0;
}
ERROR("netlink: %s route failed: %s",
cmd == RTM_NEWROUTE ? "add" : "del", strerror(errno));
return -1;
}
return 0;
}
int netlink_route_add(const struct route *r)
{
return netlink_route_op(r, RTM_NEWROUTE);
}
int netlink_route_del(const struct route *r)
{
return netlink_route_op(r, RTM_DELROUTE);
}
int linux_backend_init(void)
{
struct sockaddr_nl sa = {
.nl_family = AF_NETLINK,
};
INFO("Using Linux kernel backend (direct rtnetlink, no FRR)");
nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (nl_sock < 0) {
ERROR("Failed to create netlink socket: %s", strerror(errno));
return -1;
}
if (bind(nl_sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
ERROR("Failed to bind netlink socket: %s", strerror(errno));
close(nl_sock);
nl_sock = -1;
return -1;
}
DEBUG("Linux backend initialized, netlink socket fd=%d", nl_sock);
return 0;
}
void linux_backend_cleanup(void)
{
if (nl_sock >= 0) {
close(nl_sock);
nl_sock = -1;
DEBUG("Linux backend shutdown");
}
}
/* Check if route exists in list */
static int route_exists(struct route_head *list, const struct route *needle)
{
struct route *r;
TAILQ_FOREACH(r, list, entries) {
if (r->family != needle->family)
continue;
if (r->prefixlen != needle->prefixlen)
continue;
/* Compare prefix */
if (r->family == AF_INET) {
if (memcmp(&r->prefix.ip4, &needle->prefix.ip4, sizeof(r->prefix.ip4)))
continue;
} else {
if (memcmp(&r->prefix.ip6, &needle->prefix.ip6, sizeof(r->prefix.ip6)))
continue;
}
/* Compare nexthop type */
if (r->nh_type != needle->nh_type)
continue;
/* Compare nexthop details */
switch (r->nh_type) {
case NH_ADDR:
if (r->family == AF_INET) {
if (memcmp(&r->gateway.gw4, &needle->gateway.gw4, sizeof(r->gateway.gw4)))
continue;
} else {
if (memcmp(&r->gateway.gw6, &needle->gateway.gw6, sizeof(r->gateway.gw6)))
continue;
}
break;
case NH_IFNAME:
if (strcmp(r->ifname, needle->ifname))
continue;
break;
case NH_BLACKHOLE:
if (r->bh_type != needle->bh_type)
continue;
break;
}
/* Match found */
return 1;
}
return 0;
}
/* Read installed routes from kernel (proto=RTPROT_STATIC only) */
static int kernel_read_routes(struct route_head *routes, int family)
{
struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
struct nlmsghdr *nlh;
struct rtattr *rta;
struct msghdr msg;
struct rtmsg *rtm;
struct iovec iov;
struct route *r;
char buf[8192];
int rta_len;
int ret;
msg.msg_name = &sa;
msg.msg_namelen = sizeof(sa);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
/* Request route dump */
memset(buf, 0, sizeof(buf));
nlh = (struct nlmsghdr *)buf;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlh->nlmsg_type = RTM_GETROUTE;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
nlh->nlmsg_seq = ++nl_seq;
rtm = (struct rtmsg *)NLMSG_DATA(nlh);
rtm->rtm_family = family;
iov.iov_base = nlh;
iov.iov_len = nlh->nlmsg_len;
if (sendmsg(nl_sock, &msg, 0) < 0) {
ERROR("netlink: failed to request route dump: %s", strerror(errno));
return -1;
}
/* Read response */
while (1) {
iov.iov_base = buf;
iov.iov_len = sizeof(buf);
ret = recvmsg(nl_sock, &msg, 0);
if (ret < 0) {
ERROR("netlink: route dump recvmsg: %s", strerror(errno));
return -1;
}
for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, ret); nlh = NLMSG_NEXT(nlh, ret)) {
if (nlh->nlmsg_type == NLMSG_DONE)
return 0;
if (nlh->nlmsg_type == NLMSG_ERROR) {
ERROR("netlink: route dump error");
return -1;
}
if (nlh->nlmsg_type != RTM_NEWROUTE)
continue;
rtm = (struct rtmsg *)NLMSG_DATA(nlh);
/* Only handle routes we manage (proto=RTPROT_STATIC) */
if (rtm->rtm_protocol != RTPROT_STATIC)
continue;
/* Parse route attributes */
r = calloc(1, sizeof(*r));
if (!r)
continue;
r->family = rtm->rtm_family;
r->prefixlen = rtm->rtm_dst_len;
/* Parse attributes */
rta = RTM_RTA(rtm);
rta_len = RTM_PAYLOAD(nlh);
for (; RTA_OK(rta, rta_len); rta = RTA_NEXT(rta, rta_len)) {
switch (rta->rta_type) {
case RTA_DST:
if (r->family == AF_INET)
memcpy(&r->prefix.ip4, RTA_DATA(rta), sizeof(r->prefix.ip4));
else
memcpy(&r->prefix.ip6, RTA_DATA(rta), sizeof(r->prefix.ip6));
break;
case RTA_GATEWAY:
r->nh_type = NH_ADDR;
if (r->family == AF_INET)
memcpy(&r->gateway.gw4, RTA_DATA(rta), sizeof(r->gateway.gw4));
else
memcpy(&r->gateway.gw6, RTA_DATA(rta), sizeof(r->gateway.gw6));
break;
case RTA_OIF:
r->nh_type = NH_IFNAME;
if_indextoname(*(uint32_t *)RTA_DATA(rta), r->ifname);
break;
case RTA_PRIORITY:
r->distance = *(uint32_t *)RTA_DATA(rta);
break;
}
}
/* Detect blackhole routes */
if (rtm->rtm_type == RTN_BLACKHOLE || rtm->rtm_type == RTN_UNREACHABLE) {
r->nh_type = NH_BLACKHOLE;
if (rtm->rtm_type == RTN_UNREACHABLE)
r->bh_type = BH_REJECT;
else
r->bh_type = BH_DROP;
}
TAILQ_INSERT_TAIL(routes, r, entries);
}
}
return 0;
}
int linux_backend_apply(struct route_head *routes, struct rip_config *rip)
{
struct route_head kernel_routes = TAILQ_HEAD_INITIALIZER(kernel_routes);
struct route *r, *tmp;
int removed = 0;
int errors = 0;
int added = 0;
if (rip->enabled)
DEBUG("Linux backend: RIP not supported without FRR");
/* Read current static routes from kernel (both IPv4 and IPv6) */
kernel_read_routes(&kernel_routes, AF_INET);
kernel_read_routes(&kernel_routes, AF_INET6);
/* Remove routes no longer in config */
TAILQ_FOREACH_SAFE(r, &kernel_routes, entries, tmp) {
if (!route_exists(routes, r)) {
DEBUG("Removing old route");
if (netlink_route_del(r) == 0)
removed++;
}
}
/* Add new routes from config (kernel_routes still has old state) */
TAILQ_FOREACH(r, routes, entries) {
if (!route_exists(&kernel_routes, r)) {
DEBUG("Adding new route");
if (netlink_route_add(r) == 0)
added++;
else
errors++;
}
}
/* Free kernel routes list */
while ((tmp = TAILQ_FIRST(&kernel_routes)) != NULL) {
TAILQ_REMOVE(&kernel_routes, tmp, entries);
free(tmp);
}
INFO("Linux backend: +%d -%d routes (%d errors)", added, removed, errors);
return errors ? -1 : 0;
}
+16
View File
@@ -0,0 +1,16 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef NETD_LINUX_BACKEND_H_
#define NETD_LINUX_BACKEND_H_
#include "netd.h"
int linux_backend_init(void);
void linux_backend_cleanup(void);
int linux_backend_apply(struct route_head *routes, struct rip_config *rip);
/* Internal netlink operations */
int netlink_route_add(const struct route *r);
int netlink_route_del(const struct route *r);
#endif /* NETD_LINUX_BACKEND_H_ */
+289
View File
@@ -0,0 +1,289 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <errno.h>
#include <getopt.h>
#include <libite/lite.h>
#include "netd.h"
#include "config.h"
int debug;
static sig_atomic_t do_reload;
static sig_atomic_t do_shutdown;
static struct route_head active_routes = TAILQ_HEAD_INITIALIZER(active_routes);
static struct rip_config active_rip;
/* Backend selection at compile time */
#ifdef HAVE_FRR_GRPC
#include "grpc_backend.h"
static int backend_init(void) { return grpc_backend_init(); }
static void backend_cleanup(void) { grpc_backend_cleanup(); }
static int backend_apply(struct route_head *routes, struct rip_config *rip) {
return grpc_backend_apply(routes, rip);
}
#elif defined(HAVE_FRR_CONF)
#include "frrconf_backend.h"
static int backend_init(void) { return frrconf_backend_init(); }
static void backend_cleanup(void) { frrconf_backend_cleanup(); }
static int backend_apply(struct route_head *routes, struct rip_config *rip) {
return frrconf_backend_apply(routes, rip);
}
#elif defined(HAVE_FRR_VTYSH)
#include "vtysh_backend.h"
static int backend_init(void) { return vtysh_backend_init(); }
static void backend_cleanup(void) { vtysh_backend_cleanup(); }
static int backend_apply(struct route_head *routes, struct rip_config *rip) {
return vtysh_backend_apply(routes, rip);
}
#else
#include "linux_backend.h"
static int backend_init(void) { return linux_backend_init(); }
static void backend_cleanup(void) { linux_backend_cleanup(); }
static int backend_apply(struct route_head *routes, struct rip_config *rip) {
return linux_backend_apply(routes, rip);
}
#endif
static void sighup_handler(int sig)
{
(void)sig;
INFO("Got SIGHUP, reloading ...");
do_reload = 1;
}
static void sigterm_handler(int sig)
{
(void)sig;
do_shutdown = 1;
}
static void route_list_free(struct route_head *head)
{
struct route *r, *tmp;
TAILQ_FOREACH_SAFE(r, head, entries, tmp) {
TAILQ_REMOVE(head, r, entries);
free(r);
}
}
static void rip_config_init(struct rip_config *cfg)
{
memset(cfg, 0, sizeof(*cfg));
/* Set defaults */
cfg->default_metric = 1;
cfg->distance = 120;
cfg->timers.update = 30;
cfg->timers.invalid = 180;
cfg->timers.flush = 240;
TAILQ_INIT(&cfg->networks);
TAILQ_INIT(&cfg->neighbors);
TAILQ_INIT(&cfg->redistributes);
TAILQ_INIT(&cfg->system_cmds);
}
static void rip_config_free(struct rip_config *cfg)
{
struct rip_redistribute *redist, *redist_tmp;
struct rip_system_cmd *cmd, *cmd_tmp;
struct rip_neighbor *nbr, *nbr_tmp;
struct rip_network *net, *net_tmp;
if (!cfg)
return;
TAILQ_FOREACH_SAFE(net, &cfg->networks, entries, net_tmp) {
TAILQ_REMOVE(&cfg->networks, net, entries);
free(net);
}
TAILQ_FOREACH_SAFE(nbr, &cfg->neighbors, entries, nbr_tmp) {
TAILQ_REMOVE(&cfg->neighbors, nbr, entries);
free(nbr);
}
TAILQ_FOREACH_SAFE(redist, &cfg->redistributes, entries, redist_tmp) {
TAILQ_REMOVE(&cfg->redistributes, redist, entries);
free(redist);
}
TAILQ_FOREACH_SAFE(cmd, &cfg->system_cmds, entries, cmd_tmp) {
TAILQ_REMOVE(&cfg->system_cmds, cmd, entries);
free(cmd);
}
}
static void reload(void)
{
struct route_head new_routes = TAILQ_HEAD_INITIALIZER(new_routes);
struct rip_redistribute *redist;
struct rip_system_cmd *cmd;
struct rip_config new_rip;
struct rip_neighbor *nbr;
struct rip_network *net;
struct route *r;
int count = 0;
INFO("Reloading configuration");
rip_config_init(&new_rip);
if (config_load(&new_routes, &new_rip)) {
ERROR("Failed loading config, keeping current routes");
route_list_free(&new_routes);
rip_config_free(&new_rip);
return;
}
TAILQ_FOREACH(r, &new_routes, entries)
count++;
DEBUG("Loaded %d routes from config", count);
if (new_rip.enabled)
DEBUG("RIP configuration loaded");
/* Apply config via backend */
if (backend_apply(&new_routes, &new_rip)) {
ERROR("Failed applying config via backend");
route_list_free(&new_routes);
rip_config_free(&new_rip);
return;
}
route_list_free(&active_routes);
TAILQ_INIT(&active_routes);
rip_config_free(&active_rip);
rip_config_init(&active_rip);
/* Move new_routes to active_routes */
while ((r = TAILQ_FIRST(&new_routes)) != NULL) {
TAILQ_REMOVE(&new_routes, r, entries);
TAILQ_INSERT_TAIL(&active_routes, r, entries);
}
/* Move new_rip to active_rip - copy scalars and move lists */
active_rip.enabled = new_rip.enabled;
active_rip.default_metric = new_rip.default_metric;
active_rip.distance = new_rip.distance;
active_rip.default_route = new_rip.default_route;
active_rip.debug_events = new_rip.debug_events;
active_rip.debug_packet = new_rip.debug_packet;
active_rip.debug_kernel = new_rip.debug_kernel;
active_rip.timers = new_rip.timers;
/* Move network list */
while ((net = TAILQ_FIRST(&new_rip.networks)) != NULL) {
TAILQ_REMOVE(&new_rip.networks, net, entries);
TAILQ_INSERT_TAIL(&active_rip.networks, net, entries);
}
/* Move neighbor list */
while ((nbr = TAILQ_FIRST(&new_rip.neighbors)) != NULL) {
TAILQ_REMOVE(&new_rip.neighbors, nbr, entries);
TAILQ_INSERT_TAIL(&active_rip.neighbors, nbr, entries);
}
/* Move redistribute list */
while ((redist = TAILQ_FIRST(&new_rip.redistributes)) != NULL) {
TAILQ_REMOVE(&new_rip.redistributes, redist, entries);
TAILQ_INSERT_TAIL(&active_rip.redistributes, redist, entries);
}
/* Move system commands list */
while ((cmd = TAILQ_FIRST(&new_rip.system_cmds)) != NULL) {
TAILQ_REMOVE(&new_rip.system_cmds, cmd, entries);
TAILQ_INSERT_TAIL(&active_rip.system_cmds, cmd, entries);
}
/* Execute system commands after config is applied.
* Run in background with retry since daemons may not be ready yet. */
if (!TAILQ_EMPTY(&active_rip.system_cmds)) {
TAILQ_FOREACH(cmd, &active_rip.system_cmds, entries) {
char retry_cmd[512];
snprintf(retry_cmd, sizeof(retry_cmd),
"(for i in 1 2 3 4 5; do %s && break || sleep 1; done) &",
cmd->command);
DEBUG("Executing system command with retry: %s", cmd->command);
if (system(retry_cmd) != 0)
ERROR("Failed to launch system command: %s", cmd->command);
}
}
INFO("Configuration reloaded");
pidfile(NULL);
}
static int usage(int rc)
{
fprintf(stderr,
"Usage: netd [-dh]\n"
" -d Enable debug (log to stderr)\n"
" -h Show this help text\n");
return rc;
}
int main(int argc, char *argv[])
{
int log_opts = LOG_PID | LOG_NDELAY;
struct sigaction sa = { 0 };
int c;
while ((c = getopt(argc, argv, "dhp:")) != -1) {
switch (c) {
case 'd':
log_opts |= LOG_PERROR;
debug = 1;
break;
case 'h':
return usage(0);
default:
return usage(1);
}
}
openlog("netd", log_opts, LOG_DAEMON);
setlogmask(LOG_UPTO(LOG_INFO));
INFO("starting");
/* Set up signal handlers */
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;
while (!do_shutdown) {
if (do_reload) {
do_reload = 0;
reload();
}
pause();
}
INFO("shutting down");
route_list_free(&active_routes);
rip_config_free(&active_rip);
backend_cleanup();
closelog();
return 0;
}
+126
View File
@@ -0,0 +1,126 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef NETD_H_
#define NETD_H_
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <libite/queue.h>
extern int debug;
#define LOG(level, fmt, args...) syslog(level, fmt, ##args)
#define ERROR(fmt, args...) LOG(LOG_ERR, fmt, ##args)
#define INFO(fmt, args...) LOG(LOG_INFO, fmt, ##args)
#define DEBUG(fmt, args...) do { if (debug) LOG(LOG_DEBUG, fmt, ##args); } while (0)
#define CONF_DIR "/etc/netd/conf.d"
/* Nexthop types */
enum nh_type {
NH_IFNAME, /* Nexthop is interface name */
NH_ADDR, /* Nexthop is IP address */
NH_BLACKHOLE, /* Blackhole route */
};
/* Blackhole subtypes */
enum bh_type {
BH_NULL, /* Null0 interface */
BH_REJECT, /* ICMP unreachable */
BH_DROP, /* Silent drop */
};
/* Static route entry */
struct route {
int family; /* AF_INET or AF_INET6 */
uint8_t prefixlen; /* Prefix length */
uint8_t distance; /* Administrative distance */
uint32_t tag; /* Route tag */
union {
struct in_addr ip4;
struct in6_addr ip6;
} prefix;
enum nh_type nh_type;
enum bh_type bh_type; /* For NH_BLACKHOLE */
union {
struct in_addr gw4;
struct in6_addr gw6;
} gateway; /* For NH_ADDR */
char ifname[IFNAMSIZ]; /* For NH_IFNAME */
TAILQ_ENTRY(route) entries;
};
/* Route list head */
TAILQ_HEAD(route_head, route);
/* RIP network/interface configuration */
struct rip_network {
char ifname[IFNAMSIZ];
int passive; /* Is this interface passive? */
TAILQ_ENTRY(rip_network) entries;
};
/* RIP neighbor configuration */
struct rip_neighbor {
struct in_addr addr;
TAILQ_ENTRY(rip_neighbor) entries;
};
/* RIP redistribution configuration */
enum rip_redist_type {
RIP_REDIST_CONNECTED,
RIP_REDIST_STATIC,
RIP_REDIST_KERNEL,
RIP_REDIST_OSPF,
};
struct rip_redistribute {
enum rip_redist_type type;
TAILQ_ENTRY(rip_redistribute) entries;
};
/* System commands to execute after applying config */
#define RIP_SYSTEM_CMD_MAX 256
struct rip_system_cmd {
char command[RIP_SYSTEM_CMD_MAX];
TAILQ_ENTRY(rip_system_cmd) entries;
};
/* RIP timers */
struct rip_timers {
uint32_t update; /* Update interval (default 30) */
uint32_t invalid; /* Invalid interval (default 180) */
uint32_t flush; /* Flush interval (default 240) */
};
/* Main RIP configuration */
struct rip_config {
int enabled; /* Is RIP enabled? */
uint8_t default_metric; /* Default metric (default 1) */
uint8_t distance; /* Administrative distance (default 120) */
int default_route; /* Originate default route? */
int debug_events; /* Enable RIP events debug? */
int debug_packet; /* Enable RIP packet debug? */
int debug_kernel; /* Enable kernel routing debug? */
struct rip_timers timers;
TAILQ_HEAD(, rip_network) networks;
TAILQ_HEAD(, rip_neighbor) neighbors;
TAILQ_HEAD(, rip_redistribute) redistributes;
TAILQ_HEAD(, rip_system_cmd) system_cmds;
};
#endif /* NETD_H_ */
+298
View File
@@ -0,0 +1,298 @@
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* FRR vtysh backend for netd - applies incremental route changes
* by generating a diff (negations of old state + new state) and
* feeding it to running FRR daemons via `vtysh -b`.
*
* Old state is persisted in NETD_CONF so we can recover after a
* crash — on startup, any routes/config in that file are negated
* before new config is applied.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "netd.h"
#include "vtysh_backend.h"
#define FRR_CONF "/etc/frr/frr.conf"
#define FRR_CONF_NEXT FRR_CONF "+"
#define NETD_CONF "/etc/frr/netd.conf"
#define NETD_CONF_NEXT NETD_CONF "+"
#define OSPFD_CONF "/etc/frr/ospfd.conf"
static const char *frr_header =
"! Generated by netd\n"
"frr defaults traditional\n"
"hostname Router\n"
"password zebra\n"
"enable password zebra\n"
"no log unique-id\n"
"log syslog warnings\n"
"log facility local2\n"
"!\n";
int vtysh_backend_init(void)
{
INFO("Using FRR vtysh backend");
return 0;
}
void vtysh_backend_cleanup(void)
{
/* Nothing to clean up */
}
static void write_route(FILE *fp, struct route *r)
{
char prefix_str[INET6_ADDRSTRLEN];
char gw_str[INET6_ADDRSTRLEN];
const char *cmd;
if (r->family == AF_INET) {
cmd = "ip";
inet_ntop(AF_INET, &r->prefix.ip4, prefix_str, sizeof(prefix_str));
} else {
cmd = "ipv6";
inet_ntop(AF_INET6, &r->prefix.ip6, prefix_str, sizeof(prefix_str));
}
fprintf(fp, "%s route %s/%u ", cmd, prefix_str, r->prefixlen);
switch (r->nh_type) {
case NH_ADDR:
if (r->family == AF_INET)
inet_ntop(AF_INET, &r->gateway.gw4, gw_str, sizeof(gw_str));
else
inet_ntop(AF_INET6, &r->gateway.gw6, gw_str, sizeof(gw_str));
fputs(gw_str, fp);
break;
case NH_IFNAME:
fputs(r->ifname, fp);
break;
case NH_BLACKHOLE:
switch (r->bh_type) {
case BH_NULL:
fputs("Null0", fp);
break;
case BH_REJECT:
fputs("reject", fp);
break;
case BH_DROP:
fputs("blackhole", fp);
break;
}
break;
}
if (r->distance)
fprintf(fp, " %u", r->distance);
fputc('\n', fp);
}
static const char *redist_name(enum rip_redist_type type)
{
switch (type) {
case RIP_REDIST_CONNECTED: return "connected";
case RIP_REDIST_STATIC: return "static";
case RIP_REDIST_KERNEL: return "kernel";
case RIP_REDIST_OSPF: return "ospf";
}
return "unknown";
}
static void write_rip_config(FILE *fp, struct rip_config *rip)
{
struct rip_redistribute *redist;
struct rip_neighbor *nbr;
struct rip_network *net;
if (!rip->enabled)
return;
fputs("router rip\n", fp);
fprintf(fp, " default-metric %u\n", rip->default_metric);
fprintf(fp, " distance %u\n", rip->distance);
fprintf(fp, " timers basic %u %u %u\n",
rip->timers.update, rip->timers.invalid, rip->timers.flush);
if (rip->default_route)
fputs(" default-information originate\n", fp);
TAILQ_FOREACH(net, &rip->networks, entries) {
fprintf(fp, " network %s\n", net->ifname);
if (net->passive)
fprintf(fp, " passive-interface %s\n", net->ifname);
}
TAILQ_FOREACH(nbr, &rip->neighbors, entries) {
char addr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &nbr->addr, addr, sizeof(addr));
fprintf(fp, " neighbor %s\n", addr);
}
TAILQ_FOREACH(redist, &rip->redistributes, entries)
fprintf(fp, " redistribute %s\n", redist_name(redist->type));
fputs("!\n", fp);
}
static void append_file(FILE *fp, const char *path)
{
char buf[1024];
FILE *src;
size_t n;
src = fopen(path, "r");
if (!src)
return;
while ((n = fread(buf, 1, sizeof(buf), src)) > 0)
fwrite(buf, 1, n, fp);
fclose(src);
DEBUG("vtysh: appended %s", path);
}
/*
* Negate old state by reading previous netd.conf and prefixing
* each config line with "no ". Comment and blank lines are skipped.
*
* For "router rip" blocks we emit a single "no router rip" instead
* of negating each sub-command individually.
*/
static void negate_old_conf(FILE *fp)
{
char line[256];
int in_rip = 0;
int count = 0;
FILE *old;
old = fopen(NETD_CONF, "r");
if (!old) {
DEBUG("vtysh: no previous %s, first boot", NETD_CONF);
return;
}
while (fgets(line, sizeof(line), old)) {
size_t len = strlen(line);
/* Strip trailing newline */
if (len > 0 && line[len - 1] == '\n')
line[--len] = '\0';
/* Skip empty lines and comments */
if (len == 0 || line[0] == '!' || line[0] == '#')
continue;
/* Track router rip block */
if (strcmp(line, "router rip") == 0) {
in_rip = 1;
fputs("no router rip\n", fp);
count++;
continue;
}
/* Skip sub-commands inside router rip block */
if (in_rip) {
/* Sub-commands are indented with space */
if (line[0] == ' ')
continue;
in_rip = 0;
}
/* Negate route lines (ip route ..., ipv6 route ...) */
fprintf(fp, "no %s\n", line);
count++;
}
fclose(old);
DEBUG("vtysh: negated %d old config lines", count);
}
/*
* Save current config to netd.conf for next reload or crash recovery.
* Written atomically via rename.
*/
static int save_conf(struct route_head *routes, struct rip_config *rip)
{
struct route *r;
FILE *fp;
fp = fopen(NETD_CONF_NEXT, "w");
if (!fp) {
ERROR("vtysh: failed to open %s: %s", NETD_CONF_NEXT, strerror(errno));
return -1;
}
TAILQ_FOREACH(r, routes, entries)
write_route(fp, r);
write_rip_config(fp, rip);
fclose(fp);
if (rename(NETD_CONF_NEXT, NETD_CONF)) {
ERROR("vtysh: failed to rename %s to %s: %s",
NETD_CONF_NEXT, NETD_CONF, strerror(errno));
return -1;
}
return 0;
}
int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip)
{
struct route *r;
int rc, count = 0;
FILE *fp;
fp = fopen(FRR_CONF_NEXT, "w");
if (!fp) {
ERROR("vtysh: failed to open %s: %s", FRR_CONF_NEXT, strerror(errno));
return -1;
}
fputs(frr_header, fp);
/* Negate old state from previous netd.conf */
negate_old_conf(fp);
/* Write new routes */
TAILQ_FOREACH(r, routes, entries) {
write_route(fp, r);
count++;
}
DEBUG("vtysh: wrote %d new routes", count);
/* Write new RIP config */
write_rip_config(fp, rip);
/* Append OSPF config if present (written by confd) */
append_file(fp, OSPFD_CONF);
fclose(fp);
if (rename(FRR_CONF_NEXT, FRR_CONF)) {
ERROR("vtysh: failed to rename %s to %s: %s",
FRR_CONF_NEXT, FRR_CONF, strerror(errno));
return -1;
}
rc = system("vtysh -b");
if (rc) {
ERROR("vtysh: vtysh -b failed with status %d", rc);
return -1;
}
/* Persist new state for next reload / crash recovery */
if (save_conf(routes, rip))
ERROR("vtysh: failed to save %s, next reload may be inconsistent", NETD_CONF);
INFO("vtysh: applied config via vtysh -b");
return 0;
}
+12
View File
@@ -0,0 +1,12 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#ifndef NETD_VTYSH_BACKEND_H_
#define NETD_VTYSH_BACKEND_H_
#include "netd.h"
int vtysh_backend_init(void);
void vtysh_backend_cleanup(void);
int vtysh_backend_apply(struct route_head *routes, struct rip_config *rip);
#endif /* NETD_VTYSH_BACKEND_H_ */
@@ -93,10 +93,17 @@ def wifi(ifname):
info = get_iw_info(ifname)
mode = info.get('iftype', '').lower()
result = {}
if info.get('phy'):
result['radio'] = info['phy']
if mode == 'ap':
return wifi_ap(ifname)
result.update(wifi_ap(ifname))
else:
return wifi_station(ifname)
result.update(wifi_station(ifname))
return result
def parse_wpa_scan_result(scan_output):
+13 -2
View File
@@ -4,9 +4,13 @@ ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/igmp_basic]
==== Description
Verify that all multicast get flooded when no IGMP join exists in the system and
the flooding stops as soon a join arrives
Verify basic IGMP snooping behavior on a bridge. Without any IGMP
membership, multicast should be flooded to all ports. Once a host
joins a group, the bridge should learn the membership via IGMP and
only forward matching multicast to the member port, pruning it from
non-member ports.
....
.1
.---------------------------.
| DUT |
@@ -19,6 +23,13 @@ the flooding stops as soon a join arrives
'-------' '-------' '-------'
.2 .3 .4
HOST
....
A multicast sender on `msend` sends to group 224.1.1.1. First, with
no IGMP joins, verify the group is flooded to both `mrecv` and `!memb`.
Then `mrecv` joins the group and the test waits for the bridge MDB to
reflect the membership before verifying that `!memb` no longer receives
the group.
==== Topology
+18 -27
View File
@@ -1,16 +1,13 @@
#!/usr/bin/env python3
#
# Add join the group 224.1.1.1 from data1 on the host. Send to that
# group from `msend`, verify that `mrecv` receives it and that `!memb`
# does not.
#
"""IGMP basic
"""
IGMP basic
Verify that all multicast get flooded when no IGMP join exists in the system and
the flooding stops as soon a join arrives
Verify basic IGMP snooping behavior on a bridge. Without any IGMP
membership, multicast should be flooded to all ports. Once a host
joins a group, the bridge should learn the membership via IGMP and
only forward matching multicast to the member port, pruning it from
non-member ports.
....
.1
.---------------------------.
| DUT |
@@ -23,12 +20,20 @@ the flooding stops as soon a join arrives
'-------' '-------' '-------'
.2 .3 .4
HOST
....
A multicast sender on `msend` sends to group 224.1.1.1. First, with
no IGMP joins, verify the group is flooded to both `mrecv` and `!memb`.
Then `mrecv` joins the group and the test waits for the bridge MDB to
reflect the membership before verifying that `!memb` no longer receives
the group.
"""
import infamy
import time
import infamy.iface as iface
import infamy.multicast as mcast
from infamy.util import until
query_interval = 4
@@ -118,21 +123,7 @@ with infamy.Test() as test:
receive_ns.must_receive("ip dst 224.1.1.1")
with test.step("Verify that the group 224.1.1.1 is no longer received on host:data3"):
attempt = 0
# This retry loop exists to handle the case where the first query is lost due
# to the network just starting up. In particular there's a case where a newly
# created bridge uses the "NOOP" scheduler (noop_enqueue()) for a split second
# while it's starting up, which might drop the first query msg.
while attempt <= query_interval:
try:
nojoin_ns.must_not_receive("ip dst 224.1.1.1")
break
except Exception as e:
attempt += 1
if attempt > query_interval:
test.fail()
else:
print(f"Got mcast flood, retrying ({attempt}/{query_interval})")
until(lambda: iface.exist_bridge_multicast_filter(target, "224.1.1.1", mreceive, "br0"), attempts=10)
nojoin_ns.must_not_receive("ip dst 224.1.1.1")
test.succeed()
+5 -4
View File
@@ -6,6 +6,7 @@ Verify IPv6 autoconf on a bridge is properly set up for global prefix.
See issue #473 for details.
"""
import infamy
from infamy.util import until
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
@@ -42,9 +43,9 @@ with infamy.Test() as test:
})
with test.step("Verify using sysctl that 'net.ipv6.conf.br0.autoconf' is 1 on target"):
out = tgtssh.runsh("sysctl net.ipv6.conf.br0.autoconf").stdout
print(out)
if "autoconf = 1" not in out:
test.fail()
def check_autoconf():
out = tgtssh.runsh("sysctl net.ipv6.conf.br0.autoconf").stdout
return "autoconf = 1" in out
until(check_autoconf, attempts=10)
test.succeed()
+1 -1
View File
@@ -28,7 +28,7 @@ with infamy.Test() as test:
left = env.attach("left", "mgmt")
right = env.attach("right", "mgmt")
with test.step("Configure DUTs with tunnel {tunnel}"):
with test.step(f"Configure DUTs with tunnel {tunnel}"):
container_left4 = {
"local": "192.168.50.1",
"remote": "192.168.50.2"
@@ -178,6 +178,8 @@ with infamy.Test() as test:
}
}
})
print("Waiting for static route ...")
until(lambda: route.ipv4_route_exist(R1, "0.0.0.0/0", proto="ietf-routing:static", pref=1), attempts=200)
with test.step("Verify connectivity from PC:data12 to R2:lo via static route"):
ns0.must_reach("192.168.200.1")
+3 -3
View File
@@ -8,7 +8,7 @@ actions (log vs block/stop).
"""
import infamy
import time
from infamy.util import until
TEST_MESSAGES = [
("daemon.emerg", "Emergency: system is unusable"),
@@ -73,12 +73,12 @@ with infamy.Test() as test:
}
})
time.sleep(2)
until(lambda: tgtssh.runsh("test -f /var/log/exact-errors").returncode == 0, attempts=10)
with test.step("Send test messages at all severity levels"):
for priority, message in TEST_MESSAGES:
tgtssh.runsh(f"logger -t advtest -p {priority} '{message}'")
time.sleep(2)
until(lambda: "Error: error condition" in tgtssh.runsh("cat /var/log/exact-errors 2>/dev/null").stdout, attempts=10)
with test.step("Verify exact-errors log contains only error messages"):
rc = tgtssh.runsh("cat /var/log/exact-errors 2>/dev/null")
+3 -3
View File
@@ -7,7 +7,7 @@ message properties with different operators, case-insensitivity, and negation.
"""
import infamy
import time
from infamy.util import until
TEST_MESSAGES = [
("myapp", "Application startup"),
@@ -90,12 +90,12 @@ with infamy.Test() as test:
}
})
time.sleep(2)
until(lambda: tgtssh.runsh("test -f /var/log/baseline").returncode == 0, attempts=10)
with test.step("Send test messages"):
for tag, msg in TEST_MESSAGES:
tgtssh.runsh(f"logger -t {tag} -p daemon.info '{msg}'")
time.sleep(1)
until(lambda: "Application startup" in tgtssh.runsh("cat /var/log/baseline 2>/dev/null").stdout, attempts=10)
with test.step("Verify myapp log contains only myapp messages"):
rc = tgtssh.runsh("grep -c 'myapp' /var/log/myapp 2>/dev/null")
+1 -1
View File
@@ -82,7 +82,7 @@ def main():
return
# Build qcow2 URL
filename = f"infix-x86_64-disk-{version}.qcow2"
filename = f"infix-x86_64-v{version}.qcow2"
url = f"{REPO}/releases/download/v{version}/{filename}"
print(f"Downloading {url} to compute MD5 and size...")