[PATCH 4/4] BGP: Add peers channel and sessions spawn on export to achieve automatic peering

Matteo Perin matteo.perin at canonical.com
Fri Dec 5 14:52:32 CET 2025


This change proposal is made in order to achieve BGP automatic peering based
on discovered link-local addresses leveraging the IPv6 ND protocol.

A big part of the necessary logic is already present in the dynamic BGP implementation.
The problem is that dynamic BGP works in a passive/reactive model only, as it waits for
peers within a specified range to connect.

This integrates with this implementation by triggering the spawn of a BGP process
sending OPEN requests in order to achieve active peer connection when a valid peer
remote address has been discovered.

The discovery is done by exporting routing information from a table containing
peer data through a newly introduced peers channel. This means that the feature
will only work if there is another protocol actively importing peer data (e.g. RAdv).
The feature is also meant to work only in the presence of a valid dynamic BGP
configuration.

The BGP peers channel has two additional configuration parameters:
- from [iface]: determines the interface used for automatic BGP session establishment
- persist [yes/no]: controls the behavior on peer route withdrawal. If enabled the
BGP connection spawned from peer discovery will be left running, if disabled the
session will be torn down.

Logic to avoid duplicate sessions from both the dynamic BGP and automatic peers
discovery paths has also been introduced.

Signed-off-by: Matteo Perin <matteo.perin at canonical.com>
---
 proto/bgp/attrs.c   |  57 ++++++++++++++++
 proto/bgp/bgp.c     | 162 +++++++++++++++++++++++++++++++++++++++++---
 proto/bgp/bgp.h     |  28 +++++++-
 proto/bgp/config.Y  |  15 +++-
 proto/bgp/packets.c |  11 +++
 5 files changed, 262 insertions(+), 11 deletions(-)

diff --git a/proto/bgp/attrs.c b/proto/bgp/attrs.c
index f072e4c9..dcec1c45 100644
--- a/proto/bgp/attrs.c
+++ b/proto/bgp/attrs.c
@@ -2468,6 +2468,63 @@ bgp_rt_notify(struct proto *P, struct channel *C, const net_addr *n, rte *new, c
   if (SHUTTING_DOWN)
     return;
 
+  /* Handle peers channel - spawn BGP sessions for discovered peers */
+  if (C->net_type == NET_PEER)
+  {
+    /* If this is a spawned child, ignore all notifications from the peers channel */
+    if (p->cf->c.parent)
+      return;
+
+    const net_addr_peer *peer = (const net_addr_peer *) n;
+
+    /* Only handle peer management for dynamic BGP instances and link-local addresses */
+    if (bgp_is_dynamic(p) && ipa_is_link_local(peer->addr))
+    {
+      if (new && !old)
+      {
+        /* New peer discovered - send spawn event to main loop */
+        struct bgp_peer_spawn *ev = mb_alloc(p->p.pool, sizeof(struct bgp_peer_spawn));
+        *ev = (struct bgp_peer_spawn) {
+          .p = p,
+          .peer_addr = peer->addr,
+          .iface = bc->cf->peers_iface,
+        };
+
+        /* Initialize callback to run on main_birdloop */
+        callback_init(&ev->cb, bgp_peer_spawn, &main_birdloop);
+
+        /* Send the event */
+        callback_activate(&ev->cb);
+
+        log(L_INFO "%s: Peer %I discovered, queued for BGP session spawn", 
+          p->p.name, peer->addr);
+      }
+      else if (!new && old)
+      {
+        /* If persist is not enabled, stop spawned sessions for this peer */
+        if (!bc->cf->peers_persist)
+        {
+          /* Send removal event to main loop */
+          struct bgp_peer_remove *ev = mb_alloc(p->p.pool, sizeof(struct bgp_peer_remove));
+          *ev = (struct bgp_peer_remove) {
+            .p = p,
+            .peer_addr = peer->addr,
+          };
+
+          /* Initialize callback to run on main_birdloop */
+          callback_init(&ev->cb, bgp_peer_remove, &main_birdloop);
+
+          /* Send the event */
+          callback_activate(&ev->cb);
+
+          log(L_INFO "%s: Peer %I withdrawn from channel, queued for BGP session removal", 
+              p->p.name, peer->addr);
+        }
+      }
+    }
+    return;
+  }
+
   /* Ignore non-BGP channels */
   if (C->class != &channel_bgp)
     return;
diff --git a/proto/bgp/bgp.c b/proto/bgp/bgp.c
index ad6dd334..e17459c6 100644
--- a/proto/bgp/bgp.c
+++ b/proto/bgp/bgp.c
@@ -173,13 +173,6 @@ static void bgp_listen_close(struct bgp_proto *, struct bgp_listen_request *);
 static void bgp_graceful_restart_feed(struct bgp_channel *c);
 static void bgp_restart_route_refresh(void *_bc);
 
-/* Dynamic BGP detection */
-#define bgp_is_dynamic(x) (_Generic((x),			\
-    struct bgp_proto *: ipa_zero((x)->remote_ip),		\
-    struct bgp_config *: ipa_zero((x)->remote_ip),		\
-    struct bgp_listen_request *: ipa_zero((x)->remote_ip)))
-
-
 /*
  * BGP Instance Management
  */
@@ -1213,12 +1206,63 @@ bgp_decision(void *vp)
     bgp_down(p);
 }
 
+/**
+ * bgp_find_existing_session - check if a dynamic BGP session already exists for a peer
+ * @peer_addr: Remote peer IP address to check
+ *
+ * Checks if there's already an existing dynamic BGP session for the given peer
+ * by walking through all BGP protocols.
+ *
+ * Returns: pointer to the existing BGP protocol, or NULL if none found
+ */
+static struct bgp_proto *
+bgp_find_existing_session(ip_addr peer_addr)
+{
+  struct config *cfg = OBSREF_GET(config);
+  if (!cfg)
+    return NULL;
+
+  struct proto_config *pc;
+  WALK_LIST(pc, cfg->protos)
+  {
+    if (pc->protocol != &proto_bgp)
+      continue;
+
+    if (pc->proto)
+    {
+      struct bgp_proto *child_p = (struct bgp_proto *) pc->proto;
+      
+      if (ipa_equal(child_p->remote_ip, peer_addr))
+      {
+        log(L_DEBUG "BGP: Found existing session %s for peer %I", 
+            child_p->p.name, peer_addr);
+        return child_p;
+      }
+    }
+  }
+  return NULL;
+}
+
 static void
 bgp_spawn(struct bgp_proto *pp, struct birdsock *sk)
 {
   struct symbol *sym;
   char fmt[SYM_MAX_LEN];
 
+  /* Check if there's an existing session for this peer and shut it down.
+   * The dynamic BGP session (with the incoming socket) takes precedence. */
+  struct bgp_proto *existing = bgp_find_existing_session(sk->daddr);
+  if (existing)
+  {
+    log(L_DEBUG "BGP: Found existing session %s for %I, shutting it down to use incoming connection", 
+        existing->p.name, sk->daddr);
+    
+    /* Mark for deletion and disable */
+    existing->p.cf_new = NULL;
+    existing->p.reconfiguring = 1;
+    proto_disable(&existing->p);
+  }
+
   bsprintf(fmt, "%s%%0%dd", pp->cf->dynamic_name, pp->cf->dynamic_name_digits);
 
   /* This is hack, we would like to share config, but we need to copy it now */
@@ -1247,6 +1291,92 @@ bgp_spawn(struct bgp_proto *pp, struct birdsock *sk)
   proto_enable(&p->p);
 }
 
+void
+bgp_peer_spawn(struct callback *cb)
+{
+  struct bgp_peer_spawn *ev = (void *) cb;
+
+  /* Check if a session for this peer already exists */
+  if (bgp_find_existing_session(ev->peer_addr))
+  {
+    log(L_DEBUG "BGP: Peer %I already has existing session, not spawning duplicate", ev->peer_addr);
+    return;
+  }
+
+  struct symbol *sym;
+  char fmt[SYM_MAX_LEN];
+
+  log(L_DEBUG "BGP: Spawning new peer session for %I", ev->peer_addr);
+  bsprintf(fmt, "%s%%0%dd", ev->p->cf->dynamic_name, ev->p->cf->dynamic_name_digits);
+
+  /* Clone the configuration */
+  new_config = OBSREF_GET(config);
+  cfg_mem = new_config->mem;
+  new_config->current_scope = new_config->root_scope;
+  sym = cf_default_name(new_config, fmt, &(ev->p->dynamic_name_counter));
+  proto_clone_config(sym, ev->p->p.cf);
+  new_config = NULL;
+  cfg_mem = NULL;
+
+  /* Configure for active connection to discovered peer */
+  struct bgp_config *cf = SKIP_BACK(struct bgp_config, c, sym->proto);
+  cf->remote_ip = ev->peer_addr;
+  cf->local_ip = ev->p->cf->local_ip;
+  cf->iface = ev->iface;
+  cf->ipatt = NULL;
+  cf->passive = 0;
+
+  /* Create and enable the protocol */
+  SKIP_BACK_DECLARE(struct bgp_proto, p, p, proto_spawn(sym->proto, 1));
+
+  proto_enable(&p->p);
+}
+
+void
+bgp_peer_remove(struct callback *cb)
+{
+  struct bgp_peer_remove *ev = (void *) cb;
+  
+  /* Find ALL BGP sessions for this peer and remove them */
+  struct config *cfg = OBSREF_GET(config);
+  if (!cfg)
+    return;
+
+  struct proto_config *pc;
+  WALK_LIST(pc, cfg->protos)
+  {
+    if (pc->protocol != &proto_bgp)
+      continue;
+
+    /* Check protocol instance */
+    if (pc->proto)
+    {
+      struct bgp_proto *bgp_p = (struct bgp_proto *) pc->proto;
+      
+      /* Found a BGP session with matching peer address */
+      if (ipa_equal(bgp_p->remote_ip, ev->peer_addr))
+      {
+        /* Skip parent protocol */
+        if (bgp_is_dynamic(bgp_p))
+          continue;
+        
+        log(L_INFO "%s: Removing BGP session %s for withdrawn peer %I", 
+            ev->p->p.name, pc->name, ev->peer_addr);
+        
+        /* Mark protocol for deletion and disable it */
+        bgp_p->p.cf_new = NULL;
+        bgp_p->p.reconfiguring = 1;
+        proto_disable(&bgp_p->p);
+      }
+      else
+      {
+        log(L_DEBUG "BGP: Session %s with remote %I does not match withdrawn peer %I", 
+            pc->name, bgp_p->remote_ip, ev->peer_addr);
+      }
+    }
+  }
+}
+
 void
 bgp_stop(struct bgp_proto *p, int subcode, byte *data, uint len)
 {
@@ -2664,6 +2794,18 @@ bgp_start_locked(void *_p)
       birdloop_leave(sk_loop);
   }
 
+  if (bgp_is_dynamic(p)) {
+    /* Start peers channels immediately for dynamic BGP */
+    struct bgp_channel *c;
+    BGP_WALK_CHANNELS(p, c)
+    {
+      if (c->c.net_type == NET_PEER && !c->c.disabled)
+      {
+        channel_set_state(&c->c, CS_UP);
+      }
+    }
+  }
+
   if (cf->multihop || bgp_is_dynamic(p))
   {
     /* Multi-hop sessions do not use neighbor entries */
@@ -2974,6 +3116,10 @@ bgp_channel_init(struct channel *C, struct channel_config *CF)
   c->afi = cf->afi;
   c->desc = cf->desc;
 
+  /* Set peers channels to get announcement of any route change */
+  if (C->net_type == NET_PEER && C->ra_mode == RA_UNDEF)
+    C->ra_mode = RA_ANY;
+
   if (cf->igp_table_ip4)
     c->igp_table_ip4 = cf->igp_table_ip4->table;
 
@@ -4090,7 +4236,7 @@ struct protocol proto_bgp = {
   .name = 		"BGP",
   .template = 		"bgp%d",
   .preference = 	DEF_PREF_BGP,
-  .channel_mask =	NB_IP | NB_VPN | NB_FLOW | NB_MPLS,
+  .channel_mask =	NB_IP | NB_VPN | NB_FLOW | NB_MPLS | NB_PEER,
   .proto_size =		sizeof(struct bgp_proto),
   .config_size =	sizeof(struct bgp_config),
   .postconfig =		bgp_postconfig,
diff --git a/proto/bgp/bgp.h b/proto/bgp/bgp.h
index 8f76e7a5..e393a34e 100644
--- a/proto/bgp/bgp.h
+++ b/proto/bgp/bgp.h
@@ -26,6 +26,7 @@ struct eattr;
 
 #define BGP_AFI_IPV4		1
 #define BGP_AFI_IPV6		2
+#define BGP_AFI_PEER		3
 
 #define BGP_SAFI_UNICAST	1
 #define BGP_SAFI_MULTICAST	2
@@ -52,7 +53,7 @@ struct eattr;
 #define BGP_AF_VPN6_MC		BGP_AF( BGP_AFI_IPV6, BGP_SAFI_VPN_MULTICAST )
 #define BGP_AF_FLOW4		BGP_AF( BGP_AFI_IPV4, BGP_SAFI_FLOW )
 #define BGP_AF_FLOW6		BGP_AF( BGP_AFI_IPV6, BGP_SAFI_FLOW )
-
+#define BGP_AF_PEER		BGP_AF( BGP_AFI_PEER, BGP_SAFI_UNICAST )
 
 struct bgp_write_state;
 struct bgp_parse_state;
@@ -191,6 +192,8 @@ struct bgp_channel_config {
   u32 cost;				/* IGP cost for direct next hops */
   u8 import_table;			/* Use c.in_table as Adj-RIB-In */
   u8 export_table;			/* Keep Adj-RIB-Out and export it */
+  u8 peers_persist;			/* Keep spawned BGP sessions after peer discovery route withdrawal */
+  struct iface *peers_iface;		/* Interface for peer discovery (for link-local addresses) */
 
   struct settle_config ptx_exporter_settle;    /* Settle timer for export dumps */
 
@@ -401,6 +404,23 @@ struct bgp_incoming_socket {
   sock *sk;		/* The actual socket */
 };
 
+struct bgp_peer_spawn {
+  callback cb;
+  struct bgp_proto *p;		/* Parent protocol */
+  ip_addr peer_addr;		/* Peer address to spawn session for */
+  struct iface *iface;		/* Interface for link-local peers */
+};
+
+struct bgp_peer_remove {
+  callback cb;
+  struct bgp_proto *p;		/* Parent protocol */
+  ip_addr peer_addr;		/* Peer address to remove session for */
+};
+
+/* Callback hooks (implemented in bgp.c) */
+void bgp_peer_spawn(struct callback *cb);
+void bgp_peer_remove(struct callback *cb);
+
 struct bgp_listen_request {
   node pn;				/* Node in bgp_proto listen list */
   node sn;				/* Node in bgp_socket requests list */
@@ -978,5 +998,11 @@ enum bgp_attr_id {
 #define ORIGIN_EGP		1
 #define ORIGIN_INCOMPLETE	2
 
+/* Dynamic BGP detection */
+
+#define bgp_is_dynamic(x) (_Generic((x),			\
+    struct bgp_proto *: ipa_zero((x)->remote_ip),		\
+    struct bgp_config *: ipa_zero((x)->remote_ip),		\
+    struct bgp_listen_request *: ipa_zero((x)->remote_ip)))
 
 #endif
diff --git a/proto/bgp/config.Y b/proto/bgp/config.Y
index c7316d86..11ee90f1 100644
--- a/proto/bgp/config.Y
+++ b/proto/bgp/config.Y
@@ -33,9 +33,9 @@ CF_KEYWORDS(BGP, LOCAL, NEIGHBOR, AS, HOLD, TIME, CONNECT, RETRY, KEEPALIVE,
 	STRICT, BIND, CONFEDERATION, MEMBER, MULTICAST, FLOW4, FLOW6, LONG,
 	LIVED, STALE, IMPORT, IBGP, EBGP, MANDATORY, INTERNAL, EXTERNAL, SETS,
 	DYNAMIC, RANGE, NAME, DIGITS, AIGP, ORIGINATE, COST, ENFORCE,
-	FIRST, FREE, VALIDATE, BASE, ROLE, ROLES, PEER, PROVIDER, CUSTOMER,
+	FIRST, FREE, VALIDATE, BASE, ROLE, ROLES, PEER, PEERS, PROVIDER, CUSTOMER,
 	RS_SERVER, RS_CLIENT, REQUIRE, BGP_OTC, GLOBAL, SEND, RECV, MIN, MAX,
-	TX, SIZE, WARNING,
+	TX, SIZE, WARNING, PERSIST,
 	AUTHENTICATION, NONE, MD5, AO, FORMAT, NATIVE, SINGLE, DOUBLE)
 
 CF_KEYWORDS(KEY, KEYS, SECRET, DEPRECATED, PREFERRED, ALGORITHM, CMAC, AES128)
@@ -270,6 +270,7 @@ bgp_afi:
  | VPN6 MULTICAST	{ $$ = BGP_AF_VPN6_MC; }
  | FLOW4		{ $$ = BGP_AF_FLOW4; }
  | FLOW6		{ $$ = BGP_AF_FLOW6; }
+ | PEERS		{ $$ = BGP_AF_PEER; }
  ;
 
 tcp_ao_key_start: KEY {
@@ -444,6 +445,16 @@ bgp_channel_item:
     if (BGP_SAFI(BGP_CC->afi) != BGP_SAFI_FLOW)
       cf_error("Validate option limited to flowspec channels");
    }
+ | PERSIST bool {
+    BGP_CC->peers_persist = $2;
+    if (BGP_CC->c.net_type != NET_PEER)
+      cf_error("Persist option only available for peers channels");
+   }
+ | FROM text {
+    BGP_CC->peers_iface = if_get_by_name($2);
+    if (BGP_CC->c.net_type != NET_PEER)
+      cf_error("From option only available for peers channels");
+   }
  | GRACEFUL RESTART bool { BGP_CC->gr_able = $3; }
  | LONG LIVED GRACEFUL RESTART bool { BGP_CC->llgr_able = $5; }
  | LONG LIVED STALE TIME expr { BGP_CC->llgr_time = $5; if ($5 >= (1 << 24)) cf_error("Long-lived stale time must be less than 2^24"); }
diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c
index bdbeabd1..87a5b777 100644
--- a/proto/bgp/packets.c
+++ b/proto/bgp/packets.c
@@ -2342,6 +2342,17 @@ static const struct bgp_af_desc bgp_af_table[] = {
     .decode_next_hop = bgp_decode_next_hop_none,
     .update_next_hop = bgp_update_next_hop_none,
   },
+  {
+    .afi = BGP_AF_PEER,
+    .net = NET_PEER,
+    .no_igp = 1,
+    .name = "peers",
+    .encode_nlri = NULL,
+    .decode_nlri = NULL,
+    .encode_next_hop = NULL,
+    .decode_next_hop = NULL,
+    .update_next_hop = NULL,
+  },
 };
 
 const struct bgp_af_desc *
-- 
2.43.0



More information about the Bird-users mailing list