mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-23 01:23:00 +02:00
Merge pull request #1384 from kernelkit/new-frr
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
../available/netd.conf
|
||||
@@ -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"
|
||||
|
||||
@@ -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
-1
Submodule buildroot updated: 6eca8c5921...226eec3c54
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
#set DEBUG=1
|
||||
service <pid/confd> name:netd log \
|
||||
[2345] netd -p /run/netd.pid -- Network route daemon
|
||||
@@ -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))
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 = ≈
|
||||
+
|
||||
+ 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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
autoreconf -W portability -vifm
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'"]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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_ */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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...")
|
||||
|
||||
Reference in New Issue
Block a user