]> 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)
committerMaria Matejka <mq@ucw.cz>
Fri, 15 May 2026 16:29:06 +0000 (18:29 +0200)
The Bridge protocol synchronizes BIRD eth table with Linux kernel bridge
forwarding table. It works analogously to the Kernel protocol, but for
ethernet FDB entries instead of IP routes. The instance of Bridge
protocol is associated with the specific Linux bridge device.

The Bridge protocol handles not only bridge forwarding entries, but also
VXLAN forwarding entries, as Linux kernel VXLAN tunnel device manages its
own forwarding table, with IPs of remote endpoints.

Note that we use ethernet route next_hop to store VXLAN forwarding
address, instead of dedicated route attribute. That is preliminary and
will change in the future.

The Bridge protocol uses netlink to scan VLANs on associated interfaces,
listens to vlan requests from other protocols (EVPN), keeps state of
both desired and actual state of VLANs, and configure VLANs on managed
interfaces according to received requests.

14 files changed:
configure.ac
filter/data.h
nest/protocol.h
nest/route.h
nest/rt-attr.c
nest/rt-table.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/bsd-netlink/netlink-sys.h
sysdep/linux/netlink-sys.h
sysdep/linux/netlink.c

index 054e9c073184d047018c486bbd483d3bad63e038..8c1792b77996e72d10b3ae003b79a8a9f3d34c8c 100644 (file)
@@ -235,6 +235,7 @@ else
     linux*)
       sysdesc=linux
       default_iproutedir="/etc/iproute2"
+      proto_bridge=bridge
       ;;
     freebsd*)
       sysdesc=bsd
@@ -319,7 +320,7 @@ if test "$enable_mpls_kernel" != no ; then
   fi
 fi
 
-all_protocols="aggregator $proto_bfd babel bgp bmp l3vpn mrt ospf perf pipe radv rip rpki static"
+all_protocols="aggregator $proto_bfd babel bgp bmp $proto_bridge l3vpn mrt ospf perf pipe radv rip rpki static"
 
 all_protocols=`echo $all_protocols | sed 's/ /,/g'`
 
@@ -332,6 +333,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 c90d5c231c47ee627d17dda74143abbbee1d0975..2cae9412dbb4748163904959d087c98c6bb6502c 100644 (file)
@@ -44,6 +44,7 @@ enum f_type {
   T_ENUM_AF = 0x38,
   T_ENUM_MPLS_POLICY = 0x39,
   T_ENUM_NET_EVPN_TYPE = 0x3a,
+  T_ENUM_KBR_SOURCE = 0x3b,
 
 /* new enums go here */
   T_ENUM_EMPTY = 0x3f, /* Special hack for atomic_aggr */
index 9ab3818a2ab11202fb50607b5557a6e1e3d809f7..443b578698c2d175fa0be80f03e561febfedf245 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 0336f67cab67861346519d05fd4469abc1b4096d..da61f96937cd134a3fe1b8e2a93fa9c3e8517edb 100644 (file)
@@ -326,6 +326,7 @@ rte *rte_find(net *net, struct rte_src *src);
 rte *rte_get_temp(struct rta *, struct rte_src *src);
 void rte_update2(struct channel *c, const net_addr *n, rte *new, struct rte_src *src);
 /* rte_update() moved to protocol.h to avoid dependency conflicts */
+rte *rte_find_old_best(net *net, rte *new, rte *old);
 int rt_examine(rtable *t, net_addr *a, struct channel *c, const struct filter *filter);
 rte *rt_export_merged(struct channel *c, net *net, rte **rt_free, linpool *pool, int silent);
 void rt_refresh_begin(rtable *t, struct channel *c);
@@ -485,7 +486,8 @@ typedef struct rta {
 #define RTS_L3VPN 16                   /* MPLS L3VPN */
 #define RTS_AGGREGATED 17              /* Aggregated route */
 #define RTS_RADV 18                    /* Router Advertisement */
-#define RTS_MAX 19
+#define RTS_BRIDGE 19
+#define RTS_MAX 20
 
 #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",
index 3ea3c00fa16e000b743fb91ff6c1ce50bfcda384..42aa31b6844e09a18c62355aa4b8feb89e2eeee6 100644 (file)
@@ -1580,6 +1580,19 @@ rte_recalculate(struct channel *c, net *net, rte *new, struct rte_src *src)
     }
 }
 
+rte *
+rte_find_old_best(net *net, rte *new, rte *old)
+{
+  u32 new_id = new ? new->id : 0;
+  rte *old_best = old;
+
+  for (rte *r = net->routes; r; r=r->next)
+    if ((r->id != new_id) && rte_better(r, old_best))
+      old_best = r;
+
+  return rte_is_valid(old_best) ? old_best: NULL;
+}
+
 static int rte_update_nest_cnt;                /* Nesting counter to allow recursive updates */
 
 static inline void
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..2431360
--- /dev/null
@@ -0,0 +1,951 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023--2026 Ondrej Zajicek <santiago@crfreenet.org>
+ *     (c) 2023--2026 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:
+ * - Wait for existence (and active state) of the bridge device
+ * - Channel should be R_ANY for BUM routes, but RA_OPTIMAL for others
+ */
+
+#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 "sysdep/unix/krt.h"
+
+#include "bridge.h"
+
+static struct kbr_vlan * kbr_find_vlan(struct kbr_proto *p, uint ifi, uint vid);
+static void kbr_prune_vlans0(struct kbr_proto *p);
+static void kbr_prune_vlans1(struct kbr_proto *p);
+
+
+/*
+ *     Bridge entries
+ */
+
+static void
+kbr_trace(struct kbr_proto *p, const net_addr *n, rte *e, char *msg, bool tunnel)
+{
+  if (p->p.debug & D_PACKETS)
+  {
+    struct nexthop *nh = &e->attrs->nh;
+
+    if (!tunnel || ipa_zero(nh->gw))
+      log(L_TRACE "%s: %N dev %s: %s",
+         p->p.name, n, nh->iface->name, msg);
+    else
+      log(L_TRACE "%s: %N dev %s dst %I vni %u: %s",
+         p->p.name, n, nh->iface->name, nh->gw, (uint) nh->label[0], msg);
+  }
+}
+
+static inline void
+kbr_trace_in(struct kbr_proto *p, const net_addr *n, rte *e, char *msg)
+{
+  if (!e)
+    return;
+
+  kbr_trace(p, n, e, msg, true);
+}
+
+static void
+kbr_trace_out(struct kbr_proto *p, const net_addr *n, rte *new, rte *old, bool tunnel)
+{
+  if (new)
+    kbr_trace(p, n, new, (old ? "updating" : "installing"), tunnel);
+  else if (old)
+    kbr_trace(p, n, old, "deleting", tunnel);
+}
+
+static bool
+kbr_match_fdb(const rte *x, const rte *y)
+{
+  if (x->attrs->dest != RTD_UNICAST)
+    return false;
+
+  struct nexthop *xn = &x->attrs->nh, *yn = &y->attrs->nh;
+  return (xn->iface == yn->iface) && ipa_equal(xn->gw, yn->gw);
+}
+
+/* Find matching route in the BIRD table */
+static struct rte *
+kbr_find_fdb(struct kbr_proto *p UNUSED, struct channel *c, const net_addr *n, const rte *ref)
+{
+  net *net = net_find(c->table, n);
+  if (!net)
+    return NULL;
+
+  /* For BUM routes, find matching entry */
+  if (mac_zero(net_mac_addr(n)))
+  {
+    for (rte *rt = net->routes; rt; rt = rt->next)
+      if (rte_is_valid(rt) && kbr_match_fdb(rt, ref))
+       return rt;
+
+    return NULL;
+  }
+
+  /* For regular routes, just return the best route */
+  rte *rt = net->routes;
+  return rte_is_valid(rt) ? rt : NULL;
+}
+
+/* Export BIRD route through filters */
+static struct rte *
+kbr_export_fdb(struct kbr_proto *p UNUSED, struct channel *c, rte *rt0, rte **rt_free)
+{
+  rte *rt = rt0;
+  *rt_free = NULL;
+
+  /* Route should be already exported */
+  if (!bmap_test(&c->export_map, rt->id))
+    return NULL;
+
+  /* We could run krt_preexport() here, but it is already handled by export_map */
+  const struct filter *filter = c->out_filter;
+
+  if (filter == FILTER_REJECT)
+    return NULL;
+
+  if (filter == FILTER_ACCEPT)
+    goto accept;
+
+  if (f_run(filter, &rt, tmp_linpool, FF_SILENT) > F_ACCEPT)
+    goto reject;
+
+accept:
+  if (rt != rt0)
+    *rt_free = rt;
+  return rt;
+
+reject:
+  if (rt != rt0)
+    rte_free(rt);
+  return NULL;
+}
+
+
+/*
+ * bird async -> ignore
+ * bird scan -> review
+ * non-bird bridge -> import
+ * non-bird tunnel -> ignore
+ */
+
+/* Compare if two routes have the same destination */
+static bool
+kbr_same_dest(const rte *x, const rte *y, bool tunnel)
+{
+  rta *xa = x->attrs, *ya = y->attrs;
+
+  if (xa->dest != ya->dest)
+    return false;
+
+  if (xa->dest != RTD_UNICAST)
+    return true;
+
+  if (tunnel)
+    return nexthop_equal(&(xa->nh), &(ya->nh));
+  else
+    /* Bridge fdb entries only contain ifaces */
+    return xa->nh.iface == ya->nh.iface;
+}
+
+/* Process FDB entry received from the kernel */
+void
+kbr_got_fdb(struct kbr_proto *p, const net_addr *n, rte *e, int src, bool scan, bool tunnel)
+{
+  struct channel *c = p->p.main_channel;
+  rte *rt_free = NULL;
+
+  /* Non-BIRD routes are just imported, both scan and async */
+  if (src != KBR_SRC_BIRD)
+  {
+    /* Import of tunnel FDBs is not supported */
+    if (tunnel)
+      goto skip;
+
+    if (e) e->attrs->pref = c->preference;
+    rte_update2(c, n, e, p->p.main_source);
+    return;
+  }
+
+  /* BIRD routes are silently ignored in async, that is just echo */
+  if (!scan)
+    goto skip;
+
+  /* BIRD routes received during scan */
+  /* We wait for the initial feed to have correct export state */
+  if (!p->ready)
+    goto ignore;
+
+  rte *rt0 = kbr_find_fdb(p, c, n, e);
+
+  /* No matching route in BIRD table */
+  if (!rt0)
+    goto delete;
+
+  rte *new = kbr_export_fdb(p, c, rt0, &rt_free);
+
+  /* Rejected by filters */
+  if (!new)
+    goto delete;
+
+  /*
+   * One BIRD route can represent two kernel FDB entries, bridge entry and tunnel
+   * entry (distinguished by tunnel arg). See kbr_rt_notify() for details.
+   *
+   * In short:
+   * MAC is non-zero <-> there should be bridge entry
+   * gateway is non-zero <-> there should be tunnel entry
+   * (We only check <- direction here)
+   */
+
+  if (!tunnel && mac_zero(net_mac_addr(n)))
+    goto delete;
+
+  if (tunnel && ipa_zero(new->attrs->nh.gw))
+    goto delete;
+
+  /* Route to this destination was already seen. Strange, but it happens... */
+  if (bmap_test(&p->seen_map[tunnel], new->id))
+    goto aseen;
+
+  /* Mark route as seen */
+  bmap_set(&p->seen_map[tunnel], new->id);
+
+  /* TODO: There also may be changes in route eattrs, we ignore that for now */
+  if (!bmap_test(&p->sync_map[tunnel], new->id) || !kbr_same_dest(e, new, tunnel))
+    goto update;
+
+  goto seen;
+
+seen:
+  kbr_trace_in(p, n, e, "seen");
+  goto done;
+
+aseen:
+  kbr_trace_in(p, n, e, "already seen");
+  goto done;
+
+ignore:
+  kbr_trace_in(p, n, e, "ignored");
+  goto done;
+
+update:
+  kbr_trace_in(p, n, new, "updating");
+  if (mac_zero(net_mac_addr(n)))
+    kbr_update_fdb(p, n, new, e, tunnel);
+  else
+    kbr_replace_fdb(p, n, new, e, tunnel);
+  goto done;
+
+delete:
+  kbr_trace_in(p, n, e, "deleting");
+  if (mac_zero(net_mac_addr(n)))
+    kbr_update_fdb(p, n, NULL, e, tunnel);
+  else
+    kbr_replace_fdb(p, n, NULL, e, tunnel);
+  goto done;
+
+skip:
+done:
+  if (e)
+    rte_free(e);
+
+  if (rt_free)
+    rte_free(rt_free);
+}
+
+/* Install missing FDB entries during pruning phase */
+static void
+kbr_prune_fdb(struct kbr_proto *p, struct channel *c, const net_addr *n, rte *e)
+{
+  rte *rt_free = NULL;
+  rte *new = kbr_export_fdb(p, c, e, &rt_free);
+
+  if (!new)
+    return;
+
+  if (!mac_zero(net_mac_addr(n)) &&
+      !bmap_test(&p->seen_map[0], new->id))
+  {
+    kbr_trace(p, n, new, "installing", false);
+    kbr_replace_fdb(p, n, new, NULL, false);
+  }
+
+  if (ipa_nonzero(new->attrs->nh.gw) &&
+      !bmap_test(&p->seen_map[1], new->id))
+  {
+    kbr_trace(p, n, new, "installing", true);
+    if (mac_zero(net_mac_addr(n)))
+      kbr_update_fdb(p, n, new, NULL, true);
+    else
+      kbr_replace_fdb(p, n, new, NULL, true);
+  }
+
+  if (rt_free)
+    rte_free(rt_free);
+}
+
+static void
+kbr_prune_fdbs(struct kbr_proto *p)
+{
+  struct channel *c = p->p.main_channel;
+  struct rtable *t = c->table;
+
+  /* Should not happen */
+  if (!p->ready)
+    return;
+
+  TRACE(D_EVENTS, "Prunning bridge table");
+
+  FIB_WALK(&t->fib, net, net)
+  {
+    const net_addr *n = net->n.addr;
+
+    if (mac_zero(net_mac_addr(n)))
+    {
+      /* For BUM routes, handle all tunnel entries */
+      for (rte *rt = net->routes; rt; rt = rt->next)
+       if (rte_is_valid(rt))
+         kbr_prune_fdb(p, c, n, rt);
+    }
+    else
+    {
+      /* For regular routes, handle only the best route */
+      if (rte_is_valid(net->routes))
+       kbr_prune_fdb(p, c, n, net->routes);
+    }
+  }
+  FIB_WALK_END;
+}
+
+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");
+
+  bmap_reset(&p->seen_map[0], 1024);
+  bmap_reset(&p->seen_map[1], 1024);
+
+  if (p->vlan_filtering)
+  {
+    kbr_do_vlan_scan(p);
+    kbr_prune_vlans0(p);
+  }
+
+  rt_refresh_begin(c->table, c);
+  kbr_do_fdb_scan(p);
+  kbr_prune_fdbs(p);
+  rt_refresh_end(c->table, c);
+
+  if (p->vlan_filtering)
+    kbr_prune_vlans1(p);
+
+  if (!p->synced)
+    TRACE(D_EVENTS, "Synced");
+
+  p->synced = true;
+}
+
+static void
+kbr_rt_notify(struct proto *P, struct channel *c, net *net, rte *new, rte *old)
+{
+  struct kbr_proto *p = (void *) P;
+  const net_addr *n = net->n.addr;
+
+  /* Before first scan we do not touch the routes */
+  if (!p->synced)
+    return;
+
+  rte *r = (new ?: old);
+  rte *new_gw = (new && ipa_nonzero(new->attrs->nh.gw)) ? new : NULL;
+  rte *old_gw = (old && ipa_nonzero(old->attrs->nh.gw)) ? old : NULL;
+
+  /* For managed interfaces we ignore FDB entries when VLAN not active */
+  /* For now, we assume EVPN FDB entry <-> direct to managed iface */
+#ifdef CONFIG_EVPN
+  if (p->vlan_filtering && (r->attrs->source == RTS_EVPN))
+  {
+    struct iface *i = r->attrs->nh.iface;
+    struct kbr_vlan *v = kbr_find_vlan(p, i->index, net_vlan_id(n));
+
+    if (!v || !v->active)
+    {
+      TRACE(D_ROUTES, "%N %s ignored - vlan %u not active on %s",
+           n, (new ? "update" : "withdraw"), net_vlan_id(n), i->name);
+      return;
+    }
+  }
+#endif
+
+  /*
+   * 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(n)))
+  {
+    /* For BUM routes, we have multiple tunnel entries, but no bridge entry */
+    kbr_trace_out(p, n, new_gw, old_gw, 1);
+    kbr_update_fdb(p, n, new_gw, old_gw, 1);
+    return;
+  }
+
+  /*
+   * or regular routes, we need RA_OPTIMAL, but we are stuck with RA_ANY, so we
+   * need to find out new_best / old_best and replay rt_notify_basic()/
+   */
+  rte *rt_free = NULL;
+  rte *new_best = rte_is_valid(net->routes) ? net->routes : NULL;
+  rte *old_best = rte_find_old_best(net, new, old);
+
+  if (new_best == old_best)
+    return;
+
+  if (new_best)
+    new_best = kbr_export_fdb(p, c, new_best, &rt_free);
+
+  new_gw = (new_best && ipa_nonzero(new_best->attrs->nh.gw)) ? new_best : NULL;
+  old_gw = (old_best && ipa_nonzero(old_best->attrs->nh.gw)) ? old_best : NULL;
+
+  /* For regular routes, we have one bridge entry, perhaps also one tunnel entry */
+  kbr_trace_out(p, n, new_best, old_best, 0);
+  kbr_replace_fdb(p, n, new_best, old_best, 0);
+
+  kbr_trace_out(p, n, new_gw, old_gw, 1);
+  kbr_replace_fdb(p, n, new_gw, old_gw, 1);
+
+  if (rt_free)
+    rte_free(rt_free);
+}
+
+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;
+
+  if (p->ready)
+    tm_start(p->scan_timer, 0);
+}
+
+static void
+kbr_feed_end(struct channel *C)
+{
+  struct kbr_proto *p = (void *) C->proto;
+
+  p->ready = true;
+  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);
+}
+
+
+/*
+ *     Bridge VLANs
+ */
+
+#define VLAN_KEY(v)            v->ifi, v->vid
+#define VLAN_NEXT(v)           v->next
+#define VLAN_EQ(i1,v1,i2,v2)   i1 == i2 && v1 == v2
+#define VLAN_FN(i,v)           hash_value(u32_hash0(v, HASH_PARAM, u32_hash0(i, HASH_PARAM, 0)))
+
+#define VLAN_REHASH            bridge_vlan_rehash
+#define VLAN_PARAMS            /8, *2, 2, 2, 4, 16
+
+
+#define VLAN_VNI_KEY(v)                v->ifi, v->vni
+#define VLAN_VNI_NEXT(v)       v->next_vni
+#define VLAN_VNI_EQ(i1,v1,i2,v2) i1 == i2 && v1 == v2
+#define VLAN_VNI_FN(i,v)       hash_value(u32_hash0(v, HASH_PARAM, u32_hash0(i, HASH_PARAM, 0)))
+
+#define VLAN_VNI_REHASH                bridge_vlan_vni_rehash
+#define VLAN_VNI_PARAMS                /8, *2, 2, 2, 4, 16
+
+
+HASH_DEFINE_REHASH_FN(VLAN, struct kbr_vlan)
+HASH_DEFINE_REHASH_FN(VLAN_VNI, struct kbr_vlan)
+
+
+static struct kbr_vlan *
+kbr_find_vlan(struct kbr_proto *p, uint ifi, uint vid)
+{
+  return HASH_FIND(p->vlan_hash, VLAN, ifi, vid);
+}
+
+struct kbr_vlan *
+kbr_find_vlan_by_vni(struct kbr_proto *p, uint ifi, uint vni)
+{
+  if (!p->vlan_vni_hash.data)
+    return NULL;
+
+  return HASH_FIND(p->vlan_vni_hash, VLAN_VNI, ifi, vni);
+}
+
+static void
+kbr_vlan_set_vni(struct kbr_proto *p, struct kbr_vlan *v, uint vni)
+{
+  if (v->vni_link && (v->vni == vni))
+    return;
+
+  struct kbr_vlan *old = HASH_DELETE(p->vlan_vni_hash, VLAN_VNI, v->ifi, v->vni);
+  if (old)
+    old->vni_link = false;
+
+  if (v->vni_link)
+    HASH_REMOVE(p->vlan_vni_hash, VLAN_VNI, v);
+
+  v->vni = vni;
+  v->vni_link = true;
+
+  HASH_INSERT2(p->vlan_vni_hash, VLAN_VNI, p->p.pool, v);
+}
+
+static void
+kbr_vlan_unset_vni(struct kbr_proto *p, struct kbr_vlan *v)
+{
+  if (v->vni_link)
+    HASH_REMOVE(p->vlan_vni_hash, VLAN_VNI, v);
+
+  v->vni = 0;
+  v->vni_link = false;
+}
+
+static struct kbr_vlan *
+kbr_get_vlan(struct kbr_proto *p, uint ifi, uint vid)
+{
+  struct kbr_vlan *v = kbr_find_vlan(p, ifi, vid);
+
+  if (v)
+    return v;
+
+  v = mb_allocz(p->p.pool, sizeof(struct kbr_vlan));
+
+  v->ifi = ifi;
+  v->vid = vid;
+
+  HASH_INSERT2(p->vlan_hash, VLAN, p->p.pool, v);
+
+  return v;
+}
+
+void
+kbr_got_vlan(struct kbr_proto *p, struct iface *i, uint vid, uint flags)
+{
+  struct kbr_vlan *v = kbr_get_vlan(p, i->index, vid);
+
+  v->flags = flags;
+  v->mark_vlan = true;
+}
+
+void
+kbr_got_vlan_tunnel(struct kbr_proto *p, struct iface *i, uint vid, uint vni, uint flags UNUSED)
+{
+  struct kbr_vlan *v = kbr_get_vlan(p, i->index, vid);
+
+  v->mark_tunnel = true;
+  kbr_vlan_set_vni(p, v, vni);
+}
+
+static inline void
+kbr_vlan_changed(struct kbr_proto *p)
+{
+  if (p->ready)
+    tm_start(p->scan_timer, 100 MS);
+}
+
+static void
+kbr_vlan_req_update(struct kbr_proto *p, struct iface *i, uintptr_t owner, uint vid, uint vni)
+{
+  struct kbr_vlan *v = kbr_get_vlan(p, i->index, vid);
+
+  if ((v->owner == owner) && (v->vni_req == vni))
+    return;
+
+  v->vni_req = vni;
+  v->owner = owner;
+  kbr_vlan_changed(p);
+
+  /* Register the interface as VLAN-managed */
+  kbr_get_vlan(p, i->index, 0);
+}
+
+static void
+kbr_vlan_req_withdraw(struct kbr_proto *p, struct iface *i, uintptr_t owner, uint vid)
+{
+  struct kbr_vlan *v = kbr_find_vlan(p, i->index, vid);
+
+  if (!v || (v->owner != owner))
+    return;
+
+  v->vni_req = 0;
+  v->owner = 0;
+  kbr_vlan_changed(p);
+}
+
+static void
+kbr_vlan_req_withdraw_all(struct kbr_proto *p, uintptr_t owner)
+{
+  bool changed = false;
+
+  HASH_WALK(p->vlan_hash, next, v)
+  {
+    if (v->owner != owner)
+      continue;
+
+    v->vni_req = 0;
+    v->owner = 0;
+    changed = true;
+  }
+  HASH_WALK_END;
+
+  if (changed)
+    kbr_vlan_changed(p);
+}
+
+static void
+kbr_vlan_req_notify(ps_subscriber *sub, void *msg, uint len)
+{
+  ASSERT(len >= sizeof(struct vlan_request));
+
+  struct kbr_proto *p = sub->data;
+  struct vlan_request *req = msg;
+
+  TRACE(D_EVENTS, "VLAN %s received for %d VLANs on %s",
+       (req->update ? "request" : "withdraw"), (int) req->vlan_count, req->iface->name);
+
+  if (req->update)
+  {
+    for (int i = 0; i < req->vlan_count; i++)
+      kbr_vlan_req_update(p, req->iface, req->owner, req->vlans[i].vid, req->vlans[i].vni);
+  }
+  else if (req->vlan_count > 0)
+  {
+    for (int i = 0; i < req->vlan_count; i++)
+      kbr_vlan_req_withdraw(p, req->iface, req->owner, req->vlans[i].vid);
+  }
+  else
+    kbr_vlan_req_withdraw_all(p, req->owner);
+}
+
+static void
+kbr_prune_vlans0(struct kbr_proto *p)
+{
+  if (!p->vlan_hash.data)
+    return;
+
+  HASH_WALK(p->vlan_hash, next, v)
+  {
+    if (v->owner && (!v->mark_vlan || !v->mark_tunnel || (v->vni_req != v->vni)))
+    {
+      struct iface *ifa = if_find_by_index(v->ifi);
+      if (!ifa) continue;
+
+      TRACE(D_EVENTS, "%s VLAN %u VNI %u on %s",
+           (!v->mark_vlan ? "Adding" : "Updating"), v->vid, v->vni, ifa->name);
+      kbr_update_vlan(ifa, v->vid, true, v->mark_vlan, true, v->mark_tunnel, v->vni_req, v->vni);
+      kbr_vlan_set_vni(p, v, v->vni_req);
+    }
+
+    v->active = !!v->owner;
+  }
+  HASH_WALK_END;
+}
+
+static void
+kbr_prune_vlans1(struct kbr_proto *p)
+{
+  if (!p->vlan_hash.data)
+    return;
+
+  HASH_WALK(p->vlan_hash, next, v)
+  {
+
+    if (!v->owner && (v->mark_vlan || v->mark_tunnel) && kbr_find_vlan(p, v->ifi, 0))
+    {
+      struct iface *ifa = if_find_by_index(v->ifi);
+      if (!ifa) continue;
+
+      TRACE(D_EVENTS, "Removing VLAN %u on %s", v->vid, ifa->name);
+      kbr_update_vlan(ifa, v->vid, false, v->mark_vlan, false, v->mark_tunnel, v->vni_req, v->vni);
+      kbr_vlan_unset_vni(p, v);
+    }
+
+    v->mark_vlan = false;
+    v->mark_tunnel = false;
+  }
+  HASH_WALK_END;
+}
+
+
+/*
+ *     Bridge protocol glue
+ */
+
+static bool
+kbr_check_iface(struct kbr_proto *p, const struct iface *i)
+{
+  ea_list *attrs = i->attrs ? i->attrs->eattrs : NULL;
+
+  if (!(i->flags & IF_UP))
+  {
+    log(L_ERR "%s: Interface %s is down", p->p.name, i->name);
+    return false;
+  }
+
+  u32 if_type = ea_get_int(attrs, EA_IFACE_TYPE, IF_TYPE_UNDEF);
+  if (if_type != IF_TYPE_BRIDGE)
+  {
+    log(L_ERR "%s: Interface %s is not a bridge", p->p.name, i->name);
+    return false;
+  }
+
+  u32 if_vlan_filtering = ea_get_int(attrs, EA_IFACE_BRIDGE_VLAN_FILTERING, U32_UNDEF);
+  if (p->vlan_filtering && !if_vlan_filtering)
+  {
+    log(L_WARN "%s: Mismatch in VLAN filtering", p->p.name, i->name);
+    return false;
+  }
+
+  return true;
+}
+
+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->feed_end = kbr_feed_end;
+  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;
+
+  memset(&p->vlan_hash, 0, sizeof(p->vlan_hash));
+  p->vlan_sub = NULL;
+
+  p->scan_timer = tm_new_init(p->p.pool, kbr_scan, p, cf->scan_time, 0);
+
+  bmap_init(&p->sync_map[0], p->p.pool, 1024);
+  bmap_init(&p->sync_map[1], p->p.pool, 1024);
+  bmap_init(&p->seen_map[0], p->p.pool, 1024);
+  bmap_init(&p->seen_map[1], p->p.pool, 1024);
+
+  if (p->vlan_filtering)
+  {
+    HASH_INIT(p->vlan_hash, p->p.pool, 4);
+    HASH_INIT(p->vlan_vni_hash, p->p.pool, 4);
+
+    p->vlan_sub = ps_subscriber_new(p->p.pool, kbr_vlan_req_notify, p);
+    ps_subscribe_topic(p->vlan_sub, &vlan_requests, p->bridge_dev->name);
+  }
+
+  kbr_check_iface(p, p->bridge_dev);
+
+  kbr_sys_start(p);
+
+  return PS_UP;
+}
+
+static int
+kbr_shutdown(struct proto *P UNUSED)
+{
+  struct kbr_proto *p = (void *) P;
+
+  if (p->synced)
+    kbr_flush_routes(p);
+
+  p->ready = false;
+  p->synced = false;
+
+  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..bed5702
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023--2026 Ondrej Zajicek <santiago@crfreenet.org>
+ *     (c) 2023--2026 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;
+  struct bmap sync_map[2];     /* Bridge/VXLAN FDB entries successfully written to kernel */
+  struct bmap seen_map[2];     /* Bridge/VXLAN FDB entries seen during last periodic scan */
+
+  ps_subscriber *vlan_sub;
+  int vlan_filtering;
+  bool ready;                  /* Initial feed has been finished */
+  bool synced;                 /* First scan has been finished */
+
+  struct kbr_proto *hash_next;
+
+  HASH(struct kbr_vlan) vlan_hash;
+  HASH(struct kbr_vlan) vlan_vni_hash;
+};
+
+struct kbr_vlan {
+  u32 ifi;
+  u32 vid;
+  u32 vni;
+  u32 vni_req;
+  u16 flags;
+  bool active;
+  bool vni_link;
+  bool mark_vlan;
+  bool mark_tunnel;
+  uintptr_t owner;
+  struct kbr_vlan *next;
+  struct kbr_vlan *next_vni;
+};
+
+void kbr_got_fdb(struct kbr_proto *p, const net_addr *n, rte *e, int src, bool scan, bool tunnel);
+void kbr_got_vlan(struct kbr_proto *p, struct iface *i, uint vid, uint flags);
+void kbr_got_vlan_tunnel(struct kbr_proto *p, struct iface *i, uint vid, uint vni, uint flags);
+struct kbr_vlan * kbr_find_vlan_by_vni(struct kbr_proto *p, uint ifi, uint vni);
+
+
+/* krt sysdep */
+
+int kbr_sys_start(struct kbr_proto *p);
+void kbr_sys_shutdown(struct kbr_proto *p);
+
+void kbr_replace_fdb(struct kbr_proto *p, const net_addr *n, rte *new, rte *old, int tunnel);
+void kbr_update_fdb(struct kbr_proto *p, const net_addr *n, rte *new, rte *old, int tunnel);
+void kbr_do_fdb_scan(struct kbr_proto *p);
+
+void kbr_update_vlan(struct iface *i, uint vid, bool new, bool old, bool new_tunnel, bool old_tunnel, uint new_vni, uint old_vni);
+void kbr_do_vlan_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..99f92a6
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023--2026 Ondrej Zajicek <santiago@crfreenet.org>
+ *     (c) 2023--2026 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_ENUM(T_ENUM_KBR_SOURCE, KBR_SRC_, BIRD, LOCAL, STATIC, DYNAMIC)
+
+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_ENUM_KBR_SOURCE, EA_KBR_SOURCE); } ;
+
+CF_CODE
+
+CF_END
index d5e9e31cc98f297956bd41d751a1e4d677f026e1..bb262016077c5716651f4876a262522ce5c89816 100644 (file)
@@ -113,6 +113,31 @@ enum {
 };
 
 
+/* Missing definitions from <linux/if_bridge.h> */
+
+struct bridge_vlan_info {
+  u16 flags;
+  u16 vid;
+};
+
+enum {
+  IFLA_BRIDGE_FLAGS,
+  IFLA_BRIDGE_MODE,
+  IFLA_BRIDGE_VLAN_INFO,
+  IFLA_BRIDGE_VLAN_TUNNEL_INFO,
+  IFLA_BRIDGE_MRP,
+  IFLA_BRIDGE_CFM,
+  IFLA_BRIDGE_MST,
+};
+
+enum {
+  IFLA_BRIDGE_VLAN_TUNNEL_UNSPEC,
+  IFLA_BRIDGE_VLAN_TUNNEL_ID,
+  IFLA_BRIDGE_VLAN_TUNNEL_VID,
+  IFLA_BRIDGE_VLAN_TUNNEL_FLAGS,
+};
+
+
 static inline int
 netlink_error_to_os(int error)
 {
index 4c99307b554c7fae979c1883ea7f657f86e66eba..e69f6df2cc785b47ca6b19c2079815df9abbd9bb 100644 (file)
@@ -13,6 +13,7 @@
 #include <linux/if.h>
 #include <linux/netlink.h>
 #include <linux/rtnetlink.h>
+#include <linux/if_bridge.h>
 
 #ifdef HAVE_MPLS_KERNEL
 #include <linux/lwtunnel.h>
index 014ecf3f8184a6d631e5365f14e6296de026972f..00b21fc97f307bda66a70b6ec1a20a340efe1429 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;
 };
 
 /*
@@ -145,19 +148,29 @@ nl_send(struct nl_sock *nl, struct nlmsghdr *nh)
 }
 
 static void
-nl_request_dump_link(void)
+nl_request_dump_link(int af, u32 ext_mask)
 {
   struct {
     struct nlmsghdr nh;
     struct ifinfomsg ifi;
+    struct rtattr rta;
+    u32 ext_mask;
   } req = {
     .nh.nlmsg_type = RTM_GETLINK,
     .nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
     .nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,
     .nh.nlmsg_seq = ++(nl_scan.seq),
-    .ifi.ifi_family = AF_UNSPEC,
+    .ifi.ifi_family = af,
   };
 
+  if (ext_mask)
+  {
+    req.rta.rta_type = IFLA_EXT_MASK;
+    req.rta.rta_len = RTA_LENGTH(4);
+    req.ext_mask = ext_mask;
+    req.nh.nlmsg_len = NLMSG_ALIGN(req.nh.nlmsg_len) + req.rta.rta_len;
+  }
+
   send(nl_scan.fd, &req, sizeof(req), 0);
   nl_scan.last_hdr = NULL;
 }
@@ -210,6 +223,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)
@@ -333,7 +374,7 @@ struct nl_want_attrs {
 };
 
 
-#define BIRD_IFLA_MAX (IFLA_LINKINFO+1)
+#define BIRD_IFLA_MAX (IFLA_AF_SPEC+1)
 
 static struct nl_want_attrs ifla_attr_want[BIRD_IFLA_MAX] = {
   [IFLA_IFNAME]          = { 1, 0, 0 },
@@ -341,6 +382,7 @@ static struct nl_want_attrs ifla_attr_want[BIRD_IFLA_MAX] = {
   [IFLA_MASTER]          = { 1, 1, sizeof(u32) },
   [IFLA_WIRELESS] = { 1, 0, 0 },
   [IFLA_LINKINFO] = { 1, 0, 0 },
+  [IFLA_AF_SPEC]  = { 1, 0, 0 },
 };
 
 #define BIRD_INFO_MAX (IFLA_INFO_DATA+1)
@@ -366,6 +408,22 @@ static struct nl_want_attrs ifla_vxlan_attr_want[BIRD_IFLA_VXLAN_MAX] = {
 };
 
 
+#define BIRD_IFBRIDGE_MAX (IFLA_BRIDGE_VLAN_TUNNEL_INFO+1)
+
+static struct nl_want_attrs ifbridge_attr_want[BIRD_IFBRIDGE_MAX] UNUSED = {
+  [IFLA_BRIDGE_VLAN_INFO] = { 1, 1, sizeof(struct bridge_vlan_info) },
+  [IFLA_BRIDGE_VLAN_TUNNEL_INFO] = { 1, 0, 0 },
+};
+
+#define BIRD_IFBRIDGE_VLAN_TUNNEL_MAX (IFLA_BRIDGE_VLAN_TUNNEL_FLAGS+1)
+
+static struct nl_want_attrs ifbridge_vlan_tunnel_attr_want[BIRD_IFBRIDGE_VLAN_TUNNEL_MAX] UNUSED = {
+  [IFLA_BRIDGE_VLAN_TUNNEL_ID]         = { 1, 1, sizeof(u32) },
+  [IFLA_BRIDGE_VLAN_TUNNEL_VID]                = { 1, 1, sizeof(u16) },
+  [IFLA_BRIDGE_VLAN_TUNNEL_FLAGS]      = { 1, 1, sizeof(u16) },
+};
+
+
 #define BIRD_IFA_MAX  (IFA_FLAGS+1)
 
 static struct nl_want_attrs ifa_attr_want4[BIRD_IFA_MAX] = {
@@ -459,6 +517,18 @@ static struct nl_want_attrs rtm_attr_want_mpls[BIRD_RTA_MAX] = {
 #endif
 
 
+#define BIRD_NDA_MAX  (NDA_SRC_VNI+1)
+
+static struct nl_want_attrs ndm_attr_want[BIRD_NDA_MAX] = {
+  [NDA_DST]      = { 1, 0, 0 },
+  [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) },
+  [NDA_SRC_VNI]          = { 1, 1, sizeof(u32) },
+};
+
+
 static int
 nl_parse_attrs(struct rtattr *a, struct nl_want_attrs *want, struct rtattr **k, int ksize)
 {
@@ -510,10 +580,15 @@ static inline ip_addr rta_get_ipa(struct rtattr *a)
 {
   if (RTA_PAYLOAD(a) == sizeof(ip4_addr))
     return ipa_from_ip4(rta_get_ip4(a));
-  else
+  else if (RTA_PAYLOAD(a) == sizeof(ip6_addr))
     return ipa_from_ip6(rta_get_ip6(a));
+  else
+    return IPA_NONE;
 }
 
+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 +1356,7 @@ kif_do_scan(struct kif_proto *p UNUSED)
 
   if_start_update();
 
-  nl_request_dump_link();
+  nl_request_dump_link(AF_UNSPEC, 0);
   while (h = nl_get_scan())
     if (h->nlmsg_type == RTM_NEWLINK || h->nlmsg_type == RTM_DELLINK)
       nl_parse_link(h, 1);
@@ -1325,6 +1400,7 @@ kif_do_scan(struct kif_proto *p UNUSED)
   if_end_update();
 }
 
+
 /*
  *     Routes
  */
@@ -1343,7 +1419,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)
 
@@ -2001,6 +2077,445 @@ krt_do_scan(struct krt_proto *p)
   }
 }
 
+
+#ifdef CONFIG_BRIDGE
+
+/*
+ *     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]);
+
+    eattr *ea = ea_find(e->attrs->eattrs, EA_MPLS_LABEL);
+    if (ea)
+      nl_add_attr_u32(&r->h, rsize, NDA_SRC_VNI, ea->u.data);
+
+    r->n.ndm_state |= NUD_PERMANENT;
+  }
+
+  /* Ignore missing for DELETE */
+  return nl_exchange(&r->h, (op == NL_OP_DELETE));
+}
+
+void
+kbr_replace_fdb(struct kbr_proto *p, 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 (new)
+  {
+    if (err < 0)
+      bmap_clear(&p->sync_map[tunnel], new->id);
+    else
+      bmap_set(&p->sync_map[tunnel], new->id);
+  }
+
+  if (err < 0)
+    log(L_WARN "NL error %m");
+}
+
+void
+kbr_update_fdb(struct kbr_proto *p, 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)
+      bmap_clear(&p->sync_map[tunnel], new->id);
+    else
+      bmap_set(&p->sync_map[tunnel], new->id);
+  }
+
+  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])
+    return;
+
+  mac_addr mac = rta_get_mac(a[NDA_LLADDR]);
+  uint vid = a[NDA_VLAN] ? rta_get_u16(a[NDA_VLAN]) : 0;
+
+  net_addr n;
+  net_fill_eth(&n, mac, vid);
+
+  /* Ignore multicast entries */
+  if (!mac_is_unicast(mac))
+    return;
+
+  struct iface *oif = if_find_by_index(nd->ndm_ifindex);
+  if (!oif)
+    return;
+
+  /* NDA_MASTER is not present on 'self' entries */
+  u32 bridge_id = oif->master_index;
+
+  /* Should be filtered by kernel */
+  if (s->bridge_id && (bridge_id != s->bridge_id))
+    return;
+
+  /* When NDA_MASTER is reported, it should be the same */
+  if (a[NDA_MASTER] && (bridge_id != rta_get_u32(a[NDA_MASTER])))
+    return;
+
+  /* Do we know this bridge? */
+  struct kbr_proto *p = HASH_FIND(nl_bridge_map, BRH, bridge_id);
+  if (!p)
+    return;
+
+  rta ra = {
+    .source = RTS_BRIDGE,
+    .scope = SCOPE_UNIVERSE,
+    .dest = RTD_UNICAST,
+    .nh.iface = oif,
+  };
+
+  bool tunnel = false;
+
+  /* FDB entries from port devices */
+  if (nd->ndm_flags & NTF_SELF)
+  {
+    /* FDB entries from VXLAN devices */
+    u32 type = ea_get_int((oif->attrs ? oif->attrs->eattrs : NULL), EA_IFACE_TYPE, IF_TYPE_UNDEF);
+    if (type != IF_TYPE_VXLAN)
+      return;
+
+    if (a[NDA_DST])
+      ra.nh.gw = rta_get_ipa(a[NDA_DST]);
+
+    if (a[NDA_VNI])
+    {
+      ra.nh.labels = 1;
+      ra.nh.label[0] = rta_get_u32(a[NDA_VNI]);
+    }
+
+    if (a[NDA_SRC_VNI])
+    {
+      struct kbr_vlan *vlan = kbr_find_vlan_by_vni(p, oif->index, rta_get_u32(a[NDA_SRC_VNI]));
+      if (!vlan)
+      {
+       log(L_TRACE "%s: FDB %N unkown vni %u", p->p.name, &n, rta_get_u32(a[NDA_SRC_VNI]));
+       return;
+      }
+
+      ((net_addr_eth *) &n)->vid = vid = vlan->vid;
+    }
+
+    tunnel = true;
+  }
+
+  /* Accept VLAN-tagged entries when vlan filtering is enabled */
+  if (!vid != !p->vlan_filtering)
+    return;
+
+  int src;
+  if (nd->ndm_flags & NTF_EXT_LEARNED)
+    src = KBR_SRC_BIRD;
+  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] %d %d %d\n", (new ? "add" : "del"), &n, oif->name, nd->ndm_state, nd->ndm_flags, nd->ndm_type, (int) src, (int) s->scan, (int) tunnel);
+  kbr_got_fdb(p, &n, e, src, s->scan, tunnel);
+}
+
+void
+kbr_do_fdb_scan(struct kbr_proto *p)
+{
+  struct nl_parse_state s = {
+    .pool = nl_linpool,
+    .scan = 1,
+    .bridge_id = kbr_bridge_id(p),
+  };
+
+  struct nlmsghdr *h;
+
+  nl_request_dump_neigh(AF_BRIDGE, kbr_bridge_id(p));
+  while (h = nl_get_scan())
+  {
+    if (h->nlmsg_type == RTM_NEWNEIGH || h->nlmsg_type == RTM_DELNEIGH)
+      nl_parse_fdb(&s, h);
+  }
+}
+
+
+/*
+ *     VLANs
+ */
+
+static int
+nl_send_vlan(struct iface *i, uint vid, uint tid, uint flags, int op, int tunnel)
+{
+  struct {
+    struct nlmsghdr h;
+    struct ifinfomsg i;
+    char buf[0];
+  } *r;
+
+  int rsize = sizeof(*r) + 256;
+  r = alloca(rsize);
+
+  memset(&r->h, 0, sizeof(r->h));
+  memset(&r->i, 0, sizeof(r->i));
+  r->h.nlmsg_type = op ? RTM_SETLINK : RTM_DELLINK;
+  r->h.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
+  r->h.nlmsg_flags = op | NLM_F_REQUEST | NLM_F_ACK;
+
+  r->i.ifi_family = AF_BRIDGE;
+  r->i.ifi_index = i->index;
+
+  struct rtattr *afspec = nl_open_attr(&r->h, rsize, IFLA_AF_SPEC);
+
+  if (!tunnel)
+  {
+    struct bridge_vlan_info vinfo = { .flags = flags, .vid = vid, };
+    nl_add_attr(&r->h, rsize, IFLA_BRIDGE_VLAN_INFO, &vinfo, sizeof(vinfo));
+  }
+  else
+  {
+    struct rtattr *tinfo = nl_open_attr(&r->h, rsize, IFLA_BRIDGE_VLAN_TUNNEL_INFO);
+    nl_add_attr_u32(&r->h, rsize, IFLA_BRIDGE_VLAN_TUNNEL_ID, tid);
+    nl_add_attr_u16(&r->h, rsize, IFLA_BRIDGE_VLAN_TUNNEL_VID, vid);
+    nl_add_attr_u16(&r->h, rsize, IFLA_BRIDGE_VLAN_TUNNEL_FLAGS, flags);
+    nl_close_attr(&r->h, tinfo);
+  }
+
+  nl_close_attr(&r->h, afspec);
+
+  /* Ignore missing for DELETE */
+  return nl_exchange(&r->h, (op == NL_OP_DELETE));
+}
+
+void
+kbr_update_vlan(struct iface *i, uint vid, bool new, bool old, bool new_tunnel, bool old_tunnel, uint new_vni, uint old_vni)
+{
+  int err = 0;
+
+  if ((old && new) && (old_tunnel == new_tunnel) && (old_vni == new_vni))
+    return;
+
+  if (old_tunnel)
+    nl_send_vlan(i, vid, old_vni, 0, NL_OP_DELETE, 1);
+
+  if (old && !new)
+    nl_send_vlan(i, vid, 0, 0, NL_OP_DELETE, 0);
+
+  if (new && !old)
+    err = nl_send_vlan(i, vid, 0, 0, NL_OP_ADD, 0);
+
+  if (new_tunnel && !err)
+    err = nl_send_vlan(i, vid, new_vni, 0, NL_OP_ADD, 1);
+
+  if (err < 0)
+    log(L_WARN "NL error %m");
+}
+
+
+static void
+nl_parse_vlan(struct nl_parse_state *s, struct nlmsghdr *h)
+{
+  struct ifinfomsg *i;
+  struct rtattr *a[BIRD_IFLA_MAX];
+  // int new = (h->nlmsg_type == RTM_NEWLINK);
+
+  if (!(i = nl_checkin(h, sizeof(*i))))
+    return;
+
+  struct iface *oif = if_find_by_index(i->ifi_index);
+  if (!oif)
+    return;
+
+  if (i->ifi_family != AF_BRIDGE)
+    return;
+
+  if (!nl_parse_attrs(IFLA_RTA(i), ifla_attr_want, a, sizeof(a)))
+    return;
+
+  if (!a[IFLA_AF_SPEC] || !a[IFLA_MASTER])
+    return;
+
+  u32 bridge_id = rta_get_u32(a[IFLA_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 VLANs when vlan filtering is enabled */
+  if (!p->vlan_filtering)
+    return;
+
+  struct rtattr *list = a[IFLA_AF_SPEC];
+  uint rem = RTA_PAYLOAD(list);
+
+  /* We have to iterate over rtattrs to handle multiple instances of VLAN info attributes */
+  for (struct rtattr *vi = RTA_DATA(list); RTA_OK(vi, rem); vi = RTA_NEXT(vi, rem))
+  {
+    if (vi->rta_type == IFLA_BRIDGE_VLAN_INFO)
+    {
+      if (RTA_PAYLOAD(vi) != sizeof(struct bridge_vlan_info))
+      {
+       log(L_ERR "nl_parse_attrs: Malformed attribute received");
+       continue;
+      }
+
+      struct bridge_vlan_info *info = RTA_DATA(vi);
+
+      DBG("VLAN %u dev %s flags %x\n", info->vid, oif->name, info->flags);
+      kbr_got_vlan(p, oif, info->vid, info->flags);
+    }
+
+    if (vi->rta_type == IFLA_BRIDGE_VLAN_TUNNEL_INFO)
+    {
+      struct rtattr *a0[BIRD_IFBRIDGE_VLAN_TUNNEL_MAX];
+
+      nl_attr_len = RTA_PAYLOAD(vi);
+      if (!nl_parse_attrs(RTA_DATA(vi), ifbridge_vlan_tunnel_attr_want, a0, sizeof(a0)))
+       continue;
+
+      uint tid = a0[IFLA_BRIDGE_VLAN_TUNNEL_ID] ? rta_get_u32(a0[IFLA_BRIDGE_VLAN_TUNNEL_ID]) : 0;
+      uint vid = a0[IFLA_BRIDGE_VLAN_TUNNEL_VID] ? rta_get_u16(a0[IFLA_BRIDGE_VLAN_TUNNEL_VID]) : 0;
+      uint flags = a0[IFLA_BRIDGE_VLAN_TUNNEL_FLAGS] ? rta_get_u16(a0[IFLA_BRIDGE_VLAN_TUNNEL_FLAGS]) : 0;
+
+      DBG("VLANtun %u dev %s tunnel-id %u flags %x\n", vid, oif->name, tid, flags);
+      kbr_got_vlan_tunnel(p, oif, vid, tid, flags);
+    }
+  }
+}
+
+void
+kbr_do_vlan_scan(struct kbr_proto *p)
+{
+  struct nl_parse_state s = {
+    .pool = nl_linpool,
+    .scan = 1,
+    .bridge_id = kbr_bridge_id(p),
+  };
+
+  struct nlmsghdr *h;
+
+  nl_request_dump_link(AF_BRIDGE, RTEXT_FILTER_BRVLAN);
+  while (h = nl_get_scan())
+  {
+    if (h->nlmsg_type == RTM_NEWLINK || h->nlmsg_type == RTM_DELLINK)
+      nl_parse_vlan(&s, h);
+  }
+}
+
+#else /* CONFIG_BRIDGE */
+
+static inline void nl_parse_fdb(struct nl_parse_state *s UNUSED, struct nlmsghdr *h UNUSED) { }
+
+#endif /* CONFIG_BRIDGE */
+
+
 /*
  *     Asynchronous Netlink interface
  */
@@ -2026,18 +2541,27 @@ 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:
+      DBG("KRT: Received async neighbor notification (%d)\n", h->nlmsg_type);
+      nl_parse_fdb(&s, h);
+      break;
+
     default:
       DBG("KRT: Received unknown async notification (%d)\n", h->nlmsg_type);
     }
@@ -2124,7 +2648,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;
 
@@ -2181,7 +2705,11 @@ 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);
+
+#ifdef CONFIG_BRIDGE
+  HASH_INIT(nl_bridge_map, krt_pool, 4);
+#endif
 }
 
 int
@@ -2285,6 +2813,36 @@ krt_sys_get_attr(const eattr *a, byte *buf, int buflen UNUSED)
 }
 
 
+#ifdef CONFIG_BRIDGE
+
+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);
+}
+
+#endif /* CONFIG_BRIDGE */
+
 
 void
 kif_sys_start(struct kif_proto *p UNUSED)