[PATCH 2/4] RAdv: Add neighbor discovery based on incoming RAs to RAdv proto

Matteo Perin matteo.perin at canonical.com
Fri Dec 5 14:52:30 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 neighbor discovery [yes/no].

Signed-off-by: Matteo Perin <matteo.perin at canonical.com>
---
 proto/radv/config.Y  |   3 +-
 proto/radv/packets.c |  41 ++++++++++-
 proto/radv/radv.c    | 163 ++++++++++++++++++++++++++++++++++++++++++-
 proto/radv/radv.h    |   4 ++
 4 files changed, 206 insertions(+), 5 deletions(-)

diff --git a/proto/radv/config.Y b/proto/radv/config.Y
index 372081d2..09d39863 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, NEIGHBOR, 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; }
+ | NEIGHBOR DISCOVERY bool { RADV_IFACE->neighbor_discovery = $3; }
  ;
 
 radv_preference:
diff --git a/proto/radv/packets.c b/proto/radv/packets.c
index 40f3c41e..bd0e4fae 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->neighbor_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);
+  
+  /* Router lifetime of 0 means withdrawal */
+  if (router_lifetime == 0)
+  {
+    RADV_TRACE(D_EVENTS, "Neighbor %I on %s withdrawing (lifetime=0)",
+	       from, ifa->iface->name);
+    
+    /* Withdraw from routing table if peers channel is configured */
+    radv_withdraw_peer(p, from);
+    
+    return;
+  }
+  
+  /* Announce/update the peer in routing table */
+  RADV_TRACE(D_PACKETS, "Processed RA from %I: lifetime=%u, hop_limit=%u",
+	     from, router_lifetime, pkt->current_hop_limit);
+  
+  /* Announce to routing table with router lifetime for expiration tracking */
+  radv_announce_peer(p, from, router_lifetime);
+}
+
 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->neighbor_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..14b08212 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)
+{
+  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);
+
+  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, lifetime=%u, expires_at=%T", 
+             peer_ip, 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)
+{
+  if (!p->peers_channel)
+    return;
+
+  net_addr_peer n;
+  net_fill_peer((net_addr *) &n, peer_ip);
+
+  /* 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,84 @@ 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 neighbor discovery is not enabled */
+  if (!ifa->cf->neighbor_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;
+  };
+  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->next = expired_list;
+            expired_list = ep;
+            
+            RADV_TRACE(D_EVENTS, "Peer %I expired (expires_at=%T, now=%T)",
+                      peer_addr->addr, 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);
+    expired_list = ep->next;
+    mb_free(ep);
+  }
+}
+
 static char* ev_name[] = { NULL, "Init", "Change", "RS" };
 
 void
@@ -581,6 +712,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 +756,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 +915,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 +943,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..f046e101 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 neighbor_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);
+void radv_withdraw_peer(struct radv_proto *p, ip_addr peer_ip);
 
 /* packets.c */
 int radv_process_domain(struct radv_dnssl_config *cf);
-- 
2.43.0



More information about the Bird-users mailing list