]> git.ipfire.org Git - thirdparty/bird.git/commitdiff
Bridge: Linux bridge interface - preliminary support
authorOndrej Zajicek <santiago@crfreenet.org>
Mon, 30 Oct 2023 00:50:14 +0000 (01:50 +0100)
committerOndrej Zajicek <santiago@crfreenet.org>
Sat, 14 Jun 2025 20:55:32 +0000 (22:55 +0200)
configure.ac
nest/protocol.h
nest/route.h
nest/rt-attr.c
proto/bridge/Doc [new file with mode: 0644]
proto/bridge/Makefile [new file with mode: 0644]
proto/bridge/bridge.c [new file with mode: 0644]
proto/bridge/bridge.h [new file with mode: 0644]
proto/bridge/config.Y [new file with mode: 0644]
sysdep/linux/netlink.c

index cf6a2d581faed39829d1cfe0d2939e08db6c5451..6935693eacd45d3a0ff6b03ac6261e3be7ddf715 100644 (file)
@@ -314,7 +314,7 @@ if test "$enable_mpls_kernel" != no ; then
   fi
 fi
 
-all_protocols="aggregator $proto_bfd babel bgp l3vpn mrt ospf perf pipe radv rip rpki static"
+all_protocols="aggregator $proto_bfd babel bgp bridge l3vpn mrt ospf perf pipe radv rip rpki static"
 
 all_protocols=`echo $all_protocols | sed 's/ /,/g'`
 
@@ -327,6 +327,7 @@ AH_TEMPLATE([CONFIG_BABEL],         [Babel protocol])
 AH_TEMPLATE([CONFIG_BFD],      [BFD protocol])
 AH_TEMPLATE([CONFIG_BGP],      [BGP protocol])
 AH_TEMPLATE([CONFIG_BMP],      [BMP protocol])
+AH_TEMPLATE([CONFIG_BRIDGE],   [Bridge protocol])
 AH_TEMPLATE([CONFIG_L3VPN],    [L3VPN protocol])
 AH_TEMPLATE([CONFIG_MRT],      [MRT protocol])
 AH_TEMPLATE([CONFIG_OSPF],     [OSPF protocol])
index 97a2ba4065ad18365fd565395c20385f6de77da7..1caa7f75ceb08606294cc016ddba2b79aff7f332 100644 (file)
@@ -45,6 +45,7 @@ enum protocol_class {
   PROTOCOL_BFD,
   PROTOCOL_BGP,
   PROTOCOL_BMP,
+  PROTOCOL_BRIDGE,
   PROTOCOL_DEVICE,
   PROTOCOL_DIRECT,
   PROTOCOL_KERNEL,
@@ -106,7 +107,7 @@ void protos_dump_all(struct dump_request *);
 
 extern struct protocol
   proto_device, proto_radv, proto_rip, proto_static, proto_mrt,
-  proto_ospf, proto_perf, proto_l3vpn, proto_aggregator,
+  proto_ospf, proto_perf, proto_l3vpn, proto_aggregator, proto_bridge,
   proto_pipe, proto_bgp, proto_bmp, proto_bfd, proto_babel, proto_rpki;
 
 /*
index 5a9e7fa16fdb6be123d7711b3c9b4cc0f86ca3f5..656f6658d772fe24a605b6fc76daabac2c92a436 100644 (file)
@@ -484,7 +484,8 @@ typedef struct rta {
 #define RTS_PERF 15                    /* Perf checker */
 #define RTS_L3VPN 16                   /* MPLS L3VPN */
 #define RTS_AGGREGATED 17              /* Aggregated route */
-#define RTS_MAX 18
+#define RTS_BRIDGE 18
+#define RTS_MAX 19
 
 #define RTD_NONE 0                     /* Undefined next hop */
 #define RTD_UNICAST 1                  /* Next hop is neighbor router */
index e10e1ecbf7f4f9f814be1b965800aedfa53af69b..6392fbfe5a9916da1f101a448eb369f09a8ecfa6 100644 (file)
@@ -78,6 +78,7 @@ const char * const rta_src_names[RTS_MAX] = {
   [RTS_PERF]           = "Perf",
   [RTS_L3VPN]          = "L3VPN",
   [RTS_AGGREGATED]     = "aggregated",
+  [RTS_BRIDGE]         = "bridge",
 };
 
 const char * rta_dest_names[RTD_MAX] = {
@@ -1324,7 +1325,7 @@ rta_dump(struct dump_request *dreq, rta *a)
                         "RTS_STAT_DEV", "RTS_REDIR", "RTS_RIP",
                         "RTS_OSPF", "RTS_OSPF_IA", "RTS_OSPF_EXT1",
                         "RTS_OSPF_EXT2", "RTS_BGP", "RTS_PIPE", "RTS_BABEL",
-                        "RTS_RPKI", "RTS_PERF", "RTS_AGGREGATED", };
+                        "RTS_RPKI", "RTS_PERF", "RTS_AGGREGATED", "RTS_BRIDGE" };
   static char *rtd[] = { "", " DEV", " HOLE", " UNREACH", " PROHIBIT" };
 
   RDUMP("pref=%d uc=%d %s %s%s h=%04x",
diff --git a/proto/bridge/Doc b/proto/bridge/Doc
new file mode 100644 (file)
index 0000000..ca0136f
--- /dev/null
@@ -0,0 +1 @@
+S bridge.c
diff --git a/proto/bridge/Makefile b/proto/bridge/Makefile
new file mode 100644 (file)
index 0000000..82755ee
--- /dev/null
@@ -0,0 +1,6 @@
+src := bridge.c
+obj := $(src-o-files)
+$(all-daemon)
+$(cf-local)
+
+tests_objs := $(tests_objs) $(src-o-files)
diff --git a/proto/bridge/bridge.c b/proto/bridge/bridge.c
new file mode 100644 (file)
index 0000000..5e0a848
--- /dev/null
@@ -0,0 +1,285 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023 Ondrej Zajicek <santiago@crfreenet.org>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+/**
+ * DOC: Bridge
+ *
+ * The Bridge protocol is responsible for synchronization of BIRD ethernet
+ * table with Linux kernel bridge interface (although the code is mostly
+ * OS-independent, as Linux-specific parts are in the Netlink code). It is
+ * similar to (and based on) the Kernel protocol, but the differences are
+ * large enough to treat it as an independent protocol.
+ */
+
+/*
+ * TODO:
+ * - Better two-way synchronization, including initial clean-up
+ * - Wait for existence (and active state) of the bridge device
+ * - Check for consistency of vlan_filtering flag
+ * - Channel should be R_ANY for BUM routes, but RA_OPTIMAL for others
+ * - Configuration of VIDs?
+ */
+
+#undef LOCAL_DEBUG
+
+#include "nest/bird.h"
+#include "nest/iface.h"
+#include "nest/protocol.h"
+#include "nest/route.h"
+#include "nest/mpls.h"
+#include "nest/cli.h"
+#include "conf/conf.h"
+#include "filter/filter.h"
+#include "filter/data.h"
+#include "lib/string.h"
+
+#include "bridge.h"
+
+void
+kbr_got_route(struct kbr_proto *p, const net_addr *n, rte *e, int src UNUSED, int scan UNUSED)
+{
+  struct channel *c = p->p.main_channel;
+
+  if (e) e->attrs->pref = c->preference;
+  rte_update2(c, n, e, p->p.main_source);
+}
+
+static void
+kbr_scan(timer *t)
+{
+  struct kbr_proto *p = t->data;
+  struct channel *c = p->p.main_channel;
+
+  TRACE(D_EVENTS, "Scanning bridge table");
+
+  rt_refresh_begin(c->table, c);
+  kbr_do_scan(p);
+  rt_refresh_end(c->table, c);
+}
+
+static void
+kbr_rt_notify(struct proto *P, struct channel *c0 UNUSED, net *net, rte *new, rte *old)
+{
+  struct kbr_proto *p UNUSED = (void *) P;
+
+  rte *new_gw = (new && ipa_nonzero(new->attrs->nh.gw)) ? new : NULL;
+  rte *old_gw = (old && ipa_nonzero(old->attrs->nh.gw)) ? old : NULL;
+
+  /*
+   * This code handles peculiarities of Linux bridge behavior, where the bridge
+   * device has attached both network interfaces and a tunnel (VXLAN) device.
+   * For 'remote' MAC addresses, forwarding entries in the bridge device point
+   * to the tunnel device. The tunnel device has another forwarding table with
+   * forwarding entries, this time with IP addresses of remote endpoints.
+   *
+   * BUM frames are propagated by the bridge device to all attached devices, so
+   * there is no need to have a bridge forwarding entry, but they must have a
+   * tunnel forwarding entry for each destination.
+   */
+
+  if (mac_zero(net_mac_addr(net->n.addr)))
+  {
+    /* For BUM routes, we have multiple tunnel entries, but no bridge entry */
+    kbr_update_fdb(net->n.addr, new_gw, old_gw, 1);
+    return;
+  }
+
+  /* For regular routes, we have one bridge entry, perhaps also one tunnel entry */
+  kbr_replace_fdb(net->n.addr, new, old, 0);
+  kbr_replace_fdb(net->n.addr, new_gw, old_gw, 1);
+}
+
+static inline int
+kbr_is_installed(struct channel *c, net *n)
+{
+  return n->routes && bmap_test(&c->export_map, n->routes->id);
+}
+
+static void
+kbr_flush_routes(struct kbr_proto *p)
+{
+  struct channel *c = p->p.main_channel;
+
+  TRACE(D_EVENTS, "Flushing bridge routes");
+  FIB_WALK(&c->table->fib, net, n)
+  {
+    if (kbr_is_installed(c, n))
+      kbr_rt_notify(&p->p, c, n, NULL, n->routes);
+  }
+  FIB_WALK_END;
+}
+
+
+static int
+kbr_preexport(struct channel *C, rte *e)
+{
+  struct kbr_proto *p = (void *) C->proto;
+
+  /* Reject our own routes */
+  if (e->src->proto == &p->p)
+    return -1;
+
+  return 0;
+}
+
+static void
+kbr_reload_routes(struct channel *C)
+{
+  struct kbr_proto *p = (void *) C->proto;
+
+  tm_start(p->scan_timer, 0);
+}
+
+static inline u32
+kbr_metric(rte *e)
+{
+  u32 metric = ea_get_int(e->attrs->eattrs, EA_GEN_IGP_METRIC, e->attrs->igp_metric);
+  return MIN(metric, IGP_METRIC_UNKNOWN);
+}
+
+static int
+kbr_rte_better(rte *new, rte *old)
+{
+  /* This is hack, we should have full BGP-style comparison */
+  return kbr_metric(new) < kbr_metric(old);
+}
+
+static void
+kbr_postconfig(struct proto_config *CF)
+{
+  struct kbr_config *cf = (void *) CF;
+
+  if (! proto_cf_main_channel(CF))
+    cf_error("Channel not specified");
+
+  if (!cf->bridge_dev)
+    cf_error("Bridge device not specified");
+}
+
+static struct proto *
+kbr_init(struct proto_config *CF)
+{
+  struct proto *P = proto_new(CF);
+  // struct kbr_proto *p = (void *) P;
+  // struct kbr_config *cf = (void *) CF;
+
+  P->main_channel = proto_add_channel(P, proto_cf_main_channel(CF));
+
+  P->rt_notify = kbr_rt_notify;
+  P->preexport = kbr_preexport;
+  P->reload_routes = kbr_reload_routes;
+  P->rte_better = kbr_rte_better;
+
+  return P;
+}
+
+static int
+kbr_start(struct proto *P)
+{
+  struct kbr_proto *p = (void *) P;
+  struct kbr_config *cf = (void *) P->cf;
+
+  p->bridge_dev = cf->bridge_dev;
+  p->vlan_filtering = cf->vlan_filtering;
+
+  p->scan_timer = tm_new_init(p->p.pool, kbr_scan, p, cf->scan_time, 0);
+  tm_start(p->scan_timer, 100 MS);
+
+  kbr_sys_start(p);
+
+  return PS_UP;
+}
+
+static int
+kbr_shutdown(struct proto *P UNUSED)
+{
+  struct kbr_proto *p = (void *) P;
+
+  kbr_flush_routes(p);
+  kbr_sys_shutdown(p);
+
+  return PS_DOWN;
+}
+
+static int
+kbr_reconfigure(struct proto *P, struct proto_config *CF)
+{
+  struct kbr_proto *p = (void *) P;
+  struct kbr_config *cf = (void *) CF;
+
+  if ((p->bridge_dev != cf->bridge_dev) ||
+      (p->vlan_filtering != cf->vlan_filtering))
+    return 0;
+
+  if (!proto_configure_channel(P, &P->main_channel, proto_cf_main_channel(CF)))
+    return 0;
+
+  return 1;
+}
+
+static void
+kbr_copy_config(struct proto_config *dest UNUSED, struct proto_config *src UNUSED)
+{
+  /* Just a shallow copy, not many items here */
+}
+
+const char * const kbr_src_names[KBR_SRC_MAX] = {
+  [KBR_SRC_BIRD]       = "bird",
+  [KBR_SRC_LOCAL]      = "local",
+  [KBR_SRC_STATIC]     = "static",
+  [KBR_SRC_DYNAMIC]    = "dynamic",
+};
+
+static int
+kbr_get_attr(const eattr *a, byte *buf, int buflen UNUSED)
+{
+  switch (a->id)
+  {
+  case EA_KBR_SOURCE:;
+    const char *src = (a->u.data < KBR_SRC_MAX) ? kbr_src_names[a->u.data] : "?";
+    bsprintf(buf, "source: %s", src);
+    return GA_FULL;
+
+  default:
+    return GA_UNKNOWN;
+  }
+}
+
+static void
+kbr_get_route_info(rte *rte, byte *buf)
+{
+  eattr *a = ea_find(rte->attrs->eattrs, EA_KBR_SOURCE);
+  char src = (a && a->u.data < KBR_SRC_MAX) ? "BLSD"[a->u.data] : '?';
+
+  bsprintf(buf, " %c (%u)", src, rte->attrs->pref);
+}
+
+
+struct protocol proto_bridge = {
+  .name =              "Bridge",
+  .template =          "bridge%d",
+  .class =             PROTOCOL_BRIDGE,
+  .channel_mask =      NB_ETH,
+  .proto_size =                sizeof(struct kbr_proto),
+  .config_size =       sizeof(struct kbr_config),
+  .postconfig =                kbr_postconfig,
+  .init =              kbr_init,
+  .start =             kbr_start,
+  .shutdown =          kbr_shutdown,
+  .reconfigure =       kbr_reconfigure,
+  .copy_config =       kbr_copy_config,
+  .get_attr =          kbr_get_attr,
+  .get_route_info =    kbr_get_route_info,
+};
+
+void
+bridge_build(void)
+{
+  proto_build(&proto_bridge);
+}
diff --git a/proto/bridge/bridge.h b/proto/bridge/bridge.h
new file mode 100644 (file)
index 0000000..0022eae
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023 Ondrej Zajicek <santiago@crfreenet.org>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#ifndef _BIRD_BRIDGE_H_
+#define _BIRD_BRIDGE_H_
+
+
+#define EA_KBR_SOURCE          EA_CODE(PROTOCOL_BRIDGE, 0)
+
+#define KBR_SRC_BIRD           0
+#define KBR_SRC_LOCAL          1
+#define KBR_SRC_STATIC         2
+#define KBR_SRC_DYNAMIC                3
+#define KBR_SRC_MAX            4
+
+
+struct kbr_config {
+  struct proto_config c;
+
+  struct iface *bridge_dev;
+  btime scan_time;
+  int vlan_filtering;
+};
+
+struct kbr_proto {
+  struct proto p;
+
+  struct iface *bridge_dev;
+  timer *scan_timer;
+  int vlan_filtering;
+
+  struct kbr_proto *hash_next;
+};
+
+void kbr_got_route(struct kbr_proto *p, const net_addr *n, rte *e, int src, int scan);
+
+
+/* krt sysdep */
+
+int kbr_sys_start(struct kbr_proto *p);
+void kbr_sys_shutdown(struct kbr_proto *p);
+
+void kbr_replace_fdb(const net_addr *n, rte *new, rte *old, int tunnel);
+void kbr_update_fdb(const net_addr *n, rte *new, rte *old, int tunnel);
+void kbr_do_scan(struct kbr_proto *p);
+
+#endif
diff --git a/proto/bridge/config.Y b/proto/bridge/config.Y
new file mode 100644 (file)
index 0000000..f978bda
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023 Ondrej Zajicek <santiago@crfreenet.org>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+CF_HDR
+
+#include "proto/bridge/bridge.h"
+
+
+CF_DEFINES
+
+#define KBR_CFG ((struct kbr_config *) this_proto)
+
+
+CF_DECLS
+
+CF_KEYWORDS(BRIDGE, BRIDGE, DEVICE, VLAN, FILTERING, SCAN, TIME, KBR_SOURCE)
+
+
+CF_GRAMMAR
+
+proto: kbr_proto;
+
+
+kbr_proto_start: proto_start BRIDGE
+{
+  this_proto = proto_config_new(&proto_bridge, $1);
+  this_proto->net_type = NET_ETH;
+
+  KBR_CFG->scan_time = 60 S_;
+};
+
+kbr_proto_item:
+   proto_item
+ | proto_channel { $1->ra_mode = RA_ANY; }
+ | BRIDGE DEVICE text { KBR_CFG->bridge_dev = if_get_by_name($3); }
+ | VLAN FILTERING bool { KBR_CFG->vlan_filtering = $3; }
+ | SCAN TIME expr_us { KBR_CFG->scan_time = $3; }
+ ;
+
+kbr_proto_opts:
+   /* empty */
+ | kbr_proto_opts kbr_proto_item ';'
+ ;
+
+kbr_proto:
+   kbr_proto_start proto_name '{' kbr_proto_opts '}';
+
+
+dynamic_attr: KBR_SOURCE { $$ = f_new_dynamic_attr(EAF_TYPE_INT, T_INT, EA_KBR_SOURCE); } ;
+
+CF_CODE
+
+CF_END
index 299f132fec9192d0e887c2ba9ebb15eedab7f13b..4798944b0646ca7e0664a2c52cc5b20ef16beb2f 100644 (file)
@@ -27,6 +27,8 @@
 #include "lib/hash.h"
 #include "conf/conf.h"
 
+#include "proto/bridge/bridge.h"
+
 #include CONFIG_INCLUDE_NLSYS_H
 
 #define krt_ipv4(p) ((p)->af == AF_INET)
@@ -40,6 +42,7 @@ struct nl_parse_state
   int scan;
 
   u32 rta_flow;
+  u32 bridge_id;
 };
 
 /*
@@ -210,6 +213,34 @@ nl_request_dump_route(int af, int table_id)
   nl_scan.last_hdr = NULL;
 }
 
+static void
+nl_request_dump_neigh(int af, int bridge_id)
+{
+  struct {
+    struct nlmsghdr nh;
+    struct ndmsg ndm;
+    struct rtattr rta;
+    u32 master_id;
+  } req = {
+    .nh.nlmsg_type = RTM_GETNEIGH,
+    .nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ndmsg)),
+    .nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
+    .nh.nlmsg_seq = ++(nl_scan.seq),
+    .ndm.ndm_family = af,
+  };
+
+  if (bridge_id)
+  {
+    req.rta.rta_type = NDA_MASTER;
+    req.rta.rta_len = RTA_LENGTH(4);
+    req.master_id = bridge_id;
+    req.nh.nlmsg_len = NLMSG_ALIGN(req.nh.nlmsg_len) + req.rta.rta_len;
+  }
+
+  send(nl_scan.fd, &req, req.nh.nlmsg_len, 0);
+  nl_scan.last_hdr = NULL;
+}
+
 
 static struct nlmsghdr *
 nl_get_reply(struct nl_sock *nl)
@@ -444,6 +475,16 @@ static struct nl_want_attrs rtm_attr_want_mpls[BIRD_RTA_MAX] = {
 #endif
 
 
+#define BIRD_NDA_MAX  (NDA_MASTER+1)
+
+static struct nl_want_attrs ndm_attr_want[BIRD_NDA_MAX] = {
+  [NDA_LLADDR]   = { 1, 1, sizeof(mac_addr) },
+  [NDA_VLAN]     = { 1, 1, sizeof(u16) },
+  [NDA_VNI]      = { 1, 1, sizeof(u32) },
+  [NDA_MASTER]   = { 1, 1, sizeof(u32) },
+};
+
+
 static int
 nl_parse_attrs(struct rtattr *a, struct nl_want_attrs *want, struct rtattr **k, int ksize)
 {
@@ -496,6 +537,9 @@ static inline ip_addr rta_get_ipa(struct rtattr *a)
     return ipa_from_ip6(rta_get_ip6(a));
 }
 
+static inline mac_addr rta_get_mac(struct rtattr *a)
+{ return *(mac_addr *) RTA_DATA(a); }
+
 static inline ip_addr rta_get_via(struct rtattr *a)
 {
   struct rtvia *v = RTA_DATA(a);
@@ -1281,7 +1325,7 @@ static HASH(struct krt_proto) nl_table_map;
 #define RTH_FN(a,i)            a ^ u32_hash(i)
 
 #define RTH_REHASH             rth_rehash
-#define RTH_PARAMS             /8, *2, 2, 2, 6, 20
+#define RTH_PARAMS             /8, *1, 2, 2, 4, 20
 
 HASH_DEFINE_REHASH_FN(RTH, struct krt_proto)
 
@@ -1939,6 +1983,207 @@ krt_do_scan(struct krt_proto *p)
   }
 }
 
+
+/*
+ *     FDB entries
+ */
+
+static inline u32
+kbr_bridge_id(struct kbr_proto *p)
+{
+  return p->bridge_dev->index;
+}
+
+static HASH(struct kbr_proto) nl_bridge_map;
+
+#define BRH_KEY(p)             kbr_bridge_id(p)
+#define BRH_NEXT(p)            p->hash_next
+#define BRH_EQ(i1,i2)          i1 == i2
+#define BRH_FN(i)              u32_hash(i)
+
+#define BRH_REHASH             brh_rehash
+#define BRH_PARAMS             /8, *1, 2, 2, 4, 20
+
+HASH_DEFINE_REHASH_FN(BRH, struct kbr_proto);
+
+static int
+nl_send_fdb(const net_addr *n0, rte *e, int op, int tunnel)
+{
+  const net_addr_eth *n = (void *) n0;
+
+  struct {
+    struct nlmsghdr h;
+    struct ndmsg n;
+    char buf[0];
+  } *r;
+
+  int rsize = sizeof(*r) + 256;
+  r = alloca(rsize);
+
+  memset(&r->h, 0, sizeof(r->h));
+  memset(&r->n, 0, sizeof(r->n));
+  r->h.nlmsg_type = op ? RTM_NEWNEIGH : RTM_DELNEIGH;
+  r->h.nlmsg_len = NLMSG_LENGTH(sizeof(struct ndmsg));
+  r->h.nlmsg_flags = op | NLM_F_REQUEST | NLM_F_ACK;
+
+  r->n.ndm_family = AF_BRIDGE;
+  r->n.ndm_state = NUD_NOARP;
+  r->n.ndm_flags = (tunnel ? NTF_SELF : NTF_MASTER) | NTF_EXT_LEARNED;
+
+  nl_add_attr(&r->h, rsize, NDA_LLADDR, n->mac.addr, 6);
+
+  if (n->vid)
+    nl_add_attr_u16(&r->h, rsize, NDA_VLAN, n->vid);
+
+  struct nexthop *nh = &e->attrs->nh;
+  ASSERT(e->attrs->dest == RTD_UNICAST && !nh->next);
+  r->n.ndm_ifindex = nh->iface->index;
+
+  if (tunnel)
+  {
+    ASSERT(ipa_nonzero(nh->gw));
+    nl_add_attr_ipa(&r->h, rsize, NDA_DST, nh->gw);
+
+    if (nh->labels)
+      nl_add_attr_u32(&r->h, rsize, NDA_VNI, nh->label[0]);
+
+    r->n.ndm_state |= NUD_PERMANENT;
+  }
+
+  /* Ignore missing for DELETE */
+  return nl_exchange(&r->h, (op == NL_OP_DELETE));
+}
+
+void
+kbr_replace_fdb(const net_addr *n, rte *new, rte *old, int tunnel)
+{
+  int err = 0;
+
+  if (old && new)
+  {
+    err = nl_send_fdb(n, new, NL_OP_REPLACE, tunnel);
+  }
+  else
+  {
+    if (old)
+      nl_send_fdb(n, old, NL_OP_DELETE, tunnel);
+
+    if (new)
+      err = nl_send_fdb(n, new, NL_OP_ADD, tunnel);
+  }
+
+  if (err < 0)
+    log(L_WARN "NL error %m");
+}
+
+void
+kbr_update_fdb(const net_addr *n, rte *new, rte *old, int tunnel)
+{
+  int err = 0;
+
+  if (old)
+    nl_send_fdb(n, old, NL_OP_DELETE, tunnel);
+
+  if (new)
+    err = nl_send_fdb(n, new, NL_OP_APPEND, tunnel);
+
+  if (err < 0)
+    log(L_WARN "NL error %m");
+}
+
+#ifndef NDM_RTA
+#define NDM_RTA(n) (struct rtattr *)(((char *) n) + NLMSG_ALIGN(sizeof(struct ndmsg)))
+#endif
+
+static void
+nl_parse_fdb(struct nl_parse_state *s, struct nlmsghdr *h)
+{
+  struct ndmsg *nd;
+  struct rtattr *a[BIRD_NDA_MAX];
+  int new = (h->nlmsg_type == RTM_NEWNEIGH);
+
+  if (!(nd = nl_checkin(h, sizeof(*nd))))
+    return;
+
+  if (nd->ndm_family != AF_BRIDGE)
+    return;
+
+  if (!nl_parse_attrs(NDM_RTA(nd), ndm_attr_want, a, sizeof(a)))
+    return;
+
+  if (!a[NDA_LLADDR] || !a[NDA_MASTER])
+    return;
+
+  uint vid = a[NDA_VLAN] ? rta_get_u16(a[NDA_VLAN]) : 0;
+
+  net_addr n;
+  net_fill_eth(&n, rta_get_mac(a[NDA_LLADDR]), vid);
+
+  u32 bridge_id = rta_get_u32(a[NDA_MASTER]);
+
+  /* Should be filtered by kernel */
+  if (s->bridge_id && (bridge_id != s->bridge_id))
+    return;
+
+  /* Do we know this bridge? */
+  struct kbr_proto *p = HASH_FIND(nl_bridge_map, BRH, bridge_id);
+  if (!p)
+    return;
+
+  /* Accept VLAN-tagged entries when vlan filtering is enabled */
+  if (!vid != !p->vlan_filtering)
+    return;
+
+  struct iface *oif = if_find_by_index(nd->ndm_ifindex);
+  if (!oif)
+    return;
+
+  rta ra = {
+    .source = RTS_BRIDGE,
+    .scope = SCOPE_UNIVERSE,
+    .dest = RTD_UNICAST,
+    .nh.iface = oif,
+  };
+
+  int src;
+  if (nd->ndm_flags & NTF_EXT_LEARNED)
+    // src = KBR_SRC_BIRD;
+    return;
+  else if (nd->ndm_state & NUD_PERMANENT)
+    src = KBR_SRC_LOCAL;
+  else if (nd->ndm_state & NUD_NOARP)
+    src = KBR_SRC_STATIC;
+  else
+    src = KBR_SRC_DYNAMIC;
+
+  ea_set_attr_u32(&ra.eattrs, s->pool, EA_KBR_SOURCE, 0, EAF_TYPE_INT, src);
+
+  rte *e = new ? rte_get_temp(&ra, p->p.main_source) : NULL;
+
+  DBG("FDB %s %N dev %s [%x %x %x]\n", (new ? "add" : "del"), &n, oif->name, nd->ndm_state, nd->ndm_flags, nd->ndm_type);
+  kbr_got_route(p, &n, e, src, s->scan);
+}
+
+void
+kbr_do_scan(struct kbr_proto *p)
+{
+  struct nl_parse_state s = {
+    .pool = nl_linpool,
+    .scan = 1,
+    .bridge_id = kbr_bridge_id(p),
+  };
+
+  nl_request_dump_neigh(AF_BRIDGE, kbr_bridge_id(p));
+
+  struct nlmsghdr *h;
+  while (h = nl_get_scan())
+  {
+    if (h->nlmsg_type == RTM_NEWNEIGH || h->nlmsg_type == RTM_DELNEIGH)
+      nl_parse_fdb(&s, h);
+  }
+}
+
+
 /*
  *     Asynchronous Netlink interface
  */
@@ -1964,18 +2209,25 @@ nl_async_msg(struct nlmsghdr *h)
       DBG("KRT: Received async route notification (%d)\n", h->nlmsg_type);
       nl_parse_route(&s, h);
       break;
+
     case RTM_NEWLINK:
     case RTM_DELLINK:
       DBG("KRT: Received async link notification (%d)\n", h->nlmsg_type);
       if (kif_proto)
        nl_parse_link(h, 0);
       break;
+
     case RTM_NEWADDR:
     case RTM_DELADDR:
       DBG("KRT: Received async address notification (%d)\n", h->nlmsg_type);
       if (kif_proto)
        nl_parse_addr(h, 0);
       break;
+
+    case RTM_NEWNEIGH:
+    case RTM_DELNEIGH:
+      nl_parse_fdb(&s, h);
+
     default:
       DBG("KRT: Received unknown async notification (%d)\n", h->nlmsg_type);
     }
@@ -2062,7 +2314,7 @@ nl_open_async(void)
 
   bzero(&sa, sizeof(sa));
   sa.nl_family = AF_NETLINK;
-  sa.nl_groups = RTMGRP_LINK |
+  sa.nl_groups = RTMGRP_LINK | RTMGRP_NEIGH |
     RTMGRP_IPV4_IFADDR | RTMGRP_IPV4_ROUTE |
     RTMGRP_IPV6_IFADDR | RTMGRP_IPV6_ROUTE;
 
@@ -2119,7 +2371,8 @@ void
 krt_sys_io_init(void)
 {
   nl_linpool = lp_new_default(krt_pool);
-  HASH_INIT(nl_table_map, krt_pool, 6);
+  HASH_INIT(nl_table_map, krt_pool, 4);
+  HASH_INIT(nl_bridge_map, krt_pool, 4);
 }
 
 int
@@ -2223,6 +2476,32 @@ krt_sys_get_attr(const eattr *a, byte *buf, int buflen UNUSED)
 }
 
 
+int
+kbr_sys_start(struct kbr_proto *p)
+{
+  struct kbr_proto *old = HASH_FIND(nl_bridge_map, BRH, kbr_bridge_id(p));
+
+  if (old)
+  {
+    log(L_ERR "%s: Bridge device %s already registered by %s",
+       p->p.name, p->bridge_dev->name, old->p.name);
+    return 0;
+  }
+
+  HASH_INSERT2(nl_bridge_map, BRH, krt_pool, p);
+
+  nl_open();
+  nl_open_async();
+
+  return 1;
+}
+
+void
+kbr_sys_shutdown(struct kbr_proto *p)
+{
+  HASH_REMOVE2(nl_bridge_map, BRH, krt_pool, p);
+}
+
 
 void
 kif_sys_start(struct kif_proto *p UNUSED)