From 4ca49372ddc8b59929523660b10894802cee98ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Sun, 30 Aug 2026 06:17:26 +0200 Subject: [PATCH 1/2] Import routes installed by other software as HNAs Announce host routes some other software put in the kernel as olsr HNAs, so addresses olsr has no other way of learning about become reachable from the olsr side. The source is named by its route protocol: another routing daemon, a roaming or tunnel manager, anything installing routes under a protocol of its own. Without it the olsr side hears no route to those hosts, so the border NAT has nothing to send replies back to. ImportProto names the protocol, 0 (the default) disables it. An optional repeatable ImportPrefix bounds what may be announced; without one, every host route of that protocol is taken, transfer networks and management addresses included. Only host routes qualify, so a default route or an aggregate can never become an HNA. The startup dump reads until NLMSG_DONE, since a table dump spans as many datagrams as it needs. An address already in the list is skipped, or software that rewrites a route on every metric change would grow the list without bound. Route multicast is subscribed only when ImportProto is set. rtnetlink_read() sizes its buffer from a peek instead of a fixed 4096: route messages carry more attributes than link messages. test/route-import.sh drives the daemon in a throwaway netns - olsrd has no C test harness to hook into. Co-Authored-By: Claude Opus 5 (1M context) --- files/olsrd.conf.default.txt | 19 +++ src/cfgparser/cfgfile_gen.c | 21 +++ src/cfgparser/olsrd_conf.c | 8 + src/cfgparser/oparse.y | 64 +++++++ src/cfgparser/oscan.lex | 10 ++ src/linux/kernel_routes_nl.c | 35 +++- src/main.c | 16 +- src/olsr_cfg.h | 8 + src/route_import.c | 322 +++++++++++++++++++++++++++++++++++ src/route_import.h | 14 ++ test/route-import.sh | 115 +++++++++++++ 11 files changed, 627 insertions(+), 5 deletions(-) create mode 100644 src/route_import.c create mode 100644 src/route_import.h create mode 100755 test/route-import.sh diff --git a/files/olsrd.conf.default.txt b/files/olsrd.conf.default.txt index b1267d8aa..e7db11bbb 100644 --- a/files/olsrd.conf.default.txt +++ b/files/olsrd.conf.default.txt @@ -110,6 +110,25 @@ # RtProto 0 +# Announce host routes some other software put in the kernel as HNAs, so +# addresses olsr has no other way of learning about become reachable from +# the olsr side. The value is the route protocol identifying that software - +# another routing daemon, a roaming or tunnel manager, anything that +# installs routes under a protocol of its own. 0 (the default) disables it. +# Only host routes are taken, so a default route or an aggregate can never +# turn into an HNA. +# +# ImportPrefix, repeatable, bounds what may be announced. Without any, every +# host route of that protocol is imported - including addresses off transfer +# networks and management interfaces, so a whitelist is usually what you want. +# +# The example imports what protocol 42 installs within one address space: +# +# ImportProto 42 +# ImportPrefix 10.12.0.0/16 + +# ImportProto 0 + # Specifies the routing Table olsr uses # RtTable is for host routes, RtTableDefault for the route to the default # internet gateway (2 in case of IPv6+NIIT) and RtTableTunnel is for diff --git a/src/cfgparser/cfgfile_gen.c b/src/cfgparser/cfgfile_gen.c index 598486e0b..0722e22df 100644 --- a/src/cfgparser/cfgfile_gen.c +++ b/src/cfgparser/cfgfile_gen.c @@ -480,6 +480,27 @@ void olsrd_write_cnf_autobuf_uncached(struct autobuf *out, struct olsrd_config * cnf->rt_proto == expected ? "# " : "", cnf->rt_proto == expected ? DEF_RTPROTO : cnf->rt_proto); } + abuf_appendf(out, + "\n" + "# Route protocol whose host routes are announced as HNAs, so hosts\n" + "# only another routing daemon knows about become reachable over olsr.\n" + "# 0 disables the import. ImportPrefix, repeatable, bounds what may be\n" + "# announced; without any, every host route of that protocol is taken\n" + "\n"); + if (cnf->import_proto != DEF_IMPORTPROTO) { + struct ip_prefix_list *hna; + + abuf_appendf(out, "ImportProto %u\n", cnf->import_proto); + + for (hna = cnf->import_prefixes; hna != NULL; hna = hna->next) { + struct ipaddr_str addrbuf; + abuf_appendf(out, "ImportPrefix %s/%d\n", + olsr_ip_to_string(&addrbuf, &hna->net.prefix), hna->net.prefix_len); + } + } else { + abuf_appendf(out, "# ImportProto %u\n", DEF_IMPORTPROTO); + } + abuf_appendf(out, "\n" "# Specifies the routing Table olsr uses\n" diff --git a/src/cfgparser/olsrd_conf.c b/src/cfgparser/olsrd_conf.c index 84d488115..1ab174934 100644 --- a/src/cfgparser/olsrd_conf.c +++ b/src/cfgparser/olsrd_conf.c @@ -1049,6 +1049,10 @@ olsrd_free_cnf(struct olsrd_config **cnfVariableAddress) ip_prefix_list_clear(&cnf->hna_entries); + ip_prefix_list_clear(&cnf->import_prefixes); + + ip_prefix_list_clear(&cnf->import_prefixes); + while (cnf->plugins) { struct plugin_entry *plugin = cnf->plugins; cnf->plugins = cnf->plugins->next; @@ -1095,6 +1099,10 @@ set_default_cnf(struct olsrd_config *cnf, char * configuration_file) cnf->allow_no_interfaces = DEF_ALLOW_NO_INTS; cnf->tos = DEF_TOS; cnf->rt_proto = DEF_RTPROTO; + cnf->import_proto = DEF_IMPORTPROTO; + cnf->import_prefixes = NULL; + cnf->import_proto = DEF_IMPORTPROTO; + cnf->import_prefixes = NULL; cnf->rt_table = DEF_RT_AUTO; cnf->rt_table_default = DEF_RT_AUTO; cnf->rt_table_tunnel = DEF_RT_AUTO; diff --git a/src/cfgparser/oparse.y b/src/cfgparser/oparse.y index c311824a6..804380819 100644 --- a/src/cfgparser/oparse.y +++ b/src/cfgparser/oparse.y @@ -192,6 +192,8 @@ static int add_ipv6_addr(YYSTYPE ipaddr_arg, YYSTYPE prefixlen_arg) %token TOK_TOS %token TOK_OLSRPORT %token TOK_RTPROTO +%token TOK_IMPORTPROTO +%token TOK_IMPORTPREFIX %token TOK_RTTABLE %token TOK_RTTABLE_DEFAULT %token TOK_RTTABLE_TUNNEL @@ -296,6 +298,8 @@ stmt: idebug | atos | aolsrport | irtproto + | iimportproto + | iimportprefix | irttable | irttable_default | irttable_tunnel @@ -1062,6 +1066,66 @@ aolsrport: TOK_OLSRPORT TOK_INTEGER } ; +iimportproto: TOK_IMPORTPROTO TOK_INTEGER +{ + PARSER_DEBUG_PRINTF("ImportProto: %d\n", $2->integer); + olsr_cnf->import_proto = $2->integer; + free($2); +} +; + +iimportprefix: TOK_IMPORTPREFIX TOK_IPV4_ADDR TOK_SLASH TOK_INTEGER +{ + union olsr_ip_addr ipaddr; + + if (olsr_cnf->ip_version == AF_INET6) { + fprintf(stderr, "IPv4 addresses can only be used if \"IpVersion\" == 4, skipping ImportPrefix.\n"); + olsr_startup_sleep(3); + } else { + PARSER_DEBUG_PRINTF("ImportPrefix: %s/%d\n", $2->string, $4->integer); + + if (inet_pton(AF_INET, $2->string, &ipaddr.v4) <= 0) { + fprintf(stderr, "iimportprefix: Failed converting IP address %s\n", $2->string); + YYABORT; + } + if ($4->integer > olsr_cnf->maxplen) { + fprintf(stderr, "iimportprefix: Prefix len %u > %d is not allowed!\n", $4->integer, olsr_cnf->maxplen); + YYABORT; + } + + ip_prefix_list_add(&olsr_cnf->import_prefixes, &ipaddr, $4->integer); + } + free($2->string); + free($2); + free($4); +} + | TOK_IMPORTPREFIX TOK_IPV6_ADDR TOK_SLASH TOK_INTEGER +{ + union olsr_ip_addr ipaddr; + + if (olsr_cnf->ip_version == AF_INET) { + fprintf(stderr, "IPv6 addresses can only be used if \"IpVersion\" == 6, skipping ImportPrefix.\n"); + olsr_startup_sleep(3); + } else { + PARSER_DEBUG_PRINTF("ImportPrefix: %s/%d\n", $2->string, $4->integer); + + if (inet_pton(AF_INET6, $2->string, &ipaddr.v6) <= 0) { + fprintf(stderr, "iimportprefix: Failed converting IP address %s\n", $2->string); + YYABORT; + } + if ($4->integer > olsr_cnf->maxplen) { + fprintf(stderr, "iimportprefix: Prefix len %u > %d is not allowed!\n", $4->integer, olsr_cnf->maxplen); + YYABORT; + } + + ip_prefix_list_add(&olsr_cnf->import_prefixes, &ipaddr, $4->integer); + } + free($2->string); + free($2); + free($4); +} +; + irtproto: TOK_RTPROTO TOK_INTEGER { PARSER_DEBUG_PRINTF("RtProto: %d\n", $2->integer); diff --git a/src/cfgparser/oscan.lex b/src/cfgparser/oscan.lex index 3f77e8e53..e2899efaf 100644 --- a/src/cfgparser/oscan.lex +++ b/src/cfgparser/oscan.lex @@ -354,6 +354,16 @@ IPV6ADDR {IPV6PAT1}|{IPV6PAT2}|{IPV6PAT3}|{IPV6PAT4}|{IPV6PAT5}|{IPV6PAT6}|{IPV6 return TOK_OLSRPORT; } +"ImportProto" { + yylval = NULL; + return TOK_IMPORTPROTO; +} + +"ImportPrefix" { + yylval = NULL; + return TOK_IMPORTPREFIX; +} + "RtProto" { olsrd_config_checksum_add(yytext, yyleng); yylval = NULL; diff --git a/src/linux/kernel_routes_nl.c b/src/linux/kernel_routes_nl.c index f21d6bbe1..abec76ab7 100644 --- a/src/linux/kernel_routes_nl.c +++ b/src/linux/kernel_routes_nl.c @@ -50,6 +50,8 @@ #include "log.h" #include "net_os.h" #include "ifnet.h" +#include "olsr.h" +#include "route_import.h" #include #include @@ -166,14 +168,32 @@ static void rtnetlink_read(int sock, void *data __attribute__ ((unused)), unsign .msg_flags = 0, }; - char buffer[4096]; + /* route messages carry more attributes than a link message and can + * outgrow a fixed buffer; size each one from a peek first */ + int bufsize = 4096; + char *buffer = olsr_malloc(bufsize, "netlink receive buffer"); struct nlmsghdr *nlh = (struct nlmsghdr *)ARM_NOWARN_ALIGN(buffer); int ret; iov.iov_base = (void *) buffer; - iov.iov_len = sizeof(buffer); + iov.iov_len = bufsize; + + while ((ret = recvmsg(sock, &msg, MSG_DONTWAIT | MSG_TRUNC | MSG_PEEK)) >= 0) { + if (ret > bufsize) { + free(buffer); + buffer = olsr_malloc(ret, "netlink receive buffer expansion"); + bufsize = ret; + + iov.iov_base = (void *) buffer; + iov.iov_len = bufsize; + nlh = (struct nlmsghdr *)ARM_NOWARN_ALIGN(buffer); + } + + ret = recvmsg(sock, &msg, MSG_DONTWAIT); + if (ret < 0) { + goto end; + } - while ((ret = recvmsg(sock, &msg, MSG_DONTWAIT)) >= 0) { /*check message*/ len = nlh->nlmsg_len; plen = len - sizeof(nlh); @@ -181,7 +201,7 @@ static void rtnetlink_read(int sock, void *data __attribute__ ((unused)), unsign OLSR_PRINTF(1,"Malformed netlink message: " "len=%d left=%d plen=%d\n", len, ret, plen); - return; + goto end; } OLSR_PRINTF(3, "Netlink message received: type 0x%x\n", nlh->nlmsg_type); @@ -189,11 +209,18 @@ static void rtnetlink_read(int sock, void *data __attribute__ ((unused)), unsign /* handle ifup/ifdown */ netlink_process_link(nlh); } + + if ((nlh->nlmsg_type == RTM_NEWROUTE) || (nlh->nlmsg_type == RTM_DELROUTE)) { + /* another daemon's route came or went - see ImportProto */ + process_import_nlh(nlh, nlh->nlmsg_type == RTM_DELROUTE); + } } if (errno != EAGAIN) { OLSR_PRINTF(1,"netlink listen error %u - %s\n",errno,strerror(errno)); } +end: + free(buffer); } static void diff --git a/src/main.c b/src/main.c index 8141f8de6..2b0ccb887 100644 --- a/src/main.c +++ b/src/main.c @@ -86,6 +86,7 @@ #include #include #include "kernel_routes.h" +#include "route_import.h" #endif /* __linux__ */ @@ -560,6 +561,9 @@ int main(int argc, char *argv[]) { /* create a socket for netlink calls */ #ifdef __linux__ + { + int rtnetlink_groups = RTMGRP_LINK; + olsr_cnf->rtnl_s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); if (olsr_cnf->rtnl_s < 0) { char buf2[1024]; @@ -571,11 +575,18 @@ int main(int argc, char *argv[]) { olsr_syslog(OLSR_LOG_INFO, "rtnetlink could not be set to nonblocking"); } - if ((olsr_cnf->rt_monitor_socket = rtnetlink_register_socket(RTMGRP_LINK)) < 0) { + /* only ask the kernel for route changes when something wants them: a + * busy table would otherwise wake olsrd on every neighbour's update */ + if (olsr_cnf->import_proto != 0) { + rtnetlink_groups |= (olsr_cnf->ip_version == AF_INET) ? RTMGRP_IPV4_ROUTE : RTMGRP_IPV6_ROUTE; + } + + if ((olsr_cnf->rt_monitor_socket = rtnetlink_register_socket(rtnetlink_groups)) < 0) { char buf2[1024]; snprintf(buf2, sizeof(buf2), "rtmonitor socket: %s", strerror(errno)); olsr_exit(buf2, EXIT_FAILURE); } + } #endif /* __linux__ */ /* create routing socket */ @@ -773,6 +784,9 @@ int main(int argc, char *argv[]) { olsr_shutdown_registered = true; + /* seed the HNA list from what is already in the routing table */ + route_import_init(); + /* Starting scheduler */ olsr_scheduler(); diff --git a/src/olsr_cfg.h b/src/olsr_cfg.h index de4906271..2be30ca8b 100644 --- a/src/olsr_cfg.h +++ b/src/olsr_cfg.h @@ -81,6 +81,8 @@ #define DEF_CLEAR_SCREEN true #define DEF_OLSRPORT 698 #define DEF_RTPROTO 0 /* 0 means OS-specific default */ +#define DEF_IMPORTPROTO 0 /* 0 disables the route import */ +#define DEF_IMPORTPROTO 0 /* 0 disables the route import */ #define DEF_RT_NONE -1 #define DEF_RT_AUTO 0 @@ -303,6 +305,10 @@ struct olsrd_config { struct hyst_param hysteresis_param; struct plugin_entry *plugins; struct ip_prefix_list *hna_entries; + /* route protocol whose host routes become HNAs, 0 to disable */ + uint8_t import_proto; + /* optional whitelist bounding what import_proto may announce */ + struct ip_prefix_list *import_prefixes; struct ip_prefix_list *ipc_nets; struct if_config_options *interface_defaults; struct olsr_if *interfaces; @@ -417,6 +423,8 @@ extern "C" { int ip_prefix_list_remove(struct ip_prefix_list **, const union olsr_ip_addr *, uint8_t); + void ip_prefix_list_clear(struct ip_prefix_list **list); + struct ip_prefix_list *ip_prefix_list_find(struct ip_prefix_list *, const union olsr_ip_addr *net, uint8_t prefix_len); /* diff --git a/src/route_import.c b/src/route_import.c new file mode 100644 index 000000000..fd61a934d --- /dev/null +++ b/src/route_import.c @@ -0,0 +1,322 @@ +/* + * Import host routes some other software put in the kernel and announce + * them as OLSR HNAs, so addresses OLSR has no other way of learning about + * become reachable from the OLSR side. + * + * The source is identified by its route protocol, ImportProto - another + * routing daemon, a roaming or tunnel manager, or anything else that + * installs routes under a protocol of its own. An optional ImportPrefix + * whitelist bounds what may be announced. + * + * Only host routes are imported: this announces addresses, not networks, + * so a default route or an aggregate can never turn into an HNA. + * + * netlink walking adapted from + * https://olegkutkov.me/2019/03/24/getting-linux-routing-table-using-netlink/ + */ + +#include "olsr.h" +#include "olsr_types.h" +#include "ipcalc.h" +#include "defs.h" +#include "route_import.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* an imported route stands for one address, never a network */ +static uint8_t +host_prefix_len(void) +{ + return olsr_cnf->ip_version == AF_INET ? 32 : 128; +} + +static void +parse_rtattr(struct rtattr *tb[], int max, struct rtattr *rta, int len) +{ + memset(tb, 0, sizeof(struct rtattr *) * (max + 1)); + + while (RTA_OK(rta, len)) { + if (rta->rta_type <= max) { + tb[rta->rta_type] = rta; + } + + rta = RTA_NEXT(rta, len); + } +} + +static uint32_t +rtm_get_table(const struct rtmsg *r, struct rtattr **tb) +{ + if (tb[RTA_TABLE]) { + return *(uint32_t *) RTA_DATA(tb[RTA_TABLE]); + } + + return r->rtm_table; +} + +/* an empty whitelist means every address of the right protocol qualifies */ +static bool +prefix_wanted(const union olsr_ip_addr *prefix) +{ + const struct ip_prefix_list *entry; + + if (olsr_cnf->import_prefixes == NULL) { + return true; + } + + for (entry = olsr_cnf->import_prefixes; entry != NULL; entry = entry->next) { + if (ip_in_net(prefix, &entry->net)) { + return true; + } + } + + return false; +} + +/* 0: import it, >0: not ours, <0: malformed */ +static int +convert_route(const struct nlmsghdr *nlh, union olsr_ip_addr *prefix, uint8_t *prefix_len) +{ + struct rtmsg *r = NLMSG_DATA(nlh); + struct rtattr *tb[RTA_MAX + 1]; + int len = nlh->nlmsg_len - NLMSG_LENGTH(sizeof(*r)); + + if (len < 0) { + OLSR_PRINTF(1, "route import: short netlink message\n"); + return -1; + } + + parse_rtattr(tb, RTA_MAX, RTM_RTA(r), len); + + if (r->rtm_family != olsr_cnf->ip_version + || r->rtm_protocol != olsr_cnf->import_proto + || r->rtm_dst_len != host_prefix_len() + || rtm_get_table(r, tb) != RT_TABLE_MAIN + || !tb[RTA_DST] + || RTA_PAYLOAD(tb[RTA_DST]) < olsr_cnf->ipsize) { + return 1; + } + + memset(prefix, 0, sizeof(*prefix)); + memcpy(prefix, RTA_DATA(tb[RTA_DST]), olsr_cnf->ipsize); + *prefix_len = r->rtm_dst_len; + + if (!prefix_wanted(prefix)) { + return 1; + } + + return 0; +} + +/* + * The other daemon rewrites a route on every metric or nexthop change, so + * the same address keeps arriving; without this the HNA list would grow + * without bound. + */ +static void +import_hna_add(struct ip_prefix_list **list, const union olsr_ip_addr *net, uint8_t prefix_len) +{ + struct ipaddr_str buf; + + if (ip_prefix_list_find(*list, net, prefix_len) != NULL) { + return; + } + + OLSR_PRINTF(1, "route import: announcing %s/%u\n", olsr_ip_to_string(&buf, net), prefix_len); + ip_prefix_list_add(list, net, prefix_len); +} + +static void +import_hna_remove(struct ip_prefix_list **list, const union olsr_ip_addr *net, uint8_t prefix_len) +{ + struct ipaddr_str buf; + + if (!ip_prefix_list_remove(list, net, prefix_len)) { + /* a delete for something we never took, e.g. filtered by ImportPrefix */ + return; + } + + OLSR_PRINTF(1, "route import: withdrawing %s/%u\n", olsr_ip_to_string(&buf, net), prefix_len); +} + +void +process_import_nlh(const struct nlmsghdr *nlh, bool is_delete) +{ + union olsr_ip_addr prefix; + uint8_t prefix_len; + + if (olsr_cnf->import_proto == 0) { + return; + } + + if (convert_route(nlh, &prefix, &prefix_len)) { + return; + } + + if (is_delete) { + import_hna_remove(&olsr_cnf->hna_entries, &prefix, prefix_len); + } else { + import_hna_add(&olsr_cnf->hna_entries, &prefix, prefix_len); + } +} + +static int +dump_request(int sock) +{ + struct { + struct nlmsghdr nlh; + struct rtmsg rtm; + } req; + + memset(&req, 0, sizeof(req)); + + req.nlh.nlmsg_type = RTM_GETROUTE; + req.nlh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + req.nlh.nlmsg_len = sizeof(req); + req.nlh.nlmsg_seq = time(NULL); + req.rtm.rtm_family = olsr_cnf->ip_version; + + return send(sock, &req, sizeof(req), 0); +} + +static int +recv_reply(int sock, char **answer) +{ + struct sockaddr_nl nladdr; + struct iovec iov = { .iov_base = NULL, .iov_len = 0 }; + struct msghdr msg = { + .msg_name = &nladdr, + .msg_namelen = sizeof(nladdr), + .msg_iov = &iov, + .msg_iovlen = 1, + }; + char *buf; + int len; + + do { + len = recvmsg(sock, &msg, MSG_PEEK | MSG_TRUNC); + } while (len < 0 && errno == EINTR); + + if (len <= 0) { + return len < 0 ? -errno : -ENODATA; + } + + buf = olsr_malloc(len, "route import netlink buffer"); + iov.iov_base = buf; + iov.iov_len = len; + + do { + len = recvmsg(sock, &msg, 0); + } while (len < 0 && errno == EINTR); + + if (len <= 0) { + free(buf); + return len < 0 ? -errno : -ENODATA; + } + + *answer = buf; + + return len; +} + +/* + * A full table dump spans as many datagrams as it needs, so read until + * NLMSG_DONE - stopping at the first one silently imports a fraction of + * the table on a router with many routes. + */ +static int +dump_response(int sock, struct ip_prefix_list **out) +{ + bool done = false; + + while (!done) { + char *buf = NULL; + struct nlmsghdr *nlh; + int msglen = recv_reply(sock, &buf); + + if (msglen < 0) { + return msglen; + } + + for (nlh = (struct nlmsghdr *) buf; NLMSG_OK(nlh, (unsigned int) msglen); nlh = NLMSG_NEXT(nlh, msglen)) { + union olsr_ip_addr prefix; + uint8_t prefix_len; + int status; + + if (nlh->nlmsg_flags & NLM_F_DUMP_INTR) { + free(buf); + return -EAGAIN; + } + + if (nlh->nlmsg_type == NLMSG_DONE) { + done = true; + break; + } + + if (nlh->nlmsg_type == NLMSG_ERROR) { + free(buf); + return -EIO; + } + + status = convert_route(nlh, &prefix, &prefix_len); + + if (status < 0) { + free(buf); + return -EINVAL; + } + + if (status == 0) { + import_hna_add(out, &prefix, prefix_len); + } + } + + free(buf); + } + + return 0; +} + +/* + * Seeds the HNA list with what is already in the table. Uses its own + * blocking socket rather than olsr_cnf->rtnl_s, which is non-blocking and + * carries olsrd's own route operations. + */ +void +route_import_init(void) +{ + int sock; + + if (olsr_cnf->import_proto == 0) { + return; + } + + sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); + + if (sock < 0) { + OLSR_PRINTF(1, "route import: netlink socket: %s\n", strerror(errno)); + return; + } + + if (dump_request(sock) < 0) { + OLSR_PRINTF(1, "route import: route dump request: %s\n", strerror(errno)); + } else { + int status = dump_response(sock, &olsr_cnf->hna_entries); + + if (status < 0) { + /* not fatal: the route monitor still picks up everything that + * changes from here on */ + OLSR_PRINTF(1, "route import: route dump failed: %s\n", strerror(-status)); + } + } + + close(sock); +} diff --git a/src/route_import.h b/src/route_import.h new file mode 100644 index 000000000..ad30888f0 --- /dev/null +++ b/src/route_import.h @@ -0,0 +1,14 @@ +/* + * Import kernel routes installed by another routing daemon as OLSR HNAs. + */ + +#ifndef OLSRD_ROUTE_IMPORT_H +#define OLSRD_ROUTE_IMPORT_H + +#include +#include + +void process_import_nlh(const struct nlmsghdr *nlh, bool is_delete); +void route_import_init(void); + +#endif /* OLSRD_ROUTE_IMPORT_H */ diff --git a/test/route-import.sh b/test/route-import.sh new file mode 100755 index 000000000..2424353b9 --- /dev/null +++ b/test/route-import.sh @@ -0,0 +1,115 @@ +#!/bin/sh +# +# Tests the ImportProto/ImportPrefix route import. +# +# olsrd has no C test harness, so this drives the real daemon: it builds a +# throwaway network namespace, puts routes in it that the import must take +# and routes it must ignore, and checks what olsrd logs at debug level 1. +# +# make && sudo test/route-import.sh +# +# It needs iproute2 and either root or unprivileged user namespaces +# (it re-execs itself under "unshare -rn"). Nothing outside the namespace +# is touched. + +set -eu + +OLSRD=${OLSRD:-./olsrd} +PROTO=42 # any route protocol would do + +if [ "${ROUTE_IMPORT_INNER:-}" != 1 ]; then + [ -x "$OLSRD" ] || { echo "SKIP: no olsrd binary at $OLSRD, run make first" >&2; exit 77; } + OLSRD=$(readlink -f "$OLSRD") + export OLSRD ROUTE_IMPORT_INNER=1 + if [ "$(id -u)" = 0 ]; then + exec unshare -n -- "$0" "$@" + fi + exec unshare -rn -- "$0" "$@" +fi + +workdir=$(mktemp -d) +trap 'rm -rf "$workdir"' EXIT + +cat > "$workdir/olsrd.conf" < "$workdir/log" 2>&1 & +olsrd_pid=$! +trap 'kill $olsrd_pid 2>/dev/null || true; rm -rf "$workdir"' EXIT + +# wait for the startup dump rather than sleeping a guessed amount +i=0 +while ! grep -q "route import: announcing 10.12.245.93/32" "$workdir/log" 2>/dev/null; do + i=$((i + 1)) + [ $i -lt 100 ] || { echo "FAIL: olsrd never imported the seeded route"; cat "$workdir/log"; exit 1; } + sleep 0.1 +done + +# and these exercise the rtnetlink monitor +ip route add 10.12.245.94/32 dev dummy0 proto $PROTO +ip route add 10.99.0.1/32 dev dummy0 proto $PROTO # outside ImportPrefix +ip route replace 10.12.245.94/32 dev dummy0 proto $PROTO metric 5 # must not duplicate +ip route del 10.12.245.93/32 dev dummy0 proto $PROTO + +i=0 +while ! grep -q "route import: withdrawing 10.12.245.93/32" "$workdir/log" 2>/dev/null; do + i=$((i + 1)) + [ $i -lt 100 ] || { echo "FAIL: olsrd never withdrew the deleted route"; cat "$workdir/log"; exit 1; } + sleep 0.1 +done + +kill $olsrd_pid 2>/dev/null || true +wait $olsrd_pid 2>/dev/null || true + +failed=0 + +check() { + description=$1 expected=$2 pattern=$3 + actual=$(grep -c "$pattern" "$workdir/log" || true) + if [ "$actual" = "$expected" ]; then + echo "ok - $description" + else + echo "NOT OK - $description (expected $expected, got $actual)" + failed=1 + fi +} + +check "seeded host route is announced" 1 "announcing 10.12.245.93/32" +check "second ImportPrefix is honoured" 1 "announcing 193.33.150.9/32" +check "route added at runtime is announced" 1 "announcing 10.12.245.94/32" +check "a rewrite does not announce it twice" 1 "announcing 10.12.245.94/32" +check "deleted route is withdrawn" 1 "withdrawing 10.12.245.93/32" +check "address outside ImportPrefix ignored" 0 "10.247.113.235" +check "runtime address outside it ignored" 0 "10.99.0.1" +check "non-host route ignored" 0 "10.12.9.0" +check "default route ignored" 0 "announcing 0.0.0.0/0" +check "other protocol ignored" 0 "10.12.245.80" + +if [ $failed -ne 0 ]; then + echo + echo "--- olsrd log ---" + cat "$workdir/log" + exit 1 +fi + +echo "all route import checks passed" From 9c0c1760f70ccad5ade1c3a25d406232c3f6a297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Sun, 30 Aug 2026 19:17:50 +0200 Subject: [PATCH 2/2] Drop duplicated ImportProto defaults, test route import over IPv6 The DEF_IMPORTPROTO define and its two config-default assignments each landed twice. Harmless, but only by luck. test/route-import.sh now runs its whole case set for both families, so the IPv6 filter and RTMGRP_IPV6_ROUTE subscription are actually covered. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PC13y4JFh4V3iopRytjjJd --- src/cfgparser/olsrd_conf.c | 2 - src/olsr_cfg.h | 1 - test/route-import.sh | 176 ++++++++++++++++++++++--------------- 3 files changed, 106 insertions(+), 73 deletions(-) diff --git a/src/cfgparser/olsrd_conf.c b/src/cfgparser/olsrd_conf.c index 1ab174934..999f61b5a 100644 --- a/src/cfgparser/olsrd_conf.c +++ b/src/cfgparser/olsrd_conf.c @@ -1101,8 +1101,6 @@ set_default_cnf(struct olsrd_config *cnf, char * configuration_file) cnf->rt_proto = DEF_RTPROTO; cnf->import_proto = DEF_IMPORTPROTO; cnf->import_prefixes = NULL; - cnf->import_proto = DEF_IMPORTPROTO; - cnf->import_prefixes = NULL; cnf->rt_table = DEF_RT_AUTO; cnf->rt_table_default = DEF_RT_AUTO; cnf->rt_table_tunnel = DEF_RT_AUTO; diff --git a/src/olsr_cfg.h b/src/olsr_cfg.h index 2be30ca8b..4a0970870 100644 --- a/src/olsr_cfg.h +++ b/src/olsr_cfg.h @@ -82,7 +82,6 @@ #define DEF_OLSRPORT 698 #define DEF_RTPROTO 0 /* 0 means OS-specific default */ #define DEF_IMPORTPROTO 0 /* 0 disables the route import */ -#define DEF_IMPORTPROTO 0 /* 0 disables the route import */ #define DEF_RT_NONE -1 #define DEF_RT_AUTO 0 diff --git a/test/route-import.sh b/test/route-import.sh index 2424353b9..be302fda2 100755 --- a/test/route-import.sh +++ b/test/route-import.sh @@ -1,6 +1,6 @@ #!/bin/sh # -# Tests the ImportProto/ImportPrefix route import. +# Tests the ImportProto/ImportPrefix route import, for IPv4 and IPv6. # # olsrd has no C test harness, so this drives the real daemon: it builds a # throwaway network namespace, puts routes in it that the import must take @@ -30,86 +30,122 @@ fi workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT -cat > "$workdir/olsrd.conf" < "$workdir/log" 2>&1 & -olsrd_pid=$! -trap 'kill $olsrd_pid 2>/dev/null || true; rm -rf "$workdir"' EXIT - -# wait for the startup dump rather than sleeping a guessed amount -i=0 -while ! grep -q "route import: announcing 10.12.245.93/32" "$workdir/log" 2>/dev/null; do - i=$((i + 1)) - [ $i -lt 100 ] || { echo "FAIL: olsrd never imported the seeded route"; cat "$workdir/log"; exit 1; } - sleep 0.1 -done - -# and these exercise the rtnetlink monitor -ip route add 10.12.245.94/32 dev dummy0 proto $PROTO -ip route add 10.99.0.1/32 dev dummy0 proto $PROTO # outside ImportPrefix -ip route replace 10.12.245.94/32 dev dummy0 proto $PROTO metric 5 # must not duplicate -ip route del 10.12.245.93/32 dev dummy0 proto $PROTO - -i=0 -while ! grep -q "route import: withdrawing 10.12.245.93/32" "$workdir/log" 2>/dev/null; do - i=$((i + 1)) - [ $i -lt 100 ] || { echo "FAIL: olsrd never withdrew the deleted route"; cat "$workdir/log"; exit 1; } - sleep 0.1 -done - -kill $olsrd_pid 2>/dev/null || true -wait $olsrd_pid 2>/dev/null || true failed=0 check() { description=$1 expected=$2 pattern=$3 - actual=$(grep -c "$pattern" "$workdir/log" || true) + actual=$(grep -c "$pattern" "$log" || true) if [ "$actual" = "$expected" ]; then - echo "ok - $description" + echo "ok - IPv$fam: $description" else - echo "NOT OK - $description (expected $expected, got $actual)" + echo "NOT OK - IPv$fam: $description (expected $expected, got $actual)" failed=1 fi } -check "seeded host route is announced" 1 "announcing 10.12.245.93/32" -check "second ImportPrefix is honoured" 1 "announcing 193.33.150.9/32" -check "route added at runtime is announced" 1 "announcing 10.12.245.94/32" -check "a rewrite does not announce it twice" 1 "announcing 10.12.245.94/32" -check "deleted route is withdrawn" 1 "withdrawing 10.12.245.93/32" -check "address outside ImportPrefix ignored" 0 "10.247.113.235" -check "runtime address outside it ignored" 0 "10.99.0.1" -check "non-host route ignored" 0 "10.12.9.0" -check "default route ignored" 0 "announcing 0.0.0.0/0" -check "other protocol ignored" 0 "10.12.245.80" - -if [ $failed -ne 0 ]; then - echo - echo "--- olsrd log ---" - cat "$workdir/log" - exit 1 -fi +# waits for a log line rather than sleeping a guessed amount +wait_for() { + i=0 + while ! grep -q "$1" "$log" 2>/dev/null; do + i=$((i + 1)) + [ $i -lt 100 ] || { echo "FAIL: IPv$fam: $2"; cat "$log"; exit 1; } + sleep 0.1 + done +} + +run_family() { + fam=$1 + log=$workdir/log$fam + + case $fam in + 4) + ifaddr=10.12.6.66/16 + prefix1=10.12.0.0/16 prefix2=193.33.150.0/23 + take1=10.12.245.93/32 take2=193.33.150.9/32 + outside=10.247.113.235/32 + net=10.12.9.0/24 defaultroute=default + otherproto=10.12.245.80/32 + rt_take=10.12.245.94/32 rt_outside=10.99.0.1/32 + ;; + 6) + ifaddr=fd00:12::66/64 + prefix1=fd00:12::/32 prefix2=2001:db8::/32 + take1=fd00:12::5d/128 take2=2001:db8:150::9/128 + outside=fd00:ff::eb/128 + net=fd00:12:9::/64 defaultroute=default + otherproto=fd00:12::50/128 + rt_take=fd00:12::5e/128 rt_outside=fd00:99::1/128 + ;; + esac + + cat > "$workdir/olsrd.conf" < "$log" 2>&1 & + olsrd_pid=$! + trap 'kill $olsrd_pid 2>/dev/null || true; rm -rf "$workdir"' EXIT + + wait_for "route import: announcing $take1" "olsrd never imported the seeded route" + + # and these exercise the rtnetlink monitor + ip -"$fam" route add "$rt_take" dev dummy0 proto $PROTO + ip -"$fam" route add "$rt_outside" dev dummy0 proto $PROTO # outside ImportPrefix + ip -"$fam" route replace "$rt_take" dev dummy0 proto $PROTO metric 5 # must not duplicate + ip -"$fam" route del "$take1" dev dummy0 proto $PROTO + + wait_for "route import: withdrawing $take1" "olsrd never withdrew the deleted route" + + kill $olsrd_pid 2>/dev/null || true + wait $olsrd_pid 2>/dev/null || true + trap 'rm -rf "$workdir"' EXIT + + check "seeded host route is announced" 1 "announcing $take1" + check "second ImportPrefix is honoured" 1 "announcing $take2" + check "route added at runtime is announced" 1 "announcing $rt_take" + check "a rewrite does not announce it twice" 1 "announcing $rt_take" + check "deleted route is withdrawn" 1 "withdrawing $take1" + check "address outside ImportPrefix ignored" 0 "${outside%/*}" + check "runtime address outside it ignored" 0 "${rt_outside%/*}" + check "non-host route ignored" 0 "${net%/*}" + check "default route ignored" 0 "announcing $([ "$fam" = 4 ] && echo 0.0.0.0/0 || echo ::/0)" + check "other protocol ignored" 0 "${otherproto%/*}" + + if [ $failed -ne 0 ]; then + echo + echo "--- olsrd log (IPv$fam) ---" + cat "$log" + exit 1 + fi + + ip link del dummy0 +} + +run_family 4 +run_family 6 echo "all route import checks passed"