Repository: holo-routing/holo
Pull request #32: bier: BIFT computation and synchronization with Fastclick
Author: login: nrybowski
Created: 2024-10-01T11:41:21Z | Merged: 2025-03-15T01:26:21Z

This PR adds the computation of the BIFT upon BIRT update and synchronization with a BIFT implemented as a Fastclick dataplane element.

See also https://github.com/tbarbette/fastclick/pull/397 and https://github.com/holo-routing/holo/issues/14.

### TODO
~- [ ] Make Fastclick BIFT URI configurable through YANG model.~ (Moved in https://github.com/holo-routing/holo/issues/52)
~- [ ] Leverage BIFT-ID when pushing entries to the BIFTs. Currently, a single BIFT is supported.~ (Moved in https://github.com/holo-routing/holo/issues/52)
- [x] Batch route addition when multiple BFRs have the same F-BM.
~- [ ] Allow removing BIFT entries.~ (Moved in https://github.com/holo-routing/holo/issues/50)


Diff:
diff --git a/README.md b/README.md
index 01201c193..e2a997f0c 100644
--- a/README.md
+++ b/README.md
@@ -225,7 +225,7 @@ Holo supports the following Internet Standards:
 | ietf-bfd@2022-09-22 | 100.00% | 100.00% | - | - | [100.00%](https://holo-routing.github.io/ietf-yang-coverage/ietf-bfd@2022-09-22.html) |
 | ietf-bgp-policy@2023-07-05 | 100.00% | - | - | - | [100.00%](https://holo-routing.github.io/ietf-yang-coverage/ietf-bgp-policy@2023-07-05.html) |
 | ietf-bgp@2023-07-05 | 32.38% | 85.95% | - | - | [60.40%](https://holo-routing.github.io/ietf-yang-coverage/ietf-bgp@2023-07-05.html) |
-| ietf-bier@2023-09-12 | 65.52% | - | - | 0.00% | [47.50%](https://holo-routing.github.io/ietf-yang-coverage/ietf-bier@2023-09-12.html) |
+| ietf-bier@2023-09-12 | 100.00% | - | - | 0.00% | [72.50%](https://holo-routing.github.io/ietf-yang-coverage/ietf-bier@2023-09-12.html) |
 | ietf-if-extensions@2023-01-26 | 100.00% | 0.00% | - | - | [50.00%](https://holo-routing.github.io/ietf-yang-coverage/ietf-if-extensions@2023-01-26.html) |
 | ietf-if-vlan-encapsulation@2023-01-26 | 42.86% | - | - | - | [42.86%](https://holo-routing.github.io/ietf-yang-coverage/ietf-if-vlan-encapsulation@2023-01-26.html) |
 | ietf-interfaces@2018-02-20 | 100.00% | 0.00% | - | - | [22.22%](https://holo-routing.github.io/ietf-yang-coverage/ietf-interfaces@2018-02-20.html) |
diff --git a/holo-daemon/Cargo.toml b/holo-daemon/Cargo.toml
index 06d378b37..43a24fa4c 100644
--- a/holo-daemon/Cargo.toml
+++ b/holo-daemon/Cargo.toml
@@ -98,3 +98,4 @@ vrrp = ["dep:holo-vrrp", "holo-interface/vrrp"]
 io_uring = ["dep:tokio-uring"]
 tokio_console = ["dep:console-subscriber"]
 yang3_bundled = ["yang3/bundled"]
+fastclick = ["holo-utils/fastclick"]
diff --git a/holo-routing/src/lib.rs b/holo-routing/src/lib.rs
index 19b137164..3c6dad0df 100644
--- a/holo-routing/src/lib.rs
+++ b/holo-routing/src/lib.rs
@@ -107,6 +107,10 @@ impl Master {
                         )
                         .await;
                 }
+                Some(_) = self.birt.update_queue_rx.recv() => {
+                    self.birt
+                        .process_birt_update_queue(&self.interfaces).await;
+                }
             }
         }
     }
diff --git a/holo-routing/src/northbound/configuration.rs b/holo-routing/src/northbound/configuration.rs
index 7f8799d60..2709a72c5 100644
--- a/holo-routing/src/northbound/configuration.rs
+++ b/holo-routing/src/northbound/configuration.rs
@@ -19,8 +19,9 @@ use holo_northbound::yang::routing::segment_routing::sr_mpls;
 use holo_northbound::yang::routing::{bier, ribs};
 use holo_northbound::{CallbackKey, NbDaemonSender};
 use holo_utils::bier::{
-    BierCfgEvent, BierEncapsulation, BierEncapsulationType, BierInBiftId,
-    BierSubDomainCfg, Bsl, SubDomainId, UnderlayProtocolType,
+    BfrId, BierBift, BierBiftCfg, BierCfgEvent, BierEncapsulation,
+    BierEncapsulationType, BierInBiftId, BierOutBiftId, BierSubDomainCfg,
+    BiftNbr, Bsl, SubDomainId, UnderlayProtocolType,
 };
 use holo_utils::ibus::IbusMsg;
 use holo_utils::ip::{AddressFamily, IpNetworkKind};
@@ -60,6 +61,9 @@ pub enum ListEntry {
         Bsl,
         BierEncapsulationType,
     ),
+    BierCfgBift(BfrId),
+    BierCfgBiftBsl(BfrId, Bsl),
+    BierCfgBiftNbr(BfrId, Bsl, IpAddr),
 }
 
 #[derive(Debug, EnumAsInner)]
@@ -78,6 +82,7 @@ pub enum Event {
     BierCfgUpdate,
     BierCfgEncapUpdate(SubDomainId, AddressFamily, Bsl, BierEncapsulationType),
     BierCfgSubDomainUpdate(AddressFamily),
+    BierCfgBiftUpdate(BfrId),
 }
 
 // ===== configuration structs =====
@@ -873,6 +878,169 @@ fn load_callbacks() -> Callbacks<Master> {
         .delete_apply(|_context, _args| {
             // Nothing to do.
         })
+        .path(bier::bift::PATH)
+        .create_apply(|master, args| {
+            let bfr_id = args.dnode.get_u16_relative("./bfr-id").unwrap();
+
+            let bift_cfg = BierBiftCfg {
+                bfr_id,
+                birt: Default::default()
+            };
+
+            master.bier_config.bift_cfg.insert(bfr_id, bift_cfg);
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            event_queue.insert(Event::BierCfgBiftUpdate(bfr_id));
+        })
+        .delete_apply(|master, args| {
+            let bfr_id = args.dnode.get_u16_relative("./bfr-id").unwrap();
+            master.bier_config.bift_cfg.remove(&bfr_id);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            event_queue.insert(Event::BierCfgBiftUpdate(bfr_id));
+        })
+        .lookup(|_master, _list_entry, dnode| {
+            let bfr_id = dnode.get_u16_relative("./bfr-id").unwrap();
+            ListEntry::BierCfgBift(bfr_id)
+        })
+        .path(bier::bift::birt_bitstringlength::PATH)
+        .create_apply(|context, args| {
+            let bfr_id = args.list_entry.into_bier_cfg_bift().unwrap();
+            let bift_cfg = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+
+            let bsl = args.dnode.get_string_relative("./bsl").unwrap();
+            let bsl = Bsl::try_from_yang(&bsl).unwrap();
+
+            let bift = BierBift {
+                bsl,
+                nbr: Default::default()
+            };
+
+            bift_cfg.birt.insert(bsl, bift);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Create custom event?
+        })
+        .delete_apply(|context, args| {
+            let bfr_id = args.list_entry.into_bier_cfg_bift().unwrap();
+            let bift_cfg = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+
+            let bsl = args.dnode.get_string_relative("./bsl").unwrap();
+            let bsl = Bsl::try_from_yang(&bsl).unwrap();
+
+            bift_cfg.birt.remove(&bsl);
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Create custom event?
+        })
+        .lookup(|_master, list_entry, dnode| {
+            let bfr_id = list_entry.into_bier_cfg_bift().unwrap();
+
+            let bsl = dnode.get_string_relative("./bsl").unwrap();
+            let bsl = Bsl::try_from_yang(&bsl).unwrap();
+
+            ListEntry::BierCfgBiftBsl(bfr_id, bsl)
+        })
+        .path(bier::bift::birt_bitstringlength::bfr_nbr::PATH)
+        .create_apply(|context, args| {
+            let (bfr_id, bsl) = args.list_entry.into_bier_cfg_bift_bsl().unwrap();
+            let bift_config = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+            let birt = bift_config.birt.get_mut(&bsl).unwrap();
+
+            let bfr_nbr = args.dnode.get_ip_relative("./bfr-nbr").unwrap();
+            let encap_type = args.dnode.get_string_relative("./encapsulation-type").unwrap();
+            let encap_type = BierEncapsulationType::try_from_yang(&encap_type).unwrap();
+            let out_bift_id = args.dnode.get_u32_relative("./out-bift-id/out-bift-id");
+            let out_bift_encoding = args.dnode.get_bool_relative("./out-bift-id/out-bift-id-encoding");
+            let out_bift_id = out_bift_id.map_or(out_bift_encoding.map(BierOutBiftId::Encoding), |v| Some(BierOutBiftId::Defined(v))).unwrap();
+
+            let nbr = BiftNbr {
+                bfr_nbr,
+                encap_type,
+                out_bift_id
+            };
+
+            birt.nbr.insert(bfr_nbr, nbr);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Custom event?
+        })
+        .delete_apply(|context, args| {
+            let (bfr_id, bsl) = args.list_entry.into_bier_cfg_bift_bsl().unwrap();
+            let bift_config = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+            let birt = bift_config.birt.get_mut(&bsl).unwrap();
+
+            let bfr_nbr = args.dnode.get_ip_relative("./bfr-nbr").unwrap();
+
+            birt.nbr.remove(&bfr_nbr);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Custom event?
+        })
+        .lookup(|_context, list_entry, dnode| {
+            let (bfr_id, bsl) = list_entry.into_bier_cfg_bift_bsl().unwrap();
+            let nbr = dnode.get_ip_relative("./bfr-nbr").unwrap();
+
+            ListEntry::BierCfgBiftNbr(bfr_id, bsl, nbr)
+        })
+        .path(bier::bift::birt_bitstringlength::bfr_nbr::encapsulation_type::PATH)
+        .modify_apply(|context, args| {
+            let (bfr_id, bsl, nbr) = args.list_entry.into_bier_cfg_bift_nbr().unwrap();
+
+            let bift_config = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+            let birt = bift_config.birt.get_mut(&bsl).unwrap();
+            let nbr = birt.nbr.get_mut(&nbr).unwrap();
+
+            let encap_type = args.dnode.get_string_relative("./encapsulation-type").unwrap();
+            nbr.encap_type = BierEncapsulationType::try_from_yang(&encap_type).unwrap();
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Custom event?
+        })
+        .delete_apply(|_context, _args| {
+            // Nothing to do.
+        })
+        .path(bier::bift::birt_bitstringlength::bfr_nbr::out_bift_id::out_bift_id::PATH)
+        .modify_apply(|context, args| {
+            let (bfr_id, bsl, nbr) = args.list_entry.into_bier_cfg_bift_nbr().unwrap();
+
+            let bift_config = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+            let birt = bift_config.birt.get_mut(&bsl).unwrap();
+            let nbr = birt.nbr.get_mut(&nbr).unwrap();
+
+            let out_bift_id = args.dnode.get_u32_relative("./out-bift-id/out-bift-id").unwrap();
+            nbr.out_bift_id = BierOutBiftId::Defined(out_bift_id);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Custom event?
+        })
+        .delete_apply(|_context, _args| {
+            // Nothing to do.
+        })
+        .path(bier::bift::birt_bitstringlength::bfr_nbr::out_bift_id::out_bift_id_encoding::PATH)
+        .modify_apply(|context, args| {
+            let (bfr_id, bsl, nbr) = args.list_entry.into_bier_cfg_bift_nbr().unwrap();
+
+            let bift_config = context.bier_config.bift_cfg.get_mut(&bfr_id).unwrap();
+            let birt = bift_config.birt.get_mut(&bsl).unwrap();
+            let nbr = birt.nbr.get_mut(&nbr).unwrap();
+
+            let out_bift_id_encoding = args.dnode.get_bool_relative("./out-bift-id/out-bift-id-encoding").unwrap();
+            nbr.out_bift_id = BierOutBiftId::Encoding(out_bift_id_encoding);
+
+            let event_queue = args.event_queue;
+            event_queue.insert(Event::BierCfgUpdate);
+            // FIXME: Custom event?
+        })
+        .delete_apply(|_context, _args| {
+            // Nothing to do.
+        })
         .build()
 }
 
@@ -1100,6 +1268,9 @@ impl Provider for Master {
                     ));
                 }
             }
+            Event::BierCfgBiftUpdate(_bfr_id) => {
+                // TODO
+            }
         }
     }
 }
diff --git a/holo-routing/src/rib.rs b/holo-routing/src/rib.rs
index 300784f0a..79141eb66 100644
--- a/holo-routing/src/rib.rs
+++ b/holo-routing/src/rib.rs
@@ -10,7 +10,9 @@ use std::net::IpAddr;
 use bitflags::bitflags;
 use chrono::{DateTime, Utc};
 use derive_new::new;
-use holo_utils::bier::{BfrId, BirtEntry, Bsl, SubDomainId};
+use holo_utils::bier::{
+    self, BfrId, Bift, BirtEntry, Bitstring, Bsl, SubDomainId,
+};
 use holo_utils::ibus::{IbusSender, IbusSubscriber};
 use holo_utils::ip::{AddressFamily, IpNetworkExt, Ipv4AddrExt, Ipv6AddrExt};
 use holo_utils::mpls::Label;
@@ -41,9 +43,12 @@ pub struct Rib {
     pub subscriptions: HashMap<usize, RedistributeSub>,
 }
 
-#[derive(Debug, Default)]
+#[derive(Debug)]
 pub struct Birt {
     pub entries: BTreeMap<(SubDomainId, BfrId, Bsl), BirtEntry>,
+    pub bier_update_queue: BTreeSet<BfrId>,
+    pub update_queue_tx: UnboundedSender<()>,
+    pub update_queue_rx: UnboundedReceiver<()>,
 }
 
 #[derive(Clone, Debug, new)]
@@ -85,16 +90,24 @@ impl Birt {
     pub(crate) fn bier_nbr_add(&mut self, msg: BierNbrInstallMsg) {
         let bfr_id = msg.bier_info.bfr_id;
         msg.bier_info.bfr_bss.iter().for_each(|bsl| {
-            if let Some(nexthop) = msg.nexthops.first()
-                && let Nexthop::Address { addr, .. } = nexthop
+            if let Some(nexthop) = msg.nexthops.last()
+                && let Nexthop::Address { addr, ifindex, .. } = nexthop
             {
+                // Insert or update the entry in the BIRT
                 self.entries
                     .entry((msg.bier_info.sd_id, bfr_id, *bsl))
-                    .and_modify(|be| be.bfr_nbr = *addr)
+                    .and_modify(|be| {
+                        be.bfr_nbr = *addr;
+                        be.ifindex = *ifindex;
+                    })
                     .or_insert(BirtEntry {
                         bfr_prefix: msg.prefix.ip(),
                         bfr_nbr: (*addr),
+                        ifindex: *ifindex,
                     });
+
+                // Add BIER route to the update queue
+                self.bier_update_queue_add(bfr_id);
             }
         });
     }
@@ -102,6 +115,79 @@ impl Birt {
     pub(crate) fn bier_nbr_del(&mut self, msg: BierNbrUninstallMsg) {
         let _ = self.entries.remove(&(msg.sd_id, msg.bfr_id, msg.bsl));
     }
+
+    pub(crate) async fn process_birt_update_queue(
+        &mut self,
+        ifaces: &BTreeMap<String, Interface>,
+    ) {
+        let mut bift = Bift::new();
+
+        // Compute Forwarding BitMasks (F-BMs)
+        for ((sd_id, bfr_id, bsl), nbr) in &self.entries {
+            match Bitstring::from(*bfr_id, *bsl) {
+                Ok(bfr_bs) => {
+                    let ifname = ifaces
+                        .iter()
+                        .filter_map(|(_, iface)| {
+                            if iface.ifindex == nbr.ifindex {
+                                Some(iface.ifname.clone())
+                            } else {
+                                None
+                            }
+                        })
+                        .collect::<Vec<String>>();
+                    // Pattern matching is mandatory as Bitstring does not implement Copy, hence cannot use Entry interface
+                    let key = (*sd_id, nbr.bfr_nbr, bfr_bs.si);
+                    match bift.get_mut(&key) {
+                        Some((bitstring, bfrs, _ifindex, _ifname)) => {
+                            match bitstring.mut_or(bfr_bs) {
+                                Ok(()) => {
+                                    bfrs.push((*bfr_id, nbr.bfr_prefix));
+                                }
+                                Err(e) => {
+                                    e.log();
+                                }
+                            }
+                        }
+                        None => {
+                            let _ = bift.insert(
+                                key,
+                                (
+                                    bfr_bs,
+                                    vec![(*bfr_id, nbr.bfr_prefix)],
+                                    nbr.ifindex,
+                                    ifname.first().unwrap().to_owned(),
+                                ),
+                            );
+                        }
+                    }
+                }
+                Err(e) => {
+                    e.log();
+                }
+            }
+        }
+
+        bier::bift_sync(&bift).await;
+    }
+
+    // Adds BIER route to the update queue.
+    fn bier_update_queue_add(&mut self, bfr_id: BfrId) {
+        self.bier_update_queue.insert(bfr_id);
+        let _ = self.update_queue_tx.send(());
+    }
+}
+
+impl Default for Birt {
+    fn default() -> Self {
+        let (update_queue_tx, update_queue_rx) = mpsc::unbounded_channel();
+        Self {
+            entries: Default::default(),
+            bier_update_queue: Default::default(),
+            update_queue_tx,
+            update_queue_rx,
+        }
+    }
 }
 
 // ===== impl Rib =====
diff --git a/holo-utils/Cargo.toml b/holo-utils/Cargo.toml
index 9a9f71ab6..e2122c691 100644
--- a/holo-utils/Cargo.toml
+++ b/holo-utils/Cargo.toml
@@ -31,6 +31,7 @@ tracing.workspace = true
 yang3.workspace = true
 
 holo-yang = { path = "../holo-yang" }
+reqwest = { version = "0.12.8", optional = true }
 
 [lints.rust]
 rust_2018_idioms = "warn"
@@ -42,3 +43,4 @@ manual_range_contains = "allow"
 [features]
 default = []
 testing = []
+fastclick = ["dep:reqwest"]
diff --git a/holo-utils/src/bier.rs b/holo-utils/src/bier.rs
index 5431606df..5b5a3ee56 100644
--- a/holo-utils/src/bier.rs
+++ b/holo-utils/src/bier.rs
@@ -13,11 +13,97 @@ use derive_new::new;
 use holo_yang::{ToYang, TryFromYang};
 use ipnetwork::IpNetwork;
 use serde::{Deserialize, Serialize};
+use tracing::warn;
 
 use crate::ip::AddressFamily;
 
 pub type SubDomainId = u8;
 pub type BfrId = u16;
+pub type SetIdentifier = u8;
+pub type Bift = HashMap<
+    (SubDomainId, IpAddr, SetIdentifier),
+    (Bitstring, Vec<(BfrId, IpAddr)>, u32, String),
+>;
+
+#[cfg(feature = "fastclick")]
+pub async fn bift_sync_fastclick(bift: &Bift) {
+    for ((_sd_id, nbr, _si), (bs, ids, idx, name)) in bift.iter() {
+        // List the position of bits that are enabled in the bitstring, this is
+        // required by the Bitvectors of Fastclick but this not ideal.
+        // FIXME: Find a better way to share a bitstring with Fastclick
+        let bs = bs
+            .bs
+            .iter()
+            .enumerate()
+            .flat_map(|(idx, b)| {
+                (0..8)
+                    .filter_map(|i| {
+                        if b & (1 << i) != 0 {
+                            Some(format!("{}", idx * 8 + i))
+                        } else {
+                            None
+                        }
+                    })
+                    .collect::<Vec<String>>()
+            })
+            .collect::<Vec<String>>()
+            .join(",");
+
+        // Batching BFRs having the same FBM
+        let bfrs = ids
+            .iter()
+            .map(|(id, prefix)| format!("{:#?}_{:#?}", id, prefix))
+            .collect::<Vec<String>>()
+            .join(",");
+        let body = format!("{} {:#?} {} {} {}", bs, nbr, idx, name, bfrs);
+
+        // TODO: Use gRPC rather than plain HTTP
+        let client = reqwest::Client::new();
+        let _res = client
+            // TODO: Make Fastclick BIFT URI configurable through YANG model
+            .post("http://127.0.0.1/bift0/add")
+            .body(body)
+            .send()
+            .await;
+    }
+}
+
+pub async fn bift_sync(_bift: &Bift) {
+    #[cfg(feature = "fastclick")]
+    bift_sync_fastclick(_bift).await;
+}
+
+#[derive(Debug)]
+pub enum Error {
+    InvalidBfrId,
+    InvalidBitstring,
+}
+
+impl Error {
+    pub fn log(&self) {
+        match self {
+            Error::InvalidBfrId => {
+                warn!("{}", self);
+            }
+            Error::InvalidBitstring => {
+                warn!("{}", self);
+            }
+        }
+    }
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Error::InvalidBfrId => {
+                write!(f, "invalid BfrId")
+            }
+            Error::InvalidBitstring => {
+                write!(f, "invalid Bitstring")
+            }
+        }
+    }
+}
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
 #[derive(Deserialize, Serialize)]
@@ -31,6 +117,7 @@ pub struct BierInfo {
 pub struct BirtEntry {
     pub bfr_prefix: IpAddr,
     pub bfr_nbr: IpAddr,
+    pub ifindex: u32,
 }
 
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
@@ -102,10 +189,36 @@ pub struct BierEncapsulation {
     pub in_bift_id: BierInBiftId,
 }
 
+pub type BierOutBiftDefined = u32;
+pub type BierOutBiftEncoding = bool;
+
+#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[derive(Deserialize, Serialize)]
+pub enum BierOutBiftId {
+    Defined(BierOutBiftDefined),
+    Encoding(BierOutBiftEncoding),
+}
+
+#[derive(Clone, Debug)]
+#[derive(Deserialize, Serialize)]
+pub struct BiftNbr {
+    pub bfr_nbr: IpAddr,
+    pub encap_type: BierEncapsulationType,
+    pub out_bift_id: BierOutBiftId,
+}
+
+#[derive(Clone, Debug)]
+#[derive(Deserialize, Serialize)]
+pub struct BierBift {
+    pub bsl: Bsl,
+    pub nbr: BTreeMap<IpAddr, BiftNbr>,
+}
+
 #[derive(Clone, Debug)]
 #[derive(Deserialize, Serialize)]
 pub struct BierBiftCfg {
-    // TODO
+    pub bfr_id: BfrId,
+    pub birt: BTreeMap<Bsl, BierBift>,
 }
 
 #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@@ -185,25 +298,53 @@ impl TryFrom<u8> for Bsl {
 #[derive(Serialize, Deserialize)]
 pub struct Bitstring {
     bsl: Bsl,
-    bs: BytesMut,
+    pub bs: BytesMut,
+    pub si: SetIdentifier,
 }
 
 impl Bitstring {
     pub fn new(bsl: Bsl) -> Self {
+        let len: usize = bsl.into();
         Self {
             bsl,
-            bs: BytesMut::zeroed(bsl.into()),
+            bs: BytesMut::zeroed(len / 8),
+            si: 0,
         }
     }
 
-    pub fn from(id: BfrId, bsl: Bsl) -> Self {
-        // pub fn from(bfr: BfrId) -> Self {
-        // TODO: Ensure value fit in bitstring and use SI if required.
-        let byte = id / 8;
-        let idx = (id % 8) + 1;
+    pub fn or(&self, rhs: Self) -> Result<Self, Error> {
+        if self.si != rhs.si || self.bsl != rhs.bsl {
+            return Err(Error::InvalidBitstring);
+        }
+        let mut ret = Self::new(self.bsl);
+        ret.si = self.si;
+        for i in 0..self.bs.len() {
+            ret.bs[i] = self.bs[i] | rhs.bs[i];
+        }
+        Ok(ret)
+    }
+
+    pub fn mut_or(&mut self, rhs: Self) -> Result<(), Error> {
+        let bs = self.or(rhs)?;
+        self.bs = bs.bs;
+        Ok(())
+    }
+
+    pub fn from(id: BfrId, bsl: Bsl) -> Result<Self, Error> {
+        if id == 0 {
+            return Err(Error::InvalidBfrId);
+        }
         let mut bs = Self::new(bsl);
+        let bsl = usize::from(bsl);
+        bs.si = ((id - 1) as usize / bsl) as u8;
+
+        let id = (id - bs.si as u16 * bsl as u16) - 1;
+
+        let byte = id / 8;
+        let idx = id % 8;
         bs.bs[byte as usize] |= 1 << idx;
-        bs
+
+        Ok(bs)
     }
 
     pub fn bsl(&self) -> Bsl {
@@ -279,3 +420,72 @@ impl ToYang for BierEncapsulationType {
         }
     }
 }
+
+#[cfg(test)]
+mod test_bitstring {
+    use super::*;
+
+    fn get_64bits_bs(id: BfrId) -> Bitstring {
+        let bsl = Bsl::try_from(1).unwrap(); // 64-bit
+        Bitstring::from(id, bsl).unwrap()
+    }
+
+    #[test]
+    fn test_bfr_id_0() {
+        let bsl = Bsl::try_from(1).unwrap();
+        match Bitstring::from(0, bsl) {
+            Ok(_) => {
+                assert!(false);
+            }
+            Err(_) => {}
+        }
+    }
+
+    #[test]
+    fn test_bsl_64bits_id_1() {
+        let bs = get_64bits_bs(1);
+        assert!(bs.si == 0);
+        assert!(bs.bsl == Bsl::_64);
+        println!("{:#x?}", bs.bs);
+        assert!(bs.bs[0] == 0b1);
+        assert!(bs.bs[1] == 0);
+        assert!(bs.bs[2] == 0);
+        assert!(bs.bs[3] == 0);
+        assert!(bs.bs[4] == 0);
+        assert!(bs.bs[5] == 0);
+        assert!(bs.bs[6] == 0);
+        assert!(bs.bs[7] == 0);
+    }
+
+    #[test]
+    fn test_bsl_64bits_si0() {
+        let bs = get_64bits_bs(64);
+        assert!(bs.si == 0);
+        assert!(bs.bsl == Bsl::_64);
+        println!("{:#x?}", bs.bs);
+        assert!(bs.bs[0] == 0);
+        assert!(bs.bs[1] == 0);
+        assert!(bs.bs[2] == 0);
+        assert!(bs.bs[3] == 0);
+        assert!(bs.bs[4] == 0);
+        assert!(bs.bs[5] == 0);
+        assert!(bs.bs[6] == 0);
+        assert!(bs.bs[7] == 0b1000_0000);
+    }
+
+    #[test]
+    fn test_bsl_64bits_si1() {
+        let bs = get_64bits_bs(65);
+        assert!(bs.si == 1);
+        assert!(bs.bsl == Bsl::_64);
+        println!("{:#x?}", bs.bs);
+        assert!(bs.bs[0] == 1);
+        assert!(bs.bs[1] == 0);
+        assert!(bs.bs[2] == 0);
+        assert!(bs.bs[3] == 0);
+        assert!(bs.bs[4] == 0);
+        assert!(bs.bs[5] == 0);
+        assert!(bs.bs[6] == 0);
+        assert!(bs.bs[7] == 0);
+    }
+}
diff --git a/holo-yang/modules/deviations/holo-ietf-bier-deviations.yang b/holo-yang/modules/deviations/holo-ietf-bier-deviations.yang
index 49ac4e9e0..375100f46 100644
--- a/holo-yang/modules/deviations/holo-ietf-bier-deviations.yang
+++ b/holo-yang/modules/deviations/holo-ietf-bier-deviations.yang
@@ -236,56 +236,4 @@ module holo-ietf-bier-deviations {
     deviate not-supported;
   }
   */
-
-  deviation "/rt:routing/bier:bier/bier:bift" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:bfr-id" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bsl" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:bfr-nbr" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:encapsulation-type" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:out-bift-id" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:out-bift-id/bier:out-bift-id" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:out-bift-id/bier:out-bift-id/bier:out-bift-id" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:out-bift-id/bier:out-bift-id/bier:out-bift-id/bier:out-bift-id" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:out-bift-id/bier:out-bift-id/bier:out-bift-id-encoding" {
-    deviate not-supported;
-  }
-
-  deviation "/rt:routing/bier:bier/bier:bift/bier:birt-bitstringlength/bier:bfr-nbr/bier:out-bift-id/bier:out-bift-id/bier:out-bift-id-encoding/bier:out-bift-id-encoding" {
-    deviate not-supported;
-  }
 }


Reviews:
user: login: rwestphal
body: @nrybowski Awesome work! Being able to test the BIER dataplane is super exciting.

The code changes look good to me. I'll be traveling soon, but I'll definitely give this a try when I'm back.

One suggestion: could you gate the FastClick integration behind an optional feature and make reqwest an optional dependency tied to that feature? This might require defining a "fastclick" feature in both the `holo-daemon` and `holo-utils` Cargo.toml files.

The CI errors make no sense and should go away with a simple rebase on top of master.
state: COMMENTED
author_association: MEMBER
submitted_at: 2024-10-22T23:19:45Z
user: login: rwestphal
body: > I propose to move the remaining blocking points in dedicated issues to unlock this PR.

I totally support this. This PR lays a good foundation, and handling the remaining points in separate issues will allow for incremental progress. You've already dealt with enough rebase conflicts, no need for more. Just move this out of draft and I'll merge it!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-14T23:58:47Z
user: login: nrybowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T00:51:23Z
user: login: nrybowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-15T00:51:32Z

Review comments:
diff_hunk: @@ -89,24 +89,28 @@ impl Master {
 
         loop {
             tokio::select! {
-                Some(request) = nb_rx.recv() => {
-                    process_northbound_msg(
-                        self,
-                        &mut resources,
-                        request,
-                    )
-                    .await;
-                }
-                Some(msg) = ibus_rx.recv() => {
-                    ibus::process_msg(self, msg);
-                }
-                Some(_) = self.rib.update_queue_rx.recv() => {
-                    self.rib
-                        .process_rib_update_queue(
-                            &self.netlink_handle,
-                        )
-                        .await;
-                }
+               Some(request) = nb_rx.recv() => {
path: holo-routing/src/lib.rs
user: login: rwestphal
body: nit: unnecessary whitespace changes (unfortunately *rustfmt* ignores everything within the `select!` macro so we need to take care of this ourselves).
created_at: 2025-03-14T23:37:59Z
author_association: MEMBER
diff_hunk: @@ -13,11 +13,98 @@ use derive_new::new;
 use holo_yang::{ToYang, TryFromYang};
 use ipnetwork::IpNetwork;
 use serde::{Deserialize, Serialize};
+use tracing::warn;
 
 use crate::ip::AddressFamily;
 
 pub type SubDomainId = u8;
 pub type BfrId = u16;
+pub type SetIdentifier = u8;
+pub type Bift = HashMap<
+    (SubDomainId, IpAddr, SetIdentifier),
+    (Bitstring, Vec<(BfrId, IpAddr)>, u32, String),
+>;
+
+#[cfg(feature = "fastclick")]
+pub async fn bift_sync_fastclick(bift: &Bift) {
+    for ((_sd_id, nbr, _si), (bs, ids, idx, name)) in bift.iter() {
+        /* List the position of bits that are enabled in the bitstring, this is required by the
path: holo-utils/src/bier.rs
user: login: rwestphal
body: nit: let's stick to C++ style comments (`//`) for consistency with the rest of the codebase. Also, let's try to keep comments within 80 characters for readability.

*rustfmt* has options to handle that automatically (`normalize_comments` and `wrap_comments`), but unfortunately we can't enable them since they are still a bit unstable and unreliable.
created_at: 2025-03-14T23:46:17Z
author_association: MEMBER
diff_hunk: @@ -89,24 +89,28 @@ impl Master {
 
         loop {
             tokio::select! {
-                Some(request) = nb_rx.recv() => {
-                    process_northbound_msg(
-                        self,
-                        &mut resources,
-                        request,
-                    )
-                    .await;
-                }
-                Some(msg) = ibus_rx.recv() => {
-                    ibus::process_msg(self, msg);
-                }
-                Some(_) = self.rib.update_queue_rx.recv() => {
-                    self.rib
-                        .process_rib_update_queue(
-                            &self.netlink_handle,
-                        )
-                        .await;
-                }
+               Some(request) = nb_rx.recv() => {
path: holo-routing/src/lib.rs
user: login: nrybowski
body: Fixed in 26eb5745426fe3a042a1b0a2cabd33d18633449c.
created_at: 2025-03-15T00:51:23Z
author_association: MEMBER
diff_hunk: @@ -13,11 +13,98 @@ use derive_new::new;
 use holo_yang::{ToYang, TryFromYang};
 use ipnetwork::IpNetwork;
 use serde::{Deserialize, Serialize};
+use tracing::warn;
 
 use crate::ip::AddressFamily;
 
 pub type SubDomainId = u8;
 pub type BfrId = u16;
+pub type SetIdentifier = u8;
+pub type Bift = HashMap<
+    (SubDomainId, IpAddr, SetIdentifier),
+    (Bitstring, Vec<(BfrId, IpAddr)>, u32, String),
+>;
+
+#[cfg(feature = "fastclick")]
+pub async fn bift_sync_fastclick(bift: &Bift) {
+    for ((_sd_id, nbr, _si), (bs, ids, idx, name)) in bift.iter() {
+        /* List the position of bits that are enabled in the bitstring, this is required by the
path: holo-utils/src/bier.rs
user: login: nrybowski
body: Fixed in 26eb5745426fe3a042a1b0a2cabd33d18633449c.
created_at: 2025-03-15T00:51:32Z
author_association: MEMBER

PR comments:
user: login: nrybowski
created_at: 2024-10-29T23:50:09Z
body: Rebase on db22407ca752cd381c9f08ac772c51fbfadcf818.
author_association: MEMBER
user: login: nrybowski
created_at: 2025-02-13T15:14:40Z
body: Rebase on 2607d618ff9c93f3a7ea893f9ce0e40652eaa56a
author_association: MEMBER
user: login: nrybowski
created_at: 2025-03-14T19:23:38Z
body: This PR is stalled for quite some time while being functional but with some limitations.
I propose to move the remaining blocking points in dedicated issues to unlock this PR.

The first limitation is that a single BIFT is supported and the URL in FastClick is hard-coded (see https://github.com/holo-routing/holo/pull/32/files#diff-cae564a2ab4253b25c446a576710e228bfcfd5a5dcce4a1acb5b7f46ce5552b0R65).
The BIFTs configuration model defined by draft-ietf-bier-bier-yang-08 has been implemented (see https://github.com/holo-routing/holo/pull/32/files#diff-681d663627a437cad979cc5a0db2adc60b661f7360610b5b90686078d8d1a31eR881-R1043) but I still have to understand how to use this configuration to derive the BIFT ids.

The second limitation is the withdrawal of BIFT entries. Currently, BIRT entries are not correctly removed when routers stop advertising BIER information. Hence, the related BIFT is not updated. Fixing the BIRT update issue should directly solve the BIFT update issue.
author_association: MEMBER
user: login: nrybowski
created_at: 2025-03-14T19:25:18Z
body: Rebase on 36c8118a611b9f3f30dd28d893329694d330da32.
author_association: MEMBER
user: login: nrybowski
created_at: 2025-03-15T00:53:54Z
body: There we go :)
author_association: MEMBER