[PATCH v2 2/4] RAdv: Add neighbor discovery based on incoming RAs to RAdv proto
Matteo Perin
matteo.perin at canonical.com
Thu Dec 11 10:16:37 CET 2025
Implementation of peer discovery based on incoming Router Advertisments
to the RAdv protocol.
Up until this point no much use has been make of incoming RAs, this commit
tries to amend that by importing peer-based routes to a new peers channel.
This will allow to use the information discovered about remote routers
on other protocols which can export peers on this same channel.
RA staleness has also been taken into consideration and the routes are
withdrawn whenever the advertised router lifetime expires.
The feature is meant to be enabled via a new configuration option in
the RAdv protocol called router discovery [yes/no].
Signed-off-by: Matteo Perin <matteo.perin at canonical.com>
---
doc/bird.sgml | 11 +++
proto/radv/config.Y | 3 +-
proto/radv/packets.c | 41 ++++++++++-
proto/radv/radv.c | 165 ++++++++++++++++++++++++++++++++++++++++++-
proto/radv/radv.h | 4 ++
5 files changed, 219 insertions(+), 5 deletions(-)
diff --git a/doc/bird.sgml b/doc/bird.sgml
index b0ac094a..b0256b73 100644
--- a/doc/bird.sgml
+++ b/doc/bird.sgml
@@ -5841,6 +5841,17 @@ custom option type 38 value hex:0e:10:20:01:0d:b8:00:0a:00:0b:00:00:00:00;
<tag><label id="radv-iface-custom-local">custom option local <m/switch/</tag>
Use only local custom option definitions for this interface. See <cf/rdnss local/
option above. Default: no.
+
+ <tag><label id="radv-router-discovery">router discovery <m/switch/</tag>
+ Enable reception and processing of incoming ICMPv6 Router Advertisement
+ messages. When enabled, the RAdv protocol listens for RA messages on
+ configured interfaces and creates route-like objects in a peers routing
+ table based on detected advertisements. These objects contain information
+ about discovered routers and their lifetimes are managed by the Router
+ Lifetime field in the RA messages. When an RA expires, the corresponding
+ object is automatically removed from the peers table. Currently, this feature
+ is primarily used in conjunction with BGP unnumbered automatic peering to
+ automatically discover BGP neighbors. Default: no.
</descrip>
<p>Prefix specific options
diff --git a/proto/radv/config.Y b/proto/radv/config.Y
index 372081d2..758370af 100644
--- a/proto/radv/config.Y
+++ b/proto/radv/config.Y
@@ -42,7 +42,7 @@ CF_KEYWORDS(RADV, PREFIX, INTERFACE, MIN, MAX, RA, DELAY, INTERVAL, SOLICITED,
RETRANS, TIMER, CURRENT, HOP, LIMIT, DEFAULT, VALID, PREFERRED, MULT,
LIFETIME, SKIP, ONLINK, AUTONOMOUS, RDNSS, DNSSL, NS, DOMAIN, LOCAL,
TRIGGER, SENSITIVE, PREFERENCE, LOW, MEDIUM, HIGH, PROPAGATE, ROUTE,
- ROUTES, CUSTOM, OPTION, TYPE, VALUE, PD)
+ ROUTES, CUSTOM, OPTION, TYPE, VALUE, PD, ROUTER, DISCOVERY)
CF_ENUM(T_ENUM_RA_PREFERENCE, RA_PREF_, LOW, MEDIUM, HIGH)
@@ -140,6 +140,7 @@ radv_iface_item:
| RDNSS LOCAL bool { RADV_IFACE->rdnss_local = $3; }
| DNSSL LOCAL bool { RADV_IFACE->dnssl_local = $3; }
| CUSTOM OPTION LOCAL bool { RADV_IFACE->custom_local = $4; }
+ | ROUTER DISCOVERY bool { RADV_IFACE->router_discovery = $3; }
;
radv_preference:
diff --git a/proto/radv/packets.c b/proto/radv/packets.c
index 40f3c41e..96da232b 100644
--- a/proto/radv/packets.c
+++ b/proto/radv/packets.c
@@ -460,6 +460,44 @@ radv_receive_rs(struct radv_proto *p, struct radv_iface *ifa, ip_addr from)
radv_iface_notify(ifa, RA_EV_RS);
}
+void
+radv_process_ra(struct radv_iface *ifa, ip_addr from, struct radv_ra_packet *pkt, int length)
+{
+ struct radv_proto *p = ifa->ra;
+
+ if (!ifa->cf->router_discovery)
+ return;
+
+ /* Basic validation */
+ if ((uint)length < sizeof(struct radv_ra_packet))
+ {
+ RADV_TRACE(D_PACKETS, "Malformed RA received from %I via %s",
+ from, ifa->iface->name);
+ return;
+ }
+
+ u16 router_lifetime = ntohs(pkt->router_lifetime);
+ u32 ifindex = ifa->iface->index;
+
+ /* Router lifetime of 0 means withdrawal */
+ if (router_lifetime == 0)
+ {
+ RADV_TRACE(D_EVENTS, "Neighbor %I on %s (ifindex=%u) withdrawing (lifetime=0)",
+ from, ifa->iface->name, ifindex);
+ /* Withdraw from routing table if peers channel is configured */
+ radv_withdraw_peer(p, from, ifindex);
+
+ return;
+ }
+
+ /* Announce/update the peer in routing table */
+ RADV_TRACE(D_PACKETS, "Processed RA from %I on %s (ifindex=%u): lifetime=%u, hop_limit=%u",
+ from, ifa->iface->name, ifindex, router_lifetime, pkt->current_hop_limit);
+
+ /* Announce to routing table with router lifetime for expiration tracking and interface index */
+ radv_announce_peer(p, from, router_lifetime, ifindex);
+}
+
static int
radv_rx_hook(sock *sk, uint size)
{
@@ -493,7 +531,8 @@ radv_rx_hook(sock *sk, uint size)
case ICMPV6_RA:
RADV_TRACE(D_PACKETS, "Received RA from %I via %s",
sk->faddr, ifa->iface->name);
- /* FIXME - there should be some checking of received RAs, but we just ignore them */
+ if (ifa->cf->router_discovery)
+ radv_process_ra(ifa, sk->faddr, (struct radv_ra_packet *)buf, size);
return 1;
default:
diff --git a/proto/radv/radv.c b/proto/radv/radv.c
index 8ae411da..58260c6d 100644
--- a/proto/radv/radv.c
+++ b/proto/radv/radv.c
@@ -43,10 +43,60 @@
* RFC 6106 - DNS extensions (RDDNS, DNSSL)
*/
-static struct ea_class ea_radv_preference, ea_radv_lifetime;
+static struct ea_class ea_radv_preference, ea_radv_lifetime, ea_radv_expires_at;
static void radv_prune_prefixes(struct radv_iface *ifa);
static void radv_prune_routes(struct radv_proto *p);
+static void radv_neighbor_prune(struct radv_iface *ifa);
+
+void
+radv_announce_peer(struct radv_proto *p, ip_addr peer_ip, u16 router_lifetime, u32 ifindex)
+{
+ if (!p->peers_channel)
+ return;
+
+ /* Check if channel is ready */
+ if (p->peers_channel->channel_state != CS_UP) {
+ RADV_TRACE(D_EVENTS, "Peers channel not UP yet (state=%d), skipping peer announcement",
+ p->peers_channel->channel_state);
+ return;
+ }
+
+ /* Compute expiration time */
+ btime now = current_time();
+ btime expires_at = now + (router_lifetime S);
+
+ net_addr_peer n;
+ net_fill_peer((net_addr *) &n, peer_ip, ifindex);
+
+ ea_list *ea = NULL;
+ ea_set_attr_u32(&ea, &ea_gen_preference, 0, p->peers_channel->preference);
+ ea_set_attr_u32(&ea, &ea_gen_source, 0, RTS_DEVICE);
+ ea_set_attr_u32(&ea, &ea_radv_expires_at, 0, expires_at / 1000000);
+
+ rte e0 = {
+ .attrs = ea,
+ .src = p->p.main_source,
+ };
+
+ RADV_TRACE(D_EVENTS, "Announcing peer to channel: peer_ip: %I, ifindex=%u, lifetime=%u, expires_at=%T",
+ peer_ip, ifindex, router_lifetime, expires_at);
+
+ rte_update(p->peers_channel, (net_addr *) &n, &e0, p->p.main_source);
+}
+
+void
+radv_withdraw_peer(struct radv_proto *p, ip_addr peer_ip, u32 ifindex)
+{
+ if (!p->peers_channel)
+ return;
+
+ net_addr_peer n;
+ net_fill_peer((net_addr *) &n, peer_ip, ifindex);
+
+ /* Withdraw the route */
+ rte_update(p->peers_channel, (net_addr *) &n, NULL, p->p.main_source);
+}
static void
radv_timer(timer *tm)
@@ -63,6 +113,9 @@ radv_timer(timer *tm)
if (p->prune_time <= now)
radv_prune_routes(p);
+ /* Prune stale discovered neighbor routers entries */
+ radv_neighbor_prune(ifa);
+
radv_send_ra(ifa, IPA_NONE);
/* Update timer */
@@ -212,6 +265,86 @@ radv_prune_prefixes(struct radv_iface *ifa)
ifa->prune_time = next;
}
+void
+radv_neighbor_prune(struct radv_iface *ifa)
+{
+ struct radv_proto *p = ifa->ra;
+
+ /* Skip pruning check if router discovery is not enabled */
+ if (!ifa->cf->router_discovery)
+ return;
+
+ /* Skip if no peers channel configured */
+ if (!p->peers_channel || p->peers_channel->channel_state != CS_UP)
+ return;
+
+ /* Check for expired peers by walking the routing table */
+ btime now = current_time();
+
+ /* Temporary list to store expired peers (can't withdraw during export walk) */
+ struct expired_peer {
+ struct expired_peer *next;
+ ip_addr peer_ip;
+ u32 ifindex;
+ };
+ struct expired_peer *expired_list = NULL;
+
+ /* Walk all routes in the peers channel to check for expired entries */
+ RT_EXPORT_WALK(&p->peers_channel->out_req, u)
+ {
+ switch (u->kind)
+ {
+ case RT_EXPORT_FEED:
+ /* Check each route in the feed */
+ for (uint i = 0; i < u->feed->count_routes; i++)
+ {
+ rte *e = &u->feed->block[i];
+ if (e->flags & REF_OBSOLETE)
+ continue;
+
+ /* Only process routes from our protocol */
+ if (e->src != p->p.main_source)
+ continue;
+
+ /* Get the expiration time EA */
+ eattr *expires_ea = ea_find(e->attrs, &ea_radv_expires_at);
+ if (!expires_ea)
+ continue;
+
+ btime expires_at = expires_ea->u.data * 1000000;
+
+ if (expires_at <= now)
+ {
+ /* Peer has expired, add to withdrawal list */
+ net_addr_peer *peer_addr = (net_addr_peer *) e->net;
+
+ struct expired_peer *ep = mb_alloc(p->p.pool, sizeof(struct expired_peer));
+ ep->peer_ip = peer_addr->addr;
+ ep->ifindex = peer_addr->ifindex;
+ ep->next = expired_list;
+ expired_list = ep;
+
+ RADV_TRACE(D_EVENTS, "Peer %I (ifindex=%u) expired (expires_at=%T, now=%T)",
+ peer_addr->addr, peer_addr->ifindex, expires_at, now);
+ }
+ }
+ break;
+ default:
+ /* Nothing to do for RT_EXPORT_UPDATE or RT_EXPORT_STOP */
+ break;
+ }
+ }
+
+ /* Withdraw all expired peers */
+ while (expired_list)
+ {
+ struct expired_peer *ep = expired_list;
+ radv_withdraw_peer(p, ep->peer_ip, ep->ifindex);
+ expired_list = ep->next;
+ mb_free(ep);
+ }
+}
+
static char* ev_name[] = { NULL, "Init", "Change", "RS" };
void
@@ -581,6 +714,12 @@ radv_init(struct proto_config *CF)
P->main_channel = proto_add_channel(P, proto_cf_main_channel(CF));
+ /* Add all other configured channels (e.g., peer channel) */
+ struct channel_config *cc;
+ WALK_LIST(cc, CF->channels)
+ if (cc != proto_cf_main_channel(CF))
+ proto_add_channel(P, cc);
+
P->preexport = radv_preexport;
P->rt_notify = radv_rt_notify;
P->iface_sub.if_notify = radv_if_notify;
@@ -619,6 +758,19 @@ radv_start(struct proto *P)
radv_set_fib(p, cf->propagate_routes);
p->prune_time = TIME_INFINITY;
+ /* Find the peers channel if configured */
+ p->peers_channel = NULL;
+ struct channel *c;
+ WALK_LIST(c, P->channels)
+ {
+ if (c->net_type == NET_PEER)
+ {
+ p->peers_channel = c;
+ RADV_TRACE(D_EVENTS, "Found peers discovery channel: %s", c->name);
+ break;
+ }
+ }
+
return PS_UP;
}
@@ -765,10 +917,16 @@ static struct ea_class ea_radv_lifetime = {
.type = T_INT,
};
+static struct ea_class ea_radv_expires_at = {
+ .name = "radv_expires_at",
+ .legacy_name = "RAdv.expires_at",
+ .type = T_INT,
+};
+
struct protocol proto_radv = {
.name = "RAdv",
.template = "radv%d",
- .channel_mask = NB_IP6,
+ .channel_mask = NB_IP6 | NB_PEER,
.proto_size = sizeof(struct radv_proto),
.config_size = sizeof(struct radv_config),
.postconfig = radv_postconfig,
@@ -787,6 +945,7 @@ radv_build(void)
EA_REGISTER_ALL(
&ea_radv_preference,
- &ea_radv_lifetime
+ &ea_radv_lifetime,
+ &ea_radv_expires_at
);
}
diff --git a/proto/radv/radv.h b/proto/radv/radv.h
index 1a4afc55..4757d164 100644
--- a/proto/radv/radv.h
+++ b/proto/radv/radv.h
@@ -91,6 +91,7 @@ struct radv_iface_config
u8 route_lifetime_sensitive; /* Whether route_lifetime depends on trigger */
u8 default_preference; /* Default Router Preference (RFC 4191) */
u8 route_preference; /* Specific Route Preference (RFC 4191) */
+ u8 router_discovery; /* Enable neighbor router discovery */
};
struct radv_prefix_config
@@ -161,6 +162,7 @@ struct radv_proto
u8 fib_up; /* FIB table (routes) is initialized */
struct fib routes; /* FIB table of specific routes (struct radv_route) */
btime prune_time; /* Next time of route table pruning */
+ struct channel *peers_channel; /* Channel for peer discovery (NET_PEER) */
};
struct radv_prefix /* One prefix we advertise */
@@ -221,6 +223,8 @@ static inline void radv_invalidate(struct radv_iface *ifa)
/* radv.c */
void radv_iface_notify(struct radv_iface *ifa, int event);
+void radv_announce_peer(struct radv_proto *p, ip_addr peer_ip, u16 router_lifetime, u32 ifindex);
+void radv_withdraw_peer(struct radv_proto *p, ip_addr peer_ip, u32 ifindex);
/* packets.c */
int radv_process_domain(struct radv_dnssl_config *cf);
--
2.43.0
More information about the Bird-users
mailing list